From 6b09893bba8aedebbdc329b007d144f5bd5f6151 Mon Sep 17 00:00:00 2001 From: Colin Delahunty <72827203+colin99d@users.noreply.github.com> Date: Mon, 11 Oct 2021 20:47:29 -0400 Subject: [PATCH] Option Payoff Diagrams (#800) * Began creating payoff matrix * Updated parser abilities * Finalized ability to add, remove, and show options * Added graph generator * Cleaned code * Attempting to fix black * Attempting to fix black * Fixed typings * Added James' changes * Made changes, added documentation * Added changes * Fixed my silly mistake * Didier requests added Co-authored-by: Colin Delahunty Co-authored-by: Colin Delahunty --- .../options/options_controller.py | 15 + .../options/payoff_controller.py | 371 ++++++++++++++++++ gamestonk_terminal/options/yfinance_model.py | 65 ++- gamestonk_terminal/options/yfinance_view.py | 27 ++ website/content/options/payoff/_index.md | 5 + website/content/options/payoff/add/_index.md | 17 + website/content/options/payoff/pick/_index.md | 11 + website/content/options/payoff/plot/_index.md | 10 + website/content/options/payoff/rmv/_index.md | 13 + website/data/menu/main.yml | 11 + 10 files changed, 544 insertions(+), 1 deletion(-) create mode 100644 gamestonk_terminal/options/payoff_controller.py create mode 100644 website/content/options/payoff/_index.md create mode 100644 website/content/options/payoff/add/_index.md create mode 100644 website/content/options/payoff/pick/_index.md create mode 100644 website/content/options/payoff/plot/_index.md create mode 100644 website/content/options/payoff/rmv/_index.md diff --git a/gamestonk_terminal/options/options_controller.py b/gamestonk_terminal/options/options_controller.py index 18bc43eb37c..b248aeb188e 100644 --- a/gamestonk_terminal/options/options_controller.py +++ b/gamestonk_terminal/options/options_controller.py @@ -30,6 +30,7 @@ from gamestonk_terminal.options import ( yfinance_view, ) from gamestonk_terminal.stocks import stocks_controller +from gamestonk_terminal.options import payoff_controller class OptionsController: @@ -60,6 +61,7 @@ class OptionsController: "grhist", "unu", "stocks", + "payoff", "smile", ] @@ -136,6 +138,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] +> payoff shows payoff diagram for a selection of options [Yfinance] {Style.RESET_ALL if not colored else ''}""" print(help_text) @@ -945,6 +948,18 @@ Current Expiry: {self.selected_date or None} print("Ticker and expiration required.") return + def call_payoff(self, _): + """Process payoff command""" + if not self.ticker or not self.selected_date: + print("Ticker and expiration required.\n") + return None + ret = payoff_controller.menu(self.ticker, self.selected_date) + if ret is False: + self.print_help() + else: + return True + return False + def call_oi(self, other_args: List[str]): """Process oi command""" parser = argparse.ArgumentParser( diff --git a/gamestonk_terminal/options/payoff_controller.py b/gamestonk_terminal/options/payoff_controller.py new file mode 100644 index 00000000000..cc3d6a6eab2 --- /dev/null +++ b/gamestonk_terminal/options/payoff_controller.py @@ -0,0 +1,371 @@ +""" Payoff Controller Module """ +__docformat__ = "numpy" + +import argparse +import os +from typing import List, Dict + +import matplotlib.pyplot as plt +from prompt_toolkit.completion import NestedCompleter + +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.helper_funcs import ( + get_flair, + parse_known_args_and_warn, +) +from gamestonk_terminal.menu import session +from gamestonk_terminal.options.yfinance_model import get_option_chain, get_price +from gamestonk_terminal.options.yfinance_view import plot_payoff + + +class Payoff: + + CHOICES = ["cls", "?", "help", "q", "quit", "list"] + CHOICES_COMMANDS = [ + "list", + "add", + "rmv", + "pick", + "plot", + ] + CHOICES += CHOICES_COMMANDS + + # pylint: disable=dangerous-default-value + def __init__(self, ticker: str, expiration: str): + """Construct Payoff""" + + self.po_parser = argparse.ArgumentParser(add_help=False, prog="po") + self.po_parser.add_argument("cmd", choices=self.CHOICES) + self.chain = get_option_chain(ticker, expiration) + self.calls = list( + zip( + self.chain.calls["strike"].tolist(), + self.chain.calls["lastPrice"].tolist(), + ) + ) + self.puts = list( + zip( + self.chain.puts["strike"].tolist(), + self.chain.puts["lastPrice"].tolist(), + ) + ) + self.ticker = ticker + self.expiration = expiration + self.options: List[Dict[str, str]] = [] + self.underlying = 0 + self.current_price = get_price(ticker) + + @staticmethod + def print_help(underlying): + """Print help""" + if underlying == 1: + text = "Long" + elif underlying == 0: + text = "None" + elif underlying == -1: + text = "Short" + + help_text = f""" +>>OPTION PAYOFF DIAGRAM<< + +Underlying Asset: {text} + + list list available strike prices for calls and puts + + pick long, short, or none (default) underlying asset + add add option to the list of the options to be plotted + rmv remove option from the list of the options to be plotted + + plot show the option payoff diagram + """ + print(help_text) + + def switch(self, an_input: str): + """Process and dispatch input + + Returns + ------- + True, False or None + False - quit the menu + True - quit the program + None - continue in the menu + """ + + # Empty command + if not an_input: + print("") + return None + + (known_args, other_args) = self.po_parser.parse_known_args(an_input.split()) + + # Help menu again + if known_args.cmd == "?": + self.print_help(self.underlying) + return None + + # Clear screen + if known_args.cmd == "cls": + os.system("cls||clear") + return None + + return getattr( + self, "call_" + known_args.cmd, lambda: "Command not recognized!" + )(other_args) + + def call_help(self, _): + """Process Help command""" + self.print_help(self.underlying) + + def call_q(self, _): + """Process Q command - quit the menu""" + return False + + def call_quit(self, _): + """Process Quit command - quit the program""" + return True + + def call_list(self, _): + """Lists available calls and puts""" + length = max(len(self.calls), len(self.puts)) - 1 + print( + "Add an option using the index on the left, and not the actual strike price" + ) + print("#\tcall\tput") + for i in range(length): + call = self.calls[i][0] if i < len(self.calls) else "" + put = self.puts[i][0] if i < len(self.puts) else "" + print(f"{i}\t{call}\t{put}") + print("") + + def call_add(self, other_args: List[str]): + """Process add command""" + self.add_option(other_args) + + def call_rmv(self, other_args: List[str]): + """Process rmv command""" + self.rmv_option(other_args) + + def call_pick(self, other_args: List[str]): + """Process pick command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="long", + description="This function plots option payoff diagrams", + ) + parser.add_argument( + "-t", + "--type", + dest="type", + type=str, + help="choose what you would like to do with the underlying asset, choose from: long, short, or none", + required="-h" not in other_args, + ) + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-t") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + if ns_parser.type == "long": + self.underlying = 1 + elif ns_parser.type == "none": + self.underlying = 0 + elif ns_parser.type == "short": + self.underlying = -1 + + self.print_help(self.underlying) + + def call_plot(self, other_args): + """Process plot command""" + parser = argparse.ArgumentParser( + add_help=False, + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + prog="plot", + description="This function plots option payoff diagrams", + ) + + try: + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + plot_payoff( + self.current_price, + self.options, + self.underlying, + self.ticker, + self.expiration, + ) + + except Exception as e: + print(e, "\n") + + def show_setup(self, nl: bool = False): + """Shows the current assets to display in the diagram""" + print("#\tType\tHold\tStrike\tCost") + for i, o in enumerate(self.options): + sign = "Long" if o["sign"] == 1 else "Short" + print(f"{i}\t{o['type']}\t{sign}\t{o['strike']}\t{o['cost']}") + if nl: + print("") + + def add_option(self, other_args: List[str]): + """Add an option to the diagram""" + parser = argparse.ArgumentParser( + add_help=False, + prog="add", + description="""Add options to the diagram.""", + ) + parser.add_argument( + "-p", + "--put", + dest="put", + action="store_true", + help="buy a put instead of a call", + default=False, + ) + parser.add_argument( + "-s", + "--short", + dest="short", + action="store_true", + help="short the option instead of buying it", + default=False, + ) + parser.add_argument( + "-i", + "--index", + dest="index", + type=int, + help="list index of the option", + required="-h" not in other_args and "-k" not in other_args, + ) + + parser.add_argument( + "-k", + "--strike", + dest="strike", + type=float, + help="strike price for the option", + default=None, + ) + try: + + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-i") + + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + opt_type = "put" if ns_parser.put else "call" + sign = -1 if ns_parser.short else 1 + options_list = self.puts if ns_parser.put else self.calls + if ns_parser.strike is None: + index = ns_parser.index + elif float(ns_parser.strike) in [float(x[0]) for x in options_list]: + index = filter( + lambda x: float(x[0]) == float(ns_parser.strike), options_list + ) + index = options_list.index(list(index)[0]) + print(index) + else: + print("Invalid strike price, please select a valid price from the list") + return + try: + strike = options_list[index][0] + cost = options_list[index][1] + except IndexError: + print("Please use the index, and not the strike price\n") + return + + option = {"type": opt_type, "sign": sign, "strike": strike, "cost": cost} + self.options.append(option) + + self.show_setup(True) + + except Exception as e: + print(e, "\n") + + def rmv_option(self, other_args: List[str]): + """Remove one of the options from the diagram""" + parser = argparse.ArgumentParser( + add_help=False, + prog="rmv", + description="""Remove one of the options to be shown in the payoff.""", + ) + + parser.add_argument( + "-k", + "--strike", + dest="strike", + type=int, + help="strike price for option", + ) + + parser.add_argument( + "-a", + "--all", + dest="all", + action="store_true", + help="remove all of the options", + default=False, + ) + try: + if other_args: + if "-" not in other_args[0]: + other_args.insert(0, "-k") + ns_parser = parse_known_args_and_warn(parser, other_args) + if not ns_parser: + return + + if ns_parser.all: + self.options = [] + else: + try: + del self.options[ns_parser.strike] + except IndexError: + print("Please use the index, and not the strike price\n") + return + + self.show_setup(True) + + except Exception as e: + print(e, "\n") + + +def menu(ticker: str, expiration: str): + """Portfolio Optimization Menu""" + plt.close("all") + po_controller = Payoff(ticker, expiration) + po_controller.call_help(None) + + while True: + # Get input command from user + if session and gtff.USE_PROMPT_TOOLKIT: + completer = NestedCompleter.from_nested_dict( + {c: None for c in po_controller.CHOICES} + ) + an_input = session.prompt( + f"{get_flair()} (options)>(payoff)> ", + completer=completer, + ) + else: + an_input = input(f"{get_flair()} (options)>(payoff)> ") + + try: + plt.close("all") + + process_input = po_controller.switch(an_input) + + if process_input is not None: + return process_input + + except SystemExit: + print("The command selected doesn't exist\n") + continue diff --git a/gamestonk_terminal/options/yfinance_model.py b/gamestonk_terminal/options/yfinance_model.py index 1681e0cfc85..098325ddc05 100644 --- a/gamestonk_terminal/options/yfinance_model.py +++ b/gamestonk_terminal/options/yfinance_model.py @@ -1,7 +1,10 @@ """Yfinance options model""" __docformat__ = "numpy" +from typing import List, Dict, Tuple, Any + import yfinance as yf +import pandas as pd def option_expirations(ticker: str): @@ -24,7 +27,7 @@ def option_expirations(ticker: str): return dates -def get_option_chain(ticker: str, expiration: str): +def get_option_chain(ticker: str, expiration: str) -> pd.DataFrame: """Gets option chain from yf for given ticker and expiration Parameters @@ -42,3 +45,63 @@ def get_option_chain(ticker: str, expiration: str): yf_ticker = yf.Ticker(ticker) chains = yf_ticker.option_chain(expiration) return chains + + +def get_x_values(current_price: float, options: List[Dict[str, int]]) -> List[float]: + """Generates different price values that need to be tested""" + x_list = list(range(101)) + mini = current_price + maxi = current_price + if len(options) == 0: + mini *= 0.5 + maxi *= 1.5 + elif len(options) > 0: + biggest = max(options, key=lambda x: x["strike"]) + smallest = min(options, key=lambda x: x["strike"]) + maxi = max(maxi, biggest["strike"]) * 1.2 + mini = min(mini, smallest["strike"]) * 0.8 + num_range = maxi - mini + return [(x / 100) * num_range + mini for x in x_list] + + +def get_y_values( + base: float, + price: float, + options: List[Dict[Any, Any]], + underlying: int, +) -> float: + """Generates y values for corresponding x values""" + option_change = 0 + change = price - base + for option in options: + if option["type"] == "call": + abs_change = price - option["strike"] if price > option["strike"] else 0 + option_change += option["sign"] * abs_change + elif option["type"] == "put": + abs_change = option["strike"] - price if price < option["strike"] else 0 + option_change += option["sign"] * abs_change + return (change * underlying) + option_change + + +def generate_data( + current_price: float, options: List[Dict[str, int]], underlying: int +) -> Tuple[List[float], List[float], List[float]]: + """Gets x values, and y values before and after premiums""" + x_vals = get_x_values(current_price, options) + base = current_price + total_cost = sum(x["cost"] for x in options) + before = [get_y_values(base, x, options, underlying) for x in x_vals] + if total_cost != 0: + after = [ + get_y_values(base, x, options, underlying) - total_cost for x in x_vals + ] + return x_vals, before, after + return x_vals, before, [] + + +def get_price(ticker) -> float: + """Get current price for a given ticker""" + ticker_yahoo = yf.Ticker(ticker) + data = ticker_yahoo.history() + last_quote = data.tail(1)["Close"].iloc[0] + return last_quote diff --git a/gamestonk_terminal/options/yfinance_view.py b/gamestonk_terminal/options/yfinance_view.py index a638bdaf30d..75edab9bd8d 100644 --- a/gamestonk_terminal/options/yfinance_view.py +++ b/gamestonk_terminal/options/yfinance_view.py @@ -3,6 +3,7 @@ __docformat__ = "numpy" import os from bisect import bisect_left +from typing import List, Dict, Any import matplotlib.pyplot as plt import numpy as np @@ -14,6 +15,7 @@ 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 def plot_oi( @@ -421,3 +423,28 @@ def plot_smile(ticker: str, expiration: str, put: bool) -> None: ax.yaxis.set_major_formatter("{x:.2f}%") plt.legend() plt.show() + + +def plot_payoff( + current_price: float, + options: List[Dict[Any, Any]], + underlying: int, + ticker: str, + expiration: str, +) -> None: + """Generate a graph showing the option payoff diagram""" + x, yb, ya = generate_data(current_price, options, underlying) + _, ax = plt.subplots() + if ya: + ax.plot(x, yb, label="Payoff Before Premium") + ax.plot(x, ya, label="Payoff After Premium") + else: + ax.plot(x, yb, label="Payoff") + ax.set_title(f"Option Payoff Diagram for {ticker} on {expiration}") + ax.set_ylabel("Profit") + ax.set_xlabel("Underlying Asset Price at Expiration") + ax.xaxis.set_major_formatter("${x:.2f}") + ax.yaxis.set_major_formatter("${x:.2f}") + plt.legend() + plt.show() + print("") diff --git a/website/content/options/payoff/_index.md b/website/content/options/payoff/_index.md new file mode 100644 index 00000000000..093e02e215d --- /dev/null +++ b/website/content/options/payoff/_index.md @@ -0,0 +1,5 @@ +--- +geekdocCollapseSection: true +--- + +{{< toc-tree >}} diff --git a/website/content/options/payoff/add/_index.md b/website/content/options/payoff/add/_index.md new file mode 100644 index 00000000000..b48144ebe2f --- /dev/null +++ b/website/content/options/payoff/add/_index.md @@ -0,0 +1,17 @@ +``` +usage: add [-k ADD_STRIKE] [-i ADD_INDEX] [-s] [-p] [-h] +``` + +Add option to the diagram. + +``` +optional arguments: + -i ADD_INDEX, --index ADD_INDEX + index of strike to be used in + the diagram. + -k ADD_STRIKE, --strike ADD_STRIKE + strike to be used in the diagram. + -s, --short short the option instead of buying it + -p, --put buy a put instead of a call + -h, --help show this help message +``` diff --git a/website/content/options/payoff/pick/_index.md b/website/content/options/payoff/pick/_index.md new file mode 100644 index 00000000000..673b3d66d62 --- /dev/null +++ b/website/content/options/payoff/pick/_index.md @@ -0,0 +1,11 @@ +``` +usage: pick [-t OPTION] [-h] +``` + +Choose to long, short, or not hold the underlying asset. + +``` +optional arguments: + -t, --type choose the action you want to perform + -h, --help show this help message (default: False) +``` diff --git a/website/content/options/payoff/plot/_index.md b/website/content/options/payoff/plot/_index.md new file mode 100644 index 00000000000..5b3954cba44 --- /dev/null +++ b/website/content/options/payoff/plot/_index.md @@ -0,0 +1,10 @@ +``` +usage: plot [-h] +``` + +Shows the options payoff diagram. + +``` +optional arguments: + -h, --help show this help message (default: False) +``` diff --git a/website/content/options/payoff/rmv/_index.md b/website/content/options/payoff/rmv/_index.md new file mode 100644 index 00000000000..ebabeaab8c5 --- /dev/null +++ b/website/content/options/payoff/rmv/_index.md @@ -0,0 +1,13 @@ +``` +usage: rmv [-k RMV_STRIKE] [-h] +``` + +Remove one of the options from the diagram. + +``` +optional arguments: + -k RMV_STRIKE, --strike RMV_STRIKE + remove option from diagram + -a, --all remove all options + -h, --help show this help message +``` diff --git a/website/data/menu/main.yml b/website/data/menu/main.yml index 9a4d1ddcc18..613c93db3ba 100755 --- a/website/data/menu/main.yml +++ b/website/data/menu/main.yml @@ -582,6 +582,17 @@ main: ref: "/options/grhist" - name: smile ref: "/options/smile" + - name: portfolio optimization + ref: "/portfolio/portfolio_optimization" + sub: + - name: add + ref: "/options/payoff/add" + - name: pick + ref: "/options/payoff/pick" + - name: plot + ref: "/options/payoff/plot" + - name: rmv + ref: "/options/payoff/rmv" - name: etf ref: "/etf" sub: