[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
This commit is contained in:
Danglewood
2024-02-18 09:24:28 -08:00
committed by GitHub
parent 60a93c3266
commit 9f1891931c
15 changed files with 1543 additions and 394 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,108 +9,191 @@ interactions:
Connection:
- keep-alive
method: GET
uri: https://api.benzinga.com/api/v2.1/calendar/ratings?page=0&pageSize=100&parameters%5Btickers%5D=AAPL&token=MOCK_TOKEN
uri: https://api.benzinga.com/api/v2.1/calendar/ratings?page=0&pagesize=200&parameters%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:

View File

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

View File

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

View File

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