mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-11-08 02:11:44 +00:00
1848 lines
65 KiB
Python
1848 lines
65 KiB
Python
# pylint: disable=C0302, R0911, W0612
|
|
""" Options Chains Module """
|
|
__docformat__ = "numpy"
|
|
|
|
# IMPORTATION STANDARD
|
|
import logging
|
|
from copy import deepcopy
|
|
|
|
# IMPORTATION THIRDPARTY
|
|
from typing import Any, Optional
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
# IMPORTATION INTERNAL
|
|
from openbb_terminal.decorators import log_start_end
|
|
from openbb_terminal.stocks.options.cboe_model import load_options as load_cboe
|
|
from openbb_terminal.stocks.options.intrinio_model import load_options as load_intrinio
|
|
from openbb_terminal.stocks.options.nasdaq_model import load_options as load_nasdaq
|
|
from openbb_terminal.stocks.options.op_helpers import Options
|
|
from openbb_terminal.stocks.options.tmx_model import load_options as load_tmx
|
|
from openbb_terminal.stocks.options.tradier_model import load_options as load_tradier
|
|
from openbb_terminal.stocks.options.yfinance_model import load_options as load_yfinance
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SOURCES = ["CBOE", "YahooFinance", "Tradier", "Intrinio", "Nasdaq", "TMX"]
|
|
|
|
# mypy: disable-error-code="attr-defined, index"
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def load_options_chains(
|
|
symbol: str,
|
|
source: str = "CBOE",
|
|
date: str = "",
|
|
pydantic: bool = False,
|
|
) -> object:
|
|
"""Loads all options chains from a specific source, fields returned to each attribute will vary.
|
|
|
|
Parameters
|
|
----------
|
|
symbol : str
|
|
The underlying asset's symbol.
|
|
source: str
|
|
The source of the data. Choices are "CBOE", "YahooFinance", "Tradier", "Intrinio", "Nasdaq", or "TMX".
|
|
date: Optional[str]
|
|
The date for the EOD option chain. Format: YYYY-MM-DD.
|
|
This parameter is only available for "TMX" or "Intrinio".
|
|
pydantic: bool
|
|
Whether to return the object as a Pydantic Model or a subscriptable Pandas object. Default is False.
|
|
|
|
Returns
|
|
-------
|
|
object: Options data object
|
|
|
|
chains: dict
|
|
All options chains data from a specific source. Returns as a Pandas DataFrame if pydantic is False.
|
|
expirations: list[str]
|
|
List of all unique expiration dates.
|
|
hasGreeks: bool
|
|
True if the source returns greeks with the chains data.
|
|
hasIV: bool
|
|
True if the source returns implied volatility with the chains data.
|
|
last_price: float
|
|
The last price (or the price at the EOD for the date.of the EOD option chain).
|
|
source: str
|
|
The source that was entered in the input.
|
|
strikes: list[float]
|
|
List of all unique strike prices.
|
|
symbol: str
|
|
The symbol that was entered in the input.
|
|
SYMBOLS: pd.DataFrame
|
|
The symbol directory to the selected source, when available. Only returned when pydantic is False.
|
|
underlying_name: str
|
|
The name of the underlying asset.
|
|
underlying_price: dict
|
|
The underlying asset's price and performance. Returns as a Pandas Series if pydantic is False.
|
|
|
|
Examples
|
|
--------
|
|
Loads SPY data from CBOE, returns as a Pydantic Model, and displays the longest-dated expiration chain.
|
|
|
|
>>> from openbb_terminal.sdk import openbb
|
|
>>> import pandas as pd
|
|
>>> data = openbb.stocks.options.load_options_chains("SPY", pydantic = True)
|
|
>>> chains = pd.DataFrame(data.chains)
|
|
>>> chains[chains["expiration"] == data.expirations[-1]]
|
|
|
|
Loads QQQ data from Tradier as a Pydantic Model.
|
|
|
|
>>> from openbb_terminal.sdk import openbb
|
|
>>> data = openbb.stocks.options.load_options_chains("QQQ", source = "Tradier", pydantic = True)
|
|
|
|
Loads VIX data from YahooFinance as a Pandas object.
|
|
|
|
>>> from openbb_terminal.sdk import openbb
|
|
>>> data = openbb.stocks.options.load_options_chains("^VIX", source = "YahooFinance")
|
|
|
|
Loads XIU data from TMX and displays the 25 highest open interest options.
|
|
|
|
>>> from openbb_terminal.sdk import openbb
|
|
>>> data = openbb.stocks.options.load_options_chains("XIU", "TMX")
|
|
>>> data.chains.sort_values("openInterest", ascending=False).head(25)
|
|
|
|
Loads the EOD chains data for XIU.TO from March 15, 2020, sorted by number of transactions.
|
|
|
|
>>> from openbb_terminal.sdk import openbb
|
|
>>> data = openbb.stocks.options.load_options_chains("XIU.TO", "TMX", "2020-03-15")
|
|
>>> data.chains.sort_values("transactions", ascending=False).head(25)
|
|
"""
|
|
|
|
if source not in SOURCES:
|
|
print("Invalid choice. Choose from: ", list(SOURCES), sep=None)
|
|
return None
|
|
|
|
if source == "Nasdaq":
|
|
return load_nasdaq(symbol, pydantic)
|
|
if source == "YahooFinance":
|
|
return load_yfinance(symbol, pydantic)
|
|
if source == "Tradier":
|
|
return load_tradier(symbol, pydantic)
|
|
if source == "TMX":
|
|
if date != "":
|
|
return load_tmx(symbol, date, pydantic)
|
|
return load_tmx(symbol, pydantic=pydantic)
|
|
if source == "Intrinio":
|
|
if date != "":
|
|
return load_intrinio(symbol, date, pydantic)
|
|
return load_intrinio(symbol, pydantic=pydantic)
|
|
|
|
return load_cboe(symbol, pydantic)
|
|
|
|
|
|
def validate_object(
|
|
options: object, scope: Optional[str] = "object", days: Optional[int] = None
|
|
) -> Any:
|
|
"""This is an internal helper function for validating the Options data object passed
|
|
through the input of functions defined in the OptionsChains class. The purpose is to handle
|
|
multi-type inputs with backwards compatibility and provide robust error handling. The return
|
|
is the portion of the object, or a true/false validation, required to perform the operation.
|
|
|
|
Parameters
|
|
----------
|
|
options : object
|
|
The Options data object.
|
|
Accepts both Pydantic and Pandas object types, as defined by `load_options_chains()`.
|
|
A Pandas DataFrame, or dictionary, with the options chains data is also accepted.
|
|
scope: str
|
|
The scope of the data needing to be validated. Choices are: ["chains", "object", "strategies", "nonZeroPrices"]
|
|
days: int
|
|
The number of target number of days until the expiration.
|
|
|
|
Returns
|
|
-------
|
|
Any:
|
|
if scope == "chains":
|
|
pd.DataFrame
|
|
Pandas DataFrame with the validated data.
|
|
if scope == "object" or scope == "strategies":
|
|
bool
|
|
True if the object is a valid Options data object.
|
|
|
|
Examples
|
|
--------
|
|
Load some data first:
|
|
>>> from openbb_terminal.stocks.options import options_chains_model
|
|
>>> data = options_chains_model.load_options_chains("SPY")
|
|
To extract just the chains data, use:
|
|
>>> chains = options_chains_model.validate_object(data, scope="chains")
|
|
To pass as a true/false validation, use:
|
|
>>> if options_chains_model.validate_object(data, scope="object") is False:
|
|
>>> return
|
|
To pass and return the entire object for non-zero prices:
|
|
>>> if options_chains_model.validate_object(data, scope="object") is True:
|
|
>>> data = options_chains_model.validate_object(data, scope="nonZeroPrices")
|
|
"""
|
|
|
|
scopes = ["chains", "object", "strategies", "nonZeroPrices"]
|
|
|
|
valid: bool = True
|
|
|
|
if scope == "":
|
|
scope = "chains"
|
|
|
|
if scope not in scopes:
|
|
print("Invalid choice. The supported methods are:", scopes)
|
|
return pd.DataFrame()
|
|
|
|
if scope == "object":
|
|
try:
|
|
if (
|
|
isinstance(options.strikes, list)
|
|
and isinstance(options.expirations, list)
|
|
and hasattr(options, "last_price")
|
|
):
|
|
return valid
|
|
|
|
except AttributeError:
|
|
print(
|
|
"Error: Invalid data type supplied. The Options data object is required. "
|
|
"Use load_options_chains() first."
|
|
)
|
|
return not valid
|
|
|
|
if scope == "strategies":
|
|
try:
|
|
if isinstance(options.last_price, float):
|
|
return valid
|
|
except AttributeError:
|
|
print(
|
|
"`last_price` was not found in the OptionsChainsData object and is required for this operation."
|
|
)
|
|
return not valid
|
|
|
|
if scope == "chains":
|
|
try:
|
|
if isinstance(options, pd.DataFrame):
|
|
chains = options.copy()
|
|
|
|
if isinstance(options, dict):
|
|
chains = pd.DataFrame(options)
|
|
|
|
elif isinstance(options, object) and not isinstance(options, pd.DataFrame):
|
|
chains = (
|
|
pd.DataFrame(options.chains) # type: ignore[attr-defined]
|
|
if isinstance(options.chains, dict) # type: ignore[attr-defined]
|
|
else options.chains.copy() # type: ignore[attr-defined]
|
|
)
|
|
|
|
if options is None or chains.empty:
|
|
print(
|
|
"No options chains data found in the supplied object. Use load_options_chains()."
|
|
)
|
|
return pd.DataFrame()
|
|
|
|
if "openInterest" not in chains.columns:
|
|
print("Expected column, openInterest, not found.")
|
|
return pd.DataFrame()
|
|
|
|
if "volume" not in chains.columns:
|
|
print("Expected column, volume, not found.")
|
|
return pd.DataFrame()
|
|
except AttributeError:
|
|
print("Error: Invalid data type supplied.")
|
|
return pd.DataFrame()
|
|
|
|
return chains
|
|
|
|
if scope == "nonZeroPrices":
|
|
dte_estimate = get_nearest_dte( # noqa:F841 pylint: disable=unused-variable
|
|
options, days
|
|
)
|
|
# When Intrinio data is not EOD, there is no "ask" column, renaming "close".
|
|
if (
|
|
options.source == "Intrinio"
|
|
and options.date is None
|
|
or options.source == "Intrinio"
|
|
and options.date == ""
|
|
):
|
|
options.chains["ask"] = options.chains["close"]
|
|
options.chains["bid"] = options.chains["close"]
|
|
|
|
# Error handling for TMX EOD data when the date is an expiration date. EOD, 0-day options are excluded.
|
|
if options.source == "TMX" and options.date != "":
|
|
options.chains = options.chains[options.chains["dte"] > 0]
|
|
|
|
if (
|
|
options.source == "TMX"
|
|
and options.date is None
|
|
or options.source == "TMX"
|
|
and options.date == ""
|
|
):
|
|
options.chains["ask"] = options.chains["lastPrice"]
|
|
options.chains["bid"] = options.chains["lastPrice"]
|
|
|
|
if (
|
|
options.source == "TMX"
|
|
or options.source == "YahooFinance"
|
|
and options.chains.query("`dte` == @dte_estimate")["ask"].sum() == 0
|
|
):
|
|
options.chains["ask"] = options.chains["lastPrice"]
|
|
if (
|
|
options.source == "TMX"
|
|
or options.source == "YahooFinance"
|
|
and options.chains.query("`dte` == @dte_estimate")["bid"].sum() == 0
|
|
):
|
|
options.chains["bid"] = options.chains["lastPrice"]
|
|
|
|
return options
|
|
|
|
print(
|
|
"Error: No valid data supplied. Check the input to ensure it is not empty or None."
|
|
)
|
|
|
|
return not valid
|
|
|
|
|
|
def get_nearest_dte(options: object, days: Optional[int] = 30) -> int:
|
|
"""Gets the closest expiration date to the target number of days until expiry.
|
|
|
|
Parameters
|
|
----------
|
|
options : object
|
|
The Options data object. Use load_options_chains() to load the data.
|
|
days: int
|
|
The target number of days until expiry. Default is 30 days.
|
|
|
|
Returns
|
|
-------
|
|
int
|
|
The closest expiration date to the target number of days until expiry, expressed as DTE.
|
|
|
|
Example
|
|
-------
|
|
>>> from openbb_terminal.stocks.options import options_chains_model
|
|
>>> data = options_chains_model.load_options_chains("QQQ")
|
|
>>> options_chains_model.get_nearest_dte(data, 42)
|
|
>>> options_chains_model.get_nearest_dte(data, 90)
|
|
"""
|
|
|
|
if validate_object(options, scope="object") is False:
|
|
return pd.DataFrame()
|
|
|
|
if isinstance(options.chains, dict):
|
|
options.chains = pd.DataFrame(options.chains)
|
|
|
|
days = -1 if days == 0 else days
|
|
|
|
nearest = (options.chains["dte"] - days).abs().idxmin()
|
|
|
|
return options.chains.loc[nearest]["dte"]
|
|
|
|
|
|
def get_nearest_call_strike(
|
|
options: object, days: Optional[int] = 30, strike_price: Optional[float] = 0
|
|
) -> float:
|
|
"""Gets the closest call strike to the target price and number of days until expiry.
|
|
|
|
Parameters
|
|
----------
|
|
options : object
|
|
The Options data object. Use load_options_chains() to load the data.
|
|
days: int
|
|
The target number of days until expiry. Default is 30 days.
|
|
strike_price: float
|
|
The target strike price. Default is the last price of the underlying stock.
|
|
|
|
Returns
|
|
-------
|
|
float
|
|
The closest strike price to the target price and number of days until expiry.
|
|
|
|
Example
|
|
-------
|
|
>>> from openbb_terminal.stocks.options import options_chains_model
|
|
>>> data = options_chains_model.load_options_chains("SPY")
|
|
>>> options_chains_model.get_nearest_call_strike(data)
|
|
>>> options_chains_model.get_nearest_call_strike(data, 180, 427)
|
|
>>> days = data.chains.dte.unique().tolist()
|
|
>>> for day in days:
|
|
>>> print(get_nearest_call_strike(data, day))
|
|
"""
|
|
|
|
if validate_object(options, scope="object") is False:
|
|
return pd.DataFrame()
|
|
|
|
if validate_object(options, scope="strategies") is False:
|
|
print("`last_price` was not found in the OptionsChain data object.")
|
|
return pd.DataFrame()
|
|
|
|
if isinstance(options.chains, dict):
|
|
options.chains = pd.DataFrame(options.chains)
|
|
|
|
if strike_price == 0:
|
|
strike_price = options.last_price
|
|
|
|
dte_estimate = get_nearest_dte(options, days)
|
|
|
|
nearest = (
|
|
(
|
|
options.chains[options.chains["dte"] == dte_estimate]
|
|
.query("`optionType` == 'call'")
|
|
.convert_dtypes()["strike"]
|
|
- strike_price
|
|
)
|
|
.abs()
|
|
.idxmin()
|
|
)
|
|
|
|
return options.chains.loc[nearest]["strike"]
|
|
|
|
|
|
def get_nearest_put_strike(
|
|
options: object, days: Optional[int] = 30, strike_price: Optional[float] = 0
|
|
) -> float:
|
|
"""Gets the closest put strike to the target price and number of days until expiry.
|
|
|
|
Parameters
|
|
----------
|
|
options : object
|
|
The Options data object. Use load_options_chains() to load the data.
|
|
days: int
|
|
The target number of days until expiry. Default is 30 days.
|
|
strike_price: float
|
|
The target strike price. Default is the last price of the underlying stock.
|
|
|
|
Returns
|
|
-------
|
|
float
|
|
The closest strike price to the target price and number of days until expiry.
|
|
|
|
Example
|
|
-------
|
|
>>> from openbb_terminal.stocks.options import options_chains_model
|
|
>>> data = options_chains_model.load_options_chains('SPY')
|
|
>>> options_chains_model.get_nearest_put_strike(data, 90, 402)
|
|
>>> options_chains_model.get_nearest_put_strike(data, 90)
|
|
>>> days = data.chains.dte.unique().tolist()
|
|
>>> for day in days:
|
|
>>> print(get_nearest_put_strike(data, day))
|
|
"""
|
|
|
|
if validate_object(options, scope="object") is False:
|
|
return pd.DataFrame()
|
|
|
|
if validate_object(options, scope="strategies") is False:
|
|
print("`last_price` was not found in the OptionsChain data object.")
|
|
return pd.DataFrame()
|
|
|
|
if isinstance(options.chains, dict):
|
|
options.chains = pd.DataFrame(options.chains)
|
|
|
|
if strike_price == 0:
|
|
strike_price = options.last_price
|
|
|
|
dte_estimate = get_nearest_dte(options, days)
|
|
|
|
nearest = (
|
|
(
|
|
options.chains[options.chains["dte"] == dte_estimate]
|
|
.query("`optionType` == 'put'")
|
|
.convert_dtypes()["strike"]
|
|
- strike_price
|
|
)
|
|
.abs()
|
|
.idxmin()
|
|
)
|
|
|
|
return options.chains.loc[nearest]["strike"]
|
|
|
|
|
|
def get_nearest_otm_strike(
|
|
options: object, moneyness: Optional[float] = 5
|
|
) -> dict[str, float]:
|
|
"""Gets the nearest put and call strikes at a given percent OTM from the underlying price.
|
|
|
|
Parameters
|
|
----------
|
|
options : OptionsChains
|
|
The Options data object. Use load_options_chains() to load the data.
|
|
moneyness: float
|
|
The target percent OTM, expressed as a percent between 0 and 100. Default is 5.
|
|
|
|
Returns
|
|
-------
|
|
dict[str, float]
|
|
Dictionary of the upper (call) and lower (put) strike prices.
|
|
|
|
Example
|
|
-------
|
|
>>> from openbb_terminal.stocks.options import options_chains_model
|
|
>>> data = options_chains_model.load_options_chains('SPY')
|
|
>>> strikes = options_chains_model.get_nearest_otm_strike(data, 5)
|
|
"""
|
|
|
|
if validate_object(options, scope="object") is False:
|
|
return pd.DataFrame()
|
|
|
|
if validate_object(options, scope="strategies") is False:
|
|
print("`last_price` was not found in the OptionsChain data object.")
|
|
return pd.DataFrame()
|
|
|
|
if isinstance(options.chains, dict):
|
|
options.chains = pd.DataFrame(options.chains)
|
|
|
|
if not moneyness:
|
|
moneyness = 5
|
|
|
|
if 0 < moneyness < 100:
|
|
moneyness = moneyness / 100
|
|
|
|
if moneyness > 100 or moneyness < 0:
|
|
print("Error: Moneyness must be expressed as a percentage between 0 and 100")
|
|
return {}
|
|
|
|
upper = options.last_price * (1 + moneyness)
|
|
lower = options.last_price * (1 - moneyness)
|
|
strikes = pd.Series(options.strikes)
|
|
nearest_call = (upper - strikes).abs().idxmin()
|
|
call = strikes[nearest_call]
|
|
nearest_putt = (lower - strikes).abs().idxmin()
|
|
put = strikes[nearest_putt]
|
|
|
|
otmStrikes = {"call": call, "put": put}
|
|
|
|
return otmStrikes
|
|
|
|
|
|
def calculate_straddle(
|
|
options: object,
|
|
days: Optional[int] = 30,
|
|
strike: float = 0,
|
|
) -> pd.DataFrame:
|
|
"""Calculates the cost of a straddle and its payoff profile. Use a negative strike price for short options.
|
|
Requires the Options data object.
|
|
|
|
Parameters
|
|
----------
|
|
options : object
|
|
The Options data object. Use load_options_chains() to load the data.
|
|
days: int
|
|
The target number of days until expiry. Default is 30 days.
|
|
strike: float
|
|
The target strike price. Enter a negative value for short options.
|
|
Default is the last price of the underlying stock.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the results. Strike1 is the nearest call strike, strike2 is the nearest put strike.
|
|
|
|
Examples
|
|
--------
|
|
>>> from openbb_terminal.sdk import openbb
|
|
>>> data = openbb.stocks.options.load_options_chains('SPY')
|
|
>>> openbb.stocks.options.calculate_straddle(data)
|
|
"""
|
|
options = deepcopy(options)
|
|
if not days:
|
|
days = 30
|
|
if days and days == 0:
|
|
days = -1
|
|
|
|
short: bool = False
|
|
if strike is not None and strike < 0:
|
|
short = True
|
|
strike_price = abs(strike)
|
|
|
|
bidAsk = "bid" if short else "ask" # noqa:F841
|
|
|
|
if validate_object(options, scope="object") is False:
|
|
return pd.DataFrame()
|
|
|
|
if validate_object(options, scope="strategies") is False:
|
|
print("`last_price` was not found in the OptionsChain data object.")
|
|
return pd.DataFrame()
|
|
|
|
if isinstance(options.chains, dict):
|
|
options.chains = pd.DataFrame(options.chains)
|
|
|
|
dte_estimate = get_nearest_dte(options, days) # noqa:F841
|
|
|
|
options = validate_object(options, "nonZeroPrices", dte_estimate)
|
|
|
|
if not strike_price:
|
|
strike_price = options.last_price
|
|
|
|
call_strike_estimate = get_nearest_call_strike(
|
|
options, dte_estimate, strike_price
|
|
) # noqa:F841
|
|
put_strike_estimate = get_nearest_put_strike(
|
|
options, dte_estimate, strike_price
|
|
) # noqa:F841
|
|
|
|
call_premium = options.chains.query(
|
|
"`strike` == @call_strike_estimate and `dte` == @dte_estimate and `optionType` == 'call'"
|
|
)[bidAsk].values
|
|
|
|
put_premium = options.chains.query(
|
|
"`strike` == @put_strike_estimate and `dte` == @dte_estimate and `optionType` == 'put'"
|
|
)[bidAsk].values
|
|
|
|
straddle_cost = call_premium + put_premium
|
|
|
|
straddle = {}
|
|
|
|
# Include the as-of date if the data is historical EOD.
|
|
if (
|
|
options.source == "Intrinio"
|
|
and options.date != ""
|
|
or options.source == "TMX"
|
|
and options.date != ""
|
|
):
|
|
straddle.update({"Date": options.date})
|
|
|
|
straddle.update(
|
|
{
|
|
"Symbol": options.symbol,
|
|
"Underlying Price": options.last_price,
|
|
"Expiration": options.chains.query("`dte` == @dte_estimate")[
|
|
"expiration"
|
|
].unique()[0],
|
|
"DTE": dte_estimate,
|
|
"Strike 1": call_strike_estimate,
|
|
"Strike 2": put_strike_estimate,
|
|
"Strike 1 Premium": call_premium[0],
|
|
"Strike 2 Premium": put_premium[0],
|
|
"Cost": straddle_cost[0] * -1 if short else straddle_cost[0],
|
|
"Cost Percent": round(
|
|
straddle_cost[0] / options.last_price * 100, ndigits=4
|
|
),
|
|
"Breakeven Upper": call_strike_estimate + straddle_cost[0],
|
|
"Breakeven Upper Percent": round(
|
|
(call_strike_estimate + straddle_cost[0]) / options.last_price * 100,
|
|
ndigits=4,
|
|
)
|
|
- 100,
|
|
"Breakeven Lower": put_strike_estimate - straddle_cost[0],
|
|
"Breakeven Lower Percent": -100
|
|
+ round(
|
|
(put_strike_estimate - straddle_cost[0]) / options.last_price * 100,
|
|
ndigits=4,
|
|
),
|
|
"Max Profit": abs(straddle_cost[0]) if short else np.inf,
|
|
"Max Loss": np.inf if short else straddle_cost[0] * -1,
|
|
}
|
|
)
|
|
|
|
straddle = pd.DataFrame(data=straddle.values(), index=straddle.keys()).rename(
|
|
columns={0: "Short Straddle" if short else "Long Straddle"}
|
|
)
|
|
|
|
straddle.loc["Payoff Ratio"] = round(
|
|
abs(straddle.loc["Max Profit"][0] / straddle.loc["Max Loss"][0]), ndigits=4
|
|
)
|
|
|
|
return straddle
|
|
|
|
|
|
def calculate_strangle(
|
|
options: object,
|
|
days: Optional[int] = 30,
|
|
moneyness: Optional[float] = 5,
|
|
) -> pd.DataFrame:
|
|
"""Calculates the cost of a strangle and its payoff profile. Use a negative value for moneyness for short options.
|
|
|
|
Requires the Options data object.
|
|
|
|
Parameters
|
|
----------
|
|
options : object
|
|
The Options data object. Use load_options_chains() to load the data.
|
|
days: int
|
|
The target number of days until expiry. Default is 30 days.
|
|
moneyness: float
|
|
The percentage of OTM moneyness, expressed as a percent between -100 < 0 < 100.
|
|
Enter a negative number for short options.
|
|
Default is 5.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the results. Strike 1 is the nearest call strike, and strike 2 is the nearest put strike.
|
|
|
|
Examples
|
|
--------
|
|
>>> data = openbb.stocks.options.load_options_chains("SPY")
|
|
>>> data.calculate_strangle()
|
|
"""
|
|
|
|
options = deepcopy(options)
|
|
|
|
if not days:
|
|
days = 30
|
|
|
|
if not moneyness:
|
|
moneyness = 5
|
|
|
|
short: bool = False
|
|
|
|
if moneyness < 0:
|
|
short = True
|
|
moneyness = abs(moneyness)
|
|
bidAsk = "bid" if short else "ask" # noqa:F841
|
|
|
|
if validate_object(options, scope="object") is False:
|
|
return pd.DataFrame()
|
|
|
|
if validate_object(options, scope="strategies") is False:
|
|
print("`last_price` was not found in the OptionsChain data object.")
|
|
return pd.DataFrame()
|
|
|
|
if isinstance(options.chains, dict):
|
|
options.chains = pd.DataFrame(options.chains)
|
|
|
|
dte_estimate = get_nearest_dte(options, days) # noqa:F841
|
|
|
|
options = validate_object(options, "nonZeroPrices", dte_estimate)
|
|
|
|
if moneyness > 100 or moneyness < 0:
|
|
print("Error: Moneyness must be between 0 and 100.")
|
|
return pd.DataFrame()
|
|
|
|
strikes = get_nearest_otm_strike(options, moneyness)
|
|
|
|
call_strike_estimate = get_nearest_call_strike(
|
|
options, dte_estimate, strikes["call"]
|
|
) # noqa:F841
|
|
put_strike_estimate = get_nearest_put_strike(
|
|
options, dte_estimate, strikes["put"]
|
|
) # noqa:F841
|
|
|
|
call_premium = options.chains.query(
|
|
"`strike` == @call_strike_estimate and `dte` == @dte_estimate and `optionType` == 'call'"
|
|
)[bidAsk].values
|
|
|
|
put_premium = options.chains.query(
|
|
"`strike` == @put_strike_estimate and `dte` == @dte_estimate and `optionType` == 'put'"
|
|
)[bidAsk].values
|
|
|
|
strangle_cost = call_premium + put_premium
|
|
|
|
strangle = {}
|
|
|
|
# Includees the as-of date if it is historical EOD data.
|
|
if (
|
|
options.source == "Intrinio"
|
|
and options.date != ""
|
|
or options.source == "TMX"
|
|
and options.date != ""
|
|
):
|
|
strangle.update({"Date": options.date})
|
|
|
|
strangle.update(
|
|
{
|
|
"Symbol": options.symbol,
|
|
"Underlying Price": options.last_price,
|
|
"Expiration": options.chains.query("`dte` == @dte_estimate")[
|
|
"expiration"
|
|
].unique()[0],
|
|
"DTE": dte_estimate,
|
|
"Strike 1": call_strike_estimate,
|
|
"Strike 2": put_strike_estimate,
|
|
"Strike 1 Premium": call_premium[0],
|
|
"Strike 2 Premium": put_premium[0],
|
|
"Cost": strangle_cost[0] * -1 if short else strangle_cost[0],
|
|
"Cost Percent": round(
|
|
strangle_cost[0] / options.last_price * 100, ndigits=4
|
|
),
|
|
"Breakeven Upper": call_strike_estimate + strangle_cost[0],
|
|
"Breakeven Upper Percent": round(
|
|
(call_strike_estimate + strangle_cost[0]) / options.last_price * 100,
|
|
ndigits=4,
|
|
)
|
|
- 100,
|
|
"Breakeven Lower": put_strike_estimate - strangle_cost[0],
|
|
"Breakeven Lower Percent": -100
|
|
+ round(
|
|
(put_strike_estimate - strangle_cost[0]) / options.last_price * 100,
|
|
ndigits=4,
|
|
),
|
|
"Max Profit": abs(strangle_cost[0]) if short else np.inf,
|
|
"Max Loss": np.inf if short else strangle_cost[0] * -1,
|
|
}
|
|
)
|
|
|
|
strangle = pd.DataFrame(data=strangle.values(), index=strangle.keys()).rename(
|
|
columns={0: "Short Strangle" if short else "Long Strangle"}
|
|
)
|
|
|
|
strangle.loc["Payoff Ratio"] = round(
|
|
abs(strangle.loc["Max Profit"][0] / strangle.loc["Max Loss"][0]), ndigits=4
|
|
)
|
|
|
|
return strangle
|
|
|
|
|
|
def calculate_vertical_call_spread(
|
|
options: object,
|
|
days: Optional[int] = 30,
|
|
sold_strike: Optional[float] = 0,
|
|
bought_strike: Optional[float] = 0,
|
|
) -> pd.DataFrame:
|
|
"""Calculates the vertical call spread for the target DTE.
|
|
A bull call spread is when the sold strike is above the bought strike.
|
|
|
|
Parameters
|
|
----------
|
|
options : object
|
|
The Options data object. Use load_options_chains() to load the data.
|
|
days: int
|
|
The target number of days until expiry. This value will be used to get the nearest valid DTE.
|
|
Default is 30 days.
|
|
sold_strike: float
|
|
The target strike price for the short leg of the vertical call spread.
|
|
Default is the 5% OTM above the last price of the underlying.
|
|
bought_strike: float
|
|
The target strike price for the long leg of the vertical call spread. Default is the last price of the underlying.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the results. Strike 1 is the sold strike, and strike 2 is the bought strike.
|
|
|
|
Examples
|
|
--------
|
|
Load the data:
|
|
>>> from openbb_terminal.stocks.options.options_chains_model import OptionsChains()
|
|
>>> data = op.load_options_chains("QQQ")
|
|
|
|
For a bull call spread:
|
|
>>> data.calculate_vertical_call_spread(days=10, sold_strike=355, bought_strike=350)
|
|
|
|
For a bear call spread:
|
|
>>> data.calculate_vertical_call_spread(days=10, sold_strike=350, bought_strike=355)
|
|
"""
|
|
|
|
options = deepcopy(options)
|
|
|
|
if validate_object(options, scope="object") is False:
|
|
return pd.DataFrame()
|
|
|
|
if validate_object(options, scope="strategies") is False:
|
|
print("`last_price` was not found in the OptionsChain data object.")
|
|
return pd.DataFrame()
|
|
|
|
if isinstance(options.chains, dict):
|
|
options.chains = pd.DataFrame(options.chains)
|
|
|
|
if not days:
|
|
days = 30
|
|
|
|
if not bought_strike:
|
|
bought_strike = options.last_price * 1.0250
|
|
|
|
if not sold_strike:
|
|
sold_strike = options.last_price * 1.0750
|
|
|
|
dte_estimate = get_nearest_dte(options, days) # noqa:F841
|
|
|
|
options = validate_object(options, "nonZeroPrices", dte_estimate)
|
|
|
|
sold = get_nearest_call_strike(options, days, sold_strike)
|
|
bought = get_nearest_call_strike(options, days, bought_strike)
|
|
|
|
sold_premium = options.chains.query(
|
|
"`strike` == @sold and `dte` == @dte_estimate and `optionType` == 'call'"
|
|
)["bid"].values
|
|
bought_premium = options.chains.query(
|
|
"`strike` == @bought and `dte` == @dte_estimate and `optionType` == 'call'"
|
|
)["ask"].values
|
|
|
|
spread_cost = bought_premium - sold_premium
|
|
breakeven_price = bought + spread_cost[0]
|
|
max_profit = sold - bought - spread_cost[0]
|
|
call_spread_ = {}
|
|
if sold != bought and spread_cost != 0:
|
|
# Includees the as-of date if it is historical EOD data.
|
|
if (
|
|
options.source == "Intrinio"
|
|
and options.date != ""
|
|
or options.source == "TMX"
|
|
and options.date != ""
|
|
):
|
|
call_spread_.update({"Date": options.date})
|
|
|
|
call_spread_.update(
|
|
{
|
|
"Symbol": options.symbol,
|
|
"Underlying Price": options.last_price,
|
|
"Expiration": options.chains.query("`dte` == @dte_estimate")[
|
|
"expiration"
|
|
].unique()[0],
|
|
"DTE": dte_estimate,
|
|
"Strike 1": sold,
|
|
"Strike 2": bought,
|
|
"Strike 1 Premium": sold_premium[0],
|
|
"Strike 2 Premium": bought_premium[0],
|
|
"Cost": spread_cost[0],
|
|
"Cost Percent": round(
|
|
spread_cost[0] / options.last_price * 100, ndigits=4
|
|
),
|
|
"Breakeven Lower": breakeven_price,
|
|
"Breakeven Lower Percent": round(
|
|
(breakeven_price / options.last_price * 100) - 100, ndigits=4
|
|
),
|
|
"Breakeven Upper": np.nan,
|
|
"Breakeven Upper Percent": np.nan,
|
|
"Max Profit": max_profit,
|
|
"Max Loss": spread_cost[0] * -1,
|
|
}
|
|
)
|
|
|
|
call_spread = pd.DataFrame(
|
|
data=call_spread_.values(), index=call_spread_.keys()
|
|
).rename(columns={0: "Bull Call Spread"})
|
|
if call_spread.loc["Cost"][0] < 0:
|
|
call_spread.loc["Max Profit"][0] = call_spread.loc["Cost"][0] * -1
|
|
call_spread.loc["Max Loss"][0] = -1 * (
|
|
bought - sold + call_spread.loc["Cost"][0]
|
|
)
|
|
lower = bought if sold > bought else sold
|
|
call_spread.loc["Breakeven Upper"][0] = (
|
|
lower + call_spread.loc["Max Profit"][0]
|
|
)
|
|
call_spread.loc["Breakeven Upper Percent"][0] = round(
|
|
(breakeven_price / options.last_price * 100) - 100, ndigits=4
|
|
)
|
|
call_spread.loc["Breakeven Lower"][0] = np.nan
|
|
call_spread.loc["Breakeven Lower Percent"][0] = np.nan
|
|
call_spread.rename(
|
|
columns={"Bull Call Spread": "Bear Call Spread"}, inplace=True
|
|
)
|
|
|
|
call_spread.loc["Payoff Ratio"] = round(
|
|
abs(call_spread.loc["Max Profit"][0] / call_spread.loc["Max Loss"][0]),
|
|
ndigits=4,
|
|
)
|
|
|
|
return call_spread
|
|
return pd.DataFrame()
|
|
|
|
|
|
def calculate_vertical_put_spread(
|
|
options: object,
|
|
days: Optional[int] = 30,
|
|
sold_strike: Optional[float] = 0,
|
|
bought_strike: Optional[float] = 0,
|
|
) -> pd.DataFrame:
|
|
"""Calculates the vertical put spread for the target DTE.
|
|
A bear put spread is when the bought strike is above the sold strike.
|
|
|
|
Parameters
|
|
----------
|
|
options : object
|
|
The Options data object. Use load_options_chains() to load the data.
|
|
days: int
|
|
The target number of days until expiry. This value will be used to get the nearest valid DTE.
|
|
Default is 30 days.
|
|
sold_strike: float
|
|
The target strike price for the short leg of the vertical put spread. Default is the last price of the underlying.
|
|
bought_strike: float
|
|
The target strike price for the long leg of the vertical put spread.
|
|
Default is the 5% OTM above the last price of the underlying.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the results. Strike 1 is the sold strike, strike 2 is the bought strike.
|
|
|
|
Examples
|
|
--------
|
|
Load the data:
|
|
>>> from openbb_terminal.stocks.options.options_chains_model import OptionsChains()
|
|
>>> data = OptionsChains("QQQ")
|
|
|
|
For a bull put spread:
|
|
>>> data.get_vertical_put_spread(data, days=10, sold_strike=355, bought_strike=350)
|
|
|
|
For a bear put spread:
|
|
>>> data.get_vertical_put_spread(data, days=10, sold_strike=355, bought_strike=350)
|
|
"""
|
|
options = deepcopy(options)
|
|
|
|
if validate_object(options, scope="object") is False:
|
|
return pd.DataFrame()
|
|
|
|
if validate_object(options, scope="strategies") is False:
|
|
print("`last_price` was not found in the OptionsChain data object.")
|
|
return pd.DataFrame()
|
|
|
|
if isinstance(options.chains, dict):
|
|
options.chains = pd.DataFrame(options.chains)
|
|
|
|
if not days:
|
|
days = 30
|
|
|
|
if not bought_strike:
|
|
bought_strike = options.last_price * 0.9750
|
|
|
|
if not sold_strike:
|
|
sold_strike = options.last_price * 0.9250
|
|
|
|
dte_estimate = get_nearest_dte(options, days) # noqa:F841
|
|
options = validate_object(options, "nonZeroPrices", dte_estimate)
|
|
sold = get_nearest_put_strike(options, days, sold_strike)
|
|
bought = get_nearest_put_strike(options, days, bought_strike)
|
|
|
|
sold_premium = options.chains.query(
|
|
"`strike` == @sold and `dte` == @dte_estimate and `optionType` == 'put'"
|
|
)["bid"].values
|
|
bought_premium = options.chains.query(
|
|
"`strike` == @bought and `dte` == @dte_estimate and `optionType` == 'put'"
|
|
)["ask"].values
|
|
|
|
spread_cost = bought_premium - sold_premium
|
|
max_profit = abs(spread_cost[0])
|
|
breakeven_price = sold - max_profit
|
|
max_loss = (sold - bought - max_profit) * -1
|
|
put_spread_ = {}
|
|
if sold != bought and max_loss != 0:
|
|
# Includees the as-of date if it is historical EOD data.
|
|
if (
|
|
options.source == "Intrinio"
|
|
and options.date != ""
|
|
or options.source == "TMX"
|
|
and options.date != ""
|
|
):
|
|
put_spread_.update({"Date": options.date})
|
|
|
|
put_spread_.update(
|
|
{
|
|
"Symbol": options.symbol,
|
|
"Underlying Price": options.last_price,
|
|
"Expiration": options.chains.query("`dte` == @dte_estimate")[
|
|
"expiration"
|
|
].unique()[0],
|
|
"DTE": dte_estimate,
|
|
"Strike 1": sold,
|
|
"Strike 2": bought,
|
|
"Strike 1 Premium": sold_premium[0],
|
|
"Strike 2 Premium": bought_premium[0],
|
|
"Cost": spread_cost[0],
|
|
"Cost Percent": round(max_profit / options.last_price * 100, ndigits=4),
|
|
"Breakeven Lower": np.nan,
|
|
"Breakeven Lower Percent": np.nan,
|
|
"Breakeven Upper": breakeven_price,
|
|
"Breakeven Upper Percent": (
|
|
100 - round((breakeven_price / options.last_price) * 100, ndigits=4)
|
|
),
|
|
"Max Profit": max_profit,
|
|
"Max Loss": max_loss,
|
|
}
|
|
)
|
|
|
|
put_spread = pd.DataFrame(
|
|
data=put_spread_.values(), index=put_spread_.keys()
|
|
).rename(columns={0: "Bull Put Spread"})
|
|
if put_spread.loc["Cost"][0] > 0:
|
|
put_spread.loc["Max Profit"][0] = bought - sold - spread_cost[0]
|
|
put_spread.loc["Max Loss"][0] = spread_cost[0] * (-1)
|
|
put_spread.loc["Breakeven Lower"][0] = bought - spread_cost[0]
|
|
put_spread.loc["Breakeven Lower Percent"][0] = 100 - round(
|
|
(breakeven_price / options.last_price) * 100, ndigits=4
|
|
)
|
|
put_spread.loc["Breakeven Upper"][0] = np.nan
|
|
put_spread.loc["Breakeven Upper Percent"][0] = np.nan
|
|
put_spread.rename(
|
|
columns={"Bull Put Spread": "Bear Put Spread"}, inplace=True
|
|
)
|
|
|
|
put_spread.loc["Payoff Ratio"] = round(
|
|
abs(put_spread.loc["Max Profit"][0] / put_spread.loc["Max Loss"][0]),
|
|
ndigits=4,
|
|
)
|
|
|
|
return put_spread
|
|
return pd.DataFrame()
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def calculate_stats(options: object, by: Optional[str] = "expiration") -> pd.DataFrame:
|
|
"""Calculates basic statistics for the options chains, like OI and Vol/OI ratios.
|
|
|
|
Parameters
|
|
----------
|
|
options : object
|
|
The Options data object.
|
|
Accepts both Pydantic and Pandas object types, as defined by `load_options_chains()`.
|
|
A Pandas DataFrame, or dictionary, with the options chains data is also accepted.
|
|
by: str
|
|
Whether to calculate by strike or expiration. Default is expiration.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the calculated statistics.
|
|
|
|
Examples
|
|
--------
|
|
>>> from openbb_terminal.stocks.options.options_chains_model import OptionsChains
|
|
>>> data = OptionsChains("SPY")
|
|
|
|
By expiration date:
|
|
>>> data.calculate_stats()
|
|
|
|
By strike:
|
|
>>> data.calculate_stats(data, "strike")
|
|
"""
|
|
|
|
if by not in ["expiration", "strike"]:
|
|
print("Invalid choice. The supported methods are: [expiration, strike]")
|
|
return pd.DataFrame()
|
|
|
|
chains = deepcopy(options)
|
|
last_price = chains.last_price if hasattr(chains, "last_price") else None
|
|
chains = validate_object(chains, scope="chains")
|
|
|
|
if chains.empty or chains is None:
|
|
return chains == pd.DataFrame()
|
|
|
|
stats = pd.DataFrame()
|
|
|
|
stats["Puts OI"] = (
|
|
chains[chains["optionType"] == "put"]
|
|
.groupby(f"{by}")[["openInterest"]]
|
|
.sum(numeric_only=True)
|
|
.astype(int)
|
|
)
|
|
stats["Calls OI"] = (
|
|
chains[chains["optionType"] == "call"]
|
|
.groupby(f"{by}")[["openInterest"]]
|
|
.sum(numeric_only=True)
|
|
.astype(int)
|
|
)
|
|
stats["Total OI"] = stats["Calls OI"] + stats["Puts OI"]
|
|
stats["OI Ratio"] = round(stats["Puts OI"] / stats["Calls OI"], 2)
|
|
|
|
if by == "expiration" and last_price:
|
|
stats["Puts OTM"] = (
|
|
chains.query("`optionType` == 'put' & `strike` < @last_price")
|
|
.groupby("expiration")[["openInterest"]]
|
|
.sum(numeric_only=True)
|
|
)
|
|
stats["Calls OTM"] = (
|
|
chains.query("`optionType` == 'call' & `strike` > @last_price")
|
|
.groupby("expiration")[["openInterest"]]
|
|
.sum(numeric_only=True)
|
|
)
|
|
stats["Puts ITM"] = (
|
|
chains.query("`optionType` == 'put' & `strike` > @last_price")
|
|
.groupby("expiration")[["openInterest"]]
|
|
.sum(numeric_only=True)
|
|
)
|
|
stats["Calls ITM"] = (
|
|
chains.query("`optionType` == 'call' & `strike` < @last_price")
|
|
.groupby("expiration")[["openInterest"]]
|
|
.sum(numeric_only=True)
|
|
)
|
|
stats["OTM Ratio"] = round(stats["Puts OTM"] / stats["Calls OTM"], 2)
|
|
stats["ITM Percent"] = round(
|
|
(stats["Puts ITM"] + stats["Calls ITM"]) / stats["Total OI"] * 100, 2
|
|
)
|
|
|
|
stats["Puts Volume"] = (
|
|
chains[chains["optionType"] == "put"]
|
|
.groupby(f"{by}")[["volume"]]
|
|
.sum(numeric_only=True)
|
|
.astype(int)
|
|
)
|
|
stats["Calls Volume"] = (
|
|
chains[chains["optionType"] == "call"]
|
|
.groupby(f"{by}")
|
|
.sum(numeric_only=True)[["volume"]]
|
|
.astype(int)
|
|
)
|
|
stats["Total Volume"] = stats["Calls Volume"] + stats["Puts Volume"]
|
|
stats["Volume Ratio"] = round(stats["Puts Volume"] / stats["Calls Volume"], 2)
|
|
stats["Vol-OI Ratio"] = round(stats["Total Volume"] / stats["Total OI"], 2)
|
|
stats.rename_axis("Expiration", inplace=True)
|
|
if by == "strike":
|
|
stats.rename_axis(index={"Expiration": "Strike"}, inplace=True)
|
|
return stats.replace([np.nan, np.inf], None)
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def get_strategies(
|
|
options: object,
|
|
days: Optional[list[int]] = None,
|
|
straddle_strike: Optional[float] = 0,
|
|
strangle_moneyness: Optional[list[float]] = None,
|
|
vertical_calls: Optional[list[float]] = None,
|
|
vertical_puts: Optional[list[float]] = None,
|
|
) -> pd.DataFrame:
|
|
"""Gets options strategies for all, or a list of, DTE(s).
|
|
Currently supports straddles, strangles, and vertical spreads.
|
|
Multiple strategies, expirations, and % moneyness can be returned.
|
|
To get short options, use a negative value for the `straddle_strike` price or `strangle_moneyness`.
|
|
A sold call strike that is lower than the bought strike, or a sold put strike that is higher than the bought strike,
|
|
is a bearish vertical spread.
|
|
|
|
Parameters
|
|
----------
|
|
options: object
|
|
The Options data object. Use `load_options_chains()` to load the data.
|
|
days: list[int]
|
|
List of DTE(s) to get strategies for. Enter a single value, or multiple as a list. Defaults to all.
|
|
strike_price: float
|
|
The target strike price. Defaults to the last price of the underlying stock.
|
|
strangle_moneyness: list[float]
|
|
List of OTM moneyness to target, expressed as a percent value between 0 and 100.
|
|
Enter a single value, or multiple as a list. Defaults to 5.
|
|
vertical_calls: list[float]
|
|
Call strikes for vertical spreads, listed as [sold strike, bought strike].
|
|
vertical_puts: list[float]
|
|
Put strikes for vertical spreads, listed as [sold strike, bought strike].
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the results.
|
|
|
|
Examples
|
|
--------
|
|
Load data
|
|
>>> from openbb_terminal.stocks.options.options_chains_model import OptionsChains
|
|
>>> data = OptionsChains("SPY")
|
|
|
|
Return just straddles
|
|
>>> data.get_strategies()
|
|
|
|
Return strangles
|
|
>>> data.get_strategies()
|
|
|
|
Return multiple values for both moneness and days:
|
|
>>> data.get_strategies(days = [10,30,60,90], moneyness = [2.5,-5,10,-20])
|
|
|
|
Return vertical spreads for all expirations.
|
|
>>> data.get_strategies(vertical_calls=[430,427], vertical_puts=[420,426])
|
|
"""
|
|
options = deepcopy(options)
|
|
|
|
if validate_object(options, scope="object") is False:
|
|
return pd.DataFrame()
|
|
|
|
if validate_object(options, scope="strategies") is False:
|
|
print("`last_price` was not found in the Options data object.")
|
|
return pd.DataFrame()
|
|
|
|
if isinstance(options.chains, dict):
|
|
options.chains = pd.DataFrame(options.chains)
|
|
|
|
if strangle_moneyness is None:
|
|
strangle_moneyness = [0.0]
|
|
|
|
if days is None:
|
|
days = options.chains["dte"].unique().tolist()
|
|
|
|
# Allows a single input to be passed instead of a list.
|
|
days = [days] if isinstance(days, int) else days # type: ignore[list-item]
|
|
|
|
if not isinstance(vertical_calls, (list, float)) and vertical_calls is not None:
|
|
print(
|
|
"Two strike prices are required. Enter the sold price first, then the bought price."
|
|
)
|
|
return pd.DataFrame()
|
|
if not isinstance(vertical_puts, (list, float)) and vertical_puts is not None:
|
|
print(
|
|
"Two strike prices are required. Enter the sold price first, then the bought price."
|
|
)
|
|
return pd.DataFrame()
|
|
|
|
if strangle_moneyness is not None:
|
|
strangle_moneyness = (
|
|
[strangle_moneyness] # type: ignore[list-item]
|
|
if not isinstance(strangle_moneyness, list)
|
|
else strangle_moneyness
|
|
)
|
|
|
|
days_list = []
|
|
|
|
strategies = pd.DataFrame()
|
|
straddles = pd.DataFrame()
|
|
strangles = pd.DataFrame()
|
|
strangles_ = pd.DataFrame()
|
|
call_spreads = pd.DataFrame()
|
|
put_spreads = pd.DataFrame()
|
|
|
|
for day in days:
|
|
if day == 0:
|
|
day = -1
|
|
days_list.append(get_nearest_dte(options, day))
|
|
days = list(set(days_list))
|
|
days.sort()
|
|
|
|
if vertical_calls is not None:
|
|
cStrike1 = vertical_calls[0]
|
|
cStrike2 = vertical_calls[1]
|
|
for day in days:
|
|
call_spread = calculate_vertical_call_spread(
|
|
options, day, cStrike1, cStrike2
|
|
).transpose()
|
|
call_spreads = pd.concat([call_spreads, call_spread])
|
|
|
|
if vertical_puts:
|
|
pStrike1 = vertical_puts[0]
|
|
pStrike2 = vertical_puts[1]
|
|
for day in days:
|
|
put_spread = calculate_vertical_put_spread(
|
|
options, day, pStrike1, pStrike2
|
|
).transpose()
|
|
put_spreads = pd.concat([put_spreads, put_spread])
|
|
|
|
if straddle_strike:
|
|
straddle_strike = (
|
|
options.last_price if straddle_strike == 0 else straddle_strike
|
|
)
|
|
for day in days:
|
|
straddle = calculate_straddle(options, day, straddle_strike).transpose()
|
|
if straddle.iloc[0]["Cost"] != 0:
|
|
straddles = pd.concat(
|
|
[
|
|
straddles,
|
|
straddle,
|
|
]
|
|
)
|
|
|
|
if strangle_moneyness and strangle_moneyness[0] != 0:
|
|
for day in days:
|
|
for moneyness in strangle_moneyness:
|
|
strangle = calculate_strangle(options, day, moneyness).transpose()
|
|
if strangle.iloc[0]["Cost"] != 0:
|
|
strangles_ = pd.concat(
|
|
[
|
|
strangles_,
|
|
strangle,
|
|
]
|
|
)
|
|
|
|
strangles = pd.concat([strangles, strangles_])
|
|
strangles = strangles.query("`Strike 1` != `Strike 2`").drop_duplicates()
|
|
|
|
strategies = pd.concat([straddles, strangles, call_spreads, put_spreads])
|
|
|
|
if strategies.empty:
|
|
print("No strategy was selected, returning all ATM straddles.")
|
|
return get_strategies(options, straddle_strike=options.last_price)
|
|
|
|
strategies = strategies.reset_index().rename(columns={"index": "Strategy"})
|
|
strategies = (
|
|
strategies.set_index(["Expiration", "DTE"])
|
|
.sort_index()
|
|
.drop(columns=["Symbol"])
|
|
)
|
|
return strategies.reset_index()
|
|
|
|
|
|
@log_start_end(log=logger)
|
|
def calculate_skew(
|
|
options, expiration: Optional[str] = "", moneyness: Optional[float] = None
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Returns the skewness of the options, either vertical or horizontal.
|
|
|
|
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 is supplied.
|
|
It is expressed as the difference between skews of two equidistant OTM strikes (the closest call and put).
|
|
|
|
Parameters
|
|
-----------
|
|
options: object
|
|
The Options data object. Use `load_options_chains()` to load the data.
|
|
expiration: str
|
|
The expiration date to target. Defaults to all.
|
|
moneyness: float
|
|
The moneyness to target for calculating horizontal skew. This parameter overrides a defined expiration date.
|
|
|
|
Returns
|
|
--------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the results.
|
|
|
|
Examples
|
|
----------
|
|
>>> from openbb_terminal.stocks.options import options_chains_model as op
|
|
>>> data = op.load_options_chains("SPY")
|
|
|
|
Vertical skew at a given expiry:
|
|
>>> skew = op.calculate_skew(data, "2025-12-19")
|
|
|
|
Vertical skew at all expirations:
|
|
>>> skew = op.calculate_skew(data)
|
|
|
|
Horizontal skew at a given % OTM:
|
|
>>> skew = op.calculate_skew(data, moneyness = 10)
|
|
"""
|
|
|
|
options = deepcopy(options)
|
|
|
|
if validate_object(options, scope="object") is False:
|
|
return pd.DataFrame()
|
|
|
|
if options.hasIV is False:
|
|
print(
|
|
"Options data object does not have Implied Volatility and is required for this function."
|
|
)
|
|
return pd.DataFrame()
|
|
|
|
options.chains = validate_object(options.chains, scope="chains")
|
|
options.chains = options.chains[options.chains["impliedVolatility"] > 0]
|
|
days = options.chains["dte"].unique().tolist()
|
|
|
|
if moneyness is not None:
|
|
calls = pd.DataFrame()
|
|
atm_call_iv = pd.DataFrame()
|
|
puts = pd.DataFrame()
|
|
atm_put_iv = pd.DataFrame()
|
|
skew_df = pd.DataFrame()
|
|
strikes = get_nearest_otm_strike(options, moneyness)
|
|
for day in days:
|
|
atm_call_strike = get_nearest_call_strike( # noqa:F841
|
|
options, day, options.last_price
|
|
)
|
|
call_strike = get_nearest_call_strike( # noqa:F841
|
|
options, day, strikes["call"]
|
|
)
|
|
call_iv = options.chains[options.chains["dte"] == day].query(
|
|
"`optionType` == 'call' & `strike` == @call_strike"
|
|
)[["expiration", "strike", "impliedVolatility"]]
|
|
calls = pd.concat([calls, call_iv])
|
|
atm_call = options.chains[options.chains["dte"] == day].query(
|
|
"`optionType` == 'call' & `strike` == @atm_call_strike"
|
|
)[["expiration", "strike", "impliedVolatility"]]
|
|
atm_call_iv = pd.concat([atm_call_iv, atm_call])
|
|
atm_put_strike = get_nearest_put_strike( # noqa:F841
|
|
options, day, options.last_price
|
|
)
|
|
put_strike = get_nearest_put_strike( # noqa:F841
|
|
options, day, strikes["put"]
|
|
)
|
|
put_iv = options.chains[options.chains["dte"] == day].query(
|
|
"`optionType` == 'put' & `strike` == @put_strike"
|
|
)[["expiration", "strike", "impliedVolatility"]]
|
|
puts = pd.concat([puts, put_iv])
|
|
atm_put = options.chains[options.chains["dte"] == day].query(
|
|
"`optionType` == 'put' & `strike` == @atm_put_strike"
|
|
)[["expiration", "strike", "impliedVolatility"]]
|
|
atm_put_iv = pd.concat([atm_put_iv, atm_put])
|
|
|
|
calls = calls.set_index("expiration")
|
|
atm_call_iv = atm_call_iv.set_index("expiration")
|
|
puts = puts.set_index("expiration")
|
|
atm_put_iv = atm_put_iv.set_index("expiration")
|
|
skew_df["Call Strike"] = calls["strike"]
|
|
skew_df["Call IV"] = calls["impliedVolatility"]
|
|
skew_df["Call ATM IV"] = atm_call_iv["impliedVolatility"]
|
|
skew_df["Call Skew"] = skew_df["Call IV"] - skew_df["Call ATM IV"]
|
|
skew_df["Put Strike"] = puts["strike"]
|
|
skew_df["Put IV"] = puts["impliedVolatility"]
|
|
skew_df["Put ATM IV"] = atm_put_iv["impliedVolatility"]
|
|
skew_df["Put Skew"] = skew_df["Put IV"] - skew_df["Put ATM IV"]
|
|
skew_df["ATM Skew"] = skew_df["Call ATM IV"] - skew_df["Put ATM IV"]
|
|
skew_df["IV Skew"] = skew_df["Call Skew"] - skew_df["Put Skew"]
|
|
|
|
return skew_df
|
|
|
|
calls = options.chains[options.chains["optionType"] == "call"]
|
|
puts = options.chains[options.chains["optionType"] == "put"]
|
|
call_skew = pd.DataFrame()
|
|
put_skew = pd.DataFrame()
|
|
skew_df = pd.DataFrame()
|
|
|
|
for day in days:
|
|
atm_call_strike = get_nearest_call_strike(options, day) # noqa:F841
|
|
call = calls[calls["dte"] == day][
|
|
["expiration", "optionType", "strike", "impliedVolatility"]
|
|
]
|
|
call = call.set_index("expiration")
|
|
call_atm_iv = call.query("`strike` == @atm_call_strike")[
|
|
"impliedVolatility"
|
|
].iloc[0]
|
|
call["ATM IV"] = call_atm_iv
|
|
call["Skew"] = call["impliedVolatility"] - call["ATM IV"]
|
|
call_skew = pd.concat([call_skew, call])
|
|
atm_put_strike = get_nearest_put_strike(options, day) # noqa:F841
|
|
put = puts[puts["dte"] == day][
|
|
["expiration", "optionType", "strike", "impliedVolatility"]
|
|
]
|
|
put = put.set_index("expiration")
|
|
put_atm_iv = put.query("`strike` == @atm_put_strike")["impliedVolatility"].iloc[
|
|
0
|
|
]
|
|
put["ATM IV"] = put_atm_iv
|
|
put["Skew"] = put["impliedVolatility"] - put["ATM IV"]
|
|
put_skew = pd.concat([put_skew, put])
|
|
|
|
call_skew = call_skew.set_index(["strike", "optionType"], append=True)
|
|
put_skew = put_skew.set_index(["strike", "optionType"], append=True)
|
|
skew_df = pd.concat([call_skew, put_skew]).sort_index().reset_index()
|
|
cols = ["Expiration", "Strike", "Option Type", "IV", "ATM IV", "Skew"]
|
|
skew_df.columns = cols
|
|
if expiration != "":
|
|
if expiration not in options.expirations:
|
|
print(expiration, "is not a valid expiration.")
|
|
return skew_df
|
|
return skew_df.query("`Expiration` == @expiration")
|
|
|
|
return skew_df
|
|
|
|
|
|
class OptionsChains(Options): # pylint: disable=too-few-public-methods
|
|
"""OptionsChains class for loading and interacting with the Options data object.
|
|
|
|
Parameters
|
|
----------
|
|
symbol: str
|
|
The ticker symbol to load the data for.
|
|
source: str
|
|
The source for the data. Defaults to "CBOE". ["CBOE", "Intrinio", "Nasdaq", "TMX", "Tradier", "YahooFinance"]
|
|
date: str
|
|
The date for EOD chains data. Only available for "Intrinio" and "TMX".
|
|
pydantic: bool
|
|
Whether to return as a Pydantic Model or as a Pandas object. Defaults to False.
|
|
|
|
Returns
|
|
-------
|
|
Object: Options
|
|
chains: pd.DataFrame
|
|
The complete options chain for the ticker.
|
|
expirations: list[str]
|
|
List of unique expiration dates. (YYYY-MM-DD)
|
|
strikes: list[float]
|
|
List of unique strike prices.
|
|
last_price: float
|
|
The last price of the underlying asset.
|
|
underlying_name: str
|
|
The name of the underlying asset.
|
|
underlying_price: pd.Series
|
|
The price and recent performance of the underlying asset.
|
|
hasIV: bool
|
|
Returns implied volatility.
|
|
hasGreeks: bool
|
|
Returns greeks data.
|
|
symbol: str
|
|
The symbol entered by the user.
|
|
source: str
|
|
The source of the data.
|
|
date: str
|
|
The date, when the chains data is historical EOD.
|
|
SYMBOLS: pd.DataFrame
|
|
The symbol directory for the source, when available.
|
|
|
|
Methods
|
|
-------
|
|
get_stats: Callable
|
|
Function to get a table of summary statistics, by strike or by expiration.
|
|
get_straddle: Callable
|
|
Function to get straddles and the payoff profile.
|
|
get_strangle: Callable
|
|
Function to calculate strangles and the payoff profile.
|
|
get_vertical_call_spread: Callable
|
|
Function to get vertical call spreads.
|
|
get_vertical_put_spreads: Callable
|
|
Function to get vertical put spreads.
|
|
get_strategies: Callable
|
|
Function to get multiple straddles and strangles at different expirations and moneyness.
|
|
get_skew: Callable
|
|
Function to get the vertical or horizontal skewness of the options.
|
|
|
|
Examples
|
|
--------
|
|
>>> from openbb_terminal.stocks.options.options_chains_model import OptionsChains
|
|
>>> spy = OptionsChains("SPY")
|
|
>>> spy.__dict__
|
|
|
|
>>> xiu = OptionsChains("xiu.to", "TMX")
|
|
>>> xiu.get_straddle()
|
|
"""
|
|
|
|
def __init__(self, symbol, source="CBOE", date="", pydantic=False):
|
|
try:
|
|
options = load_options_chains(symbol, source, date, pydantic)
|
|
items = list(options.__dict__.keys())
|
|
for item in items:
|
|
setattr(self, item, options.__dict__[item])
|
|
if hasattr(self, "date") is False:
|
|
setattr(self, "date", "")
|
|
except Exception:
|
|
self.chains = pd.DataFrame()
|
|
|
|
def __repr__(self) -> str:
|
|
return f"OptionsChains(symbol={self.symbol}, source ={self.source})"
|
|
|
|
def get_stats(self, by="expiration", query=None):
|
|
"""Calculates basic statistics for the options chains, like OI and Vol/OI ratios.
|
|
|
|
Parameters
|
|
----------
|
|
by: str
|
|
Whether to calculate by strike or expiration. Default is expiration.
|
|
query: DataFrame
|
|
Entry point to perform DataFrame operations on self.chains at the input stage.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the calculated statistics.
|
|
|
|
Examples
|
|
--------
|
|
>>> from openbb_terminal.stocks.options.options_chains_model import OptionsChains
|
|
>>> data = OptionsChains("SPY")
|
|
|
|
By expiration date:
|
|
>>> data.calculate_stats()
|
|
|
|
By strike:
|
|
>>> data.calculate_stats(data, "strike")
|
|
|
|
Query:
|
|
>>> data = OptionsChains("XIU", "TMX")
|
|
>>> data.get_stats(query = data.chains.query("`openInterest` > 0"), by = "strike")
|
|
"""
|
|
|
|
if query is not None:
|
|
if isinstance(query, pd.DataFrame):
|
|
return calculate_stats(query, by)
|
|
print("Query must be passed a Pandas DataFrame with chains data.")
|
|
|
|
return calculate_stats(self, by)
|
|
|
|
def get_straddle(self, days=0, strike=0):
|
|
"""Calculates the cost of a straddle and its payoff profile. Use a negative strike price for short options.
|
|
Requires the Options data object.
|
|
|
|
Parameters
|
|
----------
|
|
days: int
|
|
The target number of days until expiry. Default is 30 days.
|
|
strike: float
|
|
The target strike price. Enter a negative value for short options.
|
|
Default is the last price of the underlying stock.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the results. Strike1 is the nearest call strike, strike2 is the nearest put strike.
|
|
|
|
Examples
|
|
--------
|
|
>>> from openbb_terminal.sdk import openbb
|
|
>>> data = openbb.stocks.options.load_options_chains('SPY')
|
|
>>> data.get_straddle()
|
|
"""
|
|
|
|
return calculate_straddle(self, days, strike)
|
|
|
|
def get_strangle(self, days=0, moneyness=0):
|
|
"""Calculates the cost of a strangle and its payoff profile.
|
|
Use a negative value for moneyness for short options.
|
|
|
|
Requires the Options data object.
|
|
|
|
Parameters
|
|
----------
|
|
days: int
|
|
The target number of days until expiry. Default is 30 days.
|
|
moneyness: float
|
|
The percentage of OTM moneyness, expressed as a percent between -100 < 0 < 100.
|
|
Enter a negative number for short options.
|
|
Default is 5.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the results.
|
|
Strike 1 is the nearest call strike, and strike 2 is the nearest put strike.
|
|
|
|
Examples
|
|
--------
|
|
>>> data = openbb.stocks.options.load_options_chains("SPY")
|
|
>>> data.get_strangle()
|
|
"""
|
|
|
|
return calculate_strangle(self, days, moneyness)
|
|
|
|
def get_vertical_call_spread(self, days=0, sold_strike=0, bought_strike=0):
|
|
"""Calculates the vertical call spread for the target DTE.
|
|
A bull call spread is when the sold strike is above the bought strike.
|
|
|
|
Parameters
|
|
----------
|
|
days: int
|
|
The target number of days until expiry. This value will be used to get the nearest valid DTE.
|
|
Default is 30 days.
|
|
sold_strike: float
|
|
The target strike price for the short leg of the vertical call spread.
|
|
Default is the 5% OTM above the last price of the underlying.
|
|
bought_strike: float
|
|
The target strike price for the long leg of the vertical call spread.
|
|
Default is the last price of the underlying.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the results. Strike 1 is the sold strike, and strike 2 is the bought strike.
|
|
|
|
Examples
|
|
--------
|
|
Load the data:
|
|
>>> from openbb_terminal.stocks.options.options_chains_model import OptionsChains
|
|
>>> data = OptionsChains("SPY")
|
|
|
|
For a bull call spread:
|
|
>>> data.get_vertical_call_spread(days=10, sold_strike=355, bought_strike=350)
|
|
|
|
For a bear call spread:
|
|
>>> data.get_vertical_call_spread(days=10, sold_strike=350, bought_strike=355)
|
|
"""
|
|
|
|
return calculate_vertical_call_spread(self, days, sold_strike, bought_strike)
|
|
|
|
def get_vertical_put_spread(self, days=0, sold_strike=0, bought_strike=0):
|
|
"""Calculates the vertical put spread for the target DTE.
|
|
A bear put spread is when the bought strike is above the sold strike.
|
|
|
|
Parameters
|
|
----------
|
|
days: int
|
|
The target number of days until expiry. This value will be used to get the nearest valid DTE.
|
|
Default is 30 days.
|
|
sold_strike: float
|
|
The target strike price for the short leg of the vertical put spread.
|
|
Default is the last price of the underlying.
|
|
bought_strike: float
|
|
The target strike price for the long leg of the vertical put spread.
|
|
Default is the 5% OTM above the last price of the underlying.
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the results. Strike 1 is the sold strike, strike 2 is the bought strike.
|
|
|
|
Examples
|
|
--------
|
|
Load the data:
|
|
>>> from openbb_terminal.stocks.options.options_chains_model import OptionsChains
|
|
>>> data = OptionsChains("QQQ")
|
|
|
|
For a bull put spread:
|
|
>>> data.get_vertical_put_spread(days=10, sold_strike=355, bought_strike=350)
|
|
|
|
For a bear put spread:
|
|
>>> data.get_vertical_put_spread(days=10, sold_strike=355, bought_strike=350)
|
|
"""
|
|
|
|
return calculate_vertical_put_spread(self, days, sold_strike, bought_strike)
|
|
|
|
def get_strategies(
|
|
self,
|
|
days: Optional[list[int]] = None,
|
|
straddle_strike: Optional[float] = None,
|
|
strangle_moneyness: Optional[float] = None,
|
|
vertical_calls: Optional[list[float]] = None,
|
|
vertical_puts: Optional[list[float]] = None,
|
|
):
|
|
"""Gets options strategies for all, or a list of, DTE(s).
|
|
Currently supports straddles, strangles, and vertical spreads.
|
|
Multiple strategies, expirations, and % moneyness can be returned.
|
|
To get short options, use a negative value for the `straddle_strike` price or `strangle_moneyness`.
|
|
A sold call strike that is lower than the bought strike,
|
|
and a sold put strike that is higher than the bought strike,
|
|
are both bearish.
|
|
|
|
Parameters
|
|
----------
|
|
days: list[int]
|
|
List of DTE(s) to get strategies for. Enter a single value, or multiple as a list. Defaults to all.
|
|
This is the only shared parameter across strategies.
|
|
strike_price: float
|
|
The target strike price for straddles. Defaults to the last price of the underlying stock.
|
|
Enter a negative value for short straddles.
|
|
strangle_moneyness: list[float]
|
|
List of OTM moneyness to target, expressed as a percent value between 0 and 100.
|
|
Enter a single value, or multiple as a list. Defaults to 5. Enter a negative value for short straddles.
|
|
vertical_calls: list[float]
|
|
Call strikes for vertical spreads, listed as [sold strike, bought strike].
|
|
vertical_puts: list[float]
|
|
Put strikes for vertical spreads, listed as [sold strike, bought strike].
|
|
|
|
Returns
|
|
-------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the results.
|
|
|
|
Examples
|
|
--------
|
|
Load data
|
|
>>> from openbb_terminal.stocks.options.options_chains_model import OptionsChains
|
|
>>> data = OptionsChains("SPY")
|
|
|
|
Return just long straddles for every expiry.
|
|
>>> data.get_strategies()
|
|
|
|
Return strangles for every expiry.
|
|
>>> data.get_strategies(strangle_moneyness = [2.5,5,10])
|
|
|
|
Return multiple values for both moneness and days:
|
|
>>> data.get_strategies(days = [10,30,60,90], moneyness = [2.5,-5,10,-20])
|
|
|
|
Return vertical spreads for all expirations.
|
|
>>> data.get_strategies(vertical_calls=[430,427], vertical_puts=[420,426])
|
|
"""
|
|
|
|
if strangle_moneyness is None:
|
|
strangle_moneyness = 0
|
|
|
|
return get_strategies(
|
|
self,
|
|
days,
|
|
straddle_strike,
|
|
strangle_moneyness,
|
|
vertical_calls,
|
|
vertical_puts,
|
|
)
|
|
|
|
def get_skew(
|
|
self, expiration: Optional[str] = "", moneyness: Optional[float] = None
|
|
):
|
|
"""
|
|
Returns the skewness of the options, either vertical or horizontal.
|
|
|
|
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 is supplied.
|
|
It is expressed as the difference between skews of two equidistant OTM strikes (the closest call and put).
|
|
|
|
Parameters
|
|
-----------
|
|
expiration: str
|
|
The expiration date to target. Defaults to all.
|
|
moneyness: float
|
|
The moneyness to target for calculating horizontal skew. This parameter overrides a defined expiration date.
|
|
|
|
Returns
|
|
--------
|
|
pd.DataFrame
|
|
Pandas DataFrame with the results.
|
|
|
|
Examples
|
|
----------
|
|
>>> from openbb_terminal.stocks.options.options_chains_model import OptionsChains
|
|
>>> data = OptionsChains("SPY")
|
|
|
|
Vertical skew at a given expiry:
|
|
>>> skew = data.get_skew("2025-12-19")
|
|
|
|
Vertical skew at all expirations:
|
|
>>> skew = data.get_skew()
|
|
|
|
Horizontal skew at a given % OTM:
|
|
>>> skew = data.get_skew(moneyness = 10)
|
|
"""
|
|
return calculate_skew(self, expiration, moneyness)
|