From c75810aae86bae9c10bb7c122cb7cb8b19730ebe Mon Sep 17 00:00:00 2001 From: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Date: Thu, 4 Nov 2021 15:26:22 -0400 Subject: [PATCH] Risk neutral options valuation (#906) * Risk neutral hedge * Changeable risk * Added documentation * Improved description * Improved description * Retry Co-authored-by: Colin Delahunty --- gamestonk_terminal/options/op_helpers.py | 39 +++- .../options/options_controller.py | 167 +++++++++++++++++- gamestonk_terminal/options/yfinance_view.py | 88 ++++++++- .../portfolio/portfolios/cash.csv | 12 ++ .../portfolio/portfolios/james.csv | 5 + .../portfolio/portfolios/market.csv | 4 + website/content/options/add/_index.md | 12 ++ website/content/options/rmv/_index.md | 12 ++ website/content/options/rnval/_index.md | 28 +++ website/content/options/show/_index.md | 10 ++ website/data/menu/main.yml | 8 + 11 files changed, 371 insertions(+), 14 deletions(-) create mode 100644 gamestonk_terminal/portfolio/portfolios/cash.csv create mode 100644 gamestonk_terminal/portfolio/portfolios/james.csv create mode 100644 gamestonk_terminal/portfolio/portfolios/market.csv create mode 100644 website/content/options/add/_index.md create mode 100644 website/content/options/rmv/_index.md create mode 100644 website/content/options/rnval/_index.md create mode 100644 website/content/options/show/_index.md diff --git a/gamestonk_terminal/options/op_helpers.py b/gamestonk_terminal/options/op_helpers.py index f08380f5d64..5782b8698b1 100644 --- a/gamestonk_terminal/options/op_helpers.py +++ b/gamestonk_terminal/options/op_helpers.py @@ -10,14 +10,14 @@ def get_loss_at_strike(strike: float, chain: pd.DataFrame) -> float: Parameters ---------- - strike: Union[int,float] + strike : Union[int,float] Value to calculate total loss at - chain: Dataframe: + chain : Dataframe: Dataframe containing at least strike and openInterest Returns ------- - loss: Union[float,int] + loss : Union[float,int] Total loss """ @@ -38,7 +38,7 @@ def calculate_max_pain(chain: pd.DataFrame) -> int: Parameters ---------- - chain: DataFrame + chain : DataFrame Dataframe to calculate value from Returns @@ -66,7 +66,7 @@ def convert(orig: str, to: str) -> float: """Convert a string to a specific type of number Parameters ---------- - orig: str + orig : str String to convert Returns ------- @@ -82,6 +82,35 @@ def convert(orig: str, to: str) -> float: raise ValueError("Invalid to format, please use '%' or ','.") +def rn_payoff(x: str, df: pd.DataFrame, put: bool, delta: int, rf: float) -> float: + """The risk neutral payoff for a stock + Parameters + ---------- + x : str + Strike price + df : pd.DataFrame + Dataframe of stocks prices and probabilities + put : bool + Whether the asset is a put or a call + delta : int + Difference between today's date and expirations date in days + rf : float + The current risk-free rate + + Returns + ------- + number : float + Risk neutral value of option + """ + if put: + df["Gain"] = np.where(x > df["Price"], x - df["Price"], 0) + else: + df["Gain"] = np.where(x < df["Price"], df["Price"] - x, 0) + df["Vals"] = df["Chance"] * df["Gain"] + risk_free = (1 + rf) ** (delta / 365) + return sum(df["Vals"]) / risk_free + + opt_chain_cols = { "lastTradeDate": {"format": "date", "label": "Last Trade Date"}, "strike": {"format": "${x:.2f}", "label": "Strike"}, diff --git a/gamestonk_terminal/options/options_controller.py b/gamestonk_terminal/options/options_controller.py index 6183a282cc7..fa2f5104143 100644 --- a/gamestonk_terminal/options/options_controller.py +++ b/gamestonk_terminal/options/options_controller.py @@ -10,6 +10,8 @@ from typing import List import matplotlib.pyplot as plt from colorama import Style +import pandas as pd +from tabulate import tabulate from prompt_toolkit.completion import NestedCompleter from gamestonk_terminal import feature_flags as gtff @@ -75,6 +77,10 @@ class OptionsController: "payoff", "plot", "parity", + "add", + "rmv", + "show", + "rnval", ] CHOICES += CHOICES_COMMANDS @@ -110,6 +116,7 @@ class OptionsController: "cmd", choices=self.CHOICES, ) + self.prices = pd.DataFrame(columns=["Price", "Chance"]) def print_help(self): """Print help.""" @@ -153,6 +160,13 @@ Current Expiry: {self.selected_date or None} parity shows whether options are above or below expected price [Yfinance] > payoff shows payoff diagram for a selection of options [Yfinance] + +Options Pricing: + add add an expected price to the list + rmv remove an expected price from the list + show show the listed of expected prices + rnval risk neutral valuation for an option: + {Style.RESET_ALL if not colored else ''}""" print(help_text) @@ -1178,7 +1192,6 @@ Current Expiry: {self.selected_date or None} prog="parity", description="Shows whether options are over or under valued", ) - parser.add_argument( "-p", "--put", @@ -1227,6 +1240,158 @@ Current Expiry: {self.selected_date or None} ) print("") + @try_except + def call_add(self, other_args: List[str]): + """Process add command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="add", + description="Adds a price to the list", + ) + parser.add_argument( + "-p", + "--price", + type=float, + required="-h" not in other_args, + dest="price", + help="Projected price of the stock at the expiration date", + ) + parser.add_argument( + "-c", + "--chance", + type=float, + required="-h" not in other_args, + dest="chance", + help="Chance that the stock is at a given projected price", + ) + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + if ns_parser.price in self.prices["Price"].to_list(): + df = self.prices[(self.prices["Price"] != ns_parser.price)] + else: + df = self.prices + + new = {"Price": ns_parser.price, "Chance": ns_parser.chance} + df = df.append(new, ignore_index=True) + self.prices = df.sort_values("Price") + + @try_except + def call_rmv(self, other_args: List[str]): + """Process rmv command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="rmv", + description="Removes a price from the list", + ) + parser.add_argument( + "-p", + "--price", + type=float, + required="-h" not in other_args and "-a" not in other_args, + dest="price", + help="Price you want to remove from the list", + ) + parser.add_argument( + "-a", + "--all", + action="store_true", + default=False, + dest="all", + help="Remove all prices from the list", + ) + if other_args and "-" not in other_args[0]: + other_args.insert(0, "-p") + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + if ns_parser.all: + self.prices = pd.DataFrame(columns=["Price", "Chance"]) + return + + self.prices = self.prices[(self.prices["Price"] != ns_parser.price)] + + @try_except + def call_show(self, _): + """Process show command""" + print(f"Estimated price(s) of {self.ticker} at {self.selected_date}") + if gtff.USE_TABULATE_DF: + print( + tabulate( + self.prices, + headers=self.prices.columns, + floatfmt=".2f", + showindex=False, + tablefmt="fancy_grid", + ), + "\n", + ) + else: + print(self.prices.to_string, "\n") + print("") + + @try_except + def call_rnval(self, other_args: List[str]): + """Process rnval command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="rnval", + description="The risk neutral value of the options", + ) + parser.add_argument( + "-p", + "--put", + action="store_true", + default=False, + help="Show puts instead of calls", + ) + 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", + ) + parser.add_argument( + "-r", + "--risk", + type=float, + default=None, + dest="risk", + help="The risk-free rate to use", + ) + 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 + if sum(self.prices["Chance"]) != 1: + print("Total chances must equal one\n") + return + + yfinance_view.risk_neutral_vals( + self.ticker, + self.selected_date, + ns_parser.put, + self.prices, + ns_parser.mini, + ns_parser.maxi, + ns_parser.risk, + ) + def menu(ticker: str = ""): """Options Menu.""" diff --git a/gamestonk_terminal/options/yfinance_view.py b/gamestonk_terminal/options/yfinance_view.py index 6cdd0532968..c9275b67077 100644 --- a/gamestonk_terminal/options/yfinance_view.py +++ b/gamestonk_terminal/options/yfinance_view.py @@ -496,17 +496,17 @@ def show_parity( Parameters ---------- - ticker: str + ticker : str Ticker to get expirations for - exp: str + exp : str Expiration to use for options - put: bool + put : bool Whether to use puts or calls - ask: bool + ask : bool Whether to use ask or lastPrice - mini: float + mini : float Minimum strike price to show - maxi: float + maxi : float Maximum strike price to show """ @@ -567,8 +567,8 @@ def show_parity( if maxi is None: maxi = filtered.strike.quantile(0.75) - filtered = filtered.loc[filtered["strike"] > mini] - filtered = filtered.loc[filtered["strike"] < maxi] + filtered = filtered.loc[filtered["strike"] >= mini] + filtered = filtered.loc[filtered["strike"] <= maxi] show = filtered[["strike", diff]].copy() @@ -590,3 +590,75 @@ def show_parity( print(show.to_string(index=False)) print("") + + +def risk_neutral_vals( + ticker: str, + exp: str, + put: bool, + df: pd.DataFrame, + mini: float, + maxi: float, + risk: float, +) -> None: + """Prints current options prices and risk neutral values [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 + df : pd.DataFrame + Estimates for stocks prices and probabilities + mini : float + Minimum strike price to show + maxi : float + Maximum strike price to show + risk : float + The risk-free rate for the asset + """ + if put: + chain = get_option_chain(ticker, exp).puts + else: + chain = get_option_chain(ticker, exp).calls + + r_date = datetime.strptime(exp, "%Y-%m-%d").date() + delta = (r_date - date.today()).days + vals = [] + if risk is None: + risk = get_rf() + for _, row in chain.iterrows(): + vals.append( + [ + row["strike"], + row["lastPrice"], + op_helpers.rn_payoff(row["strike"], df, put, delta, risk), + ] + ) + new_df = pd.DataFrame(vals, columns=["Strike", "Last Price", "Value"], dtype=float) + new_df["Difference"] = new_df["Last Price"] - new_df["Value"] + + if mini is None: + mini = new_df.Strike.quantile(0.25) + if maxi is None: + maxi = new_df.Strike.quantile(0.75) + + new_df = new_df[new_df["Strike"] >= mini] + new_df = new_df[new_df["Strike"] <= maxi] + + if gtff.USE_TABULATE_DF: + print( + tabulate( + new_df, + headers=[x.title() for x in new_df.columns], + tablefmt="fancy_grid", + showindex=False, + floatfmt=".2f", + ) + ) + else: + print(new_df.to_string(index=False)) + print("") diff --git a/gamestonk_terminal/portfolio/portfolios/cash.csv b/gamestonk_terminal/portfolio/portfolios/cash.csv new file mode 100644 index 00000000000..3efbf0887c9 --- /dev/null +++ b/gamestonk_terminal/portfolio/portfolios/cash.csv @@ -0,0 +1,12 @@ +Name,Type,Quantity,Date,Price,Fees,Premium,Side +cash,cash,1.0,2021/1/22,2000.0,,,deposit +cash,cash,1.0,2021/2/22,2000.0,,,deposit +cash,cash,1.0,2021/3/22,2000.0,,,deposit +cash,cash,1.0,2021/4/22,2000.0,,,deposit +cash,cash,1.0,2021/5/22,2000.0,,,deposit +cash,cash,1.0,2021/6/22,2000.0,,,deposit +cash,cash,1.0,2021/7/22,2000.0,,,deposit +cash,cash,1.0,2021/8/22,2000.0,,,deposit +cash,cash,1.0,2021/9/22,2000.0,,,deposit +cash,cash,1.0,2021/10/22,2000.0,,,deposit +cash,cash,1.0,2021/11/22,2000.0,,,deposit diff --git a/gamestonk_terminal/portfolio/portfolios/james.csv b/gamestonk_terminal/portfolio/portfolios/james.csv new file mode 100644 index 00000000000..73fd23447ed --- /dev/null +++ b/gamestonk_terminal/portfolio/portfolios/james.csv @@ -0,0 +1,5 @@ +Name,Type,Quantity,Date,Price,Fees,Premium,Side +cash,cash,1.0,2021/3/24,2000.0,,,deposit +aapl,stock,10,2021/3/24,120.7,0,0,Buy +msft,stock,3,2021/3/26,232.8,0,0,Buy +aapl,stock,5,2021/7/2,139.8,0,0,Sell diff --git a/gamestonk_terminal/portfolio/portfolios/market.csv b/gamestonk_terminal/portfolio/portfolios/market.csv new file mode 100644 index 00000000000..a3ea9b70e64 --- /dev/null +++ b/gamestonk_terminal/portfolio/portfolios/market.csv @@ -0,0 +1,4 @@ +Name,Type,Quantity,Date,Price,Fees,Premium,Side +spy,stock,1.0,2019/10/21,200.0,,,Buy +cash,cash,1.0,2019/10/21,200.0,,,deposit +cash,cash,1.0,2021/6/21,200.0,,,withdrawal diff --git a/website/content/options/add/_index.md b/website/content/options/add/_index.md new file mode 100644 index 00000000000..fc907165903 --- /dev/null +++ b/website/content/options/add/_index.md @@ -0,0 +1,12 @@ +``` +usage: add [-p] [-c] [-h] +``` + +add a stock price and probability to the list + +``` +optional arguments: + -p PRICE, --PRICE the estimated finishing price of the asset + -c CHANCE, --CHANCE the chance of the asset ending at the given price + -h, --help show this help message (default: False) +``` diff --git a/website/content/options/rmv/_index.md b/website/content/options/rmv/_index.md new file mode 100644 index 00000000000..135eff8d4c2 --- /dev/null +++ b/website/content/options/rmv/_index.md @@ -0,0 +1,12 @@ +``` +usage: rmv [-p] [-c] [-h] +``` + +remove a stock price from the list + +``` +optional arguments: + -p PRICE, --PRICE the estimated finishing price of the asset + -a, --all remove all items from the list of tickers + -h, --help show this help message (default: False) +``` diff --git a/website/content/options/rnval/_index.md b/website/content/options/rnval/_index.md new file mode 100644 index 00000000000..8ad9f1a2d6a --- /dev/null +++ b/website/content/options/rnval/_index.md @@ -0,0 +1,28 @@ +``` +usage: rnval [-p] [-m MIN] [-M MAX] [-r RISK] [-h] +``` + +calculates the expcected value of a given option by multiplying +the payoff at each predicted stock price by the probability of +that stock price happening. This expected profit is divided by +one plus the risk free rate to determine the value of those +future dollars today. The expected payoff for each strike price +is then compared with the last traded price to determine if there +are differences in the market. + +For example, if you assume the price of AAPL has an equal chance +of finishing at $100 or $200 for a given expiration, you could use +this to value an option at that expiration. To value a call with a +strike of $150, you would calculate the payoff for each stock price, +which would be $0 and $50. Then, we caclulate the weighted average +payoff of $25. We need to divide this amount by the risk-free rate, +assumed to be 0.02. So the value of this option expiring in one year +is 25/1.02, or $24.51. +``` +optional arguments: + -p, --put flag to calculate put option (default: False) + -m MIN, --min MIN min price to look at (default: -1) + -M MAX, --max MAX max price to look at (default: -1) + -r RISK, --risk RISK use a custom risk-free amount (default: None) + -h, --help show this help message (default: False) +``` diff --git a/website/content/options/show/_index.md b/website/content/options/show/_index.md new file mode 100644 index 00000000000..4ca85436127 --- /dev/null +++ b/website/content/options/show/_index.md @@ -0,0 +1,10 @@ +``` +usage: show [-h] +``` + +show the list of stocks prices and probabilities + +``` +optional arguments: + -h, --help show this help message (default: False) +``` diff --git a/website/data/menu/main.yml b/website/data/menu/main.yml index d64f731dd29..9655d53047b 100755 --- a/website/data/menu/main.yml +++ b/website/data/menu/main.yml @@ -551,6 +551,14 @@ main: ref: "/portfolio/portfolio_optimization" - name: parity ref: "/options/parity" + - name: add + ref: "/options/add" + - name: rmv + ref: "/options/rmv" + - name: show + ref: "/options/show" + - name: rnval + ref: "/options/rnval" - name: payoff ref: "/options/payoff" sub: