diff --git a/gamestonk_terminal/options/op_controller.py b/gamestonk_terminal/options/op_controller.py new file mode 100644 index 00000000000..6c4ce0ff1fa --- /dev/null +++ b/gamestonk_terminal/options/op_controller.py @@ -0,0 +1,188 @@ +"""Options Controller Module.""" +__docformat__ = "numpy" + +import argparse +import matplotlib.pyplot as plt + +import yfinance as yf +from prompt_toolkit.completion import NestedCompleter +from gamestonk_terminal.helper_funcs import get_flair +from gamestonk_terminal import feature_flags as gtff +from gamestonk_terminal.options import volume +from gamestonk_terminal.menu import session + + +class OptionsController: + """Options Controller class.""" + + # Command choices + CHOICES = ["help", "q", "quit", "exp", "volume"] + + def __init__( + self, + ticker: str, + ): + """Construct data.""" + self.ticker = ticker + self.raw_data_options = yf.Ticker(self.ticker) + self.expiry_date = "" + + self.__set_exp_date() + + self.op_parser = argparse.ArgumentParser(add_help=False, prog="ca") + self.op_parser.add_argument( + "cmd", + choices=self.CHOICES, + ) + + def __set_exp_date(self, date_index=0): + self.expiry_date = self.raw_data_options.options[date_index] + return + + @staticmethod + def print_exp_dates(expiry_date, all_dates): + """Print all available expiry dates.""" + print(f"Current selected expiry date: [{expiry_date}]") + print("\nAvailable dates:\n") + for i, d in enumerate(all_dates): + if len(str(i)) == 1: + spacer = " " + else: + spacer = "" + print(f" [{i}] {spacer}{d}") + print( + f"\nPlease select date: input a number between 0 and {len(all_dates)-1}\n" + ) + return + + @staticmethod + def print_help(expiry_date): + """Print help.""" + print("\nOptions insight Mode:") + print(" help show this menu again") + print(" q quit this menu, and shows back to main menu") + print(" quit quit to abandon program") + print("") + + print( + f" exp set expiry date, current selected date : [{expiry_date}]" + ) + print(" volume plot options trading volume / open interest") + print("") + return + + 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 + """ + (known_args, other_args) = self.op_parser.parse_known_args(an_input.split()) + + return getattr( + self, "call_" + known_args.cmd, lambda: "Command not recognized!" + )(other_args) + + def call_help(self, _): + """Process Help command.""" + self.print_help(self.expiry_date) + + 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_volume(self, _): + """Process volume command.""" + volume.volume_graph( + self.raw_data_options, + self.ticker, + self.expiry_date, + volume_percentile_threshold=60, + ) + print("") + + def call_exp(self, _): + """Process exp command.""" + self.print_exp_dates(self.expiry_date, self.raw_data_options.options) + + date_index_choices = list(range(len(self.raw_data_options.options) - 1)) + while True: + # Get input command from user + if session and gtff.USE_PROMPT_TOOLKIT: + completer = NestedCompleter.from_nested_dict( + {c: None for c in date_index_choices} + ) + an_input = session.prompt( + f"{get_flair()} (exp)> ", + completer=completer, + ) + else: + an_input = input(f"{get_flair()} (exp)> ") + + try: + plt.close("all") + + process_input = an_input + + if process_input is not None: + try: + self.__set_exp_date(int(an_input)) + print(f"\nNew selected expiry date : [{self.expiry_date}]\n") + return + # menu(self.ticker, self) + except IndexError: + print( + f"\nSelection must be between [0 - {len(self.raw_data_options.options) - 1}]\n" + ) + continue + except ValueError: + print("The command selected doesn't exist\n") + self.print_exp_dates( + self.expiry_date, self.raw_data_options.options + ) + + except SystemExit: + print("The command selected doesn't exist\n") + continue + + +def menu(ticker: str, op_con=None): + """Options info Menu.""" + if op_con is None: + op_controller = OptionsController(ticker) + else: + op_controller = op_con + op_controller.call_help("") + + while True: + # Get input command from user + if session and gtff.USE_PROMPT_TOOLKIT: + completer = NestedCompleter.from_nested_dict( + {c: None for c in op_controller.CHOICES} + ) + an_input = session.prompt( + f"{get_flair()} (op)> ", + completer=completer, + ) + else: + an_input = input(f"{get_flair()} (op)> ") + + try: + plt.close("all") + + process_input = op_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/op_menu.py b/gamestonk_terminal/options/op_menu.py deleted file mode 100644 index a386a584ec2..00000000000 --- a/gamestonk_terminal/options/op_menu.py +++ /dev/null @@ -1,59 +0,0 @@ -import argparse - -from prompt_toolkit.completion import NestedCompleter -from gamestonk_terminal import feature_flags as gtff -from gamestonk_terminal.helper_funcs import get_flair -from gamestonk_terminal.options import volume as vol -from gamestonk_terminal.menu import session - - -def opt_menu(s_ticker): - - # Add list of arguments that the options parser accepts - opt_parser = argparse.ArgumentParser(prog="opt", add_help=False) - choices = ["help", "q", "quit", "volume", "oi"] - opt_parser.add_argument("cmd", choices=choices) - completer = NestedCompleter.from_nested_dict({c: None for c in choices}) - - # print_options(s_ticker, s_start, s_interval) - - # Loop forever and ever - while True: - # Get input command from user - if session and gtff.USE_PROMPT_TOOLKIT: - as_input = session.prompt( - f"{get_flair()} (opt)> ", - completer=completer, - ) - else: - as_input = input(f"{get_flair()} (opt)> ") - - # Parse fundamental analysis command of the list of possible commands - try: - (ns_known_args, l_args) = opt_parser.parse_known_args(as_input.split()) - - except SystemExit: - print("The command selected doesn't exist\n") - continue - - if ns_known_args.cmd == "help": - pass - # print_options(s_ticker) - - elif ns_known_args.cmd == "q": - # Just leave the options menu - return False - - elif ns_known_args.cmd == "quit": - # Abandon the program - return True - - elif ns_known_args.cmd == "volume": - # call the volume graph - vol.volume_graph(l_args, s_ticker) - - elif ns_known_args.cmd == "oi": - # call the volume graph - vol.open_interest_graph(l_args, s_ticker) - else: - print("Command not recognized!") diff --git a/gamestonk_terminal/options/volume.py b/gamestonk_terminal/options/volume.py index 14440ef101e..3b87ac8efe7 100644 --- a/gamestonk_terminal/options/volume.py +++ b/gamestonk_terminal/options/volume.py @@ -1,144 +1,176 @@ -import argparse -import yfinance as yf +"""options info. [Source: Yahoo Finance].""" +from bisect import bisect_left import seaborn as sns import pandas as pd import matplotlib.pyplot as plt -import plotly.express as px -from gamestonk_terminal.helper_funcs import ( - parse_known_args_and_warn, - valid_date, -) +import numpy as np + +from gamestonk_terminal.helper_funcs import plot_autoscale +from gamestonk_terminal import config_plot as cfgPlot -def open_interest_graph(l_args, s_ticker): - """ Show traded options volume. [Source: Yahoo Finance] """ - parser = argparse.ArgumentParser( - add_help=False, - prog="volume", - description="""Display volume graph for a date. [Source: Yahoo Finance].""", - ) - parser.add_argument( - "-e", - "--expiry", - type=valid_date, - dest="s_expiry_date", - help="The expiry date (format YYYY-MM-DD) for the option chain", - required=True, - ) - try: - ns_parser = parse_known_args_and_warn(parser, l_args) - if not ns_parser: - return +def volume_graph(raw_data, ticker_name, exp_date, volume_percentile_threshold=50): + """Docstring make linter hap.""" + # SET VOLUME TO BE FILTERED, default = 50 + PERCENTILE_THRESHOLD = volume_percentile_threshold - __get_open_interest_graph( - s_ticker, ns_parser.s_expiry_date.strftime("%Y-%m-%d") - ) + TICKER_NAME = ticker_name + raw_data_options = raw_data + EXP_DATE = exp_date - except SystemExit: - print("") - except Exception as e: - print(e) + # current stock price + spot = __get_current_spot(raw_data_options) + calls = __parse_opt_data(raw_data_options, EXP_DATE) + puts = __parse_opt_data(raw_data_options, EXP_DATE, is_calls=False) -def volume_graph(l_args, s_ticker): - """ Show traded options volume. [Source: Yahoo Finance] """ - parser = argparse.ArgumentParser( - add_help=False, - prog="volume", - description="""Display volume graph for a date. [Source: Yahoo Finance].""", - ) - parser.add_argument( - "-e", - "--expiry", - type=valid_date, - dest="s_expiry_date", - help="The expiry date (format YYYY-MM-DD) for the option chain", - required=True, + calls = __add_max_pain_data(calls, spot) + + puts = __add_max_pain_data(puts, spot, is_calls=False) + + max_pain = __calc_max_pain(calls, puts) + + # Initialize the matplotlib figure + _, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfgPlot.PLOT_DPI) + # _, ax = plt.subplots(figsize=(12, 10)) + + # make x axis symmetric + axis_origin = max(abs(max(puts["oi+v"])), abs(max(calls["oi+v"]))) + ax.set_xlim(-axis_origin, +axis_origin) + + VOLUME_THRESHOLD = np.percentile(calls["oi+v"], PERCENTILE_THRESHOLD) + + sns.set_style(style="darkgrid") + + g = sns.barplot( + x="oi+v", + y="strike", + data=calls[calls["oi+v"] > VOLUME_THRESHOLD], + label="Calls: Open Interest", + color="lightgreen", + orient="h", ) - try: - ns_parser = parse_known_args_and_warn(parser, l_args) - if not ns_parser: - return - - __get_volume_graph(s_ticker, ns_parser.s_expiry_date.strftime("%Y-%m-%d")) - - except SystemExit: - print("") - except Exception as e: - print(e) - - -def __get_open_interest_graph(ticker_name, exp_date): - df = __get_volume_data(ticker_name, exp_date) - __generate_graph_sns(df, ticker_name, exp_date, True) - # __generate_graph_plotly(df, ticker_name, exp_date) - - -def __get_volume_graph(ticker_name, exp_date): - df = __get_volume_data(ticker_name, exp_date) - __generate_graph_sns(df, ticker_name, exp_date) - # __generate_graph_plotly(df, ticker_name, exp_date) - - -def __pull_call_put_data(call_put, flag): - df = call_put.pivot_table( - index="strike", values=["volume", "openInterest"], aggfunc="sum" + g = sns.barplot( + x="volume", + y="strike", + data=calls[calls["oi+v"] > VOLUME_THRESHOLD], + label="Calls: Volume", + color="green", + orient="h", ) - df.reindex() + g = sns.barplot( + x="oi+v", + y="strike", + data=puts[puts["oi+v"] < -VOLUME_THRESHOLD], + label="Puts: Open Interest", + color="pink", + orient="h", + ) - df["strike"] = df.index - df["type"] = flag + g = sns.barplot( + x="volume", + y="strike", + data=puts[puts["oi+v"] < -VOLUME_THRESHOLD], + label="Puts: Volume", + color="red", + orient="h", + ) + # draw spot line + s = [float(strike.get_text()) for strike in ax.get_yticklabels()] + spot_index = bisect_left(s, spot) # find where the spot is on the graph + spot_line = ax.axhline(spot_index, ls="--", color="dodgerblue", alpha=0.3) + + # draw max pain line + max_pain_index = bisect_left(s, max_pain) + max_pain_line = ax.axhline(max_pain_index, ls="-", color="black", alpha=0.3) + max_pain_line.set_linewidth(5) + + # ax.axhline(max_pain_index, ls='--') + # format ticklabels without - for puts + g.set_xticks(g.get_xticks()) + xlabels = [f"{x:,.0f}".replace("-", "") for x in g.get_xticks()] + g.set_xticklabels(xlabels) + + plt.title( + f"{TICKER_NAME.upper()} volumes for {EXP_DATE} (open interest displayed only during market hours)" + ) + ax.invert_yaxis() + + # ax.spines['left'].set_position('center') + + _ = ax.legend() + handles, _ = ax.get_legend_handles_labels() + handles.append(spot_line) + handles.append(max_pain_line) + + # create legend labels + add to graph + labels = [ + "Calls open interest", + "Calls volume ", + "Puts open interest", + "Puts volume", + "Current stock price", + f"Max pain = {max_pain}", + ] + + plt.legend(handles=handles[:], labels=labels) + sns.despine(left=True, bottom=True) + plt.show() + + +def __add_max_pain_data(df, spot, is_calls=True): + # max pain parsing + calculation + df["spot"] = round(spot, 2) + if is_calls: + df["dv"] = spot - df.index + else: + df["dv"] = df.index - spot + df["dv"] = df["dv"].apply(lambda x: max(0, x)) + df["dv"] = abs(df["dv"] * df["volume"]) return df -def __get_volume_data(ticker_name, exp_date): +def __calc_max_pain(calls, puts): - option_chain = yf.Ticker(ticker_name).option_chain(exp_date) + df = pd.merge(calls, puts, left_index=True, right_index=True) + df["dv"] = round(df["dv_x"] + df["dv_y"], 2) - calls = __pull_call_put_data(option_chain.calls, "calls") - - puts = __pull_call_put_data(option_chain.puts, "puts") - - volume_data = pd.concat([calls, puts], axis=0) - # dataframe - return volume_data + max_pain = df["dv"].idxmax() + return max_pain -def __generate_graph_plotly(df, ticker_name, exp_date, for_open_interest=False): - # version with plotly express - if for_open_interest: - op_type = "openInterest" +def __parse_opt_data(raw_data_options, exp_date, is_calls=True): + # get option chain for specific expiration + opt = raw_data_options.option_chain(exp_date) + + # PARSE DATA + if is_calls: + option_data = opt.calls + flag = "calls" else: - op_type = "volume" + option_data = opt.puts + flag = "puts" - fig = px.line( - df, - x="strike", - y=op_type, - title=f"{ticker_name} options {op_type} for {exp_date}", - color="type", - ) - fig.show() + data = option_data.pivot_table( + index="strike", values=["volume", "openInterest"], aggfunc="sum" + ).reindex() - return + data["strike"] = data.index + data["type"] = flag - -def __generate_graph_sns(df, ticker_name, exp_date, for_open_interest=False): - # version with seaborn express - if for_open_interest: - op_type = "openInterest" + if is_calls: + data["openInterest"] = data["openInterest"] + data["volume"] = data["volume"] else: - op_type = "volume" + data["openInterest"] = -data["openInterest"] + data["volume"] = -data["volume"] - plt.figure(figsize=(12, 6)) - fig = sns.lineplot( - data=df, x="strike", y=op_type, hue="type", palette=["limegreen", "tomato"] - ) + data["oi+v"] = data["openInterest"] + data["volume"] + return data - plt.title(f"{ticker_name} options {op_type} for {exp_date}") - plt.show() - return fig +def __get_current_spot(raw_data_options): + return raw_data_options.history().tail(1)["Close"].iloc[0] diff --git a/terminal.py b/terminal.py index 071e6c0f3c7..4199658323c 100644 --- a/terminal.py +++ b/terminal.py @@ -23,7 +23,7 @@ from gamestonk_terminal.papermill import papermill_controller as mill from gamestonk_terminal.behavioural_analysis import ba_controller from gamestonk_terminal.technical_analysis import ta_menu as tam from gamestonk_terminal.comparison_analysis import ca_controller -from gamestonk_terminal.options import op_menu as opm +from gamestonk_terminal.options import op_controller from gamestonk_terminal.fred import fred_menu as fm from gamestonk_terminal.portfolio import port_controller @@ -32,12 +32,13 @@ from gamestonk_terminal.portfolio import port_controller # pylint: disable=too-many-branches + + def main(): """ Gamestonk Terminal is an awesome stock market terminal that has been developed for fun, while I saw my GME shares tanking. But hey, I like the stock. """ - # Enable VT100 Escape Sequence for WINDOWS 10 Ver. 1607 if sys.platform == "win32": os.system("") @@ -200,7 +201,7 @@ def main(): b_quit = ddm.dd_menu(df_stock, s_ticker, s_start, s_interval) elif ns_known_args.opt == "op": - b_quit = opm.opt_menu(s_ticker) + b_quit = op_controller.menu(s_ticker) elif ns_known_args.opt == "fred": b_quit = fm.fred_menu()