[Feature] Options Chains From YFinance (#6468)

* add yfinance to options chains

* Explicit None

---------

Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com>
This commit is contained in:
Danglewood
2024-05-27 02:21:23 -07:00
committed by GitHub
parent 27d448e0d3
commit af6fa04316
8 changed files with 3772 additions and 7 deletions

View File

@@ -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",

View File

@@ -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",

View File

@@ -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
@@ -1214,7 +1214,8 @@
"optional": true,
"choices": null
}
]
],
"yfinance": []
},
"returns": {
"OBBject": [
@@ -1225,7 +1226,7 @@
},
{
"name": "provider",
"type": "Optional[Literal['intrinio']]",
"type": "Optional[Literal['intrinio', 'yfinance']]",
"description": "Provider name."
},
{
@@ -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"

View File

@@ -25,7 +25,7 @@ class ROUTER_derivatives_options(Container):
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."
),
@@ -38,7 +38,7 @@ class ROUTER_derivatives_options(Container):
----------
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.
@@ -50,7 +50,7 @@ class ROUTER_derivatives_options(Container):
OBBject
results : List[OptionsChains]
Serializable results.
provider : Optional[Literal['intrinio']]
provider : Optional[Literal['intrinio', 'yfinance']]
Provider name.
warnings : Optional[List[Warning_]]
List of warnings.
@@ -149,6 +149,12 @@ class ROUTER_derivatives_options(Container):
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
--------
@@ -165,7 +171,7 @@ class ROUTER_derivatives_options(Container):
"provider": self._get_provider(
provider,
"/derivatives/options/chains",
("intrinio",),
("intrinio", "yfinance"),
)
},
standard_params={

View File

@@ -27,6 +27,7 @@ from openbb_yfinance.models.index_historical import (
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,
)
@@ -69,6 +70,7 @@ financial markets and assets.""",
"KeyExecutives": YFinanceKeyExecutivesFetcher,
"KeyMetrics": YFinanceKeyMetricsFetcher,
"MarketIndices": YFinanceIndexHistoricalFetcher,
"OptionsChains": YFinanceOptionsChainsFetcher,
"PriceTargetConsensus": YFinancePriceTargetConsensusFetcher,
"ShareStatistics": YFinanceShareStatisticsFetcher,
},

View File

@@ -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,
)

View File

@@ -34,6 +34,7 @@ from openbb_yfinance.models.losers import YFLosersFetcher
from openbb_yfinance.models.market_indices import (
YFinanceMarketIndicesFetcher,
)
from openbb_yfinance.models.options_chains import YFinanceOptionsChainsFetcher
from openbb_yfinance.models.price_target_consensus import (
YFinancePriceTargetConsensusFetcher,
)
@@ -61,10 +62,21 @@ def vcr_config():
("period1", "MOCK_PERIOD_1"),
("period2", "MOCK_PERIOD_2"),
("crumb", "MOCK_CRUMB"),
("date", "MOCK_DATE"),
],
}
@pytest.mark.record_http
def test_y_finance_options_chains_fetcher(credentials=test_credentials):
"""Test YFinanceOptionsChainsFetcher."""
params = {"symbol": "OXY"}
fetcher = YFinanceOptionsChainsFetcher()
result = fetcher.test(params, credentials)
assert result is None
@pytest.mark.record_http
def test_y_finance_crypto_historical_fetcher(credentials=test_credentials):
"""Test YFinanceCryptoHistoricalFetcher."""