summaryrefslogtreecommitdiffstats
path: root/openbb_platform/providers/intrinio/openbb_intrinio/models/forward_eps_estimates.py
diff options
context:
space:
mode:
Diffstat (limited to 'openbb_platform/providers/intrinio/openbb_intrinio/models/forward_eps_estimates.py')
-rw-r--r--openbb_platform/providers/intrinio/openbb_intrinio/models/forward_eps_estimates.py230
1 files changed, 230 insertions, 0 deletions
diff --git a/openbb_platform/providers/intrinio/openbb_intrinio/models/forward_eps_estimates.py b/openbb_platform/providers/intrinio/openbb_intrinio/models/forward_eps_estimates.py
new file mode 100644
index 00000000000..9166f490f79
--- /dev/null
+++ b/openbb_platform/providers/intrinio/openbb_intrinio/models/forward_eps_estimates.py
@@ -0,0 +1,230 @@
+"""Intrinio Forward EPS Estimates Model."""
+
+# pylint: disable=unused-argument
+
+import asyncio
+from datetime import datetime
+from typing import Any, Dict, List, Literal, Optional, Union
+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.errors import EmptyDataError
+from openbb_core.provider.utils.helpers import (
+ amake_request,
+ get_querystring,
+)
+from openbb_intrinio.utils.helpers import response_callback
+from pydantic import Field, field_validator, model_validator
+
+
+class IntrinioForwardEpsEstimatesQueryParams(ForwardEpsEstimatesQueryParams):
+ """Intrinio Forward EPS Estimates Query.
+
+ https://docs.intrinio.com/documentation/web_api/get_zacks_sales_estimates_v2
+ """
+
+ __json_schema_extra__ = {"symbol": ["multiple_items_allowed"]}
+
+ fiscal_year: Optional[int] = Field(
+ default=None,
+ description="The future fiscal year to retrieve estimates for."
+ + " When no symbol and year is supplied the current calendar year is used.",
+ )
+ fiscal_period: Union[Literal["fy", "q1", "q2", "q3", "q4"], None] = Field(
+ default=None,
+ description="The future fiscal period to retrieve estimates for.",
+ )
+ calendar_year: Optional[int] = Field(
+ default=None,
+ description="The future calendar year to retrieve estimates for."
+ + " When no symbol and year is supplied the current calendar year is used.",
+ )
+ calendar_period: Union[Literal["q1", "q2", "q3", "q4"], None] = Field(
+ default=None,
+ description="The future calendar period to retrieve estimates for.",
+ )
+
+ @model_validator(mode="after")
+ @classmethod
+ def validate_choices(cls, values):
+ """Validate the model and set a safe default state."""
+ if values.symbol is None and (
+ values.calendar_year is None and values.fiscal_year is None
+ ):
+ values.calendar_year = datetime.now().year
+ return values
+
+
+class IntrinioForwardEpsEstimatesData(ForwardEpsEstimatesData):
+ """Intrinio Forward EPS Estimates Data."""
+
+ __alias_dict__ = {
+ "high_estimate": "high",
+ "low_estimate": "low",
+ "number_of_analysts": "count",
+ "mean": "estimated_sales_mean",
+ "revisions_change_percent": "percent_change",
+ "mean_1w": "mean_7_days_ago",
+ "mean_1m": "mean_30_days_ago",
+ "mean_2m": "mean_60_days_ago",
+ "mean_3m": "mean_90_days_ago",
+ }
+
+ revisions_change_percent: Optional[float] = Field(
+ default=None,
+ description="The earnings per share (EPS) percent change in estimate for the period.",
+ json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
+ )
+ mean_1w: Optional[float] = Field(
+ default=None,
+ description="The mean estimate for the period one week ago.",
+ )
+ mean_1m: Optional[float] = Field(
+ default=None,
+ description="The mean estimate for the period one month ago.",
+ )
+ mean_2m: Optional[float] = Field(
+ default=None,
+ description="The mean estimate for the period two months ago.",
+ )
+ mean_3m: Optional[float] = Field(
+ default=None,
+ description="The mean estimate for the period three months ago.",
+ )
+
+ @field_validator(
+ "revisions_change_percent",
+ mode="before",
+ check_fields=False,
+ )
+ @classmethod
+ def normalize_percent(
+ cls, v: Optional[Union[int, float]]
+ ) -> Optional[Union[int, float]]:
+ """Normalize percent values."""
+ return v / 100 if v else None
+
+
+class IntrinioForwardEpsEstimatesFetcher(
+ Fetcher[
+ IntrinioForwardEpsEstimatesQueryParams, List[IntrinioForwardEpsEstimatesData]
+ ]
+):
+ """Intrinio Forward EPS Estimates Fetcher."""
+
+ @staticmethod
+ def transform_query(
+ params: Dict[str, Any]
+ ) -> IntrinioForwardEpsEstimatesQueryParams:
+ """Transform the query params."""
+ return IntrinioForwardEpsEstimatesQueryParams(**params)
+
+ @staticmethod
+ async def aextract_data(
+ query: IntrinioForwardEpsEstimatesQueryParams,
+ credentials: Optional[Dict[str, str]],
+ **kwargs: Any,
+ ) -> List[Dict]:
+ """Return the raw data from the Intrinio endpoint."""
+ api_key = credentials.get("intrinio_api_key") if credentials else ""
+
+ BASE_URL = "https://api-v2.intrinio.com/zacks/eps_estimates?page_size=10000"
+
+ symbols = query.symbol.split(",") if query.symbol else None
+
+ query_str = get_querystring(
+ query.model_dump(by_alias=True),
+ ["symbol", "calendar_period", "fiscal_period", "limit"],
+ )
+
+ results: List[Dict] = []
+
+ async def get_one(symbol):
+ """Get the data for one symbol."""
+ url = f"{BASE_URL}&identifier={symbol}&{query_str}&api_key={api_key}"
+ new_data: List[Dict] = []
+ data = await amake_request(
+ url, response_callback=response_callback, **kwargs
+ )
+ if not data or not isinstance(data, dict) or not data.get("estimates"):
+ warn(f"Symbol Error: No data found for {symbol}")
+ if isinstance(data, dict) and data.get("estimates"):
+ new_data = data.get("estimates") # type: ignore
+ if new_data:
+ results.extend(new_data)
+
+ if symbols:
+ await asyncio.gather(*[get_one(symbol) for symbol in symbols])
+ return results
+
+ async def fetch_callback(response, session):
+ """Callback for pagination."""
+ data = await response.json()
+ messages = data.get("messages", None)
+ if messages:
+ raise RuntimeError(str(messages))
+ if data.get("estimates") and len(data.get("estimates")) > 0: # type: ignore
+ results.extend(data.get("estimates")) # type: ignore
+ while data.get("next_page"): # type: ignore
+ next_page = data["next_page"] # type: ignore
+ next_url = f"{url}&next_page={next_page}"
+ data = await amake_request(next_url, session=session, **kwargs)
+ if (
+ "estimates" in data
+ and len(data.get("estimates")) > 0 # type: ignore
+ ):
+ results.extend(data.get("estimates")) # type: ignore
+ return results
+
+ url = f"{BASE_URL}&{query_str}&api_key={api_key}"
+
+ results = await amake_request(url, response_callback=fetch_callback, **kwargs) # type: ignore
+
+ if not results:
+ raise EmptyDataError("The request was successful but was returned empty.")
+
+ return results
+
+ @staticmethod
+ def transform_data(
+ query: IntrinioForwardEpsEstimatesQueryParams,
+ data: List[Dict],
+ **kwargs: Any,
+ ) -> List[IntrinioForwardEpsEstimatesData]:
+ """Transform the raw data into the standard format."""
+
+ symbols = query.symbol.split(",") if query.symbol else []
+ results: List[IntrinioForwardEpsEstimatesData] = []
+ for item in sorted(
+ data,
+ key=lambda item: ( # type: ignore
+ (
+ symbols.index(item.get("symbol")) if item.get("symbol") in symbols else len(symbols), # type: ignore
+ item.get("date"),
+ )
+ if symbols
+ else item.get("date")
+ ),
+ ):
+ temp: Dict[str, Any] = {}
+ company = item.pop("company")
+ if company.get("ticker") is None:
+ continue
+ temp["symbol"] = company.get("ticker")
+ temp["name"] = company.get("name")
+ if query.fiscal_period and query.fiscal_period.upper() != item.get(
+ "fiscal_period"
+ ):
+ continue
+ if query.calendar_period and query.calendar_period.upper() != item.get(
+ "calendar_period"
+ ):
+ continue
+ temp.update(item)
+ results.append(IntrinioForwardEpsEstimatesData.model_validate(temp))
+
+ return results