add Intrinio ETF Holdings (#6208)

This commit is contained in:
Danglewood
2024-03-15 08:05:20 -07:00
committed by GitHub
parent 0239e6edbb
commit c55f93c865
8 changed files with 453 additions and 13 deletions

View File

@@ -333,6 +333,12 @@ def test_etf_holdings_date(params, headers):
"use_cache": False,
}
),
(
{
"symbol": "DJIA",
"provider": "intrinio",
}
),
],
)
@pytest.mark.integration

View File

@@ -325,6 +325,12 @@ def test_etf_holdings_date(params, obb):
"use_cache": False,
}
),
(
{
"symbol": "DJIA",
"provider": "intrinio",
}
),
],
)
@pytest.mark.integration

View File

@@ -21582,7 +21582,7 @@
},
{
"name": "provider",
"type": "Literal['fmp', 'sec']",
"type": "Literal['fmp', 'intrinio', 'sec']",
"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
@@ -21604,6 +21604,7 @@
"optional": true
}
],
"intrinio": [],
"sec": [
{
"name": "date",
@@ -21630,7 +21631,7 @@
},
{
"name": "provider",
"type": "Optional[Literal['fmp', 'sec']]",
"type": "Optional[Literal['fmp', 'intrinio', 'sec']]",
"description": "Provider name."
},
{
@@ -21816,6 +21817,127 @@
"optional": true
}
],
"intrinio": [
{
"name": "security_type",
"type": "str",
"description": "The type of instrument for this holding. Examples(Bond='BOND', Equity='EQUI')",
"default": null,
"optional": true
},
{
"name": "isin",
"type": "str",
"description": "The International Securities Identification Number.",
"default": null,
"optional": true
},
{
"name": "ric",
"type": "str",
"description": "The Reuters Instrument Code.",
"default": null,
"optional": true
},
{
"name": "sedol",
"type": "str",
"description": "The Stock Exchange Daily Official List.",
"default": null,
"optional": true
},
{
"name": "share_class_figi",
"type": "str",
"description": "The OpenFIGI symbol for the holding.",
"default": null,
"optional": true
},
{
"name": "country",
"type": "str",
"description": "The country or region of the holding.",
"default": null,
"optional": true
},
{
"name": "maturity_date",
"type": "date",
"description": "The maturity date for the debt security, if available.",
"default": null,
"optional": true
},
{
"name": "contract_expiry_date",
"type": "date",
"description": "Expiry date for the futures contract held, if available.",
"default": null,
"optional": true
},
{
"name": "coupon",
"type": "float",
"description": "The coupon rate of the debt security, if available.",
"default": null,
"optional": true
},
{
"name": "balance",
"type": "Union[float, int]",
"description": "The number of units of the security held, if available.",
"default": null,
"optional": true
},
{
"name": "unit",
"type": "str",
"description": "The units of the 'balance' field.",
"default": null,
"optional": true
},
{
"name": "units_per_share",
"type": "float",
"description": "Number of units of the security held per share outstanding of the ETF, if available.",
"default": null,
"optional": true
},
{
"name": "face_value",
"type": "float",
"description": "The face value of the debt security, if available.",
"default": null,
"optional": true
},
{
"name": "derivatives_value",
"type": "float",
"description": "The notional value of derivatives contracts held.",
"default": null,
"optional": true
},
{
"name": "value",
"type": "float",
"description": "The market value of the holding, on the 'as_of' date.",
"default": null,
"optional": true
},
{
"name": "weight",
"type": "float",
"description": "The weight of the holding, as a normalized percent.",
"default": null,
"optional": true
},
{
"name": "updated",
"type": "date",
"description": "The 'as_of' date for the holding.",
"default": null,
"optional": true
}
],
"sec": [
{
"name": "lei",

View File

@@ -371,7 +371,7 @@ class ROUTER_etf(Container):
str, OpenBBCustomParameter(description="Symbol to get data for. (ETF)")
],
provider: Annotated[
Optional[Literal["fmp", "sec"]],
Optional[Literal["fmp", "intrinio", "sec"]],
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."
),
@@ -384,7 +384,7 @@ class ROUTER_etf(Container):
----------
symbol : str
Symbol to get data for. (ETF)
provider : Optional[Literal['fmp', 'sec']]
provider : Optional[Literal['fmp', 'intrinio', 'sec']]
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.
@@ -401,7 +401,7 @@ class ROUTER_etf(Container):
OBBject
results : List[EtfHoldings]
Serializable results.
provider : Optional[Literal['fmp', 'sec']]
provider : Optional[Literal['fmp', 'intrinio', 'sec']]
Provider name.
warnings : Optional[List[Warning_]]
List of warnings.
@@ -423,9 +423,10 @@ class ROUTER_etf(Container):
cusip : Optional[str]
The CUSIP of the holding. (provider: fmp, sec)
isin : Optional[str]
The ISIN of the holding. (provider: fmp, sec)
The ISIN of the holding. (provider: fmp, intrinio, sec)
balance : Optional[int]
The balance of the holding, in shares or units. (provider: fmp);
The number of units of the security held, if available. (provider: intrinio);
The balance of the holding. (provider: sec)
units : Optional[Union[str, float]]
The type of units. (provider: fmp);
@@ -433,9 +434,9 @@ class ROUTER_etf(Container):
currency : Optional[str]
The currency of the holding. (provider: fmp, sec)
value : Optional[float]
The value of the holding, in dollars. (provider: fmp, sec)
The value of the holding, in dollars. (provider: fmp, intrinio, sec)
weight : Optional[float]
The weight of the holding, as a normalized percent. (provider: fmp);
The weight of the holding, as a normalized percent. (provider: fmp, intrinio);
The weight of the holding in ETF in %. (provider: sec)
payoff_profile : Optional[str]
The payoff profile of the holding. (provider: fmp, sec)
@@ -444,7 +445,7 @@ class ROUTER_etf(Container):
issuer_category : Optional[str]
The issuer category of the holding. (provider: fmp, sec)
country : Optional[str]
The country of the holding. (provider: fmp, sec)
The country of the holding. (provider: fmp, intrinio, sec)
is_restricted : Optional[str]
Whether the holding is restricted. (provider: fmp, sec)
fair_value_level : Optional[int]
@@ -460,7 +461,30 @@ class ROUTER_etf(Container):
acceptance_datetime : Optional[str]
The acceptance datetime of the filing. (provider: fmp)
updated : Optional[Union[date, datetime]]
The date the data was updated. (provider: fmp)
The date the data was updated. (provider: fmp);
The 'as_of' date for the holding. (provider: intrinio)
security_type : Optional[str]
The type of instrument for this holding. Examples(Bond='BOND', Equity='EQUI') (provider: intrinio)
ric : Optional[str]
The Reuters Instrument Code. (provider: intrinio)
sedol : Optional[str]
The Stock Exchange Daily Official List. (provider: intrinio)
share_class_figi : Optional[str]
The OpenFIGI symbol for the holding. (provider: intrinio)
maturity_date : Optional[date]
The maturity date for the debt security, if available. (provider: intrinio, sec)
contract_expiry_date : Optional[date]
Expiry date for the futures contract held, if available. (provider: intrinio)
coupon : Optional[float]
The coupon rate of the debt security, if available. (provider: intrinio)
unit : Optional[str]
The units of the 'balance' field. (provider: intrinio)
units_per_share : Optional[float]
Number of units of the security held per share outstanding of the ETF, if available. (provider: intrinio)
face_value : Optional[float]
The face value of the debt security, if available. (provider: intrinio)
derivatives_value : Optional[float]
The notional value of derivatives contracts held. (provider: intrinio)
other_id : Optional[str]
Internal identifier for the holding. (provider: sec)
loan_value : Optional[float]
@@ -469,8 +493,6 @@ class ROUTER_etf(Container):
The issuer conditions of the holding. (provider: sec)
asset_conditional : Optional[str]
The asset conditions of the holding. (provider: sec)
maturity_date : Optional[date]
The maturity date of the debt security. (provider: sec)
coupon_kind : Optional[str]
The type of coupon for the debt security. (provider: sec)
rate_type : Optional[str]
@@ -591,7 +613,7 @@ class ROUTER_etf(Container):
"provider": self._get_provider(
provider,
"/etf/holdings",
("fmp", "sec"),
("fmp", "intrinio", "sec"),
)
},
standard_params={

View File

@@ -11,6 +11,7 @@ from openbb_intrinio.models.equity_historical import IntrinioEquityHistoricalFet
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_holdings import IntrinioEtfHoldingsFetcher
from openbb_intrinio.models.etf_info import IntrinioEtfInfoFetcher
from openbb_intrinio.models.etf_price_performance import (
IntrinioEtfPricePerformanceFetcher,
@@ -60,6 +61,7 @@ intrinio_provider = Provider(
"EquityQuote": IntrinioEquityQuoteFetcher,
"EquitySearch": IntrinioEquitySearchFetcher,
"EtfHistorical": IntrinioEquityHistoricalFetcher,
"EtfHoldings": IntrinioEtfHoldingsFetcher,
"EtfInfo": IntrinioEtfInfoFetcher,
"EtfPricePerformance": IntrinioEtfPricePerformanceFetcher,
"EtfSearch": IntrinioEtfSearchFetcher,

View File

@@ -0,0 +1,202 @@
"""Intrinio ETF Holdings Model."""
# pylint: disable=unused-argument
from datetime import date as dateType
from typing import Any, Dict, List, Optional, Union
from openbb_core.provider.abstract.fetcher import Fetcher
from openbb_core.provider.standard_models.etf_holdings import (
EtfHoldingsData,
EtfHoldingsQueryParams,
)
from openbb_core.provider.utils.helpers import (
ClientResponse,
ClientSession,
amake_request,
)
from pydantic import Field, model_validator
class IntrinioEtfHoldingsQueryParams(EtfHoldingsQueryParams):
"""
Intrinio ETF Holdings Query Params.
Source: https://docs.intrinio.com/documentation/web_api/get_etf_holdings_v2
"""
class IntrinioEtfHoldingsData(EtfHoldingsData):
"""Intrinio ETF Holdings Data."""
__alias_dict__ = {
"symbol": "ticker",
"security_type": "type",
"unit": "quantity_units",
"face_value": "face",
"balance": "quantity_held",
"value": "market_value_held",
"derivatives_value": "notional_value",
"units_per_share": "quantity_per_share",
"weight": "weighting",
"updated": "as_of_date",
"country": "location",
"maturity_date": "maturity",
}
name: Optional[str] = Field(
default=None,
description="The common name for the holding.",
)
security_type: Optional[str] = Field(
default=None,
description="The type of instrument for this holding. Examples(Bond='BOND', Equity='EQUI')",
)
isin: Optional[str] = Field(
default=None,
description="The International Securities Identification Number.",
)
ric: Optional[str] = Field(
default=None,
description="The Reuters Instrument Code.",
)
sedol: Optional[str] = Field(
default=None,
description="The Stock Exchange Daily Official List.",
)
share_class_figi: Optional[str] = Field(
default=None,
description="The OpenFIGI symbol for the holding.",
)
country: Optional[str] = Field(
default=None,
description="The country or region of the holding.",
)
maturity_date: Optional[dateType] = Field(
default=None,
description="The maturity date for the debt security, if available.",
)
contract_expiry_date: Optional[dateType] = Field(
default=None,
description="Expiry date for the futures contract held, if available.",
)
coupon: Optional[float] = Field(
default=None,
description="The coupon rate of the debt security, if available.",
)
balance: Optional[Union[int, float]] = Field(
default=None,
description="The number of units of the security held, if available.",
)
unit: Optional[str] = Field(
default=None,
description="The units of the 'balance' field.",
)
units_per_share: Optional[float] = Field(
default=None,
description="Number of units of the security held per share outstanding of the ETF, if available.",
)
face_value: Optional[float] = Field(
default=None,
description="The face value of the debt security, if available.",
)
derivatives_value: Optional[float] = Field(
default=None,
description="The notional value of derivatives contracts held.",
)
value: Optional[float] = Field(
default=None,
description="The market value of the holding, on the 'as_of' date.",
)
weight: Optional[float] = Field(
default=None,
description="The weight of the holding, as a normalized percent.",
)
updated: Optional[dateType] = Field(
default=None,
description="The 'as_of' date for the holding.",
)
@model_validator(mode="before")
@classmethod
def replace_zero(cls, values):
"""Check for zero values and replace with None."""
return (
{k: None if v == 0 else v for k, v in values.items()}
if isinstance(values, dict)
else values
)
class IntrinioEtfHoldingsFetcher(
Fetcher[IntrinioEtfHoldingsQueryParams, List[IntrinioEtfHoldingsData]]
):
"""Intrinio ETF Holdings Fetcher."""
@staticmethod
def transform_query(params: Dict[str, Any]) -> IntrinioEtfHoldingsQueryParams:
"""Transform query."""
return IntrinioEtfHoldingsQueryParams(**params)
@staticmethod
async def aextract_data(
query: IntrinioEtfHoldingsQueryParams,
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 ""
symbol = query.symbol + ":US" if ":" not in query.symbol else query.symbol
URL = f"https://api-v2.intrinio.com/etfs/{symbol}/holdings?page_size=10000&api_key={api_key}"
data = []
async def response_callback(response: ClientResponse, session: ClientSession):
"""Async response callback."""
results = await response.json()
if results.get("error"): # type: ignore
return results
if results.get("holdings") and len(results.get("holdings")) > 0: # type: ignore
data.extend(results.get("holdings")) # type: ignore
while results.get("next_page"): # type: ignore
next_page = results["next_page"] # type: ignore
next_url = f"{URL}&next_page={next_page}"
results = await amake_request(next_url, session=session, **kwargs)
if (
"holdings" in results
and len(results.get("holdings")) > 0 # type: ignore
):
data.extend(results.get("holdings")) # type: ignore
return data
return await amake_request(URL, response_callback=response_callback, **kwargs) # type: ignore
@staticmethod
def transform_data(
query: IntrinioEtfHoldingsQueryParams,
data: List[Dict],
**kwargs: Any,
) -> List[IntrinioEtfHoldingsData]:
"""Transform data."""
if not data or isinstance(data, dict) and data.get("error"):
if isinstance(data, list) and data == []:
raise RuntimeError(
str(
f"No holdings were found for {query.symbol}, and the response from Intrinio was empty."
)
)
raise RuntimeError(str(f"{data.get('message')} {query.symbol}: {data['error']}")) # type: ignore
results: List[IntrinioEtfHoldingsData] = []
for d in sorted(data, key=lambda x: x["weighting"], reverse=True):
# This field is deprecated and is dupilcated in the response.
_ = d.pop("composite_figi", None)
results.append(IntrinioEtfHoldingsData.model_validate(d))
return results

View File

@@ -0,0 +1,70 @@
interactions:
- request:
body: null
headers:
Accept:
- application/json
Accept-Encoding:
- gzip, deflate
Connection:
- keep-alive
method: GET
uri: https://api-v2.intrinio.com/etfs/DJIA:US/holdings?api_key=MOCK_API_KEY&page_size=10000
response:
body:
string: !!binary |
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
Dp6WX6//AwAA//8DAMc4QPyTNwAA
headers:
Connection:
- keep-alive
Content-Encoding:
- gzip
Content-Type:
- application/json
Date:
- Thu, 14 Mar 2024 00:48:14 GMT
Transfer-Encoding:
- chunked
Vary:
- Origin,Accept-Encoding
status:
code: 200
message: OK
version: 1

View File

@@ -13,6 +13,7 @@ from openbb_intrinio.models.equity_historical import IntrinioEquityHistoricalFet
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_holdings import IntrinioEtfHoldingsFetcher
from openbb_intrinio.models.etf_info import IntrinioEtfInfoFetcher
from openbb_intrinio.models.etf_price_performance import (
IntrinioEtfPricePerformanceFetcher,
@@ -387,6 +388,15 @@ def test_intrinio_etf_info_fetcher(credentials=test_credentials):
assert result is None
@pytest.mark.record_http
def test_intrinio_etf_holdings_fetcher(credentials=test_credentials):
params = {"symbol": "DJIA"}
fetcher = IntrinioEtfHoldingsFetcher()
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"}