diff options
author | Danglewood <85772166+deeleeramone@users.noreply.github.com> | 2024-02-15 11:55:26 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-15 19:55:26 +0000 |
commit | ccf46aad9250c8858a6c4c782a88ab3a2531dc2f (patch) | |
tree | 692fda493341bb56e5f2c3e6a0e368df84681355 | |
parent | 389333853445bf9672262350a0ef05904d878774 (diff) |
[Feature] Add end point: `etf.equity_exposure()` with FMP provider (#6079)
* add etf.equity_exposure from FMP
* empty data error
* recapture cassette
* pylint unused argument
* Fix router example typo
* fix test...?
* static file to fix test?
* fix test..?
* __json_schema_extra__
* black
---------
Co-authored-by: Igor Radovanovic <74266147+IgorWounds@users.noreply.github.com>
Co-authored-by: James Maslek <jmaslek11@gmail.com>
15 files changed, 1107 insertions, 288 deletions
diff --git a/openbb_platform/core/openbb_core/provider/standard_models/etf_equity_exposure.py b/openbb_platform/core/openbb_core/provider/standard_models/etf_equity_exposure.py new file mode 100644 index 00000000000..1a190b8a87d --- /dev/null +++ b/openbb_platform/core/openbb_core/provider/standard_models/etf_equity_exposure.py @@ -0,0 +1,43 @@ +"""ETF Equity Exposure Standard Model.""" + +from typing import Optional, Union + +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 + + +class EtfEquityExposureQueryParams(QueryParams): + """ETF Equity Exposure Query Params.""" + + symbol: str = Field(description=QUERY_DESCRIPTIONS.get("symbol", "") + " (Stock)") + + @field_validator("symbol") + @classmethod + def upper_symbol(cls, v: str) -> str: + """Convert symbol to uppercase.""" + return v.upper() + + +class EtfEquityExposureData(Data): + """ETF Equity Exposure Data.""" + + equity_symbol: str = Field(description="The symbol of the equity requested.") + etf_symbol: str = Field( + description="The symbol of the ETF with exposure to the requested equity." + ) + shares: Optional[int] = Field( + default=None, + description="The number of shares held in the ETF.", + ) + weight: Optional[float] = Field( + default=None, + description="The weight of the equity in the ETF, as a normalized percent.", + json_schema_extra={"units_measurement": "percent", "frontend_multiply": 100}, + ) + market_value: Optional[Union[int, float]] = Field( + default=None, + description="The market value of the equity position in the ETF.", + ) diff --git a/openbb_platform/extensions/etf/integration/test_etf_api.py b/openbb_platform/extensions/etf/integration/test_etf_api.py index 15edfeda960..740c677b745 100644 --- a/openbb_platform/extensions/etf/integration/test_etf_api.py +++ b/openbb_platform/extensions/etf/integration/test_etf_api.py @@ -264,3 +264,20 @@ def test_etf_holdings_performance(params, headers): result = requests.get(url, headers=headers, timeout=10) assert isinstance(result, requests.Response) assert result.status_code == 200 + + +@parametrize( + "params", + [ + ({"symbol": "SPY,VOO,QQQ,IWM,IWN", "provider": "fmp"}), + ], +) +@pytest.mark.integration +def test_etf_equity_exposure(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/etf/equity_exposure?{query_str}" + result = requests.get(url, headers=headers, timeout=10) + assert isinstance(result, requests.Response) + assert result.status_code == 200 diff --git a/openbb_platform/extensions/etf/integration/test_etf_python.py b/openbb_platform/extensions/etf/integration/test_etf_python.py index 202fb1c62af..65f89e159e6 100644 --- a/openbb_platform/extensions/etf/integration/test_etf_python.py +++ b/openbb_platform/extensions/etf/integration/test_etf_python.py @@ -251,3 +251,19 @@ def test_etf_holdings_performance(params, obb): assert result assert isinstance(result, OBBject) assert len(result.results) > 0 + + +@parametrize( + "params", + [ + ({"symbol": "SPY,VOO,QQQ,IWM,IWN", "provider": "fmp"}), + ], +) +@pytest.mark.integration +def test_etf_equity_exposure(params, obb): + params = {p: v for p, v in params.items() if v} + + result = obb.etf.equity_exposure(**params) + assert result + assert isinstance(result, OBBject) + assert len(result.results) > 0 diff --git a/openbb_platform/extensions/etf/openbb_etf/etf_router.py b/openbb_platform/extensions/etf/openbb_etf/etf_router.py index 99ee39e43d5..c2906727d7a 100644 --- a/openbb_platform/extensions/etf/openbb_etf/etf_router.py +++ b/openbb_platform/extensions/etf/openbb_etf/etf_router.py @@ -18,7 +18,16 @@ router.include_router(discovery_router) # pylint: disable=unused-argument -@router.command(model="EtfSearch") +@router.command( + model="EtfSearch", + exclude_auto_examples=True, + examples=[ + "### An empty query returns the full list of ETFs from the provider. ###", + 'obb.etf.search("", provider="fmp")', + "#### The query will return results from text-based fields containing the term. ####" + 'obb.etf.search("commercial real estate", provider="fmp")', + ], +) async def search( cc: CommandContext, provider_choices: ProviderChoices, @@ -43,7 +52,15 @@ async def historical( return await OBBject.from_query(Query(**locals())) -@router.command(model="EtfInfo") +@router.command( + model="EtfInfo", + exclude_auto_examples=True, + examples=[ + 'obb.etf.info("SPY", provider="fmp")', + "#### This function accepts multiple tickers. ####", + 'obb.etf.info("SPY,IWM,QQQ,DJIA", provider="fmp")', + ], +) async def info( cc: CommandContext, provider_choices: ProviderChoices, @@ -54,7 +71,13 @@ async def info( return await OBBject.from_query(Query(**locals())) -@router.command(model="EtfSectors") +@router.command( + model="EtfSectors", + exclude_auto_examples=True, + examples=[ + 'obb.etf.sectors("SPY", provider="fmp")', + ], +) async def sectors( cc: CommandContext, provider_choices: ProviderChoices, @@ -65,7 +88,13 @@ async def sectors( return await OBBject.from_query(Query(**locals())) -@router.command(model="EtfCountries") +@router.command( + model="EtfCountries", + exclude_auto_examples=True, + examples=[ + 'obb.etf.countries("VT", provider="fmp")', + ], +) async def countries( cc: CommandContext, provider_choices: ProviderChoices, @@ -76,18 +105,34 @@ async def countries( return await OBBject.from_query(Query(**locals())) -@router.command(model="PricePerformance") +@router.command( + model="PricePerformance", + exclude_auto_examples=True, + examples=[ + 'obb.etf.price_performance("SPY,QQQ,IWM,DJIA", provider="fmp")', + ], +) async def price_performance( cc: CommandContext, provider_choices: ProviderChoices, standard_params: StandardParams, extra_params: ExtraParams, ) -> OBBject: - """Price performance as a return, over different periods.""" + """Price performance as a return, over different periods. This is a proxy for `equity.price.performance`.""" return await OBBject.from_query(Query(**locals())) -@router.command(model="EtfHoldings") +@router.command( + model="EtfHoldings", + exclude_auto_examples=True, + examples=[ + 'obb.etf.holdings("XLK", provider="fmp").to_df()', + "#### Including a date (FMP, SEC) will return the holdings as per NPORT-P filings. ####", + 'obb.etf.holdings("XLK", date="2022-03-31",provider="fmp").to_df()', + "#### The same data can be returned from the SEC directly. ####", + 'obb.etf.holdings("XLK", date="2022-03-31",provider="sec").to_df()', + ], +) async def holdings( cc: CommandContext, provider_choices: ProviderChoices, @@ -98,23 +143,54 @@ async def holdings( return await OBBject.from_query(Query(**locals())) -@router.command(model="EtfHoldingsDate") +@router.command( + model="EtfHoldingsDate", + exclude_auto_examples=True, + examples=[ + 'obb.etf.holdings_date("XLK", provider="fmp").results', + ], +) async def holdings_date( cc: CommandContext, provider_choices: ProviderChoices, standard_params: StandardParams, extra_params: ExtraParams, ) -> OBBject: - """Get the holdings filing date for an individual ETF.""" + """Use this function to get the holdings dates, if available.""" return await OBBject.from_query(Query(**locals())) -@router.command(model="EtfHoldingsPerformance") +@router.command( + model="EtfHoldingsPerformance", + exclude_auto_examples=True, + examples=[ + 'obb.etf.holdings_performance("XLK", provider="fmp")', + ], +) async def holdings_performance( cc: CommandContext, provider_choices: ProviderChoices, standard_params: StandardParams, extra_params: ExtraParams, ) -> OBBject: - """Get the ETF holdings performance.""" + """Get the recent price performance of each ticker held in the ETF.""" + return await OBBject.from_query(Query(**locals())) + + +@router.command( + model="EtfEquityExposure", + exclude_auto_examples=True, + examples=[ + 'obb.etf.equity_exposure("MSFT", provider="fmp")', + "#### This function accepts multiple tickers. ####", + 'obb.etf.equity_exposure("MSFT,AAPL", provider="fmp")', + ], +) +async def equity_exposure( + cc: CommandContext, + provider_choices: ProviderChoices, + standard_params: StandardParams, + extra_params: ExtraParams, +) -> OBBject: + """Get the exposure to ETFs for a specific stock.""" return await OBBject.from_query(Query(**locals())) diff --git a/openbb_platform/openbb/package/etf.py b/openbb_platform/openbb/package/etf.py index 9563e667242..e076e815c9c 100644 --- a/openbb_platform/openbb/package/etf.py +++ b/openbb_platform/openbb/package/etf.py @@ -14,6 +14,7 @@ from typing_extensions import Annotated class ROUTER_etf(Container): """/etf countries + equity_exposure historical holdings holdings_date @@ -69,7 +70,7 @@ class ROUTER_etf(Container): Example ------- >>> from openbb import obb - >>> obb.etf.countries(symbol="SPY") + >>> obb.etf.countries("VT", provider="fmp") """ # noqa: E501 return self._run( @@ -90,6 +91,82 @@ class ROUTER_etf(Container): ) @validate + def equity_exposure( + self, + symbol: Annotated[ + Union[str, List[str]], + OpenBBCustomParameter( + description="Symbol to get data for. (Stock) Multiple items allowed: fmp." + ), + ], + provider: Optional[Literal["fmp"]] = None, + **kwargs + ) -> OBBject: + """Get the exposure to ETFs for a specific stock. + + Parameters + ---------- + symbol : Union[str, List[str]] + Symbol to get data for. (Stock) Multiple items allowed: fmp. + provider : Optional[Literal['fmp']] + 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. + + Returns + ------- + OBBject + results : List[EtfEquityExposure] + Serializable results. + provider : Optional[Literal['fmp']] + Provider name. + warnings : Optional[List[Warning_]] + List of warnings. + chart : Optional[Chart] + Chart object. + extra: Dict[str, Any] + Extra info. + + EtfEquityExposure + ----------------- + equity_symbol : str + The symbol of the equity requested. + etf_symbol : str + The symbol of the ETF with exposure to the requested equity. + shares : Optional[int] + The number of shares held in the ETF. + weight : Optional[float] + The weight of the equity in the ETF, as a normalized percent. + market_value : Optional[Union[float, int]] + The market value of the equity position in the ETF. + + Example + ------- + >>> from openbb import obb + >>> obb.etf.equity_exposure("MSFT", provider="fmp") + >>> #### This function accepts multiple tickers. #### + >>> obb.etf.equity_exposure("MSFT,AAPL", provider="fmp") + """ # noqa: E501 + + return self._run( + "/etf/equity_exposure", + **filter_inputs( + provider_choices={ + "provider": self._get_provider( + provider, + "/etf/equity_exposure", + ("fmp",), + ) + }, + standard_params={ + "symbol": symbol, + }, + extra_params=kwargs, + extra_info={"symbol": {"multiple_items_allowed": ["fmp"]}}, + ) + ) + + @validate def historical( self, symbol: Annotated[ @@ -389,7 +466,11 @@ class ROUTER_etf(Container): Example ------- >>> from openbb import obb - >>> obb.etf.holdings(symbol="SPY") + >>> obb.etf.holdings("XLK", provider="fmp").to_df() + >>> #### Including a date (FMP, SEC) will return the holdings as per NPORT-P filings. #### + >>> obb.etf.holdings("XLK", date="2022-03-31",provider="fmp").to_df() + >>> #### The same data can be returned from the SEC directly. #### + >>> obb.etf.holdings("XLK", date="2022-03-31",provider="sec").to_df() """ # noqa: E501 return self._run( @@ -418,7 +499,7 @@ class ROUTER_etf(Container): provider: Optional[Literal["fmp"]] = None, **kwargs ) -> OBBject: - """Get the holdings filing date for an individual ETF. + """Use this function to get the holdings dates, if available. Parameters ---------- @@ -453,7 +534,7 @@ class ROUTER_etf(Container): Example ------- >>> from openbb import obb - >>> obb.etf.holdings_date(symbol="SPY") + >>> obb.etf.holdings_date("XLK", provider="fmp").results """ # noqa: E501 return self._run( @@ -482,7 +563,7 @@ class ROUTER_etf(Container): provider: Optional[Literal["fmp"]] = None, **kwargs ) -> OBBject: - """Get the ETF holdings performance. + """Get the recent price performance of each ticker held in the ETF. Parameters ---------- @@ -543,7 +624,7 @@ class ROUTER_etf(Container): Example ------- >>> from openbb import obb - >>> obb.etf.holdings_performance(symbol="SPY") + >>> obb.etf.holdings_performance("XLK", provider="fmp") """ # noqa: E501 return self._run( @@ -569,7 +650,7 @@ class ROUTER_etf(Container): symbol: Annotated[ Union[str, List[str]], OpenBBCustomParameter( - description="Symbol to get data for. (ETF) Multiple items allowed: yfinance." + description="Symbol to get data for. (ETF) Multiple items allowed: fmp, yfinance." ), ], provider: Optional[Literal["fmp", "yfinance"]] = None, @@ -580,7 +661,7 @@ class ROUTER_etf(Container): Parameters ---------- symbol : Union[str, List[str]] - Symbol to get data for. (ETF) Multiple items allowed: yfinance. + Symbol to get data for. (ETF) Multiple items allowed: fmp, yfinance. provider : Optional[Literal['fmp', 'yfinance']] The provider to use for the query, by default None. If None, the provider specified in defaults is selected or 'fmp' if there is @@ -610,30 +691,30 @@ class ROUTER_etf(Container): Description of the fund. inception_date : Optional[str] Inception date of the ETF. - asset_class : Optional[str] - Asset class of the ETF. (provider: fmp) - aum : Optional[float] - Assets under management. (provider: fmp) - avg_volume : Optional[float] - Average trading volume of the ETF. (provider: fmp) + issuer : Optional[str] + Company of the ETF. (provider: fmp) cusip : Optional[str] CUSIP of the ETF. (provider: fmp) - domicile : Optional[str] - Domicile of the ETF. (provider: fmp) - etf_company : Optional[str] - Company of the ETF. (provider: fmp) - expense_ratio : Optional[float] - Expense ratio of the ETF. (provider: fmp) isin : Optional[str] ISIN of the ETF. (provider: fmp) + domicile : Optional[str] + Domicile of the ETF. (provider: fmp) + asset_class : Optional[str] + Asset class of the ETF. (provider: fmp) + aum : Optional[float] + Assets under management. (provider: fmp) nav : Optional[float] Net asset value of the ETF. (provider: fmp) nav_currency : Optional[str] Currency of the ETF's net asset value. (provider: fmp) - website : Optional[str] - Website link of the ETF. (provider: fmp) + expense_ratio : Optional[float] + The expense ratio, as a normalized percent. (provider: fmp) holdings_count : Optional[int] - Number of holdings in the ETF. (provider: fmp) + Number of holdings. (provider: fmp) + avg_volume : Optional[float] + Average daily trading volume. (provider: fmp) + website : Optional[str] + Website of the issuer. (provider: fmp) fund_type : Optional[str] The legal type of fund. (provider: yfinance) fund_family : Optional[str] @@ -700,7 +781,9 @@ class ROUTER_etf(Container): Example ------- >>> from openbb import obb - >>> obb.etf.info(symbol="SPY") + >>> obb.etf.info("SPY", provider="fmp") + >>> #### This function accepts multiple tickers. #### + >>> obb.etf.info("SPY,IWM,QQQ,DJIA", provider="fmp") """ # noqa: E501 return self._run( @@ -717,7 +800,7 @@ class ROUTER_etf(Container): "symbol": symbol, }, extra_params=kwargs, - extra_info={"symbol": {"multiple_items_allowed": ["yfinance"]}}, + extra_info={"symbol": {"multiple_items_allowed": ["fmp", "yfinance"]}}, ) ) @@ -730,7 +813,7 @@ class ROUTER_etf(Container): provider: Optional[Literal["fmp"]] = None, **kwargs ) -> OBBject: - """Price performance as a return, over different periods. + """Price performance as a return, over different periods. This is a proxy for `equity.price.performance`. Parameters ---------- @@ -791,7 +874,7 @@ class ROUTER_etf(Container): Example ------- >>> from openbb import obb - >>> obb.etf.price_performance(symbol="SPY") + >>> obb.etf.price_performance("SPY,QQQ,IWM,DJIA", provider="fmp") """ # noqa: E501 return self._run( @@ -884,7 +967,9 @@ class ROUTER_etf(Container): Example ------- >>> from openbb import obb - >>> obb.etf.search(query="Vanguard") + >>> ### An empty query returns the full list of ETFs from the provider. ### + >>> obb.etf.search("", provider="fmp") + >>> #### The query will return results from text-based fields containing the term. ####obb.etf.search("commercial real estate", provider="fmp") """ # noqa: E501 return self._run( @@ -948,7 +1033,7 @@ class ROUTER_etf(Container): Example ------- >>> from openbb import obb - >>> obb.etf.sectors(symbol="SPY") + >>> obb.etf.sectors("SPY", provider="fmp") """ # noqa: E501 return self._run( diff --git a/openbb_platform/openbb/package/module_map.json b/openbb_platform/openbb/package/module_map.json index 39397c721a3..bae65c52ed9 100644 --- a/openbb_platform/openbb/package/module_map.json +++ b/openbb_platform/openbb/package/module_map.json @@ -96,6 +96,7 @@ "equity_shorts_fails_to_deliver": "/equity/shorts/fails_to_deliver", "etf": "/etf", "etf_countries": "/etf/countries", + "etf_equity_exposure": "/etf/equity_exposure", "etf_historical": "/etf/historical", "etf_holdings": "/etf/holdings", "etf_holdings_date": "/etf/holdings_date", diff --git a/openbb_platform/providers/fmp/openbb_fmp/__init__.py b/openbb_platform/providers/fmp/openbb_fmp/__init__.py index 1af89b46c0a..c3e2843540f 100644 --- a/openbb_platform/providers/fmp/openbb_fmp/__init__.py +++ b/openbb_platform/providers/fmp/openbb_fmp/__init__.py @@ -30,6 +30,7 @@ from openbb_fmp.models.equity_valuation_multiples import ( FMPEquityValuationMultiplesFetcher, ) from openbb_fmp.models.etf_countries import FMPEtfCountriesFetcher +from openbb_fmp.models.etf_equity_exposure import FMPEtfEquityExposureFetcher from openbb_fmp.models.etf_holdings import FMPEtfHoldingsFetcher from openbb_fmp.models.etf_holdings_date import FMPEtfHoldingsDateFetcher from openbb_fmp.models.etf_holdings_performance import FMPEtfHoldingsPerformanceFetcher @@ -96,6 +97,7 @@ fmp_provider = Provider( "EquityScreener": FMPEquityScreenerFetcher, "EquityValuationMultiples": FMPEquityValuationMultiplesFetcher, "EtfCountries": FMPEtfCountriesFetcher, + "EtfEquityExposure": FMPEtfEquityExposureFetcher, "EtfHoldings": FMPEtfHoldingsFetcher, "EtfHoldingsDate": FMPEtfHoldingsDateFetcher, "EtfHoldingsPerformance": FMPEtfHoldingsPerformanceFetcher, diff --git a/openbb_platform/providers/fmp/openbb_fmp/models/etf_countries.py b/openbb_platform/providers/fmp/openbb_fmp/models/etf_countries.py index 0109ae1ae04..4ffd1e762d6 100644 --- a/openbb_platform/providers/fmp/openbb_fmp/models/etf_countries.py +++ b/openbb_platform/providers/fmp/openbb_fmp/models/etf_countries.py @@ -14,6 +14,8 @@ from openbb_fmp.utils.helpers import create_url, get_data_many class FMPEtfCountriesQueryParams(EtfCountriesQueryParams): """FMP ETF Countries Query.""" + __json_schema_extra__ = {"symbol": ["multiple_items_allowed"]} + class FMPEtfCountriesData(EtfCountriesData): """FMP ETF Countries Data.""" diff --git a/openbb_platform/providers/fmp/openbb_fmp/models/etf_equity_exposure.py b/openbb_platform/providers/fmp/openbb_fmp/models/etf_equity_exposure.py new file mode 100644 index 00000000000..06cd0f0ba21 --- /dev/null +++ b/openbb_platform/providers/fmp/openbb_fmp/models/etf_equity_exposure.py @@ -0,0 +1,95 @@ +"""FMP ETF Equity Exposure Model.""" + +# pylint: disable=unused-argument + +import asyncio +import warnings +from typing import Any, Dict, List, Optional + +from openbb_core.provider.abstract.fetcher import Fetcher +from openbb_core.provider.standard_models.etf_equity_exposure import ( + EtfEquityExposureData, + EtfEquityExposureQueryParams, +) +from openbb_core.provider.utils.errors import EmptyDataError +from openbb_core.provider.utils.helpers import amake_request +from pydantic import field_validator + +_warn = warnings.warn + + +class FMPEtfEquityExposureQueryParams(EtfEquityExposureQueryParams): + """ + FMP ETF Equity Exposure Query Params. + + Source: https://site.financialmodelingprep.com/developer/docs/etf-stock-exposure-api/ + """ + + __json_schema_extra__ = {"symbol": [ |