diff options
author | Danglewood <85772166+deeleeramone@users.noreply.github.com> | 2024-06-14 13:30:04 -0700 |
---|---|---|
committer | Danglewood <85772166+deeleeramone@users.noreply.github.com> | 2024-06-14 13:30:04 -0700 |
commit | 071c60f04d5cf3983230fe7cae14be20838e632d (patch) | |
tree | 3f9ef3774241911f41e0fc6dba3092ae28451c20 | |
parent | 3db89dae5e6fc9a56d1499d9e4176fbe9f06af46 (diff) |
stashing
-rw-r--r-- | openbb_platform/core/openbb_core/provider/standard_models/options_chains.py | 32 | ||||
-rw-r--r-- | openbb_platform/providers/intrinio/openbb_intrinio/models/options_chains.py | 241 |
2 files changed, 228 insertions, 45 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 6a23f687f5d..b6dd8b845ed 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,23 +31,21 @@ class OptionsChainsQueryParams(QueryParams): class OptionsChainsData(Data): """Options Chains Data.""" - symbol: Optional[str] = Field( - description=DATA_DESCRIPTIONS.get("symbol", "") - + " Here, it is the underlying symbol for the option.", - default=None, - ) 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." ) expiration: dateType = Field(description="Expiration date of the contract.") + dte: Optional[int] = Field( + default=None, description="Days to expiration of the contract." + ) strike: float = Field(description="Strike price of the contract.") option_type: str = Field(description="Call or Put.") open_interest: Optional[int] = Field( - default=None, description="Open interest on the contract." + default=0, description="Open interest on the contract." ) volume: Optional[int] = Field( - default=None, description=DATA_DESCRIPTIONS.get("volume", "") + default=0, description=DATA_DESCRIPTIONS.get("volume", "") ) theoretical_price: Optional[float] = Field( default=None, description="Theoretical value of the option." @@ -55,6 +53,13 @@ class OptionsChainsData(Data): last_trade_price: Optional[float] = Field( default=None, description="Last trade price of the option." ) + last_trade_size: Optional[int] = Field( + default=None, description="Last trade size of the option." + ) + last_trade_time: Optional[datetime] = Field( + default=None, + description="The timestamp of the last trade.", + ) tick: Optional[str] = Field( default=None, description="Whether the last tick was up or down in price." ) @@ -64,12 +69,20 @@ class OptionsChainsData(Data): bid_size: Optional[int] = Field( default=None, description="Bid size for the option." ) + bid_time: Optional[datetime] = Field( + default=None, + description="The timestamp of the bid price.", + ) ask: Optional[float] = Field( default=None, description="Current ask price for the option." ) ask_size: Optional[int] = Field( default=None, description="Ask size for the option." ) + ask_time: Optional[datetime] = Field( + default=None, + description="The timestamp of the ask price.", + ) mark: Optional[float] = Field( default=None, description="The mid-price between the latest bid and ask." ) @@ -138,7 +151,8 @@ class OptionsChainsData(Data): ) change_percent: Optional[float] = Field( default=None, - description="Change, in normalizezd percentage points, of the option.", + description="Change, in normalized percentage points, of the option.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, ) implied_volatility: Optional[float] = Field( default=None, description="Implied volatility of the option." @@ -151,7 +165,7 @@ class OptionsChainsData(Data): @field_validator("expiration", mode="before", check_fields=False) @classmethod - def date_validate(cls, v): # pylint: disable=E0213 + def date_validate(cls, v): """Return the datetime object from the date string.""" if isinstance(v, datetime): return datetime.strftime(v, "%Y-%m-%d") 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 252a16766b3..ef58527bb5d 100644 --- a/openbb_platform/providers/intrinio/openbb_intrinio/models/options_chains.py +++ b/openbb_platform/providers/intrinio/openbb_intrinio/models/options_chains.py @@ -6,20 +6,27 @@ from datetime import ( datetime, timedelta, ) -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Literal, Optional, Union +from warnings import warn from dateutil import parser +from openbb_core.provider.abstract.annotated_result import AnnotatedResult from openbb_core.provider.abstract.fetcher import Fetcher from openbb_core.provider.standard_models.options_chains import ( OptionsChainsData, OptionsChainsQueryParams, ) +from openbb_core.provider.utils.errors import OpenBBError from openbb_core.provider.utils.helpers import ( ClientResponse, amake_requests, + get_querystring, ) +from openbb_intrinio.models.equity_historical import IntrinioEquityHistoricalFetcher +from openbb_intrinio.models.index_historical import IntrinioIndexHistoricalFetcher from openbb_intrinio.utils.helpers import get_data_many, get_weekday from pydantic import Field, field_validator +from pytz import timezone class IntrinioOptionsChainsQueryParams(OptionsChainsQueryParams): @@ -28,9 +35,73 @@ class IntrinioOptionsChainsQueryParams(OptionsChainsQueryParams): source: https://docs.intrinio.com/documentation/web_api/get_options_chain_eod_v2 """ + __alias_dict__ = { + "strike_gt": "strike_greater_than", + "strike_lt": "strike_less_than", + "volume_gt": "volume_greater_than", + "volume_lt": "volume_less_than", + "oi_gt": "open_interest_greater_than", + "oi_lt": "open_interest_less_than", + "option_type": "type", + } + date: Optional[dateType] = Field( default=None, description="The end-of-day date for options chains data." ) + option_type: Literal[None, Union["call", "put"]] = Field( + default=None, + description="The option type, call or put, 'None' is both (default).", + ) + 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.", + ) + strike_gt: Optional[int] = Field( + default=None, + description="Return options with a strike price greater than the given value." + + " Parameter is ignored when a date is supplied.", + ) + strike_lt: Optional[int] = Field( + default=None, + description="Return options with a strike price less than the given value." + + " Parameter is ignored when a date is supplied.", + ) + 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." + ) + 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." + ) + 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." + ) + 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." + ) + model: Literal["black_scholes", "bjerk"] = Field( + default="black_scholes", + description="The pricing model to use for options chains data, default is 'black_scholes'." + + " Parameter is ignored when a date is supplied.", + ) + 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." + ) + 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." + ) class IntrinioOptionsChainsData(OptionsChainsData): @@ -41,28 +112,42 @@ class IntrinioOptionsChainsData(OptionsChainsData): "symbol": "ticker", "eod_date": "date", "option_type": "type", + "last_trade_time": "last_timestamp", + "last_trade_price": "last", + "last_trade_size": "last_size", + "ask_time": "ask_timestamp", + "bid_time": "bid_timestamp", + "open": "trade_open", + "high": "trade_high", + "low": "trade_low", + "close": "trade_close", } - exercise_style: Optional[str] = Field( - default=None, - description="The exercise style of the option, American or European.", - ) - @field_validator( - "date", "close_time", "close_ask_time", "close_bid_time", + "ask_time", + "bid_time", + "last_trade_time", mode="before", check_fields=False, ) @classmethod - def date_validate(cls, v): # pylint: disable=E0213 + def date_validate(cls, v): """Return the datetime object from the date string.""" - # only pass it to the parser if it is not a datetime object if isinstance(v, str): - return parser.parse(v) - return v + dt = parser.parse(v) + dt = dt.replace(tzinfo=timezone("UTC")) + dt = dt.astimezone(timezone("America/New_York")) + return dt.replace(microsecond=0) + return v if v else None + + @field_validator("volume", "open_interest", mode="before", check_fields=False) + @classmethod + def volume_oi_validate(cls, v): + """Return the volume as an integer.""" + return 0 if v is None else v class IntrinioOptionsChainsFetcher( @@ -73,28 +158,25 @@ class IntrinioOptionsChainsFetcher( @staticmethod def transform_query(params: Dict[str, Any]) -> IntrinioOptionsChainsQueryParams: """Transform the query.""" - transform_params = params.copy() - if params.get("date") is not None: - if isinstance(params["date"], dateType): - transform_params["date"] = params["date"].strftime("%Y-%m-%d") - else: - transform_params["date"] = parser.parse(params["date"]).date() - - return IntrinioOptionsChainsQueryParams(**transform_params) + return IntrinioOptionsChainsQueryParams(**params) @staticmethod async def aextract_data( query: IntrinioOptionsChainsQueryParams, credentials: Optional[Dict[str, str]], **kwargs: Any, - ) -> List[Dict]: + ) -> Dict: """Return the raw data from the Intrinio endpoint.""" api_key = credentials.get("intrinio_api_key") if credentials else "" base_url = "https://api-v2.intrinio.com/options" + date = query.date if query.date is not None else datetime.now().date() + date = get_weekday(date) + 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") url = ( f"{base_url}/expirations/{query.symbol}/eod?" f"after={date}&api_key={api_key}" @@ -102,9 +184,36 @@ class IntrinioOptionsChainsFetcher( expirations = await get_data_many(url, "expirations", **kwargs) def generate_url(expiration) -> str: - url = f"{base_url}/chain/{query.symbol}/{expiration}/eod?date=" + url = f"{base_url}/chain/{query.symbol}/{expiration}/" if query.date is not None: - url += date + query_string = get_querystring( + query.model_dump(exclude_none=True), + [ + "symbol", + "date", + "model", + "volume_greater_than", + "volume_less_than", + "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}" + 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") + ) + url = url + f"realtime?{query_string}" + return url + f"&api_key={api_key}" return [generate_url(expiration) for expiration in expirations] @@ -114,28 +223,88 @@ class IntrinioOptionsChainsFetcher( response_data = await response.json() return response_data.get("chain", []) - date = datetime.now().date() if query.date is None else query.date - date = get_weekday(date) - results = await amake_requests( await get_urls(date.strftime("%Y-%m-%d")), callback, **kwargs ) + # If the EOD chains are not available for the given date, try the previous day + if not results and query.date is not None: + date = get_weekday(date - timedelta(days=1)).strftime("%Y-%m-%d") + urls = await get_urls(date) + results = await amake_requests(urls, response_callback=callback, **kwargs) if not results: - urls = await get_urls( - get_weekday(date - timedelta(days=1)).strftime("%Y-%m-%d") - ) - results = await amake_requests(urls, response_callback=callback, **kwargs) + raise OpenBBError(f"No data found for the given symbol: {query.symbol}") - return results + output: Dict = {} + underlying_price: Dict = {} + # If the EOD chains are requested, get the underlying price on the given date. + if query.date is not None: + if query.symbol.endswith("W"): + query.symbol = query.symbol[:-1] + temp = None + try: + temp = await IntrinioEquityHistoricalFetcher.fetch_data( + {"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 + ) + temp = temp[0] + except Exception: + warn(f"Failed to get underlying price for {query.symbol}: {e}") + if temp: + underlying_price["symbol"] = query.symbol + underlying_price["price"] = temp.close + underlying_price["date"] = temp.date.strftime("%Y-%m-%d") + + output = {"underlying": underlying_price, "data": results} + + return output @staticmethod def transform_data( - query: IntrinioOptionsChainsQueryParams, data: List[Dict], **kwargs: Any - ) -> List[IntrinioOptionsChainsData]: + query: IntrinioOptionsChainsQueryParams, + data: Dict, + **kwargs: Any, + ) -> AnnotatedResult[List[IntrinioOptionsChainsData]]: """Return the transformed data.""" - data = [{**item["option"], **item["prices"]} for item in data] - data = sorted( - data, key=lambda x: (x["expiration"], x["strike"], x["type"]), reverse=False + results: List[IntrinioOptionsChainsData] = [] + chains = data.get("data", []) + underlying = data.get("underlying", {}) + if query.date is not None: + for item in chains: + new_item = {**item["option"], **item["prices"]} + new_item["dte"] = ( + datetime.strptime(new_item["expiration"], "%Y-%m-%d").date() + - datetime.strptime(new_item["date"], "%Y-%m-%d").date() + ).days + _ = new_item.pop("ticker", None) + _ = new_item.pop("exercise_style", None) + results.append(IntrinioOptionsChainsData.model_validate(new_item)) + else: + for item in chains: + new_item = {**item["option"], **item["price"], **item["stats"], **item["extended_price"]} + dte = ( + 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) + underlying["date"] = datetime.now().date() + underlying["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) + results.append(IntrinioOptionsChainsData.model_validate(new_item)) + + return AnnotatedResult( + result=sorted( + results, + key=lambda x: (x.expiration, x.strike, x.option_type), + reverse=False, + ), + metadata=underlying ) - return [IntrinioOptionsChainsData.model_validate(d) for d in data] |