mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-29 05:21:48 +00:00
Refactor options menu
This commit is contained in:
@@ -9,7 +9,7 @@ import yfinance as yf
|
||||
from prompt_toolkit.completion import NestedCompleter
|
||||
from gamestonk_terminal.helper_funcs import get_flair, parse_known_args_and_warn
|
||||
from gamestonk_terminal import feature_flags as gtff
|
||||
from gamestonk_terminal.options import volume
|
||||
from gamestonk_terminal.options import volume_view
|
||||
from gamestonk_terminal.menu import session
|
||||
|
||||
|
||||
@@ -17,16 +17,15 @@ class OptionsController:
|
||||
"""Options Controller class."""
|
||||
|
||||
# Command choices
|
||||
CHOICES = ["help", "q", "quit", "exp", "volume"]
|
||||
CHOICES = ["help", "q", "quit", "exp", "voi"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
ticker: str,
|
||||
):
|
||||
def __init__(self, ticker: str, last_adj_close_price: float):
|
||||
"""Construct data."""
|
||||
self.ticker = ticker
|
||||
self.raw_data_options = yf.Ticker(self.ticker)
|
||||
self.expiry_date = self.raw_data_options.options[0]
|
||||
self.yf_ticker_data = yf.Ticker(self.ticker)
|
||||
self.expiry_date = self.yf_ticker_data.options[0]
|
||||
self.options = self.yf_ticker_data.option_chain(self.expiry_date)
|
||||
self.last_adj_close_price = last_adj_close_price
|
||||
self.op_parser = argparse.ArgumentParser(add_help=False, prog="op")
|
||||
self.op_parser.add_argument(
|
||||
"cmd",
|
||||
@@ -48,7 +47,7 @@ class OptionsController:
|
||||
action="store",
|
||||
type=int,
|
||||
default=-1,
|
||||
choices=range(len(self.raw_data_options.options)),
|
||||
choices=range(len(self.yf_ticker_data.options)),
|
||||
help=f"Expiry date index for {self.ticker}.",
|
||||
)
|
||||
|
||||
@@ -64,12 +63,13 @@ class OptionsController:
|
||||
# Print possible expiry dates
|
||||
if ns_parser.n_date == -1:
|
||||
print("\nAvailable expiry dates:")
|
||||
for i, d in enumerate(self.raw_data_options.options):
|
||||
for i, d in enumerate(self.yf_ticker_data.options):
|
||||
print(f" {(2-len(str(i)))*' '}{i}. {d}")
|
||||
|
||||
# It means an expiry date was correctly selected
|
||||
else:
|
||||
self.expiry_date = self.raw_data_options.options[ns_parser.n_date]
|
||||
self.expiry_date = self.yf_ticker_data.options[ns_parser.n_date]
|
||||
self.options = self.yf_ticker_data.option_chain(self.expiry_date)
|
||||
print(f"\nSelected expiry date : {self.expiry_date}")
|
||||
|
||||
except Exception as e:
|
||||
@@ -89,7 +89,7 @@ class OptionsController:
|
||||
print(f"Selected expiry date: {expiry_date}")
|
||||
print("")
|
||||
print(" exp see/set expiry date")
|
||||
print(" volume plot options trading volume / open interest")
|
||||
print(" voi volume + open interest options trading plot")
|
||||
print("")
|
||||
return
|
||||
|
||||
@@ -121,26 +121,27 @@ class OptionsController:
|
||||
"""Process Quit command - quit the program."""
|
||||
return True
|
||||
|
||||
def call_volume(self, _):
|
||||
"""Process volume command."""
|
||||
volume.volume_graph(
|
||||
self.raw_data_options,
|
||||
def call_voi(self, other_args: List[str]):
|
||||
"""Process voi command."""
|
||||
volume_view.volume_open_interest_graph(
|
||||
other_args,
|
||||
self.ticker,
|
||||
self.expiry_date,
|
||||
volume_percentile_threshold=60,
|
||||
self.last_adj_close_price,
|
||||
self.options.calls,
|
||||
self.options.puts,
|
||||
)
|
||||
print("")
|
||||
|
||||
def call_exp(self, other_args: List[str]):
|
||||
"""Process exp command."""
|
||||
self.expiry_dates(self, other_args)
|
||||
|
||||
|
||||
def menu(ticker: str):
|
||||
"""Options info Menu."""
|
||||
def menu(ticker: str, last_adj_close_price: float):
|
||||
""" Options Menu. """
|
||||
|
||||
try:
|
||||
op_controller = OptionsController(ticker)
|
||||
op_controller = OptionsController(ticker, last_adj_close_price)
|
||||
op_controller.call_help(None)
|
||||
except IndexError:
|
||||
print("No options found for " + ticker)
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
"""options info. [Source: Yahoo Finance]."""
|
||||
from bisect import bisect_left
|
||||
import seaborn as sns
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from gamestonk_terminal.helper_funcs import plot_autoscale
|
||||
from gamestonk_terminal import config_plot as cfgPlot
|
||||
|
||||
|
||||
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
|
||||
|
||||
TICKER_NAME = ticker_name
|
||||
raw_data_options = raw_data
|
||||
EXP_DATE = exp_date
|
||||
|
||||
# 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)
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
g = sns.barplot(
|
||||
x="volume",
|
||||
y="strike",
|
||||
data=calls[calls["oi+v"] > VOLUME_THRESHOLD],
|
||||
label="Calls: Volume",
|
||||
color="green",
|
||||
orient="h",
|
||||
)
|
||||
|
||||
g = sns.barplot(
|
||||
x="oi+v",
|
||||
y="strike",
|
||||
data=puts[puts["oi+v"] < -VOLUME_THRESHOLD],
|
||||
label="Puts: Open Interest",
|
||||
color="pink",
|
||||
orient="h",
|
||||
)
|
||||
|
||||
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 __calc_max_pain(calls, puts):
|
||||
|
||||
df = pd.merge(calls, puts, left_index=True, right_index=True)
|
||||
df["dv"] = round(df["dv_x"] + df["dv_y"], 2)
|
||||
|
||||
max_pain = df["dv"].idxmax()
|
||||
return max_pain
|
||||
|
||||
|
||||
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:
|
||||
option_data = opt.puts
|
||||
flag = "puts"
|
||||
|
||||
data = option_data.pivot_table(
|
||||
index="strike", values=["volume", "openInterest"], aggfunc="sum"
|
||||
).reindex()
|
||||
|
||||
data["strike"] = data.index
|
||||
data["type"] = flag
|
||||
|
||||
if is_calls:
|
||||
data["openInterest"] = data["openInterest"]
|
||||
data["volume"] = data["volume"]
|
||||
else:
|
||||
data["openInterest"] = -data["openInterest"]
|
||||
data["volume"] = -data["volume"]
|
||||
|
||||
data["oi+v"] = data["openInterest"] + data["volume"]
|
||||
return data
|
||||
|
||||
|
||||
def __get_current_spot(raw_data_options):
|
||||
return raw_data_options.history().tail(1)["Close"].iloc[0]
|
||||
273
gamestonk_terminal/options/volume_view.py
Normal file
273
gamestonk_terminal/options/volume_view.py
Normal file
@@ -0,0 +1,273 @@
|
||||
""" Volume view """
|
||||
__docformat__ = "numpy"
|
||||
|
||||
from bisect import bisect_left
|
||||
import seaborn as sns
|
||||
from typing import List
|
||||
import argparse
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import yfinance
|
||||
|
||||
from gamestonk_terminal.helper_funcs import (
|
||||
plot_autoscale,
|
||||
check_non_negative,
|
||||
parse_known_args_and_warn,
|
||||
)
|
||||
from gamestonk_terminal import config_plot as cfgPlot
|
||||
from gamestonk_terminal import feature_flags as gtff
|
||||
|
||||
|
||||
def volume_open_interest_graph(
|
||||
other_args: List[str],
|
||||
ticker: str,
|
||||
exp_date: str,
|
||||
last_adj_close_price: float,
|
||||
op_calls: pd.DataFrame,
|
||||
op_puts: pd.DataFrame,
|
||||
):
|
||||
"""Options volume and open interest
|
||||
|
||||
Parameters
|
||||
----------
|
||||
other_args : List[str]
|
||||
Command line arguments to be processed with argparse
|
||||
ticker : str
|
||||
Main ticker to compare income
|
||||
exp_date : str
|
||||
Expiry date of the option
|
||||
last_adj_close_price: float
|
||||
Last adjusted closing price
|
||||
op_calls: pd.DataFrame
|
||||
Option data calls
|
||||
op_puts: pd.DataFrame
|
||||
Option data puts
|
||||
"""
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
prog="voi",
|
||||
description="""
|
||||
Plots Volume + Open Interest of calls vs puts. [Source: Yahoo Finance]
|
||||
""",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v",
|
||||
"--minv",
|
||||
dest="min_vol",
|
||||
type=check_non_negative,
|
||||
default=-1,
|
||||
help="minimum volume (considering open interest) threshold of the plot.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
"--min",
|
||||
dest="min_sp",
|
||||
type=check_non_negative,
|
||||
default=-1,
|
||||
help="minimum strike price to consider in the plot.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-M",
|
||||
"--max",
|
||||
dest="max_sp",
|
||||
type=check_non_negative,
|
||||
default=-1,
|
||||
help="maximum strike price to consider in the plot.",
|
||||
)
|
||||
|
||||
try:
|
||||
ns_parser = parse_known_args_and_warn(parser, other_args)
|
||||
if not ns_parser:
|
||||
return
|
||||
|
||||
df_calls, df_puts, max_pain = get_calls_puts_maxpain(
|
||||
op_calls, op_puts, last_adj_close_price
|
||||
)
|
||||
|
||||
if (
|
||||
ns_parser.min_vol == -1
|
||||
and ns_parser.min_sp == -1
|
||||
and ns_parser.max_sp == -1
|
||||
):
|
||||
# If no argument provided, we use the percentile 50 to get 50% of upper volume data
|
||||
volume_percentile_threshold = 50
|
||||
min_vol_calls = np.percentile(df_calls["oi+v"], volume_percentile_threshold)
|
||||
min_vol_puts = np.percentile(df_puts["oi+v"], volume_percentile_threshold)
|
||||
|
||||
df_calls = df_calls[df_calls["oi+v"] > min_vol_calls]
|
||||
df_puts = df_puts[df_puts["oi+v"] < min_vol_puts]
|
||||
|
||||
else:
|
||||
if ns_parser.min_vol > -1:
|
||||
df_calls = df_calls[df_calls["oi+v"] > ns_parser.min_vol]
|
||||
df_puts = df_puts[df_puts["oi+v"] < -ns_parser.min_vol]
|
||||
|
||||
if ns_parser.min_sp > -1:
|
||||
df_calls = df_calls[df_calls["strike"] > ns_parser.min_sp]
|
||||
df_puts = df_puts[df_puts["strike"] > ns_parser.min_sp]
|
||||
|
||||
if ns_parser.max_sp > -1:
|
||||
df_calls = df_calls[df_calls["strike"] < ns_parser.max_sp]
|
||||
df_puts = df_puts[df_puts["strike"] < ns_parser.max_sp]
|
||||
|
||||
if df_calls.empty and df_puts.empty:
|
||||
print(
|
||||
"The filtering applied is too strong, there is no data available for such conditions.\n"
|
||||
)
|
||||
return
|
||||
|
||||
# Initialize the matplotlib figure
|
||||
_, ax = plt.subplots(figsize=plot_autoscale(), dpi=cfgPlot.PLOT_DPI)
|
||||
|
||||
# make x axis symmetric
|
||||
axis_origin = max(abs(max(df_puts["oi+v"])), abs(max(df_calls["oi+v"])))
|
||||
ax.set_xlim(-axis_origin, +axis_origin)
|
||||
|
||||
sns.set_style(style="darkgrid")
|
||||
|
||||
g = sns.barplot(
|
||||
x="oi+v",
|
||||
y="strike",
|
||||
data=df_calls,
|
||||
label="Calls: Open Interest",
|
||||
color="lightgreen",
|
||||
orient="h",
|
||||
)
|
||||
|
||||
g = sns.barplot(
|
||||
x="volume",
|
||||
y="strike",
|
||||
data=df_calls,
|
||||
label="Calls: Volume",
|
||||
color="green",
|
||||
orient="h",
|
||||
)
|
||||
|
||||
g = sns.barplot(
|
||||
x="oi+v",
|
||||
y="strike",
|
||||
data=df_puts,
|
||||
label="Puts: Open Interest",
|
||||
color="pink",
|
||||
orient="h",
|
||||
)
|
||||
|
||||
g = sns.barplot(
|
||||
x="volume",
|
||||
y="strike",
|
||||
data=df_puts,
|
||||
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, last_adj_close_price
|
||||
) # 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)
|
||||
|
||||
# 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} volumes for {exp_date} (open interest displayed only during market hours)"
|
||||
)
|
||||
ax.invert_yaxis()
|
||||
|
||||
_ = 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)
|
||||
|
||||
if gtff.USE_ION:
|
||||
plt.ion()
|
||||
plt.show()
|
||||
|
||||
print("")
|
||||
|
||||
except Exception as e:
|
||||
print(e, "\n")
|
||||
return
|
||||
|
||||
|
||||
def get_calls_puts_maxpain(
|
||||
op_calls: pd.DataFrame, op_puts: pd.DataFrame, last_adj_close_price: float
|
||||
):
|
||||
"""Get calls and puts dataframes, and max pain
|
||||
|
||||
Parameters
|
||||
----------
|
||||
op_calls: pd.DataFrame
|
||||
Option data calls
|
||||
op_puts: pd.DataFrame
|
||||
Option data puts
|
||||
last_adj_close_price: float
|
||||
Last adjusted closing price
|
||||
|
||||
Returns
|
||||
----------
|
||||
pd.DataFrame
|
||||
Processed calls dataframe
|
||||
pd.DataFrame
|
||||
Processed puts dataframe
|
||||
float
|
||||
Max pain
|
||||
"""
|
||||
# Process Calls Data
|
||||
df_calls = op_calls.pivot_table(
|
||||
index="strike", values=["volume", "openInterest"], aggfunc="sum"
|
||||
).reindex()
|
||||
df_calls["strike"] = df_calls.index
|
||||
df_calls["type"] = "calls"
|
||||
df_calls["openInterest"] = df_calls["openInterest"]
|
||||
df_calls["volume"] = df_calls["volume"]
|
||||
df_calls["oi+v"] = df_calls["openInterest"] + df_calls["volume"]
|
||||
df_calls["spot"] = round(last_adj_close_price, 2)
|
||||
df_calls["dv"] = last_adj_close_price - df_calls.index
|
||||
df_calls["dv"] = df_calls["dv"].apply(lambda x: max(0, x))
|
||||
df_calls["dv"] = abs(df_calls["dv"] * df_calls["volume"])
|
||||
|
||||
# Process Puts Data
|
||||
df_puts = op_puts.pivot_table(
|
||||
index="strike", values=["volume", "openInterest"], aggfunc="sum"
|
||||
).reindex()
|
||||
df_puts["strike"] = df_puts.index
|
||||
df_puts["type"] = "puts"
|
||||
df_puts["openInterest"] = df_puts["openInterest"]
|
||||
df_puts["volume"] = -df_puts["volume"]
|
||||
df_puts["openInterest"] = -df_puts["openInterest"]
|
||||
df_puts["oi+v"] = df_puts["openInterest"] + df_puts["volume"]
|
||||
df_puts["spot"] = round(last_adj_close_price, 2)
|
||||
df_puts["dv"] = df_puts.index - last_adj_close_price
|
||||
df_puts["dv"] = df_puts["dv"].apply(lambda x: max(0, x))
|
||||
df_puts["dv"] = abs(df_puts["dv"] * df_puts["volume"])
|
||||
|
||||
# Get max pain
|
||||
df_opt = pd.merge(df_calls, df_puts, left_index=True, right_index=True)
|
||||
df_opt["dv"] = round(df_opt["dv_x"] + df_opt["dv_y"], 2)
|
||||
max_pain = df_opt["dv"].idxmax()
|
||||
|
||||
return df_calls, df_puts, max_pain
|
||||
@@ -231,7 +231,9 @@ def main():
|
||||
b_quit = eda_controller.menu(df_stock, s_ticker, s_start, s_interval)
|
||||
|
||||
elif ns_known_args.opt == "op":
|
||||
b_quit = op_controller.menu(s_ticker)
|
||||
b_quit = op_controller.menu(
|
||||
s_ticker, df_stock["5. adjusted close"].values[-1]
|
||||
)
|
||||
|
||||
elif ns_known_args.opt == "fred":
|
||||
b_quit = fred_controller.menu()
|
||||
|
||||
Reference in New Issue
Block a user