diff options
author | Danglewood <85772166+deeleeramone@users.noreply.github.com> | 2024-05-25 13:35:54 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-05-25 13:35:54 -0700 |
commit | 7c7665ff6af622664cfdcb99b9ad81687f774264 (patch) | |
tree | e6451a46b7227a8f84571df2b59e91ec02446cdb | |
parent | dd2fbfa610473416e1368ffb32280f8bdec44f36 (diff) | |
parent | 5ff4d8907ce2fd0c2bd59fd1c6a0daae4656c43b (diff) |
Merge branch 'develop' into bugfix/api-settings
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 + """ + < |