mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
* add options_chains_view * vsurf charts * skew for strike vs contract * doc string * add chart_volatility * linting * codespell * mypy * types * tmx mypy * update test file * add synthetic long/short to strategies * updates test_options_chains_model.py * updates some docstrings * update documentation * actually commit the documentation page * update documentation screenshots * try catch for 403 error * add tests for options_chains_view * int64 * SPX error handling * add cboe options dashboard for terminal * codespell * enable mouse scroll wheel zoom for charts * declare key values for all widgets * add some more streamlit charts and tables * fix test_yfinance_view.py * conflict resolution * test --------- Co-authored-by: James Maslek <jmaslek11@gmail.com>
1012 lines
34 KiB
Python
1012 lines
34 KiB
Python
# mypy: disable-error-code=arg-type
|
|
|
|
# IMPORTATION STANDARD
|
|
import logging
|
|
import os
|
|
|
|
# IMPORTATION THIRDPARTY
|
|
from copy import deepcopy
|
|
from typing import Optional, Union
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
from scipy.spatial import Delaunay
|
|
|
|
# IMPORTATION INTERNAL
|
|
from openbb_terminal import OpenBBFigure
|
|
from openbb_terminal.decorators import log_start_end
|
|
from openbb_terminal.helper_funcs import export_data, print_rich_table
|
|
from openbb_terminal.stocks.options import options_chains_model
|
|
from openbb_terminal.stocks.options.op_helpers import Options
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# pylint: disable=C0302,R0913
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def display_surface(
|
|
options: Options,
|
|
option_type: str = "otm",
|
|
dte_range: Optional[list[int]] = None,
|
|
moneyness: Optional[float] = None,
|
|
strike_range: Optional[list[float]] = None,
|
|
oi: bool = False,
|
|
volume: bool = False,
|
|
raw: bool = False,
|
|
export: str = "",
|
|
sheet_name: Optional[str] = "",
|
|
external_axes: bool = False,
|
|
) -> Union[None, OpenBBFigure]:
|
|
"""Chart the volatility as a 3-D surface.
|
|
|
|
Parameters
|
|
-----------
|
|
options: Options
|
|
The options data object.
|
|
option_type: str
|
|
The type of data to display. Default is "otm".
|
|
Choices are: ["otm", "itm", "puts", "calls"]
|
|
dte_range: list[int]
|
|
Specify a min/max range of DTE to display.
|
|
moneyness: float
|
|
Specify a % moneyness to target for display.
|
|
strike_range: list[float]
|
|
Specify a min/max range of strike prices to display.
|
|
oi: bool
|
|
Filter for only options that have open interest. Default is False.
|
|
volume: bool
|
|
Filter for only options that have trading volume. Default is False.
|
|
raw: bool
|
|
Display the raw data instead of the chart.
|
|
export: str
|
|
Export dataframe data to csv,json,xlsx file.
|
|
external_axes: bool
|
|
Return the OpenBB Figure Object to a variable.
|
|
|
|
Examples
|
|
----------
|
|
>>> from openbb_terminal.stocks.options import options_chains_model, options_chains_view
|
|
>>> spy = options_chains_model.load_options_chains("SPY")
|
|
>>> options_chains_view.display_surface(spy)
|
|
|
|
Display only calls:
|
|
>>> options_chains_view.display_surface(spy, "calls")
|
|
|
|
Display only puts:
|
|
>>> options_chains_view.display_surface(spy, "puts")
|
|
|
|
Display a range of expirations:
|
|
>>> options_chains_view.display_surface(spy, dte_range=[7, 60])
|
|
|
|
Filter for a range of strike prices and include only those with open interest and trading volume:
|
|
>>> options_chains_view.display_surface(spy, strike_range=[400, 500], oi=True, volume=True)
|
|
"""
|
|
|
|
if options_chains_model.validate_object(options, "object") is False:
|
|
return None
|
|
|
|
if options.hasIV is False:
|
|
return print(
|
|
"Options data object does not have Implied Volatility and is required for this function."
|
|
)
|
|
|
|
options = deepcopy(options)
|
|
options.chains = options_chains_model.validate_object(options.chains, "chains")
|
|
|
|
last_price = options.last_price # noqa:F841
|
|
|
|
calls = options.chains.query(
|
|
"`optionType` == 'call' & `dte` >= 0 & `impliedVolatility` > 0"
|
|
)
|
|
puts = options.chains.query(
|
|
"`optionType` == 'put' & `dte` >= 0 & `impliedVolatility` > 0"
|
|
)
|
|
|
|
if option_type not in ["otm", "itm", "puts", "calls"]:
|
|
print("Invalid option type, defaulting to 'otm'.")
|
|
option_type = "otm"
|
|
|
|
if oi:
|
|
calls = calls[calls["openInterest"] > 0]
|
|
puts = puts[puts["openInterest"] > 0]
|
|
|
|
if volume:
|
|
calls = calls[calls["volume"] > 0]
|
|
puts = puts[puts["volume"] > 0]
|
|
|
|
if dte_range is not None:
|
|
dte_range = [dte_range] if isinstance(dte_range, int) else dte_range # type: ignore [list-item]
|
|
if len(dte_range) > 1:
|
|
far = ( # noqa:F841 pylint: disable=unused-variable
|
|
dte_range[1] if dte_range[1] > dte_range[0] else dte_range[0]
|
|
)
|
|
near = ( # noqa:F841 pylint: disable=unused-variable
|
|
dte_range[0] if dte_range[1] > dte_range[0] else dte_range[1]
|
|
)
|
|
calls = calls.query("@near <= `dte` <= @far")
|
|
puts = puts.query("@near <= `dte` <= @far")
|
|
|
|
if moneyness is not None and moneyness > 0:
|
|
high = ( # noqa:F841 pylint: disable=unused-variable
|
|
1 + (moneyness / 100)
|
|
) * last_price
|
|
low = ( # noqa:F841 pylint: disable=unused-variable
|
|
1 - (moneyness / 100)
|
|
) * last_price
|
|
calls = calls.query("@low <= `strike` <= @high")
|
|
puts = puts.query("@low <= `strike` <= @high")
|
|
|
|
if strike_range is not None and len(strike_range) > 1:
|
|
high = ( # noqa:F841 pylint: disable=unused-variable
|
|
strike_range[1] if strike_range[1] > strike_range[0] else strike_range[0]
|
|
)
|
|
low = ( # noqa:F841 pylint: disable=unused-variable
|
|
strike_range[0] if strike_range[1] > strike_range[0] else strike_range[1]
|
|
)
|
|
calls = calls.query("@low <= `strike` <= @high")
|
|
puts = puts.query("@low <= `strike` <= @high")
|
|
|
|
if option_type == "otm":
|
|
otm_calls = calls.query("`strike` > @last_price").set_index(
|
|
["expiration", "strike", "optionType"]
|
|
)
|
|
otm_puts = puts.query("`strike` < @last_price").set_index(
|
|
["expiration", "strike", "optionType"]
|
|
)
|
|
data = pd.concat([otm_calls, otm_puts]).sort_index().reset_index()
|
|
|
|
if option_type == "itm":
|
|
itm_calls = calls.query("`strike` < @last_price").set_index(
|
|
["expiration", "strike", "optionType"]
|
|
)
|
|
itm_puts = puts.query("`strike` > @last_price").set_index(
|
|
["expiration", "strike", "optionType"]
|
|
)
|
|
data = pd.concat([itm_calls, itm_puts]).sort_index().reset_index()
|
|
|
|
if option_type == "calls":
|
|
data = calls
|
|
if option_type == "puts":
|
|
data = puts
|
|
|
|
data = data[
|
|
[
|
|
"expiration",
|
|
"strike",
|
|
"optionType",
|
|
"dte",
|
|
"impliedVolatility",
|
|
"openInterest",
|
|
"volume",
|
|
]
|
|
]
|
|
cols = ["Expiration", "Strike", "Type", "DTE", "IV", "OI", "Volume"]
|
|
data.columns = cols
|
|
|
|
label_dict = {"calls": "Call", "puts": "Put", "otm": "OTM", "itm": "ITM"}
|
|
label = (
|
|
label_dict[option_type] + " Options IV Surface"
|
|
if not oi
|
|
else label_dict[option_type] + " Options IV Surface With Open Interest"
|
|
)
|
|
label = label + " Excluding Untraded Contracts" if volume else label
|
|
|
|
X = data["DTE"]
|
|
Y = data["Strike"]
|
|
Z = data["IV"]
|
|
|
|
points3D = np.vstack((X, Y, Z)).T
|
|
points2D = points3D[:, :2]
|
|
tri = Delaunay(points2D)
|
|
II, J, K = tri.simplices.T
|
|
|
|
fig = OpenBBFigure()
|
|
fig.set_title(f"{options.symbol} {label}")
|
|
fig_kwargs = dict(z=Z, x=X, y=Y, i=II, j=J, k=K, intensity=Z)
|
|
fig.add_mesh3d(
|
|
**fig_kwargs,
|
|
alphahull=0,
|
|
opacity=1,
|
|
contour=dict(color="black", show=True, width=15),
|
|
colorscale=[
|
|
[0, "darkred"],
|
|
[0.001, "crimson"],
|
|
[0.005, "red"],
|
|
[0.0075, "orangered"],
|
|
[0.015, "darkorange"],
|
|
[0.025, "orange"],
|
|
[0.04, "goldenrod"],
|
|
[0.055, "gold"],
|
|
[0.11, "magenta"],
|
|
[0.15, "plum"],
|
|
[0.4, "lightblue"],
|
|
[0.7, "royalblue"],
|
|
[0.9, "blue"],
|
|
[1, "darkblue"],
|
|
],
|
|
hovertemplate="<b>DTE</b>: %{x} <br><b>Strike</b>: %{y} <br><b>"
|
|
+ "IV"
|
|
+ "</b>: %{z}<extra></extra>",
|
|
showscale=True,
|
|
flatshading=True,
|
|
lighting=dict(
|
|
ambient=0.95,
|
|
diffuse=0.9,
|
|
roughness=0.8,
|
|
specular=0.9,
|
|
fresnel=0.001,
|
|
vertexnormalsepsilon=0.0001,
|
|
facenormalsepsilon=0.0001,
|
|
),
|
|
)
|
|
tick_kwargs = dict(tickfont=dict(size=12), titlefont=dict(size=14))
|
|
fig.update_layout(
|
|
scene=dict(
|
|
xaxis=dict(title="DTE", autorange="reversed", **tick_kwargs),
|
|
yaxis=dict(title="Strike", **tick_kwargs),
|
|
zaxis=dict(title="IV", **tick_kwargs),
|
|
),
|
|
title_x=0.5,
|
|
scene_camera=dict(
|
|
up=dict(x=0, y=0, z=0.75),
|
|
center=dict(x=-0.01, y=0, z=-0.3),
|
|
eye=dict(x=1.75, y=1.75, z=0.69),
|
|
),
|
|
)
|
|
fig.update_scenes(aspectmode="manual", aspectratio=dict(x=1.5, y=2.0, z=0.75))
|
|
|
|
if raw:
|
|
print_rich_table(
|
|
data,
|
|
title=label,
|
|
show_index=False,
|
|
export=bool(export),
|
|
)
|
|
return None
|
|
|
|
if export and export != "":
|
|
export_data(
|
|
export,
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
"surface",
|
|
data,
|
|
sheet_name,
|
|
fig,
|
|
)
|
|
return None
|
|
|
|
return fig.show(external=raw or external_axes)
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def display_stats(
|
|
options: Options,
|
|
by: str = "expiration",
|
|
expiry: str = "",
|
|
oi: Optional[bool] = False,
|
|
percent: Optional[bool] = True,
|
|
ratios: Optional[bool] = False,
|
|
raw: bool = False,
|
|
export: str = "",
|
|
sheet_name: Optional[str] = "",
|
|
external_axes: bool = False,
|
|
) -> Union[None, OpenBBFigure]:
|
|
"""Chart a variety of volume and open interest statistics.
|
|
|
|
Parameters
|
|
-----------
|
|
options: Options
|
|
The options data object.
|
|
by: str
|
|
Statistics can be displayed by either "expiration" or "strike". Default is "expiration".
|
|
expiry: str
|
|
The target expiration date to display. Only valid when `percent` is False.
|
|
oi: bool
|
|
Display open interest if True, else volume. Default is False.
|
|
percent: bool
|
|
Displays volume or open interest as a percentage of the total across all expirations. Default is False.
|
|
ratios: bool
|
|
Displays Put/Call ratios. This parameter overrides the others when True. Default is False.
|
|
raw: bool
|
|
Displays the raw data table instead of a chart.
|
|
export: str
|
|
Export the data to a csv,json,xlsx file.
|
|
sheet_name: str
|
|
Name of the sheet to save the data to. Only valid when `export` is a `xlsx` file.
|
|
external_axes: bool
|
|
Return the OpenBB Figure Object to a variable.
|
|
|
|
Examples
|
|
----------
|
|
>>> from openbb_terminal.sdk import openbb
|
|
>>> from openbb_terminal.stocks.options import options_chains_model, options_chains_view
|
|
>>> spy = options_chains_model.load_options_chains("SPY")
|
|
|
|
Display volume by expiration:
|
|
>>> options_chains_view.display_stats(spy)
|
|
|
|
Display volume by strike:
|
|
>>> options_chains_view.display_stats(spy, "strike")
|
|
|
|
Display open interest by expiration:
|
|
>>> options_chains_view.display_stats(spy, oi=True)
|
|
|
|
Display open interest, by expiration, as a percentage of the total:
|
|
>>> options_chains_view.display_stats(spy, oi=True, percent=True)
|
|
|
|
Display volume and open interest put/call ratios:
|
|
>>> options_chains_view.display_stats(spy, ratios=True)
|
|
"""
|
|
|
|
by_type = ["expiration", "strike"]
|
|
if by not in by_type:
|
|
by = "expiration"
|
|
|
|
if options_chains_model.validate_object(options, "object") is False:
|
|
return None
|
|
options = deepcopy(options)
|
|
stats = options_chains_model.calculate_stats(options, by=by)
|
|
|
|
if by == "strike" and expiry != "" and percent is False:
|
|
expiry = options_chains_model.get_nearest_expiration(options, expiry)
|
|
stats = options_chains_model.calculate_stats(
|
|
options.chains[options.chains["expiration"] == expiry], "strike"
|
|
)
|
|
|
|
net_volume = stats.sum()["Total Volume"]
|
|
net_oi = stats.sum()["Total OI"]
|
|
|
|
stat_type = "Volume" if oi is False else "Open Interest"
|
|
index_name = "Expiration" if by == "expiration" else "Strike"
|
|
|
|
stats["Puts OI"] = stats["Puts OI"] * (-1)
|
|
stats["Puts Volume"] = stats["Puts Volume"] * (-1)
|
|
expiry = "" if oi is True and by == "expiration" else expiry
|
|
title = (
|
|
f"Percentage of Total {options.symbol} {stat_type}"
|
|
if percent is True
|
|
else f"{options.symbol} {stat_type}"
|
|
)
|
|
xtitle = (
|
|
f"{index_name}s With {stat_type} > 0.5% of Total {stat_type}"
|
|
if percent is True
|
|
else expiry
|
|
)
|
|
ytitle = f"% of Total {stat_type}" if percent is True else stat_type
|
|
|
|
fig = OpenBBFigure()
|
|
if ratios is False:
|
|
if percent is True:
|
|
stats["% of Total Volume"] = round(
|
|
stats["Total Volume"] / net_volume * 100, 4
|
|
)
|
|
stats["% of Total OI"] = round(stats["Total OI"] / net_oi * 100, 4)
|
|
stats_df = (
|
|
stats.query("`% of Total Volume` > 0.5")
|
|
if oi is False
|
|
else stats.query("`% of Total OI` > 0.5")
|
|
)
|
|
fig.add_bar(
|
|
x=stats_df.index,
|
|
y=stats_df["% of Total Volume"]
|
|
if oi is False
|
|
else stats_df["% of Total OI"],
|
|
name=f"% of Total {stat_type}",
|
|
orientation="v",
|
|
)
|
|
if percent is False:
|
|
fig.add_bar(
|
|
x=stats.index,
|
|
y=stats["Puts Volume"] if oi is False else stats["Puts OI"],
|
|
name="Puts",
|
|
orientation="v",
|
|
marker=dict(color="red"),
|
|
)
|
|
fig.add_bar(
|
|
x=stats.index,
|
|
y=stats["Calls Volume"] if oi is False else stats["Calls OI"],
|
|
name="Calls",
|
|
orientation="v",
|
|
marker=dict(color="blue"),
|
|
)
|
|
fig.update_layout(
|
|
legend=dict(
|
|
orientation="h",
|
|
yanchor="top",
|
|
y=1.125,
|
|
xanchor="left",
|
|
x=0.01,
|
|
)
|
|
)
|
|
if ratios is True:
|
|
title = f"{options.symbol} Ratios"
|
|
ytitle = ""
|
|
xtitle = ""
|
|
if by != "expiration":
|
|
by = "expiration"
|
|
stats = options_chains_model.calculate_stats(options, by)
|
|
fig.add_scatter(
|
|
x=stats.index,
|
|
y=stats["OI Ratio"].dropna(),
|
|
mode="lines+markers",
|
|
name="Put/Call OI Ratio",
|
|
marker=dict(color="blue"),
|
|
)
|
|
fig.add_scatter(
|
|
x=stats.index,
|
|
y=stats["Vol-OI Ratio"].dropna(),
|
|
mode="lines+markers",
|
|
name="Volume/OI Ratio",
|
|
marker=dict(color="red"),
|
|
)
|
|
fig.add_scatter(
|
|
x=stats.index,
|
|
y=stats["Volume Ratio"].dropna(),
|
|
mode="lines+markers",
|
|
name="Put/Call Volume Ratio",
|
|
marker=dict(color="orange"),
|
|
)
|
|
|
|
fig.update_xaxes(type="category")
|
|
fig.update_traces(width=0.98, selector=dict(type="bar"))
|
|
fig.update_layout(
|
|
title=dict(text=title, x=0.5, y=0.97),
|
|
barmode="overlay",
|
|
bargap=0,
|
|
bargroupgap=0,
|
|
yaxis=dict(
|
|
title=dict(text=ytitle, font=dict(size=16)),
|
|
ticklen=0,
|
|
showgrid=True,
|
|
tickfont=dict(size=14),
|
|
),
|
|
xaxis=dict(
|
|
title=dict(text=xtitle, font=dict(size=16)),
|
|
showgrid=False,
|
|
autorange=True,
|
|
tickangle=90,
|
|
tickfont=dict(size=11),
|
|
),
|
|
)
|
|
if raw:
|
|
print_rich_table(
|
|
stats,
|
|
title=(
|
|
f"{options.symbol} Stats"
|
|
if expiry == "" or percent is True
|
|
else f"{options.symbol} for {expiry} Expiration"
|
|
),
|
|
show_index=True,
|
|
index_name=index_name,
|
|
floatfmt=".4f",
|
|
export=bool(export),
|
|
)
|
|
return None
|
|
|
|
if export and export != "":
|
|
export_data(
|
|
export,
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
"stats",
|
|
stats,
|
|
sheet_name,
|
|
fig,
|
|
)
|
|
return None
|
|
|
|
return fig.show(external=raw or external_axes)
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def display_skew(
|
|
options: Options,
|
|
expirations: Optional[list[str]] = None,
|
|
moneyness: Optional[float] = None,
|
|
strike: Optional[float] = None,
|
|
atm: Optional[bool] = False,
|
|
otm_only: Optional[bool] = False,
|
|
raw: Optional[bool] = False,
|
|
export: Optional[str] = "",
|
|
sheet_name: Optional[str] = "",
|
|
external_axes: Optional[bool] = False,
|
|
) -> Union[None, OpenBBFigure]:
|
|
"""Chart the vertical skew of an option expiration, or the horizontal skew of equidistant % moneyness options.
|
|
|
|
The vertical skew for each expiry and option is calculated by subtracting the IV of the ATM call or put.
|
|
Returns only where the IV is greater than 0.
|
|
|
|
Horizontal skew is returned if a value for moneyness, or a strike price, is supplied.
|
|
With a strike price specified, both call and put IV skew are displayed. For moneyness,
|
|
it is expressed as the difference between skews of two equidistant OTM strikes (the closest call and put).
|
|
|
|
Parameters
|
|
-----------
|
|
options: Options
|
|
The options data object.
|
|
expirations: list[str]
|
|
The expiration date, or a list of dates. The closest date will be returned for each entry.
|
|
Format as YYYY-MM-DD.
|
|
moneyness: float
|
|
The % moneyess. When specified, this returns the forward skew curve at the target moneyness.
|
|
strike: float
|
|
A target strike price to observe the skew vs. contract. This argument overrides other parameters.
|
|
atm: bool
|
|
When true, returns the ATM skew curve. This will override other parameters, but is overridden by strike.
|
|
otm_only: bool
|
|
When true, returns only OTM portions of the put/call skew curves.
|
|
raw: bool
|
|
Returns a table instead of a plot.
|
|
export: str
|
|
Export the data to csv, json, xlsx file.
|
|
sheet_name: str
|
|
The name of the sheet to save the data to. Only valid when `export` is a `xlsx` file.
|
|
external_axes: bool
|
|
Returns the OpenBB Figure Object to a variable.
|
|
"""
|
|
|
|
if options.hasIV is False:
|
|
return print(
|
|
"Options data object does not have Implied Volatility and is required for this function."
|
|
)
|
|
options = deepcopy(options)
|
|
options.chains = options_chains_model.validate_object(options.chains, "chains")
|
|
|
|
put_skew = pd.DataFrame()
|
|
call_skew = pd.DataFrame()
|
|
skew_df = pd.DataFrame()
|
|
export_df = pd.DataFrame()
|
|
colors = [
|
|
"blue",
|
|
"red",
|
|
"burlywood",
|
|
"orange",
|
|
"green",
|
|
"grey",
|
|
"magenta",
|
|
"cyan",
|
|
"indigo",
|
|
"yellowgreen",
|
|
]
|
|
|
|
expirations = options.expirations[1] if expirations is None else expirations
|
|
expirations = [expirations] if isinstance(expirations, str) else expirations # type: ignore [list-item]
|
|
if len(expirations) > 10 and otm_only is True:
|
|
expirations = expirations[:10] # type: ignore [index]
|
|
elif len(expirations) > 5 and otm_only is False:
|
|
expirations = expirations[:5] # type: ignore [index]
|
|
|
|
if moneyness is None:
|
|
fig = OpenBBFigure()
|
|
color = -1
|
|
for expiration in expirations: # type: ignore [union-attr]
|
|
if expiration == "":
|
|
expiration = options.expirations[1]
|
|
if expiration not in options.expirations:
|
|
expiration = options_chains_model.get_nearest_expiration(
|
|
options, expiration
|
|
)
|
|
color = color + 1
|
|
skew = options_chains_model.calculate_skew(options, expiration, moneyness)[
|
|
["Expiration", "Strike", "Option Type", "IV", "ATM IV", "Skew"]
|
|
]
|
|
if skew["IV"].sum() == 0 and len(expirations) == 1:
|
|
return print("No IV data available for this expiration.")
|
|
|
|
index_name = "Strike" if len(expirations) > 1 else expiration
|
|
call_skew = skew.query("`Option Type` == 'call'").set_index("Strike")
|
|
call_idx = call_skew.index
|
|
call_skew = call_skew[~call_idx.duplicated(keep="first")]
|
|
call_skew = call_skew["Skew"]
|
|
put_skew = skew.query("`Option Type` == 'put'").set_index("Strike")
|
|
put_idx = put_skew.index
|
|
put_skew = put_skew[~put_idx.duplicated(keep="first")]
|
|
put_skew = put_skew["Skew"]
|
|
skew_df["Call Skew"] = call_skew
|
|
skew_df["Put Skew"] = put_skew
|
|
export_df = pd.concat([export_df, skew])
|
|
if otm_only is True:
|
|
calls = skew.query("`Option Type` == 'call'").reset_index()
|
|
puts = skew.query("`Option Type` == 'put'").reset_index()
|
|
put_idx = puts[puts["Skew"] == 0].index.values[0]
|
|
call_idx = calls[calls["Skew"] == 0].index.values[0]
|
|
call_skew = calls.iloc[call_idx - 1 : -1]
|
|
call_skew = call_skew.set_index("Strike")["Skew"]
|
|
call_idx = call_skew.index
|
|
call_skew = call_skew[~call_idx.duplicated(keep="first")]
|
|
put_skew = puts.iloc[0 : put_idx + 2]
|
|
put_skew = put_skew.set_index("Strike")["Skew"]
|
|
put_idx = put_skew.index
|
|
put_skew = put_skew[~put_idx.duplicated(keep="first")]
|
|
export_df = pd.concat([export_df, skew])
|
|
fig.add_scatter(
|
|
x=call_skew.index,
|
|
y=call_skew.values,
|
|
mode="lines+markers",
|
|
name=f"{expiration} Calls" if len(expirations) > 1 else "Call IV Skew",
|
|
marker_color=colors[color] if len(expirations) > 1 else "blue",
|
|
)
|
|
color = color + 1 if otm_only is False else color
|
|
fig.add_scatter(
|
|
x=put_skew.index,
|
|
y=put_skew.values,
|
|
mode="lines+markers",
|
|
name=f"{expiration} Puts" if len(expirations) > 1 else "Put IV Skew",
|
|
marker_color=colors[color] if len(expirations) > 1 else "red",
|
|
)
|
|
|
|
title = f"IV Skew for {options.symbol}"
|
|
if otm_only is True:
|
|
title = "OTM " + title
|
|
fig.update_layout(title=title)
|
|
|
|
if atm or moneyness or strike:
|
|
if moneyness is None:
|
|
moneyness = 0
|
|
skew_df = options_chains_model.calculate_skew(options, moneyness=moneyness)
|
|
skew = skew_df.copy()
|
|
call_skew = skew_df["Call Skew"]
|
|
put_skew = skew_df["Put Skew"]
|
|
export_df = pd.concat([export_df, skew_df])
|
|
title = (
|
|
f"IV Skew for {options.symbol} at {moneyness}% OTM"
|
|
if moneyness != 0 and atm is False
|
|
else f"ATM IV Skew for {options.symbol}"
|
|
)
|
|
index_name = "Expiration"
|
|
fig = OpenBBFigure()
|
|
fig.add_scatter(
|
|
x=skew_df.index,
|
|
y=skew_df["IV Skew"],
|
|
mode="lines+markers",
|
|
name="OTM IV Skew",
|
|
marker=dict(color="green"),
|
|
)
|
|
fig.update_xaxes(type="category")
|
|
if atm:
|
|
fig = OpenBBFigure()
|
|
fig.add_scatter(
|
|
x=skew_df.index,
|
|
y=skew_df["ATM Skew"],
|
|
mode="lines+markers",
|
|
name="ATM IV Skew",
|
|
marker=dict(color="green"),
|
|
)
|
|
fig.update_xaxes(type="category")
|
|
if strike and strike > 0.25:
|
|
fig = OpenBBFigure()
|
|
strike = options_chains_model.get_nearest_call_strike(
|
|
options, strike_price=strike
|
|
)
|
|
skew_df = options_chains_model.calculate_skew(options).query(
|
|
"`Strike` == @strike"
|
|
)
|
|
call_df = skew_df.query("`Option Type` == 'call'").set_index("Expiration")
|
|
call_idx = call_df.index
|
|
call_df = call_df[~call_idx.duplicated(keep="first")]
|
|
call_df = call_df.reset_index()
|
|
put_df = skew_df.query("`Option Type` == 'put'").set_index("Expiration")
|
|
put_idx = put_df.index
|
|
put_df = put_df[~put_idx.duplicated(keep="first")]
|
|
put_df = put_df.reset_index()
|
|
title = f"IV Skew for {options.symbol} at ${strike}"
|
|
expirations = skew_df.Expiration.unique().tolist()
|
|
fig.add_scatter(
|
|
x=expirations,
|
|
y=call_df.Skew,
|
|
mode="lines+markers",
|
|
name="Call IV Skew",
|
|
marker_color="blue",
|
|
)
|
|
fig.add_scatter(
|
|
x=expirations,
|
|
y=put_df.Skew,
|
|
mode="lines+markers",
|
|
name="Put IV Skew",
|
|
marker_color="red",
|
|
)
|
|
fig.update_xaxes(type="category")
|
|
|
|
fig.update_layout(
|
|
title=dict(text=title, x=0.5, y=0.97),
|
|
yaxis=dict(
|
|
title=dict(
|
|
text="IV Skew",
|
|
),
|
|
ticklen=0,
|
|
),
|
|
xaxis=dict(
|
|
showgrid=False,
|
|
autorange=True,
|
|
title=dict(text=index_name),
|
|
),
|
|
legend=dict(
|
|
font=dict(size=12),
|
|
orientation="v",
|
|
yanchor="top",
|
|
y=0.97,
|
|
xanchor="center",
|
|
x=0.525,
|
|
),
|
|
)
|
|
|
|
if raw:
|
|
print_rich_table(
|
|
export_df,
|
|
title=title,
|
|
show_index=True,
|
|
index_name=index_name,
|
|
floatfmt=".4f",
|
|
export=bool(export),
|
|
)
|
|
return None
|
|
|
|
if export and export != "":
|
|
export_data(
|
|
export,
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
"skew",
|
|
export_df,
|
|
sheet_name,
|
|
fig,
|
|
)
|
|
return None
|
|
|
|
return fig.show(external=raw or external_axes)
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def display_volatility(
|
|
options: Options,
|
|
expirations: Optional[list[str]] = None,
|
|
moneyness: Optional[float] = None,
|
|
strike: Optional[float] = None,
|
|
oi: bool = False,
|
|
volume: bool = False,
|
|
raw: bool = False,
|
|
export: str = "",
|
|
sheet_name: Optional[str] = "",
|
|
external_axes: bool = False,
|
|
) -> Union[None, OpenBBFigure]:
|
|
"""Chart the implied volatility smile.
|
|
|
|
Parameters
|
|
-----------
|
|
options: Options
|
|
The options data object.
|
|
expirations: list[str]
|
|
Select up to five expiration(s) to display. Overridden by moneyness or strike. Format as YYYY-MM-DD.
|
|
moneyness: float
|
|
Specify a target % moneyness to display vs. contract dates. This argument overrides expirations.
|
|
strike: float
|
|
Specify a target strike price to display vs. contract dates.
|
|
Returned strike price is estimated as the closest one listed at approximately one year forward.
|
|
This argument overrides moneyness and expirations.
|
|
oi: bool
|
|
Return only contracts with open interest. Only valid for IV vs. Strike charts. Default is False.
|
|
volume: bool
|
|
Return only contracts with trading volume. Only valid for IV vs. Strike charts. Default is False.
|
|
raw: bool
|
|
Display the raw data instead of the chart.
|
|
export: str
|
|
Export dataframe data to csv,json,xlsx file.
|
|
external_axes: bool
|
|
Return the OpenBB Figure Object to a variable.
|
|
|
|
Examples
|
|
----------
|
|
>>> from openbb_terminal.stocks.options import options_chains_model, options_chains_view
|
|
>>> spy = options_chains_model.load_options_chains("SPY")
|
|
>>> options_chains_view(spy)
|
|
|
|
Plot IV @ Strike vs. contract dates:
|
|
>>> options_chains_view(spy, strike=450)
|
|
|
|
Plot IV @ % OTM vs. contract dates:
|
|
>>> options_chains_view(spy, moneyness=20)
|
|
|
|
Plot multiple expirations at once, and filter for only contracts with open interest:
|
|
>>> options_chains_view(spy, expirations=["2024-12-30", "2025-12-30"], oi=True)
|
|
"""
|
|
|
|
if options.hasIV is False:
|
|
return print(
|
|
"Options data object does not have Implied Volatility and is required for this function."
|
|
)
|
|
options = deepcopy(options)
|
|
options.chains = options_chains_model.validate_object(options.chains, "chains")
|
|
colors = [
|
|
"blue",
|
|
"red",
|
|
"burlywood",
|
|
"orange",
|
|
"green",
|
|
"grey",
|
|
"magenta",
|
|
"cyan",
|
|
"indigo",
|
|
"yellowgreen",
|
|
]
|
|
|
|
index_name = ""
|
|
title = f"{options.symbol} Volatility"
|
|
export_df = pd.DataFrame()
|
|
|
|
iv_df = options.chains[options.chains["impliedVolatility"] > 0][
|
|
[
|
|
"expiration",
|
|
"strike",
|
|
"optionType",
|
|
"dte",
|
|
"openInterest",
|
|
"volume",
|
|
"impliedVolatility",
|
|
]
|
|
]
|
|
|
|
if moneyness is not None and strike is None:
|
|
fig = OpenBBFigure()
|
|
calls = pd.DataFrame()
|
|
puts = pd.DataFrame()
|
|
title = title + f" at {moneyness}% OTM" if strike is None else title
|
|
strikes = options_chains_model.get_nearest_otm_strike(options, moneyness)
|
|
if expirations is None:
|
|
days = options.chains.dte.unique().tolist()
|
|
for day in days:
|
|
call_strike = options_chains_model.get_nearest_call_strike(
|
|
options, day, strikes["call"]
|
|
)
|
|
call = (
|
|
iv_df[iv_df["strike"] == call_strike]
|
|
.query("`optionType` == 'call' & `dte` == @day")
|
|
.set_index("expiration")
|
|
)
|
|
put_strike = options_chains_model.get_nearest_put_strike(
|
|
options, day, strikes["put"]
|
|
)
|
|
put = (
|
|
iv_df[iv_df["strike"] == put_strike]
|
|
.query("`optionType` == 'put' & `dte` == @day")
|
|
.set_index("expiration")
|
|
)
|
|
calls = pd.concat([calls, call])
|
|
puts = pd.concat([puts, put])
|
|
expirations = iv_df["expiration"].unique().tolist()
|
|
export_df = pd.concat([export_df, calls, puts])
|
|
fig.add_scatter(
|
|
x=expirations,
|
|
y=calls["impliedVolatility"],
|
|
mode="lines+markers",
|
|
name="Calls",
|
|
marker_color="blue",
|
|
)
|
|
fig.add_scatter(
|
|
x=expirations,
|
|
y=puts["impliedVolatility"],
|
|
mode="lines+markers",
|
|
name="Puts",
|
|
marker_color="red",
|
|
)
|
|
fig.update_xaxes(type="category")
|
|
|
|
if moneyness is None and strike is None:
|
|
expirations = options.expirations[1] if expirations is None else expirations
|
|
|
|
if expirations is not None and moneyness is None:
|
|
fig = OpenBBFigure()
|
|
color = -1
|
|
expirations = [expirations] if isinstance(expirations, str) else expirations # type: ignore [list-item]
|
|
if len(expirations) > 5:
|
|
expirations = expirations[:5]
|
|
if oi is True:
|
|
iv_df = iv_df[iv_df["openInterest"] > 0]
|
|
if volume is True:
|
|
iv_df = iv_df[iv_df["volume"] > 0]
|
|
for expiration in expirations:
|
|
expiration = options_chains_model.get_nearest_expiration(
|
|
options, expiration
|
|
)
|
|
data = iv_df[iv_df["expiration"] == expiration]
|
|
calls = data[data["optionType"] == "call"]
|
|
puts = data[data["optionType"] == "put"]
|
|
color = color + 1
|
|
fig.add_scatter(
|
|
x=calls["strike"].unique().tolist(),
|
|
y=calls["impliedVolatility"],
|
|
mode="lines+markers",
|
|
name="Calls" if len(expirations) <= 1 else f"Calls at {expiration}",
|
|
marker_color=colors[color] if len(expirations) > 1 else "blue",
|
|
)
|
|
color = color + 1
|
|
fig.add_scatter(
|
|
x=puts["strike"].unique().tolist(),
|
|
y=puts["impliedVolatility"],
|
|
mode="lines+markers",
|
|
name="Puts" if len(expirations) <= 1 else f"Puts at {expiration}",
|
|
marker_color=colors[color] if len(expirations) > 1 else "red",
|
|
)
|
|
export_df = pd.concat([export_df, data])
|
|
index_name = expiration if len(expirations) <= 1 else ""
|
|
title = title + " With Open Interest" if oi and not strike else title
|
|
title = (
|
|
title + " Excluding Untraded Contracts" if volume and not strike else title
|
|
)
|
|
|
|
if strike is not None:
|
|
fig = OpenBBFigure()
|
|
strike = (
|
|
options_chains_model.get_nearest_call_strike(
|
|
options, 300, strike_price=strike
|
|
)
|
|
if strike > options.last_price
|
|
else options_chains_model.get_nearest_put_strike(
|
|
options, 300, strike_price=strike
|
|
)
|
|
)
|
|
title = title + f" at ${strike}"
|
|
calls = iv_df[iv_df["strike"] == strike].query("`optionType` == 'call'")
|
|
puts = iv_df[iv_df["strike"] == strike].query("`optionType` == 'put'")
|
|
expirations = iv_df["expiration"].unique().tolist()
|
|
export_df = pd.concat([export_df, calls, puts])
|
|
fig.add_scatter(
|
|
x=expirations,
|
|
y=calls["impliedVolatility"],
|
|
mode="lines+markers",
|
|
name="Calls",
|
|
marker_color="blue",
|
|
)
|
|
fig.add_scatter(
|
|
x=expirations,
|
|
y=puts["impliedVolatility"],
|
|
mode="lines+markers",
|
|
name="Puts",
|
|
marker_color="red",
|
|
)
|
|
fig.update_xaxes(type="category")
|
|
|
|
fig.update_layout(
|
|
title=dict(text=title, x=0.5, y=0.97),
|
|
yaxis=dict(
|
|
title=dict(
|
|
text="IV",
|
|
),
|
|
ticklen=0,
|
|
),
|
|
xaxis=dict(
|
|
showgrid=False,
|
|
autorange=True,
|
|
title=dict(text=index_name),
|
|
),
|
|
legend=dict(
|
|
font=dict(size=12),
|
|
orientation="v",
|
|
yanchor="top",
|
|
y=0.97,
|
|
xanchor="center",
|
|
x=0.525,
|
|
),
|
|
)
|
|
|
|
if raw:
|
|
print_rich_table(
|
|
export_df,
|
|
title=title,
|
|
show_index=False,
|
|
floatfmt=".4f",
|
|
export=bool(export),
|
|
)
|
|
return None
|
|
|
|
if export and export != "":
|
|
export_data(
|
|
export,
|
|
os.path.dirname(os.path.abspath(__file__)),
|
|
"volatility",
|
|
export_df,
|
|
sheet_name,
|
|
fig,
|
|
)
|
|
return None
|
|
|
|
return fig.show(external=raw or external_axes)
|