From 5ff4d8907ce2fd0c2bd59fd1c6a0daae4656c43b Mon Sep 17 00:00:00 2001 From: Danglewood <85772166+deeleeramone@users.noreply.github.com> Date: Sat, 25 May 2024 04:28:45 -0700 Subject: [BugFix] Fix Seeking Alpha (#6461) * fix seeking alpha * try again sequence * prevent key error * move value check up the sequence * filter test cassette more --------- Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com> --- .../standard_models/upcoming_release_days.py | 23 -- .../equity/integration/test_equity_api.py | 42 ++- .../equity/integration/test_equity_python.py | 39 +- .../openbb_equity/discovery/discovery_router.py | 14 - .../seeking_alpha/openbb_seeking_alpha/__init__.py | 12 +- .../models/calendar_earnings.py | 144 ++++++++ .../models/forward_eps_estimates.py | 207 +++++++++++ .../models/forward_sales_estimates.py | 179 +++++++++ .../models/upcoming_release_days.py | 108 ------ .../openbb_seeking_alpha/utils/helpers.py | 40 +++ .../test_sa_calendar_earnings_fetcher.yaml | 400 +++++++++++++++++++++ .../test_sa_forward_eps_estimates.yaml | 223 ++++++++++++ .../test_sa_forward_sales_estimates.yaml | 208 +++++++++++ .../test_sa_upcoming_release_days_fetcher.yaml | 249 ------------- .../tests/test_seeking_alpha_fetchers.py | 40 ++- 15 files changed, 1490 insertions(+), 438 deletions(-) delete mode 100644 openbb_platform/core/openbb_core/provider/standard_models/upcoming_release_days.py create mode 100644 openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/calendar_earnings.py create mode 100644 openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/forward_eps_estimates.py create mode 100644 openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/forward_sales_estimates.py delete mode 100644 openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/upcoming_release_days.py create mode 100644 openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/utils/helpers.py create mode 100644 openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_calendar_earnings_fetcher.yaml create mode 100644 openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_forward_eps_estimates.yaml create mode 100644 openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_forward_sales_estimates.yaml delete mode 100644 openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_upcoming_release_days_fetcher.yaml diff --git a/openbb_platform/core/openbb_core/provider/standard_models/upcoming_release_days.py b/openbb_platform/core/openbb_core/provider/standard_models/upcoming_release_days.py deleted file mode 100644 index 34560c90e5d..00000000000 --- a/openbb_platform/core/openbb_core/provider/standard_models/upcoming_release_days.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Upcoming Release Days Standard Model.""" - -from datetime import date as dateType - -from pydantic import Field - -from openbb_core.provider.abstract.data import Data -from openbb_core.provider.abstract.query_params import QueryParams -from openbb_core.provider.utils.descriptions import DATA_DESCRIPTIONS - - -class UpcomingReleaseDaysQueryParams(QueryParams): - """Upcoming Release Days Query.""" - - -class UpcomingReleaseDaysData(Data): - """Upcoming Release Days Data.""" - - symbol: str = Field(description=DATA_DESCRIPTIONS.get("symbol", "")) - name: str = Field(description="The full name of the asset.") - exchange: str = Field(description="The exchange the asset is traded on.") - release_time_type: str = Field(description="The type of release time.") - release_date: dateType = Field(description="The date of the release.") diff --git a/openbb_platform/extensions/equity/integration/test_equity_api.py b/openbb_platform/extensions/equity/integration/test_equity_api.py index f47350f300c..ffe57cda534 100644 --- a/openbb_platform/extensions/equity/integration/test_equity_api.py +++ b/openbb_platform/extensions/equity/integration/test_equity_api.py @@ -144,6 +144,14 @@ def test_equity_calendar_splits(params, headers): ({"start_date": "2023-11-09", "end_date": "2023-11-10", "provider": "fmp"}), ({"start_date": "2023-11-09", "end_date": "2023-11-10", "provider": "nasdaq"}), ({"start_date": "2023-11-09", "end_date": "2023-11-10", "provider": "tmx"}), + ( + { + "start_date": None, + "end_date": None, + "provider": "seeking_alpha", + "country": "us", + } + ), ], ) @pytest.mark.integration @@ -397,7 +405,14 @@ def test_equity_estimates_historical(params, headers): "calendar_period": None, "provider": "intrinio", } - ) + ), + ( + { + "symbol": "AAPL,BAM:CA", + "period": "annual", + "provider": "seeking_alpha", + } + ), ], ) @pytest.mark.integration @@ -434,6 +449,13 @@ def test_equity_estimates_forward_sales(params, headers): "provider": "fmp", } ), + ( + { + "symbol": "AAPL,BAM:CA", + "period": "annual", + "provider": "seeking_alpha", + } + ), ], ) @pytest.mark.integration @@ -1652,24 +1674,6 @@ def test_equity_discovery_top_retail(params, headers): assert result.status_code == 200 -@parametrize( - "params", - [({"provider": "seeking_alpha"})], -) -@pytest.mark.integration -def test_equity_discovery_upcoming_release_days(params, headers): - """Test the equity discovery upcoming release days endpoint.""" - params = {p: v for p, v in params.items() if v} - - query_str = get_querystring(params, []) - url = ( - f"http://0.0.0.0:8000/api/v1/equity/discovery/upcoming_release_days?{query_str}" - ) - result = requests.get(url, headers=headers, timeout=30) - assert isinstance(result, requests.Response) - assert result.status_code == 200 - - @parametrize( "params", [ diff --git a/openbb_platform/extensions/equity/integration/test_equity_python.py b/openbb_platform/extensions/equity/integration/test_equity_python.py index 66ed58e6fde..0e167cae8df 100644 --- a/openbb_platform/extensions/equity/integration/test_equity_python.py +++ b/openbb_platform/extensions/equity/integration/test_equity_python.py @@ -132,6 +132,14 @@ def test_equity_calendar_splits(params, obb): ({"start_date": "2023-11-09", "end_date": "2023-11-10", "provider": "fmp"}), ({"start_date": "2023-11-09", "end_date": "2023-11-10", "provider": "nasdaq"}), ({"start_date": "2023-11-09", "end_date": "2023-11-10", "provider": "tmx"}), + ( + { + "start_date": None, + "end_date": None, + "provider": "seeking_alpha", + "country": "us", + } + ), ], ) @pytest.mark.integration @@ -705,7 +713,14 @@ def test_equity_estimates_consensus(params, obb): "calendar_period": None, "provider": "intrinio", } - ) + ), + ( + { + "symbol": "AAPL,BAM:CA", + "period": "annual", + "provider": "seeking_alpha", + } + ), ], ) @pytest.mark.integration @@ -739,6 +754,13 @@ def test_equity_estimates_forward_sales(params, obb): "provider": "fmp", } ), + ( + { + "symbol": "AAPL,BAM:CA", + "period": "annual", + "provider": "seeking_alpha", + } + ), ], ) @pytest.mark.integration @@ -1570,21 +1592,6 @@ def test_equity_discovery_top_retail(params, obb): assert len(result.results) > 0 -@parametrize( - "params", - [({"provider": "seeking_alpha"})], -) -@pytest.mark.integration -def test_equity_discovery_upcoming_release_days(params, obb): - """Test the equity discovery upcoming release days endpoint.""" - params = {p: v for p, v in params.items() if v} - - result = obb.equity.discovery.upcoming_release_days(**params) - assert result - assert isinstance(result, OBBject) - assert len(result.results) > 0 - - @parametrize( "params", [ diff --git a/openbb_platform/extensions/equity/openbb_equity/discovery/discovery_router.py b/openbb_platform/extensions/equity/openbb_equity/discovery/discovery_router.py index b8fbbc1a7be..652227975a6 100644 --- a/openbb_platform/extensions/equity/openbb_equity/discovery/discovery_router.py +++ b/openbb_platform/extensions/equity/openbb_equity/discovery/discovery_router.py @@ -152,20 +152,6 @@ async def top_retail( return await OBBject.from_query(Query(**locals())) -@router.command( - model="UpcomingReleaseDays", - examples=[APIEx(parameters={"provider": "seeking_alpha"})], -) -async def upcoming_release_days( - cc: CommandContext, - provider_choices: ProviderChoices, - standard_params: StandardParams, - extra_params: ExtraParams, -) -> OBBject: - """Get upcoming earnings release dates.""" - return await OBBject.from_query(Query(**locals())) - - @router.command( model="DiscoveryFilings", examples=[ diff --git a/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/__init__.py b/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/__init__.py index e4022980de8..38b5757c7b1 100644 --- a/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/__init__.py +++ b/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/__init__.py @@ -1,8 +1,12 @@ """Seeking Alpha Provider module.""" from openbb_core.provider.abstract.provider import Provider -from openbb_seeking_alpha.models.upcoming_release_days import ( - SAUpcomingReleaseDaysFetcher, +from openbb_seeking_alpha.models.calendar_earnings import SACalendarEarningsFetcher +from openbb_seeking_alpha.models.forward_eps_estimates import ( + SAForwardEpsEstimatesFetcher, +) +from openbb_seeking_alpha.models.forward_sales_estimates import ( + SAForwardSalesEstimatesFetcher, ) seeking_alpha_provider = Provider( @@ -11,7 +15,9 @@ seeking_alpha_provider = Provider( description="""Seeking Alpha is a data provider with access to news, analysis, and real-time alerts on stocks.""", fetcher_dict={ - "UpcomingReleaseDays": SAUpcomingReleaseDaysFetcher, + "CalendarEarnings": SACalendarEarningsFetcher, + "ForwardEpsEstimates": SAForwardEpsEstimatesFetcher, + "ForwardSalesEstimates": SAForwardSalesEstimatesFetcher, }, repr_name="Seeking Alpha", ) diff --git a/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/calendar_earnings.py b/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/calendar_earnings.py new file mode 100644 index 00000000000..67b6ee13339 --- /dev/null +++ b/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/calendar_earnings.py @@ -0,0 +1,144 @@ +"""Seeking Alpha Calendar Earnings Model.""" + +# pylint: disable=unused-argument + +import asyncio +import json +from datetime import datetime, timedelta +from typing import Any, Dict, List, Literal, Optional +from warnings import warn + +from openbb_core.provider.abstract.fetcher import Fetcher +from openbb_core.provider.standard_models.calendar_earnings import ( + CalendarEarningsData, + CalendarEarningsQueryParams, +) +from openbb_core.provider.utils.helpers import amake_request +from openbb_seeking_alpha.utils.helpers import HEADERS, date_range +from pydantic import Field, field_validator + + +class SACalendarEarningsQueryParams(CalendarEarningsQueryParams): + """Seeking Alpha Calendar Earnings Query. + + Source: https://seekingalpha.com/earnings/earnings-calendar + """ + + country: Literal["us", "ca"] = Field( + default="us", + description="The country to get calendar data for.", + json_schema_extra={"choices": ["us", "ca"]}, + ) + + +class SACalendarEarningsData(CalendarEarningsData): + """Seeking Alpha Calendar Earnings Data.""" + + market_cap: Optional[float] = Field( + default=None, + description="Market cap of the entity.", + ) + reporting_time: Optional[str] = Field( + default=None, + description="The reporting time - e.g. after market close.", + ) + exchange: Optional[str] = Field( + default=None, + description="The primary trading exchange.", + ) + sector_id: Optional[int] = Field( + default=None, + description="The Seeking Alpha Sector ID.", + ) + + @field_validator("report_date", mode="before", check_fields=False) + @classmethod + def validate_release_date(cls, v): + """Validate the release date.""" + v = v.split("T")[0] + return datetime.strptime(v, "%Y-%m-%d").date() + + +class SACalendarEarningsFetcher( + Fetcher[ + SACalendarEarningsQueryParams, + List[SACalendarEarningsData], + ] +): + """Seeking Alpha Calendar Earnings Fetcher.""" + + @staticmethod + def transform_query(params: Dict[str, Any]) -> SACalendarEarningsQueryParams: + """Transform the query.""" + now = datetime.today().date() + transformed_params = params + if not params.get("start_date"): + transformed_params["start_date"] = now + if not params.get("end_date"): + transformed_params["end_date"] = now + timedelta(days=3) + return SACalendarEarningsQueryParams(**transformed_params) + + @staticmethod + async def aextract_data( + query: SACalendarEarningsQueryParams, + credentials: Optional[Dict[str, str]], + **kwargs: Any, + ) -> List[Dict]: + """Return the raw data from the Seeking Alpha endpoint.""" + results: List[Dict] = [] + dates = [ + date.strftime("%Y-%m-%d") + for date in date_range(query.start_date, query.end_date) + ] + currency = "USD" if query.country == "us" else "CAD" + messages: List = [] + + async def get_date(date, currency): + """Get date for one date.""" + url = ( + f"https://seekingalpha.com/api/v3/earnings_calendar/tickers?" + f"filter%5Bselected_date%5D={date}" + f"&filter%5Bwith_rating%5D=false&filter%5Bcurrency%5D={currency}" + ) + response = await amake_request(url=url, headers=HEADERS) + # Try again if the response is blocked. + if "blockScript" in response: + response = await amake_request(url=url, headers=HEADERS) + if "blockScript" in response: + message = json.dumps(response) + messages.append(message) + warn(message) + if "data" in response: + results.extend(response.get("data")) + + await asyncio.gather(*[get_date(date, currency) for date in dates]) + + if not results: + raise RuntimeError(f"Error with the Seeking Alpha request -> {messages}") + + return results + + @staticmethod + def transform_data( + query: SACalendarEarningsQueryParams, + data: List[Dict], + **kwargs: Any, + ) -> List[SACalendarEarningsData]: + """Transform the data to the standard format.""" + transformed_data: List[SACalendarEarningsData] = [] + for row in sorted(data, key=lambda x: x["attributes"]["release_date"]): + attributes = row.get("attributes", {}) + transformed_data.append( + SACalendarEarningsData.model_validate( + { + "report_date": attributes.get("release_date"), + "reporting_time": attributes.get("release_time"), + "symbol": attributes.get("slug"), + "name": attributes.get("name"), + "market_cap": attributes.get("marketcap"), + "exchange": attributes.get("exchange"), + "sector_id": attributes.get("sector_id"), + } + ) + ) + return transformed_data diff --git a/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/forward_eps_estimates.py b/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/forward_eps_estimates.py new file mode 100644 index 00000000000..fb0c4458eca --- /dev/null +++ b/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/forward_eps_estimates.py @@ -0,0 +1,207 @@ +"""Seeking Alpha Forward EPS Estimates Model.""" + +# pylint: disable=unused-argument + +from typing import Any, Dict, List, Literal, Optional +from warnings import warn + +from openbb_core.provider.abstract.fetcher import Fetcher +from openbb_core.provider.standard_models.forward_eps_estimates import ( + ForwardEpsEstimatesData, + ForwardEpsEstimatesQueryParams, +) +from openbb_core.provider.utils.helpers import amake_request +from openbb_seeking_alpha.utils.helpers import HEADERS, get_seekingalpha_id +from pydantic import Field, field_validator + + +class SAForwardEpsEstimatesQueryParams(ForwardEpsEstimatesQueryParams): + """Seeking Alpha Forward EPS Estimates Query. + + Source: https://seekingalpha.com/earnings/earnings-calendar + """ + + __json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}} + + period: Literal["annual", "quarter"] = Field( + default="quarter", + description="The reporting period.", + json_schema_extra={"choices": ["annual", "quarter"]}, + ) + + @field_validator("symbol", mode="before", check_fields=False) + @classmethod + def check_symbol(cls, value): + """Check the symbol.""" + if not value: + raise RuntimeError("Error: Symbol is a required field for Seeking Alpha.") + return value + + +class SAForwardEpsEstimatesData(ForwardEpsEstimatesData): + """Seeking Alpha Forward EPS Estimates Data.""" + + normalized_actual: Optional[float] = Field( + default=None, + description="Actual normalized EPS.", + ) + period_growth: Optional[float] = Field( + default=None, + description="Estimated (or actual if reported) EPS growth for the period.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + low_estimate_gaap: Optional[float] = Field( + default=None, + description="Estimated GAAP EPS low for the period.", + ) + high_estimate_gaap: Optional[float] = Field( + default=None, + description="Estimated GAAP EPS high for the period.", + ) + mean_gaap: Optional[float] = Field( + default=None, + description="Estimated GAAP EPS mean for the period.", + ) + gaap_actual: Optional[float] = Field( + default=None, + description="Actual GAAP EPS.", + ) + + +class SAForwardEpsEstimatesFetcher( + Fetcher[ + SAForwardEpsEstimatesQueryParams, + List[SAForwardEpsEstimatesData], + ] +): + """Seeking Alpha Forward EPS Estimates Fetcher.""" + + @staticmethod + def transform_query(params: Dict[str, Any]) -> SAForwardEpsEstimatesQueryParams: + """Transform the query.""" + return SAForwardEpsEstimatesQueryParams(**params) + + @staticmethod + async def aextract_data( + query: SAForwardEpsEstimatesQueryParams, + credentials: Optional[Dict[str, str]], + **kwargs: Any, + ) -> Dict: + """Return the raw data from the Seeking Alpha endpoint.""" + tickers = query.symbol.split(",") + fp = query.period if query.period == "annual" else "quarterly" + url = "https://seekingalpha.com/api/v3/symbol_data/estimates" + querystring: Dict = { + "estimates_data_items": "eps_normalized_actual,eps_normalized_consensus_low,eps_normalized_consensus_mean," + "eps_normalized_consensus_high,eps_normalized_num_of_estimates," + "eps_gaap_actual,eps_gaap_consensus_low,eps_gaap_consensus_mean,eps_gaap_consensus_high,", + "period_type": fp, + "relative_periods": "-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11,12", + } + ids: Dict = {ticker: await get_seekingalpha_id(ticker) for ticker in tickers} + querystring["ticker_ids"] = (",").join(list(ids.values())) + payload: str = "" + response = await amake_request( + url, data=payload, headers=HEADERS, params=querystring + ) + estimates: Dict = response.get("estimates", {}) # type: ignore + if not estimates: + raise RuntimeError(f"No estimates data was returned for: {query.symbol}") + + output: Dict = {"ids": ids, "estimates": estimates} + + return output + + @staticmethod + def transform_data( + query: SAForwardEpsEstimatesQueryParams, + data: Dict, + **kwargs: Any, + ) -> List[SAForwardEpsEstimatesData]: + """Transform the data to the standard format.""" + tickers = query.symbol.split(",") + ids = data.get("ids") + estimates = data.get("estimates") + results: List[SAForwardEpsEstimatesData] = [] + for ticker in tickers: + sa_id = str(ids.get(ticker, "")) + if sa_id == "" or sa_id not in estimates: + warn(f"Symbol Error: No data found for, {ticker}") + seek_object = estimates.get(sa_id) + items = len(seek_object["eps_normalized_num_of_estimates"]) + for i in range(0, items - 4): + eps_estimates: Dict = {} + eps_estimates["symbol"] = ticker + num_estimates = seek_object["eps_normalized_num_of_estimates"].get(str(i)) + if not num_estimates: + continue + period = num_estimates[0].get("period", {}) + if period: + period_type = period.get("periodtypeid") + eps_estimates["calendar_year"] = period.get("calendaryear") + eps_estimates["calendar_period"] = ( + "Q" + str(period.get("calendarquarter", "")) + if period_type == "quarterly" + else "FY" + ) + eps_estimates["date"] = period.get("periodenddate").split("T")[0] + eps_estimates["fiscal_year"] = period.get("fiscalyear") + eps_estimates["fiscal_period"] = ( + "Q" + str(period.get("fiscalquarter", "")) + if period_type == "quarterly" + else "FY" + ) + eps_estimates["number_of_analysts"] = num_estimates[0].get( + "dataitemvalue" + ) + actual = seek_object["eps_normalized_actual"].get(str(i)) + if actual: + eps_estimates["normalized_actual"] = actual[0].get("dataitemvalue") + gaap_actual = seek_object["eps_gaap_actual"].get(str(i)) + if gaap_actual: + eps_estimates["gaap_actual"] = gaap_actual[0].get("dataitemvalue") + low = seek_object["eps_normalized_consensus_low"].get(str(i)) + if low: + eps_estimates["low_estimate"] = low[0].get("dataitemvalue") + gaap_low = seek_object["eps_gaap_consensus_low"].get(str(i)) + if gaap_low: + eps_estimates["low_estimate_gaap"] = gaap_low[0].get( + "dataitemvalue" + ) + high = seek_object["eps_normalized_consensus_high"].get(str(i)) + if high: + eps_estimates["high_estimate"] = high[0].get("dataitemvalue") + gaap_high = seek_object["eps_gaap_consensus_high"].get(str(i)) + if gaap_high: + eps_estimates["high_estimate_gaap"] = gaap_high[0].get( + "dataitemvalue" + ) + mean = seek_object["eps_normalized_consensus_mean"].get(str(i)) + if mean: + mean = mean[0].get("dataitemvalue") + eps_estimates["mean"] = mean + gaap_mean = seek_object["eps_gaap_consensus_mean"].get(str(i)) + if gaap_mean: + eps_estimates["mean_gaap"] = gaap_mean[0].get("dataitemvalue") + # Calculate the estimated growth percent. + this = float(mean) if mean else None + prev = None + percent = None + try: + prev = float( + seek_object["eps_normalized_actual"][str(i - 1)][0].get( + "dataitemvalue" + ) + ) + except KeyError: + prev = float( + seek_object["eps_normalized_consensus_mean"][str(i - 1)][0].get( + "dataitemvalue" + ) + ) + if this and prev: + percent = (this - prev) / prev + eps_estimates["period_growth"] = percent + results.append(SAForwardEpsEstimatesData.model_validate(eps_estimates)) + + return results diff --git a/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/forward_sales_estimates.py b/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/forward_sales_estimates.py new file mode 100644 index 00000000000..c20e371bac0 --- /dev/null +++ b/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/forward_sales_estimates.py @@ -0,0 +1,179 @@ +"""Seeking Alpha Forward Sales Estimates Model.""" + +# pylint: disable=unused-argument + +from typing import Any, Dict, List, Literal, Optional +from warnings import warn + +from openbb_core.provider.abstract.fetcher import Fetcher +from openbb_core.provider.standard_models.forward_sales_estimates import ( + ForwardSalesEstimatesData, + ForwardSalesEstimatesQueryParams, +) +from openbb_core.provider.utils.helpers import amake_request +from openbb_seeking_alpha.utils.helpers import HEADERS, get_seekingalpha_id +from pydantic import Field, field_validator + + +class SAForwardSalesEstimatesQueryParams(ForwardSalesEstimatesQueryParams): + """Seeking Alpha Forward Sales Estimates Query. + + Source: https://seekingalpha.com/earnings/earnings-calendar + """ + + __json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}} + + period: Literal["annual", "quarter"] = Field( + default="quarter", + description="The reporting period.", + json_schema_extra={"choices": ["annual", "quarter"]}, + ) + + @field_validator("symbol", mode="before", check_fields=False) + @classmethod + def check_symbol(cls, value): + """Check the symbol.""" + if not value: + raise RuntimeError("Error: Symbol is a required field for Seeking Alpha.") + return value + + +class SAForwardSalesEstimatesData(ForwardSalesEstimatesData): + """Seeking Alpha Forward Sales Estimates Data.""" + + actual: Optional[int] = Field( + default=None, + description="Actual sales (revenue) for the period.", + ) + period_growth: Optional[float] = Field( + default=None, + description="Estimated (or actual if reported) EPS growth for the period.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + + +class SAForwardSalesEstimatesFetcher( + Fetcher[ + SAForwardSalesEstimatesQueryParams, + List[SAForwardSalesEstimatesData], + ] +): + """Seeking Alpha Forward Sales Estimates Fetcher.""" + + @staticmethod + def transform_query(params: Dict[str, Any]) -> SAForwardSalesEstimatesQueryParams: + """Transform the query.""" + return SAForwardSalesEstimatesQueryParams(**params) + + @staticmethod + async def aextract_data( + query: SAForwardSalesEstimatesQueryParams, + credentials: Optional[Dict[str, str]], + **kwargs: Any, + ) -> Dict: + """Return the raw data from the Seeking Alpha endpoint.""" + tickers = query.symbol.split(",") + fp = query.period if query.period == "annual" else "quarterly" + url = "https://seekingalpha.com/api/v3/symbol_data/estimates" + payload = "" + querystring = { + "estimates_data_items": "revenue_actual,revenue_consensus_low,revenue_consensus_mean," + "revenue_consensus_high,revenue_num_of_estimates", + "period_type": fp, + "relative_periods": "-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11,12", + } + ids = {ticker: await get_seekingalpha_id(ticker) for ticker in tickers} + querystring["ticker_ids"] = (",").join(list(ids.values())) + response = await amake_request( + url, data=payload, headers=HEADERS, params=querystring + ) + estimates = response.get("estimates", {}) # type: ignore + if not estimates: + raise RuntimeError(f"No estimates data was returned for: {query.symbol}") + output: Dict = {"ids": ids, "estimates": estimates} + + return output + + @staticmethod + def transform_data( + query: SAForwardSalesEstimatesQueryParams, + data: Dict, + **kwargs: Any, + ) -> List[SAForwardSalesEstimatesData]: + """Transform the data to the standard format.""" + tickers = query.symbol.split(",") + ids = data.get("ids") + estimates = data.get("estimates") + results: List[SAForwardSalesEstimatesData] = [] + for ticker in tickers: + sa_id = str(ids.get(ticker, "")) + if sa_id == "" or sa_id not in estimates: + warn(f"Symbol Error: No data found for, {ticker}") + seek_object = estimates.get(sa_id) + items = len(seek_object["revenue_num_of_estimates"]) + for i in range(0, items - 4): + rev_estimates: Dict = {} + rev_estimates["symbol"] = ticker + num_estimates = seek_object["revenue_num_of_estimates"].get(str(i)) + if not num_estimates: + continue + period = num_estimates[0].get("period", {}) + if period: + period_type = period.get("periodtypeid") + rev_estimates["calendar_year"] = period.get("calendaryear") + rev_estimates["calendar_period"] = ( + "Q" + str(period.get("calendarquarter", "")) + if period_type == "quarterly" + else "FY" + ) + rev_estimates["date"] = period.get("periodenddate").split("T")[0] + rev_estimates["fiscal_year"] = period.get("fiscalyear") + rev_estimates["fiscal_period"] = ( + "Q" + str(period.get("fiscalquarter", "")) + if period_type == "quarterly" + else "FY" + ) + rev_estimates["number_of_analysts"] = num_estimates[0].get( + "dataitemvalue" + ) + mean = seek_object["revenue_consensus_mean"].get(str(i)) + if mean: + mean = mean[0].get("dataitemvalue") + rev_estimates["mean"] = int(float(mean)) + actual = ( + seek_object["revenue_actual"][str(i)][0].get("dataitemvalue") + if i < 1 + else None + ) + if actual: + rev_estimates["actual"] = int(float(actual)) + low = seek_object["revenue_consensus_low"].get(str(i)) + if low: + low = low[0].get("dataitemvalue") + rev_estimates["low_estimate"] = int(float(low)) + high = seek_object["revenue_consensus_high"].get(str(i)) + if high: + high = high[0].get("dataitemvalue") + rev_estimates["high_estimate"] = int(float(high)) + # Calculate the estimated growth percent. + this = float(mean) if mean else None + prev = None + percent = None + try: + prev = float( + seek_object["revenue"][str(i - 1)][0].get("dataitemvalue") + ) + except KeyError: + prev = float( + seek_object["revenue_consensus_mean"][str(i - 1)][0].get( + "dataitemvalue" + ) + ) + if this and prev: + percent = (this - prev) / prev + rev_estimates["period_growth"] = percent + results.append( + SAForwardSalesEstimatesData.model_validate(rev_estimates) + ) + + return results diff --git a/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/upcoming_release_days.py b/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/upcoming_release_days.py deleted file mode 100644 index 3dda9734307..00000000000 --- a/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/upcoming_release_days.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Seeking Alpha Upcoming Release Days Model.""" - -from datetime import datetime -from typing import Any, Dict, List, Optional - -import requests -from openbb_core.provider.abstract.fetcher import Fetcher -from openbb_core.provider.standard_models.upcoming_release_days import ( - UpcomingReleaseDaysData, - UpcomingReleaseDaysQueryParams, -) -from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS -from pydantic import Field, field_validator - - -class SAUpcomingReleaseDaysQueryParams(UpcomingReleaseDaysQueryParams): - """Seeking Alpha Upcoming Release Days Query. - - Source: https://seekingalpha.com/api/v3/earnings_calendar/tickers - """ - - limit: int = Field( - description=QUERY_DESCRIPTIONS.get("limit", "") - + "In this case, the number of lookahead days.", - default=10, - ) - - -class SAUpcomingReleaseDaysData(UpcomingReleaseDaysData): - """Seeking Alpha Upcoming Release Days Data.""" - - __alias_dict__ = {"symbol": "slug", "release_time_type": "release_time"} - - sector_id: Optional[int] = Field( - description="The sector ID of the asset.", - ) - - @field_validator("release_date", mode="before", check_fields=False) - def validate_release_date(cls, v: Any) -> Any: # pylint: disable=E0213 - """Validate the release date.""" - v = v.split("T")[0] - return datetime.strptime(v, "%Y-%m-%d").date() - - -class SAUpcomingReleaseDaysFetcher( - Fetcher[ - SAUpcomingReleaseDaysQueryParams, - List[SAUpcomingReleaseDaysData], - ] -): - """Transform the query, extract and transform the data from the Seeking Alpha endpoints.""" - - @staticmethod - def transform_query(params: Dict[str, Any]) -> SAUpcomingReleaseDaysQueryParams: - """Transform the query.""" - return SAUpcomingReleaseDaysQueryParams(**params) - - @staticmethod - def extract_data( - query: SAUpcomingReleaseDaysQueryParams, # pylint: disable=W0613 - credentials: Optional[Dict[str, str]], - **kwargs: Any, - ) -> Dict: - """Return the raw data from the Seeking Alpha endpoint.""" - url = ( - f"https://seekingalpha.com/api/v3/earnings_calendar/tickers?" - f"filter%5Bselected_date%5D={str(datetime.now().date())}" # cspell:disable-line - f"&filter%5Bwith_rating%5D=false&filter%5Bcurrency%5D=USD" # cspell:disable-line - ) - response = requests.get( - url=url, - timeout=5, - ) - if response.status_code != 200: - raise ValueError( - f"Seeking Alpha Upcoming Release Days Fetcher failed with status code " - f"{response.status_code}" - ) - - if not response.json()["data"]: - raise ValueError( - "Seeking Alpha Upcoming Release Days Fetcher failed with empty response." - ) - - return response.json() - - @staticmethod - def transform_data( - query: SAUpcomingReleaseDaysQueryParams, data: Dict, **kwargs: Any - ) -> List[SAUpcomingReleaseDaysData]: - """Transform the data to the standard format.""" - transformed_data: List[Dict[str, Any]] = [] - data = data["data"] - for row in data: - transformed_data.append( - { - "symbol": row["attributes"]["slug"], - "name": row["attributes"]["name"], - "exchange": row["attributes"]["exchange"], - "release_time_type": row["attributes"]["release_time"], - "release_date": row["attributes"]["release_date"], - "sector_id": row["attributes"]["sector_id"], - } - ) - - return [ - SAUpcomingReleaseDaysData(**row) for row in transformed_data[: query.limit] - ] diff --git a/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/utils/helpers.py b/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/utils/helpers.py new file mode 100644 index 00000000000..656b4d0c4fe --- /dev/null +++ b/openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/utils/helpers.py @@ -0,0 +1,40 @@ +"""Seeking Alpha Utilities.""" + +from datetime import timedelta + +from openbb_core.provider.utils.helpers import amake_request + +HEADERS = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:106.0) Gecko/20100101 Firefox/106.0", + "Accept": "*/*", + "Accept-Language": "de,en-US;q=0.7,en;q=0.3", + "Accept-Encoding": "gzip, deflate, br", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-origin", + "Connection": "keep-alive", +} + + +def date_range(start_date, end_date): + """Yield dates between start_date and end_date.""" + for n in range(int((end_date - start_date).days) + 1): + yield start_date + timedelta(n) + + +async def get_seekingalpha_id(symbol: str) -> str: + """Map a ticker symbol to its Seeking Alpha ID.""" + url = "https://seekingalpha.com/api/v3/searches" + querystring = { + "filter[type]": "symbols", + "filter[list]": "all", + "page[size]": "100", + } + querystring["filter[query]"] = symbol + payload = "" + response = await amake_request( + url, data=payload, headers=HEADERS, params=querystring + ) + ids = response.get("symbols") # type: ignore + + return str(ids[0].get("id", "")) diff --git a/openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_calendar_earnings_fetcher.yaml b/openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_calendar_earnings_fetcher.yaml new file mode 100644 index 00000000000..cc799034988 --- /dev/null +++ b/openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_calendar_earnings_fetcher.yaml @@ -0,0 +1,400 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://seekingalpha.com/api/v3/earnings_calendar/tickers?filter%5Bcurrency%5D=USD&filter%5Bselected_date%5D=MOCK_DATE&filter%5Bwith_rating%5D=false + response: + body: + string: !!binary | + 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 + D5Tg08YdOwAA + headers: + Accept-Ranges: + - bytes + Age: + - '21758' + Allow: + - GET, POST, HEAD, PUT, PATCH, DELETE, OPTIONS + Cache-Control: + - max-age=600, public + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Length: + - '2517' + Content-Security-Policy: + - 'default-src https: wss: data: blob: ''unsafe-inline'' ''unsafe-eval''; report-uri + https://seekingalpha.com/report/csp' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 23 May 2024 16:42:04 GMT + Etag: + - W/"2a09f706fdc5455baebaa05403eea943" + Referrer-Policy: + - strict-origin-when-cross-origin + Set-Cookie: + - machine_cookie=3502582417922; expires=Wed, 23 May 2029 16:42:04 GMT; path=/; + - machine_cookie_ts=1716482524; expires=Wed, 23 May 2029 16:42:04 GMT; path=/; + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Vary: + - User-Agent, Accept-Encoding + X-Cache: + - HIT + X-Cache-Hits: + - '0' + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - jv3Z3eZW3zjx9Eig + X-Runtime: + - '3.360883' + X-Served-By: + - cache-bfi-krnt7300052-BFI + X-Timer: + - S1716482525.972443,VS0,VE1 + X-XSS-Protection: + - '0' + alt-svc: + - h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://seekingalpha.com/api/v3/earnings_calendar/tickers?filter%5Bcurrency%5D=USD&filter%5Bselected_date%5D=MOCK_DATE&filter%5Bwith_rating%5D=false + response: + body: + string: !!binary | + H4sIAAAAAAAAA6tWSkksSVSyio6tBQCUZLJeCwAAAA== + headers: + Accept-Ranges: + - bytes + Age: + - '21736' + Allow: + - GET, POST, HEAD, PUT, PATCH, DELETE, OPTIONS + Cache-Control: + - max-age=600, public + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Length: + - '31' + Content-Security-Policy: + - 'default-src https: wss: data: blob: ''unsafe-inline'' ''unsafe-eval''; report-uri + https://seekingalpha.com/report/csp' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 23 May 2024 16:42:04 GMT + Etag: + - W/"8fe32e407a1038ee38753b70e5374b3a" + Referrer-Policy: + - strict-origin-when-cross-origin + Set-Cookie: + - machine_cookie=3093286235387; expires=Wed, 23 May 2029 16:42:04 GMT; path=/; + - machine_cookie_ts=1716482524; expires=Wed, 23 May 2029 16:42:04 GMT; path=/; + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Vary: + - User-Agent, Accept-Encoding + X-Cache: + - HIT + X-Cache-Hits: + - '0' + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - xndndKwIIPRTNj0D + X-Runtime: + - '3.198017' + X-Served-By: + - cache-bfi-krnt7300039-BFI + X-Timer: + - S1716482525.972185,VS0,VE2 + X-XSS-Protection: + - '0' + alt-svc: + - h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://seekingalpha.com/api/v3/earnings_calendar/tickers?filter%5Bcurrency%5D=USD&filter%5Bselected_date%5D=MOCK_DATE&filter%5Bwith_rating%5D=false + response: + body: + string: !!binary | + H4sIAAAAAAAAA6tWSkksSVSyio6tBQCUZLJeCwAAAA== + headers: + Accept-Ranges: + - bytes + Age: + - '21742' + Allow: + - GET, POST, HEAD, PUT, PATCH, DELETE, OPTIONS + Cache-Control: + - max-age=600, public + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Length: + - '31' + Content-Security-Policy: + - 'default-src https: wss: data: blob: ''unsafe-inline'' ''unsafe-eval''; report-uri + https://seekingalpha.com/report/csp' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 23 May 2024 16:42:04 GMT + Etag: + - W/"8fe32e407a1038ee38753b70e5374b3a" + Referrer-Policy: + - strict-origin-when-cross-origin + Set-Cookie: + - machine_cookie=0300067817068; expires=Wed, 23 May 2029 16:42:04 GMT; path=/; + - machine_cookie_ts=1716482524; expires=Wed, 23 May 2029 16:42:04 GMT; path=/; + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Vary: + - User-Agent, Accept-Encoding + X-Cache: + - HIT + X-Cache-Hits: + - '0' + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - zbTuU4fDBYmMmSFT + X-Runtime: + - '3.296164' + X-Served-By: + - cache-bfi-krnt7300087-BFI + X-Timer: + - S1716482525.972668,VS0,VE1 + X-XSS-Protection: + - '0' + alt-svc: + - h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400 + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://seekingalpha.com/api/v3/earnings_calendar/tickers?filter%5Bcurrency%5D=USD&filter%5Bselected_date%5D=MOCK_DATE&filter%5Bwith_rating%5D=false + response: + body: + string: !!binary | + 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 + zdTsZgW2Vx7+/e2/icJqGdpKAAA= + headers: + Accept-Ranges: + - bytes + Age: + - '21750' + Allow: + - GET, POST, HEAD, PUT, PATCH, DELETE, OPTIONS + Cache-Control: + - max-age=600, public + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Length: + - '3098' + Content-Security-Policy: + - 'default-src https: wss: data: blob: ''unsafe-inline'' ''unsafe-eval''; report-uri + https://seekingalpha.com/report/csp' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 23 May 2024 16:42:04 GMT + Etag: + - W/"c0d703afc9ed2f546a04aaee2925a9bf" + Referrer-Policy: + - strict-origin-when-cross-origin + Set-Cookie: + - machine_cookie=0933180971065; expires=Wed, 23 May 2029 16:42:04 GMT; path=/; + - machine_cookie_ts=1716482524; expires=Wed, 23 May 2029 16:42:04 GMT; path=/; + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Vary: + - User-Agent, Accept-Encoding + X-Cache: + - HIT + X-Cache-Hits: + - '0' + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - zjGiaWaQMoFNHTkQ + X-Runtime: + - '3.387168' + X-Served-By: + - cache-bfi-krnt7300112-BFI + X-Timer: + - S1716482525.972384,VS0,VE1 + X-XSS-Protection: + - '0' + alt-svc: + - h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400 + status: + code: 200 + message: OK +version: 1 diff --git a/openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_forward_eps_estimates.yaml b/openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_forward_eps_estimates.yaml new file mode 100644 index 00000000000..8f408139aab --- /dev/null +++ b/openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_forward_eps_estimates.yaml @@ -0,0 +1,223 @@ +interactions: +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Accept-Language: + - de,en-US;q=0.7,en;q=0.3 + Connection: + - keep-alive + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + method: GET + uri: https://seekingalpha.com/api/v3/searches?filter%5Blist%5D=all&filter%5Bquery%5D=NVDA&filter%5Btype%5D=symbols&page%5Bsize%5D=100 + response: + body: + string: !!binary | + H4sIAAAAAAAAA5WUUW/aMBSF/4qVp02CQOzYSVBVKSUwIXVsErQCTXsIxKWWTMLspCKq+O+zCyVx + k1Tb47124k/nnnteLVnuNxmX1ujXq8USa+Q4eNiz8vJArdHl0OpZcpsJ1fACglFgo55VCK7OB+ev + B/PHKFS30nivv7rZ3OrGzWBzq5rbLM1pmqv+/HEWzUIwzsQhE3HOslT/mRc7dZa+JLF16p0ZPBgE + BHZSOJ7vERs6uAVjXWEoBl1V768Z5cn3+AhqgODHQYOAWbrN9hQscgVGdyWYLKcGXFnBERejT+CI + EslWOrXAhaNxUybV+2+lRttKLEI84vndYhHoD22C2njuDbF0VYn1TcQpy+niORZUAngE91m6M5SL + YsabQvGrUAQSiJRVOrzkIOz7to9Ji1ALA0xXFdhSxIkAjg3xysC5o7EA7UzyyoQdV73XSTR0sA2R + 00JkALUJJa9CLZ4zkf+DUjVHIU/7pQsLugjZrqtm3Ni6B4NLV5VQERP0qL19FkWbXi2Z9hq4KzgH + cAUu0/1o9sJEczvRAk0W4KCFLDLIdPUp2SUvwNsUnU6ypEaGHdRtex/5apSB95FsrVLB2MKq0Qiq + L7Wc+Are0uNdsZ+FyjBJzZgoVYjVNxPBIfS756oiDHpDX99ozDVc9aPJ9GE+XvbPf6nUvECtGqER + ci4PgqktjdgLFZI9MZqAyZ+C5SWYFmkC+mDMYylBCHQM1ZL32E/oU5Fu8/fXTr9PfwHF9HT0GwYA + AA== + headers: + Accept-Ranges: + - bytes + Age: + - '0' + Allow: + - GET, POST, HEAD, PUT, PATCH, DELETE, OPTIONS + Cache-Control: + - max-age=60, public + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Length: + - '571' + Content-Security-Policy: + - 'default-src https: wss: data: blob: ''unsafe-inline'' ''unsafe-eval''; report-uri + https://seekingalpha.com/report/csp' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 23 May 2024 18:10:32 GMT + Etag: + - W/"1a4846bf634ee5326277e66d6bb54460" + Referrer-Policy: + - strict-origin-when-cross-origin + Set-Cookie: + - machine_cookie=1388282386286; expires=Wed, 23 May 2029 18:10:32 GMT; path=/; + - machine_cookie_ts=1716487832; expires=Wed, 23 May 2029 18:10:32 GMT; path=/; + Strict-Transport-Security: + - max-age=63072000; includeSubDomains; preload + Vary: + - User-Agent, Accept-Encoding + X-Cache: + - MISS + X-Cache-Hits: + - '0' + X-Content-Type-Options: + - nosniff + X-Download-Options: + - noopen + X-Frame-Options: + - SAMEORIGIN + X-Permitted-Cross-Domain-Policies: + - none + X-Request-Id: + - Gkj-RwREjk1ZTUrZ + X-Runtime: + - '0.062366' + X-Served-By: + - cache-bfi-krnt7300078-BFI + X-Timer: + - S1716487832.960725,VS0,VE132 + X-XSS-Protection: + - '0' + alt-svc: + - h3=":443";ma=86400,h3-29=":443";ma=86400,h3-27=":443";ma=86400 + status: + code: 200 + message: OK +- request: + body: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, br + Accept-Language: + - de,en-US;q=0.7,en;q=0.3 + Connection: + - keep-alive + Sec-Fetch-Dest: + - empty + Sec-Fetch-Mode: + - cors + Sec-Fetch-Site: + - same-origin + method: GET + uri: https://seekingalpha.com/api/v3/symbol_data/estimates?estimates_data_items=MOCK_ITEMS&period_type=MOCK_PERIOD&relative_periods=MOCK_PERIODS&ticker_ids=MOCK_TICKER_IDS + response: + body: + string: !!binary | + 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 + 9z+Ftwv5io9Je/vl5eVfb7A0d7OJAAA= + headers: + Accept-Ranges: + - bytes + Age: + - '0' + Allow: + - GET, POST, HEAD, PUT, PATCH, DELETE, OPTIONS + Cache-Control: + - max-age=60, public + Connection: +