Skip to content

Commit

Permalink
[Feature] Options Chains From YFinance (#6468)
Browse files Browse the repository at this point in the history
* add yfinance to options chains

* Explicit None

---------

Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com>
  • Loading branch information
deeleeramone and IgorWounds authored May 27, 2024
1 parent 27d448e commit af6fa04
Show file tree
Hide file tree
Showing 8 changed files with 3,772 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ def headers():
({"provider": "intrinio", "symbol": "AAPL", "date": "2023-01-25"}),
({"provider": "cboe", "symbol": "AAPL", "use_cache": False}),
({"provider": "tradier", "symbol": "AAPL"}),
({"provider": "yfinance", "symbol": "AAPL"}),
(
{
"provider": "tmx",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def obb(pytestconfig):
({"provider": "intrinio", "symbol": "AAPL", "date": "2023-01-25"}),
({"provider": "cboe", "symbol": "AAPL", "use_cache": False}),
({"provider": "tradier", "symbol": "AAPL"}),
({"provider": "yfinance", "symbol": "AAPL"}),
(
{
"provider": "tmx",
Expand Down
33 changes: 30 additions & 3 deletions openbb_platform/openbb/assets/reference.json
Original file line number Diff line number Diff line change
Expand Up @@ -1199,7 +1199,7 @@
},
{
"name": "provider",
"type": "Literal['intrinio']",
"type": "Literal['intrinio', 'yfinance']",
"description": "The provider to use for the query, by default None. If None, the provider specified in defaults is selected or 'intrinio' if there is no default.",
"default": "intrinio",
"optional": true
Expand All @@ -1214,7 +1214,8 @@
"optional": true,
"choices": null
}
]
],
"yfinance": []
},
"returns": {
"OBBject": [
Expand All @@ -1225,7 +1226,7 @@
},
{
"name": "provider",
"type": "Optional[Literal['intrinio']]",
"type": "Optional[Literal['intrinio', 'yfinance']]",
"description": "Provider name."
},
{
Expand Down Expand Up @@ -1601,6 +1602,32 @@
"optional": true,
"choices": null
}
],
"yfinance": [
{
"name": "dte",
"type": "int",
"description": "Days to expiration.",
"default": null,
"optional": true,
"choices": null
},
{
"name": "in_the_money",
"type": "bool",
"description": "Whether the option is in the money.",
"default": null,
"optional": true,
"choices": null
},
{
"name": "last_trade_timestamp",
"type": "datetime",
"description": "Timestamp for when the option was last traded.",
"default": null,
"optional": true,
"choices": null
}
]
},
"model": "OptionsChains"
Expand Down
14 changes: 10 additions & 4 deletions openbb_platform/openbb/package/derivatives_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def chains(
self,
symbol: Annotated[str, OpenBBField(description="Symbol to get data for.")],
provider: Annotated[
Optional[Literal["intrinio"]],
Optional[Literal["intrinio", "yfinance"]],
OpenBBField(
description="The provider to use for the query, by default None.\n If None, the provider specified in defaults is selected or 'intrinio' if there is\n no default."
),
Expand All @@ -38,7 +38,7 @@ def chains(
----------
symbol : str
Symbol to get data for.
provider : Optional[Literal['intrinio']]
provider : Optional[Literal['intrinio', 'yfinance']]
The provider to use for the query, by default None.
If None, the provider specified in defaults is selected or 'intrinio' if there is
no default.
Expand All @@ -50,7 +50,7 @@ def chains(
OBBject
results : List[OptionsChains]
Serializable results.
provider : Optional[Literal['intrinio']]
provider : Optional[Literal['intrinio', 'yfinance']]
Provider name.
warnings : Optional[List[Warning_]]
List of warnings.
Expand Down Expand Up @@ -149,6 +149,12 @@ def chains(
Rho of the option.
exercise_style : Optional[str]
The exercise style of the option, American or European. (provider: intrinio)
dte : Optional[int]
Days to expiration. (provider: yfinance)
in_the_money : Optional[bool]
Whether the option is in the money. (provider: yfinance)
last_trade_timestamp : Optional[datetime]
Timestamp for when the option was last traded. (provider: yfinance)
Examples
--------
Expand All @@ -165,7 +171,7 @@ def chains(
"provider": self._get_provider(
provider,
"/derivatives/options/chains",
("intrinio",),
("intrinio", "yfinance"),
)
},
standard_params={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
from openbb_yfinance.models.key_executives import YFinanceKeyExecutivesFetcher
from openbb_yfinance.models.key_metrics import YFinanceKeyMetricsFetcher
from openbb_yfinance.models.losers import YFLosersFetcher
from openbb_yfinance.models.options_chains import YFinanceOptionsChainsFetcher
from openbb_yfinance.models.price_target_consensus import (
YFinancePriceTargetConsensusFetcher,
)
Expand Down Expand Up @@ -69,6 +70,7 @@
"KeyExecutives": YFinanceKeyExecutivesFetcher,
"KeyMetrics": YFinanceKeyMetricsFetcher,
"MarketIndices": YFinanceIndexHistoricalFetcher,
"OptionsChains": YFinanceOptionsChainsFetcher,
"PriceTargetConsensus": YFinancePriceTargetConsensusFetcher,
"ShareStatistics": YFinanceShareStatisticsFetcher,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"""YFinance Options Chains Model."""

# pylint: disable=unused-argument

import asyncio
from datetime import datetime
from typing import Any, Dict, List, Optional

import yfinance as yf
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 EmptyDataError
from pandas import concat
from pydantic import Field
from pytz import timezone


class YFinanceOptionsChainsQueryParams(OptionsChainsQueryParams):
"""YFinance Options Chains Query Parameters."""


class YFinanceOptionsChainsData(OptionsChainsData):
"""YFinance Options Chains Data."""

__alias_dict__ = {
"contract_symbol": "contractSymbol",
"last_trade_timestamp": "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(
default=None,
description="Timestamp for when the option was last traded.",
)


class YFinanceOptionsChainsFetcher(
Fetcher[YFinanceOptionsChainsQueryParams, List[YFinanceOptionsChainsData]]
):
"""YFinance Options Chains Fetcher."""

@staticmethod
def transform_query(params: Dict[str, Any]) -> YFinanceOptionsChainsQueryParams:
"""Transform the query."""
return YFinanceOptionsChainsQueryParams(**params)

@staticmethod
async def aextract_data(
query: YFinanceOptionsChainsQueryParams,
credentials: Optional[Dict[str, str]],
**kwargs: Any,
) -> Dict:
"""Extract the raw data from YFinance."""
symbol = query.symbol.upper()
ticker = yf.Ticker(symbol)
expirations = list(ticker.options)
if not expirations or len(expirations) == 0:
raise ValueError(f"No options found for {symbol}")
chains_output: List = []
underlying = ticker.option_chain(expirations[0])[2]
underlying_output: Dict = {
"symbol": symbol,
"name": underlying.get("longName"),
"exchange": underlying.get("fullExchangeName"),
"exchange_tz": underlying.get("exchangeTimezoneName"),
"currency": underlying.get("currency"),
"bid": underlying.get("bid"),
"bid_size": underlying.get("bidSize"),
"ask": underlying.get("ask"),
"ask_size": underlying.get("askSize"),
"last_price": underlying.get(
"postMarketPrice", underlying.get("regularMarketPrice")
),
"open": underlying.get("regularMarketOpen", None),
"high": underlying.get("regularMarketDayHigh", None),
"low": underlying.get("regularMarketDayLow", None),
"close": underlying.get("regularMarketPrice", None),
"prev_close": underlying.get("regularMarketPreviousClose", None),
"change": underlying.get("regularMarketChange", None),
"change_percent": underlying.get("regularMarketChangePercent", None),
"volume": underlying.get("regularMarketVolume", None),
"dividend_yield": float(underlying.get("dividendYield", 0)) / 100,
"dividend_yield_ttm": underlying.get("trailingAnnualDividendYield", None),
"year_high": underlying.get("fiftyTwoWeekHigh", None),
"year_low": underlying.get("fiftyTwoWeekLow", None),
"ma_50": underlying.get("fiftyDayAverage", None),
"ma_200": underlying.get("twoHundredDayAverage", None),
"volume_avg_10d": underlying.get("averageDailyVolume10Day", None),
"volume_avg_3m": underlying.get("averageDailyVolume3Month", None),
"market_cap": underlying.get("marketCap", None),
"shares_outstanding": underlying.get("sharesOutstanding", None),
}
tz = timezone(underlying_output.get("exchange_tz", "UTC"))

async def get_chain(ticker, expiration, tz):
"""Get the data for one expiration."""
exp = datetime.strptime(expiration, "%Y-%m-%d").date()
now = datetime.now().date()
dte = (exp - now).days
calls = ticker.option_chain(expiration, tz=tz)[0]
calls["option_type"] = "call"
calls["expiration"] = expiration
puts = ticker.option_chain(expiration, tz=tz)[1]
puts["option_type"] = "put"
puts["expiration"] = expiration
chain = concat([calls, puts])
chain = (
chain.set_index(["strike", "option_type", "contractSymbol"])
.sort_index()
.reset_index()
)
chain["dte"] = dte
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]
)

if not chains_output:
raise EmptyDataError(f"No data was returned for {symbol}")
return {"underlying": underlying_output, "chains": chains_output}

@staticmethod
def transform_data(
query: YFinanceOptionsChainsQueryParams,
data: Dict,
**kwargs: Any,
) -> List[YFinanceOptionsChainsData]:
"""Transform the data."""
if not data:
raise EmptyDataError()
metadata = data.get("underlying", {})
records = data.get("chains", [])
return AnnotatedResult(
result=[YFinanceOptionsChainsData.model_validate(r) for r in records],
metadata=metadata,
)
Loading

0 comments on commit af6fa04

Please sign in to comment.