From 46f4834cb222a4cd425a172f4e6620ff822f54a7 Mon Sep 17 00:00:00 2001 From: Danglewood <85772166+deeleeramone@users.noreply.github.com> Date: Fri, 15 Mar 2024 05:24:55 -0700 Subject: [PATCH] [Feature] Intrinio ETF Price Performance (#6220) * remap to etf router * add the intrinio files this time * update standard model * the rest of the changes * static files * black * pylint * test thing * black * fix test? * try another way? * ruff * maybe? * new cassette? --- .../standard_models/recent_performance.py | 100 ++++++-- .../etf/integration/test_etf_api.py | 8 + .../etf/integration/test_etf_python.py | 8 + .../extensions/etf/openbb_etf/etf_router.py | 4 +- openbb_platform/openbb/assets/reference.json | 224 ++++++++++++++--- openbb_platform/openbb/package/etf.py | 72 ++++-- .../finviz/openbb_finviz/__init__.py | 1 + .../openbb_finviz/models/price_performance.py | 13 +- .../providers/fmp/openbb_fmp/__init__.py | 1 + .../intrinio/openbb_intrinio/__init__.py | 4 + .../models/etf_price_performance.py | 227 ++++++++++++++++++ .../openbb_intrinio/utils/references.py | 33 +++ ...ntrinio_etf_price_performance_fetcher.yaml | 82 +++++++ .../intrinio/tests/test_intrinio_fetchers.py | 12 + 14 files changed, 724 insertions(+), 65 deletions(-) create mode 100644 openbb_platform/providers/intrinio/openbb_intrinio/models/etf_price_performance.py create mode 100644 openbb_platform/providers/intrinio/tests/record/http/test_intrinio_fetchers/test_intrinio_etf_price_performance_fetcher.yaml diff --git a/openbb_platform/core/openbb_core/provider/standard_models/recent_performance.py b/openbb_platform/core/openbb_core/provider/standard_models/recent_performance.py index 37314e80c50..3955e4eacd7 100644 --- a/openbb_platform/core/openbb_core/provider/standard_models/recent_performance.py +++ b/openbb_platform/core/openbb_core/provider/standard_models/recent_performance.py @@ -6,7 +6,10 @@ from pydantic import Field, field_validator from openbb_core.provider.abstract.data import Data from openbb_core.provider.abstract.query_params import QueryParams -from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS +from openbb_core.provider.utils.descriptions import ( + DATA_DESCRIPTIONS, + QUERY_DESCRIPTIONS, +) class RecentPerformanceQueryParams(QueryParams): @@ -22,23 +25,88 @@ class RecentPerformanceQueryParams(QueryParams): class RecentPerformanceData(Data): - """Recent Performance Data.""" + """Recent Performance Data. All returns are normalized percents.""" - one_day: Optional[float] = Field(description="One-day return.", default=None) - wtd: Optional[float] = Field(description="Week to date return.", default=None) - one_week: Optional[float] = Field(description="One-week return.", default=None) - mtd: Optional[float] = Field(description="Month to date return.", default=None) - one_month: Optional[float] = Field(description="One-month return.", default=None) - qtd: Optional[float] = Field(description="Quarter to date return.", default=None) + symbol: Optional[str] = Field( + default=None, description=DATA_DESCRIPTIONS.get("symbol", "") + ) + one_day: Optional[float] = Field( + default=None, + description="One-day return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + wtd: Optional[float] = Field( + default=None, + description="Week to date return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + one_week: Optional[float] = Field( + default=None, + description="One-week return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + mtd: Optional[float] = Field( + default=None, + description="Month to date return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + one_month: Optional[float] = Field( + default=None, + description="One-month return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + qtd: Optional[float] = Field( + default=None, + description="Quarter to date return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) three_month: Optional[float] = Field( - description="Three-month return.", default=None + default=None, + description="Three-month return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + six_month: Optional[float] = Field( + default=None, + description="Six-month return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + ytd: Optional[float] = Field( + default=None, + description="Year to date return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + one_year: Optional[float] = Field( + default=None, + description="One-year return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + two_year: Optional[float] = Field( + default=None, + description="Two-year return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + three_year: Optional[float] = Field( + default=None, + description="Three-year return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + four_year: Optional[float] = Field( + default=None, + description="Four-year", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + five_year: Optional[float] = Field( + default=None, + description="Five-year return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + ten_year: Optional[float] = Field( + default=None, + description="Ten-year return.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, ) - six_month: Optional[float] = Field(description="Six-month return.", default=None) - ytd: Optional[float] = Field(description="Year to date return.", default=None) - one_year: Optional[float] = Field(description="One-year return.", default=None) - three_year: Optional[float] = Field(description="Three-year return.", default=None) - five_year: Optional[float] = Field(description="Five-year return.", default=None) - ten_year: Optional[float] = Field(description="Ten-year return.", default=None) max: Optional[float] = Field( - description="Return from the beginning of the time series.", default=None + default=None, + description="Return from the beginning of the time series.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, ) diff --git a/openbb_platform/extensions/etf/integration/test_etf_api.py b/openbb_platform/extensions/etf/integration/test_etf_api.py index b53b4bfb2b1..d0b03385a36 100644 --- a/openbb_platform/extensions/etf/integration/test_etf_api.py +++ b/openbb_platform/extensions/etf/integration/test_etf_api.py @@ -351,6 +351,14 @@ def test_etf_holdings(params, headers): [ ({"symbol": "SPY,VOO,QQQ,IWM,IWN,GOVT,JNK", "provider": "fmp"}), ({"symbol": "SPY,VOO,QQQ,IWM,IWN,GOVT,JNK", "provider": "finviz"}), + ( + { + "symbol": "SPY,VOO,QQQ,IWM,IWN,GOVT,JNK", + "return_type": "trailing", + "adjustment": "splits_and_dividends", + "provider": "intrinio", + } + ), ], ) @pytest.mark.integration diff --git a/openbb_platform/extensions/etf/integration/test_etf_python.py b/openbb_platform/extensions/etf/integration/test_etf_python.py index 6f76921edb5..417c4eae3a9 100644 --- a/openbb_platform/extensions/etf/integration/test_etf_python.py +++ b/openbb_platform/extensions/etf/integration/test_etf_python.py @@ -342,6 +342,14 @@ def test_etf_holdings(params, obb): [ ({"symbol": "SPY,VOO,QQQ,IWM,IWN,GOVT,JNK", "provider": "fmp"}), ({"symbol": "SPY,VOO,QQQ,IWM,IWN,GOVT,JNK", "provider": "finviz"}), + ( + { + "symbol": "SPY,VOO,QQQ,IWM,IWN,GOVT,JNK", + "return_type": "trailing", + "adjustment": "splits_and_dividends", + "provider": "intrinio", + } + ), ], ) @pytest.mark.integration diff --git a/openbb_platform/extensions/etf/openbb_etf/etf_router.py b/openbb_platform/extensions/etf/openbb_etf/etf_router.py index caa8c90938b..3476644510d 100644 --- a/openbb_platform/extensions/etf/openbb_etf/etf_router.py +++ b/openbb_platform/extensions/etf/openbb_etf/etf_router.py @@ -116,7 +116,7 @@ async def countries( @router.command( - model="PricePerformance", + model="EtfPricePerformance", examples=[ APIEx(parameters={"symbol": "QQQ", "provider": "fmp"}), APIEx(parameters={"symbol": "SPY,QQQ,IWM,DJIA", "provider": "fmp"}), @@ -128,7 +128,7 @@ async def price_performance( standard_params: StandardParams, extra_params: ExtraParams, ) -> OBBject: - """Price performance as a return, over different periods. This is a proxy for `equity.price.performance`.""" + """Price performance as a return, over different periods.""" return await OBBject.from_query(Query(**locals())) diff --git a/openbb_platform/openbb/assets/reference.json b/openbb_platform/openbb/assets/reference.json index bdbae0cf973..beb7f8439a4 100644 --- a/openbb_platform/openbb/assets/reference.json +++ b/openbb_platform/openbb/assets/reference.json @@ -17729,6 +17729,13 @@ }, "data": { "standard": [ + { + "name": "symbol", + "type": "str", + "description": "Symbol representing the entity requested in the data.", + "default": null, + "optional": true + }, { "name": "one_day", "type": "float", @@ -17799,6 +17806,13 @@ "default": null, "optional": true }, + { + "name": "two_year", + "type": "float", + "description": "Two-year return.", + "default": null, + "optional": true + }, { "name": "three_year", "type": "float", @@ -17806,6 +17820,13 @@ "default": null, "optional": true }, + { + "name": "four_year", + "type": "float", + "description": "Four-year", + "default": null, + "optional": true + }, { "name": "five_year", "type": "float", @@ -17828,15 +17849,7 @@ "optional": true } ], - "fmp": [ - { - "name": "symbol", - "type": "str", - "description": "The ticker symbol.", - "default": "", - "optional": false - } - ] + "fmp": [] }, "model": "PricePerformance" }, @@ -21252,37 +21265,53 @@ "flag": null, "message": null }, - "description": "Price performance as a return, over different periods. This is a proxy for `equity.price.performance`.", + "description": "Price performance as a return, over different periods.", "examples": "\nExamples\n--------\n\n```python\nfrom openbb import obb\nobb.etf.price_performance(symbol='QQQ', provider='fmp')\nobb.etf.price_performance(symbol='SPY,QQQ,IWM,DJIA', provider='fmp')\n```\n\n", "parameters": { "standard": [ { "name": "symbol", "type": "Union[str, List[str]]", - "description": "Symbol to get data for. Multiple items allowed for provider(s): fmp.", + "description": "Symbol to get data for. Multiple items allowed for provider(s): fmp, intrinio.", "default": "", "optional": false }, { "name": "provider", - "type": "Literal['fmp']", + "type": "Literal['fmp', 'intrinio']", "description": "The provider to use for the query, by default None. If None, the provider specified in defaults is selected or 'fmp' if there is no default.", "default": "fmp", "optional": true } ], - "fmp": [] + "fmp": [], + "intrinio": [ + { + "name": "return_type", + "type": "Literal['trailing', 'calendar']", + "description": "The type of returns to return, a trailing or calendar window.", + "default": "trailing", + "optional": true + }, + { + "name": "adjustment", + "type": "Literal['splits_only', 'splits_and_dividends']", + "description": "The adjustment factor, 'splits_only' will return pure price performance.", + "default": "splits_and_dividends", + "optional": true + } + ] }, "returns": { "OBBject": [ { "name": "results", - "type": "List[PricePerformance]", + "type": "List[EtfPricePerformance]", "description": "Serializable results." }, { "name": "provider", - "type": "Optional[Literal['fmp']]", + "type": "Optional[Literal['fmp', 'intrinio']]", "description": "Provider name." }, { @@ -21304,6 +21333,13 @@ }, "data": { "standard": [ + { + "name": "symbol", + "type": "str", + "description": "Symbol representing the entity requested in the data.", + "default": null, + "optional": true + }, { "name": "one_day", "type": "float", @@ -21374,6 +21410,13 @@ "default": null, "optional": true }, + { + "name": "two_year", + "type": "float", + "description": "Two-year return.", + "default": null, + "optional": true + }, { "name": "three_year", "type": "float", @@ -21381,6 +21424,13 @@ "default": null, "optional": true }, + { + "name": "four_year", + "type": "float", + "description": "Four-year", + "default": null, + "optional": true + }, { "name": "five_year", "type": "float", @@ -21403,17 +21453,116 @@ "optional": true } ], - "fmp": [ + "fmp": [], + "intrinio": [ { - "name": "symbol", - "type": "str", - "description": "The ticker symbol.", - "default": "", - "optional": false + "name": "max_annualized", + "type": "float", + "description": "Annualized rate of return from inception.", + "default": null, + "optional": true + }, + { + "name": "volatility_one_year", + "type": "float", + "description": "Trailing one-year annualized volatility.", + "default": null, + "optional": true + }, + { + "name": "volatility_three_year", + "type": "float", + "description": "Trailing three-year annualized volatility.", + "default": null, + "optional": true + }, + { + "name": "volatility_five_year", + "type": "float", + "description": "Trailing five-year annualized volatility.", + "default": null, + "optional": true + }, + { + "name": "volume", + "type": "int", + "description": "The trading volume.", + "default": null, + "optional": true + }, + { + "name": "volume_avg_30", + "type": "float", + "description": "The one-month average daily volume.", + "default": null, + "optional": true + }, + { + "name": "volume_avg_90", + "type": "float", + "description": "The three-month average daily volume.", + "default": null, + "optional": true + }, + { + "name": "volume_avg_180", + "type": "float", + "description": "The six-month average daily volume.", + "default": null, + "optional": true + }, + { + "name": "beta", + "type": "float", + "description": "Beta compared to the S&P 500.", + "default": null, + "optional": true + }, + { + "name": "nav", + "type": "float", + "description": "Net asset value per share.", + "default": null, + "optional": true + }, + { + "name": "year_high", + "type": "float", + "description": "The 52-week high price.", + "default": null, + "optional": true + }, + { + "name": "year_low", + "type": "float", + "description": "The 52-week low price.", + "default": null, + "optional": true + }, + { + "name": "market_cap", + "type": "float", + "description": "The market capitalization.", + "default": null, + "optional": true + }, + { + "name": "shares_outstanding", + "type": "int", + "description": "The number of shares outstanding.", + "default": null, + "optional": true + }, + { + "name": "updated", + "type": "date", + "description": "The date of the data.", + "default": null, + "optional": true } ] }, - "model": "PricePerformance" + "model": "EtfPricePerformance" }, "/etf/holdings": { "deprecated": { @@ -22317,6 +22466,13 @@ }, "data": { "standard": [ + { + "name": "symbol", + "type": "str", + "description": "Symbol representing the entity requested in the data.", + "default": null, + "optional": true + }, { "name": "one_day", "type": "float", @@ -22387,6 +22543,13 @@ "default": null, "optional": true }, + { + "name": "two_year", + "type": "float", + "description": "Two-year return.", + "default": null, + "optional": true + }, { "name": "three_year", "type": "float", @@ -22394,6 +22557,13 @@ "default": null, "optional": true }, + { + "name": "four_year", + "type": "float", + "description": "Four-year", + "default": null, + "optional": true + }, { "name": "five_year", "type": "float", @@ -22416,15 +22586,7 @@ "optional": true } ], - "fmp": [ - { - "name": "symbol", - "type": "str", - "description": "The ticker symbol.", - "default": "", - "optional": false - } - ] + "fmp": [] }, "model": "EtfHoldingsPerformance" }, diff --git a/openbb_platform/openbb/package/etf.py b/openbb_platform/openbb/package/etf.py index 663014c576d..74703b23143 100644 --- a/openbb_platform/openbb/package/etf.py +++ b/openbb_platform/openbb/package/etf.py @@ -716,6 +716,8 @@ class ROUTER_etf(Container): EtfHoldingsPerformance ---------------------- + symbol : Optional[str] + Symbol representing the entity requested in the data. one_day : Optional[float] One-day return. wtd : Optional[float] @@ -736,16 +738,18 @@ class ROUTER_etf(Container): Year to date return. one_year : Optional[float] One-year return. + two_year : Optional[float] + Two-year return. three_year : Optional[float] Three-year return. + four_year : Optional[float] + Four-year five_year : Optional[float] Five-year return. ten_year : Optional[float] Ten-year return. max : Optional[float] Return from the beginning of the time series. - symbol : Optional[str] - The ticker symbol. (provider: fmp) Examples -------- @@ -1170,34 +1174,38 @@ class ROUTER_etf(Container): symbol: Annotated[ Union[str, List[str]], OpenBBCustomParameter( - description="Symbol to get data for. Multiple items allowed for provider(s): fmp." + description="Symbol to get data for. Multiple items allowed for provider(s): fmp, intrinio." ), ], provider: Annotated[ - Optional[Literal["fmp"]], + Optional[Literal["fmp", "intrinio"]], OpenBBCustomParameter( description="The provider to use for the query, by default None.\n If None, the provider specified in defaults is selected or 'fmp' if there is\n no default." ), ] = None, **kwargs ) -> OBBject: - """Price performance as a return, over different periods. This is a proxy for `equity.price.performance`. + """Price performance as a return, over different periods. Parameters ---------- symbol : Union[str, List[str]] - Symbol to get data for. Multiple items allowed for provider(s): fmp. - provider : Optional[Literal['fmp']] + Symbol to get data for. Multiple items allowed for provider(s): fmp, intrinio. + provider : Optional[Literal['fmp', 'intrinio']] The provider to use for the query, by default None. If None, the provider specified in defaults is selected or 'fmp' if there is no default. + return_type : Literal['trailing', 'calendar'] + The type of returns to return, a trailing or calendar window. (provider: intrinio) + adjustment : Literal['splits_only', 'splits_and_dividends'] + The adjustment factor, 'splits_only' will return pure price performance. (provider: intrinio) Returns ------- OBBject - results : List[PricePerformance] + results : List[EtfPricePerformance] Serializable results. - provider : Optional[Literal['fmp']] + provider : Optional[Literal['fmp', 'intrinio']] Provider name. warnings : Optional[List[Warning_]] List of warnings. @@ -1206,8 +1214,10 @@ class ROUTER_etf(Container): extra : Dict[str, Any] Extra info. - PricePerformance - ---------------- + EtfPricePerformance + ------------------- + symbol : Optional[str] + Symbol representing the entity requested in the data. one_day : Optional[float] One-day return. wtd : Optional[float] @@ -1228,16 +1238,48 @@ class ROUTER_etf(Container): Year to date return. one_year : Optional[float] One-year return. + two_year : Optional[float] + Two-year return. three_year : Optional[float] Three-year return. + four_year : Optional[float] + Four-year five_year : Optional[float] Five-year return. ten_year : Optional[float] Ten-year return. max : Optional[float] Return from the beginning of the time series. - symbol : Optional[str] - The ticker symbol. (provider: fmp) + max_annualized : Optional[float] + Annualized rate of return from inception. (provider: intrinio) + volatility_one_year : Optional[float] + Trailing one-year annualized volatility. (provider: intrinio) + volatility_three_year : Optional[float] + Trailing three-year annualized volatility. (provider: intrinio) + volatility_five_year : Optional[float] + Trailing five-year annualized volatility. (provider: intrinio) + volume : Optional[int] + The trading volume. (provider: intrinio) + volume_avg_30 : Optional[float] + The one-month average daily volume. (provider: intrinio) + volume_avg_90 : Optional[float] + The three-month average daily volume. (provider: intrinio) + volume_avg_180 : Optional[float] + The six-month average daily volume. (provider: intrinio) + beta : Optional[float] + Beta compared to the S&P 500. (provider: intrinio) + nav : Optional[float] + Net asset value per share. (provider: intrinio) + year_high : Optional[float] + The 52-week high price. (provider: intrinio) + year_low : Optional[float] + The 52-week low price. (provider: intrinio) + market_cap : Optional[float] + The market capitalization. (provider: intrinio) + shares_outstanding : Optional[int] + The number of shares outstanding. (provider: intrinio) + updated : Optional[date] + The date of the data. (provider: intrinio) Examples -------- @@ -1253,14 +1295,14 @@ class ROUTER_etf(Container): "provider": self._get_provider( provider, "/etf/price_performance", - ("fmp",), + ("fmp", "intrinio"), ) }, standard_params={ "symbol": symbol, }, extra_params=kwargs, - extra_info={"symbol": {"multiple_items_allowed": ["fmp"]}}, + extra_info={"symbol": {"multiple_items_allowed": ["fmp", "intrinio"]}}, ) ) diff --git a/openbb_platform/providers/finviz/openbb_finviz/__init__.py b/openbb_platform/providers/finviz/openbb_finviz/__init__.py index 1be40385df3..87e660f1fed 100644 --- a/openbb_platform/providers/finviz/openbb_finviz/__init__.py +++ b/openbb_platform/providers/finviz/openbb_finviz/__init__.py @@ -14,6 +14,7 @@ finviz_provider = Provider( credentials=None, fetcher_dict={ "CompareGroups": FinvizCompareGroupsFetcher, + "EtfPricePerformance": FinvizPricePerformanceFetcher, "EquityInfo": FinvizEquityProfileFetcher, "KeyMetrics": FinvizKeyMetricsFetcher, "PricePerformance": FinvizPricePerformanceFetcher, diff --git a/openbb_platform/providers/finviz/openbb_finviz/models/price_performance.py b/openbb_platform/providers/finviz/openbb_finviz/models/price_performance.py index 2c2a9af5954..020c43f8852 100644 --- a/openbb_platform/providers/finviz/openbb_finviz/models/price_performance.py +++ b/openbb_platform/providers/finviz/openbb_finviz/models/price_performance.py @@ -2,6 +2,7 @@ # pylint: disable=unused-argument from typing import Any, Dict, List, Optional +from warnings import warn from finvizfinance.screener import performance from openbb_core.provider.abstract.fetcher import Fetcher @@ -108,7 +109,17 @@ class FinvizPricePerformanceFetcher( screen_df = screen_df.fillna("N/A").replace("N/A", None) # type: ignore except Exception as e: raise e from e - return screen_df.to_dict(orient="records") + symbols = query.symbol.split(",") + + # Check for missing symbols and warn of the missing symbols. + for symbol in symbols: + if symbol not in screen_df["Ticker"].tolist(): + warn(f"Symbol Error: {symbol} was not found.") + + return sorted( + screen_df.to_dict(orient="records"), + key=(lambda item: (symbols.index(item.get("Ticker", len(symbols))))), + ) @staticmethod def transform_data( diff --git a/openbb_platform/providers/fmp/openbb_fmp/__init__.py b/openbb_platform/providers/fmp/openbb_fmp/__init__.py index 61a9d331bd5..e8602091d5e 100644 --- a/openbb_platform/providers/fmp/openbb_fmp/__init__.py +++ b/openbb_platform/providers/fmp/openbb_fmp/__init__.py @@ -103,6 +103,7 @@ fmp_provider = Provider( "EtfHoldingsDate": FMPEtfHoldingsDateFetcher, "EtfHoldingsPerformance": FMPEtfHoldingsPerformanceFetcher, "EtfInfo": FMPEtfInfoFetcher, + "EtfPricePerformance": FMPPricePerformanceFetcher, "EtfSearch": FMPEtfSearchFetcher, "EtfSectors": FMPEtfSectorsFetcher, "ExecutiveCompensation": FMPExecutiveCompensationFetcher, diff --git a/openbb_platform/providers/intrinio/openbb_intrinio/__init__.py b/openbb_platform/providers/intrinio/openbb_intrinio/__init__.py index be7cb40870d..f4d001dcc1b 100644 --- a/openbb_platform/providers/intrinio/openbb_intrinio/__init__.py +++ b/openbb_platform/providers/intrinio/openbb_intrinio/__init__.py @@ -12,6 +12,9 @@ from openbb_intrinio.models.equity_info import IntrinioEquityInfoFetcher from openbb_intrinio.models.equity_quote import IntrinioEquityQuoteFetcher from openbb_intrinio.models.equity_search import IntrinioEquitySearchFetcher from openbb_intrinio.models.etf_info import IntrinioEtfInfoFetcher +from openbb_intrinio.models.etf_price_performance import ( + IntrinioEtfPricePerformanceFetcher, +) from openbb_intrinio.models.etf_search import IntrinioEtfSearchFetcher from openbb_intrinio.models.financial_ratios import IntrinioFinancialRatiosFetcher from openbb_intrinio.models.fred_series import IntrinioFredSeriesFetcher @@ -58,6 +61,7 @@ intrinio_provider = Provider( "EquitySearch": IntrinioEquitySearchFetcher, "EtfHistorical": IntrinioEquityHistoricalFetcher, "EtfInfo": IntrinioEtfInfoFetcher, + "EtfPricePerformance": IntrinioEtfPricePerformanceFetcher, "EtfSearch": IntrinioEtfSearchFetcher, "FinancialRatios": IntrinioFinancialRatiosFetcher, "FredSeries": IntrinioFredSeriesFetcher, diff --git a/openbb_platform/providers/intrinio/openbb_intrinio/models/etf_price_performance.py b/openbb_platform/providers/intrinio/openbb_intrinio/models/etf_price_performance.py new file mode 100644 index 00000000000..8ea7e5c27c0 --- /dev/null +++ b/openbb_platform/providers/intrinio/openbb_intrinio/models/etf_price_performance.py @@ -0,0 +1,227 @@ +"""Intrinio ETF Performance Model.""" + +# pylint: disable=unused-argument + +import asyncio +from datetime import date as dateType +from typing import Any, Dict, List, Literal, Optional +from warnings import warn + +from openbb_core.provider.abstract.fetcher import Fetcher +from openbb_core.provider.standard_models.recent_performance import ( + RecentPerformanceData, + RecentPerformanceQueryParams, +) +from openbb_core.provider.utils.descriptions import DATA_DESCRIPTIONS +from openbb_core.provider.utils.errors import EmptyDataError +from openbb_core.provider.utils.helpers import amake_request +from openbb_intrinio.utils.references import ETF_PERFORMANCE_MAP +from pydantic import Field + + +class IntrinioEtfPricePerformanceQueryParams(RecentPerformanceQueryParams): + """ + Intrinio ETF Performance Query Params. + + Source: https://docs.intrinio.com/documentation/web_api/get_etf_stats_v2 + Source: https://docs.intrinio.com/documentation/web_api/get_etf_analytics_v2 + """ + + __json_schema_extra__ = {"symbol": ["multiple_items_allowed"]} + + return_type: Literal["trailing", "calendar"] = Field( + default="trailing", + description="The type of returns to return, a trailing or calendar window.", + ) + adjustment: Literal["splits_only", "splits_and_dividends"] = Field( + default="splits_and_dividends", + description="The adjustment factor, 'splits_only' will return pure price performance.", + ) + + +class IntrinioEtfPricePerformanceData(RecentPerformanceData): + """Intrinio ETF Performance Data.""" + + __alias_dict__ = { + "updated": "date", + "year_high": "fifty_two_week_high", + "year_low": "fifty_two_week_low", + "volume": "volume_traded", + "volume_avg_30": "average_daily_volume_one_month", + "volume_avg_90": "average_daily_volume_three_month", + "volume_avg_180": "average_daily_volume_six_month", + } + + max_annualized: Optional[float] = Field( + default=None, + description="Annualized rate of return from inception.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + volatility_one_year: Optional[float] = Field( + default=None, + description="Trailing one-year annualized volatility.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + volatility_three_year: Optional[float] = Field( + default=None, + description="Trailing three-year annualized volatility.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + volatility_five_year: Optional[float] = Field( + default=None, + description="Trailing five-year annualized volatility.", + json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100}, + ) + volume: Optional[int] = Field( + default=None, + description=DATA_DESCRIPTIONS.get("volume", ""), + ) + volume_avg_30: Optional[float] = Field( + default=None, + description="The one-month average daily volume.", + ) + volume_avg_90: Optional[float] = Field( + default=None, + description="The three-month average daily volume.", + ) + volume_avg_180: Optional[float] = Field( + default=None, + description="The six-month average daily volume.", + ) + beta: Optional[float] = Field( + default=None, + description="Beta compared to the S&P 500.", + ) + nav: Optional[float] = Field( + default=None, + description="Net asset value per share.", + ) + year_high: Optional[float] = Field( + default=None, + description="The 52-week high price.", + ) + year_low: Optional[float] = Field( + default=None, + description="The 52-week low price.", + ) + + market_cap: Optional[float] = Field( + default=None, + description="The market capitalization.", + ) + shares_outstanding: Optional[int] = Field( + default=None, + description="The number of shares outstanding.", + ) + updated: Optional[dateType] = Field( + default=None, + description=DATA_DESCRIPTIONS.get("date", ""), + ) + + +class IntrinioEtfPricePerformanceFetcher( + Fetcher[ + IntrinioEtfPricePerformanceQueryParams, List[IntrinioEtfPricePerformanceData] + ] +): + """Intrinio ETF Performance Fetcher.""" + + @staticmethod + def transform_query( + params: Dict[str, Any] + ) -> IntrinioEtfPricePerformanceQueryParams: + """Transform query.""" + return IntrinioEtfPricePerformanceQueryParams(**params) + + @staticmethod + async def aextract_data( + query: IntrinioEtfPricePerformanceQueryParams, + credentials: Optional[Dict[str, str]], + **kwargs: Any, + ) -> List[Dict]: + """Return the raw data from the Intrinio endpoint.""" + + api_key = credentials.get("intrinio_api_key") if credentials else "" + base_url = "https://api-v2.intrinio.com/etfs/" + symbols = query.symbol.split(",") + symbols = [ + symbol + ":US" if ":" not in symbol else symbol for symbol in symbols + ] + + adjustment = ( + "split_only" + if query.adjustment == "splits_and_dividends" + else "split_and_dividend" + ) + + return_type = "trailing" if query.return_type == "calendar" else "calendar" + + results = [] + + async def get_one(symbol: str, **kwargs): + """Get data for one symbol.""" + + url = f"{base_url}{symbol}/stats?api_key={api_key}" + result = await amake_request(url, **kwargs) + + if "message" in result and result["message"] != []: # type: ignore + warn(f"Symbol Error: {symbol} - {result['message']}") # type: ignore + return + _ = result.pop("message", None) # type: ignore + _ = result.pop("messages", None) # type: ignore + + data = {} + etf = result.pop("etf", {}) # type: ignore + data["symbol"] = etf.get("ticker") + # These items will be kept regardless of the adjustment and return_type. + keep = ["volatility", "month", "year_to_date"] + for k, v in result.copy().items(): # type: ignore + if not any(substring in k for substring in keep): + _ = result.pop(k, None) if adjustment in k else None # type: ignore + _ = result.pop(k, None) if return_type in k else None # type: ignore + if k in result: + data[ETF_PERFORMANCE_MAP.get(k, k)] = v + # Get an additional set of data to combine with the first set. + analytics_url = ( + f"https://api-v2.intrinio.com/etfs/{symbol}/analytics?api_key={api_key}" + ) + if data: + analytics = await amake_request(analytics_url, **kwargs) + if "messages" in analytics and analytics["messages"] != []: # type: ignore + warn( + f"Symbol Error: {analytics['messages']}" # type: ignore + + f"for {etf.get('ticker')}" # type: ignore + ) + return + # Remove the duplicate data from the analytics response. + _ = analytics.pop("messages", None) # type: ignore + _ = analytics.pop("etf", None) # type: ignore + _ = analytics.pop("date", None) # type: ignore + + data.update(analytics) # type: ignore + + results.append(data) + + tasks = [get_one(symbol, **kwargs) for symbol in symbols] + + await asyncio.gather(*tasks) + + if not results: + raise EmptyDataError("No data was returned.") + + # Undo any formatting changes made to the symbols before sorting. + symbols = query.symbol.replace(":US", "").split(",") + + return sorted( + results, + key=(lambda item: (symbols.index(item.get("symbol", len(symbols))))), + ) + + @staticmethod + def transform_data( + query: IntrinioEtfPricePerformanceQueryParams, + data: List[Dict], + **kwargs: Any, + ) -> List[IntrinioEtfPricePerformanceData]: + """Transform data.""" + return [IntrinioEtfPricePerformanceData.model_validate(d) for d in data] diff --git a/openbb_platform/providers/intrinio/openbb_intrinio/utils/references.py b/openbb_platform/providers/intrinio/openbb_intrinio/utils/references.py index 204c2df492b..23c15c14c6c 100644 --- a/openbb_platform/providers/intrinio/openbb_intrinio/utils/references.py +++ b/openbb_platform/providers/intrinio/openbb_intrinio/utils/references.py @@ -64,6 +64,39 @@ ETF_EXCHANGES = Literal[ ] +ETF_PERFORMANCE_MAP = { + "trailing_one_month_return_split_and_dividend": "one_month", + "trailing_one_month_return_split_only": "one_month", + "trailing_one_year_return_split_and_dividend": "one_year", + "trailing_one_year_return_split_only": "one_year", + "trailing_one_year_volatility_annualized": "volatility_one_year", + "trailing_three_year_annualized_return_split_and_dividend": "three_year", + "trailing_three_year_annualized_return_split_only": "three_year", + "trailing_three_year_volatility_annualized": "volatility_three_year", + "trailing_five_year_annualized_return_split_and_dividend": "five_year", + "trailing_five_year_annualized_return_split_only": "five_year", + "trailing_five_year_volatility_annualized": "volatility_five_year", + "trailing_ten_year_annualized_return_split_and_dividend": "ten_year", + "trailing_ten_year_annualized_return_split_only": "ten_year", + "inception_annualized_return_split_and_dividend": "max_annualized", + "inception_annualized_return_split_only": "max_annualized", + "calendar_year_5_return_split_and_dividend": "five_year", + "calendar_year_5_return_split_only": "five_year", + "calendar_year_4_return_split_and_dividend": "four_year", + "calendar_year_4_return_split_only": "four_year", + "calendar_year_3_return_split_and_dividend": "three_year", + "calendar_year_3_return_split_only": "three_year", + "calendar_year_2_return_split_and_dividend": "two_year", + "calendar_year_2_return_split_only": "two_year", + "calendar_year_1_return_split_and_dividend": "one_year", + "calendar_year_1_return_split_only": "one_year", + "calendar_year_to_date_return_split_and_dividend": "ytd", + "calendar_year_to_date_return_split_only": "ytd", + "net_asset_value": "nav", + "beta_vs_spy": "beta", +} + + class IntrinioCompany(Data): """Intrinio Company Data.""" diff --git a/openbb_platform/providers/intrinio/tests/record/http/test_intrinio_fetchers/test_intrinio_etf_price_performance_fetcher.yaml b/openbb_platform/providers/intrinio/tests/record/http/test_intrinio_fetchers/test_intrinio_etf_price_performance_fetcher.yaml new file mode 100644 index 00000000000..712b1e362c1 --- /dev/null +++ b/openbb_platform/providers/intrinio/tests/record/http/test_intrinio_fetchers/test_intrinio_etf_price_performance_fetcher.yaml @@ -0,0 +1,82 @@ +interactions: +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://api-v2.intrinio.com/etfs/SPY:US/stats?api_key=MOCK_API_KEY + response: + body: + string: !!binary | + H4sIAE9n82UAA5RVUW8aMQz+K9U9U2Q7cZzwNm3rc1daqdM2nW6Q0mjXo7s70Gi1/z4fXSkqlCsv + QEK+z/6cz/FjdhebppjFJht9+zHIpkUbs1FGQPYUzCmabJBVsc2LptHPZVEu9G9GHgZwKDLIfsa2 + yJdN3tyvshEOYZC1dZHKVM3yeRXzu3nV3uZ1bBd1pWfKpFTVNJ+mZZrGapqNYAgUAqFhi5a9WLCu + n2Nelat3YVexqA+HNxzIk1EsiID1vQTPsY3xgYI36C2zFeR9yOW8LFrda1cauFoUZXqI67BICI6Y + nIgxwNvg9raO/+EvmMMiEMGTAHsmAGesHEe3KWewNgSL3hkkYXH7ad4WJSYEdAKA+oXebOFv0vJY + UVZNFvRaDT5d7lFsz5rQgGUQQs/KJ8btZXlTEkEIWggExOAZeDuJNlZHKiItrAvsLPgQjJdjyDaC + tPNMAGIwFmFtnVRN4n2b5tX7M9EGN+LYeW0fMGTfQ7KxiSeLol4TIW/VL4NsUpTKrNmvJXBPyyGR + tp3VuwVWo5ke/HNc8l1Q1CqKA3b8GmZ7NHujTmIiVW2EQg98U2+Hjgw4fSf0pWDvX+PM4bCaNXmP + wTlWDwu5HvxGrUD3sOk9oQS3UyM6FPW0U4uiWq0+UoBqO+kheArbAYP1qA5lFQx6R/QaiD16dTA4 + UMm8tgjsBMY39FryToKw18bQe9qpUzvPu/HUM028PsbqTkQhTR52st/L8uLsHXRsb7LRY5aUvfud + P/wG8+W6G4vFXTcqx+efLk7G3xcA5M5PGODk8+XZyWW9aFo91KbJr1ivj33V5U2apXx7b3Q11u06 + TZ6Ww3NdpSZVurwai7eOzhAM6G4Tp/OyG83eAiHrTvwzuS2qmQ7INfzDxcfr7O/ffwAAAP//AwCV + rhxr0gcAAA== + headers: + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Thu, 14 Mar 2024 21:08:31 GMT + Transfer-Encoding: + - chunked + Vary: + - Origin,Accept-Encoding + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - application/json + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + method: GET + uri: https://api-v2.intrinio.com/etfs/SPY:US/analytics?api_key=MOCK_API_KEY + response: + body: + string: !!binary | + H4sIAE9n82UAA3RRy07DMBD8lcjnNFo7Dzu5IaDn0rRSESDLStzEah4QO31Q8e9sCqqQUOWLd2Zn + xzs+k1ZbqyptSfby5pNSOU0ywoBFMwhnNCQ+2ZqtO0l36OVB652sTVWTLKYiYAzoP7rpDyQLRRhw + 5PZ9M7ZaukGVukRRTCECIQLwidrrAX1lqUxzkr+Nfadl23cODRIe85RFIohTwENvKFw96KuGJ2Ga + JhELKKeCxzck1hyvgpSylAOgiYhYzHzSqmGnnSzUOz4XcBywRIQpCybS1mrQVvajs051pekqkqU8 + 4YJRmkxLabcl2ZkY3HW6y88PCJ82mGGn2inXfPGw9PLXEXDqwosBvMfV3FsNo3XY5Eyx08Ol7fmS + e2XkXyxb5wgPpvgpgwVWxpoOy3XORZSwOYUQELW67JvpH0UEjMaI6GNRqw6jaC/yu+X9hnx9fQMA + AP//AwCMIK2E/wEAAA== + headers: + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Thu, 14 Mar 2024 21:08:31 GMT + Transfer-Encoding: + - chunked + Vary: + - Origin,Accept-Encoding + status: + code: 200 + message: OK +version: 1 diff --git a/openbb_platform/providers/intrinio/tests/test_intrinio_fetchers.py b/openbb_platform/providers/intrinio/tests/test_intrinio_fetchers.py index 0092ff2afc3..8c0ac8340e2 100644 --- a/openbb_platform/providers/intrinio/tests/test_intrinio_fetchers.py +++ b/openbb_platform/providers/intrinio/tests/test_intrinio_fetchers.py @@ -14,6 +14,9 @@ from openbb_intrinio.models.equity_info import IntrinioEquityInfoFetcher from openbb_intrinio.models.equity_quote import IntrinioEquityQuoteFetcher from openbb_intrinio.models.equity_search import IntrinioEquitySearchFetcher from openbb_intrinio.models.etf_info import IntrinioEtfInfoFetcher +from openbb_intrinio.models.etf_price_performance import ( + IntrinioEtfPricePerformanceFetcher, +) from openbb_intrinio.models.etf_search import IntrinioEtfSearchFetcher from openbb_intrinio.models.financial_ratios import IntrinioFinancialRatiosFetcher from openbb_intrinio.models.fred_series import IntrinioFredSeriesFetcher @@ -382,3 +385,12 @@ def test_intrinio_etf_info_fetcher(credentials=test_credentials): fetcher = IntrinioEtfInfoFetcher() result = fetcher.test(params, credentials) assert result is None + + +@pytest.mark.record_http +def test_intrinio_etf_price_performance_fetcher(credentials=test_credentials): + params = {"symbol": "SPY:US"} + + fetcher = IntrinioEtfPricePerformanceFetcher() + result = fetcher.test(params, credentials) + assert result is None