diff options
author | Danglewood <85772166+deeleeramone@users.noreply.github.com> | 2024-06-14 17:13:09 -0700 |
---|---|---|
committer | Danglewood <85772166+deeleeramone@users.noreply.github.com> | 2024-06-14 17:13:09 -0700 |
commit | fc70fb9f317b3a3acbd8f725cc072f4c1dd43f8d (patch) | |
tree | 3f15f00c02aab9562313e997b331e02dc21e3abc | |
parent | 071c60f04d5cf3983230fe7cae14be20838e632d (diff) |
intrinio delayed options
8 files changed, 151 insertions, 99 deletions
diff --git a/openbb_platform/core/openbb_core/provider/standard_models/options_chains.py b/openbb_platform/core/openbb_core/provider/standard_models/options_chains.py index b6dd8b845ed..38aca52b57d 100644 --- a/openbb_platform/core/openbb_core/provider/standard_models/options_chains.py +++ b/openbb_platform/core/openbb_core/provider/standard_models/options_chains.py @@ -31,6 +31,14 @@ class OptionsChainsQueryParams(QueryParams): class OptionsChainsData(Data): """Options Chains Data.""" + underlying_symbol: Optional[str] = Field( + default=None, + description="Underlying symbol for the option.", + ) + underlying_price: Optional[float] = Field( + default=None, + description="Price of the underlying stock.", + ) contract_symbol: str = Field(description="Contract symbol for the option.") eod_date: Optional[dateType] = Field( default=None, description="Date for which the options chains are returned." @@ -73,6 +81,9 @@ class OptionsChainsData(Data): default=None, description="The timestamp of the bid price.", ) + bid_exchange: Optional[str] = Field( + default=None, description="The exchange of the bid price." + ) ask: Optional[float] = Field( default=None, description="Current ask price for the option." ) @@ -83,6 +94,9 @@ class OptionsChainsData(Data): default=None, description="The timestamp of the ask price.", ) + ask_exchange: Optional[str] = Field( + default=None, description="The exchange of the ask price." + ) mark: Optional[float] = Field( default=None, description="The mid-price between the latest bid and ask." ) diff --git a/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py b/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py index 210ab79190c..23089fe9f1b 100644 --- a/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py +++ b/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py @@ -24,7 +24,24 @@ def headers(): @parametrize( "params", [ - ({"provider": "intrinio", "symbol": "AAPL", "date": "2023-01-25"}), + ( + { + "provider": "intrinio", + "symbol": "AAPL", + "date": "2023-01-25", + "option_type": None, + "moneyness": "all", + "strike_gt": None, + "strike_lt": None, + "volume_gt": None, + "volume_lt": None, + "oi_gt": None, + "oi_lt": None, + "model": "black_scholes", + "show_extended_price": False, + "include_related_symbols": False, + } + ), ({"provider": "cboe", "symbol": "AAPL", "use_cache": False}), ({"provider": "tradier", "symbol": "AAPL"}), ({"provider": "yfinance", "symbol": "AAPL"}), diff --git a/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py b/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py index 1bc0597855f..c03cc32d6a7 100644 --- a/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py +++ b/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py @@ -20,7 +20,24 @@ def obb(pytestconfig): @parametrize( "params", [ - ({"provider": "intrinio", "symbol": "AAPL", "date": "2023-01-25"}), + ( + { + "provider": "intrinio", + "symbol": "AAPL", + "date": "2023-01-25", + "option_type": None, + "moneyness": "all", + "strike_gt": None, + "strike_lt": None, + "volume_gt": None, + "volume_lt": None, + "oi_gt": None, + "oi_lt": None, + "model": "black_scholes", + "show_extended_price": False, + "include_related_symbols": False, + } + ), ({"provider": "cboe", "symbol": "AAPL", "use_cache": False}), ({"provider": "tradier", "symbol": "AAPL"}), ({"provider": "yfinance", "symbol": "AAPL"}), diff --git a/openbb_platform/providers/cboe/openbb_cboe/models/options_chains.py b/openbb_platform/providers/cboe/openbb_cboe/models/options_chains.py index c1c0602d3cc..ca049ee80a7 100644 --- a/openbb_platform/providers/cboe/openbb_cboe/models/options_chains.py +++ b/openbb_platform/providers/cboe/openbb_cboe/models/options_chains.py @@ -39,11 +39,6 @@ class CboeOptionsChainsQueryParams(OptionsChainsQueryParams): class CboeOptionsChainsData(OptionsChainsData): """CBOE Options Chains Data.""" - last_trade_timestamp: Optional[datetime] = Field( - description="Last trade timestamp of the option.", default=None - ) - dte: int = Field(description="Days to expiration for the option.") - class CboeOptionsChainsFetcher( Fetcher[ @@ -51,7 +46,7 @@ class CboeOptionsChainsFetcher( List[CboeOptionsChainsData], ] ): - """Transform the query, extract and transform the data from the CBOE endpoints.""" + """Cboe Options Chains Fetcher.""" @staticmethod def transform_query(params: Dict[str, Any]) -> CboeOptionsChainsQueryParams: @@ -137,7 +132,6 @@ class CboeOptionsChainsFetcher( "theo": "theoretical_price", "percent_change": "change_percent", "prev_day_close": "prev_close", - "last_trade_time": "last_trade_timestamp", } ) @@ -162,7 +156,9 @@ class CboeOptionsChainsFetcher( option_df_index.expiration = DatetimeIndex( option_df_index.expiration, yearfirst=True ).astype(str) - option_df_index = option_df_index.drop(columns=["Ticker"]) + option_df_index = option_df_index.rename( + columns={"Ticker": "underlying_symbol"} + ) # Joins the parsed symbol into the dataframe. @@ -173,20 +169,22 @@ class CboeOptionsChainsFetcher( temp_ = (temp - now).days + 1 quotes["dte"] = temp_ - quotes["last_trade_timestamp"] = ( - to_datetime(quotes["last_trade_timestamp"], format="%Y-%m-%dT%H:%M:%S") + quotes["last_trade_time"] = ( + to_datetime(quotes["last_trade_time"], format="%Y-%m-%dT%H:%M:%S") .fillna(value="-") .replace("-", None) ) quotes = quotes.set_index( keys=["expiration", "strike", "option_type"] ).sort_index() + if results_metadata.get("current_price"): + quotes["underlying_price"] = results_metadata["current_price"] quotes["open_interest"] = quotes["open_interest"].astype("int64") quotes["volume"] = quotes["volume"].astype("int64") quotes["bid_size"] = quotes["bid_size"].astype("int64") quotes["ask_size"] = quotes["ask_size"].astype("int64") - quotes["prev_close"] = round(quotes["prev_close"], 2) - quotes["change_percent"] = round(quotes["change_percent"] / 100, 4) + quotes["prev_close"] = quotes["prev_close"] + quotes["change_percent"] = quotes["change_percent"] / 100 return AnnotatedResult( result=[ diff --git a/openbb_platform/providers/intrinio/openbb_intrinio/models/options_chains.py b/openbb_platform/providers/intrinio/openbb_intrinio/models/options_chains.py index ef58527bb5d..d2f253b5c4a 100644 --- a/openbb_platform/providers/intrinio/openbb_intrinio/models/options_chains.py +++ b/openbb_platform/providers/intrinio/openbb_intrinio/models/options_chains.py @@ -51,11 +51,13 @@ class IntrinioOptionsChainsQueryParams(OptionsChainsQueryParams): option_type: Literal[None, Union["call", "put"]] = Field( default=None, description="The option type, call or put, 'None' is both (default).", + json_schema_extra={"choices": ["call", "put"]}, ) moneyness: Literal["otm", "itm", "all"] = Field( default="all", description="Return only contracts that are in or out of the money, default is 'all'." + " Parameter is ignored when a date is supplied.", + json_schema_extra={"choices": ["otm", "itm", "all"]}, ) strike_gt: Optional[int] = Field( default=None, @@ -70,22 +72,22 @@ class IntrinioOptionsChainsQueryParams(OptionsChainsQueryParams): volume_gt: Optional[int] = Field( default=None, description="Return options with a volume greater than the given value." - + " Parameter is ignored when a date is supplied." + + " Parameter is ignored when a date is supplied.", ) volume_lt: Optional[int] = Field( default=None, description="Return options with a volume less than the given value." - + " Parameter is ignored when a date is supplied." + + " Parameter is ignored when a date is supplied.", ) oi_gt: Optional[int] = Field( default=None, description="Return options with an open interest greater than the given value." - + " Parameter is ignored when a date is supplied." + + " Parameter is ignored when a date is supplied.", ) oi_lt: Optional[int] = Field( default=None, description="Return options with an open interest less than the given value." - + " Parameter is ignored when a date is supplied." + + " Parameter is ignored when a date is supplied.", ) model: Literal["black_scholes", "bjerk"] = Field( default="black_scholes", @@ -95,12 +97,12 @@ class IntrinioOptionsChainsQueryParams(OptionsChainsQueryParams): show_extended_price: bool = Field( default=True, description="Whether to include OHLC type fields, default is True." - + " Parameter is ignored when a date is supplied." + + " Parameter is ignored when a date is supplied.", ) include_related_symbols: bool = Field( default=False, description="Include related symbols that end in a 1 or 2 because of a corporate action," - + " default is False." + + " default is False.", ) @@ -137,7 +139,7 @@ class IntrinioOptionsChainsData(OptionsChainsData): def date_validate(cls, v): """Return the datetime object from the date string.""" if isinstance(v, str): - dt = parser.parse(v) + dt = parser.parse(v) dt = dt.replace(tzinfo=timezone("UTC")) dt = dt.astimezone(timezone("America/New_York")) return dt.replace(microsecond=0) @@ -176,7 +178,9 @@ class IntrinioOptionsChainsFetcher( async def get_urls(date: str) -> List[str]: """Return the urls for the given date.""" - date = (datetime.strptime(date, "%Y-%m-%d") - timedelta(days=1)).strftime("%Y-%m-%d") + date = (datetime.strptime(date, "%Y-%m-%d") - timedelta(days=1)).strftime( + "%Y-%m-%d" + ) url = ( f"{base_url}/expirations/{query.symbol}/eod?" f"after={date}&api_key={api_key}" @@ -197,21 +201,17 @@ class IntrinioOptionsChainsFetcher( "moneyness", "open_interest_greater_than", "open_interest_less_than", - "strike_greater_than", - "strike_less_than", "show_extended_price", - "include_related_symbols", - ] + ], ) - url = url+f"eod?date={date}&{query_string}" + url = url + f"eod?date={date}&{query_string}" else: query_string = get_querystring( query.model_dump(exclude_none=True), ["symbol", "date"] ) - query_string = ( - query_string.replace("otm", "out_of_the_money") - .replace("itm", "in_the_money") - ) + query_string = query_string.replace( + "otm", "out_of_the_money" + ).replace("itm", "in_the_money") url = url + f"realtime?{query_string}" return url + f"&api_key={api_key}" @@ -244,14 +244,16 @@ class IntrinioOptionsChainsFetcher( temp = None try: temp = await IntrinioEquityHistoricalFetcher.fetch_data( - {"symbol": query.symbol, "start_date": date, "end_date": date}, credentials + {"symbol": query.symbol, "start_date": date, "end_date": date}, + credentials, ) temp = temp[0] # If the symbol is SPX, or similar, try to get the underlying price from the index. except Exception as e: try: temp = await IntrinioIndexHistoricalFetcher.fetch_data( - {"symbol": query.symbol, "start_date": date, "end_date": date}, credentials + {"symbol": query.symbol, "start_date": date, "end_date": date}, + credentials, ) temp = temp[0] except Exception: @@ -275,6 +277,7 @@ class IntrinioOptionsChainsFetcher( results: List[IntrinioOptionsChainsData] = [] chains = data.get("data", []) underlying = data.get("underlying", {}) + last_price = underlying.get("price") if query.date is not None: for item in chains: new_item = {**item["option"], **item["prices"]} @@ -282,19 +285,29 @@ class IntrinioOptionsChainsFetcher( datetime.strptime(new_item["expiration"], "%Y-%m-%d").date() - datetime.strptime(new_item["date"], "%Y-%m-%d").date() ).days - _ = new_item.pop("ticker", None) + if last_price: + new_item["underlying_price"] = last_price _ = new_item.pop("exercise_style", None) + new_item["underlying_symbol"] = new_item.pop("ticker") results.append(IntrinioOptionsChainsData.model_validate(new_item)) else: for item in chains: - new_item = {**item["option"], **item["price"], **item["stats"], **item["extended_price"]} + new_item = { + **item["option"], + **item["price"], + **item["stats"], + **item["extended_price"], + } dte = ( - datetime.strptime(new_item["expiration"], "%Y-%m-%d").date() - datetime.now().date() + datetime.strptime(new_item["expiration"], "%Y-%m-%d").date() + - datetime.now().date() ).days new_item["dte"] = dte - underlying["underlying_symbol"] = new_item.pop("underlying_price_ticker", None) + new_item["underlying_symbol"] = new_item.pop( + "underlying_price_ticker", None + ) underlying["date"] = datetime.now().date() - underlying["underlying_price"] = new_item.pop("underlying_price", None) + new_item["underlying_price"] = new_item.pop("underlying_price", None) _ = new_item.pop("ticker", None) _ = new_item.pop("trade_exchange", None) _ = new_item.pop("exercise_style", None) @@ -306,5 +319,5 @@ class IntrinioOptionsChainsFetcher( key=lambda x: (x.expiration, x.strike, x.option_type), reverse=False, ), - metadata=underlying + metadata=underlying, ) diff --git a/openbb_platform/providers/tmx/openbb_tmx/models/options_chains.py b/openbb_platform/providers/tmx/openbb_tmx/models/options_chains.py index 7e5e455a48d..2368b3f9942 100644 --- a/openbb_platform/providers/tmx/openbb_tmx/models/options_chains.py +++ b/openbb_platform/providers/tmx/openbb_tmx/models/options_chains.py @@ -15,6 +15,7 @@ from openbb_core.provider.standard_models.options_chains import ( from openbb_core.provider.utils.descriptions import ( QUERY_DESCRIPTIONS, ) +from openbb_tmx.models.equity_quote import TmxEquityQuoteFetcher from openbb_tmx.utils.helpers import download_eod_chains, get_current_options from pydantic import Field, field_validator @@ -48,12 +49,6 @@ class TmxOptionsChainsData(OptionsChainsData): settlement_price: Optional[float] = Field( description="Settlement price on that date.", default=None ) - underlying_price: Optional[float] = Field( - description="Price of the underlying stock on that date.", default=None - ) - dte: Optional[int] = Field( - description="Days to expiration for the option.", default=None - ) @field_validator("expiration", mode="before", check_fields=False) @classmethod @@ -89,6 +84,13 @@ class TmxOptionsChainsFetcher( ) else: chains = await get_current_options(query.symbol, use_cache=query.use_cache) + underlying_quote = await TmxEquityQuoteFetcher.fetch_data( + {"symbol": query.symbol}, credentials + ) + underlying_price = underlying_quote[0].last_price + if underlying_price and not chains.empty: + chains["underlying_price"] = underlying_price + chains["underlying_symbol"] = query.symbol + ":CA" if not chains.empty: results = chains.to_dict(orient="records") diff --git a/openbb_platform/providers/tradier/openbb_tradier/models/options_chains.py b/openbb_platform/providers/tradier/openbb_tradier/models/options_chains.py index 16cdbab51f0..ace5e50e68f 100644 --- a/openbb_platform/providers/tradier/openbb_tradier/models/options_chains.py +++ b/openbb_platform/providers/tradier/openbb_tradier/models/options_chains.py @@ -14,6 +14,7 @@ from openbb_core.provider.standard_models.options_chains import ( ) from openbb_core.provider.utils.errors import EmptyDataError from openbb_core.provider.utils.helpers import amake_request, safe_fromtimestamp +from openbb_tradier.models.equity_quote import TradierEquityQuoteFetcher from openbb_tradier.utils.constants import OPTIONS_EXCHANGES, STOCK_EXCHANGES from pydantic import Field, field_validator, model_validator from pytz import timezone @@ -34,6 +35,7 @@ class TradierOptionsChainsData(OptionsChainsData): __alias_dict__ = { "expiration": "expiration_date", + "underlying_symbol": "underlying", "contract_symbol": "symbol", "last_trade_price": "last", "bid_size": "bidsize", @@ -41,16 +43,16 @@ class TradierOptionsChainsData(OptionsChainsData): "change_percent": "change_percentage", "orats_final_iv": "smv_vol", "implied_volatility": "mid_iv", - "greeks_timestamp": "updated_at", + "greeks_time": "updated_at", "prev_close": "prevclose", "year_high": "week_52_high", "year_low": "week_52_low", - "last_trade_timestamp": "trade_date", - "last_trade_volume": "last_volume", + "last_trade_time": "trade_date", + "last_trade_size": "last_volume", "ask_exchange": "askexch", - "ask_timestamp": "ask_date", + "ask_time": "ask_date", "bid_exchange": "bidexch", - "bid_timestamp": "bid_date", + "bid_time": "bid_date", } phi: Optional[float] = Field( @@ -77,48 +79,21 @@ class TradierOptionsChainsData(OptionsChainsData): default=None, description="52-week low price of the option.", ) - last_trade_volume: Optional[int] = Field( - default=None, - description="Volume of the last trade.", - ) - dte: Optional[int] = Field( - default=None, - description="Days to expiration.", - ) contract_size: Optional[int] = Field( default=None, description="Size of the contract.", ) - bid_exchange: Optional[str] = Field( - default=None, - description="Exchange of the bid price.", - ) - bid_timestamp: Optional[datetime] = Field( - default=None, - description="Timestamp of the bid price.", - ) - ask_exchange: Optional[str] = Field( - default=None, - description="Exchange of the ask price.", - ) - ask_timestamp: Optional[datetime] = Field( - default=None, - description="Timestamp of the ask price.", - ) - greeks_timestamp: Optional[datetime] = Field( + greeks_time: Optional[datetime] = Field( default=None, description="Timestamp of the last greeks update." + " Greeks/IV data is updated once per hour.", ) - last_trade_timestamp: Optional[datetime] = Field( - default=None, description="Timestamp of the last trade." - ) @field_validator( - "last_trade_timestamp", - "greeks_timestamp", - "ask_timestamp", - "bid_timestamp", + "last_trade_time", + "greeks_time", + "ask_time", + "bid_time", mode="before", check_fields=False, ) @@ -162,7 +137,12 @@ class TradierOptionsChainsData(OptionsChainsData): """Check for zero values and replace with None.""" return ( { - k: None if (v == 0 or str(v) == "0") and k != "dte" else v + k: ( + None + if (v == 0 or str(v) == "0") + and k not in ["dte", "open_interest", "volume"] + else v + ) for k, v in values.items() } if isinstance(values, dict) @@ -231,14 +211,19 @@ class TradierOptionsChainsFetcher( results = [] - async def get_one(url): + underlying_quote = await TradierEquityQuoteFetcher.fetch_data( + {"symbol": query.symbol}, credentials + ) + underlying_price = underlying_quote[0].last_price + + async def get_one(url, underlying_price): """Get the chain for a single expiration.""" chain = await amake_request(url, headers=HEADERS) if chain.get("options") and isinstance(chain["options"].get("option", []), list): # type: ignore data = chain["options"]["option"] # type: ignore for d in data.copy(): # Remove any strikes returned without data. - keys = ["volume", "open_interest", "last", "bid", "ask"] + keys = ["last", "bid", "ask"] if all(d.get(key) in [0, "0", None] for key in keys): data.remove(d) continue @@ -252,7 +237,6 @@ class TradierOptionsChainsFetcher( "exch", "type", "expiration_type", - "underlying", "description", "average_volume", ] @@ -262,6 +246,8 @@ class TradierOptionsChainsFetcher( datetime.strptime(d["expiration_date"], "%Y-%m-%d").date() - datetime.now().date() ).days + if underlying_price is not None: + d["underlying_price"] = underlying_price results.extend(data) @@ -270,7 +256,7 @@ class TradierOptionsChainsFetcher( for expiration in expirations # type: ignore ] - tasks = [get_one(url) for url in urls] + tasks = [get_one(url, underlying_price) for url in urls] await asyncio.gather(*tasks) diff --git a/openbb_platform/providers/yfinance/openbb_yfinance/models/options_chains.py b/openbb_platform/providers/yfinance/openbb_yfinance/models/options_chains.py index 62651b94c84..ba4a09fd051 100644 --- a/openbb_platform/providers/yfinance/openbb_yfinance/models/options_chains.py +++ b/openbb_platform/providers/yfinance/openbb_yfinance/models/options_chains.py @@ -28,24 +28,21 @@ class YFinanceOptionsChainsData(OptionsChainsData): __alias_dict__ = { "contract_symbol": "contractSymbol", - "last_trade_timestamp": "lastTradeDate", + "last_trade_time": "lastTradeDate", "last_trade_price": "lastPrice", "change_percent": "percentChange", "open_interest": "openInterest", "implied_volatility": "impliedVolatility", "in_the_money": "inTheMoney", } - dte: Optional[int] = Field( - default=None, - description="Days to expiration.", - ) + in_the_money: Optional[bool] = Field( default=None, description="Whether the option is in the money.", ) - last_trade_timestamp: Optional[datetime] = Field( + currency: Optional[str] = Field( default=None, - description="Timestamp for when the option was last traded.", + description="Currency of the option.", ) @@ -67,6 +64,7 @@ class YFinanceOptionsChainsFetcher( ) -> Dict: """Extract the raw data from YFinance.""" symbol = query.symbol.upper() + symbol = "^" + symbol if symbol in ["VIX", "RUT", "SPX", "NDX"] else symbol ticker = yf.Ticker(symbol) expirations = list(ticker.options) if not expirations or len(expirations) == 0: @@ -107,7 +105,9 @@ class YFinanceOptionsChainsFetcher( } tz = timezone(underlying_output.get("exchange_tz", "UTC")) - async def get_chain(ticker, expiration, tz): + underlying_price = underlying_output.get("last_price") + + async def get_chain(ticker, expiration, tz, underlying_price): """Get the data for one expiration.""" exp = datetime.strptime(expiration, "%Y-%m-%d").date() now = datetime.now().date() @@ -124,18 +124,23 @@ class YFinanceOptionsChainsFetcher( .sort_index() .reset_index() ) + chain = chain.drop(columns=["contractSize"]) chain["dte"] = dte + if underlying_price is not None: + chain["underlying_price"] = underlying_price + chain["underlying_symbol"] = symbol chain["percentChange"] = chain["percentChange"] / 100 - for col in ["currency", "contractSize"]: - if col in chain.columns: - chain = chain.drop(col, axis=1) + if len(chain) > 0: chains_output.extend( chain.fillna("N/A").replace("N/A", None).to_dict("records") ) await asyncio.gather( - *[get_chain(ticker, expiration, tz) for expiration in expirations] + *[ + get_chain(ticker, expiration, tz, underlying_price) + for expiration in expirations + ] ) if not chains_output: |