From e5ff56c6aecaed0702ba09edaea4a8d9dbed9065 Mon Sep 17 00:00:00 2001 From: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Date: Fri, 15 Oct 2021 21:50:23 -0400 Subject: [PATCH] Parity (#806) * Refactored code, added parity * Got call price * Finalized feature * added documentation * Added changes except number limit * Added limit to number of data points * Updated duplicated variable names * Added docstrings, updated filtering Co-authored-by: Colin Delahunty Co-authored-by: Colin Delahunty --- gamestonk_terminal/helper_funcs.py | 21 +++++ .../options/options_controller.py | 62 ++++++++++++++ gamestonk_terminal/options/yfinance_view.py | 85 ++++++++++++++++++- .../optimizer_helper.py | 14 --- .../portfolio_optimization/optimizer_view.py | 2 +- .../portfolio_optimization/po_controller.py | 2 +- .../stocks/fundamental_analysis/dcf_model.py | 11 --- .../stocks/fundamental_analysis/dcf_view.py | 4 +- website/content/options/parity/_index.md | 14 +++ website/data/menu/main.yml | 6 +- 10 files changed, 190 insertions(+), 31 deletions(-) create mode 100644 website/content/options/parity/_index.md diff --git a/gamestonk_terminal/helper_funcs.py b/gamestonk_terminal/helper_funcs.py index d0fc6c31c6e..c74b54d78fd 100644 --- a/gamestonk_terminal/helper_funcs.py +++ b/gamestonk_terminal/helper_funcs.py @@ -17,6 +17,7 @@ from colorama import Fore, Style from pandas._config.config import get_option from pandas.plotting import register_matplotlib_converters import pandas.io.formats.format +import requests from screeninfo import get_monitors from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal import config_plot as cfgPlot @@ -711,3 +712,23 @@ def export_data( print("Wrong export file specified.\n") print(f"Saved file: {saved_path}\n") + + +def get_rf() -> float: + """ + Uses the fiscaldata.gov API to get most recent T-Bill rate + + Returns + ------- + float + The current US T-Bill rate + """ + try: + base = "https://api.fiscaldata.treasury.gov/services/api/fiscal_service" + end = "/v2/accounting/od/avg_interest_rates" + filters = "?filter=security_desc:eq:Treasury Bills&sort=-record_date" + response = requests.get(base + end + filters) + latest = response.json()["data"][0] + return round(float(latest["avg_interest_rate_amt"]) / 100, 8) + except Exception: + return 0.02 diff --git a/gamestonk_terminal/options/options_controller.py b/gamestonk_terminal/options/options_controller.py index b248aeb188e..6f05f86dffb 100644 --- a/gamestonk_terminal/options/options_controller.py +++ b/gamestonk_terminal/options/options_controller.py @@ -62,6 +62,7 @@ class OptionsController: "unu", "stocks", "payoff", + "parity", "smile", ] @@ -138,6 +139,7 @@ Current Expiry: {self.selected_date or None} hist plot option history [Tradier] grhist plot option greek history [Syncretism.io] smile plot the volatility smile for the expiration date [Yfinance] + parity shows whether options are above or below expected price [Yfinance] > payoff shows payoff diagram for a selection of options [Yfinance] {Style.RESET_ALL if not colored else ''}""" print(help_text) @@ -1076,6 +1078,66 @@ Current Expiry: {self.selected_date or None} """Process stocks command""" return stocks_controller.menu(self.ticker) + def call_parity(self, other_args: List[str]): + """Process parity command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="parity", + description="Shows whether options are over or under valued", + ) + + parser.add_argument( + "-p", + "--put", + action="store_true", + default=False, + dest="put", + help="Shows puts instead of calls", + ) + parser.add_argument( + "-a", + "--ask", + action="store_true", + default=False, + dest="ask", + help="Use ask price instead of lastPrice", + ) + parser.add_argument( + "-m", + "--min", + type=float, + default=None, + dest="mini", + help="Minimum strike price shown", + ) + parser.add_argument( + "-M", + "--max", + type=float, + default=None, + dest="maxi", + help="Maximum strike price shown", + ) + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + if not self.ticker and not self.selected_date: + print("Ticker and expiration required. \n") + return + yfinance_view.show_parity( + self.ticker, + self.selected_date, + ns_parser.put, + ns_parser.ask, + ns_parser.mini, + ns_parser.maxi, + ) + print("") + except Exception as e: + print(e, "\n") + def menu(ticker: str = ""): """Options Menu.""" diff --git a/gamestonk_terminal/options/yfinance_view.py b/gamestonk_terminal/options/yfinance_view.py index 75edab9bd8d..9f507fdd943 100644 --- a/gamestonk_terminal/options/yfinance_view.py +++ b/gamestonk_terminal/options/yfinance_view.py @@ -4,18 +4,25 @@ __docformat__ = "numpy" import os from bisect import bisect_left from typing import List, Dict, Any +from datetime import datetime, date import matplotlib.pyplot as plt import numpy as np import pandas as pd import seaborn as sns import yfinance as yf +from tabulate import tabulate import gamestonk_terminal.config_plot as cfp import gamestonk_terminal.feature_flags as gtff from gamestonk_terminal.helper_funcs import export_data, plot_autoscale from gamestonk_terminal.options import op_helpers, yfinance_model -from gamestonk_terminal.options.yfinance_model import generate_data +from gamestonk_terminal.options.yfinance_model import ( + generate_data, + get_option_chain, + get_price, +) +from gamestonk_terminal.helper_funcs import get_rf def plot_oi( @@ -448,3 +455,79 @@ def plot_payoff( plt.legend() plt.show() print("") + + +def show_parity( + ticker: str, exp: str, put: bool, ask: bool, mini: float, maxi: float +) -> None: + """Prints options and whether they are under or over priced [Source: Yahoo Finance] + + Parameters + ---------- + ticker: str + Ticker to get expirations for + exp: str + Expiration to use for options + put: bool + Whether to use puts or calls + ask: bool + Whether to use ask or lastPrice + mini: float + Minimum strike price to show + maxi: float + Maximum strike price to show + + """ + r_date = datetime.strptime(exp, "%Y-%m-%d").date() + delta = (r_date - date.today()).days / 365 + rate = ((1 + get_rf()) ** delta) - 1 + stock = get_price(ticker) + chain = get_option_chain(ticker, exp) + name = "ask" if ask else "lastPrice" + o_type = "put" if put else "call" + + calls = chain.calls[["strike", name]].copy() + calls = calls.rename(columns={name: "callPrice"}) + puts = chain.puts[["strike", name]].copy() + puts = puts.rename(columns={name: "putPrice"}) + + opts = pd.merge(calls, puts, on="strike") + opts = opts.dropna() + opts = opts.loc[opts["callPrice"] * opts["putPrice"] != 0] + + opts["callParity"] = opts["putPrice"] + stock - (opts["strike"] / (1 + rate)) + opts["putParity"] = (opts["strike"] / (1 + rate)) + opts["callPrice"] - stock + + diff = o_type + " Difference" + opts[diff] = opts[o_type + "Price"] - opts[o_type + "Parity"] + opts["distance"] = abs(stock - opts["strike"]) + filtered = opts.copy() + + if mini is None: + mini = filtered.strike.quantile(0.25) + if maxi is None: + maxi = filtered.strike.quantile(0.75) + + filtered = filtered.loc[filtered["strike"] > mini] + filtered = filtered.loc[filtered["strike"] < maxi] + + show = filtered[["strike", diff]].copy() + + print("Warning: Low volume options may be difficult to trade.\n") + if ask: + print("Warning: Options with no current ask price not shown.\n") + + if gtff.USE_TABULATE_DF: + print( + tabulate( + show, + headers=[x.title() for x in show.columns], + tablefmt="fancy_grid", + showindex=False, + floatfmt=".2f", + ) + ) + else: + print(show.to_string(index=False)) + + print("") diff --git a/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_helper.py b/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_helper.py index 51bc9480e4f..a9866b09afd 100644 --- a/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_helper.py +++ b/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_helper.py @@ -2,7 +2,6 @@ __docformat__ = "numpy" import argparse -import requests # These are all the possible yfinance properties l_valid_property_infos = [ @@ -106,16 +105,3 @@ def check_valid_property_type(aproperty: str) -> str: return aproperty raise argparse.ArgumentTypeError(f"{aproperty} is not a valid info") - - -def get_rf() -> float: - """Uses the fiscaldata.gov API to get most recent T-Bill rate""" - try: - base = "https://api.fiscaldata.treasury.gov/services/api/fiscal_service" - end = "/v2/accounting/od/avg_interest_rates" - filters = "?filter=security_desc:eq:Treasury Bills&sort=-record_date" - response = requests.get(base + end + filters) - latest = response.json()["data"][0] - return round(float(latest["avg_interest_rate_amt"]) / 100, 8) - except Exception: - return 0.02 diff --git a/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_view.py b/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_view.py index 32e145b694b..cc0aab9c9cc 100644 --- a/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_view.py +++ b/gamestonk_terminal/portfolio/portfolio_optimization/optimizer_view.py @@ -16,7 +16,7 @@ from gamestonk_terminal import feature_flags as gtff from gamestonk_terminal.config_plot import PLOT_DPI from gamestonk_terminal.helper_funcs import plot_autoscale from gamestonk_terminal.portfolio.portfolio_optimization import optimizer_model -from gamestonk_terminal.portfolio.portfolio_optimization.optimizer_helper import get_rf +from gamestonk_terminal.helper_funcs import get_rf d_period = { "1d": "[1 Day]", diff --git a/gamestonk_terminal/portfolio/portfolio_optimization/po_controller.py b/gamestonk_terminal/portfolio/portfolio_optimization/po_controller.py index 8d2fd991f9a..e100063ec6e 100644 --- a/gamestonk_terminal/portfolio/portfolio_optimization/po_controller.py +++ b/gamestonk_terminal/portfolio/portfolio_optimization/po_controller.py @@ -18,7 +18,7 @@ from gamestonk_terminal.portfolio.portfolio_optimization import ( optimizer_helper, ) -from gamestonk_terminal.portfolio.portfolio_optimization.optimizer_helper import get_rf +from gamestonk_terminal.helper_funcs import get_rf period_choices = [ "1d", diff --git a/gamestonk_terminal/stocks/fundamental_analysis/dcf_model.py b/gamestonk_terminal/stocks/fundamental_analysis/dcf_model.py index 0a3b997cf38..52f206a1e54 100644 --- a/gamestonk_terminal/stocks/fundamental_analysis/dcf_model.py +++ b/gamestonk_terminal/stocks/fundamental_analysis/dcf_model.py @@ -11,7 +11,6 @@ from openpyxl.styles import Border, Side, Font, PatternFill, Alignment from openpyxl import worksheet import yfinance as yf import pandas as pd -import requests opts = Union[int, str, float] @@ -60,16 +59,6 @@ def set_cell( ws[cell].number_format = num_form -def get_rf(): - """Uses the fiscaldata.gov API to get most recent T-Bill rate""" - base = "https://api.fiscaldata.treasury.gov/services/api/fiscal_service" - end = "/v2/accounting/od/avg_interest_rates" - filters = "?filter=security_desc:eq:Treasury Bills&sort=-record_date" - response = requests.get(base + end + filters) - latest = response.json()["data"][0] - return latest["avg_interest_rate_amt"] - - def get_fama_raw(): """Gets base Fama French data to calculate risk""" with urlopen( diff --git a/gamestonk_terminal/stocks/fundamental_analysis/dcf_view.py b/gamestonk_terminal/stocks/fundamental_analysis/dcf_view.py index c1d27ef0294..0f182e8f435 100644 --- a/gamestonk_terminal/stocks/fundamental_analysis/dcf_view.py +++ b/gamestonk_terminal/stocks/fundamental_analysis/dcf_view.py @@ -21,6 +21,8 @@ import requests from gamestonk_terminal.helper_funcs import parse_known_args_and_warn from gamestonk_terminal.stocks.fundamental_analysis import dcf_model +from gamestonk_terminal.helper_funcs import get_rf + int_or_str = Union[int, str] @@ -90,7 +92,7 @@ class CreateExcelFA: self.df_is: pd.DataFrame = self.get_data("IS", self.is_start, True) self.df_cf: pd.DataFrame = self.get_data("CF", self.cf_start, False) self.info: pd.DataFrame = yf.Ticker(ticker).info - self.t_bill: float = dcf_model.get_rf() + self.t_bill: float = get_rf() self.r_ff: int = dcf_model.get_fama_coe(self.ticker) def create_workbook(self): diff --git a/website/content/options/parity/_index.md b/website/content/options/parity/_index.md new file mode 100644 index 00000000000..bf15395f967 --- /dev/null +++ b/website/content/options/parity/_index.md @@ -0,0 +1,14 @@ +``` +usage: parity [-p] [-a] [-m MINIMUM] [-M MAXIMUM] [-h] +``` + +shows the parity for options. in theory a short plus a stock should equal a call plus a bond. + +``` +optional arguments: + -a, --ask uses ask price instead of lastPrice (default: False) + -p, --put shows puts instead of calls (default: False) + -m, --min the minimum strike price to be shown (default: None) + -M, --max the maximum strike price to be shown (default: None) + -h, --help show this help message (default: False) +``` diff --git a/website/data/menu/main.yml b/website/data/menu/main.yml index 55b678ee6b3..eeca0c0c50d 100755 --- a/website/data/menu/main.yml +++ b/website/data/menu/main.yml @@ -584,8 +584,10 @@ main: ref: "/options/grhist" - name: smile ref: "/options/smile" - - name: portfolio optimization - ref: "/portfolio/portfolio_optimization" + - name: parity + ref: "/options/parity" + - name: payoff + ref: "/options/payoff" sub: - name: add ref: "/options/payoff/add"