summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanglewood <85772166+deeleeramone@users.noreply.github.com>2024-05-25 13:36:08 -0700
committerGitHub <noreply@github.com>2024-05-25 13:36:08 -0700
commitf666ad401d63ee92aef88c4b4d2c612bcc217dae (patch)
tree75919ea96d0f1d01ffc01a0af22bb089e1c8bc80
parentce239aa30046a257bb28c80a0ac7c9250835e821 (diff)
parent5ff4d8907ce2fd0c2bd59fd1c6a0daae4656c43b (diff)
Merge branch 'develop' into bugfix/intrinio-balance-q4bugfix/intrinio-balance-q4
-rw-r--r--openbb_platform/core/openbb_core/provider/standard_models/upcoming_release_days.py23
-rw-r--r--openbb_platform/extensions/equity/integration/test_equity_api.py42
-rw-r--r--openbb_platform/extensions/equity/integration/test_equity_python.py39
-rw-r--r--openbb_platform/extensions/equity/openbb_equity/discovery/discovery_router.py14
-rw-r--r--openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/__init__.py12
-rw-r--r--openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/calendar_earnings.py144
-rw-r--r--openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/forward_eps_estimates.py207
-rw-r--r--openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/forward_sales_estimates.py179
-rw-r--r--openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/models/upcoming_release_days.py108
-rw-r--r--openbb_platform/providers/seeking_alpha/openbb_seeking_alpha/utils/helpers.py40
-rw-r--r--openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_calendar_earnings_fetcher.yaml400
-rw-r--r--openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_forward_eps_estimates.yaml223
-rw-r--r--openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_forward_sales_estimates.yaml208
-rw-r--r--openbb_platform/providers/seeking_alpha/tests/record/http/test_seeking_alpha_fetchers/test_sa_upcoming_release_days_fetcher.yaml249
-rw-r--r--openbb_platform/providers/seeking_alpha/tests/test_seeking_alpha_fetchers.py40
15 files changed, 1490 insertions, 438 deletions
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
@@ -1654,24 +1676,6 @@ def test_equity_discovery_top_retail(params, headers):
@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
@@ -1572,21 +1594,6 @@ def test_equity_discovery_top_retail(params, obb):
@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
@@ -153,20 +153,6 @@ async def top_retail(
@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=[
APIEx(parameters={"provider": "fmp"}),
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."""
+