From 0be4c8a2d77ba297ab08e5a055d2824f4f6ee395 Mon Sep 17 00:00:00 2001 From: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Date: Wed, 8 Dec 2021 16:23:03 -0500 Subject: [PATCH] Binomial Options Pricing (#1017) * Added variables for binom function * Finished binom function * Updated .ipynb handling * James updates * Expanded definitions * Expanded description --- .gitignore | 1 + .../stocks/options/options_controller.py | 75 ++++++ .../stocks/options/yfinance_model.py | 49 +++- .../stocks/options/yfinance_view.py | 239 ++++++++++++++++++ .../content/stocks/options/binom/_index.md | 25 ++ website/data/menu/main.yml | 2 + 6 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 website/content/stocks/options/binom/_index.md diff --git a/.gitignore b/.gitignore index 9ac7efe8926..ede8b33b6f8 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ exports/* htmlcov gamestonk_terminal/reports/stored/* gamestonk_terminal/portfolio/portfolios/* +.ipynb_checkpoints/* # Jupyter Lab Extension build files jupyterlab/*/*.bundle.* diff --git a/gamestonk_terminal/stocks/options/options_controller.py b/gamestonk_terminal/stocks/options/options_controller.py index 7691c39f2b0..16a05d34c8d 100644 --- a/gamestonk_terminal/stocks/options/options_controller.py +++ b/gamestonk_terminal/stocks/options/options_controller.py @@ -75,6 +75,7 @@ class OptionsController: "unu", "plot", "parity", + "binom", ] CHOICES_MENUS = [ @@ -151,6 +152,7 @@ Current Expiry: {self.selected_date or None} grhist plot option greek history [Syncretism.io] plot plot variables provided by the user [Yfinance] parity shows whether options are above or below expected price [Yfinance] + binom shows the value of an option using binomial options pricing [Yfinance] > payoff shows payoff diagram for a selection of options [Yfinance] > pricing shows options pricing and risk neutral valuation [Yfinance] @@ -1260,6 +1262,79 @@ Current Expiry: {self.selected_date or None} ) print("") + @try_except + def call_binom(self, other_args: List[str]): + """Process binom command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="parity", + description="Gives the option value using binomial option valuation", + ) + parser.add_argument( + "-s", + "--strike", + type=float, + default=0, + dest="strike", + help="Strike price for option shown", + ) + parser.add_argument( + "-p", + "--put", + action="store_true", + default=False, + dest="put", + help="Value a put instead of a call", + ) + parser.add_argument( + "-e", + "--european", + action="store_true", + default=False, + dest="europe", + help="Value a European option instead of an American one", + ) + parser.add_argument( + "--export", + action="store_true", + default=False, + dest="export", + help="Export an excel spreadsheet with binomial pricing data", + ) + parser.add_argument( + "--plot", + action="store_true", + default=False, + dest="plot", + help="Plot expected ending values", + ) + parser.add_argument( + "-v", + "--volatility", + type=float, + default=None, + dest="volatility", + help="Underlying asset annualized volatility", + ) + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + if not self.ticker or not self.selected_date: + print("Ticker and expiration required. \n") + return + + yfinance_view.show_binom( + self.ticker, + self.selected_date, + ns_parser.strike, + ns_parser.put, + ns_parser.europe, + ns_parser.export, + ns_parser.plot, + ns_parser.volatility, + ) + @try_except def call_pricing(self, _): """Process pricing command""" diff --git a/gamestonk_terminal/stocks/options/yfinance_model.py b/gamestonk_terminal/stocks/options/yfinance_model.py index 4e64dd21b4b..961aa3b6470 100644 --- a/gamestonk_terminal/stocks/options/yfinance_model.py +++ b/gamestonk_terminal/stocks/options/yfinance_model.py @@ -117,9 +117,54 @@ def generate_data( return x_vals, before, [] -def get_price(ticker) -> float: - """Get current price for a given ticker""" +def get_price(ticker: str) -> float: + """Get current price for a given ticker + + Parameters + ---------- + ticker : str + The ticker to get the price for + + Returns + ---------- + price : float + The price of the ticker + """ ticker_yahoo = yf.Ticker(ticker) data = ticker_yahoo.history() last_quote = data.tail(1)["Close"].iloc[0] return last_quote + + +def get_info(ticker: str): + """Get info for a given ticker + + Parameters + ---------- + ticker : str + The ticker to get the price for + + Returns + ---------- + price : float + The info for a given ticker + """ + tick = yf.Ticker(ticker) + return tick.info + + +def get_closing(ticker: str) -> pd.Series: + """Get closing prices for a given ticker + + Parameters + ---------- + ticker : str + The ticker to get the price for + + Returns + ---------- + price : List[float] + A list of closing prices for a ticker + """ + tick = yf.Ticker(ticker) + return tick.history(period="1y")["Close"] diff --git a/gamestonk_terminal/stocks/options/yfinance_view.py b/gamestonk_terminal/stocks/options/yfinance_view.py index 54e7ba588f3..022e2408a24 100644 --- a/gamestonk_terminal/stocks/options/yfinance_view.py +++ b/gamestonk_terminal/stocks/options/yfinance_view.py @@ -2,17 +2,21 @@ __docformat__ = "numpy" import os +import math from bisect import bisect_left from typing import List, Dict, Any from datetime import datetime, date, timedelta import matplotlib.pyplot as plt import matplotlib.dates as mdates +import matplotlib.ticker as mtick +from scipy.stats import binom import numpy as np import pandas as pd import seaborn as sns import yfinance as yf from tabulate import tabulate +from openpyxl import Workbook import gamestonk_terminal.config_plot as cfp import gamestonk_terminal.feature_flags as gtff @@ -662,3 +666,238 @@ def risk_neutral_vals( else: print(new_df.to_string(index=False)) print("") + + +def plot_expected_prices( + und_vals: List[List[float]], p: float, ticker: str, expiration: str +) -> None: + """Plot expected prices of the underlying asset at expiration + + Parameters + ---------- + und_vals : List[List[float]] + The expected underlying values at the expiration date + p : float + The probability of the stock price moving upward each round + ticker : str + The ticker of the option's underlying asset + expiration : str + The expiration for the option + """ + up_moves = list(range(len(und_vals[-1]))) + up_moves.reverse() + probs = [binom.pmf(r, len(up_moves), p) for r in up_moves] + fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfp.PLOT_DPI) + ax.set_title(f"Probabilities for ending prices of {ticker} on {expiration}") + ax.xaxis.set_major_formatter("${x:1.2f}") + ax.yaxis.set_major_formatter(mtick.PercentFormatter()) + plt.plot(und_vals[-1], probs) + fig.tight_layout() + if gtff.USE_ION: + plt.ion() + plt.show() + + +def export_binomial_calcs( + up: float, + prob_up: float, + discount: float, + und_vals: List[List[float]], + opt_vals: List[List[float]], + days: int, + ticker: str, +) -> None: + """Create an excel spreadsheet with binomial tables for underlying asset value and option value + + Parameters + ---------- + up : float + The stock's increase on an upward move + prob_up : float + The probability of an upward move + discount : float + The daily discount rate + und_vals : List[List[float]] + The underlying asset values at each step + opt_vals : List[List[float]] + The values for the option at each step + days : int + The number of days until the option expires + ticker : str + The ticker for the company + """ + letters = [ + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + ] + letters += [ + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + ] + opts = ( + [f"{x}" for x in letters] + + [f"{x}{y}" for x in letters for y in letters] + + [f"{x}{y}{z}" for x in letters for y in letters for z in letters] + ) + wb = Workbook() + ws = wb.active + + ws["A1"] = "Up Move" + ws["B1"] = up + ws["A2"] = "Down Move" + ws["B2"] = 1 / up + ws["D1"] = "Prob Up" + ws["E1"] = prob_up + ws["D2"] = "Prob Down" + ws["E2"] = 1 - prob_up + ws["D3"] = "Discount" + ws["E3"] = discount + ws["A4"] = "Binomial Tree for Underlying Values" + for i, _ in enumerate(und_vals): + for j, _ in enumerate(und_vals[i]): + ws[f"{opts[i]}{j+5}"] = und_vals[i][j] + + ws[f"A{days+7}"] = "Binomial Tree for Option Values" + for i, _ in enumerate(opt_vals): + for j, _ in enumerate(opt_vals[i]): + ws[f"{opts[i]}{j+8+days}"] = opt_vals[i][j] + + trypath = os.path.join( + os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..")), + "exports", + "stocks", + "options", + f"{ticker} {datetime.now()}.xlsx", + ) + wb.save(trypath) + print(f"Analysis ran for {ticker}\nPlease look in {trypath} for the file.\n") + + +def show_binom( + ticker: str, + expiration: str, + strike: float, + put: bool, + europe: bool, + export: bool, + plot: bool, + vol: float, +) -> None: + """Get binomial pricing for option + + Parameters + ---------- + ticker : str + The ticker of the option's underlying asset + expiration : str + The expiration for the option + strike : float + The strike price for the option + put : bool + Value a put instead of a call + europe : bool + Value a European option instead of an American option + export : bool + Export the options data to an excel spreadsheet + plot : bool + Show a graph of expected ending prices + vol : float + The annualized volatility for the underlying asset + """ + # Base variables to calculate values + info = yfinance_model.get_info(ticker) + price = info["regularMarketPrice"] + if vol is None: + closings = yfinance_model.get_closing(ticker) + vol = (closings / closings.shift()).std() * (252 ** 0.5) + div_yield = ( + info["trailingAnnualDividendYield"] + if info["trailingAnnualDividendYield"] is not None + else 0 + ) + delta_t = 1 / 252 + rf = get_rf() + exp_date = datetime.strptime(expiration, "%Y-%m-%d").date() + today = date.today() + days = (exp_date - today).days + + # Binomial pricing specific variables + up = math.exp(vol * (delta_t ** 0.5)) + down = 1 / up + prob_up = (math.exp((rf - div_yield) * delta_t) - down) / (up - down) + prob_down = 1 - prob_up + discount = math.exp(delta_t * rf) + + und_vals: List[List[float]] = [[price]] + + # Binomial tree for underlying values + for i in range(days): + cur_date = today + timedelta(days=i + 1) + if cur_date.weekday() < 5: + last = und_vals[-1] + new = [x * up for x in last] + new.append(last[-1] * down) + und_vals.append(new) + + # Binomial tree for option values + if put: + opt_vals = [[max(strike - x, 0) for x in und_vals[-1]]] + else: + opt_vals = [[max(x - strike, 0) for x in und_vals[-1]]] + + j = 2 + while len(opt_vals[0]) > 1: + new_vals = [] + for i in range(len(opt_vals[0]) - 1): + if europe: + value = ( + opt_vals[0][i] * prob_up + opt_vals[0][i + 1] * prob_down + ) / discount + else: + if put: + value = max( + (opt_vals[0][i] * prob_up + opt_vals[0][i + 1] * prob_down) + / discount, + strike - und_vals[-j][i], + ) + else: + value = max( + (opt_vals[0][i] * prob_up + opt_vals[0][i + 1] * prob_down) + / discount, + und_vals[-j][i] - strike, + ) + new_vals.append(value) + opt_vals.insert(0, new_vals) + j += 1 + + if export: + export_binomial_calcs(up, prob_up, discount, und_vals, opt_vals, days, ticker) + + if plot: + plot_expected_prices(und_vals, prob_up, ticker, expiration) + + print( + f"{ticker} {'put' if put else 'call'} at ${strike:.2f} expiring on {expiration} is worth ${opt_vals[0][0]:.2f}\n" + ) diff --git a/website/content/stocks/options/binom/_index.md b/website/content/stocks/options/binom/_index.md new file mode 100644 index 00000000000..46a3797acec --- /dev/null +++ b/website/content/stocks/options/binom/_index.md @@ -0,0 +1,25 @@ +``` +usage: binom [-s STRIKE] [-p] [-e] [-E] [-P] [-v VOLATILITY] [-h] +``` + +Shows the value of an option using binomial option pricing. Can also show raw data and provide a graph with predicted underlying asset ending values. The binomial options model calculates how big an up step or down step in the next time period will likely be. Then it creates a tree doing this at each period. The end results of this is a tree with possible asset values at each "step". For our calculations we use a day as our "step" time period. We then take all of the expected values at the finishing date and use this to begin a tree of option values at each step. The ending results is the value of the option today. + +The up step is calculated by taking e to the power of volatility times the square root of the change in time during the step. This is the percentage we expect the stock to increase if there is an upward movement. The down step is the inverse of the up step. The probability of the up step is calculated by taking e to the power of the risk free rate minus the dividend yield. This is multiplied by the change in time for the step and then subtracted by the expected downward movement. This number is then divided by the up step subtracted by then down step. The probability of a downward step is just one minus the probability of an upward step. + +Formulas: +up_step = e ^ (volatility * (delta_t ^ (1 / 2))) +down_step = 1 / up_step + +prob_up = (e ^ ((risk_free - div_yield) * delta_t) - down_step) / (up_step - down_step) +prob_down = 1 - prob_up +``` +optional arguments: + -s STRIKE, --strike STRIKE + strike price for the option (default: 0) + -p, --put value the option as a put (default: False) + -e, --european value the option as a European option (default: False) + --export export the binomial trees (default: False) + -P, --plot plots the expected underlying asset ending prices (default: False) + -v, --volatility sets the volatility for the underlying asset(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 5fd24721195..ab24d022591 100755 --- a/website/data/menu/main.yml +++ b/website/data/menu/main.yml @@ -422,6 +422,8 @@ main: ref: "/portfolio/portfolio_optimization" - name: parity ref: "/stocks/options/parity" + - name: binom + ref: "/stocks/options/binom" - name: payoff ref: "/stocks/options/payoff" sub: