From 0eee602098d9b783ec0df03c464ee6bc1da4d9bf Mon Sep 17 00:00:00 2001 From: Danglewood <85772166+deeleeramone@users.noreply.github.com> Date: Tue, 14 May 2024 03:49:29 -0700 Subject: [PATCH] [Feature] Add Forward PE Estimates (#6398) * forward_pe * ruff * merge branch develop * mypy * typo --------- Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com> Co-authored-by: montezdesousa <79287829+montezdesousa@users.noreply.github.com> --- .../standard_models/forward_pe_estimates.py | 54 +++++++ .../equity/integration/test_equity_api.py | 29 ++++ .../equity/integration/test_equity_python.py | 28 ++++ .../estimates/estimates_router.py | 22 +++ openbb_platform/openbb/assets/reference.json | 133 +++++++++++++++++ .../openbb/package/equity_estimates.py | 92 ++++++++++++ .../intrinio/openbb_intrinio/__init__.py | 4 + .../models/forward_pe_estimates.py | 141 ++++++++++++++++++ .../test_intrinio_forward_pe_fetcher.yaml | 70 +++++++++ .../intrinio/tests/test_intrinio_fetchers.py | 13 ++ 10 files changed, 586 insertions(+) create mode 100644 openbb_platform/core/openbb_core/provider/standard_models/forward_pe_estimates.py create mode 100644 openbb_platform/providers/intrinio/openbb_intrinio/models/forward_pe_estimates.py create mode 100644 openbb_platform/providers/intrinio/tests/record/http/test_intrinio_fetchers/test_intrinio_forward_pe_fetcher.yaml diff --git a/openbb_platform/core/openbb_core/provider/standard_models/forward_pe_estimates.py b/openbb_platform/core/openbb_core/provider/standard_models/forward_pe_estimates.py new file mode 100644 index 000000000000..8888ce2ce2b6 --- /dev/null +++ b/openbb_platform/core/openbb_core/provider/standard_models/forward_pe_estimates.py @@ -0,0 +1,54 @@ +"""Forward PE Estimates Standard Model.""" + +from typing import Optional + +from pydantic import Field, field_validator + +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, + QUERY_DESCRIPTIONS, +) + + +class ForwardPeEstimatesQueryParams(QueryParams): + """Forward PE Estimates Query Parameters.""" + + symbol: Optional[str] = Field( + default=None, + description=QUERY_DESCRIPTIONS["symbol"], + ) + + @field_validator("symbol", mode="before", check_fields=False) + @classmethod + def to_upper(cls, v): + """Convert field to uppercase.""" + return v.upper() if v else None + + +class ForwardPeEstimatesData(Data): + """Forward PE Estimates Data.""" + + symbol: str = Field(description=DATA_DESCRIPTIONS.get("symbol", "")) + name: Optional[str] = Field(default=None, description="Name of the entity.") + year1: Optional[float] = Field( + default=None, + description="Estimated PE ratio for the next fiscal year.", + ) + year2: Optional[float] = Field( + default=None, + description="Estimated PE ratio two fiscal years from now.", + ) + year3: Optional[float] = Field( + default=None, + description="Estimated PE ratio three fiscal years from now.", + ) + year4: Optional[float] = Field( + default=None, + description="Estimated PE ratio four fiscal years from now.", + ) + year5: Optional[float] = Field( + default=None, + description="Estimated PE ratio five fiscal years from now.", + ) diff --git a/openbb_platform/extensions/equity/integration/test_equity_api.py b/openbb_platform/extensions/equity/integration/test_equity_api.py index 8159874b4374..ad38630c0693 100644 --- a/openbb_platform/extensions/equity/integration/test_equity_api.py +++ b/openbb_platform/extensions/equity/integration/test_equity_api.py @@ -1953,3 +1953,32 @@ def test_equity_ownership_form_13f(params, headers): result = requests.get(url, headers=headers, timeout=10) assert isinstance(result, requests.Response) assert result.status_code == 200 + + +@parametrize( + "params", + [ + ( + { + "symbol": "NVDA,MSFT", + "provider": "intrinio", + } + ), + ( + { + "symbol": None, + "provider": "intrinio", + } + ), + ], +) +@pytest.mark.integration +def test_equity_estimates_forward_pe(params, headers): + """Test the equity estimates forward_pe 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/estimates/forward_pe?{query_str}" + result = requests.get(url, headers=headers, timeout=10) + assert isinstance(result, requests.Response) + assert result.status_code == 200 diff --git a/openbb_platform/extensions/equity/integration/test_equity_python.py b/openbb_platform/extensions/equity/integration/test_equity_python.py index 67ce1c92fca8..f13df3df5585 100644 --- a/openbb_platform/extensions/equity/integration/test_equity_python.py +++ b/openbb_platform/extensions/equity/integration/test_equity_python.py @@ -1819,3 +1819,31 @@ def test_equity_ownership_form_13f(params, obb): assert result assert isinstance(result, OBBject) assert len(result.results) > 0 + + +@parametrize( + "params", + [ + ( + { + "symbol": "NVDA,MSFT", + "provider": "intrinio", + } + ), + ( + { + "symbol": None, + "provider": "intrinio", + } + ), + ], +) +@pytest.mark.integration +def test_equity_estimates_forward_pe(params, obb): + """Test the equity estimates forward_pe endpoint.""" + params = {p: v for p, v in params.items() if v} + + result = obb.equity.estimates.forward_pe(**params) + assert result + assert isinstance(result, OBBject) + assert len(result.results) > 0 diff --git a/openbb_platform/extensions/equity/openbb_equity/estimates/estimates_router.py b/openbb_platform/extensions/equity/openbb_equity/estimates/estimates_router.py index d321c3a9d316..36aae0761b81 100644 --- a/openbb_platform/extensions/equity/openbb_equity/estimates/estimates_router.py +++ b/openbb_platform/extensions/equity/openbb_equity/estimates/estimates_router.py @@ -137,3 +137,25 @@ async def forward_eps( ) -> OBBject: """Get forward EPS estimates.""" return await OBBject.from_query(Query(**locals())) + + +@router.command( + model="ForwardPeEstimates", + examples=[ + APIEx(parameters={"provider": "intrinio"}), + APIEx( + parameters={ + "symbol": "AAPL,MSFT,GOOG", + "provider": "intrinio", + } + ), + ], +) +async def forward_pe( + cc: CommandContext, + provider_choices: ProviderChoices, + standard_params: StandardParams, + extra_params: ExtraParams, +) -> OBBject: + """Get forward PE estimates.""" + return await OBBject.from_query(Query(**locals())) diff --git a/openbb_platform/openbb/assets/reference.json b/openbb_platform/openbb/assets/reference.json index febb1baad07d..91095c1bd2ab 100644 --- a/openbb_platform/openbb/assets/reference.json +++ b/openbb_platform/openbb/assets/reference.json @@ -6750,6 +6750,139 @@ }, "model": "ForwardEpsEstimates" }, + "/equity/estimates/forward_pe": { + "deprecated": { + "flag": null, + "message": null + }, + "description": "Get forward PE estimates.", + "examples": "\nExamples\n--------\n\n```python\nfrom openbb import obb\nobb.equity.estimates.forward_pe(provider='intrinio')\nobb.equity.estimates.forward_pe(symbol='AAPL,MSFT,GOOG', provider='intrinio')\n```\n\n", + "parameters": { + "standard": [ + { + "name": "symbol", + "type": "Union[str, List[str]]", + "description": "Symbol to get data for. Multiple items allowed for provider(s): intrinio.", + "default": null, + "optional": true + }, + { + "name": "provider", + "type": "Literal['intrinio']", + "description": "The provider to use for the query, by default None. If None, the provider specified in defaults is selected or 'intrinio' if there is no default.", + "default": "intrinio", + "optional": true + } + ], + "intrinio": [] + }, + "returns": { + "OBBject": [ + { + "name": "results", + "type": "List[ForwardPeEstimates]", + "description": "Serializable results." + }, + { + "name": "provider", + "type": "Optional[Literal['intrinio']]", + "description": "Provider name." + }, + { + "name": "warnings", + "type": "Optional[List[Warning_]]", + "description": "List of warnings." + }, + { + "name": "chart", + "type": "Optional[Chart]", + "description": "Chart object." + }, + { + "name": "extra", + "type": "Dict[str, Any]", + "description": "Extra info." + } + ] + }, + "data": { + "standard": [ + { + "name": "symbol", + "type": "str", + "description": "Symbol representing the entity requested in the data.", + "default": "", + "optional": false + }, + { + "name": "name", + "type": "str", + "description": "Name of the entity.", + "default": null, + "optional": true + }, + { + "name": "year1", + "type": "float", + "description": "Estimated PE ratio for the next fiscal year.", + "default": null, + "optional": true + }, + { + "name": "year2", + "type": "float", + "description": "Estimated PE ratio two fiscal years from now.", + "default": null, + "optional": true + }, + { + "name": "year3", + "type": "float", + "description": "Estimated PE ratio three fiscal years from now.", + "default": null, + "optional": true + }, + { + "name": "year4", + "type": "float", + "description": "Estimated PE ratio four fiscal years from now.", + "default": null, + "optional": true + }, + { + "name": "year5", + "type": "float", + "description": "Estimated PE ratio five fiscal years from now.", + "default": null, + "optional": true + } + ], + "intrinio": [ + { + "name": "peg_ratio_year1", + "type": "float", + "description": "Estimated Forward PEG ratio for the next fiscal year.", + "default": null, + "optional": true + }, + { + "name": "eps_ttm", + "type": "float", + "description": "The latest trailing twelve months earnings per share.", + "default": null, + "optional": true + }, + { + "name": "last_udpated", + "type": "date", + "description": "The date the data was last updated.", + "default": null, + "optional": true + } + ] + }, + "model": "ForwardPeEstimates" + }, "/equity/discovery/gainers": { "deprecated": { "flag": null, diff --git a/openbb_platform/openbb/package/equity_estimates.py b/openbb_platform/openbb/package/equity_estimates.py index 01b95add7a5d..efa40206a14c 100644 --- a/openbb_platform/openbb/package/equity_estimates.py +++ b/openbb_platform/openbb/package/equity_estimates.py @@ -15,6 +15,7 @@ class ROUTER_equity_estimates(Container): analyst_search consensus forward_eps + forward_pe forward_sales historical price_target @@ -468,6 +469,97 @@ def forward_eps( ) ) + @exception_handler + @validate + def forward_pe( + self, + symbol: Annotated[ + Union[str, None, List[Optional[str]]], + OpenBBField( + description="Symbol to get data for. Multiple comma separated items allowed for provider(s): intrinio." + ), + ] = None, + provider: Annotated[ + Optional[Literal["intrinio"]], + OpenBBField( + description="The provider to use for the query, by default None.\n If None, the provider specified in defaults is selected or 'intrinio' if there is\n no default." + ), + ] = None, + **kwargs + ) -> OBBject: + """Get forward PE estimates. + + Parameters + ---------- + symbol : Union[str, None, List[Optional[str]]] + Symbol to get data for. Multiple comma separated items allowed for provider(s): intrinio. + provider : Optional[Literal['intrinio']] + The provider to use for the query, by default None. + If None, the provider specified in defaults is selected or 'intrinio' if there is + no default. + + Returns + ------- + OBBject + results : List[ForwardPeEstimates] + Serializable results. + provider : Optional[Literal['intrinio']] + Provider name. + warnings : Optional[List[Warning_]] + List of warnings. + chart : Optional[Chart] + Chart object. + extra : Dict[str, Any] + Extra info. + + ForwardPeEstimates + ------------------ + symbol : str + Symbol representing the entity requested in the data. + name : Optional[str] + Name of the entity. + year1 : Optional[float] + Estimated PE ratio for the next fiscal year. + year2 : Optional[float] + Estimated PE ratio two fiscal years from now. + year3 : Optional[float] + Estimated PE ratio three fiscal years from now. + year4 : Optional[float] + Estimated PE ratio four fiscal years from now. + year5 : Optional[float] + Estimated PE ratio five fiscal years from now. + peg_ratio_year1 : Optional[float] + Estimated Forward PEG ratio for the next fiscal year. (provider: intrinio) + eps_ttm : Optional[float] + The latest trailing twelve months earnings per share. (provider: intrinio) + last_udpated : Optional[date] + The date the data was last updated. (provider: intrinio) + + Examples + -------- + >>> from openbb import obb + >>> obb.equity.estimates.forward_pe(provider='intrinio') + >>> obb.equity.estimates.forward_pe(symbol='AAPL,MSFT,GOOG', provider='intrinio') + """ # noqa: E501 + + return self._run( + "/equity/estimates/forward_pe", + **filter_inputs( + provider_choices={ + "provider": self._get_provider( + provider, + "/equity/estimates/forward_pe", + ("intrinio",), + ) + }, + standard_params={ + "symbol": symbol, + }, + extra_params=kwargs, + info={"symbol": {"intrinio": {"multiple_items_allowed": True}}}, + ) + ) + @exception_handler @validate def forward_sales( diff --git a/openbb_platform/providers/intrinio/openbb_intrinio/__init__.py b/openbb_platform/providers/intrinio/openbb_intrinio/__init__.py index ae473c72e5c3..370069557f00 100644 --- a/openbb_platform/providers/intrinio/openbb_intrinio/__init__.py +++ b/openbb_platform/providers/intrinio/openbb_intrinio/__init__.py @@ -21,6 +21,9 @@ from openbb_intrinio.models.forward_eps_estimates import ( IntrinioForwardEpsEstimatesFetcher, ) +from openbb_intrinio.models.forward_pe_estimates import ( + IntrinioForwardPeEstimatesFetcher, +) from openbb_intrinio.models.forward_sales_estimates import ( IntrinioForwardSalesEstimatesFetcher, ) @@ -77,6 +80,7 @@ "EtfSearch": IntrinioEtfSearchFetcher, "FinancialRatios": IntrinioFinancialRatiosFetcher, "ForwardEpsEstimates": IntrinioForwardEpsEstimatesFetcher, + "ForwardPeEstimates": IntrinioForwardPeEstimatesFetcher, "ForwardSalesEstimates": IntrinioForwardSalesEstimatesFetcher, "FredSeries": IntrinioFredSeriesFetcher, "HistoricalAttributes": IntrinioHistoricalAttributesFetcher, diff --git a/openbb_platform/providers/intrinio/openbb_intrinio/models/forward_pe_estimates.py b/openbb_platform/providers/intrinio/openbb_intrinio/models/forward_pe_estimates.py new file mode 100644 index 000000000000..552db46a76ff --- /dev/null +++ b/openbb_platform/providers/intrinio/openbb_intrinio/models/forward_pe_estimates.py @@ -0,0 +1,141 @@ +"""Intrinio Forward PE Estimates Model.""" + +# pylint: disable=unused-argument + +import asyncio +from datetime import date as dateType +from typing import Any, Dict, List, Optional +from warnings import warn + +from openbb_core.provider.abstract.fetcher import Fetcher +from openbb_core.provider.standard_models.forward_pe_estimates import ( + ForwardPeEstimatesData, + ForwardPeEstimatesQueryParams, +) +from openbb_core.provider.utils.errors import EmptyDataError +from openbb_core.provider.utils.helpers import amake_request +from openbb_intrinio.utils.helpers import response_callback +from pydantic import Field + + +class IntrinioForwardPeEstimatesQueryParams(ForwardPeEstimatesQueryParams): + """Intrinio Forward PE Estimates Query. + + https://api-v2.intrinio.com/zacks/forward_pe? + """ + + __json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}} + + +class IntrinioForwardPeEstimatesData(ForwardPeEstimatesData): + """Intrinio Forward PE Estimates Data.""" + + __alias_dict__ = { + "symbol": "ticker", + "name": "company_name", + "year1": "forward_pe_year1", + "year2": "forward_pe_year2", + "year3": "forward_pe_year3", + "year4": "forward_pe_year4", + "year5": "forward_pe_year5", + "peg_ratio_year1": "forward_peg_ratio_year1", + "eps_ttm": "latest_ttm_eps", + "last_udpated": "updated_date", + } + + peg_ratio_year1: Optional[float] = Field( + default=None, + description="Estimated Forward PEG ratio for the next fiscal year.", + ) + eps_ttm: Optional[float] = Field( + default=None, + description="The latest trailing twelve months earnings per share.", + ) + last_udpated: Optional[dateType] = Field( + default=None, + description="The date the data was last updated.", + ) + + +class IntrinioForwardPeEstimatesFetcher( + Fetcher[IntrinioForwardPeEstimatesQueryParams, List[IntrinioForwardPeEstimatesData]] +): + """Intrinio Forward PE Estimates Fetcher.""" + + @staticmethod + def transform_query( + params: Dict[str, Any] + ) -> IntrinioForwardPeEstimatesQueryParams: + """Transform the query params.""" + return IntrinioForwardPeEstimatesQueryParams(**params) + + @staticmethod + async def aextract_data( + query: IntrinioForwardPeEstimatesQueryParams, + 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/forward_pe" + symbols = query.symbol.split(",") if query.symbol else None + results: List[Dict] = [] + + async def get_one(symbol): + """Get the data for one symbol.""" + url = f"{BASE_URL}/{symbol}?api_key={api_key}" + try: + data = await amake_request( + url, response_callback=response_callback, **kwargs + ) + except Exception as e: + warn(f"Symbol Error: {symbol} --> {e}") + else: + if data: + results.append(data) # type: ignore + + if symbols: + await asyncio.gather(*[get_one(symbol) for symbol in symbols]) + if not results: + raise EmptyDataError( + f"There were no results found for any of the given symbols. -> {symbols}" + ) + return results + + async def fetch_callback(response, session): + """Use callback for pagination.""" + data = await response.json() + error = data.get("error", None) + if error: + message = data.get("message", None) + raise RuntimeError(f"Error: {error} -> {message}") + forward_pe = data.get("forward_pe") + if forward_pe and len(forward_pe) > 0: # type: ignore + results.extend(forward_pe) # type: ignore + return results + + url = f"{BASE_URL}?page_size=10000&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: IntrinioForwardPeEstimatesQueryParams, + data: List[Dict], + **kwargs: Any, + ) -> List[IntrinioForwardPeEstimatesData]: + """Transform the raw data into the standard format.""" + symbols = query.symbol.split(",") if query.symbol else [] + if symbols: + data.sort( + key=lambda item: ( + symbols.index(item.get("ticker")) # type: ignore + if item.get("ticker") in symbols + else len(symbols) + ) + ) + return [IntrinioForwardPeEstimatesData.model_validate(d) for d in data] diff --git a/openbb_platform/providers/intrinio/tests/record/http/test_intrinio_fetchers/test_intrinio_forward_pe_fetcher.yaml b/openbb_platform/providers/intrinio/tests/record/http/test_intrinio_fetchers/test_intrinio_forward_pe_fetcher.yaml new file mode 100644 index 000000000000..dd99f2c9269a --- /dev/null +++ b/openbb_platform/providers/intrinio/tests/record/http/test_intrinio_fetchers/test_intrinio_forward_pe_fetcher.yaml @@ -0,0 +1,70 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://api-v2.intrinio.com/zacks/forward_pe/MSFT?api_key=MOCK_API_KEY + response: + body: + string: !!binary | + H4sIAGSBQWYAA2TOMQuDMBAF4L9Sbq5HcpoWswpCh2JR9xA0LdKqIaYUKf3vjUOHkuWG93GP9wY/ + dHfjQMK5KVvYQzePVk+rmvRotvRU1FVTle2uqOpL8KfttTe92m5wYpQlTCScBbvO7qVdr6xRq9GO + g0wF0iECCsAxExGkIOmILMqzkBPGRQIkz5H9Fd2U036YfwsIeb6HR1i7eOX9qIxdwhdHIT5fAAAA + //8DADhyNhP+AAAA + headers: + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Mon, 13 May 2024 02:56:36 GMT + Transfer-Encoding: + - chunked + Vary: + - Origin,Accept-Encoding + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://api-v2.intrinio.com/zacks/forward_pe/AAPL?api_key=MOCK_API_KEY + response: + body: + string: !!binary | + H4sIAGSBQWYAA2zOywrCMBQE0F+Ru25DkiY+siviQiiSP7iEJkqxj5CmSBH/3dSFCHUzizkMzBNi + U99dAAVlqSvIoB46b/oZe9O5pdW6Om3Ol2OiyVsTncUlE3HKRU5lzmiy6xAeJlj0DmdnAgPFd2Qv + V8ATSCLYCooE/N9CfOAgViBB9VPb/vY3DCY2w/cC4Rm06e0YMcYOnR9BbYkoXm8AAAD//wMALdic + IvgAAAA= + headers: + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Mon, 13 May 2024 02:56:36 GMT + Transfer-Encoding: + - chunked + Vary: + - Origin,Accept-Encoding + status: + code: 200 + message: OK +version: 1 diff --git a/openbb_platform/providers/intrinio/tests/test_intrinio_fetchers.py b/openbb_platform/providers/intrinio/tests/test_intrinio_fetchers.py index b319d94ca37d..8c2217c92c73 100644 --- a/openbb_platform/providers/intrinio/tests/test_intrinio_fetchers.py +++ b/openbb_platform/providers/intrinio/tests/test_intrinio_fetchers.py @@ -25,6 +25,9 @@ from openbb_intrinio.models.forward_eps_estimates import ( IntrinioForwardEpsEstimatesFetcher, ) +from openbb_intrinio.models.forward_pe_estimates import ( + IntrinioForwardPeEstimatesFetcher, +) from openbb_intrinio.models.forward_sales_estimates import ( IntrinioForwardSalesEstimatesFetcher, ) @@ -505,3 +508,13 @@ def test_intrinio_price_target_consensus_fetcher(credentials=test_credentials): fetcher = IntrinioPriceTargetConsensusFetcher() result = fetcher.test(params, credentials) assert result is None + + +@pytest.mark.record_http +def test_intrinio_forward_pe_fetcher(credentials=test_credentials): + """Test forward pe fetcher.""" + params = {"symbol": "AAPL,MSFT"} + + fetcher = IntrinioForwardPeEstimatesFetcher() + result = fetcher.test(params, credentials) + assert result is None