diff options
author | Danglewood <85772166+deeleeramone@users.noreply.github.com> | 2024-05-27 02:21:23 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-27 09:21:23 +0000 |
commit | af6fa043167f4fcece55b6eb80036d99acc1788d (patch) | |
tree | dd5e0e1d1433b1035fe029264b9d113deaacc9f9 | |
parent | 27d448e0d37909ac901fab1162a80372d07f58f4 (diff) |
[Feature] Options Chains From YFinance (#6468)
* add yfinance to options chains
* Explicit None
---------
Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com>
8 files changed, 3772 insertions, 7 deletions
diff --git a/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py b/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py index a777ddb3bf5..210ab79190c 100644 --- a/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py +++ b/openbb_platform/extensions/derivatives/integration/test_derivatives_api.py @@ -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", diff --git a/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py b/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py index 1ba7c63fd2d..1bc0597855f 100644 --- a/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py +++ b/openbb_platform/extensions/derivatives/integration/test_derivatives_python.py @@ -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", diff --git a/openbb_platform/openbb/assets/reference.json b/openbb_platform/openbb/assets/reference.json index 0c01282e9c1..0b586facb6f 100644 --- a/openbb_platform/openbb/assets/reference.json +++ b/openbb_platform/openbb/assets/reference.json @@ -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" diff --git a/openbb_platform/openbb/package/derivatives_options.py b/openbb_platform/openbb/package/derivatives_options.py index 759fba32fda..513b470d6dd 100644 --- a/openbb_platform/openbb/package/derivatives_options.py +++ b/openbb_platform/openbb/package/derivatives_options.py @@ -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={ diff --git a/openbb_platform/providers/yfinance/openbb_yfinance/__init__.py b/openbb_platform/providers/yfinance/openbb_yfinance/__init__.py index fe6e2934de8..c5bd29a8386 100644 --- a/openbb_platform/providers/yfinance/openbb_yfinance/__init__.py +++ b/openbb_platform/providers/yfinance/openbb_yfinance/__init__.py @@ -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, }, diff --git a/openbb_platform/providers/yfinance/openbb_yfinance/models/options_chains.py b/openbb_platform/providers/yfinance/openbb_yfinance/models/options_chains.py new file mode 100644 index 00000000000..62651b94c84 --- /dev/null +++ b/openbb_platform/providers/yfinance/openbb_yfinance/models/options_chains.py @@ -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, + ) diff --git a/openbb_platform/providers/yfinance/tests/record/http/test_yfinance_fetchers/test_y_finance_options_chains_fetcher.yaml b/openbb_platform/providers/yfinance/tests/record/http/test_yfinance_fetchers/test_y_finance_options_chains_fetcher.yaml new file mode 100644 index 00000000000..a09bb6d644c --- /dev/null +++ b/openbb_platform/providers/yfinance/tests/record/http/test_yfinance_fetchers/test_y_finance_options_chains_fetcher.yaml @@ -0,0 +1,3557 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://fc.yahoo.com/ + response: + body: + string: "<!DOCTYPE html>\n<html lang=\"en-us\">\n <head>\n <meta http-equiv=\"content-type\" + content=\"text/html; charset=UTF-8\">\n <meta charset=\"utf-8\">\n <title>Yahoo</title>\n + \ <meta name=\"viewport\" content=\"width=device-width,initial-scale=1,minimal-ui\">\n + \ <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge,chrome=1\">\n <style>\n + \ html {\n height: 100%;\n }\n body {\n background: + #fafafc url(https://s.yimg.com/nn/img/sad-panda-201402200631.png) 50% 50%;\n + \ background-size: cover;\n height: 100%;\n text-align: + center;\n font: 300 18px \"helvetica neue\", helvetica, verdana, + tahoma, arial, sans-serif;\n margin: 0;\n }\n table {\n + \ height: 100%;\n width: 100%;\n table-layout: fixed;\n + \ border-collapse: collapse;\n border-spacing: 0;\n border: + none;\n }\n h1 {\n font-size: 42px;\n font-weight: + 400;\n color: #400090;\n }\n p {\n color: #1A1A1A;\n + \ }\n #message-1 {\n font-weight: bold;\n margin: + 0;\n }\n #message-2 {\n display: inline-block;\n *display: + inline;\n zoom: 1;\n max-width: 17em;\n _width: + 17em;\n }\n </style>\n <script>\n !function(){if(window==window.top){var + o=window.location.host;o.endsWith(\".yahoo.com\")&&window.location.replace(\"https://www.yahoo.com/\"),o.endsWith(\".aol.com\")&&window.location.replace(\"https://www.aol.com/\"),o.endsWith(\".huffpost.com\")&&window.location.replace(\"https://www.huffpost.com/\"),o.endsWith(\".engadget.com\")&&window.location.replace(\"https://www.engadget.com/\")}}();\n + \ </script>\n </head>\n <body>\n <!-- status code : 404 -->\n <!-- + Not Found on Accelerator -->\n <!-- host machine: e11.ycpi.swb.yahoo.com + -->\n <!-- timestamp: 1716668161.976 -->\n <!-- url: https://fc.yahoo.com/-->\n + \ <script type=\"text/javascript\">\n function buildUrl(url, parameters){\n + \ var qs = [];\n for(var key in parameters) {\n var value + = parameters[key];\n qs.push(encodeURIComponent(key) + \"=\" + encodeURIComponent(value));\n + \ }\n url = url + \"?\" + qs.join('&');\n return url;\n }\n\n + \ function generateBRBMarkup(site) {\n params.source = 'brb';\n generateBeaconMarkup(params);\n + \ var englishHeader = 'Will be right back...';\n var englishMessage1 + = 'Thank you for your patience.';\n var englishMessage2 = 'Our engineers + are working quickly to resolve the issue.';\n var defaultLogoStyle = + '';\n var siteDataMap = {\n 'default': {\n logo: 'https://s.yimg.com/rz/p/yahoo_frontpage_en-US_s_f_p_205x58_frontpage.png',\n + \ logoAlt: 'Yahoo Logo',\n logoStyle: defaultLogoStyle,\n + \ header: englishHeader,\n message1: englishMessage1,\n message2: + englishMessage2\n }\n };\n\n var siteDetails = siteDataMap['default'];\n\n + \ document.write('<table><tbody><tr><td>');\n document.write('<div + id=\"content\">');\n document.write('<img src=\"' + siteDetails['logo'] + + '\" alt=\"' + siteDetails['logoAlt'] + '\" style=\"' + siteDetails['logoStyle'] + + '\">');\n document.write('<h1 style=\"margin-top:20px;\">' + siteDetails['header'] + + '</h1>');\n document.write('<p id=\"message-1\">' + siteDetails['message1'] + + '</p>');\n document.write('<p id=\"message-2\">' + siteDetails['message2'] + + '</p>');\n document.write('</div>');\n document.write('</td></tr></tbody></table>');\n + \ }\n\n function generateBeaconMarkup(params) {\n document.write('<img + src=\"' + buildUrl('//geo.yahoo.com/b', params) + '\" style=\"display:none;\" + width=\"0px\" height=\"0px\"/>');\n var beacon = new Image();\n beacon.src + = buildUrl('//bcn.fp.yahoo.com/p', params);\n }\n\n var hostname = window.location.hostname;\n + \ var device = 'desktop';\n var ynet = ('-' === '1');\n var time = + new Date().getTime();\n var params = {\n s: '1197757129',\n t: + time,\n err_url: document.URL,\n err: '404',\n test: + '-',\n ats_host: 'e11.ycpi.swb.yahoo.com',\n rid: '-',\n message: + 'Not Found on Accelerator'\n };\n\n if(ynet) {\n document.write('<div + style=\"height: 5px; background-color: red;\"></div>');\n }\n generateBRBMarkup(hostname, + params);\n\n </script>\n <noscript>\n <table>\n <tbody>\n <tr>\n + \ <td>\n <div id=\"englishContent\">\n <h1 style=\"margin-top:20px;\">Will + be right back...</h1>\n <p id=\"message-1\">Thank you for your + patience.</p>\n <p id=\"message-2\">Our engineers are working quickly + to resolve the issue.</p>\n </div>\n </td>\n </tr>\n + \ </tbody>\n </table>\n </noscript>\n </body>\n</html>\n" + headers: + Cache-Control: + - no-store + Connection: + - keep-alive + Content-Language: + - en + Content-Length: + - '4744' + Content-Type: + - text/html + Date: + - Sat, 25 May 2024 20:16:01 GMT + Expect-CT: + - max-age=31536000, report-uri="http://csp.yahoo.com/beacon/csp?src=yahoocom-expect-ct-report-only" + Server: + - ATS + Set-Cookie: + - A3=d=AQABBAFHUmYCEC-YNMKwonOxadtidauIWR8FEgEBAQGYU2ZcZiXUxyMA_eMAAA&S=AQAAAix-3-hNc-o8mQIBdHXDwDA; + Expires=Mon, 26 May 2025 02:16:01 GMT; Max-Age=31557600; Domain=.yahoo.com; + Path=/; SameSite=None; Secure; HttpOnly + X-Content-Type-Options: + - nosniff + X-XSS-Protection: + - 1; mode=block + status: + code: 404 + message: Not Found on Accelerator +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Cookie: + - MOCK_COOKIE + method: GET + uri: https://query1.finance.yahoo.com/v1/test/getcrumb + response: + body: + string: lBVv2pgWbZ4 + headers: + Age: + - '1' + Connection: + - keep-alive + Expect-CT: + - max-age=31536000, report-uri="http://csp.yahoo.com/beacon/csp?src=yahoocom-expect-ct-report-only" + Referrer-Policy: + - no-referrer-when-downgrade + Strict-Transport-Security: + - max-age=31536000 + X-Content-Type-Options: + - nosniff + X-XSS-Protection: + - 1; mode=block + cache-control: + - private, max-age=60, stale-while-revalidate=30 + content-length: + - '11' + content-type: + - text/plain;charset=utf-8 + date: + - Sat, 25 May 2024 20:16:01 GMT + server: + - ATS + vary: + - Origin,Accept-Encoding + x-envoy-decorator-operation: + - finance-external-services-api--mtls-baseline-production-gq1.finance-k8s.svc.yahoo.local:4080/* + x-envoy-upstream-service-time: + - '1' + y-rid: + - 4ue40o1j54ho2 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Cookie: + - MOCK_COOKIE + method: GET + uri: https://query2.finance.yahoo.com/v7/finance/options/OXY?crumb=MOCK_CRUMB + response: + body: + string: !!binary | + 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 + 3qnLRwAA + headers: + Age: + - '1' + Connection: + - keep-alive + Expect-CT: + - max-age=31536000, report-uri="http://csp.yahoo.com/beacon/csp?src=yahoocom-expect-ct-report-only" + Referrer-Policy: + - no-referrer-when-downgrade + Strict-Transport-Security: + - max-age=31536000 + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + X-XSS-Protection: + - 1; mode=block + cache-control: + - public, max-age=1, stale-while-revalidate=9 + content-encoding: + - gzip + content-type: + - application/json;charset=utf-8 + date: + - Sat, 25 May 2024 20:16:01 GMT |