Improve options menu slightly + close some bugs (#4793)

* platform to logs

* dte not /365 and delete code i never got to

* 4 decimals on chains

* Change options default load to the defaut source

* mypy

* Less polluting of logs

* #4420

* #4811

* Fix test

* Remove unnecessary log decs

* Save us money

* whoops
This commit is contained in:
James Maslek
2023-04-18 08:54:44 -04:00
committed by GitHub
parent 229f732b74
commit da7fb3a831
20 changed files with 1927 additions and 2147 deletions

View File

@@ -24,6 +24,7 @@ class SystemModel(BaseModel):
# System section
OS: str = str(platform.system())
PYTHON_VERSION: str = str(platform.python_version())
PLATFORM: str = str(platform.platform())
# OpenBB section
VERSION = "3.0.0rc2"

View File

@@ -15,7 +15,6 @@ from openbb_terminal.rich_config import console
logger = logging.getLogger(__name__)
@log_start_end(log=logger)
def get_currency_list() -> List:
"""Load AV currency codes from a local file."""
path = os.path.join(os.path.dirname(__file__), "data/av_forex_currencies.csv")

View File

@@ -1075,7 +1075,11 @@ class BaseController(metaclass=ABCMeta):
an_input = candidate_input
else:
an_input = similar_cmd[0]
if not self.contains_keys(an_input):
if not self.contains_keys(an_input) and an_input not in [
"exit",
"quit",
"help",
]:
logger.warning("Replacing by %s", an_input)
console.print(f"[green]Replacing by '{an_input}'.[/green]\n")
self.queue.insert(0, an_input)

View File

@@ -63,7 +63,6 @@ def getFINRAweeks(tier: str = "T1", is_ats: bool = True) -> List:
return response.json() if response.status_code == 200 else list()
@log_start_end(log=logger)
def getFINRAdata_offset(
start_date: str,
tier: str = "T1",

View File

@@ -82,7 +82,7 @@ class DataBento(BaseModel):
"""Prepares the request to be made. I am using their https interface instead of their python client.
Their next update will change symbol.V.0 to symbol.C.0"""
base_url = "https://hist.databento.com/v0/timeseries.stream"
base_url = "https://hist.databento.com/v0/timeseries.get_range"
symbol = self.symbol + ".V.0" if self.exchange == "GLBX.MDP3" else self.symbol
params = {
"dataset": self.exchange,

View File

@@ -487,7 +487,6 @@ def get_earnings(symbol: str, quarterly: bool = False) -> pd.DataFrame:
return df_fa
@log_start_end(log=logger)
def df_values(
df: pd.DataFrame, item: str, index: int = 0, length: int = 2
) -> List[int]:

View File

@@ -139,7 +139,6 @@ CURRENCIES = [
]
@log_start_end(log=logger)
def string_float(string: str) -> float:
"""Convert a string to a float
@@ -190,7 +189,6 @@ def insert_row(
return df
@log_start_end(log=logger)
def set_cell(
ws: worksheet,
cell: str,
@@ -566,7 +564,6 @@ def get_value(df: pd.DataFrame, row: str, column: int) -> Tuple[float, float]:
return fin_val1, fin_val2
@log_start_end(log=logger)
def frac(num: float, denom: float) -> Union[str, float]:
"""
Converts a numerator and a denominator in a fraction, checking for invalid denominators

View File

@@ -982,7 +982,6 @@ class CreateExcelFA:
if text:
self.custom_exp(row, text)
@log_start_end(log=logger)
def title_to_row(self, title: str) -> int:
df = (
self.df["IS"]

View File

@@ -64,7 +64,6 @@ def display_fundamentals(
fundamentals = fundamentals.iloc[:, :limit]
fundamentals = fundamentals[fundamentals.columns[::-1]]
if statement == "income":
fundamentals.index = [
stocks_helper.INCOME_PLOT["Polygon"][i]

View File

@@ -39,7 +39,6 @@ columns_to_drop = [
]
@log_start_end(log=logger)
def calculate_dte(chain_df: pd.DataFrame) -> pd.DataFrame:
"""Adds a column calculating the difference between expiration and the date data is from
@@ -63,8 +62,7 @@ def calculate_dte(chain_df: pd.DataFrame) -> pd.DataFrame:
lambda row: (
datetime.strptime(row["expiration"], "%Y-%m-%d")
- datetime.strptime(row["date"], "%Y-%m-%d")
).days
/ 365,
).days,
axis=1,
)
return chain_df

View File

@@ -47,7 +47,6 @@ sorted_chain_columns = [
]
@log_start_end(log=logger)
def get_dte_from_expiration(date: str) -> float:
"""
Converts a date to total days until the option would expire.

View File

@@ -30,7 +30,6 @@ def get_strikes(
return min_strike, max_strike
@log_start_end(log=logger)
def get_loss_at_strike(strike: float, chain: pd.DataFrame) -> float:
"""Function to get the loss at the given expiry
@@ -86,7 +85,6 @@ def calculate_max_pain(chain: pd.DataFrame) -> Union[int, float]:
return max_pain
@log_start_end(log=logger)
def convert(orig: str, to: str) -> float:
"""Convert a string to a specific type of number
Parameters
@@ -596,171 +594,3 @@ class Option:
* norm.pdf(self.d1)
/ self._sigma
)
def delta_at_strike(
strike: Union[float, pd.Series, np.ndarray],
underlying_price: float,
iv: Union[float, pd.Series, np.ndarray],
dte: Union[float, pd.Series, np.ndarray],
option: Union[str, pd.Series, np.ndarray] = "call",
rf_rate: float = 0.03,
) -> Union[float, pd.Series, np.ndarray]:
"""Gets delta at a theoretical underlying
Parameters
----------
strike : float
Option strike price
underlying_price : float
Underlying price for Black-Scholes calculation
iv : float
Implied volatility for calculation
dte : float
Time to expiration in years
option : str, optional
Type of option, either "call" or "put", by default "call"
rf_rate : float, optional
Risk free rate for calculation, by default 0.03
Returns
-------
float
Value of delta at theoretical underlying
"""
d1 = (np.log(underlying_price / strike) + (rf_rate + 0.5 * iv**2) * dte) / (
iv * np.sqrt(dte)
)
return np.where(option == "call", norm.cdf(d1), norm.cdf(d1) - 1)
def gamma_at_strike(
strike: Union[float, pd.Series, np.ndarray],
underlying_price: float,
iv: Union[float, pd.Series, np.ndarray],
dte: Union[float, pd.Series, np.ndarray],
rf_rate: float = 0.03,
) -> Union[float, pd.Series, np.ndarray]:
"""Get gamma at a theoretical underlying
Parameters
----------
strike : Union[float,pd.Series]
Option strike price
underlying_price : float
Underlying price for Black-Scholes calculation
iv : float
Implied volatility for calculation
dte : float
Time to expiration in years
rf_rate : float, optional
Risk free rate for calculation, by default 0.03
Returns
-------
float
Value of delta at theoretical underlying
"""
d1 = (np.log(underlying_price / strike) + (rf_rate + 0.5 * iv**2) * dte) / (
iv * np.sqrt(dte)
)
return norm.pdf(d1) / (underlying_price * iv * np.sqrt(dte))
@log_start_end(log=logger)
def get_abs_market_delta(
underlying_price: float, df: pd.DataFrame, rf_rate: float = 0.03
) -> float:
"""
Get the total market delta across entire chain.
In this calculation, we find delta at a theoretical underlying and then take the sum of delta x open interest.
Delta neutral is the theoretical underlying where this sum is 0. To prepare the minimization, we take the absolute
value of this sum.
Parameters
----------
underlying_price : float
Theoretical underlying price
df : pd.DataFrame
Dataframe of option chain. Requires the following columns: impliedVolatility,dte,openInterest,strike,optionType
rf_rate : float, optional
Optional risk free rate for calculation, by default 0.03
Returns
-------
float
Market delta at underlying price.
Raises
------
ValueError
If any of the required columns are not in the dataframe
"""
for col in ["impliedVolatility", "dte", "openInterest", "strike", "optionType"]:
if col not in df.columns:
raise ValueError(f"{col} needs to be in df")
df["new_delta"] = delta_at_strike(
df.strike.to_numpy(),
underlying_price,
df.impliedVolatility.to_numpy(),
df.dte.to_numpy(),
df.optionType.to_numpy(),
rf_rate,
)
return np.abs(np.sum(df["new_delta"].to_numpy() * df["openInterest"].to_numpy()))
@log_start_end(log=logger)
def get_market_gamma(
underlying_price: float,
df: pd.DataFrame,
abs_mg: bool = True,
rf_rate: float = 0.03,
):
"""
Get the total market gamma across entire chain.
In this calculation, we find gamma at a theoretical underlying and then take the sum of delta x open interest.
Because gamma is positive, we define gamma for puts to be negative when taking the sum.
Gamma neutral is the theoretical underlying where this sum is 0. To prepare the minimization, we take the absolute
value of this sum.
Gamma max is where the sum of the abolutele value of gamma x open interest is maximized.
Parameters
----------
underlying_price : float
Theoretical underlying price
df : pd.DataFrame
Dataframe of option chain. Requires the following columns: impliedVolatility,dte,openInterest,strike,optionType
abs_mg : bool, optional
Flag to indicate if getting gamma neutral by default True
rf_rate : float, optional
Optional float value for risk free rate, by default 0.03
Returns
-------
float
Value of gamma max or gamma neutral at the underlying price
Raises
------
ValueError
If any of the required columns are not in the dataframe
"""
for col in ["impliedVolatility", "dte", "openInterest", "strike", "optionType"]:
if col not in df.columns:
raise ValueError(f"{col} needs to be in df")
df["new_gamma"] = gamma_at_strike(
df.strike.to_numpy(),
underlying_price,
df.impliedVolatility.to_numpy(),
df.dte.to_numpy(),
rf_rate,
)
fact = np.where(df.optionType == "put", -1, 1)
mg = np.sum(df["new_gamma"].to_numpy() * df["openInterest"].to_numpy() * fact)
if abs_mg:
# gamma neutral
return np.abs(np.sum(mg))
# Gamma max
return -1 * np.sum(np.abs(mg))

View File

@@ -132,29 +132,26 @@ class OptionsController(BaseController):
self.selected_date = expiration or ""
self.chain: pd.DataFrame = pd.DataFrame()
self.full_chain: pd.DataFrame = pd.DataFrame()
self.expiry_dates: List[str] = []
self.current_price = 0.0
# Keeps track of initial source of load so we can use correct commands later
self.source = ""
self.source = get_ordered_list_sources(f"{self.PATH}load")[0]
if ticker:
if (
get_current_user().credentials.API_TRADIER_TOKEN == "REPLACE_ME"
): # nosec
console.print("Loaded expiry dates from Yahoo Finance")
self.expiry_dates = yfinance_model.option_expirations(self.ticker)
else:
console.print("Loaded expiry dates from Tradier")
self.expiry_dates = tradier_model.option_expirations(self.ticker)
self.source = "Tradier"
self.set_option_chain()
self.set_current_price()
self.set_expiry_dates()
self.selected_date = ""
if not self.expiry_dates and not self.chain.empty:
console.print(
f"""[red]Issue loading options for "{self.ticker}" from source "{self.source}".[/red]"""
)
console.print("Defaulting to YahooFinance now.")
self.expiry_dates = nasdaq_model.option_expirations(self.ticker)
else:
self.expiry_dates = []
self.default_chain = get_ordered_list_sources(f"{self.PATH}load")[0]
if session and get_current_user().preferences.USE_PROMPT_TOOLKIT:
choices: dict = self.choices_default
@@ -162,6 +159,8 @@ class OptionsController(BaseController):
self.choices = choices
self.completer = NestedCompleter.from_nested_dict(choices)
self.update_runtime_choices()
def parse_input(self, an_input: str) -> List:
"""Parse controller input

View File

@@ -7,7 +7,6 @@ from typing import Optional, Union
import numpy as np
import pandas as pd
from scipy.optimize import minimize
from openbb_terminal.decorators import log_start_end
from openbb_terminal.helper_funcs import get_rf
@@ -16,7 +15,6 @@ from openbb_terminal.stocks.options import (
chartexchange_model,
intrinio_model,
nasdaq_model,
op_helpers,
tradier_model,
yfinance_model,
)
@@ -219,54 +217,6 @@ def hist(
return output
def get_delta_neutral(symbol: str, date: str, x0: Optional[float] = None) -> float:
"""Get delta neutral price for symbol at a given close date
Parameters
----------
symbol : str
Symbol to get delta neutral price for
date : str
Date to get delta neutral price for
x0 : float, optional
Optional initial guess for solver, defaults to close price of that day
Returns
-------
float
Delta neutral price
"""
# Need an initial guess for the solver
if x0:
x0_guess = x0
else:
# Check that the close price exists. I am finding that holidays are not consistent, such as June 20, 2022
try:
x0_guess = intrinio_model.get_close_at_date(symbol, date)
except Exception:
console.print("Error getting close price for symbol, check date and symbol")
return np.nan
x0_guess = x0 if x0 else intrinio_model.get_close_at_date(symbol, date)
chains = intrinio_model.get_full_chain_eod(symbol, date)
if chains.empty:
return np.nan
# Lots of things can go wrong with minimizing, so lets add a general exception catch here.
try:
res = minimize(
op_helpers.get_abs_market_delta,
x0=x0_guess,
args=(chains),
bounds=[(0.01, np.inf)],
method="l-bfgs-b",
)
return res.x[0]
except Exception as e:
logging.info(
"Error getting delta neutral price for %s on %s: error:%s", symbol, date, e
)
return np.nan
def get_greeks(
current_price: float,
chain: pd.DataFrame,

View File

@@ -70,6 +70,7 @@ def print_raw(
show_index=False,
title=table_title,
export=bool(export),
floatfmt=".6f",
)
for opt_type, only in zip(["call", "put"], [puts_only, calls_only]):
@@ -81,6 +82,7 @@ def print_raw(
show_index=False,
title=f"{title} - {opt_type.title()}s",
export=bool(export),
floatfmt=".4f",
)
return None

View File

@@ -251,7 +251,6 @@ def get_closing(symbol: str) -> pd.Series:
return tick.history(period="1y")["Close"]
@log_start_end(log=logger)
def get_dte(date_value: str) -> int:
"""Gets days to expiration from yfinance option date"""
return (datetime.strptime(date_value, "%Y-%m-%d") - datetime.now()).days

View File

@@ -119,6 +119,12 @@ INCOME_PLOT = {
"interest_expense_operating": "income_tax_expense",
"income_loss_before_equity_method_investments": "income_loss_before_equity_method_investments",
"cost_of_revenue_goods": "cost_of_revenue_goods",
"noninterest_income": "noninterest_income",
"interest_income_expense_after_provision_for_losses": "interest_income_expense_after_provision_for_losses",
"interest_income_expense_operating_net": "interest_income_expense_operating_net",
"noninterest_expense": "noninterest_expense",
"interest_and_dividend_income_operating": "interest_and_dividend_income_operating",
"provision_for_loan_lease_and_other_losses": "provision_for_loan_lease_and_other_losses",
},
"YahooFinance": {
"total_revenue": "total_revenue",

View File

@@ -11,7 +11,7 @@ interactions:
Connection:
- keep-alive
method: GET
uri: https://hist.databento.com/v0/timeseries.stream?dataset=XNAS.ITCH&symbols=AAPL&schema=ohlcv-1d&start=2022-08-01&end=2022-12-01&stype_in=native&encoding=csv
uri: https://hist.databento.com/v0/timeseries.get_range?dataset=XNAS.ITCH&symbols=AAPL&schema=ohlcv-1d&start=2022-08-01&end=2022-12-01&stype_in=native&encoding=csv
response:
body:
string: 'ts_event,publisher_id,product_id,open,high,low,close,volume
@@ -107,7 +107,7 @@ interactions:
'
headers:
date:
- Tue, 21 Feb 2023 14:37:29 GMT
- Mon, 17 Apr 2023 18:16:03 GMT
server:
- uvicorn
transfer-encoding:

View File

@@ -65,7 +65,7 @@ def test_calculate_dte():
)
new_df = intrinio_model.calculate_dte(sample_df)
assert "dte" in new_df.columns
assert new_df["dte"].tolist() == [1095 / 365, 0, 1 / 365, 61 / 365]
assert new_df["dte"].tolist() == [1095, 0, 1, 61]
pd.testing.assert_frame_equal(new_df[sample_df.columns], new_df)