summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanglewood <85772166+deeleeramone@users.noreply.github.com>2024-06-14 13:30:04 -0700
committerDanglewood <85772166+deeleeramone@users.noreply.github.com>2024-06-14 13:30:04 -0700
commit071c60f04d5cf3983230fe7cae14be20838e632d (patch)
tree3f9ef3774241911f41e0fc6dba3092ae28451c20
parent3db89dae5e6fc9a56d1499d9e4176fbe9f06af46 (diff)
stashing
-rw-r--r--openbb_platform/core/openbb_core/provider/standard_models/options_chains.py32
-rw-r--r--openbb_platform/providers/intrinio/openbb_intrinio/models/options_chains.py241
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]