Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Options Chains From YFinance #6468

Merged
merged 5 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading