mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user