summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanglewood <85772166+deeleeramone@users.noreply.github.com>2024-05-27 02:21:23 -0700
committerGitHub <noreply@github.com>2024-05-27 09:21:23 +0000
commitaf6fa043167f4fcece55b6eb80036d99acc1788d (patch)
treedd5e0e1d1433b1035fe029264b9d113deaacc9f9
parent27d448e0d37909ac901fab1162a80372d07f58f4 (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>
-rw-r--r--openbb_platform/extensions/derivatives/integration/test_derivatives_api.py1
-rw-r--r--openbb_platform/extensions/derivatives/integration/test_derivatives_python.py1
-rw-r--r--openbb_platform/openbb/assets/reference.json33
-rw-r--r--openbb_platform/openbb/package/derivatives_options.py14
-rw-r--r--openbb_platform/providers/yfinance/openbb_yfinance/__init__.py2
-rw-r--r--openbb_platform/providers/yfinance/openbb_yfinance/models/options_chains.py159
-rw-r--r--openbb_platform/providers/yfinance/tests/record/http/test_yfinance_fetchers/test_y_finance_options_chains_fetcher.yaml3557
-rw-r--r--openbb_platform/providers/yfinance/tests/test_yfinance_fetchers.py12
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
+ server:
+ - ATS
+ vary:
+ - Origin,Accept-Encoding
+ x-envoy-decorator-operation:
+ - f