[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>
This commit is contained in:
Danglewood
2024-05-14 03:49:29 -07:00
committed by GitHub
parent 17a7e7d263
commit 0eee602098
10 changed files with 586 additions and 0 deletions

View File

@@ -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.",
)

View File

@@ -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

View File

@@ -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

View File

@@ -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()))

View File

@@ -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,

View File

@@ -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 @@ class ROUTER_equity_estimates(Container):
)
)
@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(

View File

@@ -21,6 +21,9 @@ from openbb_intrinio.models.financial_ratios import IntrinioFinancialRatiosFetch
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 @@ historical financial market data to businesses and developers through an API."""
"EtfSearch": IntrinioEtfSearchFetcher,
"FinancialRatios": IntrinioFinancialRatiosFetcher,
"ForwardEpsEstimates": IntrinioForwardEpsEstimatesFetcher,
"ForwardPeEstimates": IntrinioForwardPeEstimatesFetcher,
"ForwardSalesEstimates": IntrinioForwardSalesEstimatesFetcher,
"FredSeries": IntrinioFredSeriesFetcher,
"HistoricalAttributes": IntrinioHistoricalAttributesFetcher,

View File

@@ -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]

View File

@@ -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

View File

@@ -25,6 +25,9 @@ from openbb_intrinio.models.financial_ratios import IntrinioFinancialRatiosFetch
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