From 9f1891931cca50ea43587bc38dfeb49006222a65 Mon Sep 17 00:00:00 2001 From: Danglewood <85772166+deeleeramone@users.noreply.github.com> Date: Sun, 18 Feb 2024 09:24:28 -0800 Subject: [PATCH] [Enhancement] Add `analyst_search` to the Equity/Estimates Router (#6088) * standardize price_target * add analyst search * black * more black * ruff * linter, test parmas * last missing param * get rid of convert to upper * ruff * pylint --- .../standard_models/analyst_search.py | 50 +++ .../provider/standard_models/price_target.py | 76 ++-- .../equity/integration/test_equity_api.py | 39 +- .../equity/integration/test_equity_python.py | 36 +- .../estimates/estimates_router.py | 47 ++- .../openbb/package/equity_estimates.py | 315 ++++++++++---- .../openbb/package/module_map.json | 1 + .../benzinga/openbb_benzinga/__init__.py | 2 + .../openbb_benzinga/models/analyst_search.py | 393 ++++++++++++++++++ .../openbb_benzinga/models/price_target.py | 349 +++++++++------- .../test_benzinga_analyst_search_fetcher.yaml | 282 +++++++++++++ .../test_benzinga_price_target_fetcher.yaml | 293 ++++++++----- .../benzinga/tests/test_benzinga_fetchers.py | 10 + .../fmp/openbb_fmp/models/equity_profile.py | 2 + .../fmp/openbb_fmp/models/price_target.py | 42 +- 15 files changed, 1543 insertions(+), 394 deletions(-) create mode 100644 openbb_platform/core/openbb_core/provider/standard_models/analyst_search.py create mode 100644 openbb_platform/providers/benzinga/openbb_benzinga/models/analyst_search.py create mode 100644 openbb_platform/providers/benzinga/tests/record/http/test_benzinga_fetchers/test_benzinga_analyst_search_fetcher.yaml diff --git a/openbb_platform/core/openbb_core/provider/standard_models/analyst_search.py b/openbb_platform/core/openbb_core/provider/standard_models/analyst_search.py new file mode 100644 index 00000000000..c4b13b4067c --- /dev/null +++ b/openbb_platform/core/openbb_core/provider/standard_models/analyst_search.py @@ -0,0 +1,50 @@ +"""Analyst Search Standard Model.""" + +from datetime import ( + datetime, +) +from typing import Optional + +from pydantic import Field + +from openbb_core.provider.abstract.data import Data +from openbb_core.provider.abstract.query_params import QueryParams + + +class AnalystSearchQueryParams(QueryParams): + """Analyst Search Query.""" + + analyst_name: Optional[str] = Field( + default=None, + description="A comma separated list of analyst names to bring back." + + " Omitting will bring back all available analysts.", + ) + firm_name: Optional[str] = Field( + default=None, + description="A comma separated list of firm names to bring back." + + " Omitting will bring back all available firms.", + ) + + +class AnalystSearchData(Data): + """Analyst Search data.""" + + last_updated: Optional[datetime] = Field( + default=None, + description="Date of the last update.", + ) + firm_name: Optional[str] = Field( + default=None, + description="Firm name of the analyst.", + ) + name_first: Optional[str] = Field( + default=None, + description="Analyst first name.", + ) + name_last: Optional[str] = Field( + default=None, + description="Analyst last name.", + ) + name_full: str = Field( + description="Analyst full name.", + ) diff --git a/openbb_platform/core/openbb_core/provider/standard_models/price_target.py b/openbb_platform/core/openbb_core/provider/standard_models/price_target.py index 6c8babd0d3a..5481ec9ba5b 100644 --- a/openbb_platform/core/openbb_core/provider/standard_models/price_target.py +++ b/openbb_platform/core/openbb_core/provider/standard_models/price_target.py @@ -1,7 +1,11 @@ """Price Target Standard Model.""" -from datetime import datetime -from typing import List, Optional, Set, Union +from datetime import ( + date as dateType, + datetime, + time, +) +from typing import Optional, Union from pydantic import Field, NonNegativeInt, field_validator @@ -16,49 +20,67 @@ from openbb_core.provider.utils.descriptions import ( class PriceTargetQueryParams(QueryParams): """Price Target Query.""" - symbol: str = Field(description=QUERY_DESCRIPTIONS.get("symbol", "")) + symbol: Optional[str] = Field( + default=None, description=QUERY_DESCRIPTIONS.get("symbol", "") + ) limit: NonNegativeInt = Field( - default=100, description=QUERY_DESCRIPTIONS.get("limit", "") + default=200, description=QUERY_DESCRIPTIONS.get("limit", "") ) @field_validator("symbol", mode="before", check_fields=False) @classmethod - def upper_symbol(cls, v: str) -> str: + def upper_symbol(cls, v: str): """Convert symbol to uppercase.""" - return v.upper() + return v.upper() if v else None class PriceTargetData(Data): """Price Target Data.""" - symbol: str = Field(description=DATA_DESCRIPTIONS.get("symbol", "")) - published_date: datetime = Field(description="Published date of the price target.") - news_url: Optional[str] = Field( - default=None, description="News URL of the price target." + published_date: Union[dateType, datetime] = Field( + description="Published date of the price target." ) - news_title: Optional[str] = Field( - default=None, description="News title of the price target." + published_time: Optional[time] = Field( + default=None, description="Time of the original rating, UTC." + ) + symbol: str = Field(description=DATA_DESCRIPTIONS.get("symbol", "")) + exchange: Optional[str] = Field( + default=None, description="Exchange where the company is traded." + ) + company_name: Optional[str] = Field( + default=None, description="Name of company that is the subject of rating." ) analyst_name: Optional[str] = Field(default=None, description="Analyst name.") - analyst_company: Optional[str] = Field(default=None, description="Analyst company.") - price_target: Optional[float] = Field(default=None, description="Price target.") + analyst_firm: Optional[str] = Field( + default=None, + description="Name of the analyst firm that published the price target.", + ) + currency: Optional[str] = Field( + default=None, description="Currency the data is denominated in." + ) + price_target: Optional[float] = Field( + default=None, description="The current price target." + ) adj_price_target: Optional[float] = Field( - default=None, description="Adjusted price target." + default=None, + description="Adjusted price target for splits and stock dividends.", + ) + price_target_previous: Optional[float] = Field( + default=None, description="Previous price target." + ) + previous_adj_price_target: Optional[float] = Field( + default=None, description="Previous adjusted price target." ) price_when_posted: Optional[float] = Field( default=None, description="Price when posted." ) - news_publisher: Optional[str] = Field( - default=None, description="News publisher of the price target." + rating_current: Optional[str] = Field( + default=None, description="The analyst's rating for the company." ) - news_base_url: Optional[str] = Field( - default=None, description="News base URL of the price target." + rating_previous: Optional[str] = Field( + default=None, description="Previous analyst rating for the company." + ) + action: Optional[str] = Field( + default=None, + description="Description of the change in rating from firm's last rating.", ) - - @field_validator("symbol", mode="before", check_fields=False) - @classmethod - def upper_symbol(cls, v: Union[str, List[str], Set[str]]): - """Convert symbol to uppercase.""" - if isinstance(v, str): - return v.upper() - return ",".join([symbol.upper() for symbol in list(v)]) diff --git a/openbb_platform/extensions/equity/integration/test_equity_api.py b/openbb_platform/extensions/equity/integration/test_equity_api.py index 4a69d38954a..30dddf91234 100644 --- a/openbb_platform/extensions/equity/integration/test_equity_api.py +++ b/openbb_platform/extensions/equity/integration/test_equity_api.py @@ -627,7 +627,7 @@ def test_equity_ownership_major_holders(params, headers): @parametrize( "params", [ - ({"symbol": "AAPL", "limit": 10, "provider": "fmp"}), + ({"symbol": "AAPL", "limit": 10, "provider": "fmp", "with_grade": True}), ({"symbol": "AAPL", "provider": "finviz"}), ( { @@ -637,13 +637,14 @@ def test_equity_ownership_major_holders(params, headers): # optional provider params "fields": None, "date": None, - "date_from": None, - "date_to": None, + "start_date": None, + "end_date": None, "importance": None, "updated": None, "action": None, - "analyst": None, - "firm": None, + "analyst_ids": None, + "firm_ids": None, + "page": 0, } ), ], @@ -659,6 +660,34 @@ def test_equity_estimates_price_target(params, headers): assert result.status_code == 200 +@parametrize( + "params", + [ + ( + { + "limit": 10, + "provider": "benzinga", + # optional provider params + "fields": None, + "analyst_ids": None, + "firm_ids": None, + "firm_name": "Barclays", + "analyst_name": None, + } + ), + ], +) +@pytest.mark.integration +def test_equity_estimates_analyst_search(params, headers): + 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/analyst_search?{query_str}" + result = requests.get(url, headers=headers, timeout=10) + 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 88bb010785b..d7f4127da35 100644 --- a/openbb_platform/extensions/equity/integration/test_equity_python.py +++ b/openbb_platform/extensions/equity/integration/test_equity_python.py @@ -586,7 +586,7 @@ def test_equity_ownership_major_holders(params, obb): @parametrize( "params", [ - ({"symbol": "AAPL", "limit": 10, "provider": "fmp"}), + ({"symbol": "AAPL", "limit": 10, "provider": "fmp", "with_grade": True}), ( { "symbol": "AAPL", @@ -595,13 +595,14 @@ def test_equity_ownership_major_holders(params, obb): # optional provider params "fields": None, "date": None, - "date_from": None, - "date_to": None, + "start_date": None, + "end_date": None, "importance": None, "updated": None, "action": None, - "analyst": None, - "firm": None, + "analyst_ids": None, + "firm_ids": None, + "page": 0, } ), ({"symbol": "AAPL", "provider": "finviz"}), @@ -615,6 +616,31 @@ def test_equity_estimates_price_target(params, obb): assert result.results is not None +@parametrize( + "params", + [ + ( + { + "limit": 10, + "provider": "benzinga", + # optional provider params + "fields": None, + "analyst_ids": None, + "firm_ids": None, + "firm_name": "Barclays", + "analyst_name": None, + } + ), + ], +) +@pytest.mark.integration +def test_equity_estimates_analyst_search(params, obb): + result = obb.equity.estimates.analyst_search(**params) + assert result + assert isinstance(result, OBBject) + assert result.results is not None + + @parametrize( "params", [ 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 b02585e8e05..b1fff34159f 100644 --- a/openbb_platform/extensions/equity/openbb_equity/estimates/estimates_router.py +++ b/openbb_platform/extensions/equity/openbb_equity/estimates/estimates_router.py @@ -15,34 +15,69 @@ router = Router(prefix="/estimates") # pylint: disable=unused-argument -@router.command(model="PriceTarget") +@router.command( + model="PriceTarget", + exclude_auto_examples=True, + examples=[ + 'obb.equity.estimates.price_target(start_date="2020-01-01", end_date="2024-02-16",limit=10, symbol="msft", provider="benzinga",action="downgrades").to_df()' # noqa: E501 pylint: disable=line-too-long + ], +) async def price_target( cc: CommandContext, provider_choices: ProviderChoices, standard_params: StandardParams, extra_params: ExtraParams, ) -> OBBject: - """Price Target. Price target data.""" + """Analyst price targets by company.""" return await OBBject.from_query(Query(**locals())) -@router.command(model="AnalystEstimates") +@router.command( + model="AnalystEstimates", + exclude_auto_examples=True, + examples=[ + 'obb.equity.estimates.historical("AAPL", period="quarter", provider="fmp").to_df()', + ], +) async def historical( cc: CommandContext, provider_choices: ProviderChoices, standard_params: StandardParams, extra_params: ExtraParams, ) -> OBBject: - """Historical Analyst Estimates. Analyst stock recommendations.""" + """Historical analyst estimates for earnings and revenue.""" return await OBBject.from_query(Query(**locals())) -@router.command(model="PriceTargetConsensus") +@router.command( + model="PriceTargetConsensus", + exclude_auto_examples=True, + examples=[ + 'obb.equity.estimates.consensus("AAPL,MSFT", provider="yfinance").to_df()' + ], +) async def consensus( cc: CommandContext, provider_choices: ProviderChoices, standard_params: StandardParams, extra_params: ExtraParams, ) -> OBBject: - """Price Target Consensus. Price target consensus data.""" + """Consensus price target and recommendation.""" + return await OBBject.from_query(Query(**locals())) + + +@router.command( + model="AnalystSearch", + exclude_auto_examples=True, + examples=[ + 'obb.equity.estimates.analyst_search(firm_name="Wedbush", provider="benzinga").to_df()', + ], +) +async def analyst_search( + cc: CommandContext, + provider_choices: ProviderChoices, + standard_params: StandardParams, + extra_params: ExtraParams, +) -> OBBject: + """Search for specific analysts and get their forecast track record.""" return await OBBject.from_query(Query(**locals())) diff --git a/openbb_platform/openbb/package/equity_estimates.py b/openbb_platform/openbb/package/equity_estimates.py index 4d318e341f9..f27dae4eaa3 100644 --- a/openbb_platform/openbb/package/equity_estimates.py +++ b/openbb_platform/openbb/package/equity_estimates.py @@ -12,6 +12,7 @@ from typing_extensions import Annotated class ROUTER_equity_estimates(Container): """/equity/estimates + analyst_search consensus historical price_target @@ -20,6 +21,176 @@ class ROUTER_equity_estimates(Container): def __repr__(self) -> str: return self.__doc__ or "" + @validate + def analyst_search( + self, + analyst_name: Annotated[ + Optional[str], + OpenBBCustomParameter( + description="A comma separated list of analyst names to bring back. Omitting will bring back all available analysts." + ), + ] = None, + firm_name: Annotated[ + Optional[str], + OpenBBCustomParameter( + description="A comma separated list of firm names to bring back. Omitting will bring back all available firms." + ), + ] = None, + provider: Optional[Literal["benzinga"]] = None, + **kwargs + ) -> OBBject: + """Search for specific analysts and get their forecast track record. + + Parameters + ---------- + analyst_name : Optional[str] + A comma separated list of analyst names to bring back. Omitting will bring back all available analysts. + firm_name : Optional[str] + A comma separated list of firm names to bring back. Omitting will bring back all available firms. + provider : Optional[Literal['benzinga']] + The provider to use for the query, by default None. + If None, the provider specified in defaults is selected or 'benzinga' if there is + no default. + analyst_ids : Optional[Union[str, List[str]]] + A comma separated list of analyst IDs to bring back. (provider: benzinga) + firm_ids : Optional[Union[str, List[str]]] + A comma separated list of firm IDs to bring back. (provider: benzinga) + limit : Optional[int] + Number of results returned. Limit 1000. (provider: benzinga) + page : Optional[int] + Page offset. For optimization, performance and technical reasons, page offsets are limited from 0 - 100000. Limit the query results by other parameters such as date. (provider: benzinga) + fields : Optional[Union[str, List[str]]] + Comma-separated list of fields to include in the response. See https://docs.benzinga.io/benzinga-apis/calendar/get-ratings to learn about the available fields. (provider: benzinga) + + Returns + ------- + OBBject + results : List[AnalystSearch] + Serializable results. + provider : Optional[Literal['benzinga']] + Provider name. + warnings : Optional[List[Warning_]] + List of warnings. + chart : Optional[Chart] + Chart object. + extra: Dict[str, Any] + Extra info. + + AnalystSearch + ------------- + last_updated : Optional[datetime] + Date of the last update. + firm_name : Optional[str] + Firm name of the analyst. + name_first : Optional[str] + Analyst first name. + name_last : Optional[str] + Analyst last name. + name_full : str + Analyst full name. + analyst_id : Optional[str] + ID of the analyst. (provider: benzinga) + firm_id : Optional[str] + ID of the analyst firm. (provider: benzinga) + smart_score : Optional[float] + A weighted average of the total_ratings_percentile, overall_avg_return_percentile, and overall_success_rate (provider: benzinga) + overall_success_rate : Optional[float] + The percentage (normalized) of gain/loss ratings that resulted in a gain overall. (provider: benzinga) + overall_avg_return_percentile : Optional[float] + The percentile (normalized) of this analyst's overall average return per rating in comparison to other analysts' overall average returns per rating. (provider: benzinga) + total_ratings_percentile : Optional[float] + The percentile (normalized) of this analyst's total number of ratings in comparison to the total number of ratings published by all other analysts (provider: benzinga) + total_ratings : Optional[int] + Number of recommendations made by this analyst. (provider: benzinga) + overall_gain_count : Optional[int] + The number of ratings that have gained value since the date of recommendation (provider: benzinga) + overall_loss_count : Optional[int] + The number of ratings that have lost value since the date of recommendation (provider: benzinga) + overall_average_return : Optional[float] + The average percent (normalized) price difference per rating since the date of recommendation (provider: benzinga) + overall_std_dev : Optional[float] + The standard deviation in percent (normalized) price difference in the analyst's ratings since the date of recommendation (provider: benzinga) + gain_count_1m : Optional[int] + The number of ratings that have gained value over the last month (provider: benzinga) + loss_count_1m : Optional[int] + The number of ratings that have lost value over the last month (provider: benzinga) + average_return_1m : Optional[float] + The average percent (normalized) price difference per rating over the last month (provider: benzinga) + std_dev_1m : Optional[float] + The standard deviation in percent (normalized) price difference in the analyst's ratings over the last month (provider: benzinga) + gain_count_3m : Optional[int] + The number of ratings that have gained value over the last 3 months (provider: benzinga) + loss_count_3m : Optional[int] + The number of ratings that have lost value over the last 3 months (provider: benzinga) + average_return_3m : Optional[float] + The average percent (normalized) price difference per rating over the last 3 months (provider: benzinga) + std_dev_3m : Optional[float] + The standard deviation in percent (normalized) price difference in the analyst's ratings over the last 3 months (provider: benzinga) + gain_count_6m : Optional[int] + The number of ratings that have gained value over the last 6 months (provider: benzinga) + loss_count_6m : Optional[int] + The number of ratings that have lost value over the last 6 months (provider: benzinga) + average_return_6m : Optional[float] + The average percent (normalized) price difference per rating over the last 6 months (provider: benzinga) + std_dev_6m : Optional[float] + The standard deviation in percent (normalized) price difference in the analyst's ratings over the last 6 months (provider: benzinga) + gain_count_9m : Optional[int] + The number of ratings that have gained value over the last 9 months (provider: benzinga) + loss_count_9m : Optional[int] + The number of ratings that have lost value over the last 9 months (provider: benzinga) + average_return_9m : Optional[float] + The average percent (normalized) price difference per rating over the last 9 months (provider: benzinga) + std_dev_9m : Optional[float] + The standard deviation in percent (normalized) price difference in the analyst's ratings over the last 9 months (provider: benzinga) + gain_count_1y : Optional[int] + The number of ratings that have gained value over the last 1 year (provider: benzinga) + loss_count_1y : Optional[int] + The number of ratings that have lost value over the last 1 year (provider: benzinga) + average_return_1y : Optional[float] + The average percent (normalized) price difference per rating over the last 1 year (provider: benzinga) + std_dev_1y : Optional[float] + The standard deviation in percent (normalized) price difference in the analyst's ratings over the last 1 year (provider: benzinga) + gain_count_2y : Optional[int] + The number of ratings that have gained value over the last 2 years (provider: benzinga) + loss_count_2y : Optional[int] + The number of ratings that have lost value over the last 2 years (provider: benzinga) + average_return_2y : Optional[float] + The average percent (normalized) price difference per rating over the last 2 years (provider: benzinga) + std_dev_2y : Optional[float] + The standard deviation in percent (normalized) price difference in the analyst's ratings over the last 2 years (provider: benzinga) + gain_count_3y : Optional[int] + The number of ratings that have gained value over the last 3 years (provider: benzinga) + loss_count_3y : Optional[int] + The number of ratings that have lost value over the last 3 years (provider: benzinga) + average_return_3y : Optional[float] + The average percent (normalized) price difference per rating over the last 3 years (provider: benzinga) + std_dev_3y : Optional[float] + The standard deviation in percent (normalized) price difference in the analyst's ratings over the last 3 years (provider: benzinga) + + Example + ------- + >>> from openbb import obb + >>> obb.equity.estimates.analyst_search(firm_name="Wedbush", provider="benzinga").to_df() + """ # noqa: E501 + + return self._run( + "/equity/estimates/analyst_search", + **filter_inputs( + provider_choices={ + "provider": self._get_provider( + provider, + "/equity/estimates/analyst_search", + ("benzinga",), + ) + }, + standard_params={ + "analyst_name": analyst_name, + "firm_name": firm_name, + }, + extra_params=kwargs, + ) + ) + @validate def consensus( self, @@ -32,7 +203,7 @@ class ROUTER_equity_estimates(Container): provider: Optional[Literal["fmp", "yfinance"]] = None, **kwargs ) -> OBBject: - """Price Target Consensus. Price target consensus data. + """Consensus price target and recommendation. Parameters ---------- @@ -83,7 +254,7 @@ class ROUTER_equity_estimates(Container): Example ------- >>> from openbb import obb - >>> obb.equity.estimates.consensus(symbol="AAPL") + >>> obb.equity.estimates.consensus("AAPL,MSFT", provider="yfinance").to_df() """ # noqa: E501 return self._run( @@ -121,7 +292,7 @@ class ROUTER_equity_estimates(Container): provider: Optional[Literal["fmp"]] = None, **kwargs ) -> OBBject: - """Historical Analyst Estimates. Analyst stock recommendations. + """Historical analyst estimates for earnings and revenue. Parameters ---------- @@ -200,7 +371,7 @@ class ROUTER_equity_estimates(Container): Example ------- >>> from openbb import obb - >>> obb.equity.estimates.historical(symbol="AAPL", period="annual", limit=30) + >>> obb.equity.estimates.historical("AAPL", period="quarter", provider="fmp").to_df() """ # noqa: E501 return self._run( @@ -226,23 +397,23 @@ class ROUTER_equity_estimates(Container): def price_target( self, symbol: Annotated[ - Union[str, List[str]], + Union[str, None, List[str]], OpenBBCustomParameter( description="Symbol to get data for. Multiple items allowed: benzinga." ), - ], + ] = None, limit: Annotated[ int, OpenBBCustomParameter(description="The number of data entries to return."), - ] = 100, + ] = 200, provider: Optional[Literal["benzinga", "fmp"]] = None, **kwargs ) -> OBBject: - """Price Target. Price target data. + """Analyst price targets by company. Parameters ---------- - symbol : Union[str, List[str]] + symbol : Union[str, None, List[str]] Symbol to get data for. Multiple items allowed: benzinga. limit : int The number of data entries to return. @@ -250,24 +421,26 @@ class ROUTER_equity_estimates(Container): The provider to use for the query, by default None. If None, the provider specified in defaults is selected or 'benzinga' if there is no default. - fields : Optional[str] - Comma-separated list of fields to include in the response. See https://docs.benzinga.io/benzinga-apis/calendar/get-ratings to learn about the available fields. (provider: benzinga) - date : Optional[str] + page : Optional[int] + Page offset. For optimization, performance and technical reasons, page offsets are limited from 0 - 100000. Limit the query results by other parameters such as date. Used in conjunction with the limit and date parameters. (provider: benzinga) + date : Optional[datetime.date] Date for calendar data, shorthand for date_from and date_to. (provider: benzinga) - date_from : Optional[str] - Date to query from point in time. (provider: benzinga) - date_to : Optional[str] - Date to query to point in time. (provider: benzinga) + start_date : Optional[datetime.date] + Start date of the data, in YYYY-MM-DD format. (provider: benzinga) + end_date : Optional[datetime.date] + End date of the data, in YYYY-MM-DD format. (provider: benzinga) + updated : Optional[Union[datetime.date, int]] + Records last Updated Unix timestamp (UTC). This will force the sort order to be Greater Than or Equal to the timestamp indicated. The date can be a date string or a Unix timestamp. The date string must be in the format of YYYY-MM-DD. (provider: benzinga) importance : Optional[int] - Importance level to filter by. (provider: benzinga) - updated : Optional[int] - Records last updated Unix timestamp (UTC). (provider: benzinga) - action : Optional[Literal['Downgrades', 'Maintains', 'Reinstates', 'Reiterates', 'Upgrades', 'Assumes', 'Initiates Coverage On', 'Terminates Coverage On', 'Removes', 'Suspends', 'Firm Dissolved']] + Importance level to filter by. Uses Greater Than or Equal To the importance indicated (provider: benzinga) + action : Optional[Literal['downgrades', 'maintains', 'reinstates', 'reiterates', 'upgrades', 'assumes', 'initiates', 'terminates', 'removes', 'suspends', 'firm_dissolved']] Filter by a specific action_company. (provider: benzinga) - analyst : Optional[str] - Comma-separated list of analyst (person) IDs. (provider: benzinga) - firm : Optional[str] - Comma-separated list of analyst firm IDs. (provider: benzinga) + analyst_ids : Optional[Union[str, List[str]]] + Comma-separated list of analyst (person) IDs. Omitting will bring back all available analysts. (provider: benzinga) + firm_ids : Optional[Union[str, List[str]]] + Comma-separated list of firm IDs. (provider: benzinga) + fields : Optional[Union[str, List[str]]] + Comma-separated list of fields to include in the response. See https://docs.benzinga.io/benzinga-apis/calendar/get-ratings to learn about the available fields. (provider: benzinga) with_grade : bool Include upgrades and downgrades in the response. (provider: fmp) @@ -287,75 +460,67 @@ class ROUTER_equity_estimates(Container): PriceTarget ----------- + published_date : Union[date, datetime] + Published date of the price target. + published_time : Optional[datetime.time] + Time of the original rating, UTC. symbol : str Symbol representing the entity requested in the data. - published_date : datetime - Published date of the price target. - news_url : Optional[str] - News URL of the price target. - news_title : Optional[str] - News title of the price target. + exchange : Optional[str] + Exchange where the company is traded. + company_name : Optional[str] + Name of company that is the subject of rating. analyst_name : Optional[str] Analyst name. - analyst_company : Optional[str] - Analyst company. + analyst_firm : Optional[str] + Name of the analyst firm that published the price target. + currency : Optional[str] + Currency the data is denominated in. price_target : Optional[float] - Price target. + The current price target. adj_price_target : Optional[float] - Adjusted price target. + Adjusted price target for splits and stock dividends. + price_target_previous : Optional[float] + Previous price target. + previous_adj_price_target : Optional[float] + Previous adjusted price target. price_when_posted : Optional[float] Price when posted. - news_publisher : Optional[str] - News publisher of the price target. - news_base_url : Optional[str] - News base URL of the price target. - action_company : Optional[Literal['Downgrades', 'Maintains', 'Reinstates', 'Reiterates', 'Upgrades', 'Assumes', 'Initiates Coverage On', 'Terminates Coverage On', 'Removes', 'Suspends', 'Firm Dissolved', '']] - Description of the change in rating from firm's last rating.Note that all of these terms are precisely defined. (provider: benzinga) - action_pt : Optional[Literal['Announces', 'Maintains', 'Lowers', 'Raises', 'Removes', 'Adjusts', '']] + rating_current : Optional[str] + The analyst's rating for the company. + rating_previous : Optional[str] + Previous analyst rating for the company. + action : Optional[str] + Description of the change in rating from firm's last rating. + action_change : Optional[Literal['Announces', 'Maintains', 'Lowers', 'Raises', 'Removes', 'Adjusts']] Description of the change in price target from firm's last price target. (provider: benzinga) - adjusted_pt_prior : Optional[str] - Analyst's prior price target, adjusted to account for stock splits and stock dividends. If none are applicable, the pt_prior value is used. (provider: benzinga) - analyst_id : Optional[str] - Id of the analyst. (provider: benzinga) - currency : Optional[str] - Currency the data is denominated in. (provider: benzinga) - exchange : Optional[str] - Exchange of the price target. (provider: benzinga) - id : Optional[str] - Unique ID of this entry. (provider: benzinga) importance : Optional[Literal[0, 1, 2, 3, 4, 5]] Subjective Basis of How Important Event is to Market. 5 = High (provider: benzinga) notes : Optional[str] Notes of the price target. (provider: benzinga) - pt_prior : Optional[str] - Analyst's prior price target. (provider: benzinga) - rating_current : Optional[str] - The analyst's rating for the company. (provider: benzinga) - rating_prior : Optional[str] - Prior analyst rating for the company. (provider: benzinga) - ratings_accuracy : Optional[str] - Ratings accuracy of the price target. (provider: benzinga) - time : Optional[str] - Last updated timestamp, UTC. (provider: benzinga) - updated : Optional[int] - Last updated timestamp, UTC. (provider: benzinga) - url : Optional[str] + analyst_id : Optional[str] + Id of the analyst. (provider: benzinga) + url_news : Optional[str] + URL for analyst ratings news articles for this ticker on Benzinga.com. (provider: benzinga) + url_analyst : Optional[str] URL for analyst ratings page for this ticker on Benzinga.com. (provider: benzinga) - url_calendar : Optional[str] - URL for analyst ratings page for this ticker on Benzinga.com. (provider: benzinga) - name : Optional[str] - Name of company that is subject of rating. (provider: benzinga) - new_grade : Optional[str] - New grade (provider: fmp) - previous_grade : Optional[str] - Previous grade (provider: fmp) - grading_company : Optional[str] - Grading company (provider: fmp) + id : Optional[str] + Unique ID of this entry. (provider: benzinga) + last_updated : Optional[datetime] + Last updated timestamp, UTC. (provider: benzinga) + news_url : Optional[str] + News URL of the price target. (provider: fmp) + news_title : Optional[str] + News title of the price target. (provider: fmp) + news_publisher : Optional[str] + News publisher of the price target. (provider: fmp) + news_base_url : Optional[str] + News base URL of the price target. (provider: fmp) Example ------- >>> from openbb import obb - >>> obb.equity.estimates.price_target(symbol="AAPL", limit=100) + >>> obb.equity.estimates.price_target(start_date="2020-01-01", end_date="2024-02-16",limit=10, symbol="msft", provider="benzinga",action="downgrades").to_df() """ # noqa: E501 return self._run( diff --git a/openbb_platform/openbb/package/module_map.json b/openbb_platform/openbb/package/module_map.json index bae65c52ed9..6b9c08e3199 100644 --- a/openbb_platform/openbb/package/module_map.json +++ b/openbb_platform/openbb/package/module_map.json @@ -49,6 +49,7 @@ "equity_discovery_undervalued_growth": "/equity/discovery/undervalued_growth", "equity_discovery_undervalued_large_caps": "/equity/discovery/undervalued_large_caps", "equity_estimates": "/equity/estimates", + "equity_estimates_analyst_search": "/equity/estimates/analyst_search", "equity_estimates_consensus": "/equity/estimates/consensus", "equity_estimates_historical": "/equity/estimates/historical", "equity_estimates_price_target": "/equity/estimates/price_target", diff --git a/openbb_platform/providers/benzinga/openbb_benzinga/__init__.py b/openbb_platform/providers/benzinga/openbb_benzinga/__init__.py index 28ecf589c0e..32a08aa446f 100644 --- a/openbb_platform/providers/benzinga/openbb_benzinga/__init__.py +++ b/openbb_platform/providers/benzinga/openbb_benzinga/__init__.py @@ -1,5 +1,6 @@ """Benzinga provider module.""" +from openbb_benzinga.models.analyst_search import BenzingaAnalystSearchFetcher from openbb_benzinga.models.company_news import BenzingaCompanyNewsFetcher from openbb_benzinga.models.price_target import BenzingaPriceTargetFetcher from openbb_benzinga.models.world_news import BenzingaWorldNewsFetcher @@ -12,6 +13,7 @@ benzinga_provider = Provider( focused on information that moves the market.""", credentials=["api_key"], fetcher_dict={ + "AnalystSearch": BenzingaAnalystSearchFetcher, "CompanyNews": BenzingaCompanyNewsFetcher, "WorldNews": BenzingaWorldNewsFetcher, "PriceTarget": BenzingaPriceTargetFetcher, diff --git a/openbb_platform/providers/benzinga/openbb_benzinga/models/analyst_search.py b/openbb_platform/providers/benzinga/openbb_benzinga/models/analyst_search.py new file mode 100644 index 00000000000..4917e1ddb44 --- /dev/null +++ b/openbb_platform/providers/benzinga/openbb_benzinga/models/analyst_search.py @@ -0,0 +1,393 @@ +"""Benzinga Analyst Search Model.""" + +# pylint: disable=unused-argument + +from datetime import datetime +from typing import Any, Dict, List, Optional, Union + +from openbb_core.provider.abstract.fetcher import Fetcher +from openbb_core.provider.standard_models.analyst_search import ( + AnalystSearchData, + AnalystSearchQueryParams, +) +from openbb_core.provider.utils.errors import EmptyDataError +from openbb_core.provider.utils.helpers import amake_request, get_querystring +from pydantic import Field, field_validator, model_validator +from pytz import UTC + + +class BenzingaAnalystSearchQueryParams(AnalystSearchQueryParams): + """Benzinga Analyst Search Query. + + Source: https://docs.benzinga.io/benzinga-apis/calendar/get-analysts + """ + + __alias_dict__ = { + "analyst_ids": "id", + "firm_ids": "firm", + "limit": "pageSize", + } + + analyst_ids: Optional[Union[str, List[str]]] = Field( + default=None, + description="A comma separated list of analyst IDs to bring back.", + ) + firm_ids: Optional[Union[str, List[str]]] = Field( + default=None, + description="A comma separated list of firm IDs to bring back.", + ) + limit: Optional[int] = Field( + default=100, + description="Number of results returned. Limit 1000.", + ) + page: Optional[int] = Field( + default=0, + description="Page offset. For optimization," + + " performance and technical reasons, page offsets" + + " are limited from 0 - 100000." + + " Limit the query results by other parameters such as date.", + ) + fields: Optional[Union[List[str], str]] = Field( + default=None, + description="Comma-separated list of fields to include in the response." + " See https://docs.benzinga.io/benzinga-apis/calendar/get-ratings to learn about the available fields.", + ) + + @field_validator( + "fields", "firm_ids", "analyst_ids", mode="before", check_fields=False + ) + @classmethod + def convert_list(cls, v: Union[str, List[str]]): + """Convert a List[str] to a string list.""" + if isinstance(v, str): + return v.upper() + return ",".join([symbol.upper() for symbol in list(v)]) + + @field_validator( + "analyst_ids", + "firm_ids", + "analyst_name", + "firm_name", + mode="before", + check_fields=False, + ) + @classmethod + def validate_list(cls, v: Union[str, List[str], None]): + """Validate list.""" + if isinstance(v, str): + return v + return ",".join(v) if v else None + + +class BenzingaAnalystSearchData(AnalystSearchData): + """Benzinga Analyst Search Data.""" + + __alias_dict__ = { + "analyst_id": "id", + "last_updated": "updated", + } + analyst_id: Optional[str] = Field( + default=None, + description="ID of the analyst.", + ) + firm_id: Optional[str] = Field( + default=None, + description="ID of the analyst firm.", + ) + smart_score: Optional[float] = Field( + default=None, + description="A weighted average of the total_ratings_percentile," + + " overall_avg_return_percentile, and overall_success_rate", + ) + overall_success_rate: Optional[float] = Field( + default=None, + description="The percentage (normalized) of gain/loss ratings that resulted in a gain overall.", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + ) + overall_avg_return_percentile: Optional[float] = Field( + default=None, + description="The percentile (normalized) of this analyst's overall average" + + " return per rating in comparison to other analysts' overall average returns per rating.", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + ) + total_ratings_percentile: Optional[float] = Field( + default=None, + description="The percentile (normalized) of this analyst's total number of ratings" + + " in comparison to the total number of ratings published by all other analysts", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + ) + total_ratings: Optional[int] = Field( + default=None, + description="Number of recommendations made by this analyst.", + ) + overall_gain_count: Optional[int] = Field( + default=None, + description="The number of ratings that have gained value since the date of recommendation", + ) + overall_loss_count: Optional[int] = Field( + default=None, + description="The number of ratings that have lost value since the date of recommendation", + ) + overall_average_return: Optional[float] = Field( + default=None, + description="The average percent (normalized) price difference per rating since the date of recommendation", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + ) + overall_std_dev: Optional[float] = Field( + default=None, + description="The standard deviation in percent (normalized) price difference in the" + + " analyst's ratings since the date of recommendation", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="overall_stdev", + ) + gain_count_1m: Optional[int] = Field( + default=None, + description="The number of ratings that have gained value over the last month", + alias="1m_gain_count", + ) + loss_count_1m: Optional[int] = Field( + default=None, + description="The number of ratings that have lost value over the last month", + alias="1m_loss_count", + ) + average_return_1m: Optional[float] = Field( + default=None, + description="The average percent (normalized) price difference per rating over the last month", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="1m_average_return", + ) + std_dev_1m: Optional[float] = Field( + default=None, + description="The standard deviation in percent (normalized) price difference in the" + + " analyst's ratings over the last month", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="1m_stdev", + ) + gain_count_3m: Optional[int] = Field( + default=None, + description="The number of ratings that have gained value over the last 3 months", + alias="3m_gain_count", + ) + loss_count_3m: Optional[int] = Field( + default=None, + description="The number of ratings that have lost value over the last 3 months", + alias="3m_loss_count", + ) + average_return_3m: Optional[float] = Field( + default=None, + description="The average percent (normalized) price difference per rating over" + + " the last 3 months", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="3m_average_return", + ) + std_dev_3m: Optional[float] = Field( + default=None, + description="The standard deviation in percent (normalized) price difference in the" + + " analyst's ratings over the last 3 months", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="3m_stdev", + ) + gain_count_6m: Optional[int] = Field( + default=None, + description="The number of ratings that have gained value over the last 6 months", + alias="6m_gain_count", + ) + loss_count_6m: Optional[int] = Field( + default=None, + description="The number of ratings that have lost value over the last 6 months", + alias="6m_loss_count", + ) + average_return_6m: Optional[float] = Field( + default=None, + description="The average percent (normalized) price difference per rating over" + + " the last 6 months", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="6m_average_return", + ) + std_dev_6m: Optional[float] = Field( + default=None, + description="The standard deviation in percent (normalized) price difference in the" + + " analyst's ratings over the last 6 months", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="6m_stdev", + ) + gain_count_9m: Optional[int] = Field( + default=None, + description="The number of ratings that have gained value over the last 9 months", + alias="9m_gain_count", + ) + loss_count_9m: Optional[int] = Field( + default=None, + description="The number of ratings that have lost value over the last 9 months", + alias="9m_loss_count", + ) + average_return_9m: Optional[float] = Field( + default=None, + description="The average percent (normalized) price difference per rating over" + + " the last 9 months", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="9m_average_return", + ) + std_dev_9m: Optional[float] = Field( + default=None, + description="The standard deviation in percent (normalized) price difference in the" + + " analyst's ratings over the last 9 months", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="9m_stdev", + ) + gain_count_1y: Optional[int] = Field( + default=None, + description="The number of ratings that have gained value over the last 1 year", + alias="1y_gain_count", + ) + loss_count_1y: Optional[int] = Field( + default=None, + description="The number of ratings that have lost value over the last 1 year", + alias="1y_loss_count", + ) + average_return_1y: Optional[float] = Field( + default=None, + description="The average percent (normalized) price difference per rating over" + + " the last 1 year", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="1y_average_return", + ) + std_dev_1y: Optional[float] = Field( + default=None, + description="The standard deviation in percent (normalized) price difference in the" + + " analyst's ratings over the last 1 year", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="1y_stdev", + ) + gain_count_2y: Optional[int] = Field( + default=None, + description="The number of ratings that have gained value over the last 2 years", + alias="2y_gain_count", + ) + loss_count_2y: Optional[int] = Field( + default=None, + description="The number of ratings that have lost value over the last 2 years", + alias="2y_loss_count", + ) + average_return_2y: Optional[float] = Field( + default=None, + description="The average percent (normalized) price difference per rating over" + + " the last 2 years", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="2y_average_return", + ) + std_dev_2y: Optional[float] = Field( + default=None, + description="The standard deviation in percent (normalized) price difference in the" + + " analyst's ratings over the last 2 years", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="2y_stdev", + ) + gain_count_3y: Optional[int] = Field( + default=None, + description="The number of ratings that have gained value over the last 3 years", + alias="3y_gain_count", + ) + loss_count_3y: Optional[int] = Field( + default=None, + description="The number of ratings that have lost value over the last 3 years", + alias="3y_loss_count", + ) + average_return_3y: Optional[float] = Field( + default=None, + description="The average percent (normalized) price difference per rating over" + + " the last 3 years", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="3y_average_return", + ) + std_dev_3y: Optional[float] = Field( + default=None, + description="The standard deviation in percent (normalized) price difference in the" + + " analyst's ratings over the last 3 years", + json_schema_extra={"unit_measurement": "percent", "frontend_multiply": 100}, + alias="3y_stdev", + ) + + @field_validator("last_updated", mode="before", check_fields=False) + @classmethod + def validate_date(cls, v): + if v: + dt = datetime.fromtimestamp(v, UTC) + return dt.date() if dt.time() == dt.min.time() else dt + return v + + @model_validator(mode="before") + @classmethod + def replace_empty_strings(cls, values): + """Check for empty strings and replace with None.""" + return ( + {k: None if v == "" else v for k, v in values.items()} + if isinstance(values, dict) + else values + ) + + @model_validator(mode="before") + @classmethod + def normalize_percent(cls, values): + """Normalize percent values.""" + contains = ["return", "percentile", "stdev", "rate"] + for key in values: + if any(x in key for x in contains): + values[key] = ( + float(values[key]) / 100 + if values[key] != "" or values[key] is not None + else None + ) + return values + + +class BenzingaAnalystSearchFetcher( + Fetcher[BenzingaAnalystSearchQueryParams, List[BenzingaAnalystSearchData]] +): + """Benzinga Analyst Search Fetcher.""" + + @staticmethod + def transform_query(params: Dict[str, Any]) -> BenzingaAnalystSearchQueryParams: + """Transform query params.""" + return BenzingaAnalystSearchQueryParams(**params) + + @staticmethod + async def aextract_data( + query: BenzingaAnalystSearchQueryParams, + credentials: Optional[Dict[str, str]], + **kwargs: Any, + ) -> List[Dict]: + """Extract the raw data.""" + + token = credentials.get("benzinga_api_key") if credentials else "" + querystring = get_querystring(query.model_dump(), []) + url = f"https://api.benzinga.com/api/v2.1/calendar/ratings/analysts?{querystring}&token={token}" + response = await amake_request(url, **kwargs) + + if not response: + raise EmptyDataError() + + return response.get("analyst_ratings_analyst") # type: ignore + + @staticmethod + def transform_data( + query: BenzingaAnalystSearchQueryParams, + data: List[Dict], + **kwargs: Any, + ) -> List[BenzingaAnalystSearchData]: + """Transform the data.""" + results: List[BenzingaAnalystSearchData] = [] + for item in data: + if item.get("firm_id"): + result = { + "updated": item.get("updated", None), + "firm_id": item.get("firm_id", None), + "firm_name": item.get("firm_name", None), + "id": item.get("id", None), + "name_first": item.get("name_first", None), + "name_full": item.get("name_full", None), + "name_last": item.get("name_last", None), + **item["ratings_accuracy"], + } + results.append(BenzingaAnalystSearchData.model_validate(result)) + return results diff --git a/openbb_platform/providers/benzinga/openbb_benzinga/models/price_target.py b/openbb_platform/providers/benzinga/openbb_benzinga/models/price_target.py index a91ac5b6495..5f12153d5d6 100644 --- a/openbb_platform/providers/benzinga/openbb_benzinga/models/price_target.py +++ b/openbb_platform/providers/benzinga/openbb_benzinga/models/price_target.py @@ -1,17 +1,39 @@ """Benzinga Price Target Model.""" -import math -from copy import deepcopy -from datetime import datetime -from typing import Any, Dict, List, Literal, Optional +# pylint: disable=unused-argument + +from datetime import ( + date as dateType, + datetime, + time, + timezone, +) +from typing import Any, Dict, List, Literal, Optional, Union from openbb_core.provider.abstract.fetcher import Fetcher from openbb_core.provider.standard_models.price_target import ( PriceTargetData, PriceTargetQueryParams, ) +from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS +from openbb_core.provider.utils.errors import EmptyDataError from openbb_core.provider.utils.helpers import amake_requests, get_querystring -from pydantic import Field, PrivateAttr, field_validator, model_validator +from pydantic import Field, field_validator, model_validator +from pytz import UTC + +COVERAGE_DICT = { + "downgrades": "Downgrades", + "maintains": "Maintains", + "reinstates": "Reinstates", + "reiterates": "Reiterates", + "upgrades": "Upgrades", + "assumes": "Assumes", + "initiates": "Initiates Coverage On", + "terminates": "Terminates Coverage On", + "removes": "Removes", + "suspends": "Suspends", + "firm_dissolved": "Firm Dissolved", +} class BenzingaPriceTargetQueryParams(PriceTargetQueryParams): @@ -21,38 +43,131 @@ class BenzingaPriceTargetQueryParams(PriceTargetQueryParams): """ __alias_dict__ = { - "limit": "pageSize", + "limit": "pagesize", "symbol": "parameters[tickers]", } __json_schema_extra__ = {"symbol": ["multiple_items_allowed"]} - fields: Optional[str] = Field( + page: Optional[int] = Field( + default=0, + description="Page offset. For optimization, performance and technical reasons," + + " page offsets are limited from 0 - 100000. Limit the query results by other parameters such as date." + + " Used in conjunction with the limit and date parameters.", + ) + date: Optional[dateType] = Field( + default=None, + description="Date for calendar data, shorthand for date_from and date_to.", + alias="parameters[date]", + ) + start_date: Optional[dateType] = Field( + default=None, + description=QUERY_DESCRIPTIONS.get("start_date", ""), + alias="parameters[date_from]", + ) + end_date: Optional[dateType] = Field( + default=None, + description=QUERY_DESCRIPTIONS.get("end_date", ""), + alias="parameters[date_to]", + ) + updated: Optional[Union[dateType, int]] = Field( + default=None, + description="Records last Updated Unix timestamp (UTC)." + + " This will force the sort order to be Greater Than or Equal to the timestamp indicated." + + " The date can be a date string or a Unix timestamp." + + " The date string must be in the format of YYYY-MM-DD.", + alias="parameters[updated]", + ) + importance: Optional[int] = Field( + default=None, + description="Importance level to filter by." + + " Uses Greater Than or Equal To the importance indicated", + alias="parameters[importance]", + ) + action: Union[ + None, + Literal[ + "downgrades", + "maintains", + "reinstates", + "reiterates", + "upgrades", + "assumes", + "initiates", + "terminates", + "removes", + "suspends", + "firm_dissolved", + ], + ] = Field( + default=None, + description="Filter by a specific action_company.", + alias="parameters[action]", + ) + analyst_ids: Optional[Union[List[str], str]] = Field( + default=None, + description="Comma-separated list of analyst (person) IDs." + + " Omitting will bring back all available analysts.", + alias="parameters[analyst_id]", + ) + firm_ids: Optional[Union[List[str], str]] = Field( + default=None, + description="Comma-separated list of firm IDs.", + alias="parameters[firm_id]", + ) + fields: Optional[Union[List[str], str]] = Field( default=None, description="Comma-separated list of fields to include in the response." " See https://docs.benzinga.io/benzinga-apis/calendar/get-ratings to learn about the available fields.", ) - date: Optional[str] = Field( - default=None, - description="Date for calendar data, shorthand for date_from and date_to.", - is_parameters=True, + + @field_validator("action", mode="after", check_fields=False) + @classmethod + def convert_action(cls, v): + """Convert to the action string.""" + return COVERAGE_DICT[v] if v else None + + @field_validator("updated", mode="after", check_fields=False) + @classmethod + def date_validate(cls, v): + """Convert the the dates to a standard format.""" + if isinstance(v, datetime): + v = v.replace(tzinfo=timezone.utc) + return int(v.timestamp()) + if isinstance(v, dateType): + v = datetime.combine(v, time(), tzinfo=timezone.utc) + return int(v.timestamp()) + return None + + @field_validator( + "fields", "firm_ids", "analyst_ids", mode="before", check_fields=False ) - date_from: Optional[str] = Field( - default=None, - description="Date to query from point in time.", - is_parameters=True, - ) - date_to: Optional[str] = Field( - default=None, description="Date to query to point in time.", is_parameters=True - ) - importance: Optional[int] = Field( - default=None, description="Importance level to filter by.", is_parameters=True - ) - updated: Optional[int] = Field( - default=None, - description="Records last updated Unix timestamp (UTC).", - is_parameters=True, - ) - action: Optional[ + @classmethod + def convert_list(cls, v: Union[str, List[str]]): + """Convert a List[str] to a string list.""" + if isinstance(v, str): + return v + return ",".join(v) if v else None + + +class BenzingaPriceTargetData(PriceTargetData): + """Benzinga Price Target Data.""" + + __alias_dict__ = { + "symbol": "ticker", + "published_date": "date", + "adj_price_target": "adjusted_pt_current", + "price_target": "pt_current", + "price_target_previous": "pt_prior", + "previous_adj_price_target": "adjusted_pt_prior", + "published_time": "time", + "analyst_firm": "analyst", + "company_name": "name", + "rating_previous": "rating_prior", + "url_analyst": "url", + } + + action: Union[ + None, Literal[ "Downgrades", "Maintains", @@ -65,147 +180,62 @@ class BenzingaPriceTargetQueryParams(PriceTargetQueryParams): "Removes", "Suspends", "Firm Dissolved", - ] + ], ] = Field( default=None, - description="Filter by a specific action_company.", - is_parameters=True, - ) - analyst: Optional[str] = Field( - default=None, description="Comma-separated list of analyst (person) IDs." - ) - firm: Optional[str] = Field( - default=None, description="Comma-separated list of analyst firm IDs." - ) - _parameters: Dict[str, Any] = PrivateAttr(default_factory=dict) - - @model_validator(mode="after") - @classmethod - def assemble_parameters( - cls, values: "BenzingaPriceTargetQueryParams" - ) -> "BenzingaPriceTargetQueryParams": - """Assemble the parameters private attribute.""" - model_fields = values.model_fields - - for field in deepcopy(values): - key, value = field - field_info = model_fields[key] - if field_info.json_schema_extra and field_info.json_schema_extra.get( - "is_parameters", False - ): - if value: - # add to parameters - # pylint: disable=protected-access - values._parameters[f"parameters[{key}]"] = value - # remove from values - delattr(values, key) - - return values - - -class BenzingaPriceTargetData(PriceTargetData): - """Benzinga Price Target Data.""" - - __alias_dict__ = { - "symbol": "ticker", - "published_date": "date", - "news_url": "url_news", - "adj_price_target": "adjusted_pt_current", - "price_target": "pt_current", - } - - action_company: Literal[ - "Downgrades", - "Maintains", - "Reinstates", - "Reiterates", - "Upgrades", - "Assumes", - "Initiates Coverage On", - "Terminates Coverage On", - "Removes", - "Suspends", - "Firm Dissolved", - "", - ] = Field( - default="", description="Description of the change in rating from firm's last rating." "Note that all of these terms are precisely defined.", + alias="action_company", ) - action_pt: Literal[ - "Announces", "Maintains", "Lowers", "Raises", "Removes", "Adjusts", "" + action_change: Union[ + None, + Literal["Announces", "Maintains", "Lowers", "Raises", "Removes", "Adjusts"], ] = Field( - default="", - description="Description of the change in price target from firm's last price target.", - ) - adjusted_pt_prior: Optional[str] = Field( default=None, - description="Analyst's prior price target, adjusted to account for stock splits and stock dividends." - " If none are applicable, the pt_prior value is used.", + description="Description of the change in price target from firm's last price target.", + alias="action_pt", ) - analyst_id: Optional[str] = Field(default=None, description="Id of the analyst.") - currency: Optional[str] = Field( - default=None, description="Currency the data is denominated in." - ) - exchange: Optional[str] = Field( - default=None, description="Exchange of the price target." - ) - id: Optional[str] = Field(default=None, description="Unique ID of this entry.") - importance: Optional[Literal[0, 1, 2, 3, 4, 5]] = Field( + importance: Union[None, Literal[0, 1, 2, 3, 4, 5]] = Field( default=None, description="Subjective Basis of How Important Event is to Market. 5 = High", ) notes: Optional[str] = Field(default=None, description="Notes of the price target.") - pt_prior: Optional[str] = Field( - default=None, description="Analyst's prior price target." + analyst_id: Optional[str] = Field(default=None, description="Id of the analyst.") + url_news: Optional[str] = Field( + default=None, + description="URL for analyst ratings news articles for this ticker on Benzinga.com.", ) - rating_current: Optional[str] = Field( - default=None, description="The analyst's rating for the company." - ) - rating_prior: Optional[str] = Field( - default=None, description="Prior analyst rating for the company." - ) - ratings_accuracy: Optional[str] = Field( - default=None, description="Ratings accuracy of the price target." - ) - time: Optional[str] = Field( - default=None, description="Last updated timestamp, UTC." - ) - updated: Optional[int] = Field( - default=None, description="Last updated timestamp, UTC." - ) - url: str = Field( + url_analyst: Optional[str] = Field( default=None, description="URL for analyst ratings page for this ticker on Benzinga.com.", ) - url_calendar: Optional[str] = Field( + id: Optional[str] = Field(default=None, description="Unique ID of this entry.") + last_updated: Optional[datetime] = Field( default=None, - description="URL for analyst ratings page for this ticker on Benzinga.com.", - ) - name: Optional[str] = Field( - default=None, description="Name of company that is subject of rating." + description="Last updated timestamp, UTC.", + alias="updated", ) @field_validator("published_date", mode="before", check_fields=False) @classmethod - def parse_date(cls, v: str) -> datetime: - """Parse the date field.""" - if v: - v = datetime.strptime(v, "%Y-%m-%d") + def parse_date(cls, v: str): + """Parse the publisihed_date.""" + return datetime.strptime(v, "%Y-%m-%d").date() if v else None + @field_validator("last_updated", mode="before", check_fields=False) + @classmethod + def validate_date(cls, v): + """Convert the Unix timestamp to a datetime object.""" + if v: + dt = datetime.fromtimestamp(v, UTC) + return dt.date() if dt.time() == dt.min.time() else dt return v - @field_validator("adj_price_target", mode="before", check_fields=False) + @model_validator(mode="before") @classmethod - def parse_adj_price_target(cls, v: str) -> float: - """Parse the adj_price_target field.""" - return float(v) if v else 0.0 - - @field_validator("price_target", mode="before", check_fields=False) - @classmethod - def parse_price_target(cls, v: str) -> float: - """Parse the price_target field.""" - return float(v) if v else 0.0 + def replace_empty_strings(cls, values): + """Check for empty strings and replace with None.""" + return {k: None if v == "" else v for k, v in values.items()} class BenzingaPriceTargetFetcher( @@ -226,34 +256,31 @@ class BenzingaPriceTargetFetcher( query: BenzingaPriceTargetQueryParams, credentials: Optional[Dict[str, str]], **kwargs: Any, - ) -> Dict: + ) -> List[Dict]: """Return the raw data from the Benzinga endpoint.""" token = credentials.get("benzinga_api_key") if credentials else "" base_url = "https://api.benzinga.com/api/v2.1/calendar/ratings" - full_query = { - **query.model_dump(by_alias=True), - **query._parameters, # pylint: disable=protected-access - } - querystring = get_querystring(full_query, exclude=[]) + querystring = get_querystring(query.model_dump(by_alias=True), []) - pages = math.ceil(query.limit / 100) if query.limit else 1 + url = f"{base_url}?{querystring}&token={token}" + data = await amake_requests(url, **kwargs) - urls = [ - f"{base_url}?{querystring}&page={page}&token={token}" - for page in range(pages) - ] + if not data: + raise EmptyDataError() - data = await amake_requests(urls, **kwargs) + return data[0].get("ratings") - return data[: query.limit] - - # pylint: disable=unused-argument @staticmethod def transform_data( query: BenzingaPriceTargetQueryParams, - data: Dict, + data: List[Dict], **kwargs: Any, ) -> List[BenzingaPriceTargetData]: """Return the transformed data.""" - return [BenzingaPriceTargetData.model_validate(d) for d in data[0]["ratings"]] + results: List[BenzingaPriceTargetData] = [] + # Remove duplicated field with a URL + for item in data: + item.pop("url_calendar", None) + results.append(BenzingaPriceTargetData.model_validate(item)) + return results diff --git a/openbb_platform/providers/benzinga/tests/record/http/test_benzinga_fetchers/test_benzinga_analyst_search_fetcher.yaml b/openbb_platform/providers/benzinga/tests/record/http/test_benzinga_fetchers/test_benzinga_analyst_search_fetcher.yaml new file mode 100644 index 00000000000..ef67ec5cd0f --- /dev/null +++ b/openbb_platform/providers/benzinga/tests/record/http/test_benzinga_fetchers/test_benzinga_analyst_search_fetcher.yaml @@ -0,0 +1,282 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://api.benzinga.com/api/v2.1/calendar/ratings/analysts?firm_name=Barclays&page=0&pageSize=100&token=MOCK_TOKEN + response: + body: + string: !!binary | + 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 + headers: + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json; charset=utf-8 + Date: + - Fri, 16 Feb 2024 20:32:34 GMT + Transfer-Encoding: + - chunked + Vary: + - Accept-Encoding + Via: + - kong/2.4.1, 1.1 9adef5b1c5fc9ca80d6f4f8d19e103a2.cloudfront.net (CloudFront) + X-Amz-Cf-Id: + - wzPB7RswTYmBQtCmdpuOoCBrs9caz6DluhsLZXglRNPo13Tc9phzyA== + X-Amz-Cf-Pop: + - SEA19-C3 + X-Bz-Request-Id: + - bBEB7mxrS7rediVwPHqUVe + X-Cache: + - Miss from cloudfront + X-Kong-Proxy-Latency: + - '0' + X-Kong-Upstream-Latency: + - '35' + X-Powered-By: + - Athlon 64 X2 + status: + code: 200 + message: OK +version: 1 diff --git a/openbb_platform/providers/benzinga/tests/record/http/test_benzinga_fetchers/test_benzinga_price_target_fetcher.yaml b/openbb_platform/providers/benzinga/tests/record/http/test_benzinga_fetchers/test_benzinga_price_target_fetcher.yaml index 8c9933734eb..6272d94ad83 100644 --- a/openbb_platform/providers/benzinga/tests/record/http/test_benzinga_fetchers/test_benzinga_price_target_fetcher.yaml +++ b/openbb_platform/providers/benzinga/tests/record/http/test_benzinga_fetchers/test_benzinga_price_target_fetcher.yaml @@ -9,108 +9,191 @@ interactions: Connection: - keep-alive method: GET - uri: https://api.benzinga.com/api/v2.1/calendar/ratings?page=0&pageSize=100¶meters%5Btickers%5D=AAPL&token=MOCK_TOKEN + uri: https://api.benzinga.com/api/v2.1/calendar/ratings?page=0&pagesize=200¶meters%5Btickers%5D=AAPL&token=MOCK_TOKEN response: body: string: !!binary | - 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 + AwAA//8WFqvx7vUBAA== headers: Connection: - keep-alive @@ -119,25 +202,25 @@ interactions: Content-Type: - application/json; charset=utf-8 Date: - - Fri, 12 Jan 2024 10:16:49 GMT + - Fri, 16 Feb 2024 19:52:20 GMT Transfer-Encoding: - chunked Vary: - Accept-Encoding Via: - - kong/2.4.1, 1.1 56d84f665e9029878cb3adcd83a21026.cloudfront.net (CloudFront) + - kong/2.4.1, 1.1 0906d4887f6625f4a4467d8d4fd268d2.cloudfront.net (CloudFront) X-Amz-Cf-Id: - - u7IyZpkTSODX4-a_LDLxUQJI4E_R06BB8oFSA40IJHC7YxXml3QJuw== + - FHckqBx4BKNJXwvRb3EbbAcqCMXmvOJY4btI-H_zsgzrYwfmLk2VEQ== X-Amz-Cf-Pop: - - LIS50-P1 + - SEA19-C3 X-Bz-Request-Id: - - DSIshnOBTn5TzNsr70peza + - w44XRYJcNVDLtWZCdKXWlD X-Cache: - Miss from cloudfront X-Kong-Proxy-Latency: - '0' X-Kong-Upstream-Latency: - - '16' + - '43' X-Powered-By: - Athlon 64 X2 status: diff --git a/openbb_platform/providers/benzinga/tests/test_benzinga_fetchers.py b/openbb_platform/providers/benzinga/tests/test_benzinga_fetchers.py index 562280ad262..d82677ef469 100644 --- a/openbb_platform/providers/benzinga/tests/test_benzinga_fetchers.py +++ b/openbb_platform/providers/benzinga/tests/test_benzinga_fetchers.py @@ -1,4 +1,5 @@ import pytest +from openbb_benzinga.models.analyst_search import BenzingaAnalystSearchFetcher from openbb_benzinga.models.company_news import BenzingaCompanyNewsFetcher from openbb_benzinga.models.price_target import BenzingaPriceTargetFetcher from openbb_benzinga.models.world_news import BenzingaWorldNewsFetcher @@ -44,3 +45,12 @@ def test_benzinga_price_target_fetcher(credentials=test_credentials): fetcher = BenzingaPriceTargetFetcher() result = fetcher.test(params, credentials) assert result is None + + +@pytest.mark.record_http +def test_benzinga_analyst_search_fetcher(credentials=test_credentials): + params = {"firm_name": "Barclays"} + + fetcher = BenzingaAnalystSearchFetcher() + result = fetcher.test(params, credentials) + assert result is None diff --git a/openbb_platform/providers/fmp/openbb_fmp/models/equity_profile.py b/openbb_platform/providers/fmp/openbb_fmp/models/equity_profile.py index 773fd84fb4d..bb430816b08 100644 --- a/openbb_platform/providers/fmp/openbb_fmp/models/equity_profile.py +++ b/openbb_platform/providers/fmp/openbb_fmp/models/equity_profile.py @@ -23,6 +23,8 @@ class FMPEquityProfileQueryParams(EquityInfoQueryParams): Source: https://site.financialmodelingprep.com/developer/docs/companies-key-stats-free-api/ """ + __json_schema_extra__ = {"symbol": ["multiple_items_allowed"]} + class FMPEquityProfileData(EquityInfoData): """FMP Equity Profile Data.""" diff --git a/openbb_platform/providers/fmp/openbb_fmp/models/price_target.py b/openbb_platform/providers/fmp/openbb_fmp/models/price_target.py index e88775efe90..ad53c335c0a 100644 --- a/openbb_platform/providers/fmp/openbb_fmp/models/price_target.py +++ b/openbb_platform/providers/fmp/openbb_fmp/models/price_target.py @@ -1,5 +1,7 @@ """FMP Equity Ownership Model.""" +# pylint: disable=unused-argument + from datetime import datetime from typing import Any, Dict, List, Optional @@ -27,15 +29,31 @@ class FMPPriceTargetQueryParams(PriceTargetQueryParams): class FMPPriceTargetData(PriceTargetData): """FMP Price Target Data.""" - __alias_dict__ = {"news_url": "newsURL", "news_base_url": "newsBaseURL"} + __alias_dict__ = { + "analyst_firm": "gradingCompany", + "rating_current": "newGrade", + "rating_previous": "previousGrade", + "news_title": "newsTitle", + "url_news": "newsURL", + "url_base": "newsBaseURL", + } - new_grade: Optional[str] = Field(description="New grade", default=None) - previous_grade: Optional[str] = Field(description="Previous grade", default=None) - grading_company: Optional[str] = Field(description="Grading company", default=None) + news_url: Optional[str] = Field( + default=None, description="News URL of the price target." + ) + news_title: Optional[str] = Field( + default=None, description="News title of the price target." + ) + news_publisher: Optional[str] = Field( + default=None, description="News publisher of the price target." + ) + news_base_url: Optional[str] = Field( + default=None, description="News base URL of the price target." + ) @field_validator("published_date", mode="before", check_fields=False) - def fiscal_date_ending_validate(cls, v: str): # pylint: disable=E0213 - """Return the fiscal date ending as a datetime object.""" + def validate_date(cls, v: str): # pylint: disable=E0213 + """Validate the published date.""" v = v.replace("\n", "") return datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.%fZ") # type: ignore @@ -53,7 +71,6 @@ class FMPPriceTargetFetcher( """Transform the query params.""" return FMPPriceTargetQueryParams(**params) - # pylint: disable=unused-argument @staticmethod async def aextract_data( query: FMPPriceTargetQueryParams, @@ -71,11 +88,16 @@ class FMPPriceTargetFetcher( return await get_data_urls(urls) - # pylint: disable=unused-argument @staticmethod def transform_data( query: FMPPriceTargetQueryParams, data: List[Dict], **kwargs: Any ) -> List[FMPPriceTargetData]: """Return the transformed data.""" - data = data[: query.limit] - return [FMPPriceTargetData.model_validate(d) for d in data] + results: List[FMPPriceTargetData] = [] + for item in data: + new_item = { + k if k != "analystCompany" else "analyst_firm": v + for k, v in item.items() + } + results.append(FMPPriceTargetData.model_validate(new_item)) + return results