Upload 292 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- openbb_platform/providers/imf/README.md +35 -0
- openbb_platform/providers/imf/__init__.py +1 -0
- openbb_platform/providers/imf/openbb_imf/__init__.py +18 -0
- openbb_platform/providers/imf/openbb_imf/assets/__init__.py +1 -0
- openbb_platform/providers/imf/openbb_imf/assets/imf_country_map.json +260 -0
- openbb_platform/providers/imf/openbb_imf/assets/imf_symbols.json +0 -0
- openbb_platform/providers/imf/openbb_imf/models/__init__.py +1 -0
- openbb_platform/providers/imf/openbb_imf/models/available_indicators.py +126 -0
- openbb_platform/providers/imf/openbb_imf/models/direction_of_trade.py +274 -0
- openbb_platform/providers/imf/openbb_imf/models/economic_indicators.py +303 -0
- openbb_platform/providers/imf/openbb_imf/utils/__init__.py +1 -0
- openbb_platform/providers/imf/openbb_imf/utils/constants.py +125 -0
- openbb_platform/providers/imf/openbb_imf/utils/dot_helpers.py +87 -0
- openbb_platform/providers/imf/openbb_imf/utils/fsi_helpers.py +247 -0
- openbb_platform/providers/imf/openbb_imf/utils/helpers.py +34 -0
- openbb_platform/providers/imf/openbb_imf/utils/irfcl_helpers.py +283 -0
- openbb_platform/providers/imf/poetry.lock +0 -0
- openbb_platform/providers/imf/pyproject.toml +19 -0
- openbb_platform/providers/imf/tests/__init__.py +1 -0
- openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_direction_of_trade_fetcher_urllib3_v1.yaml +55 -0
- openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_direction_of_trade_fetcher_urllib3_v2.yaml +55 -0
- openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_economic_indicators_fetcher_urllib3_v1.yaml +54 -0
- openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_economic_indicators_fetcher_urllib3_v2.yaml +54 -0
- openbb_platform/providers/imf/tests/test_imf_fetchers.py +78 -0
- openbb_platform/providers/intrinio/README.md +13 -0
- openbb_platform/providers/intrinio/__init__.py +1 -0
- openbb_platform/providers/intrinio/openbb_intrinio/__init__.py +116 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/__init__.py +1 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/balance_sheet.py +506 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/calendar_ipo.py +193 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/cash_flow.py +338 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/company_filings.py +175 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/company_news.py +295 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/currency_pairs.py +90 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/equity_historical.py +274 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/equity_info.py +78 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/equity_quote.py +142 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/equity_search.py +89 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/etf_holdings.py +214 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/etf_info.py +655 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/etf_price_performance.py +227 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/etf_search.py +148 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/financial_attributes.py +78 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/financial_ratios.py +320 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/forward_ebitda_estimates.py +201 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/forward_eps_estimates.py +236 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/forward_pe_estimates.py +165 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/forward_sales_estimates.py +254 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/fred_series.py +114 -0
- openbb_platform/providers/intrinio/openbb_intrinio/models/historical_attributes.py +144 -0
openbb_platform/providers/imf/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenBB IMF Provider Extension
|
| 2 |
+
|
| 3 |
+
This package adds the `openbb-imf` provider extension to the OpenBB Platform.
|
| 4 |
+
|
| 5 |
+
## Installation
|
| 6 |
+
|
| 7 |
+
Install from PyPI with:
|
| 8 |
+
|
| 9 |
+
```sh
|
| 10 |
+
pip install openbb-imf
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
## Implementation
|
| 14 |
+
|
| 15 |
+
The extension utilizes the JSON RESTful Web Service ((https://datahelp.imf.org/knowledgebase/articles/630877-data-services)[https://datahelp.imf.org/knowledgebase/articles/630877-data-services])
|
| 16 |
+
|
| 17 |
+
No authorization is required to use, but IP addresses are bound by the limitations described in the link above.
|
| 18 |
+
|
| 19 |
+
## Coverage
|
| 20 |
+
|
| 21 |
+
- Databases:
|
| 22 |
+
- International Reserves and Foreign Currency Liquidity
|
| 23 |
+
- Direction of Trade Statistics
|
| 24 |
+
- Financial Soundness Indicators
|
| 25 |
+
|
| 26 |
+
Coverage:
|
| 27 |
+
- All IRFCL tables.
|
| 28 |
+
- Individual, or multiple, time series from single or multiple countries.
|
| 29 |
+
- Core and Encouraged Set tables, plus all individual underlying series.
|
| 30 |
+
|
| 31 |
+
### Endpoints
|
| 32 |
+
|
| 33 |
+
- `obb.economy.available_indicators`
|
| 34 |
+
- `obb.economy.indicators`
|
| 35 |
+
- `obb.economy.direction_of_trade`
|
openbb_platform/providers/imf/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""IMF Provoider Extension."""
|
openbb_platform/providers/imf/openbb_imf/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB IMF Provider Module."""
|
| 2 |
+
|
| 3 |
+
from openbb_core.provider.abstract.provider import Provider
|
| 4 |
+
from openbb_imf.models.available_indicators import ImfAvailableIndicatorsFetcher
|
| 5 |
+
from openbb_imf.models.direction_of_trade import ImfDirectionOfTradeFetcher
|
| 6 |
+
from openbb_imf.models.economic_indicators import ImfEconomicIndicatorsFetcher
|
| 7 |
+
|
| 8 |
+
imf_provider = Provider(
|
| 9 |
+
name="imf",
|
| 10 |
+
website="https://datahelp.imf.org/knowledgebase/articles/667681-using-json-restful-web-service",
|
| 11 |
+
description="This provider allows you to access International Monetary Fund data through the IMF Public Data API.",
|
| 12 |
+
fetcher_dict={
|
| 13 |
+
"AvailableIndicators": ImfAvailableIndicatorsFetcher,
|
| 14 |
+
"DirectionOfTrade": ImfDirectionOfTradeFetcher,
|
| 15 |
+
"EconomicIndicators": ImfEconomicIndicatorsFetcher,
|
| 16 |
+
},
|
| 17 |
+
repr_name="International Monetary Fund (IMF) Public Data API",
|
| 18 |
+
)
|
openbb_platform/providers/imf/openbb_imf/assets/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""IMF Static Assets."""
|
openbb_platform/providers/imf/openbb_imf/assets/imf_country_map.json
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"AF": "Afghanistan",
|
| 3 |
+
"AL": "Albania",
|
| 4 |
+
"DZ": "Algeria",
|
| 5 |
+
"AS": "American Samoa",
|
| 6 |
+
"AO": "Angola",
|
| 7 |
+
"AI": "Anguilla",
|
| 8 |
+
"AG": "Antigua and Barbuda",
|
| 9 |
+
"AR": "Argentina",
|
| 10 |
+
"AM": "Armenia",
|
| 11 |
+
"AW": "Aruba",
|
| 12 |
+
"S19": "Asia not allocated",
|
| 13 |
+
"AU": "Australia",
|
| 14 |
+
"AT": "Austria",
|
| 15 |
+
"AZ": "Azerbaijan",
|
| 16 |
+
"BS": "Bahamas",
|
| 17 |
+
"BH": "Bahrain",
|
| 18 |
+
"BD": "Bangladesh",
|
| 19 |
+
"BB": "Barbados",
|
| 20 |
+
"BY": "Belarus",
|
| 21 |
+
"BE": "Belgium",
|
| 22 |
+
"R1": "Belgo-Luxembourg Economic Union",
|
| 23 |
+
"BZ": "Belize",
|
| 24 |
+
"BJ": "Benin",
|
| 25 |
+
"BM": "Bermuda",
|
| 26 |
+
"BT": "Bhutan",
|
| 27 |
+
"BO": "Bolivia",
|
| 28 |
+
"BA": "Bosnia and Herzegovina",
|
| 29 |
+
"BW": "Botswana",
|
| 30 |
+
"BR": "Brazil",
|
| 31 |
+
"BN": "Brunei Darussalam",
|
| 32 |
+
"BG": "Bulgaria",
|
| 33 |
+
"BF": "Burkina Faso",
|
| 34 |
+
"BI": "Burundi",
|
| 35 |
+
"CV": "Cabo Verde",
|
| 36 |
+
"KH": "Cambodia",
|
| 37 |
+
"CM": "Cameroon",
|
| 38 |
+
"CA": "Canada",
|
| 39 |
+
"5Y": "Eastern Caribbean Currency Union",
|
| 40 |
+
"CF": "Central African Republic",
|
| 41 |
+
"TD": "Chad",
|
| 42 |
+
"CL": "Chile",
|
| 43 |
+
"HK": "Hong Kong",
|
| 44 |
+
"MO": "Macao",
|
| 45 |
+
"CN": "China",
|
| 46 |
+
"CO": "Colombia",
|
| 47 |
+
"KM": "Comoros",
|
| 48 |
+
"CD": "Democratic Republic of the Congo",
|
| 49 |
+
"CG": "Congo",
|
| 50 |
+
"CR": "Costa Rica",
|
| 51 |
+
"CI": "Ivory Coast",
|
| 52 |
+
"HR": "Croatia",
|
| 53 |
+
"CU": "Cuba",
|
| 54 |
+
"CW": "Curacao",
|
| 55 |
+
"CY": "Cyprus",
|
| 56 |
+
"CZ": "Czech Republic",
|
| 57 |
+
"CSH": "Former Czechoslovakia",
|
| 58 |
+
"DK": "Denmark",
|
| 59 |
+
"DJ": "Djibouti",
|
| 60 |
+
"DM": "Dominica",
|
| 61 |
+
"DO": "Dominican Republic",
|
| 62 |
+
"DE2": "East Germany",
|
| 63 |
+
"EC": "Ecuador",
|
| 64 |
+
"EG": "Egypt",
|
| 65 |
+
"SV": "El Salvador",
|
| 66 |
+
"GQ": "Equatorial Guinea",
|
| 67 |
+
"ER": "Eritrea",
|
| 68 |
+
"EE": "Estonia",
|
| 69 |
+
"SZ": "Eswatini",
|
| 70 |
+
"ET": "Ethiopia",
|
| 71 |
+
"E19": "Europe not allocated",
|
| 72 |
+
"FK": "Falkland Islands (Malvinas)",
|
| 73 |
+
"FO": "Faroe Islands",
|
| 74 |
+
"FJ": "Fiji",
|
| 75 |
+
"FI": "Finland",
|
| 76 |
+
"FR": "France",
|
| 77 |
+
"PF": "French Polynesia",
|
| 78 |
+
"GA": "Gabon",
|
| 79 |
+
"GM": "Gambia",
|
| 80 |
+
"GE": "Georgia",
|
| 81 |
+
"DE": "Germany",
|
| 82 |
+
"GH": "Ghana",
|
| 83 |
+
"GI": "Gibraltar",
|
| 84 |
+
"GR": "Greece",
|
| 85 |
+
"GL": "Greenland",
|
| 86 |
+
"GD": "Grenada",
|
| 87 |
+
"GU": "Guam",
|
| 88 |
+
"GT": "Guatemala",
|
| 89 |
+
"GN": "Guinea",
|
| 90 |
+
"GW": "Guinea-Bissau",
|
| 91 |
+
"GY": "Guyana",
|
| 92 |
+
"HT": "Haiti",
|
| 93 |
+
"VA": "Vatican City State",
|
| 94 |
+
"HN": "Honduras",
|
| 95 |
+
"HU": "Hungary",
|
| 96 |
+
"IS": "Iceland",
|
| 97 |
+
"IN": "India",
|
| 98 |
+
"ID": "Indonesia",
|
| 99 |
+
"IR": "Iran",
|
| 100 |
+
"IQ": "Iraq",
|
| 101 |
+
"IE": "Ireland",
|
| 102 |
+
"IL": "Israel",
|
| 103 |
+
"IT": "Italy",
|
| 104 |
+
"JM": "Jamaica",
|
| 105 |
+
"JP": "Japan",
|
| 106 |
+
"JO": "Jordan",
|
| 107 |
+
"KZ": "Kazakhstan",
|
| 108 |
+
"KE": "Kenya",
|
| 109 |
+
"KI": "Kiribati",
|
| 110 |
+
"KP": "North Korea",
|
| 111 |
+
"KR": "South Korea",
|
| 112 |
+
"XK": "Kosovo",
|
| 113 |
+
"KW": "Kuwait",
|
| 114 |
+
"KG": "Kyrgyzstan",
|
| 115 |
+
"LA": "Lao",
|
| 116 |
+
"LV": "Latvia",
|
| 117 |
+
"LB": "Lebanon",
|
| 118 |
+
"LS": "Lesotho",
|
| 119 |
+
"LR": "Liberia",
|
| 120 |
+
"LY": "Libya",
|
| 121 |
+
"LT": "Lithuania",
|
| 122 |
+
"LU": "Luxembourg",
|
| 123 |
+
"MG": "Madagascar",
|
| 124 |
+
"MW": "Malawi",
|
| 125 |
+
"MY": "Malaysia",
|
| 126 |
+
"1C_554": "West Malaysia",
|
| 127 |
+
"MV": "Maldives",
|
| 128 |
+
"ML": "Mali",
|
| 129 |
+
"MT": "Malta",
|
| 130 |
+
"MH": "Marshall islands",
|
| 131 |
+
"MR": "Mauritania",
|
| 132 |
+
"MU": "Mauritius",
|
| 133 |
+
"MX": "Mexico",
|
| 134 |
+
"FM": "Micronesia",
|
| 135 |
+
"F979": "Middle East and Central Asia not specified",
|
| 136 |
+
"MD": "Moldova",
|
| 137 |
+
"MN": "Mongolia",
|
| 138 |
+
"ME": "Montenegro",
|
| 139 |
+
"MS": "Montserrat",
|
| 140 |
+
"MA": "Morocco",
|
| 141 |
+
"MZ": "Mozambique",
|
| 142 |
+
"MM": "Myanmar",
|
| 143 |
+
"NA": "Namibia",
|
| 144 |
+
"NR": "Nauru",
|
| 145 |
+
"NP": "Nepal",
|
| 146 |
+
"AN": "Antilles",
|
| 147 |
+
"NL": "Netherlands",
|
| 148 |
+
"NC": "New Caledonia",
|
| 149 |
+
"NZ": "New Zealand",
|
| 150 |
+
"NI": "Nicaragua",
|
| 151 |
+
"NE": "Niger",
|
| 152 |
+
"NG": "Nigeria",
|
| 153 |
+
"MK": "North Macedonia",
|
| 154 |
+
"1C_958": "North Vietnam",
|
| 155 |
+
"NO": "Norway",
|
| 156 |
+
"OM": "Oman",
|
| 157 |
+
"PK": "Pakistan",
|
| 158 |
+
"PW": "Palau",
|
| 159 |
+
"PA": "Panama",
|
| 160 |
+
"PG": "Papua New Guinea",
|
| 161 |
+
"PY": "Paraguay",
|
| 162 |
+
"PE": "Peru",
|
| 163 |
+
"PH": "Philippines",
|
| 164 |
+
"PL": "Poland",
|
| 165 |
+
"PT": "Portugal",
|
| 166 |
+
"QA": "Qatar",
|
| 167 |
+
"RO": "Romania",
|
| 168 |
+
"RU": "Russia",
|
| 169 |
+
"RW": "Rwanda",
|
| 170 |
+
"WS": "Samoa",
|
| 171 |
+
"SM": "San Marino",
|
| 172 |
+
"ST": "Sao Tome and Principe",
|
| 173 |
+
"SA": "Saudi Arabia",
|
| 174 |
+
"SN": "Senegal",
|
| 175 |
+
"CS": "Serbia and Montenegro",
|
| 176 |
+
"RS": "Serbia",
|
| 177 |
+
"SC": "Seychelles",
|
| 178 |
+
"SL": "Sierra Leone",
|
| 179 |
+
"SG": "Singapore",
|
| 180 |
+
"SX": "Sint Maarten (Dutch part)",
|
| 181 |
+
"SK": "Slovakia",
|
| 182 |
+
"SI": "Slovenia",
|
| 183 |
+
"SB": "Solomon Islands",
|
| 184 |
+
"SO": "Somalia",
|
| 185 |
+
"ZA": "South Africa",
|
| 186 |
+
"1C_198": "South African Common Customs Area",
|
| 187 |
+
"SS": "South Sudan",
|
| 188 |
+
"ES": "Spain",
|
| 189 |
+
"LK": "Sri Lanka",
|
| 190 |
+
"KN": "Saint Kitts and Nevis",
|
| 191 |
+
"LC": "Saint Lucia",
|
| 192 |
+
"VC": "Saint Vincent and the Grenadines",
|
| 193 |
+
"SD": "Sudan",
|
| 194 |
+
"SR": "Suriname",
|
| 195 |
+
"SE": "Sweden",
|
| 196 |
+
"CH": "Switzerland",
|
| 197 |
+
"SY": "Syria",
|
| 198 |
+
"TW": "Taiwan",
|
| 199 |
+
"TJ": "Tajikistan",
|
| 200 |
+
"TZ": "Tanzania",
|
| 201 |
+
"TH": "Thailand",
|
| 202 |
+
"TL": "Timor-Leste",
|
| 203 |
+
"TG": "Togo",
|
| 204 |
+
"TO": "Tonga",
|
| 205 |
+
"TT": "Trinidad and Tobago",
|
| 206 |
+
"TN": "Tunisia",
|
| 207 |
+
"TR": "Turkey",
|
| 208 |
+
"TM": "Turkmenistan",
|
| 209 |
+
"TV": "Tuvalu",
|
| 210 |
+
"UG": "Uganda",
|
| 211 |
+
"UA": "Ukraine",
|
| 212 |
+
"AE": "United Arab Emirates",
|
| 213 |
+
"GB": "United Kingdom",
|
| 214 |
+
"US": "United States",
|
| 215 |
+
"UY": "Uruguay",
|
| 216 |
+
"SUH": "Former USSR",
|
| 217 |
+
"UZ": "Uzbekistan",
|
| 218 |
+
"VU": "Vanuatu",
|
| 219 |
+
"VE": "Venezuela",
|
| 220 |
+
"VN": "Viet Nam",
|
| 221 |
+
"PS": "Palestine",
|
| 222 |
+
"YE": "Yemen",
|
| 223 |
+
"YUC": "Former Yugoslavia",
|
| 224 |
+
"ZM": "Zambia",
|
| 225 |
+
"ZW": "Zimbabwe",
|
| 226 |
+
"R14": "Community of Independent States",
|
| 227 |
+
"U2": "Euro Area",
|
| 228 |
+
"E1": "Europe",
|
| 229 |
+
"B0": "European Union",
|
| 230 |
+
"F97": "Middle East",
|
| 231 |
+
"XS5": "Middle East and Central Asia",
|
| 232 |
+
"X88": "Other Countries n.i.e. (IMF)",
|
| 233 |
+
"F1": "Africa",
|
| 234 |
+
"F6": "Sub-Saharan Africa",
|
| 235 |
+
"F19": "Africa not allocated",
|
| 236 |
+
"A10": "Western Hemisphere",
|
| 237 |
+
"A109": "Western Hemisphere not allocated",
|
| 238 |
+
"W00": "World",
|
| 239 |
+
"XR99": "Special Categories and Economic Zones",
|
| 240 |
+
"XS25": "Developing Asia (IMF)",
|
| 241 |
+
"XR43": "Emerging and Developing Countries",
|
| 242 |
+
"XR29": "Advanced Economies (IMF)",
|
| 243 |
+
"1C_ALLC": "All Countries",
|
| 244 |
+
"1C_ALL": "All Countries and Country Groups",
|
| 245 |
+
"1C_ALLG": "All Country Groups",
|
| 246 |
+
"1C_903": "Emerging and Developing Europe",
|
| 247 |
+
"1C_080": "Export Earnings: Fuel",
|
| 248 |
+
"1C_092": "Export Earnings: Nonfuel",
|
| 249 |
+
"1C_440": "Middle East, North Africa, Afghanistan, and Pakistan",
|
| 250 |
+
"1C_473": "Yemen Arab Rep",
|
| 251 |
+
"1C_459": "Yemen P.D. Rep",
|
| 252 |
+
"1C_All_Reporting_Countries_Data": "All Reporting Countries, Data",
|
| 253 |
+
"1C_All_Reporting_Countries_Metadata": "All Reporting Countries, Metadata",
|
| 254 |
+
"1C_Daily_Reporters": "Daily Reporters",
|
| 255 |
+
"1C_Dual_Reporters": "Dual Reporters",
|
| 256 |
+
"1C_Monthly_Reporters": "Monthly Reporters",
|
| 257 |
+
"1C_Single_Reporters": "Single Reporters",
|
| 258 |
+
"1C_Single_Reporters_USA_and_Peru": "Single Reporters - USA and Peru",
|
| 259 |
+
"_X": "Unspecified"
|
| 260 |
+
}
|
openbb_platform/providers/imf/openbb_imf/assets/imf_symbols.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
openbb_platform/providers/imf/openbb_imf/models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""OpenBB IMF Provider Models."""
|
openbb_platform/providers/imf/openbb_imf/models/available_indicators.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""IMF Available Indicators."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
from typing import Any, Optional, Union
|
| 6 |
+
|
| 7 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 8 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 9 |
+
from openbb_core.provider.standard_models.available_indicators import (
|
| 10 |
+
AvailableIndicatorsData,
|
| 11 |
+
AvailableIndicesQueryParams,
|
| 12 |
+
)
|
| 13 |
+
from pydantic import Field
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class ImfAvailableIndicatorsQueryParams(AvailableIndicesQueryParams):
|
| 17 |
+
"""IMF Available Indicators Query Parameters."""
|
| 18 |
+
|
| 19 |
+
__json_schema_extra__ = {"query": {"multiple_items_allowed": True}}
|
| 20 |
+
|
| 21 |
+
query: Optional[str] = Field(
|
| 22 |
+
default=None,
|
| 23 |
+
description="The query string to search through the available indicators."
|
| 24 |
+
+ " Use semicolons to separate multiple terms.",
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class ImfAvailableIndicatorsData(AvailableIndicatorsData):
|
| 29 |
+
"""IMF Available Indicators Data."""
|
| 30 |
+
|
| 31 |
+
__alias_dict__ = {
|
| 32 |
+
"symbol_root": "parent",
|
| 33 |
+
"description": "title",
|
| 34 |
+
}
|
| 35 |
+
dataset: Optional[str] = Field(
|
| 36 |
+
default=None,
|
| 37 |
+
description="The IMF dataset associated with the symbol.",
|
| 38 |
+
)
|
| 39 |
+
table: Optional[str] = Field(
|
| 40 |
+
default=None,
|
| 41 |
+
description="The name of the table associated with the symbol.",
|
| 42 |
+
)
|
| 43 |
+
level: Optional[int] = Field(
|
| 44 |
+
default=None,
|
| 45 |
+
description="The indentation level of the data, relative to the table and symbol_root",
|
| 46 |
+
)
|
| 47 |
+
order: Optional[Union[int, float]] = Field(
|
| 48 |
+
default=None,
|
| 49 |
+
description="Order of the data, relative to the table.",
|
| 50 |
+
)
|
| 51 |
+
children: Optional[str] = Field(
|
| 52 |
+
default=None,
|
| 53 |
+
description="The symbol of the child data, if any.",
|
| 54 |
+
)
|
| 55 |
+
unit: Optional[str] = Field(
|
| 56 |
+
default=None,
|
| 57 |
+
description="The unit of the data.",
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class ImfAvailableIndicatorsFetcher(
|
| 62 |
+
Fetcher[ImfAvailableIndicatorsQueryParams, list[ImfAvailableIndicatorsData]]
|
| 63 |
+
):
|
| 64 |
+
"""IMF Available Indicators Fetcher."""
|
| 65 |
+
|
| 66 |
+
@staticmethod
|
| 67 |
+
def transform_query(params: dict[str, Any]) -> ImfAvailableIndicatorsQueryParams:
|
| 68 |
+
"""Transform the query."""
|
| 69 |
+
return ImfAvailableIndicatorsQueryParams(**params)
|
| 70 |
+
|
| 71 |
+
@staticmethod
|
| 72 |
+
def extract_data(
|
| 73 |
+
query: ImfAvailableIndicatorsQueryParams,
|
| 74 |
+
credentials: Optional[dict[str, Any]] = None,
|
| 75 |
+
**kwargs: Any,
|
| 76 |
+
) -> list[dict]:
|
| 77 |
+
"""Fetch the data."""
|
| 78 |
+
# pylint: disable=import-outside-toplevel
|
| 79 |
+
from numpy import nan
|
| 80 |
+
from openbb_core.provider.utils.errors import EmptyDataError
|
| 81 |
+
from openbb_imf.utils.constants import load_symbols
|
| 82 |
+
from pandas import DataFrame, Series
|
| 83 |
+
|
| 84 |
+
try:
|
| 85 |
+
all_symbols = load_symbols("all")
|
| 86 |
+
except OpenBBError as e:
|
| 87 |
+
raise OpenBBError(f"Failed to load IMF symbols static file: {e}") from e
|
| 88 |
+
|
| 89 |
+
terms = [term.strip() for term in query.query.split(";")] if query.query else []
|
| 90 |
+
|
| 91 |
+
df = (
|
| 92 |
+
DataFrame(all_symbols)
|
| 93 |
+
.T.reset_index()
|
| 94 |
+
.rename(columns={"index": "symbol"})
|
| 95 |
+
.replace({nan: None})
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
if not terms:
|
| 99 |
+
records = df.to_dict(orient="records")
|
| 100 |
+
else:
|
| 101 |
+
combined_mask = Series([True] * len(df))
|
| 102 |
+
for term in terms:
|
| 103 |
+
mask = df.apply(
|
| 104 |
+
lambda row, term=term: row.astype(str).str.contains(
|
| 105 |
+
term, case=False, regex=True, na=False
|
| 106 |
+
)
|
| 107 |
+
).any(axis=1)
|
| 108 |
+
combined_mask &= mask
|
| 109 |
+
|
| 110 |
+
matches = df[combined_mask]
|
| 111 |
+
|
| 112 |
+
if matches.empty:
|
| 113 |
+
raise EmptyDataError("No results found for the provided query.")
|
| 114 |
+
|
| 115 |
+
records = matches.to_dict(orient="records")
|
| 116 |
+
|
| 117 |
+
return records
|
| 118 |
+
|
| 119 |
+
@staticmethod
|
| 120 |
+
def transform_data(
|
| 121 |
+
query: ImfAvailableIndicatorsQueryParams,
|
| 122 |
+
data: list[dict],
|
| 123 |
+
**kwargs: Any,
|
| 124 |
+
) -> list[ImfAvailableIndicatorsData]:
|
| 125 |
+
"""Transform the data."""
|
| 126 |
+
return [ImfAvailableIndicatorsData.model_validate(d) for d in data]
|
openbb_platform/providers/imf/openbb_imf/models/direction_of_trade.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""IMF Direction Of Trade Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from typing import Any, Optional, Union
|
| 7 |
+
|
| 8 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 9 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 10 |
+
from openbb_core.provider.standard_models.direction_of_trade import (
|
| 11 |
+
DirectionOfTradeData,
|
| 12 |
+
DirectionOfTradeQueryParams,
|
| 13 |
+
)
|
| 14 |
+
from openbb_core.provider.utils.errors import EmptyDataError
|
| 15 |
+
from openbb_imf.utils.dot_helpers import (
|
| 16 |
+
load_country_map,
|
| 17 |
+
load_country_to_code_map,
|
| 18 |
+
validate_countries,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
dot_indicators_dict = {
|
| 22 |
+
"exports": "TXG_FOB_USD",
|
| 23 |
+
"imports": "TMG_CIF_USD",
|
| 24 |
+
"balance": "TBG_USD",
|
| 25 |
+
"all": "TXG_FOB_USD+TMG_CIF_USD+TBG_USD",
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
dot_titles_map = {
|
| 29 |
+
"TXG_FOB_USD": "Goods, Value of Exports, Free on board (FOB), US Dollars",
|
| 30 |
+
"TMG_CIF_USD": "Goods, Value of Imports, Cost, Insurance, Freight (CIF), US Dollars",
|
| 31 |
+
"TBG_USD": "Goods, Value of Trade Balance, US Dollars",
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class ImfDirectionOfTradeQueryParams(DirectionOfTradeQueryParams):
|
| 36 |
+
"""IMF Direction Of Trade Query Parameters."""
|
| 37 |
+
|
| 38 |
+
__json_schema_extra__ = {
|
| 39 |
+
"country": {
|
| 40 |
+
"multiple_items_allowed": True,
|
| 41 |
+
"choices": ["all"] + sorted(list(load_country_to_code_map())),
|
| 42 |
+
},
|
| 43 |
+
"counterpart": {
|
| 44 |
+
"multiple_items_allowed": True,
|
| 45 |
+
"choices": ["all"] + sorted(list(load_country_to_code_map())),
|
| 46 |
+
},
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class ImfDirectionOfTradeData(DirectionOfTradeData):
|
| 51 |
+
"""IMF Direction Of Trade Data."""
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
class ImfDirectionOfTradeFetcher(
|
| 55 |
+
Fetcher[ImfDirectionOfTradeQueryParams, list[ImfDirectionOfTradeData]]
|
| 56 |
+
):
|
| 57 |
+
"""IMF Direction Of Trade Fetcher."""
|
| 58 |
+
|
| 59 |
+
@staticmethod
|
| 60 |
+
def transform_query(params: dict[str, Any]) -> ImfDirectionOfTradeQueryParams:
|
| 61 |
+
"""Transform query parameters."""
|
| 62 |
+
countries = params.get("country", "")
|
| 63 |
+
countries = countries.split(",") if countries else "all"
|
| 64 |
+
if countries != "all":
|
| 65 |
+
countries = validate_countries(countries)
|
| 66 |
+
counterparts = params.get("counterpart", "")
|
| 67 |
+
counterparts = counterparts.split(",") if params.get("counterpart") else "all"
|
| 68 |
+
if counterparts != "all":
|
| 69 |
+
counterparts = validate_countries(counterparts)
|
| 70 |
+
now = datetime.now().date()
|
| 71 |
+
|
| 72 |
+
if countries == "all" and counterparts == "all":
|
| 73 |
+
raise OpenBBError(
|
| 74 |
+
"Both 'country' and 'counterpart' cannot be None, or 'all'."
|
| 75 |
+
+ " Please supply lowercase country names or two-letter ISO codes."
|
| 76 |
+
)
|
| 77 |
+
if countries == counterparts:
|
| 78 |
+
raise OpenBBError("The 'country' and 'counterpart' cannot be the same.")
|
| 79 |
+
|
| 80 |
+
params["country"] = countries
|
| 81 |
+
params["counterpart"] = counterparts
|
| 82 |
+
|
| 83 |
+
if not params.get("end_date"):
|
| 84 |
+
params["end_date"] = now.replace(month=12, day=31).strftime("%Y-%m-%d")
|
| 85 |
+
|
| 86 |
+
if (countries == "all" or counterparts == "all") and not params.get(
|
| 87 |
+
"start_date"
|
| 88 |
+
):
|
| 89 |
+
params["start_date"] = now.replace(year=now.year - 1).strftime("%Y-%m-%d")
|
| 90 |
+
|
| 91 |
+
return ImfDirectionOfTradeQueryParams(**params)
|
| 92 |
+
|
| 93 |
+
@staticmethod
|
| 94 |
+
async def aextract_data(
|
| 95 |
+
query: ImfDirectionOfTradeQueryParams,
|
| 96 |
+
credentials: Optional[dict[str, str]],
|
| 97 |
+
**kwargs: Any,
|
| 98 |
+
) -> list[dict]:
|
| 99 |
+
"""Extract the data from the IMF API."""
|
| 100 |
+
# pylint: disable=import-outside-toplevel
|
| 101 |
+
from aiohttp.client_exceptions import ContentTypeError # noqa
|
| 102 |
+
from json import JSONDecodeError
|
| 103 |
+
from openbb_core.provider.utils.helpers import amake_request
|
| 104 |
+
from pandas import to_datetime
|
| 105 |
+
from pandas.tseries import offsets
|
| 106 |
+
|
| 107 |
+
start_date = query.start_date
|
| 108 |
+
end_date = query.end_date
|
| 109 |
+
frequency = query.frequency[0].upper()
|
| 110 |
+
country = query.country if query.country != "all" else ""
|
| 111 |
+
counterpart = query.counterpart if query.counterpart != "all" else ""
|
| 112 |
+
indicator = dot_indicators_dict[query.direction]
|
| 113 |
+
# Adjust the dates to the date relative to frequency.
|
| 114 |
+
# The API does not accept arbitrary dates, so we need to adjust them.
|
| 115 |
+
if start_date:
|
| 116 |
+
start_date = to_datetime(start_date)
|
| 117 |
+
if frequency == "Q":
|
| 118 |
+
start_date = offsets.QuarterBegin(startingMonth=1).rollback(start_date)
|
| 119 |
+
elif frequency == "A":
|
| 120 |
+
start_date = offsets.YearBegin().rollback(start_date)
|
| 121 |
+
else:
|
| 122 |
+
start_date = offsets.MonthBegin().rollback(start_date)
|
| 123 |
+
start_date = start_date.strftime("%Y-%m-%d") # type: ignore
|
| 124 |
+
|
| 125 |
+
if end_date:
|
| 126 |
+
end_date = to_datetime(end_date)
|
| 127 |
+
if frequency == "Q":
|
| 128 |
+
end_date = offsets.QuarterEnd().rollforward(end_date)
|
| 129 |
+
elif frequency == "A":
|
| 130 |
+
end_date = offsets.YearEnd().rollforward(end_date)
|
| 131 |
+
else:
|
| 132 |
+
end_date = offsets.MonthEnd().rollforward(end_date)
|
| 133 |
+
end_date = end_date.strftime("%Y-%m-%d") # type: ignore
|
| 134 |
+
|
| 135 |
+
date_range = ( # type: ignore
|
| 136 |
+
f"?startPeriod={start_date}&endPeriod={end_date}"
|
| 137 |
+
if start_date and end_date
|
| 138 |
+
else ""
|
| 139 |
+
)
|
| 140 |
+
base_url = "http://dataservices.imf.org/REST/SDMX_JSON.svc/"
|
| 141 |
+
key = f"CompactData/DOT/{frequency}.{country}.{indicator}.{counterpart}"
|
| 142 |
+
url = f"{base_url}{key}{date_range}"
|
| 143 |
+
|
| 144 |
+
try:
|
| 145 |
+
response = await amake_request(url, timeout=20)
|
| 146 |
+
except (JSONDecodeError, ContentTypeError) as e:
|
| 147 |
+
raise OpenBBError(
|
| 148 |
+
"Error fetching data; This might be rate-limiting. Try again later."
|
| 149 |
+
) from e
|
| 150 |
+
|
| 151 |
+
if "ErrorDetails" in response:
|
| 152 |
+
raise OpenBBError(
|
| 153 |
+
f"{response['ErrorDetails'].get('Code')} -> {response['ErrorDetails'].get('Message')}" # type: ignore
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
series = response.get("CompactData", {}).get("DataSet", {}).pop("Series", {}) # type: ignore
|
| 157 |
+
|
| 158 |
+
if not series:
|
| 159 |
+
raise OpenBBError(f"No time series data found -> {url} -> {response}")
|
| 160 |
+
|
| 161 |
+
# If there is only one series, they ruturn a dict instead of a list.
|
| 162 |
+
if series and isinstance(series, dict):
|
| 163 |
+
series = [series]
|
| 164 |
+
|
| 165 |
+
return series
|
| 166 |
+
|
| 167 |
+
@staticmethod
|
| 168 |
+
def transform_data(
|
| 169 |
+
query: ImfDirectionOfTradeQueryParams,
|
| 170 |
+
data: list[dict],
|
| 171 |
+
**kwargs: Any,
|
| 172 |
+
) -> list[ImfDirectionOfTradeData]:
|
| 173 |
+
"""Transform the data."""
|
| 174 |
+
# pylint: disable=import-outside-toplevel
|
| 175 |
+
from openbb_imf.utils.constants import UNIT_MULTIPLIERS_MAP # noqa
|
| 176 |
+
from pandas import Categorical, DataFrame, to_datetime
|
| 177 |
+
from pandas.tseries import offsets
|
| 178 |
+
|
| 179 |
+
if not data:
|
| 180 |
+
raise EmptyDataError()
|
| 181 |
+
|
| 182 |
+
dot_code_to_country = load_country_map()
|
| 183 |
+
series = data
|
| 184 |
+
results: list = []
|
| 185 |
+
|
| 186 |
+
for s in series:
|
| 187 |
+
if "Obs" not in s:
|
| 188 |
+
continue
|
| 189 |
+
meta = {
|
| 190 |
+
k.replace("@", "").lower(): (
|
| 191 |
+
UNIT_MULTIPLIERS_MAP.get(str(v), v) if k == "@UNIT_MULT" else v
|
| 192 |
+
)
|
| 193 |
+
for k, v in s.items()
|
| 194 |
+
if k != "Obs"
|
| 195 |
+
}
|
| 196 |
+
_symbol = meta.get("indicator", "")
|
| 197 |
+
_title = None
|
| 198 |
+
|
| 199 |
+
_data = s.pop("Obs", [])
|
| 200 |
+
|
| 201 |
+
if isinstance(_data, dict):
|
| 202 |
+
_data = [_data]
|
| 203 |
+
|
| 204 |
+
for d in _data:
|
| 205 |
+
_date = d.pop("@TIME_PERIOD", None)
|
| 206 |
+
val: Union[float, None] = d.pop("@OBS_VALUE", None)
|
| 207 |
+
_ = d.pop("@OBS_STATUS", None)
|
| 208 |
+
val = float(val) if val else None
|
| 209 |
+
if not val:
|
| 210 |
+
continue
|
| 211 |
+
|
| 212 |
+
if _date:
|
| 213 |
+
offset = (
|
| 214 |
+
offsets.QuarterEnd
|
| 215 |
+
if "Q" in _date
|
| 216 |
+
else (
|
| 217 |
+
offsets.YearEnd
|
| 218 |
+
if len(str(_date)) == 4
|
| 219 |
+
else offsets.MonthEnd
|
| 220 |
+
)
|
| 221 |
+
)
|
| 222 |
+
_date = to_datetime(_date)
|
| 223 |
+
_date = _date + offset(0)
|
| 224 |
+
_date = _date.strftime("%Y-%m-%d")
|
| 225 |
+
vals = {
|
| 226 |
+
k: v
|
| 227 |
+
for k, v in {
|
| 228 |
+
"date": _date,
|
| 229 |
+
"symbol": _symbol,
|
| 230 |
+
"country": dot_code_to_country.get(
|
| 231 |
+
meta.get("ref_area"), meta.get("ref_area")
|
| 232 |
+
),
|
| 233 |
+
"counterpart": dot_code_to_country.get(
|
| 234 |
+
meta.get("counterpart_area"), meta.get("counterpart_area")
|
| 235 |
+
),
|
| 236 |
+
"title": dot_titles_map.get(_symbol),
|
| 237 |
+
"scale": meta.get("unit_mult"),
|
| 238 |
+
"value": val,
|
| 239 |
+
}.items()
|
| 240 |
+
if v
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
if (
|
| 244 |
+
vals.get("value")
|
| 245 |
+
and vals.get("date")
|
| 246 |
+
and vals.get("country") != vals.get("counterpart")
|
| 247 |
+
):
|
| 248 |
+
d.update(vals)
|
| 249 |
+
|
| 250 |
+
if _data:
|
| 251 |
+
results.extend([d for d in _data if d])
|
| 252 |
+
|
| 253 |
+
df = DataFrame(results)
|
| 254 |
+
df["symbol"] = Categorical(
|
| 255 |
+
df["symbol"],
|
| 256 |
+
categories=list(dot_titles_map),
|
| 257 |
+
ordered=True,
|
| 258 |
+
)
|
| 259 |
+
df["country"] = Categorical(
|
| 260 |
+
df["country"],
|
| 261 |
+
categories=sorted(df.country.unique().tolist()),
|
| 262 |
+
ordered=True,
|
| 263 |
+
)
|
| 264 |
+
df["counterpart"] = Categorical(
|
| 265 |
+
df["counterpart"],
|
| 266 |
+
categories=sorted(df.counterpart.unique().tolist()),
|
| 267 |
+
ordered=True,
|
| 268 |
+
)
|
| 269 |
+
df = df.sort_values(by=["date", "country", "counterpart"])
|
| 270 |
+
|
| 271 |
+
return [
|
| 272 |
+
ImfDirectionOfTradeData.model_validate(r)
|
| 273 |
+
for r in df.to_dict(orient="records")
|
| 274 |
+
]
|
openbb_platform/providers/imf/openbb_imf/models/economic_indicators.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""IMF Economic Indicators Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
from datetime import datetime
|
| 6 |
+
from typing import Any, Dict, List, Literal, Optional, Union
|
| 7 |
+
|
| 8 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 9 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 10 |
+
from openbb_core.provider.standard_models.economic_indicators import (
|
| 11 |
+
EconomicIndicatorsData,
|
| 12 |
+
EconomicIndicatorsQueryParams,
|
| 13 |
+
)
|
| 14 |
+
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
| 15 |
+
from openbb_core.provider.utils.errors import EmptyDataError
|
| 16 |
+
from openbb_imf.utils.constants import (
|
| 17 |
+
FSI_PRESETS,
|
| 18 |
+
IRFCL_PRESET,
|
| 19 |
+
IRFCL_TABLES,
|
| 20 |
+
load_symbols,
|
| 21 |
+
)
|
| 22 |
+
from openbb_imf.utils.irfcl_helpers import (
|
| 23 |
+
load_country_to_code_map,
|
| 24 |
+
validate_countries,
|
| 25 |
+
)
|
| 26 |
+
from pydantic import Field, field_validator
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class ImfEconomicIndicatorsQueryParams(EconomicIndicatorsQueryParams):
|
| 30 |
+
"""IMF Economic Indicators Query."""
|
| 31 |
+
|
| 32 |
+
__json_schema_extra__ = {
|
| 33 |
+
"symbol": {
|
| 34 |
+
"multiple_items_allowed": True,
|
| 35 |
+
},
|
| 36 |
+
"country": {
|
| 37 |
+
"multiple_items_allowed": True,
|
| 38 |
+
"choices": ["all"] + list(list(load_country_to_code_map())),
|
| 39 |
+
},
|
| 40 |
+
"frequency": {
|
| 41 |
+
"choices": ["annual", "quarter", "month"],
|
| 42 |
+
},
|
| 43 |
+
}
|
| 44 |
+
symbol: str = Field(
|
| 45 |
+
default="irfcl_top_lines",
|
| 46 |
+
description=QUERY_DESCRIPTIONS.get("symbol", "")
|
| 47 |
+
+ " Use `available_indicators()` to get the list of available symbols."
|
| 48 |
+
+ " Use 'IRFCL' to get all the data from International Reserves & Foreign Currency Liquidity indicators."
|
| 49 |
+
+ " Use 'core_fsi' to get the core Financial Soundness Indicators."
|
| 50 |
+
+ " Use 'core_fsi_underlying' to include underlying data for the core Financial Soundness Indicators."
|
| 51 |
+
+ " Complete tables are available only by single country, and are keyed as described below."
|
| 52 |
+
+ " The default is 'irfcl_top_lines'. Available presets not listed in `available_indicators()` are:"
|
| 53 |
+
+ """\n
|
| 54 |
+
'IRFCL': All the data from the set of indicators. Not compatible with multiple countries.
|
| 55 |
+
'irfcl_top_lines': The default, top line items from the IRFCL data. Compatible with multiple countries.
|
| 56 |
+
'reserve_assets_and_other_fx_assets': Table I of the IRFCL data. Not compatible with multiple countries.
|
| 57 |
+
'predetermined_drains_on_fx_assets': Table II of the IRFCL data. Not compatible with multiple countries.
|
| 58 |
+
'contingent_drains_fx_assets': Table III of the IRFCL data. Not compatible with multiple countries.
|
| 59 |
+
'memorandum_items': The memorandum items table of the IRFCL data. Not compatible with multiple countries.
|
| 60 |
+
'gold_reserves': Gold reserves as value in USD and Fine Troy Ounces. Compatible with multiple countries.
|
| 61 |
+
'derivative_assets': Net derivative assets as value in USD. Compatible with multipile countries.
|
| 62 |
+
'fsi_core': The core Financial Soundness Indicators. Compatible with multiple countries.
|
| 63 |
+
'fsi_core_underlying': The core FSIs underlying series data. Not compatible with country='all'.
|
| 64 |
+
'fsi_encouraged_set': The encouraged set of Financial Soundness Indicators. Not compatible with country='all'.
|
| 65 |
+
'fsi_other': The other Financial Soundness Indicators. Not compatible with country='all'.
|
| 66 |
+
'fsi_balance_sheets': Data categorized as Balance Sheets and Income Statements. Not compatible with country='all'.
|
| 67 |
+
'fsi_all': All the Financial Soundness Indicators. Not compatible with multiple countries.
|
| 68 |
+
""",
|
| 69 |
+
)
|
| 70 |
+
frequency: Literal["annual", "quarter", "month"] = Field(
|
| 71 |
+
default="quarter",
|
| 72 |
+
description="Frequency of the data, default is 'quarter'.",
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
@field_validator("symbol", mode="before", check_fields=False)
|
| 76 |
+
@classmethod
|
| 77 |
+
def _count_presets(cls, v):
|
| 78 |
+
"""Validate the symbol."""
|
| 79 |
+
if not v:
|
| 80 |
+
return v
|
| 81 |
+
presets = list(IRFCL_PRESET) + FSI_PRESETS
|
| 82 |
+
n_preset = 0
|
| 83 |
+
symbols = v.split(",")
|
| 84 |
+
for symbol in symbols:
|
| 85 |
+
n_preset += 1 if symbol in presets else 0
|
| 86 |
+
if n_preset > 1:
|
| 87 |
+
raise ValueError("only one preset symbol can be used at a time.")
|
| 88 |
+
return v
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class ImfEconomicIndicatorsData(EconomicIndicatorsData):
|
| 92 |
+
"""IMF Economic Indicators Data."""
|
| 93 |
+
|
| 94 |
+
__alias_dict__ = {
|
| 95 |
+
"symbol_root": "parent",
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
unit: Optional[str] = Field(
|
| 99 |
+
default=None,
|
| 100 |
+
description="The unit of the value.",
|
| 101 |
+
)
|
| 102 |
+
scale: Optional[str] = Field(
|
| 103 |
+
default=None,
|
| 104 |
+
description="The scale of the value.",
|
| 105 |
+
)
|
| 106 |
+
table: Optional[str] = Field(
|
| 107 |
+
default=None,
|
| 108 |
+
description="The name of the table associated with the symbol.",
|
| 109 |
+
)
|
| 110 |
+
level: Optional[int] = Field(
|
| 111 |
+
default=None,
|
| 112 |
+
description="The indentation level of the data, relative to the table and symbol_root",
|
| 113 |
+
)
|
| 114 |
+
order: Optional[Union[int, float]] = Field(
|
| 115 |
+
default=None,
|
| 116 |
+
description="Order of the data, relative to the table.",
|
| 117 |
+
)
|
| 118 |
+
reference_sector: Optional[str] = Field(
|
| 119 |
+
default=None,
|
| 120 |
+
description="The reference sector for the data.",
|
| 121 |
+
)
|
| 122 |
+
title: Optional[str] = Field(
|
| 123 |
+
default=None,
|
| 124 |
+
description="The title of the series associated with the symbol.",
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
class ImfEconomicIndicatorsFetcher(
|
| 129 |
+
Fetcher[ImfEconomicIndicatorsQueryParams, List[ImfEconomicIndicatorsData]]
|
| 130 |
+
):
|
| 131 |
+
"""IMF Economic Indicators Fetcher."""
|
| 132 |
+
|
| 133 |
+
@staticmethod
|
| 134 |
+
def transform_query(params: Dict[str, Any]) -> ImfEconomicIndicatorsQueryParams:
|
| 135 |
+
"""Transform the query."""
|
| 136 |
+
symbols = params.get("symbol", "")
|
| 137 |
+
countries = params.get("country")
|
| 138 |
+
now = datetime.now().date()
|
| 139 |
+
symbols = (
|
| 140 |
+
"IRFCL"
|
| 141 |
+
if (("all" in symbols or "IRFCL" in symbols) and "fsi_all" not in symbols)
|
| 142 |
+
else symbols if symbols else "irfcl_top_lines"
|
| 143 |
+
)
|
| 144 |
+
incompatible = (
|
| 145 |
+
"fsi_other" in symbols
|
| 146 |
+
or "fsi_encouraged_set" in symbols
|
| 147 |
+
or "fsi_all" in symbols
|
| 148 |
+
or "fsi_core_underlying" in symbols
|
| 149 |
+
or "fsi_balance_sheets" in symbols
|
| 150 |
+
)
|
| 151 |
+
if (symbols == "IRFCL" or incompatible) and not (
|
| 152 |
+
countries or countries == "all"
|
| 153 |
+
):
|
| 154 |
+
raise OpenBBError(
|
| 155 |
+
f"The selected symbol(s), {params.get('symbol')}, is not compatible with the all-countries group."
|
| 156 |
+
" Please provide country names or two-letter ISO country codes."
|
| 157 |
+
)
|
| 158 |
+
|
| 159 |
+
if countries:
|
| 160 |
+
params["country"] = validate_countries(countries)
|
| 161 |
+
|
| 162 |
+
if symbols and symbols in IRFCL_PRESET:
|
| 163 |
+
params["symbol"] = IRFCL_PRESET[symbols]
|
| 164 |
+
if symbols in IRFCL_TABLES and countries and countries.split(",") > 1:
|
| 165 |
+
raise OpenBBError(
|
| 166 |
+
f"Symbol '{symbols}' is a table and can only be used with one country."
|
| 167 |
+
)
|
| 168 |
+
elif symbols:
|
| 169 |
+
params["symbol"] = symbols
|
| 170 |
+
|
| 171 |
+
if not params.get("start_date") and (not countries or countries == "all"):
|
| 172 |
+
params["start_date"] = now.replace(
|
| 173 |
+
year=now.year - 1, month=1, day=1
|
| 174 |
+
).strftime("%Y-%m-%d")
|
| 175 |
+
|
| 176 |
+
if not params.get("end_date"):
|
| 177 |
+
params["end_date"] = now.replace(month=12, day=31).strftime("%Y-%m-%d")
|
| 178 |
+
|
| 179 |
+
if (not symbols or symbols == "all") and not params.get("start_date"):
|
| 180 |
+
params["start_date"] = now.replace(year=now.year - 1).strftime("%Y-%m-%d")
|
| 181 |
+
|
| 182 |
+
return ImfEconomicIndicatorsQueryParams(**params)
|
| 183 |
+
|
| 184 |
+
@staticmethod
|
| 185 |
+
async def aextract_data(
|
| 186 |
+
query: ImfEconomicIndicatorsQueryParams,
|
| 187 |
+
credentials: Optional[Dict[str, Any]] = None,
|
| 188 |
+
**kwargs: Any,
|
| 189 |
+
) -> List[Dict]:
|
| 190 |
+
"""Extract the data."""
|
| 191 |
+
# pylint: disable = import-outside-toplevel
|
| 192 |
+
from openbb_imf.utils.fsi_helpers import _get_fsi_data # noqa
|
| 193 |
+
from openbb_imf.utils.irfcl_helpers import _get_irfcl_data
|
| 194 |
+
from warnings import warn
|
| 195 |
+
|
| 196 |
+
fsi_symbols = load_symbols("FSI")
|
| 197 |
+
irfcl_symbols = load_symbols("IRFCL")
|
| 198 |
+
symbols = query.symbol.split(",")
|
| 199 |
+
new_symbols_irfcl: Union[list, str] = []
|
| 200 |
+
new_symbols_fsi: Union[list, str] = []
|
| 201 |
+
for symbol in symbols:
|
| 202 |
+
if symbol in list(IRFCL_PRESET) + ["all", "IRFCL"]:
|
| 203 |
+
new_symbols_irfcl = symbol
|
| 204 |
+
elif symbol in FSI_PRESETS:
|
| 205 |
+
new_symbols_fsi = symbol
|
| 206 |
+
elif symbol.upper() in fsi_symbols:
|
| 207 |
+
new_symbols_fsi.append(symbol.upper()) # type: ignore
|
| 208 |
+
elif symbol.upper() in irfcl_symbols:
|
| 209 |
+
new_symbols_irfcl.append(symbol.upper()) # type: ignore
|
| 210 |
+
|
| 211 |
+
if not new_symbols_irfcl and not new_symbols_fsi:
|
| 212 |
+
raise OpenBBError(
|
| 213 |
+
f"No valid symbols found -> {query.symbol} -> "
|
| 214 |
+
"Use 'available_indicators(provider='imf')' to get the list of available symbols."
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
results: list = []
|
| 218 |
+
exceptions: list = []
|
| 219 |
+
try:
|
| 220 |
+
try:
|
| 221 |
+
if new_symbols_irfcl:
|
| 222 |
+
_kwargs = query.model_dump(exclude_none=True)
|
| 223 |
+
_kwargs["symbol"] = new_symbols_irfcl
|
| 224 |
+
results.extend(await _get_irfcl_data(**_kwargs))
|
| 225 |
+
except (EmptyDataError, OpenBBError) as e:
|
| 226 |
+
if new_symbols_fsi:
|
| 227 |
+
exceptions.append(
|
| 228 |
+
f"IRFCL dataset error -> {e.__class__.__name__}: {e}"
|
| 229 |
+
)
|
| 230 |
+
else:
|
| 231 |
+
raise
|
| 232 |
+
if new_symbols_fsi:
|
| 233 |
+
try:
|
| 234 |
+
_kwargs = query.model_dump(exclude_none=True)
|
| 235 |
+
_kwargs["symbol"] = new_symbols_fsi
|
| 236 |
+
results.extend(await _get_fsi_data(**_kwargs))
|
| 237 |
+
except (EmptyDataError, OpenBBError) as e:
|
| 238 |
+
if new_symbols_irfcl and len(results) > 0:
|
| 239 |
+
exceptions.append(
|
| 240 |
+
f"FSI dataset error -> {e.__class__.__name__}: {e}"
|
| 241 |
+
)
|
| 242 |
+
elif not new_symbols_irfcl:
|
| 243 |
+
raise
|
| 244 |
+
except OpenBBError as exc:
|
| 245 |
+
raise exc from exc
|
| 246 |
+
|
| 247 |
+
if not results:
|
| 248 |
+
raise EmptyDataError("No results returned for the query.")
|
| 249 |
+
|
| 250 |
+
if results and exceptions:
|
| 251 |
+
msgs = "\n".join(exceptions)
|
| 252 |
+
warn("An error occurred while fetching the data -> " + msgs)
|
| 253 |
+
|
| 254 |
+
return results
|
| 255 |
+
|
| 256 |
+
@staticmethod
|
| 257 |
+
def transform_data(
|
| 258 |
+
query: ImfEconomicIndicatorsQueryParams,
|
| 259 |
+
data: List[Dict],
|
| 260 |
+
**kwargs: Any,
|
| 261 |
+
) -> List[ImfEconomicIndicatorsData]:
|
| 262 |
+
"""Transform the data."""
|
| 263 |
+
# pylint: disable = import-outside-toplevel
|
| 264 |
+
from numpy import nan
|
| 265 |
+
from pandas import Categorical, DataFrame
|
| 266 |
+
|
| 267 |
+
if not data:
|
| 268 |
+
raise EmptyDataError("The data is empty.")
|
| 269 |
+
|
| 270 |
+
all_symbols = {
|
| 271 |
+
**load_symbols("all"),
|
| 272 |
+
}
|
| 273 |
+
df = DataFrame(data)
|
| 274 |
+
|
| 275 |
+
if df.empty:
|
| 276 |
+
raise EmptyDataError("The data is empty.")
|
| 277 |
+
|
| 278 |
+
df = df[df["symbol"].isin(all_symbols)]
|
| 279 |
+
|
| 280 |
+
if len(df) == 0:
|
| 281 |
+
raise OpenBBError("The data has a length of 0.")
|
| 282 |
+
|
| 283 |
+
df["symbol"] = Categorical(
|
| 284 |
+
df["symbol"],
|
| 285 |
+
categories=all_symbols,
|
| 286 |
+
ordered=True,
|
| 287 |
+
)
|
| 288 |
+
df["parent"] = Categorical(
|
| 289 |
+
df["parent"],
|
| 290 |
+
categories=all_symbols,
|
| 291 |
+
ordered=True,
|
| 292 |
+
)
|
| 293 |
+
df = df.sort_values(
|
| 294 |
+
by=["date", "parent", "symbol", "value"],
|
| 295 |
+
ascending=[True, True, True, False],
|
| 296 |
+
).reset_index(drop=True)
|
| 297 |
+
|
| 298 |
+
df.loc[:, "title"] = df.symbol.apply(
|
| 299 |
+
lambda x: all_symbols.get(x, {}).get("title")
|
| 300 |
+
)
|
| 301 |
+
records = df.replace({nan: None}).to_dict(orient="records")
|
| 302 |
+
|
| 303 |
+
return [ImfEconomicIndicatorsData.model_validate(r) for r in records]
|
openbb_platform/providers/imf/openbb_imf/utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""IMF Utilities."""
|
openbb_platform/providers/imf/openbb_imf/utils/constants.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""IMF Constants."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=line-too-long
|
| 4 |
+
# flake8: noqa: E501
|
| 5 |
+
|
| 6 |
+
FSI_PRESETS = [
|
| 7 |
+
"fsi_core",
|
| 8 |
+
"fsi_core_underlying",
|
| 9 |
+
"fsi_other",
|
| 10 |
+
"fsi_encouraged_set",
|
| 11 |
+
"fsi_balance_sheets",
|
| 12 |
+
"fsi_all",
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
IRFCL_HEADLINE = "RAF_USD,RAFA_USD,RAFAFX_USD,RAOFA_USD,RAPFA_USD,RAFAIMF_USD,RAFASDR_USD,RAFAGOLD_USD,RACFA_USD,RAMDCD_USD,RAMFIFC_USD,RAMSR_USD"
|
| 16 |
+
|
| 17 |
+
RESERVE_ASSETS_AND_OTHER_FX_ASSETS = "RAF_USD,RAFA_USD,RAFAFX_USD,RAFAFXS_USD,RAFAFXSI_USD,RAFAFXCD_USD,RAFAFXCDN_USD,RAFAFXCDBI_USD,RAFAFXCDBIA_USD,RAFAFXCDBO_USD,RAFAFXCDBOA_USD,RAFAIMF_USD,RAFASDR_USD,RAFAGOLD_USD,RAFAGOLDV_OZT,RAFAO_USD,RAFAOF_USD,RAFAOL_USD,RAFAOO_USD,RAOFA_USD,RAOFAS_USD,RAOFAD_USD,RAOFAL_USD,RAOFAF_USD,RAOFAG_USD"
|
| 18 |
+
|
| 19 |
+
PREDETERMINED_DRAINS_ON_FX_ASSETS = "RAPFA_USD,RAPFALSD_USD,RAPFALSD_1M_USD,RAPFALSD_1M_3M_USD,RAPFALSD_3M_1Y_USD,RAPFALSDOP_USD,RAPFALSDOP_1M_USD,RAPFALSDOP_1M_3M_USD,RAPFALSDOP_3M_1Y_USD,RAPFALSDOI_USD,RAPFALSDOI_1M_USD,RAPFALSDOI_1M_3M_USD,RAPFALSDOI_3M_1Y_USD,RAPFALSDIP_USD,RAPFALSDIP_1M_USD,RAPFALSDIP_1M_3M_USD,RAPFALSDIP_3M_1Y_USD,RAPFALSDII_USD,RAPFALSDII_1M_USD,RAPFALSDII_1M_3M_USD,RAPFALSDII_3M_1Y_USD,RAPFAFFS_USD,RAPFAFFS_1M_USD,RAPFAFFS_1M_3M_USD,RAPFAFFS_3M_1Y_USD,RAPFAFFL_USD,RAPFAFFL_1M_USD,RAPFAFFL_1M_3M_USD,RAPFAFFL_3M_1Y_USD,RAPFAO_USD,RAPFAO_1M_USD,RAPFAO_1M_3M_USD,RAPFAO_3M_1Y_USD,RAPFAOOR_USD,RAPFAOOR_1M_USD,RAPFAOOR_1M_3M_USD,RAPFAOOR_3M_1Y_USD,RAPFAOIRR_USD,RAPFAOIRR_1M_USD,RAPFAOIRR_1M_3M_USD,RAPFAOIRR_3M_1Y_USD,RAPFAOOC_USD,RAPFAOOC_1M_USD,RAPFAOOC_1M_3M_USD,RAPFAOOC_3M_1Y_USD,RAPFAOIC_USD,RAPFAOIC_1M_USD,RAPFAOIC_1M_3M_USD,RAPFAOIC_3M_1Y_USD,RAPFAOOP_USD,RAPFAOOP_1M_USD,RAPFAOOP_1M_3M_USD,RAPFAOOP_3M_1Y_USD,RAPFAOIR_USD,RAPFAOIR_1M_USD,RAPFAOIR_1M_3M_USD,RAPFAOIR_3M_1Y_USD,RAFA_RAPFA_RO"
|
| 20 |
+
|
| 21 |
+
CONTINGENT_DRAINS_FX_ASSETS = "RACFA_USD,RACFAL_USD,RACFAL_1M_USD,RACFAL_1M_3M_USD,RACFAL_3M_1Y_USD,RACFALG_USD,RACFALG_1M_USD,RACFALG_1M_3M_USD,RACFALO_USD,RACFALO_1M_USD,RACFALO_1M_3M_USD,RACFALO_3M_1Y_USD,RACFAS_USD,RACFACB_USD,RACFACB_1M_USD,RACFACB_1M_3M_USD,RACFACB_3M_1Y_USD,RACFACBA_USD,RACFACBA_1M_USD,RACFACBA_1M_3M_USD,RACFACBA_3M_1Y_USD,RACFACBAOI_USD,RACFACBAOI_1M_USD,RACFACBAOI_1M_3M_USD,RACFACBAON_USD,RACFACBAON_1M_USD,RACFACBAON_1M_3M_USD,RACFACBAON_3M_1Y_USD,RACFACBABIS_USD,RACFACBAIMF_1M_USD,RACFACBAIMF_1M_3M_USD,RACFACBAIMF_3M_1Y_USD,RACFACBAIMF_USD,RACFACBFIR_USD,RACFACBFIR_1M_USD,RACFACBFIR_1M_3M_USD,RACFACBFIR_3M_1Y_USD,RACFACBFIO_USD,RACFACBFIO_1M_USD,RACFACBFIO_1M_3M_USD,RACFACBFIO_3M_1Y_USD,RACFACT_USD,RACFACT_1M_USD,RACFACT_1M_3M_USD,RACFACT_3M_1Y_USD,RACFACTA_USD,RACFACTA_1M_USD,RACFACTA_1M_3M_USD,RACFACTA_3M_1Y_USD,RACFACTAOI_USD,RACFACTAOI_1M_USD,RACFACTAOI_1M_3M_USD,RACFACTAOI_3M_1Y_USD,RACFACTAON_USD,RACFACTAON_1M_USD,RACFACTAON_1M_3M_USD,RACFACTAON_3M_1Y_USD,RACFACTABIS_USD,RACFACTABIS_1M_USD,RACFACTABIS_1M_3M_USD,RACFACTABIS_3M_1Y_USD,RACFACTAIMF_USD,RACFACTAIMF_1M_USD,RACFACTFIR_USD,RACFACTFIR_1M_USD,RACFACTFIR_1M_3M_USD,RACFACTFIR_3M_1Y_USD,RACFACTFIO_USD,RACFACTFIO_1M_USD,RACFACTFIO_1M_3M_USD,RACFACTFIO_3M_1Y_USD,RACFAPPS_USD,RACFAPPS_1M_USD,RACFAPPS_1M_3M_USD,RACFAPPS_3M_1Y_USD,RACFAPPSBP_USD,RACFAPPSBP_1M_USD,RACFAPPSBP_1M_3M_USD,RACFAPPSBP_3M_1Y_USD,RACFAPPSWC_USD,RACFAPPSWC_1M_USD,RACFAPPSWC_1M_3M_USD,RACFAPPSWC_3M_1Y_USD,RACFAPPL_USD,RACFAPPL_1M_USD,RACFAPPL_1M_3M_USD,RACFAPPL_3M_1Y_USD,RACFAPPLBC_USD,RACFAPPLBC_1M_USD,RACFAPPLBC_1M_3M_USD,RACFAPPLBC_3M_1Y_USD,RACFAPPLWP_USD,RACFAPPLWP_1M_USD,RACFAPPLWP_1M_3M_USD,RACFAPPLWP_3M_1Y_USD,RACFAMPAS_USD,RACFAMPAS_1M_USD,RACFAMPAS_1M_3M_USD,RACFAMPAS_3M_1Y_USD,RACFAMPAL_USD,RACFAMPAL_1M_USD,RACFAMPAL_1M_3M_USD,RACFAMPAL_3M_1Y_USD,RACFAMPBS_USD,RACFAMPBS_1M_USD,RACFAMPBS_1M_3M_USD,RACFAMPBS_3M_1Y_USD,RACFAMPBL_USD,RACFAMPBL_1M_USD,RACFAMPBL_1M_3M_USD,RACFAMPBL_3M_1Y_USD,RACFAMPCS_USD,RACFAMPCS_1M_USD,RACFAMPCS_1M_3M_USD,RACFAMPCS_3M_1Y_USD,RACFAMPCL_USD,RACFAMPCL_1M_USD,RACFAMPCL_1M_3M_USD,RACFAMPCL_3M_1Y_USD,RACFAMPDS_USD,RACFAMPDS_1M_USD,RACFAMPDS_1M_3M_USD,RACFAMPDS_3M_1Y_USD,RACFAMPDL_USD,RACFAMPDL_1M_USD,RACFAMPDL_1M_3M_USD,RACFAMPDL_3M_1Y_USD,RACFAMPES_USD,RACFAMPES_1M_USD,RACFAMPES_1M_3M_USD,RACFAMPES_3M_1Y_USD,RACFAMPEL_USD,RACFAMPEL_1M_USD,RACFAMPEL_1M_3M_USD,RACFAMPEL_3M_1Y_USD,RACFAMPFS_USD,RACFAMPFS_1M_USD,RACFAMPFS_1M_3M_USD,RACFAMPFS_3M_1Y_USD,RACFAMPFL_USD,RACFAMPFL_1M_USD,RACFAMPFL_1M_3M_USD,RACFAMPFL_3M_1Y_USD"
|
| 22 |
+
|
| 23 |
+
IRFCL_MEMORANDUM_ITEMS = "RAMDCD_USD,RAMFIFC_USD,RAMPA_USD,RAMFFS_USD,RAMPAOA_USD,RAMSR_USD,RAMSRLRI_USD,RAMSRLRN_USD,RAMSRBRI_USD,RAMSRBAN_USD,RAMFDA_USD,RAMFDAF_USD,RAMFDAU_USD,RAMFDAW_USD,RAMFDAP_USD,RAMFDAO_USD,RAMFFL_USD,RAMPPS_USD,RAMPPSBP_USD,RAMPPSWC_USD,RAMPPL_USD,RAMPPLBP_USD,RAMPPLWC_USD,RAMCR_USD,RAMCRISDR_USD,RAMCRIC_USD_USD,RAMCRIC_EUR_USD,RAMCRIC_CNY_USD,RAMCRIC_JPY_USD,RAMCRIC_GBP_USD,RAMCROSDR_USD"
|
| 24 |
+
|
| 25 |
+
IRFCL_TABLES = {
|
| 26 |
+
"reserve_assets_and_other_fx_assets": RESERVE_ASSETS_AND_OTHER_FX_ASSETS,
|
| 27 |
+
"predetermined_drains_on_fx_assets": PREDETERMINED_DRAINS_ON_FX_ASSETS,
|
| 28 |
+
"contingent_drains_fx_assets": CONTINGENT_DRAINS_FX_ASSETS,
|
| 29 |
+
"memorandum_items": IRFCL_MEMORANDUM_ITEMS,
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
IRFCL_PRESET = {
|
| 33 |
+
"irfcl_top_lines": IRFCL_HEADLINE,
|
| 34 |
+
**IRFCL_TABLES,
|
| 35 |
+
"gold_reserves": "RAFAGOLD_USD,RAFAGOLDV_OZT",
|
| 36 |
+
"derivative_assets": "RAMFDA_USD",
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
FREQUENCY_DICT = {
|
| 40 |
+
"month": "M",
|
| 41 |
+
"quarter": "Q",
|
| 42 |
+
"annual": "A",
|
| 43 |
+
}
|
| 44 |
+
REF_SECTORS_DICT = {
|
| 45 |
+
"government_ex_social_security": "S1311",
|
| 46 |
+
"central_bank": "S121",
|
| 47 |
+
"monetary_authorities": "S1X",
|
| 48 |
+
"all_sectors": "",
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
SECTOR_MAP = {v: k for k, v in REF_SECTORS_DICT.items()}
|
| 52 |
+
|
| 53 |
+
UNIT_MULTIPLIERS_MAP = {
|
| 54 |
+
"0": "Units",
|
| 55 |
+
"2": "Hundreds",
|
| 56 |
+
"3": "Thousands",
|
| 57 |
+
"6": "Millions",
|
| 58 |
+
"9": "Billions",
|
| 59 |
+
"12": "Trillions",
|
| 60 |
+
"N15": "Quadrillionths",
|
| 61 |
+
"N14": "Hundred Trillionths",
|
| 62 |
+
"N13": "Ten Trillionths",
|
| 63 |
+
"N12": "Trillionths",
|
| 64 |
+
"N11": "Hundred Billionths",
|
| 65 |
+
"N10": "Ten Billionths",
|
| 66 |
+
"N9": "Billionths",
|
| 67 |
+
"N8": "Hundred Millionths",
|
| 68 |
+
"N7": "Ten Millionths",
|
| 69 |
+
"N6": "Millionths",
|
| 70 |
+
"N5": "Hundred Thousandths",
|
| 71 |
+
"N4": "Ten Thousandths",
|
| 72 |
+
"N3": "Thousandths",
|
| 73 |
+
"N2": "Hundredths",
|
| 74 |
+
"N1": "Tenths",
|
| 75 |
+
"1": "Tens",
|
| 76 |
+
"4": "Ten Thousands",
|
| 77 |
+
"5": "Hundred Thousands",
|
| 78 |
+
"7": "Ten Millions",
|
| 79 |
+
"8": "Hundred Millions",
|
| 80 |
+
"10": "Ten Billions",
|
| 81 |
+
"11": "Hundred Billions",
|
| 82 |
+
"13": "Ten Trillions",
|
| 83 |
+
"14": "Hundred Trillions",
|
| 84 |
+
"15": "Quadrillions",
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
TIME_FORMAT_MAP = {
|
| 88 |
+
"P1Y": "Annual",
|
| 89 |
+
"P6M": "Bi-annual",
|
| 90 |
+
"P3M": "Quarterly",
|
| 91 |
+
"P1M": "Monthly",
|
| 92 |
+
"P7D": "Weekly",
|
| 93 |
+
"P1D": "Daily",
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
REF_SECTOR_MAP = {
|
| 97 |
+
"S1311": "Central government excluding social security",
|
| 98 |
+
"S121": "Central Bank",
|
| 99 |
+
"S1X": "Monetary Authorities",
|
| 100 |
+
"1C_AS": "All Sectors",
|
| 101 |
+
"AllSectorsIncludingAllSectors": "All Sectors Including All Sectors",
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def load_symbols(dataset: str) -> dict:
|
| 106 |
+
"""Load IMF symbol list."""
|
| 107 |
+
# pylint: disable=import-outside-toplevel
|
| 108 |
+
import json # noqa
|
| 109 |
+
from json.decoder import JSONDecodeError
|
| 110 |
+
from pathlib import Path
|
| 111 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 112 |
+
|
| 113 |
+
try:
|
| 114 |
+
symbols_file = Path(__file__).parents[1].joinpath("assets", "imf_symbols.json")
|
| 115 |
+
with symbols_file.open(encoding="utf-8") as file:
|
| 116 |
+
symbols = json.load(file)
|
| 117 |
+
except (FileNotFoundError, JSONDecodeError) as e:
|
| 118 |
+
raise OpenBBError(
|
| 119 |
+
f"Failed to load IMF symbols from the static file: {e}"
|
| 120 |
+
) from e
|
| 121 |
+
|
| 122 |
+
if dataset == "all":
|
| 123 |
+
return symbols
|
| 124 |
+
|
| 125 |
+
return {k: v for k, v in symbols.items() if v["dataset"] == dataset}
|
openbb_platform/providers/imf/openbb_imf/utils/dot_helpers.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Direction Of Trade Utilities."""
|
| 2 |
+
|
| 3 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def load_country_map() -> dict:
|
| 7 |
+
"""Load IMF IRFCL country map."""
|
| 8 |
+
# pylint: disable=import-outside-toplevel
|
| 9 |
+
import json # noqa
|
| 10 |
+
from json.decoder import JSONDecodeError
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
country_map_file = (
|
| 15 |
+
Path(__file__).parents[1].joinpath("assets", "imf_country_map.json")
|
| 16 |
+
)
|
| 17 |
+
with country_map_file.open(encoding="utf-8") as file:
|
| 18 |
+
country_map_dict = json.load(file)
|
| 19 |
+
except (FileNotFoundError, JSONDecodeError) as e:
|
| 20 |
+
raise OpenBBError(f"Failed to load IMF DOT country map: {e}") from e
|
| 21 |
+
|
| 22 |
+
return country_map_dict
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def load_country_to_code_map() -> dict:
|
| 26 |
+
"""Load a map of lowercase country name to 2-letter ISO symbol."""
|
| 27 |
+
# pylint: disable=import-outside-toplevel
|
| 28 |
+
from warnings import warn # noqa
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
return {
|
| 32 |
+
(
|
| 33 |
+
"euro_area"
|
| 34 |
+
if k == "U2"
|
| 35 |
+
else v.lower()
|
| 36 |
+
.replace(".", "")
|
| 37 |
+
.replace(",", "")
|
| 38 |
+
.replace(":", "")
|
| 39 |
+
.split("(")[0]
|
| 40 |
+
.strip()
|
| 41 |
+
.replace(" ", "_")
|
| 42 |
+
): k
|
| 43 |
+
for k, v in load_country_map().items()
|
| 44 |
+
if not k.startswith("1C_ALL")
|
| 45 |
+
and "Report" not in v
|
| 46 |
+
and k not in ("GW", "_X")
|
| 47 |
+
}
|
| 48 |
+
except OpenBBError:
|
| 49 |
+
warn(f"Failed to load country to code map. -> {OpenBBError}")
|
| 50 |
+
return {}
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def validate_countries(countries: list[str]) -> str:
|
| 54 |
+
"""Validate the list of countries."""
|
| 55 |
+
# pylint: disable=import-outside-toplevel
|
| 56 |
+
from warnings import warn
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
country_to_code_map = load_country_to_code_map()
|
| 60 |
+
except OpenBBError as e:
|
| 61 |
+
raise OpenBBError(f"Failed to load country to code map: {e}") from e
|
| 62 |
+
if not country_to_code_map:
|
| 63 |
+
raise OpenBBError("Failed to load country to code map.")
|
| 64 |
+
|
| 65 |
+
new_countries: list = []
|
| 66 |
+
|
| 67 |
+
for country in countries:
|
| 68 |
+
if country == "all":
|
| 69 |
+
return country
|
| 70 |
+
if country in country_to_code_map:
|
| 71 |
+
new_countries.append(country_to_code_map.get(country))
|
| 72 |
+
elif country.upper() in country_to_code_map.values():
|
| 73 |
+
new_countries.append(country.upper())
|
| 74 |
+
elif country.lower() == "eu":
|
| 75 |
+
new_countries.append(country_to_code_map.get("european_union"))
|
| 76 |
+
elif country.lower() == "ea":
|
| 77 |
+
new_countries.append(country_to_code_map.get("euro_area"))
|
| 78 |
+
else:
|
| 79 |
+
warn(f"Invalid country {country}")
|
| 80 |
+
continue
|
| 81 |
+
|
| 82 |
+
if not new_countries:
|
| 83 |
+
raise OpenBBError(
|
| 84 |
+
f"No valid countries found. Please choose from {list(country_to_code_map)}"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
return "+".join(new_countries)
|
openbb_platform/providers/imf/openbb_imf/utils/fsi_helpers.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""IMF FSI Data Set Helpers."""
|
| 2 |
+
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def load_fsi_symbols() -> dict:
|
| 7 |
+
"""Load IMF FSI symbols."""
|
| 8 |
+
# pylint: disable=import-outside-toplevel
|
| 9 |
+
from openbb_imf.utils.constants import load_symbols
|
| 10 |
+
|
| 11 |
+
return load_symbols("FSI")
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def validate_symbols(symbols) -> str:
|
| 15 |
+
"""Validate the IMF FSI symbols."""
|
| 16 |
+
# pylint: disable=import-outside-toplevel
|
| 17 |
+
from warnings import warn # noqa
|
| 18 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 19 |
+
from openbb_imf.utils.constants import FSI_PRESETS
|
| 20 |
+
|
| 21 |
+
fsi_symbols = load_fsi_symbols()
|
| 22 |
+
|
| 23 |
+
if isinstance(symbols, str):
|
| 24 |
+
symbols = symbols.split(",")
|
| 25 |
+
elif not isinstance(symbols, list):
|
| 26 |
+
raise OpenBBError("Invalid symbols list.")
|
| 27 |
+
|
| 28 |
+
new_symbols: list = []
|
| 29 |
+
|
| 30 |
+
for symbol in symbols:
|
| 31 |
+
if symbol in FSI_PRESETS:
|
| 32 |
+
return symbol
|
| 33 |
+
if symbol.upper() not in fsi_symbols:
|
| 34 |
+
warn(f"Unsupported IMF FSI symbol: {symbol}")
|
| 35 |
+
new_symbols.append(symbol.upper())
|
| 36 |
+
|
| 37 |
+
return "+".join(new_symbols) if len(new_symbols) > 1 else new_symbols[0]
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
|
| 41 |
+
async def _get_fsi_data(**kwargs) -> list[dict]: # noqa:PLR0912
|
| 42 |
+
"""Get IMF FSI data.
|
| 43 |
+
This function is not intended to be called directly,
|
| 44 |
+
but through the `ImfEconomicIndicatorsFetcher` class.
|
| 45 |
+
"""
|
| 46 |
+
# pylint: disable=import-outside-toplevel
|
| 47 |
+
from datetime import date # noqa
|
| 48 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 49 |
+
from openbb_imf.utils.helpers import get_data
|
| 50 |
+
from openbb_imf.utils.irfcl_helpers import load_country_to_code_map
|
| 51 |
+
from openbb_imf.utils import constants
|
| 52 |
+
from pandas import to_datetime
|
| 53 |
+
from pandas.tseries import offsets
|
| 54 |
+
|
| 55 |
+
countries = kwargs.get("country", "")
|
| 56 |
+
countries = (
|
| 57 |
+
"" if countries == "all" else countries.replace(",", "+") if countries else ""
|
| 58 |
+
)
|
| 59 |
+
frequency = constants.FREQUENCY_DICT.get(kwargs.get("frequency", "quarter"), "Q")
|
| 60 |
+
start_date = kwargs.get("start_date", "")
|
| 61 |
+
end_date = kwargs.get("end_date", "")
|
| 62 |
+
all_symbols = load_fsi_symbols()
|
| 63 |
+
core_only = [k for k, v in all_symbols.items() if v.get("table") == "fsi_core"]
|
| 64 |
+
encouraged_set = [
|
| 65 |
+
k for k, v in all_symbols.items() if v.get("table") == "fsi_encouraged_set"
|
| 66 |
+
]
|
| 67 |
+
indicator = kwargs.get("symbol")
|
| 68 |
+
indicators = (
|
| 69 |
+
"+".join(core_only if indicator == "fsi_core" else encouraged_set)
|
| 70 |
+
if indicator in ["fsi_core", "fsi_encouraged_set"]
|
| 71 |
+
else (
|
| 72 |
+
""
|
| 73 |
+
if indicator
|
| 74 |
+
in ["fsi_other", "fsi_all", "fsi_core_underlying", "fsi_balance_sheets"]
|
| 75 |
+
else validate_symbols(indicator)
|
| 76 |
+
)
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
if not indicators and not countries:
|
| 80 |
+
raise OpenBBError(
|
| 81 |
+
"All countries not supported for this group of indicators. Please supply a country."
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
if not start_date and not end_date and not countries:
|
| 85 |
+
start_date = (
|
| 86 |
+
date.today().replace(year=date.today().year - 1).strftime("%Y-%m-%d")
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
if start_date and not end_date:
|
| 90 |
+
end_date = date.today().strftime("%Y-%m-%d")
|
| 91 |
+
|
| 92 |
+
# Adjust the dates to the date relative to frequency.
|
| 93 |
+
# The API does not accept arbitrary dates, so we need to adjust them.
|
| 94 |
+
if start_date:
|
| 95 |
+
start_date = to_datetime(start_date)
|
| 96 |
+
if frequency == "Q":
|
| 97 |
+
start_date = offsets.QuarterBegin(startingMonth=1).rollback(start_date)
|
| 98 |
+
elif frequency == "A":
|
| 99 |
+
start_date = offsets.YearBegin().rollback(start_date)
|
| 100 |
+
else:
|
| 101 |
+
start_date = offsets.MonthBegin().rollback(start_date)
|
| 102 |
+
start_date = start_date.strftime("%Y-%m-%d")
|
| 103 |
+
|
| 104 |
+
if end_date:
|
| 105 |
+
end_date = to_datetime(end_date)
|
| 106 |
+
if frequency == "Q":
|
| 107 |
+
end_date = offsets.QuarterEnd().rollforward(end_date)
|
| 108 |
+
elif frequency == "A":
|
| 109 |
+
end_date = offsets.YearEnd().rollforward(end_date)
|
| 110 |
+
else:
|
| 111 |
+
end_date = offsets.MonthEnd().rollforward(end_date)
|
| 112 |
+
end_date = end_date.strftime("%Y-%m-%d")
|
| 113 |
+
|
| 114 |
+
date_range = (
|
| 115 |
+
f"?startPeriod={start_date}&endPeriod={end_date}"
|
| 116 |
+
if start_date and end_date
|
| 117 |
+
else ""
|
| 118 |
+
)
|
| 119 |
+
base_url = "http://dataservices.imf.org/REST/SDMX_JSON.svc/"
|
| 120 |
+
key = f"CompactData/FSI/{frequency}.{countries}.{indicators}"
|
| 121 |
+
url = f"{base_url}{key}{date_range}"
|
| 122 |
+
series = await get_data(url)
|
| 123 |
+
data: list = []
|
| 124 |
+
all_symbols = load_fsi_symbols()
|
| 125 |
+
|
| 126 |
+
if indicator in ["fsi_core", "fsi_encouraged_set"]:
|
| 127 |
+
all_symbols = {
|
| 128 |
+
k: v for k, v in all_symbols.items() if v.get("table") == indicator
|
| 129 |
+
}
|
| 130 |
+
elif indicator == "fsi_core_underlying":
|
| 131 |
+
all_symbols = {
|
| 132 |
+
k: v
|
| 133 |
+
for k, v in all_symbols.items()
|
| 134 |
+
if "core set" in v.get("title", "").lower() and v.get("unit") != "Percent"
|
| 135 |
+
}
|
| 136 |
+
elif indicator == "fsi_balance_sheets":
|
| 137 |
+
all_symbols = {
|
| 138 |
+
k: v
|
| 139 |
+
for k, v in all_symbols.items()
|
| 140 |
+
if "balance sheets" in v.get("title", "").lower()
|
| 141 |
+
}
|
| 142 |
+
elif indicator == "fsi_other":
|
| 143 |
+
all_symbols = {
|
| 144 |
+
k: v
|
| 145 |
+
for k, v in all_symbols.items()
|
| 146 |
+
if "Additional FSIs" in v.get("title", "")
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
country_map_dict = {
|
| 150 |
+
v: k.replace("_", " ").title().replace("Ecb", "ECB")
|
| 151 |
+
for k, v in load_country_to_code_map().items()
|
| 152 |
+
}
|
| 153 |
+
# Iterate over the series to extract observations and map the metadata.
|
| 154 |
+
for s in series:
|
| 155 |
+
if "Obs" not in s:
|
| 156 |
+
continue
|
| 157 |
+
meta = {
|
| 158 |
+
k.replace("@", "").lower(): (
|
| 159 |
+
constants.UNIT_MULTIPLIERS_MAP.get(str(v), v)
|
| 160 |
+
if k == "@UNIT_MULT"
|
| 161 |
+
else v
|
| 162 |
+
)
|
| 163 |
+
for k, v in s.items()
|
| 164 |
+
if k != "Obs"
|
| 165 |
+
}
|
| 166 |
+
_symbol = meta.get("indicator")
|
| 167 |
+
_parent: Optional[str] = None
|
| 168 |
+
_order: Optional[str] = None
|
| 169 |
+
_level: Optional[str] = None
|
| 170 |
+
_table: Optional[str] = None
|
| 171 |
+
_title: Optional[str] = None
|
| 172 |
+
_unit: Optional[str] = None
|
| 173 |
+
|
| 174 |
+
if _symbol not in all_symbols:
|
| 175 |
+
continue
|
| 176 |
+
|
| 177 |
+
_table = all_symbols.get(_symbol, {}).get("table", "")
|
| 178 |
+
if _table and indicator == "fsi_core_underlying":
|
| 179 |
+
_table = _table.replace("fsi_other", "fsi_core_underlying") # type: ignore
|
| 180 |
+
_parent = all_symbols.get(_symbol, {}).get("parent", "")
|
| 181 |
+
_order = all_symbols.get(_symbol, {}).get("order", "")
|
| 182 |
+
_level = all_symbols.get(_symbol, {}).get("level", "")
|
| 183 |
+
_title = all_symbols.get(_symbol, {}).get("title", "")
|
| 184 |
+
if _title:
|
| 185 |
+
_title = " - ".join(_title.split(", ")[1:-1]).replace(
|
| 186 |
+
"Financial Soundness Indicators - ", ""
|
| 187 |
+
)
|
| 188 |
+
_unit = all_symbols.get(_symbol, {}).get("unit", "")
|
| 189 |
+
|
| 190 |
+
_data = s.pop("Obs", [])
|
| 191 |
+
|
| 192 |
+
if isinstance(_data, dict):
|
| 193 |
+
_data = [_data]
|
| 194 |
+
|
| 195 |
+
for d in _data:
|
| 196 |
+
_date = d.pop("@TIME_PERIOD", None)
|
| 197 |
+
val = d.pop("@OBS_VALUE", None)
|
| 198 |
+
_ = d.pop("@OBS_STATUS", None)
|
| 199 |
+
if not val:
|
| 200 |
+
continue
|
| 201 |
+
val = float(val)
|
| 202 |
+
if _unit and _unit.lower() == "percent":
|
| 203 |
+
val = val / 100
|
| 204 |
+
if _date:
|
| 205 |
+
offset = (
|
| 206 |
+
offsets.QuarterEnd
|
| 207 |
+
if "Q" in _date
|
| 208 |
+
else offsets.YearEnd if len(str(_date)) == 4 else offsets.MonthEnd
|
| 209 |
+
)
|
| 210 |
+
_date = to_datetime(_date)
|
| 211 |
+
_date = _date + offset(0)
|
| 212 |
+
_date = _date.strftime("%Y-%m-%d")
|
| 213 |
+
vals = {
|
| 214 |
+
k: v
|
| 215 |
+
for k, v in {
|
| 216 |
+
"date": _date,
|
| 217 |
+
"table": _table,
|
| 218 |
+
"symbol": _symbol,
|
| 219 |
+
"parent": _parent if _parent else _symbol,
|
| 220 |
+
"level": _level,
|
| 221 |
+
"order": _order,
|
| 222 |
+
"country": country_map_dict.get(
|
| 223 |
+
meta.get("ref_area"), meta.get("ref_area")
|
| 224 |
+
),
|
| 225 |
+
"reference_sector": None,
|
| 226 |
+
"title": _title,
|
| 227 |
+
"value": val,
|
| 228 |
+
"unit": (
|
| 229 |
+
_unit.upper() if _unit and _unit in ["usd", "eur"] else _unit
|
| 230 |
+
),
|
| 231 |
+
"scale": (
|
| 232 |
+
"Basis Points"
|
| 233 |
+
if _unit and _unit.lower() == "percent"
|
| 234 |
+
else meta.get("unit_mult")
|
| 235 |
+
),
|
| 236 |
+
}.items()
|
| 237 |
+
if v
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
+
if vals.get("value") and vals.get("date"):
|
| 241 |
+
d.update(vals)
|
| 242 |
+
data.append(d)
|
| 243 |
+
|
| 244 |
+
if not data:
|
| 245 |
+
raise OpenBBError(f"No data found for, '{indicator}', in, '{countries}'.")
|
| 246 |
+
|
| 247 |
+
return data
|
openbb_platform/providers/imf/openbb_imf/utils/helpers.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""IMF Helper Utilities."""
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
async def get_data(url: str) -> list[dict]:
|
| 5 |
+
"""Get data from the IMF API."""
|
| 6 |
+
# pylint: disable=import-outside-toplevel
|
| 7 |
+
|
| 8 |
+
from aiohttp.client_exceptions import ContentTypeError # noqa
|
| 9 |
+
from json.decoder import JSONDecodeError
|
| 10 |
+
from openbb_core.provider.utils.helpers import amake_request
|
| 11 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 12 |
+
|
| 13 |
+
try:
|
| 14 |
+
response = await amake_request(url, timeout=20)
|
| 15 |
+
except (JSONDecodeError, ContentTypeError) as e:
|
| 16 |
+
raise OpenBBError(
|
| 17 |
+
"Error fetching data; This might be rate-limiting. Try again later."
|
| 18 |
+
) from e
|
| 19 |
+
|
| 20 |
+
if "ErrorDetails" in response:
|
| 21 |
+
raise OpenBBError(
|
| 22 |
+
f"{response['ErrorDetails'].get('Code')} -> {response['ErrorDetails'].get('Message')}" # type: ignore
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
series = response.get("CompactData", {}).get("DataSet", {}).pop("Series", {}) # type: ignore
|
| 26 |
+
|
| 27 |
+
if not series:
|
| 28 |
+
raise OpenBBError(f"No time series data found -> {response}")
|
| 29 |
+
|
| 30 |
+
# If there is only one series, they ruturn a dict instead of a list.
|
| 31 |
+
if series and isinstance(series, dict):
|
| 32 |
+
series = [series]
|
| 33 |
+
|
| 34 |
+
return series
|
openbb_platform/providers/imf/openbb_imf/utils/irfcl_helpers.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""IMF IRFCL Data Set Helpers."""
|
| 2 |
+
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def load_irfcl_symbols() -> dict:
|
| 7 |
+
"""Load IMF IRFCL symbols."""
|
| 8 |
+
# pylint: disable=import-outside-toplevel
|
| 9 |
+
from openbb_imf.utils.constants import load_symbols
|
| 10 |
+
|
| 11 |
+
return load_symbols("IRFCL")
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def load_country_map() -> dict:
|
| 15 |
+
"""Load IMF IRFCL country map."""
|
| 16 |
+
# pylint: disable=import-outside-toplevel
|
| 17 |
+
import json # noqa
|
| 18 |
+
from json.decoder import JSONDecodeError
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 21 |
+
|
| 22 |
+
try:
|
| 23 |
+
country_map_file = (
|
| 24 |
+
Path(__file__).parents[1].joinpath("assets", "imf_country_map.json")
|
| 25 |
+
)
|
| 26 |
+
with country_map_file.open(encoding="utf-8") as file:
|
| 27 |
+
country_map_dict = json.load(file)
|
| 28 |
+
except (FileNotFoundError, JSONDecodeError) as e:
|
| 29 |
+
raise OpenBBError(f"Failed to load IMF IRFCL country map: {e}") from e
|
| 30 |
+
|
| 31 |
+
return {
|
| 32 |
+
k: v.split(",")[0].split("_(")[0]
|
| 33 |
+
for k, v in country_map_dict.items()
|
| 34 |
+
if len(k) == 2
|
| 35 |
+
and k[0] not in ("5", "1", "7")
|
| 36 |
+
and k not in ("X0", "R1", "GW", "F1", "F6")
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def load_country_to_code_map() -> dict:
|
| 41 |
+
"""Load a map of lowercase country name to 2-letter ISO symbol."""
|
| 42 |
+
return {
|
| 43 |
+
(
|
| 44 |
+
"euro_area"
|
| 45 |
+
if k == "U2"
|
| 46 |
+
else v.lower()
|
| 47 |
+
.replace(" ", "_")
|
| 48 |
+
.replace("`", "")
|
| 49 |
+
.split(",")[0]
|
| 50 |
+
.split("_(")[0]
|
| 51 |
+
): k
|
| 52 |
+
for k, v in load_country_map().items()
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def validate_countries(countries) -> str:
|
| 57 |
+
"""Validate the country and convert to a 2-letter ISO country code."""
|
| 58 |
+
# pylint: disable=import-outside-toplevel
|
| 59 |
+
from warnings import warn # noqa
|
| 60 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 61 |
+
|
| 62 |
+
country_map_dict = load_country_to_code_map()
|
| 63 |
+
|
| 64 |
+
if isinstance(countries, str):
|
| 65 |
+
countries = countries.split(",")
|
| 66 |
+
elif not isinstance(countries, list):
|
| 67 |
+
raise OpenBBError("Invalid countries list.")
|
| 68 |
+
|
| 69 |
+
new_countries: list = []
|
| 70 |
+
|
| 71 |
+
if "all" in countries or "ALL" in countries:
|
| 72 |
+
return "all"
|
| 73 |
+
|
| 74 |
+
for country in countries:
|
| 75 |
+
if country.lower() not in country_map_dict and country.upper() not in list(
|
| 76 |
+
country_map_dict.values()
|
| 77 |
+
):
|
| 78 |
+
warn(f"Invalid IMF IRFCL country: {country}")
|
| 79 |
+
continue
|
| 80 |
+
|
| 81 |
+
if country.upper() in list(country_map_dict.values()):
|
| 82 |
+
new_countries.append(country.upper())
|
| 83 |
+
else:
|
| 84 |
+
new_countries.append(country_map_dict.get(country, country).upper())
|
| 85 |
+
|
| 86 |
+
new_countries = [c for c in new_countries if c]
|
| 87 |
+
|
| 88 |
+
if not new_countries:
|
| 89 |
+
raise OpenBBError("No valid countries found in the supplied list.")
|
| 90 |
+
|
| 91 |
+
return ",".join(new_countries)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def validate_symbols(symbols) -> str:
|
| 95 |
+
"""Validate the IMF IRFCL symbols."""
|
| 96 |
+
# pylint: disable=import-outside-toplevel
|
| 97 |
+
from warnings import warn # noqa
|
| 98 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 99 |
+
from openbb_imf.utils.constants import IRFCL_PRESET
|
| 100 |
+
|
| 101 |
+
irfcl_symbols = load_irfcl_symbols()
|
| 102 |
+
|
| 103 |
+
if isinstance(symbols, str):
|
| 104 |
+
symbols = symbols.split(",")
|
| 105 |
+
elif not isinstance(symbols, list):
|
| 106 |
+
raise OpenBBError("Invalid symbols list.")
|
| 107 |
+
|
| 108 |
+
if "IRFCL" in symbols or "all" in symbols:
|
| 109 |
+
return "all"
|
| 110 |
+
|
| 111 |
+
new_symbols: list = []
|
| 112 |
+
|
| 113 |
+
for symbol in symbols:
|
| 114 |
+
if symbol in IRFCL_PRESET:
|
| 115 |
+
return IRFCL_PRESET[symbol].replace(",", "+")
|
| 116 |
+
if symbol.upper() not in irfcl_symbols:
|
| 117 |
+
warn(f"Invalid IMF IRFCL symbol: {symbol}")
|
| 118 |
+
new_symbols.append(symbol.upper())
|
| 119 |
+
|
| 120 |
+
return "+".join(new_symbols) if len(new_symbols) > 1 else new_symbols[0]
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
# We use this as a helper to allow future expansion of the supported IMF indicators.
|
| 124 |
+
# Each database has its own nuances with URL construction and schemas.
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
# pylint: disable=too-many-branches,too-many-statements,too-many-locals
|
| 128 |
+
async def _get_irfcl_data(**kwargs) -> list[dict]:
|
| 129 |
+
"""Get IMF IRFCL data.
|
| 130 |
+
This function is not intended to be called directly,
|
| 131 |
+
but through the `ImfEconomicIndicatorsFetcher` class.
|
| 132 |
+
"""
|
| 133 |
+
# pylint: disable=import-outside-toplevel
|
| 134 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 135 |
+
from openbb_imf.utils import constants
|
| 136 |
+
from openbb_imf.utils.helpers import get_data
|
| 137 |
+
from pandas import to_datetime
|
| 138 |
+
from pandas.tseries import offsets
|
| 139 |
+
|
| 140 |
+
countries = kwargs.get("country", "")
|
| 141 |
+
countries = (
|
| 142 |
+
"" if countries == "all" else countries.replace(",", "+") if countries else ""
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
frequency = constants.FREQUENCY_DICT.get(kwargs.get("frequency", "quarter"), "Q")
|
| 146 |
+
sector = kwargs.get("sector", "monetary_authorities")
|
| 147 |
+
sector = constants.REF_SECTORS_DICT.get(sector, "")
|
| 148 |
+
start_date = kwargs.get("start_date", "")
|
| 149 |
+
end_date = kwargs.get("end_date", "")
|
| 150 |
+
|
| 151 |
+
# Adjust the dates to the date relative to frequency.
|
| 152 |
+
# The API does not accept arbitrary dates, so we need to adjust them.
|
| 153 |
+
if start_date:
|
| 154 |
+
start_date = to_datetime(start_date)
|
| 155 |
+
if frequency == "Q":
|
| 156 |
+
start_date = offsets.QuarterBegin(startingMonth=1).rollback(start_date)
|
| 157 |
+
elif frequency == "A":
|
| 158 |
+
start_date = offsets.YearBegin().rollback(start_date)
|
| 159 |
+
else:
|
| 160 |
+
start_date = offsets.MonthBegin().rollback(start_date)
|
| 161 |
+
start_date = start_date.strftime("%Y-%m-%d")
|
| 162 |
+
|
| 163 |
+
if end_date:
|
| 164 |
+
end_date = to_datetime(end_date)
|
| 165 |
+
if frequency == "Q":
|
| 166 |
+
end_date = offsets.QuarterEnd().rollforward(end_date)
|
| 167 |
+
elif frequency == "A":
|
| 168 |
+
end_date = offsets.YearEnd().rollforward(end_date)
|
| 169 |
+
else:
|
| 170 |
+
end_date = offsets.MonthEnd().rollforward(end_date)
|
| 171 |
+
end_date = end_date.strftime("%Y-%m-%d")
|
| 172 |
+
|
| 173 |
+
indicator = kwargs.get("symbol")
|
| 174 |
+
indicators = validate_symbols(indicator) if indicator else ""
|
| 175 |
+
indicators = "" if indicators == "all" else indicators
|
| 176 |
+
|
| 177 |
+
if not indicators and not countries:
|
| 178 |
+
raise OpenBBError("Country is required when returning the complete dataset.")
|
| 179 |
+
|
| 180 |
+
date_range = (
|
| 181 |
+
f"?startPeriod={start_date}&endPeriod={end_date}"
|
| 182 |
+
if start_date and end_date
|
| 183 |
+
else ""
|
| 184 |
+
)
|
| 185 |
+
base_url = "http://dataservices.imf.org/REST/SDMX_JSON.svc/"
|
| 186 |
+
key = f"CompactData/IRFCL/{frequency}.{countries}.{indicators}.{sector}"
|
| 187 |
+
url = f"{base_url}{key}{date_range}"
|
| 188 |
+
|
| 189 |
+
series = await get_data(url)
|
| 190 |
+
|
| 191 |
+
data: list = []
|
| 192 |
+
all_symbols = load_irfcl_symbols()
|
| 193 |
+
country_map_dict = {
|
| 194 |
+
v: k.replace("_", " ").title().replace("Ecb", "ECB")
|
| 195 |
+
for k, v in load_country_to_code_map().items()
|
| 196 |
+
}
|
| 197 |
+
# Iterate over the series to extract observations and map the metadata.
|
| 198 |
+
for s in series:
|
| 199 |
+
if "Obs" not in s:
|
| 200 |
+
continue
|
| 201 |
+
meta = {
|
| 202 |
+
k.replace("@", "").lower(): (
|
| 203 |
+
constants.UNIT_MULTIPLIERS_MAP.get(str(v), v)
|
| 204 |
+
if k == "@UNIT_MULT"
|
| 205 |
+
else v
|
| 206 |
+
)
|
| 207 |
+
for k, v in s.items()
|
| 208 |
+
if k != "Obs"
|
| 209 |
+
}
|
| 210 |
+
_symbol = meta.get("indicator")
|
| 211 |
+
_parent: Optional[str] = None
|
| 212 |
+
_order: Optional[str] = None
|
| 213 |
+
_level: Optional[str] = None
|
| 214 |
+
_table: Optional[str] = None
|
| 215 |
+
_title: Optional[str] = None
|
| 216 |
+
_unit: Optional[str] = None
|
| 217 |
+
|
| 218 |
+
if _symbol not in all_symbols:
|
| 219 |
+
continue
|
| 220 |
+
|
| 221 |
+
_table = all_symbols.get(_symbol, {}).get("table")
|
| 222 |
+
_parent = all_symbols.get(_symbol, {}).get("parent", "")
|
| 223 |
+
_order = all_symbols.get(_symbol, {}).get("order", "")
|
| 224 |
+
_level = all_symbols.get(_symbol, {}).get("level", "")
|
| 225 |
+
_title = all_symbols.get(_symbol, {}).get("title", "").replace(", ", " - ")
|
| 226 |
+
_unit = all_symbols.get(_symbol, {}).get("unit", "")
|
| 227 |
+
|
| 228 |
+
if _title:
|
| 229 |
+
_title = " - ".join(_title.split(", ")[:-1])
|
| 230 |
+
|
| 231 |
+
_data = s.pop("Obs", [])
|
| 232 |
+
|
| 233 |
+
if isinstance(_data, dict):
|
| 234 |
+
_data = [_data]
|
| 235 |
+
|
| 236 |
+
for d in _data:
|
| 237 |
+
_date = d.pop("@TIME_PERIOD", None)
|
| 238 |
+
val = d.pop("@OBS_VALUE", None)
|
| 239 |
+
_ = d.pop("@OBS_STATUS", None)
|
| 240 |
+
|
| 241 |
+
if not val:
|
| 242 |
+
continue
|
| 243 |
+
|
| 244 |
+
if _date:
|
| 245 |
+
offset = (
|
| 246 |
+
offsets.QuarterEnd
|
| 247 |
+
if "Q" in _date
|
| 248 |
+
else offsets.YearEnd if len(str(_date)) == 4 else offsets.MonthEnd
|
| 249 |
+
)
|
| 250 |
+
_date = to_datetime(_date)
|
| 251 |
+
_date = _date + offset(0)
|
| 252 |
+
_date = _date.strftime("%Y-%m-%d")
|
| 253 |
+
vals = {
|
| 254 |
+
k: v
|
| 255 |
+
for k, v in {
|
| 256 |
+
"date": _date,
|
| 257 |
+
"table": _table,
|
| 258 |
+
"symbol": _symbol,
|
| 259 |
+
"parent": _parent,
|
| 260 |
+
"order": _order,
|
| 261 |
+
"level": _level,
|
| 262 |
+
"country": country_map_dict.get(
|
| 263 |
+
meta.get("ref_area"), meta.get("ref_area")
|
| 264 |
+
),
|
| 265 |
+
"reference_sector": constants.SECTOR_MAP.get(
|
| 266 |
+
meta.get("ref_sector"), meta.get("ref_sector")
|
| 267 |
+
),
|
| 268 |
+
"title": _title,
|
| 269 |
+
"value": float(val) if val else None,
|
| 270 |
+
"unit": _unit,
|
| 271 |
+
"scale": meta.get("unit_mult"),
|
| 272 |
+
}.items()
|
| 273 |
+
if v
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
if vals.get("value") and vals.get("date"):
|
| 277 |
+
d.update(vals)
|
| 278 |
+
data.append(d)
|
| 279 |
+
|
| 280 |
+
if not data:
|
| 281 |
+
raise OpenBBError(f"No data found for '{indicator}' in '{countries}'.")
|
| 282 |
+
|
| 283 |
+
return data
|
openbb_platform/providers/imf/poetry.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
openbb_platform/providers/imf/pyproject.toml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[tool.poetry]
|
| 2 |
+
name = "openbb-imf"
|
| 3 |
+
version = "1.1.1"
|
| 4 |
+
description = "https://datahelp.imf.org/knowledgebase/articles/630877-api"
|
| 5 |
+
authors = ["OpenBB Team <hello@openbb.co>"]
|
| 6 |
+
license = "AGPL-3.0-only"
|
| 7 |
+
readme = "README.md"
|
| 8 |
+
packages = [{ include = "openbb_imf" }]
|
| 9 |
+
|
| 10 |
+
[tool.poetry.dependencies]
|
| 11 |
+
python = ">=3.9.21,<3.13"
|
| 12 |
+
openbb-core = "^1.4.6"
|
| 13 |
+
|
| 14 |
+
[build-system]
|
| 15 |
+
requires = ["poetry-core"]
|
| 16 |
+
build-backend = "poetry.core.masonry.api"
|
| 17 |
+
|
| 18 |
+
[tool.poetry.plugins."openbb_provider_extension"]
|
| 19 |
+
imf = "openbb_imf:imf_provider"
|
openbb_platform/providers/imf/tests/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""IMF Provider Fetcher Tests."""
|
openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_direction_of_trade_fetcher_urllib3_v1.yaml
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interactions:
|
| 2 |
+
- request:
|
| 3 |
+
body: null
|
| 4 |
+
headers:
|
| 5 |
+
Accept:
|
| 6 |
+
- application/json
|
| 7 |
+
Accept-Encoding:
|
| 8 |
+
- gzip, deflate
|
| 9 |
+
Connection:
|
| 10 |
+
- keep-alive
|
| 11 |
+
method: GET
|
| 12 |
+
uri: http://dataservices.imf.org/REST/SDMX_JSON.svc/CompactData/DOT/A.US.TXG_FOB_USD.W00+B0?startPeriod=2020-01-01&endPeriod=2023-12-31
|
| 13 |
+
response:
|
| 14 |
+
body:
|
| 15 |
+
string: !!binary |
|
| 16 |
+
H4sIAAAAAAAEA6SSX6+aQBDFv4qhL216gQVRlCdRsCURsfxprH0gK4xeEgHDbtXG3O/e2dWm1zRp
|
| 17 |
+
btIXEs6e35nZmb0qs7Y+0oJ7lFPFuSqTS31omHNhleIoz5wfHV0/n8/aua+13V43CTH0dbhIimeo
|
| 18 |
+
qVo1jNOmAOXpD1i+AZR+VjlMxizagvKqbR7BxAvXsmYHrP3RFcB0IYUL/UYx/WTmRK+BMbqHnuiV
|
| 19 |
+
YbMd7CvGu58aK+uL5B/sMuKGaBdWSgypEq/PoDtVWEar6p0Ei9todC9K3+oTmfJyYor/cx8M+Qy0
|
| 20 |
+
hE4sJfAwqrBKwzQGoI6GI1u1LPyMh9uhurUte0TtHRkbBKkUGEf3jh6YWMuqgyPtoETJJKalkrFq
|
| 21 |
+
ktQYOQPiWBb6E2hKkFUmlXAZM4LqktYgKovn4Bxos0ceGjx4x+HC8ScI58rLkzJrG46vR1izOEBd
|
| 22 |
+
rAHnKZ7MfYwIpXCA43PbABo+9ozee2zlQ29o9tWhaRLlBYNiKKA6vW5ks9mICh5uJgEuR4CLeCWJ
|
| 23 |
+
oqK/hmHsvW6JbvaPPWIzCXQVIPL9qkzmsf8FYRflSezPczf2XfzPEiEESy+YuWkUo5KuP+XzaJpn
|
| 24 |
+
iSeOZlG2TP145cbpb2ZKxEG2DNI8zBYpMkMhpEHoIxmHrpBWxjcUo+29vDxc+XEQeeg3iSkzommS
|
| 25 |
+
f3UXmS+0ft8eWJpt2wbeHDv+CzEwcPKA2L9ao3cbBoIQiqK9OLYQjz+VONpS6N1jhx6N5GALuAIO
|
| 26 |
+
0gVKlNehkd9GHVVGZrCQQ6RbFJ0elMEm8Zhrnmu/G0Vf/OW4mxQm3e1kbIw+HLupIj1LQB7Z/Ler
|
| 27 |
+
cDiWUSdM5TBrg5W1lzso1ltUP7LXzLwBuF028KsFAAA=
|
| 28 |
+
headers:
|
| 29 |
+
Cache-Control:
|
| 30 |
+
- private
|
| 31 |
+
Connection:
|
| 32 |
+
- Keep-Alive
|
| 33 |
+
Content-Encoding:
|
| 34 |
+
- gzip
|
| 35 |
+
Content-Length:
|
| 36 |
+
- '659'
|
| 37 |
+
Content-Type:
|
| 38 |
+
- application/json; charset=utf-8
|
| 39 |
+
Date:
|
| 40 |
+
- Fri, 20 Sep 2024 22:50:44 GMT
|
| 41 |
+
Set-Cookie: null
|
| 42 |
+
Vary:
|
| 43 |
+
- Accept-Encoding
|
| 44 |
+
X-Content-Type-Options:
|
| 45 |
+
- nosniff
|
| 46 |
+
X-Frame-Options:
|
| 47 |
+
- SAMEORIGIN
|
| 48 |
+
X-Permitted-Cross-Domain-Policies:
|
| 49 |
+
- none
|
| 50 |
+
X-XSS-Protection:
|
| 51 |
+
- 1; mode=block
|
| 52 |
+
status:
|
| 53 |
+
code: 200
|
| 54 |
+
message: OK
|
| 55 |
+
version: 1
|
openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_direction_of_trade_fetcher_urllib3_v2.yaml
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interactions:
|
| 2 |
+
- request:
|
| 3 |
+
body: null
|
| 4 |
+
headers:
|
| 5 |
+
Accept:
|
| 6 |
+
- application/json
|
| 7 |
+
Accept-Encoding:
|
| 8 |
+
- gzip, deflate
|
| 9 |
+
Connection:
|
| 10 |
+
- keep-alive
|
| 11 |
+
method: GET
|
| 12 |
+
uri: http://dataservices.imf.org/REST/SDMX_JSON.svc/CompactData/DOT/A.US.TXG_FOB_USD.W00+B0?startPeriod=2020-01-01&endPeriod=2023-12-31
|
| 13 |
+
response:
|
| 14 |
+
body:
|
| 15 |
+
string: !!binary |
|
| 16 |
+
H4sIAAAAAAAEA6SSX6+aQBDFv4qhL216gWVBUJ5EwZZExPKnsfaBbGFUEgHDUrUx97t3Fm3uNU2T
|
| 17 |
+
m/Rxz57fmZmdvUqzpjqyvHNZxyT7Kk0u1aHm9oWXki3tu+5oq+r5fFbOutK0O5USoqnrYBHne6iY
|
| 18 |
+
XNa8Y3UO0tMLWLwB7P28tHkfs2hy1pVN/QjGbrDua7bAm59tDlwVUrBQbxRXTzQjagWcsx0MRK8c
|
| 19 |
+
m21hV/Ku/aXworr0/IO9j7ghyoUXPYZUgeNzaE8lllHKatuD+e1pVDdM3uoTmf1w4hX/Zx4M+Qys
|
| 20 |
+
gFYsxXcxKtcLTaNkLBcFMWVDHxGZWflQHm1Nczg2iAHWFqkEeIfuLTtwsZZVC0fWQoESJdSQkack
|
| 21 |
+
0Ua2MbIJQX8MdXGrMimFS5sJdckqEJXFd7APrN4hDzXa33Vw6fDgB3Pp+UmaNXWHv0dY08hHff/y
|
| 22 |
+
Ze7PiFACBzjumxrQ8HGgDd5jKx8GJtVlk1IiPWNQBDmUp9eNbDYbUcHFzcTQ9U+Ai3gliaKiv5pj
|
| 23 |
+
7L1ugW4O/94jNhNDWwIi36/SZB55XxB2UJ5E3jxzIs/BcxoLwV+6/sxJwgiVZP0pm4fTLI1dcTUL
|
| 24 |
+
02XiRSsnSv4wUyIu0qWfZEG6SJAxhZD4gYdkFDhCWmnfUAx/3Mv3lysv8kMX/ZTQPiOcxtlXZ5F6
|
| 25 |
+
QtN1a2golmVpODl2/BeiYeDkAbHo+Her9G7DMAwDAXSX1AHBo/jtskUqj8LdLbuMICCFBzjw+MgE
|
| 26 |
+
BdJyk5HfzDBkKqlCXTahsYS8wpzCWcVfffR79ntQ9Hv/5+dpUqhUlZGyMmqz7KKKsEgBmUfx367C
|
| 27 |
+
bphGFdAhm1kLrMxeZiCfZxnjkj26+wT887SyqwUAAA==
|
| 28 |
+
headers:
|
| 29 |
+
Cache-Control:
|
| 30 |
+
- private
|
| 31 |
+
Connection:
|
| 32 |
+
- Keep-Alive
|
| 33 |
+
Content-Encoding:
|
| 34 |
+
- gzip
|
| 35 |
+
Content-Length:
|
| 36 |
+
- '658'
|
| 37 |
+
Content-Type:
|
| 38 |
+
- application/json; charset=utf-8
|
| 39 |
+
Date:
|
| 40 |
+
- Fri, 20 Sep 2024 22:48:00 GMT
|
| 41 |
+
Set-Cookie: null
|
| 42 |
+
Vary:
|
| 43 |
+
- Accept-Encoding
|
| 44 |
+
X-Content-Type-Options:
|
| 45 |
+
- nosniff
|
| 46 |
+
X-Frame-Options:
|
| 47 |
+
- SAMEORIGIN
|
| 48 |
+
X-Permitted-Cross-Domain-Policies:
|
| 49 |
+
- none
|
| 50 |
+
X-XSS-Protection:
|
| 51 |
+
- 1; mode=block
|
| 52 |
+
status:
|
| 53 |
+
code: 200
|
| 54 |
+
message: OK
|
| 55 |
+
version: 1
|
openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_economic_indicators_fetcher_urllib3_v1.yaml
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interactions:
|
| 2 |
+
- request:
|
| 3 |
+
body: null
|
| 4 |
+
headers:
|
| 5 |
+
Accept:
|
| 6 |
+
- application/json
|
| 7 |
+
Accept-Encoding:
|
| 8 |
+
- gzip, deflate
|
| 9 |
+
Connection:
|
| 10 |
+
- keep-alive
|
| 11 |
+
method: GET
|
| 12 |
+
uri: http://dataservices.imf.org/REST/SDMX_JSON.svc/CompactData/IRFCL/M.JP.RAMFDA_USD.S1X?startPeriod=2023-01-01&endPeriod=2023-12-31
|
| 13 |
+
response:
|
| 14 |
+
body:
|
| 15 |
+
string: !!binary |
|
| 16 |
+
H4sIAAAAAAAEA6SSX4+aQBTFv4qhL226yICiC09S/qQ0oBawMfaBTJmrkggYZlZtzH733hlNttuk
|
| 17 |
+
zSZ9nHPP7557Z+aq+V1zpJUIqKCae9Vml+bQcvfCa83V9kIcXcM4n8/D82jY9TvDIsQ01mmSV3to
|
| 18 |
+
qF63XNC2Au3hBWRvAJWf1y5XbZKuoqLu2tdgHqRrldkD7576CrghpTQxbhQ3TlZJjAY4pzsYyFk5
|
| 19 |
+
DtvDruai/znkrLko/pVdtbghwwtnCkOK4foc+lONMcO62Sqwul2NEWeRn7zdKfuqBeVN/s9O2OQz
|
| 20 |
+
UAa9fJg4wFbjEXu02cTW2eN2pI8dRnUHto5Ox4wSx7bNCiykCuAC3Vt64PJplj0caQ8MJYtYY504
|
| 21 |
+
ukkKc+qatmvZ6M+hvafMaukyfYLqnDYgk+WXcA+03SEPLRbeCbgIPMRppD0/aH7XCvxB0rrKYtT3
|
| 22 |
+
L9/mfpUIFXCA475rAQ0fB+bgPY7yYTCxRvrEsoj2jI0yqKA+gVp3VjM0bjYbmRBQQXMQcYCSeozf
|
| 23 |
+
RBkrJ2w5Fu/JDP38n6+JA+XQ14DQVZtFWfgV6RTVWRZGpZeFHp6/LKUQz4PY94pFhkrmpVHglas8
|
| 24 |
+
kBVpzUP/VsrNtdRW87go01VSoHsihSJOwzJaZKknpaUpQxY/MPc7BqviMsziRYB+i+BtEFNSi095
|
| 25 |
+
+c1LViHK+tQZ48J/sVt/2m0y/dXnHNwADIMwFN2l90oBAnGG6SjZvTmDjM9P1ufaslYfXM+ssY1r
|
| 26 |
+
z1pMGx6Fo2tZhesCj0HhNjfnu3BfQbmMwrt3kcwxJj/XrF8NPOc7dz97tJr2sAUAAA==
|
| 27 |
+
headers:
|
| 28 |
+
Cache-Control:
|
| 29 |
+
- private
|
| 30 |
+
Connection:
|
| 31 |
+
- Keep-Alive
|
| 32 |
+
Content-Encoding:
|
| 33 |
+
- gzip
|
| 34 |
+
Content-Length:
|
| 35 |
+
- '619'
|
| 36 |
+
Content-Type:
|
| 37 |
+
- application/json; charset=utf-8
|
| 38 |
+
Date:
|
| 39 |
+
- Tue, 10 Sep 2024 21:15:25 GMT
|
| 40 |
+
Set-Cookie: null
|
| 41 |
+
Vary:
|
| 42 |
+
- Accept-Encoding
|
| 43 |
+
X-Content-Type-Options:
|
| 44 |
+
- nosniff
|
| 45 |
+
X-Frame-Options:
|
| 46 |
+
- SAMEORIGIN
|
| 47 |
+
X-Permitted-Cross-Domain-Policies:
|
| 48 |
+
- none
|
| 49 |
+
X-XSS-Protection:
|
| 50 |
+
- 1; mode=block
|
| 51 |
+
status:
|
| 52 |
+
code: 200
|
| 53 |
+
message: OK
|
| 54 |
+
version: 1
|
openbb_platform/providers/imf/tests/record/http/test_imf_fetchers/test_imf_economic_indicators_fetcher_urllib3_v2.yaml
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
interactions:
|
| 2 |
+
- request:
|
| 3 |
+
body: null
|
| 4 |
+
headers:
|
| 5 |
+
Accept:
|
| 6 |
+
- application/json
|
| 7 |
+
Accept-Encoding:
|
| 8 |
+
- gzip, deflate
|
| 9 |
+
Connection:
|
| 10 |
+
- keep-alive
|
| 11 |
+
method: GET
|
| 12 |
+
uri: http://dataservices.imf.org/REST/SDMX_JSON.svc/CompactData/IRFCL/M.JP.RAMFDA_USD.S1X?startPeriod=2023-01-01&endPeriod=2023-12-31
|
| 13 |
+
response:
|
| 14 |
+
body:
|
| 15 |
+
string: !!binary |
|
| 16 |
+
H4sIAAAAAAAEA6SSXY+aQBSG/4qhN226yDB+ULmS8pHSgFrAxtgLMg5HJREwDKs2Zv97z4w27W6y
|
| 17 |
+
zSa95J3nPe/54Kq5TXVkvPNYxzT7qk0v1aEW9kWUmq3tu+5oG8b5fO6fB/2m3RmUENNYxVHK91Ax
|
| 18 |
+
vaxFx2oO2sMfY/EGo+JFaQtVJmo468qmfm5MvXilMlsQzWPLQRhSiiPj5hLGiebEqEAItoOe7FVg
|
| 19 |
+
sy3sStG1P/uiqC7K/wxXJW6W/kUUyoauAscX0J5KjOmX1VYZ+W01RpgEbvR2UtZVA8pN/s9MWOQL
|
| 20 |
+
sAJaeZjQw1IFZeaWTLjOh9ZAH47ISN8UfKOTicVhY32a8BFFVwaiQ3rLDkKeZtHCkbVQoEQJHSKt
|
| 21 |
+
EyszB/aQ2qMx8inU95RpKSnTJajOWAUyWf4S9oHVO/RDjQ/vOrh0+BHGgfb0oLlN3TGOwlVbJiHq
|
| 22 |
+
8hS4U/nb3FeJpgwOcNw3NSDwsWf23mMrH3pjOtDHlBLtCQslwKE8gRp3WhYIrtdrmeDhdVLo1ArU
|
| 23 |
+
Mf4Sf3dYC+TvyQXy4p/XxIZSaEtA01WbBon/Dd0xqtPED3In8R38/rqQQjjzQtfJ5gkqiRMHnpMv
|
| 24 |
+
U0++SDT13dtTaq6ktpyFWR4vowzpsRSyMPbzYJ7EjpQWpgyZbzD3Bwarx4WfhHMPeUpwG8SUrvnn
|
| 25 |
+
NP/uREsfZd2aDHHgV3D6Eh8R63V68JL+1eecmwAMxEAU7cW5QfdRjEvZ3r2xhDTxY/ikMGupOpJn
|
| 26 |
+
rVUj08Kt8dhavHHymGOicZaceTaubiNHaHx7R6w8QOZzqvoli+d85+4HilPlVbAFAAA=
|
| 27 |
+
headers:
|
| 28 |
+
Cache-Control:
|
| 29 |
+
- private
|
| 30 |
+
Connection:
|
| 31 |
+
- Keep-Alive
|
| 32 |
+
Content-Encoding:
|
| 33 |
+
- gzip
|
| 34 |
+
Content-Length:
|
| 35 |
+
- '620'
|
| 36 |
+
Content-Type:
|
| 37 |
+
- application/json; charset=utf-8
|
| 38 |
+
Date:
|
| 39 |
+
- Sat, 07 Sep 2024 17:42:56 GMT
|
| 40 |
+
Set-Cookie: null
|
| 41 |
+
Vary:
|
| 42 |
+
- Accept-Encoding
|
| 43 |
+
X-Content-Type-Options:
|
| 44 |
+
- nosniff
|
| 45 |
+
X-Frame-Options:
|
| 46 |
+
- SAMEORIGIN
|
| 47 |
+
X-Permitted-Cross-Domain-Policies:
|
| 48 |
+
- none
|
| 49 |
+
X-XSS-Protection:
|
| 50 |
+
- 1; mode=block
|
| 51 |
+
status:
|
| 52 |
+
code: 200
|
| 53 |
+
message: OK
|
| 54 |
+
version: 1
|
openbb_platform/providers/imf/tests/test_imf_fetchers.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""IMF Fetcher Tests."""
|
| 2 |
+
|
| 3 |
+
from datetime import date
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
from openbb_core.app.service.user_service import UserService
|
| 7 |
+
from openbb_imf.models.available_indicators import ImfAvailableIndicatorsFetcher
|
| 8 |
+
from openbb_imf.models.direction_of_trade import ImfDirectionOfTradeFetcher
|
| 9 |
+
from openbb_imf.models.economic_indicators import ImfEconomicIndicatorsFetcher
|
| 10 |
+
|
| 11 |
+
test_credentials = UserService().default_user_settings.credentials.model_dump(
|
| 12 |
+
mode="json"
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def scrub_string(key):
|
| 17 |
+
"""Scrub a string from the response."""
|
| 18 |
+
|
| 19 |
+
def before_record_response(response):
|
| 20 |
+
response["headers"][key] = response["headers"].update({key: None})
|
| 21 |
+
return response
|
| 22 |
+
|
| 23 |
+
return before_record_response
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
@pytest.fixture(scope="module")
|
| 27 |
+
def vcr_config():
|
| 28 |
+
"""VCR configuration."""
|
| 29 |
+
return {
|
| 30 |
+
"filter_headers": [("User-Agent", None)],
|
| 31 |
+
"before_record_response": [
|
| 32 |
+
scrub_string("Set-Cookie"),
|
| 33 |
+
],
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
@pytest.mark.record_http
|
| 38 |
+
def test_imf_economic_indicators_fetcher(credentials=test_credentials):
|
| 39 |
+
"""Test the IMF EconomicIndicators fetcher."""
|
| 40 |
+
params = {
|
| 41 |
+
"country": "JP",
|
| 42 |
+
"frequency": "month",
|
| 43 |
+
"symbol": "RAMFDA_USD",
|
| 44 |
+
"start_date": date(2023, 1, 1),
|
| 45 |
+
"end_date": date(2023, 12, 31),
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
fetcher = ImfEconomicIndicatorsFetcher()
|
| 49 |
+
result = fetcher.test(params, credentials)
|
| 50 |
+
assert result is None
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# The data for this request are local files, so we can't record them.
|
| 54 |
+
def test_imf_available_indicators_fetcher(credentials=test_credentials):
|
| 55 |
+
"""Test the IMF Available Indicators fetcher."""
|
| 56 |
+
params = {}
|
| 57 |
+
|
| 58 |
+
fetcher = ImfAvailableIndicatorsFetcher()
|
| 59 |
+
result = fetcher.test(params, credentials)
|
| 60 |
+
assert result is None
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
@pytest.mark.record_http
|
| 64 |
+
def test_imf_direction_of_trade_fetcher(credentials=test_credentials):
|
| 65 |
+
"""Test the ImfDirectionOfTrade fetcher."""
|
| 66 |
+
params = {
|
| 67 |
+
"provider": "imf",
|
| 68 |
+
"country": "us",
|
| 69 |
+
"counterpart": "world,eu",
|
| 70 |
+
"frequency": "annual",
|
| 71 |
+
"direction": "exports",
|
| 72 |
+
"start_date": date(2020, 1, 1),
|
| 73 |
+
"end_date": date(2023, 1, 1),
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
fetcher = ImfDirectionOfTradeFetcher()
|
| 77 |
+
result = fetcher.test(params, credentials)
|
| 78 |
+
assert result is None
|
openbb_platform/providers/intrinio/README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# OpenBB Intrinio Provider
|
| 2 |
+
|
| 3 |
+
This extension integrates the [Intrinio](https://intrinio.com/) data provider into the OpenBB Platform.
|
| 4 |
+
|
| 5 |
+
## Installation
|
| 6 |
+
|
| 7 |
+
To install the extension:
|
| 8 |
+
|
| 9 |
+
```bash
|
| 10 |
+
pip install openbb-intrinio
|
| 11 |
+
```
|
| 12 |
+
|
| 13 |
+
Documentation available [here](https://docs.openbb.co/platform/developer_guide/contributing).
|
openbb_platform/providers/intrinio/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio provider."""
|
openbb_platform/providers/intrinio/openbb_intrinio/__init__.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Provider Modules."""
|
| 2 |
+
|
| 3 |
+
from openbb_core.provider.abstract.provider import Provider
|
| 4 |
+
from openbb_intrinio.models.balance_sheet import IntrinioBalanceSheetFetcher
|
| 5 |
+
from openbb_intrinio.models.calendar_ipo import IntrinioCalendarIpoFetcher
|
| 6 |
+
from openbb_intrinio.models.cash_flow import IntrinioCashFlowStatementFetcher
|
| 7 |
+
from openbb_intrinio.models.company_filings import IntrinioCompanyFilingsFetcher
|
| 8 |
+
from openbb_intrinio.models.company_news import IntrinioCompanyNewsFetcher
|
| 9 |
+
from openbb_intrinio.models.currency_pairs import IntrinioCurrencyPairsFetcher
|
| 10 |
+
from openbb_intrinio.models.equity_historical import IntrinioEquityHistoricalFetcher
|
| 11 |
+
from openbb_intrinio.models.equity_info import IntrinioEquityInfoFetcher
|
| 12 |
+
from openbb_intrinio.models.equity_quote import IntrinioEquityQuoteFetcher
|
| 13 |
+
from openbb_intrinio.models.equity_search import IntrinioEquitySearchFetcher
|
| 14 |
+
from openbb_intrinio.models.etf_holdings import IntrinioEtfHoldingsFetcher
|
| 15 |
+
from openbb_intrinio.models.etf_info import IntrinioEtfInfoFetcher
|
| 16 |
+
from openbb_intrinio.models.etf_price_performance import (
|
| 17 |
+
IntrinioEtfPricePerformanceFetcher,
|
| 18 |
+
)
|
| 19 |
+
from openbb_intrinio.models.etf_search import IntrinioEtfSearchFetcher
|
| 20 |
+
from openbb_intrinio.models.financial_ratios import IntrinioFinancialRatiosFetcher
|
| 21 |
+
from openbb_intrinio.models.forward_ebitda_estimates import (
|
| 22 |
+
IntrinioForwardEbitdaEstimatesFetcher,
|
| 23 |
+
)
|
| 24 |
+
from openbb_intrinio.models.forward_eps_estimates import (
|
| 25 |
+
IntrinioForwardEpsEstimatesFetcher,
|
| 26 |
+
)
|
| 27 |
+
from openbb_intrinio.models.forward_pe_estimates import (
|
| 28 |
+
IntrinioForwardPeEstimatesFetcher,
|
| 29 |
+
)
|
| 30 |
+
from openbb_intrinio.models.forward_sales_estimates import (
|
| 31 |
+
IntrinioForwardSalesEstimatesFetcher,
|
| 32 |
+
)
|
| 33 |
+
from openbb_intrinio.models.fred_series import IntrinioFredSeriesFetcher
|
| 34 |
+
from openbb_intrinio.models.historical_attributes import (
|
| 35 |
+
IntrinioHistoricalAttributesFetcher,
|
| 36 |
+
)
|
| 37 |
+
from openbb_intrinio.models.historical_dividends import (
|
| 38 |
+
IntrinioHistoricalDividendsFetcher,
|
| 39 |
+
)
|
| 40 |
+
from openbb_intrinio.models.historical_market_cap import (
|
| 41 |
+
IntrinioHistoricalMarketCapFetcher,
|
| 42 |
+
)
|
| 43 |
+
from openbb_intrinio.models.income_statement import IntrinioIncomeStatementFetcher
|
| 44 |
+
from openbb_intrinio.models.index_historical import IntrinioIndexHistoricalFetcher
|
| 45 |
+
from openbb_intrinio.models.insider_trading import IntrinioInsiderTradingFetcher
|
| 46 |
+
|
| 47 |
+
# from openbb_intrinio.models.institutional_ownership import (
|
| 48 |
+
# IntrinioInstitutionalOwnershipFetcher,
|
| 49 |
+
# )
|
| 50 |
+
from openbb_intrinio.models.key_metrics import IntrinioKeyMetricsFetcher
|
| 51 |
+
from openbb_intrinio.models.latest_attributes import IntrinioLatestAttributesFetcher
|
| 52 |
+
from openbb_intrinio.models.market_snapshots import IntrinioMarketSnapshotsFetcher
|
| 53 |
+
from openbb_intrinio.models.options_chains import IntrinioOptionsChainsFetcher
|
| 54 |
+
from openbb_intrinio.models.options_snapshots import IntrinioOptionsSnapshotsFetcher
|
| 55 |
+
from openbb_intrinio.models.options_unusual import IntrinioOptionsUnusualFetcher
|
| 56 |
+
from openbb_intrinio.models.price_target_consensus import (
|
| 57 |
+
IntrinioPriceTargetConsensusFetcher,
|
| 58 |
+
)
|
| 59 |
+
from openbb_intrinio.models.reported_financials import IntrinioReportedFinancialsFetcher
|
| 60 |
+
from openbb_intrinio.models.search_attributes import (
|
| 61 |
+
IntrinioSearchAttributesFetcher,
|
| 62 |
+
)
|
| 63 |
+
from openbb_intrinio.models.share_statistics import IntrinioShareStatisticsFetcher
|
| 64 |
+
from openbb_intrinio.models.world_news import IntrinioWorldNewsFetcher
|
| 65 |
+
|
| 66 |
+
intrinio_provider = Provider(
|
| 67 |
+
name="intrinio",
|
| 68 |
+
website="https://intrinio.com",
|
| 69 |
+
description="""Intrinio is a financial data platform that provides real-time and
|
| 70 |
+
historical financial market data to businesses and developers through an API.""",
|
| 71 |
+
credentials=["api_key"],
|
| 72 |
+
fetcher_dict={
|
| 73 |
+
"BalanceSheet": IntrinioBalanceSheetFetcher,
|
| 74 |
+
"CalendarIpo": IntrinioCalendarIpoFetcher,
|
| 75 |
+
"CashFlowStatement": IntrinioCashFlowStatementFetcher,
|
| 76 |
+
"CompanyFilings": IntrinioCompanyFilingsFetcher,
|
| 77 |
+
"CompanyNews": IntrinioCompanyNewsFetcher,
|
| 78 |
+
"CurrencyPairs": IntrinioCurrencyPairsFetcher,
|
| 79 |
+
"EquityHistorical": IntrinioEquityHistoricalFetcher,
|
| 80 |
+
"EquityInfo": IntrinioEquityInfoFetcher,
|
| 81 |
+
"EquityQuote": IntrinioEquityQuoteFetcher,
|
| 82 |
+
"EquitySearch": IntrinioEquitySearchFetcher,
|
| 83 |
+
"EtfHistorical": IntrinioEquityHistoricalFetcher,
|
| 84 |
+
"EtfHoldings": IntrinioEtfHoldingsFetcher,
|
| 85 |
+
"EtfInfo": IntrinioEtfInfoFetcher,
|
| 86 |
+
"EtfPricePerformance": IntrinioEtfPricePerformanceFetcher,
|
| 87 |
+
"EtfSearch": IntrinioEtfSearchFetcher,
|
| 88 |
+
"FinancialRatios": IntrinioFinancialRatiosFetcher,
|
| 89 |
+
"ForwardEbitdaEstimates": IntrinioForwardEbitdaEstimatesFetcher,
|
| 90 |
+
"ForwardEpsEstimates": IntrinioForwardEpsEstimatesFetcher,
|
| 91 |
+
"ForwardPeEstimates": IntrinioForwardPeEstimatesFetcher,
|
| 92 |
+
"ForwardSalesEstimates": IntrinioForwardSalesEstimatesFetcher,
|
| 93 |
+
"FredSeries": IntrinioFredSeriesFetcher,
|
| 94 |
+
"HistoricalAttributes": IntrinioHistoricalAttributesFetcher,
|
| 95 |
+
"HistoricalDividends": IntrinioHistoricalDividendsFetcher,
|
| 96 |
+
"HistoricalMarketCap": IntrinioHistoricalMarketCapFetcher,
|
| 97 |
+
"IncomeStatement": IntrinioIncomeStatementFetcher,
|
| 98 |
+
"IndexHistorical": IntrinioIndexHistoricalFetcher,
|
| 99 |
+
"InsiderTrading": IntrinioInsiderTradingFetcher,
|
| 100 |
+
# "InstitutionalOwnership": IntrinioInstitutionalOwnershipFetcher, # Disabled due to unreliable Intrinio endpoint
|
| 101 |
+
"KeyMetrics": IntrinioKeyMetricsFetcher,
|
| 102 |
+
"LatestAttributes": IntrinioLatestAttributesFetcher,
|
| 103 |
+
"MarketSnapshots": IntrinioMarketSnapshotsFetcher,
|
| 104 |
+
"OptionsChains": IntrinioOptionsChainsFetcher,
|
| 105 |
+
"OptionsSnapshots": IntrinioOptionsSnapshotsFetcher,
|
| 106 |
+
"OptionsUnusual": IntrinioOptionsUnusualFetcher,
|
| 107 |
+
"PriceTargetConsensus": IntrinioPriceTargetConsensusFetcher,
|
| 108 |
+
"ReportedFinancials": IntrinioReportedFinancialsFetcher,
|
| 109 |
+
"SearchAttributes": IntrinioSearchAttributesFetcher,
|
| 110 |
+
"ShareStatistics": IntrinioShareStatisticsFetcher,
|
| 111 |
+
"WorldNews": IntrinioWorldNewsFetcher,
|
| 112 |
+
},
|
| 113 |
+
repr_name="Intrinio",
|
| 114 |
+
deprecated_credentials={"API_INTRINIO_KEY": "intrinio_api_key"},
|
| 115 |
+
instructions="Go to: https://intrinio.com/starter-plan\n\n\n\nAn API key will be issued with a subscription. Find the token value within the account dashboard.", # noqa: E501 pylint: disable=line-too-long
|
| 116 |
+
)
|
openbb_platform/providers/intrinio/openbb_intrinio/models/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio models directory."""
|
openbb_platform/providers/intrinio/openbb_intrinio/models/balance_sheet.py
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Balance Sheet Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 6 |
+
from warnings import warn
|
| 7 |
+
|
| 8 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 9 |
+
from openbb_core.provider.standard_models.balance_sheet import (
|
| 10 |
+
BalanceSheetData,
|
| 11 |
+
BalanceSheetQueryParams,
|
| 12 |
+
)
|
| 13 |
+
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
| 14 |
+
from openbb_core.provider.utils.helpers import ClientResponse, amake_requests
|
| 15 |
+
from openbb_intrinio.utils.helpers import get_data_one
|
| 16 |
+
from pydantic import Field, field_validator, model_validator
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class IntrinioBalanceSheetQueryParams(BalanceSheetQueryParams):
|
| 20 |
+
"""Intrinio Balance Sheet Query.
|
| 21 |
+
|
| 22 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_company_fundamentals_v2
|
| 23 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_fundamental_standardized_financials_v2
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
__json_schema_extra__ = {
|
| 27 |
+
"period": {
|
| 28 |
+
"choices": ["annual", "quarter"],
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
period: Literal["annual", "quarter"] = Field(
|
| 33 |
+
default="annual",
|
| 34 |
+
description=QUERY_DESCRIPTIONS.get("period", ""),
|
| 35 |
+
)
|
| 36 |
+
fiscal_year: Optional[int] = Field(
|
| 37 |
+
default=None,
|
| 38 |
+
description="The specific fiscal year. Reports do not go beyond 2008.",
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
@field_validator("symbol", mode="after", check_fields=False)
|
| 42 |
+
@classmethod
|
| 43 |
+
def handle_symbol(cls, v) -> str:
|
| 44 |
+
"""Handle symbols with a dash and replace it with a dot for Intrinio."""
|
| 45 |
+
return v.replace("-", ".")
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class IntrinioBalanceSheetData(BalanceSheetData):
|
| 49 |
+
"""Intrinio Balance Sheet Data."""
|
| 50 |
+
|
| 51 |
+
__alias_dict__ = {
|
| 52 |
+
"cash_and_cash_equivalents": "cashandequivalents",
|
| 53 |
+
"restricted_cash": "restrictedcash",
|
| 54 |
+
"short_term_investments": "shortterminvestments",
|
| 55 |
+
"federal_funds_sold": "fedfundssold",
|
| 56 |
+
"note_and_lease_receivable": "notereceivable",
|
| 57 |
+
"interest_bearing_deposits_at_other_banks": "interestbearingdepositsatotherbanks",
|
| 58 |
+
"accounts_receivable": "accountsreceivable",
|
| 59 |
+
"time_deposits_placed_and_other_short_term_investments": "timedepositsplaced",
|
| 60 |
+
"inventories": "netinventory",
|
| 61 |
+
"trading_account_securities": "tradingaccountsecurities",
|
| 62 |
+
"prepaid_expenses": "prepaidexpenses",
|
| 63 |
+
"loans_and_leases": "loansandleases",
|
| 64 |
+
"allowance_for_loan_and_lease_losses": "allowanceforloanandleaselosses",
|
| 65 |
+
"current_deferred_refundable_income_taxes": "currentdeferredtaxassets",
|
| 66 |
+
"other_current_assets": "othercurrentassets",
|
| 67 |
+
"loans_and_leases_net_of_allowance": "netloansandleases",
|
| 68 |
+
"other_current_non_operating_assets": "othercurrentnonoperatingassets",
|
| 69 |
+
"loans_held_for_sale": "loansheldforsale",
|
| 70 |
+
"total_current_assets": "totalcurrentassets",
|
| 71 |
+
"accrued_investment_income": "accruedinvestmentincome",
|
| 72 |
+
"plant_property_equipment_gross": "grossppe",
|
| 73 |
+
"customer_and_other_receivables": "customerandotherreceivables",
|
| 74 |
+
"accumulated_depreciation": "accumulateddepreciation",
|
| 75 |
+
"premises_and_equipment_net": "netpremisesandequipment",
|
| 76 |
+
"plant_property_equipment_net": "netppe",
|
| 77 |
+
"mortgage_servicing_rights": "mortgageservicingrights",
|
| 78 |
+
"long_term_investments": "longterminvestments",
|
| 79 |
+
"unearned_premiums_asset": "unearnedpremiumsdebit",
|
| 80 |
+
"non_current_note_lease_receivables": "noncurrentnotereceivables",
|
| 81 |
+
"deferred_acquisition_cost": "deferredacquisitioncost",
|
| 82 |
+
"goodwill": "goodwill",
|
| 83 |
+
"separate_account_business_assets": "separateaccountbusinessassets",
|
| 84 |
+
"intangible_assets": "intangibleassets",
|
| 85 |
+
"non_current_deferred_refundable_income_taxes": "noncurrentdeferredtaxassets",
|
| 86 |
+
"employee_benefit_assets": "employeebenefitassets",
|
| 87 |
+
"other_assets": "otherassets",
|
| 88 |
+
"other_non_current_operating_assets": "othernoncurrentassets",
|
| 89 |
+
"total_assets": "totalassets",
|
| 90 |
+
"other_non_current_non_operating_assets": "othernoncurrentnonoperatingassets",
|
| 91 |
+
"non_interest_bearing_deposits": "noninterestbearingdeposits",
|
| 92 |
+
"interest_bearing_deposits": "interestbearingdeposits",
|
| 93 |
+
"total_non_current_assets": "totalnoncurrentassets",
|
| 94 |
+
"federal_funds_purchased_and_securities_sold": "fedfundspurchased",
|
| 95 |
+
"short_term_debt": "shorttermdebt",
|
| 96 |
+
"bankers_acceptance_out_standing": "bankersacceptances",
|
| 97 |
+
"accrued_interest_payable": "accruedinterestpayable",
|
| 98 |
+
"accounts_payable": "accountspayable",
|
| 99 |
+
"accrued_expenses": "accruedexpenses",
|
| 100 |
+
"other_short_term_payables": "othershorttermpayables",
|
| 101 |
+
"long_term_debt": "longtermdebt",
|
| 102 |
+
"customer_deposits": "customerdeposits",
|
| 103 |
+
"capital_lease_obligations": "capitalleaseobligations",
|
| 104 |
+
"dividends_payable": "dividendspayable",
|
| 105 |
+
"claims_and_claim_expense": "claimsandclaimexpenses",
|
| 106 |
+
"current_deferred_revenue": "currentdeferredrevenue",
|
| 107 |
+
"future_policy_benefits": "futurepolicybenefits",
|
| 108 |
+
"current_deferred_payable_income_tax_liabilities": "currentdeferredtaxliabilities",
|
| 109 |
+
"current_employee_benefit_liabilities": "currentemployeebenefitliabilities",
|
| 110 |
+
"unearned_premiums_liability": "unearnedpremiumscredit",
|
| 111 |
+
"other_taxes_payable": "othertaxespayable",
|
| 112 |
+
"policy_holder_funds": "policyholderfunds",
|
| 113 |
+
"other_current_liabilities": "othercurrentliabilities",
|
| 114 |
+
"participating_policy_holder_equity": "participatingpolicyholderequity",
|
| 115 |
+
"other_current_non_operating_liabilities": "othercurrentnonoperatingliabilities",
|
| 116 |
+
"separate_account_business_liabilities": "separateaccountbusinessliabilities",
|
| 117 |
+
"total_current_liabilities": "totalcurrentliabilities",
|
| 118 |
+
"other_long_term_liabilities": "otherlongtermliabilities",
|
| 119 |
+
"total_liabilities": "totalliabilities",
|
| 120 |
+
"commitments_contingencies": "commitmentsandcontingencies",
|
| 121 |
+
"asset_retirement_reserve_litigation_obligation": "assetretirementandlitigationobligation",
|
| 122 |
+
"redeemable_non_controlling_interest": "redeemablenoncontrollinginterest",
|
| 123 |
+
"non_current_deferred_revenue": "noncurrentdeferredrevenue",
|
| 124 |
+
"preferred_stock": "totalpreferredequity",
|
| 125 |
+
"common_stock": "commonequity",
|
| 126 |
+
"non_current_deferred_payable_income_tax_liabilities": "noncurrentdeferredtaxliabilities",
|
| 127 |
+
"non_current_employee_benefit_liabilities": "noncurrentemployeebenefitliabilities",
|
| 128 |
+
"retained_earnings": "retainedearnings",
|
| 129 |
+
"other_non_current_operating_liabilities": "othernoncurrentliabilities",
|
| 130 |
+
"treasury_stock": "treasurystock",
|
| 131 |
+
"accumulated_other_comprehensive_income": "aoci",
|
| 132 |
+
"other_non_current_non_operating_liabilities": "othernoncurrentnonoperatingliabilities",
|
| 133 |
+
"other_equity_adjustments": "otherequity",
|
| 134 |
+
"total_non_current_liabilities": "totalnoncurrentliabilities",
|
| 135 |
+
"total_common_equity": "totalcommonequity",
|
| 136 |
+
"total_preferred_common_equity": "totalequity",
|
| 137 |
+
"non_controlling_interest": "noncontrollinginterests",
|
| 138 |
+
"total_equity_non_controlling_interests": "totalequityandnoncontrollinginterests",
|
| 139 |
+
"total_liabilities_shareholders_equity": "totalliabilitiesandequity",
|
| 140 |
+
}
|
| 141 |
+
reported_currency: Optional[str] = Field(
|
| 142 |
+
description="The currency in which the balance sheet is reported.",
|
| 143 |
+
default=None,
|
| 144 |
+
)
|
| 145 |
+
cash_and_cash_equivalents: Optional[float] = Field(
|
| 146 |
+
description="Cash and cash equivalents.", default=None
|
| 147 |
+
)
|
| 148 |
+
cash_and_due_from_banks: Optional[float] = Field(
|
| 149 |
+
description="Cash and due from banks.", default=None
|
| 150 |
+
)
|
| 151 |
+
restricted_cash: Optional[float] = Field(
|
| 152 |
+
description="Restricted cash.", default=None
|
| 153 |
+
)
|
| 154 |
+
short_term_investments: Optional[float] = Field(
|
| 155 |
+
description="Short term investments.", default=None
|
| 156 |
+
)
|
| 157 |
+
federal_funds_sold: Optional[float] = Field(
|
| 158 |
+
description="Federal funds sold.", default=None
|
| 159 |
+
)
|
| 160 |
+
accounts_receivable: Optional[float] = Field(
|
| 161 |
+
description="Accounts receivable.", default=None
|
| 162 |
+
)
|
| 163 |
+
note_and_lease_receivable: Optional[float] = Field(
|
| 164 |
+
description="Note and lease receivable. (Vendor non-trade receivables)",
|
| 165 |
+
default=None,
|
| 166 |
+
)
|
| 167 |
+
inventories: Optional[float] = Field(description="Net Inventories.", default=None)
|
| 168 |
+
customer_and_other_receivables: Optional[float] = Field(
|
| 169 |
+
description="Customer and other receivables.", default=None
|
| 170 |
+
)
|
| 171 |
+
interest_bearing_deposits_at_other_banks: Optional[float] = Field(
|
| 172 |
+
description="Interest bearing deposits at other banks.", default=None
|
| 173 |
+
)
|
| 174 |
+
time_deposits_placed_and_other_short_term_investments: Optional[float] = Field(
|
| 175 |
+
description="Time deposits placed and other short term investments.",
|
| 176 |
+
default=None,
|
| 177 |
+
)
|
| 178 |
+
trading_account_securities: Optional[float] = Field(
|
| 179 |
+
description="Trading account securities.", default=None
|
| 180 |
+
)
|
| 181 |
+
loans_and_leases: Optional[float] = Field(
|
| 182 |
+
description="Loans and leases.", default=None
|
| 183 |
+
)
|
| 184 |
+
allowance_for_loan_and_lease_losses: Optional[float] = Field(
|
| 185 |
+
description="Allowance for loan and lease losses.", default=None
|
| 186 |
+
)
|
| 187 |
+
current_deferred_refundable_income_taxes: Optional[float] = Field(
|
| 188 |
+
description="Current deferred refundable income taxes.", default=None
|
| 189 |
+
)
|
| 190 |
+
other_current_assets: Optional[float] = Field(
|
| 191 |
+
description="Other current assets.", default=None
|
| 192 |
+
)
|
| 193 |
+
loans_and_leases_net_of_allowance: Optional[float] = Field(
|
| 194 |
+
description="Loans and leases net of allowance.", default=None
|
| 195 |
+
)
|
| 196 |
+
accrued_investment_income: Optional[float] = Field(
|
| 197 |
+
description="Accrued investment income.", default=None
|
| 198 |
+
)
|
| 199 |
+
other_current_non_operating_assets: Optional[float] = Field(
|
| 200 |
+
description="Other current non-operating assets.", default=None
|
| 201 |
+
)
|
| 202 |
+
loans_held_for_sale: Optional[float] = Field(
|
| 203 |
+
description="Loans held for sale.", default=None
|
| 204 |
+
)
|
| 205 |
+
prepaid_expenses: Optional[float] = Field(
|
| 206 |
+
description="Prepaid expenses.", default=None
|
| 207 |
+
)
|
| 208 |
+
total_current_assets: Optional[float] = Field(
|
| 209 |
+
description="Total current assets.", default=None
|
| 210 |
+
)
|
| 211 |
+
plant_property_equipment_gross: Optional[float] = Field(
|
| 212 |
+
description="Plant property equipment gross.", default=None
|
| 213 |
+
)
|
| 214 |
+
accumulated_depreciation: Optional[float] = Field(
|
| 215 |
+
description="Accumulated depreciation.", default=None
|
| 216 |
+
)
|
| 217 |
+
premises_and_equipment_net: Optional[float] = Field(
|
| 218 |
+
description="Net premises and equipment.", default=None
|
| 219 |
+
)
|
| 220 |
+
plant_property_equipment_net: Optional[float] = Field(
|
| 221 |
+
description="Net plant property equipment.", default=None
|
| 222 |
+
)
|
| 223 |
+
long_term_investments: Optional[float] = Field(
|
| 224 |
+
description="Long term investments.", default=None
|
| 225 |
+
)
|
| 226 |
+
mortgage_servicing_rights: Optional[float] = Field(
|
| 227 |
+
description="Mortgage servicing rights.", default=None
|
| 228 |
+
)
|
| 229 |
+
unearned_premiums_asset: Optional[float] = Field(
|
| 230 |
+
description="Unearned premiums asset.", default=None
|
| 231 |
+
)
|
| 232 |
+
non_current_note_lease_receivables: Optional[float] = Field(
|
| 233 |
+
description="Non-current note lease receivables.", default=None
|
| 234 |
+
)
|
| 235 |
+
deferred_acquisition_cost: Optional[float] = Field(
|
| 236 |
+
description="Deferred acquisition cost.", default=None
|
| 237 |
+
)
|
| 238 |
+
goodwill: Optional[float] = Field(description="Goodwill.", default=None)
|
| 239 |
+
separate_account_business_assets: Optional[float] = Field(
|
| 240 |
+
description="Separate account business assets.", default=None
|
| 241 |
+
)
|
| 242 |
+
non_current_deferred_refundable_income_taxes: Optional[float] = Field(
|
| 243 |
+
description="Noncurrent deferred refundable income taxes.", default=None
|
| 244 |
+
)
|
| 245 |
+
intangible_assets: Optional[float] = Field(
|
| 246 |
+
description="Intangible assets.", default=None
|
| 247 |
+
)
|
| 248 |
+
employee_benefit_assets: Optional[float] = Field(
|
| 249 |
+
description="Employee benefit assets.", default=None
|
| 250 |
+
)
|
| 251 |
+
other_assets: Optional[float] = Field(description="Other assets.", default=None)
|
| 252 |
+
other_non_current_operating_assets: Optional[float] = Field(
|
| 253 |
+
description="Other noncurrent operating assets.", default=None
|
| 254 |
+
)
|
| 255 |
+
other_non_current_non_operating_assets: Optional[float] = Field(
|
| 256 |
+
description="Other noncurrent non-operating assets.", default=None
|
| 257 |
+
)
|
| 258 |
+
interest_bearing_deposits: Optional[float] = Field(
|
| 259 |
+
description="Interest bearing deposits.", default=None
|
| 260 |
+
)
|
| 261 |
+
total_non_current_assets: Optional[float] = Field(
|
| 262 |
+
description="Total noncurrent assets.", default=None
|
| 263 |
+
)
|
| 264 |
+
total_assets: Optional[float] = Field(description="Total assets.", default=None)
|
| 265 |
+
non_interest_bearing_deposits: Optional[float] = Field(
|
| 266 |
+
description="Non interest bearing deposits.", default=None
|
| 267 |
+
)
|
| 268 |
+
federal_funds_purchased_and_securities_sold: Optional[float] = Field(
|
| 269 |
+
description="Federal funds purchased and securities sold.", default=None
|
| 270 |
+
)
|
| 271 |
+
bankers_acceptance_outstanding: Optional[float] = Field(
|
| 272 |
+
description="Bankers acceptance outstanding.", default=None
|
| 273 |
+
)
|
| 274 |
+
short_term_debt: Optional[float] = Field(
|
| 275 |
+
description="Short term debt.", default=None
|
| 276 |
+
)
|
| 277 |
+
accounts_payable: Optional[float] = Field(
|
| 278 |
+
description="Accounts payable.", default=None
|
| 279 |
+
)
|
| 280 |
+
current_deferred_revenue: Optional[float] = Field(
|
| 281 |
+
description="Current deferred revenue.", default=None
|
| 282 |
+
)
|
| 283 |
+
current_deferred_payable_income_tax_liabilities: Optional[float] = Field(
|
| 284 |
+
description="Current deferred payable income tax liabilities.", default=None
|
| 285 |
+
)
|
| 286 |
+
accrued_interest_payable: Optional[float] = Field(
|
| 287 |
+
description="Accrued interest payable.", default=None
|
| 288 |
+
)
|
| 289 |
+
accrued_expenses: Optional[float] = Field(
|
| 290 |
+
description="Accrued expenses.", default=None
|
| 291 |
+
)
|
| 292 |
+
other_short_term_payables: Optional[float] = Field(
|
| 293 |
+
description="Other short term payables.", default=None
|
| 294 |
+
)
|
| 295 |
+
customer_deposits: Optional[float] = Field(
|
| 296 |
+
description="Customer deposits.", default=None
|
| 297 |
+
)
|
| 298 |
+
dividends_payable: Optional[float] = Field(
|
| 299 |
+
description="Dividends payable.", default=None
|
| 300 |
+
)
|
| 301 |
+
claims_and_claim_expense: Optional[float] = Field(
|
| 302 |
+
description="Claims and claim expense.", default=None
|
| 303 |
+
)
|
| 304 |
+
future_policy_benefits: Optional[float] = Field(
|
| 305 |
+
description="Future policy benefits.", default=None
|
| 306 |
+
)
|
| 307 |
+
current_employee_benefit_liabilities: Optional[float] = Field(
|
| 308 |
+
description="Current employee benefit liabilities.", default=None
|
| 309 |
+
)
|
| 310 |
+
unearned_premiums_liability: Optional[float] = Field(
|
| 311 |
+
description="Unearned premiums liability.", default=None
|
| 312 |
+
)
|
| 313 |
+
other_taxes_payable: Optional[float] = Field(
|
| 314 |
+
description="Other taxes payable.", default=None
|
| 315 |
+
)
|
| 316 |
+
policy_holder_funds: Optional[float] = Field(
|
| 317 |
+
description="Policy holder funds.", default=None
|
| 318 |
+
)
|
| 319 |
+
other_current_liabilities: Optional[float] = Field(
|
| 320 |
+
description="Other current liabilities.", default=None
|
| 321 |
+
)
|
| 322 |
+
other_current_non_operating_liabilities: Optional[float] = Field(
|
| 323 |
+
description="Other current non-operating liabilities.", default=None
|
| 324 |
+
)
|
| 325 |
+
separate_account_business_liabilities: Optional[float] = Field(
|
| 326 |
+
description="Separate account business liabilities.", default=None
|
| 327 |
+
)
|
| 328 |
+
total_current_liabilities: Optional[float] = Field(
|
| 329 |
+
description="Total current liabilities.", default=None
|
| 330 |
+
)
|
| 331 |
+
long_term_debt: Optional[float] = Field(description="Long term debt.", default=None)
|
| 332 |
+
other_long_term_liabilities: Optional[float] = Field(
|
| 333 |
+
description="Other long term liabilities.", default=None
|
| 334 |
+
)
|
| 335 |
+
non_current_deferred_revenue: Optional[float] = Field(
|
| 336 |
+
description="Non-current deferred revenue.", default=None
|
| 337 |
+
)
|
| 338 |
+
non_current_deferred_payable_income_tax_liabilities: Optional[float] = Field(
|
| 339 |
+
description="Non-current deferred payable income tax liabilities.", default=None
|
| 340 |
+
)
|
| 341 |
+
non_current_employee_benefit_liabilities: Optional[float] = Field(
|
| 342 |
+
description="Non-current employee benefit liabilities.", default=None
|
| 343 |
+
)
|
| 344 |
+
other_non_current_operating_liabilities: Optional[float] = Field(
|
| 345 |
+
description="Other non-current operating liabilities.", default=None
|
| 346 |
+
)
|
| 347 |
+
other_non_current_non_operating_liabilities: Optional[float] = Field(
|
| 348 |
+
description="Other non-current, non-operating liabilities.", default=None
|
| 349 |
+
)
|
| 350 |
+
total_non_current_liabilities: Optional[float] = Field(
|
| 351 |
+
description="Total non-current liabilities.", default=None
|
| 352 |
+
)
|
| 353 |
+
capital_lease_obligations: Optional[float] = Field(
|
| 354 |
+
description="Capital lease obligations.", default=None
|
| 355 |
+
)
|
| 356 |
+
asset_retirement_reserve_litigation_obligation: Optional[float] = Field(
|
| 357 |
+
description="Asset retirement reserve litigation obligation.", default=None
|
| 358 |
+
)
|
| 359 |
+
total_liabilities: Optional[float] = Field(
|
| 360 |
+
description="Total liabilities.", default=None
|
| 361 |
+
)
|
| 362 |
+
commitments_contingencies: Optional[float] = Field(
|
| 363 |
+
description="Commitments contingencies.", default=None
|
| 364 |
+
)
|
| 365 |
+
redeemable_non_controlling_interest: Optional[float] = Field(
|
| 366 |
+
description="Redeemable non-controlling interest.", default=None
|
| 367 |
+
)
|
| 368 |
+
preferred_stock: Optional[float] = Field(
|
| 369 |
+
description="Preferred stock.", default=None
|
| 370 |
+
)
|
| 371 |
+
common_stock: Optional[float] = Field(description="Common stock.", default=None)
|
| 372 |
+
retained_earnings: Optional[float] = Field(
|
| 373 |
+
description="Retained earnings.", default=None
|
| 374 |
+
)
|
| 375 |
+
treasury_stock: Optional[float] = Field(description="Treasury stock.", default=None)
|
| 376 |
+
accumulated_other_comprehensive_income: Optional[float] = Field(
|
| 377 |
+
description="Accumulated other comprehensive income.", default=None
|
| 378 |
+
)
|
| 379 |
+
participating_policy_holder_equity: Optional[float] = Field(
|
| 380 |
+
description="Participating policy holder equity.", default=None
|
| 381 |
+
)
|
| 382 |
+
other_equity_adjustments: Optional[float] = Field(
|
| 383 |
+
description="Other equity adjustments.", default=None
|
| 384 |
+
)
|
| 385 |
+
total_common_equity: Optional[float] = Field(
|
| 386 |
+
description="Total common equity.", default=None
|
| 387 |
+
)
|
| 388 |
+
total_preferred_common_equity: Optional[float] = Field(
|
| 389 |
+
description="Total preferred common equity.", default=None
|
| 390 |
+
)
|
| 391 |
+
non_controlling_interest: Optional[float] = Field(
|
| 392 |
+
description="Non-controlling interest.", default=None
|
| 393 |
+
)
|
| 394 |
+
total_equity_non_controlling_interests: Optional[float] = Field(
|
| 395 |
+
description="Total equity non-controlling interests.", default=None
|
| 396 |
+
)
|
| 397 |
+
total_liabilities_shareholders_equity: Optional[float] = Field(
|
| 398 |
+
description="Total liabilities and shareholders equity.", default=None
|
| 399 |
+
)
|
| 400 |
+
|
| 401 |
+
@model_validator(mode="before")
|
| 402 |
+
@classmethod
|
| 403 |
+
def replace_zero(cls, values): # pylint: disable=no-self-argument
|
| 404 |
+
"""Check for zero values and replace with None."""
|
| 405 |
+
return (
|
| 406 |
+
{k: None if v == 0 else v for k, v in values.items()}
|
| 407 |
+
if isinstance(values, dict)
|
| 408 |
+
else values
|
| 409 |
+
)
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
class IntrinioBalanceSheetFetcher(
|
| 413 |
+
Fetcher[
|
| 414 |
+
IntrinioBalanceSheetQueryParams,
|
| 415 |
+
List[IntrinioBalanceSheetData],
|
| 416 |
+
]
|
| 417 |
+
):
|
| 418 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 419 |
+
|
| 420 |
+
@staticmethod
|
| 421 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioBalanceSheetQueryParams:
|
| 422 |
+
"""Transform the query params."""
|
| 423 |
+
return IntrinioBalanceSheetQueryParams(**params)
|
| 424 |
+
|
| 425 |
+
@staticmethod
|
| 426 |
+
async def aextract_data(
|
| 427 |
+
query: IntrinioBalanceSheetQueryParams,
|
| 428 |
+
credentials: Optional[Dict[str, str]],
|
| 429 |
+
**kwargs: Any,
|
| 430 |
+
) -> List[Dict]:
|
| 431 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 432 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 433 |
+
statement_code = "balance_sheet_statement"
|
| 434 |
+
fundamentals_data: Dict = {}
|
| 435 |
+
base_url = "https://api-v2.intrinio.com"
|
| 436 |
+
period = "FY" if query.period == "annual" else "QTR"
|
| 437 |
+
fundamentals_url = (
|
| 438 |
+
f"{base_url}/companies/{query.symbol}/fundamentals?"
|
| 439 |
+
f"statement_code={statement_code}&type={period}"
|
| 440 |
+
)
|
| 441 |
+
if query.fiscal_year is not None:
|
| 442 |
+
if query.fiscal_year < 2008:
|
| 443 |
+
warn("Financials data is only available from 2008 and later.")
|
| 444 |
+
query.fiscal_year = 2008
|
| 445 |
+
fundamentals_url = fundamentals_url + f"&fiscal_year={query.fiscal_year}"
|
| 446 |
+
fundamentals_url = fundamentals_url + f"&api_key={api_key}"
|
| 447 |
+
fundamentals_data = (await get_data_one(fundamentals_url, **kwargs)).get(
|
| 448 |
+
"fundamentals", []
|
| 449 |
+
)
|
| 450 |
+
fiscal_periods = [
|
| 451 |
+
f"{item['fiscal_year']}-{item['fiscal_period']}"
|
| 452 |
+
for item in fundamentals_data
|
| 453 |
+
]
|
| 454 |
+
fiscal_periods = fiscal_periods[: query.limit]
|
| 455 |
+
|
| 456 |
+
async def callback(response: ClientResponse, _: Any) -> Dict:
|
| 457 |
+
"""Return the response."""
|
| 458 |
+
statement_data = await response.json() # type: ignore
|
| 459 |
+
return {
|
| 460 |
+
"period_ending": statement_data["fundamental"]["end_date"], # type: ignore
|
| 461 |
+
"fiscal_year": statement_data["fundamental"]["fiscal_year"], # type: ignore
|
| 462 |
+
"fiscal_period": statement_data["fundamental"]["fiscal_period"], # type: ignore
|
| 463 |
+
"financials": statement_data["standardized_financials"], # type: ignore
|
| 464 |
+
}
|
| 465 |
+
|
| 466 |
+
urls = [
|
| 467 |
+
f"{base_url}/fundamentals/{query.symbol}-{statement_code}-{p}/standardized_financials?api_key={api_key}"
|
| 468 |
+
for p in fiscal_periods
|
| 469 |
+
]
|
| 470 |
+
|
| 471 |
+
return await amake_requests(urls, callback, **kwargs) # type: ignore
|
| 472 |
+
|
| 473 |
+
@staticmethod
|
| 474 |
+
def transform_data(
|
| 475 |
+
query: IntrinioBalanceSheetQueryParams, data: List[Dict], **kwargs: Any
|
| 476 |
+
) -> List[IntrinioBalanceSheetData]:
|
| 477 |
+
"""Return the transformed data."""
|
| 478 |
+
transformed_data: List[IntrinioBalanceSheetData] = []
|
| 479 |
+
period = "FY" if query.period == "annual" else "QTR"
|
| 480 |
+
units = []
|
| 481 |
+
for item in data:
|
| 482 |
+
sub_dict: Dict[str, Any] = {}
|
| 483 |
+
|
| 484 |
+
for sub_item in item["financials"]:
|
| 485 |
+
field_name = sub_item["data_tag"]["tag"]
|
| 486 |
+
unit = sub_item["data_tag"].get("unit", "")
|
| 487 |
+
if unit and len(unit) == 3:
|
| 488 |
+
units.append(unit)
|
| 489 |
+
sub_dict[field_name] = (
|
| 490 |
+
float(sub_item["value"])
|
| 491 |
+
if sub_item["value"] and sub_item["value"] != 0
|
| 492 |
+
else None
|
| 493 |
+
)
|
| 494 |
+
|
| 495 |
+
sub_dict["period_ending"] = item["period_ending"]
|
| 496 |
+
sub_dict["fiscal_year"] = item["fiscal_year"]
|
| 497 |
+
sub_dict["fiscal_period"] = item["fiscal_period"]
|
| 498 |
+
sub_dict["reported_currency"] = list(set(units))[0]
|
| 499 |
+
|
| 500 |
+
# Intrinio does not return Q4 data but FY data instead
|
| 501 |
+
if period == "QTR" and item["fiscal_period"] == "FY":
|
| 502 |
+
sub_dict["fiscal_period"] = "Q4"
|
| 503 |
+
|
| 504 |
+
transformed_data.append(IntrinioBalanceSheetData(**sub_dict))
|
| 505 |
+
|
| 506 |
+
return transformed_data
|
openbb_platform/providers/intrinio/openbb_intrinio/models/calendar_ipo.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio IPO Calendar Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 6 |
+
|
| 7 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 8 |
+
from openbb_core.provider.standard_models.calendar_ipo import (
|
| 9 |
+
CalendarIpoData,
|
| 10 |
+
CalendarIpoQueryParams,
|
| 11 |
+
)
|
| 12 |
+
from openbb_core.provider.utils.errors import EmptyDataError
|
| 13 |
+
from openbb_core.provider.utils.helpers import get_querystring
|
| 14 |
+
from openbb_intrinio.utils.helpers import get_data_one
|
| 15 |
+
from openbb_intrinio.utils.references import IntrinioCompany, IntrinioSecurity
|
| 16 |
+
from pydantic import Field
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class IntrinioCalendarIpoQueryParams(CalendarIpoQueryParams):
|
| 20 |
+
"""Intrinio IPO Calendar Query.
|
| 21 |
+
|
| 22 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_company_ipos_v2
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
__alias_dict__ = {
|
| 26 |
+
"symbol": "ticker",
|
| 27 |
+
"limit": "page_size",
|
| 28 |
+
"min_value": "offer_amount_greater_than",
|
| 29 |
+
"max_value": "offer_amount_less_than",
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
status: Optional[Literal["upcoming", "priced", "withdrawn"]] = Field(
|
| 33 |
+
description="Status of the IPO. [upcoming, priced, or withdrawn]", default=None
|
| 34 |
+
)
|
| 35 |
+
min_value: Optional[int] = Field(
|
| 36 |
+
description="Return IPOs with an offer dollar amount greater than the given amount.",
|
| 37 |
+
default=None,
|
| 38 |
+
)
|
| 39 |
+
max_value: Optional[int] = Field(
|
| 40 |
+
description="Return IPOs with an offer dollar amount less than the given amount.",
|
| 41 |
+
default=None,
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class IntrinioCalendarIpoData(CalendarIpoData):
|
| 46 |
+
"""Intrinio IPO Calendar Data."""
|
| 47 |
+
|
| 48 |
+
__alias_dict__ = {"symbol": "ticker", "ipo_date": "date"}
|
| 49 |
+
|
| 50 |
+
status: Optional[Literal["upcoming", "priced", "withdrawn"]] = Field(
|
| 51 |
+
description=(
|
| 52 |
+
"The status of the IPO. Upcoming IPOs have not taken place yet but are expected to. "
|
| 53 |
+
"Priced IPOs have taken place. Withdrawn IPOs were expected to take place, but were subsequently withdrawn."
|
| 54 |
+
),
|
| 55 |
+
default=None,
|
| 56 |
+
)
|
| 57 |
+
exchange: Optional[str] = Field(
|
| 58 |
+
description=(
|
| 59 |
+
"The acronym of the stock exchange that the company is going to trade publicly on. "
|
| 60 |
+
"Typically NYSE or NASDAQ."
|
| 61 |
+
),
|
| 62 |
+
default=None,
|
| 63 |
+
)
|
| 64 |
+
offer_amount: Optional[float] = Field(
|
| 65 |
+
description="The total dollar amount of shares offered in the IPO. Typically this is share price * share count",
|
| 66 |
+
default=None,
|
| 67 |
+
)
|
| 68 |
+
share_price: Optional[float] = Field(
|
| 69 |
+
description="The price per share at which the IPO was offered.", default=None
|
| 70 |
+
)
|
| 71 |
+
share_price_lowest: Optional[float] = Field(
|
| 72 |
+
description=(
|
| 73 |
+
"The expected lowest price per share at which the IPO will be offered. "
|
| 74 |
+
"Before an IPO is priced, companies typically provide a range of prices per share at which "
|
| 75 |
+
"they expect to offer the IPO (typically available for upcoming IPOs)."
|
| 76 |
+
),
|
| 77 |
+
default=None,
|
| 78 |
+
)
|
| 79 |
+
share_price_highest: Optional[float] = Field(
|
| 80 |
+
description=(
|
| 81 |
+
"The expected highest price per share at which the IPO will be offered. "
|
| 82 |
+
"Before an IPO is priced, companies typically provide a range of prices per share at which "
|
| 83 |
+
"they expect to offer the IPO (typically available for upcoming IPOs)."
|
| 84 |
+
),
|
| 85 |
+
default=None,
|
| 86 |
+
)
|
| 87 |
+
share_count: Optional[int] = Field(
|
| 88 |
+
description="The number of shares offered in the IPO.", default=None
|
| 89 |
+
)
|
| 90 |
+
share_count_lowest: Optional[int] = Field(
|
| 91 |
+
description=(
|
| 92 |
+
"The expected lowest number of shares that will be offered in the IPO. Before an IPO is priced, "
|
| 93 |
+
"companies typically provide a range of shares that they expect to offer in the IPO "
|
| 94 |
+
"(typically available for upcoming IPOs)."
|
| 95 |
+
),
|
| 96 |
+
default=None,
|
| 97 |
+
)
|
| 98 |
+
share_count_highest: Optional[int] = Field(
|
| 99 |
+
description=(
|
| 100 |
+
"The expected highest number of shares that will be offered in the IPO. Before an IPO is priced, "
|
| 101 |
+
"companies typically provide a range of shares that they expect to offer in the IPO "
|
| 102 |
+
"(typically available for upcoming IPOs)."
|
| 103 |
+
),
|
| 104 |
+
default=None,
|
| 105 |
+
)
|
| 106 |
+
announcement_url: Optional[str] = Field(
|
| 107 |
+
description="The URL to the company's announcement of the IPO", default=None
|
| 108 |
+
)
|
| 109 |
+
sec_report_url: Optional[str] = Field(
|
| 110 |
+
description=(
|
| 111 |
+
"The URL to the company's S-1, S-1/A, F-1, or F-1/A SEC filing, which is required to be filed "
|
| 112 |
+
"before an IPO takes place."
|
| 113 |
+
),
|
| 114 |
+
default=None,
|
| 115 |
+
)
|
| 116 |
+
open_price: Optional[float] = Field(
|
| 117 |
+
description="The opening price at the beginning of the first trading day (only available for priced IPOs).",
|
| 118 |
+
default=None,
|
| 119 |
+
)
|
| 120 |
+
close_price: Optional[float] = Field(
|
| 121 |
+
description="The closing price at the end of the first trading day (only available for priced IPOs).",
|
| 122 |
+
default=None,
|
| 123 |
+
)
|
| 124 |
+
volume: Optional[int] = Field(
|
| 125 |
+
description="The volume at the end of the first trading day (only available for priced IPOs).",
|
| 126 |
+
default=None,
|
| 127 |
+
)
|
| 128 |
+
day_change: Optional[float] = Field(
|
| 129 |
+
description=(
|
| 130 |
+
"The percentage change between the open price and the close price on the first trading day "
|
| 131 |
+
"(only available for priced IPOs)."
|
| 132 |
+
),
|
| 133 |
+
default=None,
|
| 134 |
+
)
|
| 135 |
+
week_change: Optional[float] = Field(
|
| 136 |
+
description=(
|
| 137 |
+
"The percentage change between the open price on the first trading day and the close price approximately "
|
| 138 |
+
"a week after the first trading day (only available for priced IPOs)."
|
| 139 |
+
),
|
| 140 |
+
default=None,
|
| 141 |
+
)
|
| 142 |
+
month_change: Optional[float] = Field(
|
| 143 |
+
description=(
|
| 144 |
+
"The percentage change between the open price on the first trading day and the close price approximately "
|
| 145 |
+
"a month after the first trading day (only available for priced IPOs)."
|
| 146 |
+
),
|
| 147 |
+
default=None,
|
| 148 |
+
)
|
| 149 |
+
id: Optional[str] = Field(description="The Intrinio ID of the IPO.", default=None)
|
| 150 |
+
company: Optional[IntrinioCompany] = Field(
|
| 151 |
+
description="The company that is going public via the IPO.", default=None
|
| 152 |
+
)
|
| 153 |
+
security: Optional[IntrinioSecurity] = Field(
|
| 154 |
+
description="The primary Security for the Company that is going public via the IPO",
|
| 155 |
+
default=None,
|
| 156 |
+
)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
class IntrinioCalendarIpoFetcher(
|
| 160 |
+
Fetcher[IntrinioCalendarIpoQueryParams, List[IntrinioCalendarIpoData]]
|
| 161 |
+
):
|
| 162 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 163 |
+
|
| 164 |
+
@staticmethod
|
| 165 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioCalendarIpoQueryParams:
|
| 166 |
+
"""Transform the query params."""
|
| 167 |
+
return IntrinioCalendarIpoQueryParams(**params)
|
| 168 |
+
|
| 169 |
+
@staticmethod
|
| 170 |
+
async def aextract_data(
|
| 171 |
+
query: IntrinioCalendarIpoQueryParams,
|
| 172 |
+
credentials: Optional[Dict[str, str]],
|
| 173 |
+
**kwargs: Any,
|
| 174 |
+
) -> List[Dict]:
|
| 175 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 176 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 177 |
+
|
| 178 |
+
base_url = "https://api-v2.intrinio.com/companies/ipos"
|
| 179 |
+
query_str = get_querystring(query.model_dump(by_alias=True), [])
|
| 180 |
+
url = f"{base_url}?{query_str}&api_key={api_key}"
|
| 181 |
+
|
| 182 |
+
data = await get_data_one(url, **kwargs)
|
| 183 |
+
|
| 184 |
+
return data.get("initial_public_offerings", [])
|
| 185 |
+
|
| 186 |
+
@staticmethod
|
| 187 |
+
def transform_data(
|
| 188 |
+
query: IntrinioCalendarIpoQueryParams, data: List[Dict], **kwargs: Any
|
| 189 |
+
) -> List[IntrinioCalendarIpoData]:
|
| 190 |
+
"""Return the transformed data."""
|
| 191 |
+
if not data:
|
| 192 |
+
raise EmptyDataError("The request was returned empty.")
|
| 193 |
+
return [IntrinioCalendarIpoData.model_validate(d) for d in data]
|
openbb_platform/providers/intrinio/openbb_intrinio/models/cash_flow.py
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Cash Flow Statement Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 6 |
+
from warnings import warn
|
| 7 |
+
|
| 8 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 9 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 10 |
+
from openbb_core.provider.standard_models.cash_flow import (
|
| 11 |
+
CashFlowStatementData,
|
| 12 |
+
CashFlowStatementQueryParams,
|
| 13 |
+
)
|
| 14 |
+
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
| 15 |
+
from openbb_core.provider.utils.helpers import ClientResponse, amake_requests
|
| 16 |
+
from openbb_intrinio.utils.helpers import get_data_one
|
| 17 |
+
from pydantic import Field, field_validator, model_validator
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class IntrinioCashFlowStatementQueryParams(CashFlowStatementQueryParams):
|
| 21 |
+
"""Intrinio Cash Flow Statement Query.
|
| 22 |
+
|
| 23 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_company_fundamentals_v2
|
| 24 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_fundamental_standardized_financials_v2
|
| 25 |
+
"""
|
| 26 |
+
|
| 27 |
+
__json_schema_extra__ = {
|
| 28 |
+
"period": {
|
| 29 |
+
"choices": ["annual", "quarter", "ttm", "ytd"],
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
period: Literal["annual", "quarter", "ttm", "ytd"] = Field(
|
| 34 |
+
default="annual",
|
| 35 |
+
description=QUERY_DESCRIPTIONS.get("period", ""),
|
| 36 |
+
)
|
| 37 |
+
fiscal_year: Optional[int] = Field(
|
| 38 |
+
default=None,
|
| 39 |
+
description="The specific fiscal year. Reports do not go beyond 2008.",
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
@field_validator("symbol", mode="after", check_fields=False)
|
| 43 |
+
@classmethod
|
| 44 |
+
def handle_symbol(cls, v) -> str:
|
| 45 |
+
"""Handle symbols with a dash and replace it with a dot for Intrinio."""
|
| 46 |
+
return v.replace("-", ".")
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class IntrinioCashFlowStatementData(CashFlowStatementData):
|
| 50 |
+
"""Intrinio Cash Flow Statement Data."""
|
| 51 |
+
|
| 52 |
+
__alias_dict__ = {
|
| 53 |
+
"cash_and_equivalents": "cashandequivalents",
|
| 54 |
+
"acquisitions": "acquisitions",
|
| 55 |
+
"amortization_expense": "amortizationexpense",
|
| 56 |
+
"cash_income_taxes_paid": "cashincometaxespaid",
|
| 57 |
+
"cash_interest_paid": "cashinterestpaid",
|
| 58 |
+
"cash_interest_received": "cashinterestreceived",
|
| 59 |
+
"depreciation_expense": "depreciationexpense",
|
| 60 |
+
"divestitures": "divestitures",
|
| 61 |
+
"effect_of_exchange_rate_changes": "effectofexchangeratechanges",
|
| 62 |
+
"changes_in_operating_assets_and_liabilities": "increasedecreaseinoperatingcapital",
|
| 63 |
+
"issuance_of_common_equity": "issuanceofcommonequity",
|
| 64 |
+
"issuance_of_debt": "issuanceofdebt",
|
| 65 |
+
"issuance_of_preferred_equity": "issuanceofpreferredequity",
|
| 66 |
+
"loans_held_for_sale": "loansheldforsalenet",
|
| 67 |
+
"net_cash_from_continuing_financing_activities": "netcashfromcontinuingfinancingactivities",
|
| 68 |
+
"net_cash_from_continuing_investing_activities": "netcashfromcontinuinginvestingactivities",
|
| 69 |
+
"net_cash_from_continuing_operating_activities": "netcashfromcontinuingoperatingactivities",
|
| 70 |
+
"net_cash_from_discontinued_financing_activities": "netcashfromdiscontinuedfinancingactivities",
|
| 71 |
+
"net_cash_from_discontinued_investing_activities": "netcashfromdiscontinuedinvestingactivities",
|
| 72 |
+
"net_cash_from_discontinued_operating_activities": "netcashfromdiscontinuedoperatingactivities",
|
| 73 |
+
"net_cash_from_financing_activities": "netcashfromfinancingactivities",
|
| 74 |
+
"net_cash_from_investing_activities": "netcashfrominvestingactivities",
|
| 75 |
+
"net_cash_from_operating_activities": "netcashfromoperatingactivities",
|
| 76 |
+
"net_change_in_cash_and_equivalents": "netchangeincash",
|
| 77 |
+
"net_change_in_deposits": "netchangeindeposits",
|
| 78 |
+
"net_income": "netincome",
|
| 79 |
+
"net_income_continuing_operations": "netincomecontinuing",
|
| 80 |
+
"net_income_discontinued_operations": "netincomediscontinued",
|
| 81 |
+
"net_increase_in_fed_funds_sold": "netincreaseinfedfundssold",
|
| 82 |
+
"non_cash_adjustments_to_reconcile_net_income": "noncashadjustmentstonetincome",
|
| 83 |
+
"other_financing_activities": "otherfinancingactivitiesnet",
|
| 84 |
+
"other_investing_activities": "otherinvestingactivitiesnet",
|
| 85 |
+
"other_net_changes_in_cash": "othernetchangesincash",
|
| 86 |
+
"payment_of_dividends": "paymentofdividends",
|
| 87 |
+
"provision_for_loan_losses": "provisionforloanlosses",
|
| 88 |
+
"purchase_of_investments": "purchaseofinvestments",
|
| 89 |
+
"purchase_of_investment_securities": "purchaseofinvestments",
|
| 90 |
+
"purchase_of_property_plant_and_equipment": "purchaseofplantpropertyandequipment",
|
| 91 |
+
"repayment_of_debt": "repaymentofdebt",
|
| 92 |
+
"repurchase_of_common_equity": "repurchaseofcommonequity",
|
| 93 |
+
"repurchase_of_preferred_equity": "repurchaseofpreferredequity",
|
| 94 |
+
"sale_and_maturity_of_investments": "saleofinvestments",
|
| 95 |
+
"sale_of_property_plant_and_equipment": "saleofplantpropertyandequipment",
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
reported_currency: Optional[str] = Field(
|
| 99 |
+
description="The currency in which the balance sheet is reported.",
|
| 100 |
+
default=None,
|
| 101 |
+
)
|
| 102 |
+
net_income_continuing_operations: Optional[float] = Field(
|
| 103 |
+
default=None, description="Net Income (Continuing Operations)"
|
| 104 |
+
)
|
| 105 |
+
net_income_discontinued_operations: Optional[float] = Field(
|
| 106 |
+
default=None, description="Net Income (Discontinued Operations)"
|
| 107 |
+
)
|
| 108 |
+
net_income: Optional[float] = Field(
|
| 109 |
+
default=None, description="Consolidated Net Income."
|
| 110 |
+
)
|
| 111 |
+
provision_for_loan_losses: Optional[float] = Field(
|
| 112 |
+
default=None, description="Provision for Loan Losses"
|
| 113 |
+
)
|
| 114 |
+
provision_for_credit_losses: Optional[float] = Field(
|
| 115 |
+
default=None, description="Provision for credit losses"
|
| 116 |
+
)
|
| 117 |
+
depreciation_expense: Optional[float] = Field(
|
| 118 |
+
default=None, description="Depreciation Expense."
|
| 119 |
+
)
|
| 120 |
+
amortization_expense: Optional[float] = Field(
|
| 121 |
+
default=None, description="Amortization Expense."
|
| 122 |
+
)
|
| 123 |
+
share_based_compensation: Optional[float] = Field(
|
| 124 |
+
default=None, description="Share-based compensation."
|
| 125 |
+
)
|
| 126 |
+
non_cash_adjustments_to_reconcile_net_income: Optional[float] = Field(
|
| 127 |
+
default=None, description="Non-Cash Adjustments to Reconcile Net Income."
|
| 128 |
+
)
|
| 129 |
+
changes_in_operating_assets_and_liabilities: Optional[float] = Field(
|
| 130 |
+
default=None, description="Changes in Operating Assets and Liabilities (Net)"
|
| 131 |
+
)
|
| 132 |
+
net_cash_from_continuing_operating_activities: Optional[float] = Field(
|
| 133 |
+
default=None, description="Net Cash from Continuing Operating Activities"
|
| 134 |
+
)
|
| 135 |
+
net_cash_from_discontinued_operating_activities: Optional[float] = Field(
|
| 136 |
+
default=None, description="Net Cash from Discontinued Operating Activities"
|
| 137 |
+
)
|
| 138 |
+
net_cash_from_operating_activities: Optional[float] = Field(
|
| 139 |
+
default=None, description="Net Cash from Operating Activities"
|
| 140 |
+
)
|
| 141 |
+
divestitures: Optional[float] = Field(default=None, description="Divestitures")
|
| 142 |
+
sale_of_property_plant_and_equipment: Optional[float] = Field(
|
| 143 |
+
default=None, description="Sale of Property, Plant, and Equipment"
|
| 144 |
+
)
|
| 145 |
+
acquisitions: Optional[float] = Field(default=None, description="Acquisitions")
|
| 146 |
+
purchase_of_investments: Optional[float] = Field(
|
| 147 |
+
default=None, description="Purchase of Investments"
|
| 148 |
+
)
|
| 149 |
+
purchase_of_investment_securities: Optional[float] = Field(
|
| 150 |
+
default=None, description="Purchase of Investment Securities"
|
| 151 |
+
)
|
| 152 |
+
sale_and_maturity_of_investments: Optional[float] = Field(
|
| 153 |
+
default=None, description="Sale and Maturity of Investments"
|
| 154 |
+
)
|
| 155 |
+
loans_held_for_sale: Optional[float] = Field(
|
| 156 |
+
default=None, description="Loans Held for Sale (Net)"
|
| 157 |
+
)
|
| 158 |
+
purchase_of_property_plant_and_equipment: Optional[float] = Field(
|
| 159 |
+
default=None, description="Purchase of Property, Plant, and Equipment"
|
| 160 |
+
)
|
| 161 |
+
other_investing_activities: Optional[float] = Field(
|
| 162 |
+
default=None, description="Other Investing Activities (Net)"
|
| 163 |
+
)
|
| 164 |
+
net_cash_from_continuing_investing_activities: Optional[float] = Field(
|
| 165 |
+
default=None, description="Net Cash from Continuing Investing Activities"
|
| 166 |
+
)
|
| 167 |
+
net_cash_from_discontinued_investing_activities: Optional[float] = Field(
|
| 168 |
+
default=None, description="Net Cash from Discontinued Investing Activities"
|
| 169 |
+
)
|
| 170 |
+
net_cash_from_investing_activities: Optional[float] = Field(
|
| 171 |
+
default=None, description="Net Cash from Investing Activities"
|
| 172 |
+
)
|
| 173 |
+
payment_of_dividends: Optional[float] = Field(
|
| 174 |
+
default=None, description="Payment of Dividends"
|
| 175 |
+
)
|
| 176 |
+
repurchase_of_common_equity: Optional[float] = Field(
|
| 177 |
+
default=None, description="Repurchase of Common Equity"
|
| 178 |
+
)
|
| 179 |
+
repurchase_of_preferred_equity: Optional[float] = Field(
|
| 180 |
+
default=None, description="Repurchase of Preferred Equity"
|
| 181 |
+
)
|
| 182 |
+
issuance_of_common_equity: Optional[float] = Field(
|
| 183 |
+
default=None, description="Issuance of Common Equity"
|
| 184 |
+
)
|
| 185 |
+
issuance_of_preferred_equity: Optional[float] = Field(
|
| 186 |
+
default=None, description="Issuance of Preferred Equity"
|
| 187 |
+
)
|
| 188 |
+
issuance_of_debt: Optional[float] = Field(
|
| 189 |
+
default=None, description="Issuance of Debt"
|
| 190 |
+
)
|
| 191 |
+
repayment_of_debt: Optional[float] = Field(
|
| 192 |
+
default=None, description="Repayment of Debt"
|
| 193 |
+
)
|
| 194 |
+
other_financing_activities: Optional[float] = Field(
|
| 195 |
+
default=None, description="Other Financing Activities (Net)"
|
| 196 |
+
)
|
| 197 |
+
cash_interest_received: Optional[float] = Field(
|
| 198 |
+
default=None, description="Cash Interest Received"
|
| 199 |
+
)
|
| 200 |
+
net_change_in_deposits: Optional[float] = Field(
|
| 201 |
+
default=None, description="Net Change in Deposits"
|
| 202 |
+
)
|
| 203 |
+
net_increase_in_fed_funds_sold: Optional[float] = Field(
|
| 204 |
+
default=None, description="Net Increase in Fed Funds Sold"
|
| 205 |
+
)
|
| 206 |
+
net_cash_from_continuing_financing_activities: Optional[float] = Field(
|
| 207 |
+
default=None, description="Net Cash from Continuing Financing Activities"
|
| 208 |
+
)
|
| 209 |
+
net_cash_from_discontinued_financing_activities: Optional[float] = Field(
|
| 210 |
+
default=None, description="Net Cash from Discontinued Financing Activities"
|
| 211 |
+
)
|
| 212 |
+
net_cash_from_financing_activities: Optional[float] = Field(
|
| 213 |
+
default=None, description="Net Cash from Financing Activities"
|
| 214 |
+
)
|
| 215 |
+
effect_of_exchange_rate_changes: Optional[float] = Field(
|
| 216 |
+
default=None, description="Effect of Exchange Rate Changes"
|
| 217 |
+
)
|
| 218 |
+
other_net_changes_in_cash: Optional[float] = Field(
|
| 219 |
+
default=None, description="Other Net Changes in Cash"
|
| 220 |
+
)
|
| 221 |
+
net_change_in_cash_and_equivalents: Optional[float] = Field(
|
| 222 |
+
default=None, description="Net Change in Cash and Equivalents"
|
| 223 |
+
)
|
| 224 |
+
cash_income_taxes_paid: Optional[float] = Field(
|
| 225 |
+
default=None, description="Cash Income Taxes Paid"
|
| 226 |
+
)
|
| 227 |
+
cash_interest_paid: Optional[float] = Field(
|
| 228 |
+
default=None, description="Cash Interest Paid"
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
@model_validator(mode="before")
|
| 232 |
+
@classmethod
|
| 233 |
+
def replace_zero(cls, values):
|
| 234 |
+
"""Check for zero values and replace with None."""
|
| 235 |
+
return (
|
| 236 |
+
{k: None if v == 0 else v for k, v in values.items()}
|
| 237 |
+
if isinstance(values, dict)
|
| 238 |
+
else values
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
class IntrinioCashFlowStatementFetcher(
|
| 243 |
+
Fetcher[
|
| 244 |
+
IntrinioCashFlowStatementQueryParams,
|
| 245 |
+
List[IntrinioCashFlowStatementData],
|
| 246 |
+
]
|
| 247 |
+
):
|
| 248 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 249 |
+
|
| 250 |
+
@staticmethod
|
| 251 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioCashFlowStatementQueryParams:
|
| 252 |
+
"""Transform the query params."""
|
| 253 |
+
return IntrinioCashFlowStatementQueryParams(**params)
|
| 254 |
+
|
| 255 |
+
@staticmethod
|
| 256 |
+
async def aextract_data(
|
| 257 |
+
query: IntrinioCashFlowStatementQueryParams,
|
| 258 |
+
credentials: Optional[Dict[str, str]],
|
| 259 |
+
**kwargs: Any,
|
| 260 |
+
) -> List[Dict]:
|
| 261 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 262 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 263 |
+
statement_code = "cash_flow_statement"
|
| 264 |
+
if query.period in ["quarter", "annual"]:
|
| 265 |
+
period_type = "FY" if query.period == "annual" else "QTR"
|
| 266 |
+
elif query.period in ["ttm", "ytd"]:
|
| 267 |
+
period_type = query.period.upper()
|
| 268 |
+
else:
|
| 269 |
+
raise OpenBBError(f"Period '{query.period}' not supported.")
|
| 270 |
+
|
| 271 |
+
base_url = "https://api-v2.intrinio.com"
|
| 272 |
+
fundamentals_url = (
|
| 273 |
+
f"{base_url}/companies/{query.symbol}/fundamentals?"
|
| 274 |
+
f"statement_code={statement_code}&type={period_type}"
|
| 275 |
+
)
|
| 276 |
+
if query.fiscal_year is not None:
|
| 277 |
+
if query.fiscal_year < 2008:
|
| 278 |
+
warn("Financials data is only available from 2008 and later.")
|
| 279 |
+
query.fiscal_year = 2008
|
| 280 |
+
fundamentals_url = fundamentals_url + f"&fiscal_year={query.fiscal_year}"
|
| 281 |
+
fundamentals_url = fundamentals_url + f"&api_key={api_key}"
|
| 282 |
+
fundamentals_data = (await get_data_one(fundamentals_url, **kwargs)).get(
|
| 283 |
+
"fundamentals", []
|
| 284 |
+
)
|
| 285 |
+
|
| 286 |
+
fiscal_periods = [
|
| 287 |
+
f"{item['fiscal_year']}-{item['fiscal_period']}"
|
| 288 |
+
for item in fundamentals_data
|
| 289 |
+
]
|
| 290 |
+
fiscal_periods = fiscal_periods[: query.limit]
|
| 291 |
+
|
| 292 |
+
async def callback(response: ClientResponse, _: Any) -> Dict:
|
| 293 |
+
"""Return the response."""
|
| 294 |
+
statement_data = await response.json()
|
| 295 |
+
return {
|
| 296 |
+
"period_ending": statement_data["fundamental"]["end_date"], # type: ignore
|
| 297 |
+
"fiscal_period": statement_data["fundamental"]["fiscal_period"], # type: ignore
|
| 298 |
+
"fiscal_year": statement_data["fundamental"]["fiscal_year"], # type: ignore
|
| 299 |
+
"financials": statement_data["standardized_financials"], # type: ignore
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
intrinio_id = f"{query.symbol}-{statement_code}"
|
| 303 |
+
urls = [
|
| 304 |
+
f"{base_url}/fundamentals/{intrinio_id}-{period}/standardized_financials?api_key={api_key}"
|
| 305 |
+
for period in fiscal_periods
|
| 306 |
+
]
|
| 307 |
+
|
| 308 |
+
return await amake_requests(urls, callback, **kwargs)
|
| 309 |
+
|
| 310 |
+
@staticmethod
|
| 311 |
+
def transform_data(
|
| 312 |
+
query: IntrinioCashFlowStatementQueryParams, data: List[Dict], **kwargs: Any
|
| 313 |
+
) -> List[IntrinioCashFlowStatementData]:
|
| 314 |
+
"""Return the transformed data."""
|
| 315 |
+
transformed_data: List[IntrinioCashFlowStatementData] = []
|
| 316 |
+
units = []
|
| 317 |
+
for item in data:
|
| 318 |
+
sub_dict: Dict[str, Any] = {}
|
| 319 |
+
|
| 320 |
+
for sub_item in item["financials"]:
|
| 321 |
+
unit = sub_item["data_tag"].get("unit", "")
|
| 322 |
+
if unit and len(unit) == 3:
|
| 323 |
+
units.append(unit)
|
| 324 |
+
field_name = sub_item["data_tag"]["tag"]
|
| 325 |
+
sub_dict[field_name] = (
|
| 326 |
+
float(sub_item["value"])
|
| 327 |
+
if sub_item["value"] and sub_item["value"] != 0
|
| 328 |
+
else None
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
sub_dict["period_ending"] = item["period_ending"]
|
| 332 |
+
sub_dict["fiscal_year"] = item["fiscal_year"]
|
| 333 |
+
sub_dict["fiscal_period"] = item["fiscal_period"]
|
| 334 |
+
sub_dict["reported_currency"] = list(set(units))[0]
|
| 335 |
+
|
| 336 |
+
transformed_data.append(IntrinioCashFlowStatementData(**sub_dict))
|
| 337 |
+
|
| 338 |
+
return transformed_data
|
openbb_platform/providers/intrinio/openbb_intrinio/models/company_filings.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Company Filings Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
from datetime import (
|
| 6 |
+
date as dateType,
|
| 7 |
+
datetime,
|
| 8 |
+
)
|
| 9 |
+
from typing import Any, Optional
|
| 10 |
+
|
| 11 |
+
from dateutil.relativedelta import relativedelta
|
| 12 |
+
from openbb_core.provider.abstract.annotated_result import AnnotatedResult
|
| 13 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 14 |
+
from openbb_core.provider.standard_models.company_filings import (
|
| 15 |
+
CompanyFilingsData,
|
| 16 |
+
CompanyFilingsQueryParams,
|
| 17 |
+
)
|
| 18 |
+
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
| 19 |
+
from pydantic import Field, field_validator
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class IntrinioCompanyFilingsQueryParams(CompanyFilingsQueryParams):
|
| 23 |
+
"""Intrinio Company Filings Query.
|
| 24 |
+
|
| 25 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_company_filings_v2
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
__alias_dict__ = {"form_type": "report_type", "limit": "page_size"}
|
| 29 |
+
|
| 30 |
+
form_type: Optional[str] = Field(
|
| 31 |
+
default=None, description="SEC form type to filter by."
|
| 32 |
+
)
|
| 33 |
+
start_date: Optional[dateType] = Field(
|
| 34 |
+
default=None,
|
| 35 |
+
description=QUERY_DESCRIPTIONS["start_date"],
|
| 36 |
+
)
|
| 37 |
+
end_date: Optional[dateType] = Field(
|
| 38 |
+
default=None,
|
| 39 |
+
description=QUERY_DESCRIPTIONS["end_date"],
|
| 40 |
+
)
|
| 41 |
+
limit: Optional[int] = Field(
|
| 42 |
+
default=None,
|
| 43 |
+
description=QUERY_DESCRIPTIONS["limit"],
|
| 44 |
+
)
|
| 45 |
+
thea_enabled: Optional[bool] = Field(
|
| 46 |
+
default=None,
|
| 47 |
+
description="Return filings that have been read by Intrinio's Thea NLP.",
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
@field_validator("symbol", mode="before", check_fields=False)
|
| 51 |
+
@classmethod
|
| 52 |
+
def _validate_symbol(cls, v):
|
| 53 |
+
"""Validate symbol."""
|
| 54 |
+
if not v:
|
| 55 |
+
raise ValueError("Symbol is required for Intrinio.")
|
| 56 |
+
return v
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class IntrinioCompanyFilingsData(CompanyFilingsData):
|
| 60 |
+
"""Intrinio Company Filings Data."""
|
| 61 |
+
|
| 62 |
+
id: str = Field(description="Intrinio ID of the filing.")
|
| 63 |
+
period_end_date: Optional[dateType] = Field(
|
| 64 |
+
default=None,
|
| 65 |
+
description="Ending date of the fiscal period for the filing.",
|
| 66 |
+
)
|
| 67 |
+
accepted_date: Optional[datetime] = Field(
|
| 68 |
+
default=None, description="Accepted date of the filing."
|
| 69 |
+
)
|
| 70 |
+
sec_unique_id: str = Field(description="SEC unique ID of the filing.")
|
| 71 |
+
filing_url: Optional[str] = Field(
|
| 72 |
+
default=None, description="URL to the filing page."
|
| 73 |
+
)
|
| 74 |
+
instance_url: Optional[str] = Field(
|
| 75 |
+
default=None,
|
| 76 |
+
description="URL for the XBRL filing for the report.",
|
| 77 |
+
)
|
| 78 |
+
industry_group: str = Field(description="Industry group of the company.")
|
| 79 |
+
industry_category: str = Field(description="Industry category of the company.")
|
| 80 |
+
word_count: Optional[int] = Field(
|
| 81 |
+
default=None, description="Number of words in the filing, if available."
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class IntrinioCompanyFilingsFetcher(
|
| 86 |
+
Fetcher[
|
| 87 |
+
IntrinioCompanyFilingsQueryParams,
|
| 88 |
+
list[IntrinioCompanyFilingsData],
|
| 89 |
+
]
|
| 90 |
+
):
|
| 91 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 92 |
+
|
| 93 |
+
@staticmethod
|
| 94 |
+
def transform_query(params: dict[str, Any]) -> IntrinioCompanyFilingsQueryParams:
|
| 95 |
+
"""Transform the query."""
|
| 96 |
+
transformed_params = params
|
| 97 |
+
|
| 98 |
+
now = datetime.now().date()
|
| 99 |
+
if params.get("start_date") is None and params.get("form_type") is None:
|
| 100 |
+
transformed_params["start_date"] = now - relativedelta(years=1)
|
| 101 |
+
if params.get("end_date") is None and params.get("form_type") is None:
|
| 102 |
+
transformed_params["end_date"] = now
|
| 103 |
+
|
| 104 |
+
return IntrinioCompanyFilingsQueryParams(**transformed_params)
|
| 105 |
+
|
| 106 |
+
@staticmethod
|
| 107 |
+
async def aextract_data(
|
| 108 |
+
query: IntrinioCompanyFilingsQueryParams,
|
| 109 |
+
credentials: Optional[dict[str, str]],
|
| 110 |
+
**kwargs: Any,
|
| 111 |
+
) -> dict:
|
| 112 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 113 |
+
# pylint: disable=import-outside-toplevel
|
| 114 |
+
from openbb_core.provider.utils.errors import EmptyDataError, OpenBBError
|
| 115 |
+
from openbb_core.provider.utils.helpers import (
|
| 116 |
+
get_async_requests_session,
|
| 117 |
+
get_querystring,
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 121 |
+
|
| 122 |
+
base_url = "https://api-v2.intrinio.com/companies"
|
| 123 |
+
query_str = get_querystring(
|
| 124 |
+
query.model_dump(by_alias=True), ["symbol", "limit", "page_size"]
|
| 125 |
+
)
|
| 126 |
+
url = f"{base_url}/{query.symbol}/filings?{query_str}&page_size={query.limit or 10000}&api_key={api_key}"
|
| 127 |
+
results: list = []
|
| 128 |
+
metadata: dict = {}
|
| 129 |
+
session = await get_async_requests_session()
|
| 130 |
+
|
| 131 |
+
async with await session.get(url) as response:
|
| 132 |
+
if response.status != 200:
|
| 133 |
+
raise OpenBBError(
|
| 134 |
+
f"Error fetching data from Intrinio: {response.status} -> {response.text}"
|
| 135 |
+
)
|
| 136 |
+
result = await response.json()
|
| 137 |
+
if filings := result.get("filings", []):
|
| 138 |
+
results.extend(filings)
|
| 139 |
+
|
| 140 |
+
metadata = result.get("company", {})
|
| 141 |
+
|
| 142 |
+
while next_page := result.get("next_page"):
|
| 143 |
+
url += f"&next_page={next_page}"
|
| 144 |
+
async with await session.get(url) as next_response:
|
| 145 |
+
if response.status != 200:
|
| 146 |
+
raise OpenBBError(
|
| 147 |
+
f"Error fetching data from Intrinio: {response.status} -> {response.text}"
|
| 148 |
+
)
|
| 149 |
+
result = await next_response.json()
|
| 150 |
+
if filings := result.get("filings", []):
|
| 151 |
+
results.extend(filings)
|
| 152 |
+
|
| 153 |
+
if not results:
|
| 154 |
+
raise EmptyDataError("No data was returned for the symbol provided.")
|
| 155 |
+
|
| 156 |
+
return {"data": results, "metadata": metadata}
|
| 157 |
+
|
| 158 |
+
@staticmethod
|
| 159 |
+
def transform_data(
|
| 160 |
+
query: IntrinioCompanyFilingsQueryParams, data: dict, **kwargs: Any
|
| 161 |
+
) -> AnnotatedResult[list[IntrinioCompanyFilingsData]]:
|
| 162 |
+
"""Return the transformed data."""
|
| 163 |
+
return AnnotatedResult(
|
| 164 |
+
result=[
|
| 165 |
+
IntrinioCompanyFilingsData.model_validate(
|
| 166 |
+
{
|
| 167 |
+
k: v
|
| 168 |
+
for k, v in d.items()
|
| 169 |
+
if k not in ["thea_enabled", "earnings_release"]
|
| 170 |
+
}
|
| 171 |
+
)
|
| 172 |
+
for d in data.get("data", [])
|
| 173 |
+
],
|
| 174 |
+
metadata=data.get("metadata", {}),
|
| 175 |
+
)
|
openbb_platform/providers/intrinio/openbb_intrinio/models/company_news.py
ADDED
|
@@ -0,0 +1,295 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Company News Model."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 6 |
+
from warnings import warn
|
| 7 |
+
|
| 8 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 9 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 10 |
+
from openbb_core.provider.standard_models.company_news import (
|
| 11 |
+
CompanyNewsData,
|
| 12 |
+
CompanyNewsQueryParams,
|
| 13 |
+
)
|
| 14 |
+
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
|
| 15 |
+
from openbb_core.provider.utils.helpers import (
|
| 16 |
+
amake_request,
|
| 17 |
+
get_querystring,
|
| 18 |
+
)
|
| 19 |
+
from openbb_intrinio.utils.helpers import get_data
|
| 20 |
+
from openbb_intrinio.utils.references import IntrinioSecurity
|
| 21 |
+
from pydantic import Field, field_validator
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class IntrinioCompanyNewsQueryParams(CompanyNewsQueryParams):
|
| 25 |
+
"""Intrinio Company News Query.
|
| 26 |
+
|
| 27 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_company_news_v2
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
__alias_dict__ = {
|
| 31 |
+
"limit": "page_size",
|
| 32 |
+
"source": "specific_source",
|
| 33 |
+
}
|
| 34 |
+
__json_schema_extra__ = {
|
| 35 |
+
"symbol": {"multiple_items_allowed": True},
|
| 36 |
+
"source": {
|
| 37 |
+
"choices": ["yahoo", "moody", "moody_us_news", "moody_us_press_releases"]
|
| 38 |
+
},
|
| 39 |
+
"sentiment": {"choices": ["positive", "neutral", "negative"]},
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
source: Optional[
|
| 43 |
+
Literal["yahoo", "moody", "moody_us_news", "moody_us_press_releases"]
|
| 44 |
+
] = Field(
|
| 45 |
+
default=None,
|
| 46 |
+
description="The source of the news article.",
|
| 47 |
+
)
|
| 48 |
+
sentiment: Optional[Literal["positive", "neutral", "negative"]] = Field(
|
| 49 |
+
default=None,
|
| 50 |
+
description="Return news only from this source.",
|
| 51 |
+
)
|
| 52 |
+
language: Optional[str] = Field(
|
| 53 |
+
default=None,
|
| 54 |
+
description="Filter by language. Unsupported for yahoo source.",
|
| 55 |
+
)
|
| 56 |
+
topic: Optional[str] = Field(
|
| 57 |
+
default=None,
|
| 58 |
+
description="Filter by topic. Unsupported for yahoo source.",
|
| 59 |
+
)
|
| 60 |
+
word_count_greater_than: Optional[int] = Field(
|
| 61 |
+
default=None,
|
| 62 |
+
description="News stories will have a word count greater than this value."
|
| 63 |
+
+ " Unsupported for yahoo source.",
|
| 64 |
+
)
|
| 65 |
+
word_count_less_than: Optional[int] = Field(
|
| 66 |
+
default=None,
|
| 67 |
+
description="News stories will have a word count less than this value."
|
| 68 |
+
+ " Unsupported for yahoo source.",
|
| 69 |
+
)
|
| 70 |
+
is_spam: Optional[bool] = Field(
|
| 71 |
+
default=None,
|
| 72 |
+
description="Filter whether it is marked as spam or not."
|
| 73 |
+
+ " Unsupported for yahoo source.",
|
| 74 |
+
)
|
| 75 |
+
business_relevance_greater_than: Optional[float] = Field(
|
| 76 |
+
default=None,
|
| 77 |
+
ge=0,
|
| 78 |
+
le=1,
|
| 79 |
+
description="News stories will have a business relevance score more than this value."
|
| 80 |
+
+ " Unsupported for yahoo source. Value is a decimal between 0 and 1.",
|
| 81 |
+
)
|
| 82 |
+
business_relevance_less_than: Optional[float] = Field(
|
| 83 |
+
default=None,
|
| 84 |
+
ge=0,
|
| 85 |
+
le=1,
|
| 86 |
+
description="News stories will have a business relevance score less than this value."
|
| 87 |
+
+ " Unsupported for yahoo source. Value is a decimal between 0 and 1.",
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
@field_validator("symbol", mode="before", check_fields=False)
|
| 91 |
+
@classmethod
|
| 92 |
+
def _symbol_mandatory(cls, v):
|
| 93 |
+
"""Symbol mandatory validator."""
|
| 94 |
+
if not v:
|
| 95 |
+
raise OpenBBError("Required field missing -> symbol")
|
| 96 |
+
return v
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
class IntrinioCompanyNewsData(CompanyNewsData):
|
| 100 |
+
"""Intrinio Company News Data."""
|
| 101 |
+
|
| 102 |
+
__alias_dict__ = {
|
| 103 |
+
"date": "publication_date",
|
| 104 |
+
"sentiment": "article_sentiment",
|
| 105 |
+
"sentiment_confidence": "article_sentiment_confidence",
|
| 106 |
+
"symbols": "symbol",
|
| 107 |
+
}
|
| 108 |
+
source: Optional[str] = Field(
|
| 109 |
+
default=None,
|
| 110 |
+
description="The source of the news article.",
|
| 111 |
+
)
|
| 112 |
+
summary: Optional[str] = Field(
|
| 113 |
+
default=None,
|
| 114 |
+
description="The summary of the news article.",
|
| 115 |
+
)
|
| 116 |
+
topics: Optional[str] = Field(
|
| 117 |
+
default=None,
|
| 118 |
+
description="The topics related to the news article.",
|
| 119 |
+
)
|
| 120 |
+
word_count: Optional[int] = Field(
|
| 121 |
+
default=None,
|
| 122 |
+
description="The word count of the news article.",
|
| 123 |
+
)
|
| 124 |
+
business_relevance: Optional[float] = Field(
|
| 125 |
+
default=None,
|
| 126 |
+
description=" How strongly correlated the news article is to the business",
|
| 127 |
+
)
|
| 128 |
+
sentiment: Optional[str] = Field(
|
| 129 |
+
default=None,
|
| 130 |
+
description="The sentiment of the news article - i.e, negative, positive.",
|
| 131 |
+
)
|
| 132 |
+
sentiment_confidence: Optional[float] = Field(
|
| 133 |
+
default=None,
|
| 134 |
+
description="The confidence score of the sentiment rating.",
|
| 135 |
+
)
|
| 136 |
+
language: Optional[str] = Field(
|
| 137 |
+
default=None,
|
| 138 |
+
description="The language of the news article.",
|
| 139 |
+
)
|
| 140 |
+
spam: Optional[bool] = Field(
|
| 141 |
+
default=None,
|
| 142 |
+
description="Whether the news article is spam.",
|
| 143 |
+
)
|
| 144 |
+
copyright: Optional[str] = Field(
|
| 145 |
+
default=None,
|
| 146 |
+
description="The copyright notice of the news article.",
|
| 147 |
+
)
|
| 148 |
+
id: str = Field(description="Article ID.")
|
| 149 |
+
security: Optional[IntrinioSecurity] = Field(
|
| 150 |
+
default=None,
|
| 151 |
+
description="The Intrinio Security object. Contains the security details related to the news article.",
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
@field_validator("publication_date", mode="before", check_fields=False)
|
| 155 |
+
@classmethod
|
| 156 |
+
def date_validate(cls, v):
|
| 157 |
+
"""Return the date as a datetime object."""
|
| 158 |
+
return datetime.strptime(v, "%Y-%m-%dT%H:%M:%S.000Z")
|
| 159 |
+
|
| 160 |
+
@field_validator("topics", mode="before", check_fields=False)
|
| 161 |
+
@classmethod
|
| 162 |
+
def topics_validate(cls, v):
|
| 163 |
+
"""Parse the topics as a string."""
|
| 164 |
+
if v:
|
| 165 |
+
topics = [t.get("name") for t in v if t and t not in ["", " "]]
|
| 166 |
+
return ", ".join(topics)
|
| 167 |
+
return None
|
| 168 |
+
|
| 169 |
+
@field_validator("copyright", mode="before", check_fields=False)
|
| 170 |
+
@classmethod
|
| 171 |
+
def copyright_validate(cls, v):
|
| 172 |
+
"""Clean empty strings."""
|
| 173 |
+
return None if v in ["", " "] else v
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
class IntrinioCompanyNewsFetcher(
|
| 177 |
+
Fetcher[
|
| 178 |
+
IntrinioCompanyNewsQueryParams,
|
| 179 |
+
List[IntrinioCompanyNewsData],
|
| 180 |
+
]
|
| 181 |
+
):
|
| 182 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 183 |
+
|
| 184 |
+
@staticmethod
|
| 185 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioCompanyNewsQueryParams:
|
| 186 |
+
"""Transform the query params."""
|
| 187 |
+
transformed_params = params
|
| 188 |
+
if not transformed_params.get("start_date"):
|
| 189 |
+
transformed_params["start_date"] = (
|
| 190 |
+
datetime.now() - timedelta(days=365)
|
| 191 |
+
).date()
|
| 192 |
+
if not transformed_params.get("end_date"):
|
| 193 |
+
transformed_params["end_date"] = (datetime.now() + timedelta(days=1)).date()
|
| 194 |
+
if transformed_params["start_date"] == transformed_params["end_date"]:
|
| 195 |
+
transformed_params["end_date"] = (
|
| 196 |
+
transformed_params["end_date"] + timedelta(days=1)
|
| 197 |
+
).date()
|
| 198 |
+
return IntrinioCompanyNewsQueryParams(**transformed_params)
|
| 199 |
+
|
| 200 |
+
@staticmethod
|
| 201 |
+
async def aextract_data(
|
| 202 |
+
query: IntrinioCompanyNewsQueryParams,
|
| 203 |
+
credentials: Optional[Dict[str, str]],
|
| 204 |
+
**kwargs: Any,
|
| 205 |
+
) -> List[Dict]:
|
| 206 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 207 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 208 |
+
|
| 209 |
+
base_url = "https://api-v2.intrinio.com/companies"
|
| 210 |
+
ignore = (
|
| 211 |
+
["symbol", "page_size", "is_spam"]
|
| 212 |
+
if not query.source or query.source == "yahoo"
|
| 213 |
+
else ["symbol", "page_size"]
|
| 214 |
+
)
|
| 215 |
+
query_str = get_querystring(query.model_dump(by_alias=True), ignore)
|
| 216 |
+
symbols = query.symbol.split(",") if query.symbol else []
|
| 217 |
+
news: List = []
|
| 218 |
+
|
| 219 |
+
async def callback(response, session):
|
| 220 |
+
"""Response callback."""
|
| 221 |
+
result = await response.json()
|
| 222 |
+
|
| 223 |
+
if isinstance(result, dict) and "error" in result:
|
| 224 |
+
if "api key" in result.get("message", "").lower():
|
| 225 |
+
raise UnauthorizedError(
|
| 226 |
+
f"Unauthorized Intrinio request -> {result.get('message')}"
|
| 227 |
+
)
|
| 228 |
+
raise OpenBBError(f"Error in Intrinio request -> {result}")
|
| 229 |
+
|
| 230 |
+
symbol = response.url.parts[-2]
|
| 231 |
+
_data = result.get("news", [])
|
| 232 |
+
data = []
|
| 233 |
+
data.extend([{"symbol": symbol, **d} for d in _data])
|
| 234 |
+
articles = len(data)
|
| 235 |
+
next_page = result.get("next_page")
|
| 236 |
+
# query.limit can be None...
|
| 237 |
+
limit = query.limit or 2500
|
| 238 |
+
while next_page and limit > articles:
|
| 239 |
+
url = (
|
| 240 |
+
f"{base_url}/{symbol}/news?{query_str}"
|
| 241 |
+
+ f"&page_size={query.limit}&api_key={api_key}&next_page={next_page}"
|
| 242 |
+
)
|
| 243 |
+
result = await get_data(url, session=session, **kwargs)
|
| 244 |
+
_data = result.get("news", [])
|
| 245 |
+
if _data:
|
| 246 |
+
data.extend([{"symbol": symbol, **d} for d in _data])
|
| 247 |
+
articles = len(data)
|
| 248 |
+
next_page = result.get("next_page")
|
| 249 |
+
return data
|
| 250 |
+
|
| 251 |
+
seen = set()
|
| 252 |
+
|
| 253 |
+
async def get_one(symbol):
|
| 254 |
+
"""Get the data for one symbol."""
|
| 255 |
+
url = f"{base_url}/{symbol}/news?{query_str}&page_size={query.limit}&api_key={api_key}"
|
| 256 |
+
data = await amake_request(url, response_callback=callback, **kwargs)
|
| 257 |
+
if not data:
|
| 258 |
+
warn(f"No data found for: {symbol}")
|
| 259 |
+
if data:
|
| 260 |
+
data = [x for x in data if not (x["url"] in seen or seen.add(x["url"]))] # type: ignore
|
| 261 |
+
news.extend(
|
| 262 |
+
sorted(data, key=lambda x: x["publication_date"], reverse=True)[
|
| 263 |
+
: query.limit
|
| 264 |
+
]
|
| 265 |
+
)
|
| 266 |
+
|
| 267 |
+
tasks = [get_one(symbol) for symbol in symbols]
|
| 268 |
+
|
| 269 |
+
await asyncio.gather(*tasks)
|
| 270 |
+
|
| 271 |
+
if not news:
|
| 272 |
+
raise EmptyDataError(
|
| 273 |
+
"Error: The request was returned as empty."
|
| 274 |
+
+ " Try adjusting the requested date ranges, if applicable."
|
| 275 |
+
)
|
| 276 |
+
|
| 277 |
+
return news
|
| 278 |
+
|
| 279 |
+
# pylint: disable=unused-argument
|
| 280 |
+
@staticmethod
|
| 281 |
+
def transform_data(
|
| 282 |
+
query: IntrinioCompanyNewsQueryParams, data: List[Dict], **kwargs: Any
|
| 283 |
+
) -> List[IntrinioCompanyNewsData]:
|
| 284 |
+
"""Return the transformed data."""
|
| 285 |
+
results: List[IntrinioCompanyNewsData] = []
|
| 286 |
+
for item in data:
|
| 287 |
+
body = item.get("body", {})
|
| 288 |
+
if not body:
|
| 289 |
+
item["text"] = item.pop("summary")
|
| 290 |
+
if body:
|
| 291 |
+
_ = item.pop("body")
|
| 292 |
+
item["publication_date"] = body.get("publication_date", None)
|
| 293 |
+
item["text"] = body.get("body", None)
|
| 294 |
+
results.append(IntrinioCompanyNewsData.model_validate(item))
|
| 295 |
+
return results
|
openbb_platform/providers/intrinio/openbb_intrinio/models/currency_pairs.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Currency Available Pairs Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 8 |
+
from openbb_core.provider.standard_models.currency_pairs import (
|
| 9 |
+
CurrencyPairsData,
|
| 10 |
+
CurrencyPairsQueryParams,
|
| 11 |
+
)
|
| 12 |
+
from openbb_core.provider.utils.errors import EmptyDataError
|
| 13 |
+
from pydantic import Field
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class IntrinioCurrencyPairsQueryParams(CurrencyPairsQueryParams):
|
| 17 |
+
"""Intrinio Currency Available Pairs Query.
|
| 18 |
+
|
| 19 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_forex_pairs_v2
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class IntrinioCurrencyPairsData(CurrencyPairsData):
|
| 24 |
+
"""Intrinio Currency Available Pairs Data."""
|
| 25 |
+
|
| 26 |
+
__alias_dict__ = {"symbol": "code"}
|
| 27 |
+
|
| 28 |
+
base_currency: str = Field(
|
| 29 |
+
description="ISO 4217 currency code of the base currency."
|
| 30 |
+
)
|
| 31 |
+
quote_currency: str = Field(
|
| 32 |
+
description="ISO 4217 currency code of the quote currency."
|
| 33 |
+
)
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class IntrinioCurrencyPairsFetcher(
|
| 37 |
+
Fetcher[
|
| 38 |
+
IntrinioCurrencyPairsQueryParams,
|
| 39 |
+
List[IntrinioCurrencyPairsData],
|
| 40 |
+
]
|
| 41 |
+
):
|
| 42 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 43 |
+
|
| 44 |
+
@staticmethod
|
| 45 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioCurrencyPairsQueryParams:
|
| 46 |
+
"""Transform the query params."""
|
| 47 |
+
return IntrinioCurrencyPairsQueryParams(**params)
|
| 48 |
+
|
| 49 |
+
@staticmethod
|
| 50 |
+
async def aextract_data(
|
| 51 |
+
query: IntrinioCurrencyPairsQueryParams,
|
| 52 |
+
credentials: Optional[Dict[str, str]],
|
| 53 |
+
**kwargs: Any,
|
| 54 |
+
) -> List[Dict]:
|
| 55 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 56 |
+
# pylint: disable=import-outside-toplevel
|
| 57 |
+
from openbb_intrinio.utils.helpers import get_data_many
|
| 58 |
+
|
| 59 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 60 |
+
|
| 61 |
+
base_url = "https://api-v2.intrinio.com"
|
| 62 |
+
url = f"{base_url}/forex/pairs?api_key={api_key}"
|
| 63 |
+
return await get_data_many(url, "pairs", **kwargs)
|
| 64 |
+
|
| 65 |
+
@staticmethod
|
| 66 |
+
def transform_data(
|
| 67 |
+
query: IntrinioCurrencyPairsQueryParams, data: List[Dict], **kwargs: Any
|
| 68 |
+
) -> List[IntrinioCurrencyPairsData]:
|
| 69 |
+
"""Return the transformed data."""
|
| 70 |
+
# pylint: disable=import-outside-toplevel
|
| 71 |
+
from pandas import DataFrame
|
| 72 |
+
|
| 73 |
+
if not data:
|
| 74 |
+
raise EmptyDataError("The request was returned empty.")
|
| 75 |
+
df = DataFrame(data)
|
| 76 |
+
if query.query:
|
| 77 |
+
df = df[
|
| 78 |
+
df["code"].str.contains(query.query, case=False)
|
| 79 |
+
| df["base_currency"].str.contains(query.query, case=False)
|
| 80 |
+
| df["quote_currency"].str.contains(query.query, case=False)
|
| 81 |
+
]
|
| 82 |
+
if len(df) == 0:
|
| 83 |
+
raise EmptyDataError(
|
| 84 |
+
f"No results were found with the query supplied. -> {query.query}"
|
| 85 |
+
+ " Hint: Names and descriptions are not searchable from Intrinio, try 3-letter symbols."
|
| 86 |
+
)
|
| 87 |
+
return [
|
| 88 |
+
IntrinioCurrencyPairsData.model_validate(d)
|
| 89 |
+
for d in df.to_dict(orient="records")
|
| 90 |
+
]
|
openbb_platform/providers/intrinio/openbb_intrinio/models/equity_historical.py
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Equity Historical Price Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable = unused-argument
|
| 4 |
+
|
| 5 |
+
from datetime import datetime, time
|
| 6 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 7 |
+
|
| 8 |
+
from dateutil.relativedelta import relativedelta
|
| 9 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 10 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 11 |
+
from openbb_core.provider.standard_models.equity_historical import (
|
| 12 |
+
EquityHistoricalData,
|
| 13 |
+
EquityHistoricalQueryParams,
|
| 14 |
+
)
|
| 15 |
+
from openbb_core.provider.utils.descriptions import (
|
| 16 |
+
DATA_DESCRIPTIONS,
|
| 17 |
+
QUERY_DESCRIPTIONS,
|
| 18 |
+
)
|
| 19 |
+
from openbb_core.provider.utils.errors import EmptyDataError
|
| 20 |
+
from openbb_core.provider.utils.helpers import (
|
| 21 |
+
ClientResponse,
|
| 22 |
+
ClientSession,
|
| 23 |
+
amake_request,
|
| 24 |
+
get_querystring,
|
| 25 |
+
)
|
| 26 |
+
from pydantic import Field, PrivateAttr, model_validator
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class IntrinioEquityHistoricalQueryParams(EquityHistoricalQueryParams):
|
| 30 |
+
"""Intrinio Equity Historical Price Query.
|
| 31 |
+
|
| 32 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_security_interval_prices_v2
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
__json_schema_extra__ = {
|
| 36 |
+
"interval": {
|
| 37 |
+
"choices": [
|
| 38 |
+
"1m",
|
| 39 |
+
"5m",
|
| 40 |
+
"10m",
|
| 41 |
+
"15m",
|
| 42 |
+
"30m",
|
| 43 |
+
"60m",
|
| 44 |
+
"1h",
|
| 45 |
+
"1d",
|
| 46 |
+
"1W",
|
| 47 |
+
"1M",
|
| 48 |
+
"1Q",
|
| 49 |
+
"1Y",
|
| 50 |
+
],
|
| 51 |
+
},
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
symbol: str = Field(
|
| 55 |
+
description="A Security identifier (Ticker, FIGI, ISIN, CUSIP, Intrinio ID)."
|
| 56 |
+
)
|
| 57 |
+
interval: Literal[
|
| 58 |
+
"1m", "5m", "10m", "15m", "30m", "60m", "1h", "1d", "1W", "1M", "1Q", "1Y"
|
| 59 |
+
] = Field(default="1d", description=QUERY_DESCRIPTIONS.get("interval", ""))
|
| 60 |
+
start_time: Optional[time] = Field(
|
| 61 |
+
default=None,
|
| 62 |
+
description="Return intervals starting at the specified time on the `start_date` formatted as 'HH:MM:SS'.",
|
| 63 |
+
)
|
| 64 |
+
end_time: Optional[time] = Field(
|
| 65 |
+
default=None,
|
| 66 |
+
description="Return intervals stopping at the specified time on the `end_date` formatted as 'HH:MM:SS'.",
|
| 67 |
+
)
|
| 68 |
+
timezone: Optional[str] = Field(
|
| 69 |
+
default="America/New_York",
|
| 70 |
+
description="Timezone of the data, in the IANA format (Continent/City).",
|
| 71 |
+
)
|
| 72 |
+
source: Literal["realtime", "delayed", "nasdaq_basic"] = Field(
|
| 73 |
+
default="realtime", description="The source of the data."
|
| 74 |
+
)
|
| 75 |
+
_interval_size: Literal["1m", "5m", "10m", "15m", "30m", "60m", "1h"] = PrivateAttr(
|
| 76 |
+
default=None
|
| 77 |
+
)
|
| 78 |
+
_frequency: Literal["daily", "weekly", "monthly", "quarterly", "yearly"] = (
|
| 79 |
+
PrivateAttr(default=None)
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
# pylint: disable=protected-access
|
| 83 |
+
@model_validator(mode="after")
|
| 84 |
+
@classmethod
|
| 85 |
+
def set_time_params(cls, values: "IntrinioEquityHistoricalQueryParams"):
|
| 86 |
+
"""Set the default start & end date and time params for Intrinio API."""
|
| 87 |
+
frequency_dict = {
|
| 88 |
+
"1d": "daily",
|
| 89 |
+
"1W": "weekly",
|
| 90 |
+
"1M": "monthly",
|
| 91 |
+
"1Q": "quarterly",
|
| 92 |
+
"1Y": "yearly",
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
if values.interval in ["1m", "5m", "10m", "15m", "30m", "60m", "1h"]:
|
| 96 |
+
values._interval_size = values.interval # type: ignore
|
| 97 |
+
elif values.interval in ["1d", "1W", "1M", "1Q", "1Y"]:
|
| 98 |
+
values._frequency = frequency_dict[values.interval] # type: ignore
|
| 99 |
+
|
| 100 |
+
return values
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
class IntrinioEquityHistoricalData(EquityHistoricalData):
|
| 104 |
+
"""Intrinio Equity Historical Price Data."""
|
| 105 |
+
|
| 106 |
+
__alias_dict__ = {
|
| 107 |
+
"date": "time",
|
| 108 |
+
"change_percent": "percent_change",
|
| 109 |
+
"interval": "frequency",
|
| 110 |
+
"intra_period": "intraperiod",
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
average: Optional[float] = Field(
|
| 114 |
+
default=None,
|
| 115 |
+
description="Average trade price of an individual equity during the interval.",
|
| 116 |
+
)
|
| 117 |
+
change: Optional[float] = Field(
|
| 118 |
+
default=None,
|
| 119 |
+
description="Change in the price of the symbol from the previous day.",
|
| 120 |
+
)
|
| 121 |
+
change_percent: Optional[float] = Field(
|
| 122 |
+
default=None,
|
| 123 |
+
description="Percent change in the price of the symbol from the previous day.",
|
| 124 |
+
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
|
| 125 |
+
)
|
| 126 |
+
adj_open: Optional[float] = Field(
|
| 127 |
+
default=None,
|
| 128 |
+
description="The adjusted open price.",
|
| 129 |
+
)
|
| 130 |
+
adj_high: Optional[float] = Field(
|
| 131 |
+
default=None,
|
| 132 |
+
description="The adjusted high price.",
|
| 133 |
+
)
|
| 134 |
+
adj_low: Optional[float] = Field(
|
| 135 |
+
default=None,
|
| 136 |
+
description="The adjusted low price.",
|
| 137 |
+
)
|
| 138 |
+
adj_close: Optional[float] = Field(
|
| 139 |
+
default=None,
|
| 140 |
+
description=DATA_DESCRIPTIONS.get("adj_close", ""),
|
| 141 |
+
)
|
| 142 |
+
adj_volume: Optional[float] = Field(
|
| 143 |
+
default=None,
|
| 144 |
+
description="The adjusted volume.",
|
| 145 |
+
)
|
| 146 |
+
fifty_two_week_high: Optional[float] = Field(
|
| 147 |
+
default=None,
|
| 148 |
+
description="52 week high price for the symbol.",
|
| 149 |
+
)
|
| 150 |
+
fifty_two_week_low: Optional[float] = Field(
|
| 151 |
+
default=None,
|
| 152 |
+
description="52 week low price for the symbol.",
|
| 153 |
+
)
|
| 154 |
+
factor: Optional[float] = Field(
|
| 155 |
+
default=None,
|
| 156 |
+
description="factor by which to multiply equity prices before this "
|
| 157 |
+
"date, in order to calculate historically-adjusted equity prices.",
|
| 158 |
+
)
|
| 159 |
+
split_ratio: Optional[float] = Field(
|
| 160 |
+
default=None,
|
| 161 |
+
description="Ratio of the equity split, if a split occurred.",
|
| 162 |
+
)
|
| 163 |
+
dividend: Optional[float] = Field(
|
| 164 |
+
default=None,
|
| 165 |
+
description="Dividend amount, if a dividend was paid.",
|
| 166 |
+
)
|
| 167 |
+
close_time: Optional[datetime] = Field(
|
| 168 |
+
default=None,
|
| 169 |
+
description="The timestamp that represents the end of the interval span.",
|
| 170 |
+
)
|
| 171 |
+
interval: Optional[str] = Field(
|
| 172 |
+
default=None,
|
| 173 |
+
description="The data time frequency.",
|
| 174 |
+
)
|
| 175 |
+
intra_period: Optional[bool] = Field(
|
| 176 |
+
default=None,
|
| 177 |
+
description="If true, the equity price represents an unfinished period "
|
| 178 |
+
"(be it day, week, quarter, month, or year), meaning that the close "
|
| 179 |
+
"price is the latest price available, not the official close price "
|
| 180 |
+
"for the period",
|
| 181 |
+
)
|
| 182 |
+
|
| 183 |
+
|
| 184 |
+
class IntrinioEquityHistoricalFetcher(
|
| 185 |
+
Fetcher[
|
| 186 |
+
IntrinioEquityHistoricalQueryParams,
|
| 187 |
+
List[IntrinioEquityHistoricalData],
|
| 188 |
+
]
|
| 189 |
+
):
|
| 190 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 191 |
+
|
| 192 |
+
@staticmethod
|
| 193 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioEquityHistoricalQueryParams:
|
| 194 |
+
"""Transform the query params."""
|
| 195 |
+
transformed_params = params
|
| 196 |
+
|
| 197 |
+
now = datetime.now().date()
|
| 198 |
+
if params.get("start_date") is None:
|
| 199 |
+
transformed_params["start_date"] = now - relativedelta(years=1)
|
| 200 |
+
|
| 201 |
+
if params.get("end_date") is None:
|
| 202 |
+
transformed_params["end_date"] = now
|
| 203 |
+
|
| 204 |
+
if params.get("start_time") is None:
|
| 205 |
+
transformed_params["start_time"] = time(0, 0, 0)
|
| 206 |
+
|
| 207 |
+
if params.get("end_time") is None:
|
| 208 |
+
transformed_params["end_time"] = time(23, 59, 59)
|
| 209 |
+
|
| 210 |
+
return IntrinioEquityHistoricalQueryParams(**transformed_params)
|
| 211 |
+
|
| 212 |
+
# pylint: disable=protected-access
|
| 213 |
+
@staticmethod
|
| 214 |
+
async def aextract_data(
|
| 215 |
+
query: IntrinioEquityHistoricalQueryParams,
|
| 216 |
+
credentials: Optional[Dict[str, str]],
|
| 217 |
+
**kwargs: Any,
|
| 218 |
+
) -> List[Dict]:
|
| 219 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 220 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 221 |
+
base_url = f"https://api-v2.intrinio.com/securities/{query.symbol}/prices"
|
| 222 |
+
query_str = get_querystring(
|
| 223 |
+
query.model_dump(by_alias=True), ["symbol", "interval"]
|
| 224 |
+
)
|
| 225 |
+
|
| 226 |
+
if query._interval_size:
|
| 227 |
+
base_url += f"/intervals?interval_size={query._interval_size}"
|
| 228 |
+
data_key = "intervals"
|
| 229 |
+
elif query._frequency:
|
| 230 |
+
base_url += f"?frequency={query._frequency}"
|
| 231 |
+
data_key = "stock_prices"
|
| 232 |
+
|
| 233 |
+
async def callback(response: ClientResponse, session: ClientSession) -> list:
|
| 234 |
+
"""Return the response."""
|
| 235 |
+
init_response = await response.json()
|
| 236 |
+
if "error" in init_response:
|
| 237 |
+
raise OpenBBError(
|
| 238 |
+
f"Intrinio Error Message -> {init_response['error']}: {init_response.get('message')}" # type: ignore
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
all_data: list = init_response.get(data_key, []) # type: ignore
|
| 242 |
+
|
| 243 |
+
next_page = init_response.get("next_page", None) # type: ignore
|
| 244 |
+
while next_page:
|
| 245 |
+
url = response.url.update_query(next_page=next_page).human_repr()
|
| 246 |
+
response_data = await session.get_json(url)
|
| 247 |
+
|
| 248 |
+
all_data.extend(response_data.get(data_key, [])) # type: ignore
|
| 249 |
+
next_page = response_data.get("next_page", None) # type: ignore
|
| 250 |
+
|
| 251 |
+
return all_data
|
| 252 |
+
|
| 253 |
+
url = f"{base_url}&{query_str}&api_key={api_key}"
|
| 254 |
+
|
| 255 |
+
return await amake_request(url, response_callback=callback, **kwargs) # type: ignore
|
| 256 |
+
|
| 257 |
+
@staticmethod
|
| 258 |
+
def transform_data(
|
| 259 |
+
query: IntrinioEquityHistoricalQueryParams,
|
| 260 |
+
data: List[Dict],
|
| 261 |
+
**kwargs: Any,
|
| 262 |
+
) -> List[IntrinioEquityHistoricalData]:
|
| 263 |
+
"""Return the transformed data."""
|
| 264 |
+
if not data:
|
| 265 |
+
raise EmptyDataError("The request was returned empty.")
|
| 266 |
+
date_col = (
|
| 267 |
+
"time"
|
| 268 |
+
if query.interval in ["1m", "5m", "10m", "15m", "30m", "60m", "1h"]
|
| 269 |
+
else "date"
|
| 270 |
+
)
|
| 271 |
+
return [
|
| 272 |
+
IntrinioEquityHistoricalData.model_validate(d)
|
| 273 |
+
for d in sorted(data, key=lambda x: x[date_col], reverse=False)
|
| 274 |
+
]
|
openbb_platform/providers/intrinio/openbb_intrinio/models/equity_info.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Equity Info Model."""
|
| 2 |
+
|
| 3 |
+
from typing import Any, Dict, List, Optional
|
| 4 |
+
|
| 5 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 6 |
+
from openbb_core.provider.standard_models.equity_info import (
|
| 7 |
+
EquityInfoData,
|
| 8 |
+
EquityInfoQueryParams,
|
| 9 |
+
)
|
| 10 |
+
from openbb_core.provider.utils.helpers import (
|
| 11 |
+
amake_requests,
|
| 12 |
+
)
|
| 13 |
+
from openbb_intrinio.utils.helpers import response_callback
|
| 14 |
+
from pydantic import Field
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class IntrinioEquityInfoQueryParams(EquityInfoQueryParams):
|
| 18 |
+
"""Intrinio Equity Info Query.
|
| 19 |
+
|
| 20 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_company_v2
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
__json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class IntrinioEquityInfoData(EquityInfoData):
|
| 27 |
+
"""Intrinio Equity Info Data."""
|
| 28 |
+
|
| 29 |
+
__alias_dict__ = {
|
| 30 |
+
"symbol": "ticker",
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
id: str = Field(default=None, description="Intrinio ID for the company.")
|
| 34 |
+
thea_enabled: Optional[bool] = Field(
|
| 35 |
+
default=None, description="Whether the company has been enabled for Thea."
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class IntrinioEquityInfoFetcher(
|
| 40 |
+
Fetcher[
|
| 41 |
+
IntrinioEquityInfoQueryParams,
|
| 42 |
+
List[IntrinioEquityInfoData],
|
| 43 |
+
]
|
| 44 |
+
):
|
| 45 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 46 |
+
|
| 47 |
+
@staticmethod
|
| 48 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioEquityInfoQueryParams:
|
| 49 |
+
"""Transform the query."""
|
| 50 |
+
return IntrinioEquityInfoQueryParams(**params)
|
| 51 |
+
|
| 52 |
+
# pylint: disable=W0613:unused-argument
|
| 53 |
+
@staticmethod
|
| 54 |
+
async def aextract_data(
|
| 55 |
+
query: IntrinioEquityInfoQueryParams,
|
| 56 |
+
credentials: Optional[Dict[str, str]],
|
| 57 |
+
**kwargs: Any,
|
| 58 |
+
) -> Dict:
|
| 59 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 60 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 61 |
+
base_url = "https://api-v2.intrinio.com"
|
| 62 |
+
|
| 63 |
+
urls = [
|
| 64 |
+
f"{base_url}/companies/{s.strip()}?api_key={api_key}"
|
| 65 |
+
for s in query.symbol.split(",")
|
| 66 |
+
]
|
| 67 |
+
|
| 68 |
+
return await amake_requests(urls, response_callback, **kwargs)
|
| 69 |
+
|
| 70 |
+
# pylint: disable=W0613:unused-argument
|
| 71 |
+
@staticmethod
|
| 72 |
+
def transform_data(
|
| 73 |
+
query: IntrinioEquityInfoQueryParams,
|
| 74 |
+
data: List[Dict],
|
| 75 |
+
**kwargs: Any,
|
| 76 |
+
) -> List[IntrinioEquityInfoData]:
|
| 77 |
+
"""Transform the data."""
|
| 78 |
+
return [IntrinioEquityInfoData.model_validate(d) for d in data]
|
openbb_platform/providers/intrinio/openbb_intrinio/models/equity_quote.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Equity Quote Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
import re
|
| 5 |
+
import warnings
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import Any, Dict, List, Optional
|
| 8 |
+
|
| 9 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 10 |
+
from openbb_core.provider.standard_models.equity_quote import (
|
| 11 |
+
EquityQuoteData,
|
| 12 |
+
EquityQuoteQueryParams,
|
| 13 |
+
)
|
| 14 |
+
from openbb_core.provider.utils.helpers import (
|
| 15 |
+
ClientResponse,
|
| 16 |
+
amake_requests,
|
| 17 |
+
)
|
| 18 |
+
from openbb_intrinio.utils.references import SOURCES, VENUES, IntrinioSecurity
|
| 19 |
+
from pydantic import Field, field_validator
|
| 20 |
+
|
| 21 |
+
_warn = warnings.warn
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class IntrinioEquityQuoteQueryParams(EquityQuoteQueryParams):
|
| 25 |
+
"""Intrinio Equity Quote Query.
|
| 26 |
+
|
| 27 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_security_realtime_price_v2
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
__json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
|
| 31 |
+
|
| 32 |
+
symbol: str = Field(
|
| 33 |
+
description="A Security identifier (Ticker, FIGI, ISIN, CUSIP, Intrinio ID)."
|
| 34 |
+
)
|
| 35 |
+
source: SOURCES = Field(default="iex", description="Source of the data.")
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class IntrinioEquityQuoteData(EquityQuoteData):
|
| 39 |
+
"""Intrinio Equity Quote Data."""
|
| 40 |
+
|
| 41 |
+
__alias_dict__ = {
|
| 42 |
+
"exchange": "listing_venue",
|
| 43 |
+
"market_center": "market_center_code",
|
| 44 |
+
"bid": "bid_price",
|
| 45 |
+
"ask": "ask_price",
|
| 46 |
+
"open": "open_price",
|
| 47 |
+
"close": "close_price",
|
| 48 |
+
"low": "low_price",
|
| 49 |
+
"high": "high_price",
|
| 50 |
+
"last_timestamp": "last_time",
|
| 51 |
+
"volume": "market_volume",
|
| 52 |
+
}
|
| 53 |
+
is_darkpool: Optional[bool] = Field(
|
| 54 |
+
default=None, description="Whether or not the current trade is from a darkpool."
|
| 55 |
+
)
|
| 56 |
+
source: Optional[str] = Field(
|
| 57 |
+
default=None, description="Source of the Intrinio data."
|
| 58 |
+
)
|
| 59 |
+
updated_on: datetime = Field(
|
| 60 |
+
description="Date and Time when the data was last updated."
|
| 61 |
+
)
|
| 62 |
+
security: Optional[IntrinioSecurity] = Field(
|
| 63 |
+
default=None, description="Security details related to the quote."
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
@field_validator("last_time", "updated_on", mode="before", check_fields=False)
|
| 67 |
+
@classmethod
|
| 68 |
+
def date_validate(cls, v): # pylint: disable=E0213
|
| 69 |
+
"""Return the date as a datetime object."""
|
| 70 |
+
return (
|
| 71 |
+
datetime.fromisoformat(v.replace("Z", "+00:00"))
|
| 72 |
+
if v.endswith(("Z", "+00:00"))
|
| 73 |
+
else datetime.fromisoformat(v)
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
@field_validator("sales_conditions", mode="before", check_fields=False)
|
| 77 |
+
@classmethod
|
| 78 |
+
def validate_sales_conditions(cls, v):
|
| 79 |
+
"""Validate sales conditions and remove empty strings."""
|
| 80 |
+
if v:
|
| 81 |
+
control_char_re = re.compile(r"[\x00-\x1f\x7f-\x9f]")
|
| 82 |
+
v = control_char_re.sub("", v).strip()
|
| 83 |
+
v = None if v == "" else v
|
| 84 |
+
return v if v else None
|
| 85 |
+
|
| 86 |
+
@field_validator("exchange", "market_center", mode="before", check_fields=False)
|
| 87 |
+
@classmethod
|
| 88 |
+
def validate_listing_venue(cls, v):
|
| 89 |
+
"""Validate listing venue and remove empty strings."""
|
| 90 |
+
if v:
|
| 91 |
+
return VENUES.get(v, v)
|
| 92 |
+
return None
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
class IntrinioEquityQuoteFetcher(
|
| 96 |
+
Fetcher[
|
| 97 |
+
IntrinioEquityQuoteQueryParams,
|
| 98 |
+
List[IntrinioEquityQuoteData],
|
| 99 |
+
]
|
| 100 |
+
):
|
| 101 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 102 |
+
|
| 103 |
+
@staticmethod
|
| 104 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioEquityQuoteQueryParams:
|
| 105 |
+
"""Transform the query params."""
|
| 106 |
+
return IntrinioEquityQuoteQueryParams(**params)
|
| 107 |
+
|
| 108 |
+
@staticmethod
|
| 109 |
+
async def aextract_data(
|
| 110 |
+
query: IntrinioEquityQuoteQueryParams,
|
| 111 |
+
credentials: Optional[Dict[str, str]],
|
| 112 |
+
**kwargs: Any,
|
| 113 |
+
) -> List[Dict]:
|
| 114 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 115 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 116 |
+
|
| 117 |
+
base_url = "https://api-v2.intrinio.com"
|
| 118 |
+
|
| 119 |
+
async def callback(response: ClientResponse, _: Any) -> dict:
|
| 120 |
+
"""Return the response."""
|
| 121 |
+
if response.status != 200:
|
| 122 |
+
return {}
|
| 123 |
+
|
| 124 |
+
response_data = await response.json()
|
| 125 |
+
response_data["symbol"] = response_data["security"].get("ticker", None) # type: ignore
|
| 126 |
+
if "messages" in response_data and response_data.get("messages"): # type: ignore
|
| 127 |
+
_message = list(response_data.pop("messages")) # type: ignore
|
| 128 |
+
_warn(str(",".join(_message)))
|
| 129 |
+
return response_data # type: ignore
|
| 130 |
+
|
| 131 |
+
urls = [
|
| 132 |
+
f"{base_url}/securities/{s.strip()}/prices/realtime?source={query.source}&api_key={api_key}"
|
| 133 |
+
for s in query.symbol.split(",")
|
| 134 |
+
]
|
| 135 |
+
return await amake_requests(urls, callback, **kwargs)
|
| 136 |
+
|
| 137 |
+
@staticmethod
|
| 138 |
+
def transform_data(
|
| 139 |
+
query: IntrinioEquityQuoteQueryParams, data: List[Dict], **kwargs: Any
|
| 140 |
+
) -> List[IntrinioEquityQuoteData]:
|
| 141 |
+
"""Return the transformed data."""
|
| 142 |
+
return [IntrinioEquityQuoteData.model_validate(d) for d in data]
|
openbb_platform/providers/intrinio/openbb_intrinio/models/equity_search.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Equity Search Model."""
|
| 2 |
+
|
| 3 |
+
from typing import Any, Dict, List, Optional
|
| 4 |
+
|
| 5 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 6 |
+
from openbb_core.provider.standard_models.equity_search import (
|
| 7 |
+
EquitySearchData,
|
| 8 |
+
EquitySearchQueryParams,
|
| 9 |
+
)
|
| 10 |
+
from openbb_core.provider.utils.descriptions import (
|
| 11 |
+
DATA_DESCRIPTIONS,
|
| 12 |
+
QUERY_DESCRIPTIONS,
|
| 13 |
+
)
|
| 14 |
+
from openbb_core.provider.utils.helpers import get_querystring
|
| 15 |
+
from openbb_intrinio.utils.helpers import get_data_one
|
| 16 |
+
from pydantic import Field
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class IntrinioEquitySearchQueryParams(EquitySearchQueryParams):
|
| 20 |
+
"""Intrinio Equity Search Query.
|
| 21 |
+
|
| 22 |
+
Source: https://docs.intrinio.com/documentation/web_api/search_companies_v2
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
__alias_dict__ = {
|
| 26 |
+
"limit": "page_size",
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
active: bool = Field(
|
| 30 |
+
default=True,
|
| 31 |
+
description="When true, return companies that are actively traded (having stock prices within the past 14 days)."
|
| 32 |
+
+ " When false, return companies that are not actively traded or never have been traded.",
|
| 33 |
+
)
|
| 34 |
+
limit: Optional[int] = Field(
|
| 35 |
+
default=10000,
|
| 36 |
+
description=QUERY_DESCRIPTIONS.get("limit", ""),
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class IntrinioEquitySearchData(EquitySearchData):
|
| 41 |
+
"""Intrinio Equity Search Data."""
|
| 42 |
+
|
| 43 |
+
__alias_dict__ = {
|
| 44 |
+
"intrinio_id": "id",
|
| 45 |
+
"symbol": "ticker",
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
cik: Optional[str] = Field(description=DATA_DESCRIPTIONS.get("CIK", ""))
|
| 49 |
+
lei: Optional[str] = Field(
|
| 50 |
+
description="The Legal Entity Identifier (LEI) of the company."
|
| 51 |
+
)
|
| 52 |
+
intrinio_id: str = Field(description="The Intrinio ID of the company.")
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class IntrinioEquitySearchFetcher(
|
| 56 |
+
Fetcher[
|
| 57 |
+
IntrinioEquitySearchQueryParams,
|
| 58 |
+
List[IntrinioEquitySearchData],
|
| 59 |
+
]
|
| 60 |
+
):
|
| 61 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 62 |
+
|
| 63 |
+
@staticmethod
|
| 64 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioEquitySearchQueryParams:
|
| 65 |
+
"""Transform the query."""
|
| 66 |
+
return IntrinioEquitySearchQueryParams(**params)
|
| 67 |
+
|
| 68 |
+
@staticmethod
|
| 69 |
+
async def aextract_data(
|
| 70 |
+
query: IntrinioEquitySearchQueryParams, # pylint: disable=unused-argument
|
| 71 |
+
credentials: Optional[Dict[str, str]],
|
| 72 |
+
**kwargs: Any,
|
| 73 |
+
) -> List[Dict]:
|
| 74 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 75 |
+
|
| 76 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 77 |
+
query_str = get_querystring(query.model_dump(by_alias=True), ["is_symbol"])
|
| 78 |
+
base_url = "https://api-v2.intrinio.com/companies/search?"
|
| 79 |
+
url = f"{base_url}{query_str}&api_key={api_key}"
|
| 80 |
+
data = await get_data_one(url, **kwargs)
|
| 81 |
+
return data
|
| 82 |
+
|
| 83 |
+
@staticmethod
|
| 84 |
+
def transform_data(
|
| 85 |
+
query: IntrinioEquitySearchQueryParams, data: Dict, **kwargs: Any
|
| 86 |
+
) -> List[IntrinioEquitySearchData]:
|
| 87 |
+
"""Transform the data to the standard format."""
|
| 88 |
+
|
| 89 |
+
return [IntrinioEquitySearchData.model_validate(d) for d in data["companies"]]
|
openbb_platform/providers/intrinio/openbb_intrinio/models/etf_holdings.py
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio ETF Holdings Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
from datetime import date as dateType
|
| 6 |
+
from typing import Any, Dict, List, Optional, Union
|
| 7 |
+
|
| 8 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 9 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 10 |
+
from openbb_core.provider.standard_models.etf_holdings import (
|
| 11 |
+
EtfHoldingsData,
|
| 12 |
+
EtfHoldingsQueryParams,
|
| 13 |
+
)
|
| 14 |
+
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
| 15 |
+
from openbb_core.provider.utils.helpers import (
|
| 16 |
+
ClientResponse,
|
| 17 |
+
ClientSession,
|
| 18 |
+
amake_request,
|
| 19 |
+
)
|
| 20 |
+
from pydantic import Field, model_validator
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
class IntrinioEtfHoldingsQueryParams(EtfHoldingsQueryParams):
|
| 24 |
+
"""
|
| 25 |
+
Intrinio ETF Holdings Query Params.
|
| 26 |
+
|
| 27 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_etf_holdings_v2
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
__alias_dict__ = {"date": "as_of_date"}
|
| 31 |
+
|
| 32 |
+
date: Optional[dateType] = Field(
|
| 33 |
+
default=None, description=QUERY_DESCRIPTIONS.get("date", "")
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class IntrinioEtfHoldingsData(EtfHoldingsData):
|
| 38 |
+
"""Intrinio ETF Holdings Data."""
|
| 39 |
+
|
| 40 |
+
__alias_dict__ = {
|
| 41 |
+
"symbol": "ticker",
|
| 42 |
+
"security_type": "type",
|
| 43 |
+
"unit": "quantity_units",
|
| 44 |
+
"face_value": "face",
|
| 45 |
+
"balance": "quantity_held",
|
| 46 |
+
"value": "market_value_held",
|
| 47 |
+
"derivatives_value": "notional_value",
|
| 48 |
+
"units_per_share": "quantity_per_share",
|
| 49 |
+
"weight": "weighting",
|
| 50 |
+
"updated": "as_of_date",
|
| 51 |
+
"country": "location",
|
| 52 |
+
"maturity_date": "maturity",
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
name: Optional[str] = Field(
|
| 56 |
+
default=None,
|
| 57 |
+
description="The common name for the holding.",
|
| 58 |
+
)
|
| 59 |
+
security_type: Optional[str] = Field(
|
| 60 |
+
default=None,
|
| 61 |
+
description="The type of instrument for this holding. Examples(Bond='BOND', Equity='EQUI')",
|
| 62 |
+
)
|
| 63 |
+
isin: Optional[str] = Field(
|
| 64 |
+
default=None,
|
| 65 |
+
description="The International Securities Identification Number.",
|
| 66 |
+
)
|
| 67 |
+
ric: Optional[str] = Field(
|
| 68 |
+
default=None,
|
| 69 |
+
description="The Reuters Instrument Code.",
|
| 70 |
+
)
|
| 71 |
+
sedol: Optional[str] = Field(
|
| 72 |
+
default=None,
|
| 73 |
+
description="The Stock Exchange Daily Official List.",
|
| 74 |
+
)
|
| 75 |
+
share_class_figi: Optional[str] = Field(
|
| 76 |
+
default=None,
|
| 77 |
+
description="The OpenFIGI symbol for the holding.",
|
| 78 |
+
)
|
| 79 |
+
country: Optional[str] = Field(
|
| 80 |
+
default=None,
|
| 81 |
+
description="The country or region of the holding.",
|
| 82 |
+
)
|
| 83 |
+
maturity_date: Optional[dateType] = Field(
|
| 84 |
+
default=None,
|
| 85 |
+
description="The maturity date for the debt security, if available.",
|
| 86 |
+
)
|
| 87 |
+
contract_expiry_date: Optional[dateType] = Field(
|
| 88 |
+
default=None,
|
| 89 |
+
description="Expiry date for the futures contract held, if available.",
|
| 90 |
+
)
|
| 91 |
+
coupon: Optional[float] = Field(
|
| 92 |
+
default=None,
|
| 93 |
+
description="The coupon rate of the debt security, if available.",
|
| 94 |
+
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
|
| 95 |
+
)
|
| 96 |
+
balance: Optional[Union[int, float]] = Field(
|
| 97 |
+
default=None,
|
| 98 |
+
description="The number of units of the security held, if available.",
|
| 99 |
+
)
|
| 100 |
+
unit: Optional[str] = Field(
|
| 101 |
+
default=None,
|
| 102 |
+
description="The units of the 'balance' field.",
|
| 103 |
+
)
|
| 104 |
+
units_per_share: Optional[float] = Field(
|
| 105 |
+
default=None,
|
| 106 |
+
description="Number of units of the security held per share outstanding of the ETF, if available.",
|
| 107 |
+
)
|
| 108 |
+
face_value: Optional[float] = Field(
|
| 109 |
+
default=None,
|
| 110 |
+
description="The face value of the debt security, if available.",
|
| 111 |
+
json_schema_extra={"x-unit_measurement": "currency"},
|
| 112 |
+
)
|
| 113 |
+
derivatives_value: Optional[float] = Field(
|
| 114 |
+
default=None,
|
| 115 |
+
description="The notional value of derivatives contracts held.",
|
| 116 |
+
json_schema_extra={"x-unit_measurement": "currency"},
|
| 117 |
+
)
|
| 118 |
+
value: Optional[float] = Field(
|
| 119 |
+
default=None,
|
| 120 |
+
description="The market value of the holding, on the 'as_of' date.",
|
| 121 |
+
json_schema_extra={"x-unit_measurement": "currency"},
|
| 122 |
+
)
|
| 123 |
+
weight: Optional[float] = Field(
|
| 124 |
+
default=None,
|
| 125 |
+
description="The weight of the holding, as a normalized percent.",
|
| 126 |
+
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
|
| 127 |
+
)
|
| 128 |
+
updated: Optional[dateType] = Field(
|
| 129 |
+
default=None,
|
| 130 |
+
description="The 'as_of' date for the holding.",
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
@model_validator(mode="before")
|
| 134 |
+
@classmethod
|
| 135 |
+
def replace_zero(cls, values):
|
| 136 |
+
"""Check for zero values and replace with None."""
|
| 137 |
+
return (
|
| 138 |
+
{k: None if v == 0 else v for k, v in values.items()}
|
| 139 |
+
if isinstance(values, dict)
|
| 140 |
+
else values
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
class IntrinioEtfHoldingsFetcher(
|
| 145 |
+
Fetcher[IntrinioEtfHoldingsQueryParams, List[IntrinioEtfHoldingsData]]
|
| 146 |
+
):
|
| 147 |
+
"""Intrinio ETF Holdings Fetcher."""
|
| 148 |
+
|
| 149 |
+
@staticmethod
|
| 150 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioEtfHoldingsQueryParams:
|
| 151 |
+
"""Transform query."""
|
| 152 |
+
return IntrinioEtfHoldingsQueryParams(**params)
|
| 153 |
+
|
| 154 |
+
@staticmethod
|
| 155 |
+
async def aextract_data(
|
| 156 |
+
query: IntrinioEtfHoldingsQueryParams,
|
| 157 |
+
credentials: Optional[Dict[str, str]],
|
| 158 |
+
**kwargs: Any,
|
| 159 |
+
) -> List[Dict]:
|
| 160 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 161 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 162 |
+
symbol = query.symbol + ":US" if ":" not in query.symbol else query.symbol
|
| 163 |
+
URL = f"https://api-v2.intrinio.com/etfs/{symbol}/holdings?page_size=10000&api_key={api_key}"
|
| 164 |
+
if query.date:
|
| 165 |
+
URL += f"&as_of_date={query.date}"
|
| 166 |
+
data: List = []
|
| 167 |
+
|
| 168 |
+
async def response_callback(response: ClientResponse, session: ClientSession):
|
| 169 |
+
"""Async response callback."""
|
| 170 |
+
results = await response.json()
|
| 171 |
+
|
| 172 |
+
if results.get("error"): # type: ignore
|
| 173 |
+
return results
|
| 174 |
+
|
| 175 |
+
if results.get("holdings") and len(results.get("holdings")) > 0: # type: ignore
|
| 176 |
+
data.extend(results.get("holdings")) # type: ignore
|
| 177 |
+
while results.get("next_page"): # type: ignore
|
| 178 |
+
next_page = results["next_page"] # type: ignore
|
| 179 |
+
next_url = f"{URL}&next_page={next_page}"
|
| 180 |
+
results = await amake_request(next_url, session=session, **kwargs)
|
| 181 |
+
if (
|
| 182 |
+
"holdings" in results
|
| 183 |
+
and len(results.get("holdings")) > 0 # type: ignore
|
| 184 |
+
):
|
| 185 |
+
data.extend(results.get("holdings")) # type: ignore
|
| 186 |
+
return data
|
| 187 |
+
|
| 188 |
+
return await amake_request(URL, response_callback=response_callback, **kwargs) # type: ignore
|
| 189 |
+
|
| 190 |
+
@staticmethod
|
| 191 |
+
def transform_data(
|
| 192 |
+
query: IntrinioEtfHoldingsQueryParams,
|
| 193 |
+
data: List[Dict],
|
| 194 |
+
**kwargs: Any,
|
| 195 |
+
) -> List[IntrinioEtfHoldingsData]:
|
| 196 |
+
"""Transform data."""
|
| 197 |
+
if not data or isinstance(data, dict) and data.get("error"):
|
| 198 |
+
if isinstance(data, list) and data == []:
|
| 199 |
+
raise OpenBBError(
|
| 200 |
+
str(
|
| 201 |
+
f"No holdings were found for {query.symbol}, and the response from Intrinio was empty."
|
| 202 |
+
)
|
| 203 |
+
)
|
| 204 |
+
raise OpenBBError(str(f"{data.get('message')} {query.symbol}: {data['error']}")) # type: ignore
|
| 205 |
+
|
| 206 |
+
results: List[IntrinioEtfHoldingsData] = []
|
| 207 |
+
for d in sorted(data, key=lambda x: x["weighting"], reverse=True):
|
| 208 |
+
# This field is deprecated and is dupilcated in the response.
|
| 209 |
+
_ = d.pop("composite_figi", None)
|
| 210 |
+
if d.get("coupon"):
|
| 211 |
+
d["coupon"] = d["coupon"] / 100
|
| 212 |
+
results.append(IntrinioEtfHoldingsData.model_validate(d))
|
| 213 |
+
|
| 214 |
+
return results
|
openbb_platform/providers/intrinio/openbb_intrinio/models/etf_info.py
ADDED
|
@@ -0,0 +1,655 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio ETF Info Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
from datetime import date as dateType
|
| 6 |
+
from typing import Any, Dict, List, Optional
|
| 7 |
+
from warnings import warn
|
| 8 |
+
|
| 9 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 10 |
+
from openbb_core.provider.standard_models.etf_info import (
|
| 11 |
+
EtfInfoData,
|
| 12 |
+
EtfInfoQueryParams,
|
| 13 |
+
)
|
| 14 |
+
from openbb_core.provider.utils.errors import EmptyDataError
|
| 15 |
+
from openbb_core.provider.utils.helpers import amake_requests
|
| 16 |
+
from pydantic import Field
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class IntrinioEtfInfoQueryParams(EtfInfoQueryParams):
|
| 20 |
+
"""
|
| 21 |
+
Intrinio ETF Info Query Params.
|
| 22 |
+
|
| 23 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_etf_v2
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
__json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class IntrinioEtfInfoData(EtfInfoData):
|
| 30 |
+
"""Intrinio ETF Info Data."""
|
| 31 |
+
|
| 32 |
+
__alias_dict__ = {
|
| 33 |
+
"symbol": "ticker",
|
| 34 |
+
"exchange": "exchange_mic",
|
| 35 |
+
"issuer": "sponsor",
|
| 36 |
+
"investment_style": "type",
|
| 37 |
+
"industry_group": "sub_industry",
|
| 38 |
+
"holds_mlp": "holds_ml_ps",
|
| 39 |
+
"holds_adr": "holds_ad_rs",
|
| 40 |
+
"index_symbol": "index_ticker",
|
| 41 |
+
"figi_symbol": "figi_ticker",
|
| 42 |
+
"intrinio_id": "id",
|
| 43 |
+
"is_listed": "is_live_listed",
|
| 44 |
+
"beta_type": "smartvs_traditional_beta",
|
| 45 |
+
"beta_details": "smartvs_traditional_beta_level2",
|
| 46 |
+
"listing_country": "primary_ticker_country_code",
|
| 47 |
+
"listing_region": "primary_listing_region",
|
| 48 |
+
"primary_symbol": "primary_ticker",
|
| 49 |
+
"intraday_nav_symbol": "intraday_nav_ticker",
|
| 50 |
+
"issuer_country": "issuing_entity_country_code",
|
| 51 |
+
"livestock_type": "livestock",
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
fund_listing_date: Optional[dateType] = Field(
|
| 55 |
+
default=None,
|
| 56 |
+
description="The date on which the Exchange Traded Product (ETP)"
|
| 57 |
+
+ " or share class of the ETP is listed on a specific exchange.",
|
| 58 |
+
)
|
| 59 |
+
data_change_date: Optional[dateType] = Field(
|
| 60 |
+
default=None,
|
| 61 |
+
description="The last date on which there was a change in a classifications data field for this ETF.",
|
| 62 |
+
)
|
| 63 |
+
etn_maturity_date: Optional[dateType] = Field(
|
| 64 |
+
default=None,
|
| 65 |
+
description="If the product is an ETN, this field identifies the maturity date for the ETN.",
|
| 66 |
+
)
|
| 67 |
+
is_listed: Optional[bool] = Field(
|
| 68 |
+
default=None,
|
| 69 |
+
description="If true, the ETF is still listed on an exchange.",
|
| 70 |
+
)
|
| 71 |
+
close_date: Optional[dateType] = Field(
|
| 72 |
+
default=None,
|
| 73 |
+
description="The date on which the ETF was de-listed if it is no longer listed.",
|
| 74 |
+
)
|
| 75 |
+
exchange: Optional[str] = Field(
|
| 76 |
+
default=None,
|
| 77 |
+
description="The exchange Market Identifier Code (MIC).",
|
| 78 |
+
)
|
| 79 |
+
isin: Optional[str] = Field(
|
| 80 |
+
default=None,
|
| 81 |
+
description="International Securities Identification Number (ISIN).",
|
| 82 |
+
)
|
| 83 |
+
ric: Optional[str] = Field(
|
| 84 |
+
default=None,
|
| 85 |
+
description="Reuters Instrument Code (RIC).",
|
| 86 |
+
)
|
| 87 |
+
sedol: Optional[str] = Field(
|
| 88 |
+
default=None,
|
| 89 |
+
description="Stock Exchange Daily Official List (SEDOL).",
|
| 90 |
+
)
|
| 91 |
+
figi_symbol: Optional[str] = Field(
|
| 92 |
+
default=None,
|
| 93 |
+
description="Financial Instrument Global Identifier (FIGI) symbol.",
|
| 94 |
+
)
|
| 95 |
+
share_class_figi: Optional[str] = Field(
|
| 96 |
+
default=None,
|
| 97 |
+
description="Financial Instrument Global Identifier (FIGI).",
|
| 98 |
+
)
|
| 99 |
+
firstbridge_id: Optional[str] = Field(
|
| 100 |
+
default=None,
|
| 101 |
+
description="The FirstBridge unique identifier for the Exchange Traded Fund (ETF).",
|
| 102 |
+
)
|
| 103 |
+
firstbridge_parent_id: Optional[str] = Field(
|
| 104 |
+
default=None,
|
| 105 |
+
description="The FirstBridge unique identifier for the parent Exchange Traded Fund (ETF), if applicable.",
|
| 106 |
+
)
|
| 107 |
+
intrinio_id: Optional[str] = Field(
|
| 108 |
+
default=None,
|
| 109 |
+
description="Intrinio unique identifier for the security.",
|
| 110 |
+
)
|
| 111 |
+
intraday_nav_symbol: Optional[str] = Field(
|
| 112 |
+
default=None,
|
| 113 |
+
description="Intraday Net Asset Value (NAV) symbol.",
|
| 114 |
+
)
|
| 115 |
+
primary_symbol: Optional[str] = Field(
|
| 116 |
+
default=None,
|
| 117 |
+
description="The primary ticker field is used for Exchange Traded Products (ETPs)"
|
| 118 |
+
+ " that have multiple listings and share classes."
|
| 119 |
+
+ " If an ETP has multiple listings or share classes,"
|
| 120 |
+
+ " the same primary ticker is assigned to all the listings and share classes.",
|
| 121 |
+
)
|
| 122 |
+
etp_structure_type: Optional[str] = Field(
|
| 123 |
+
default=None,
|
| 124 |
+
description="Classifies Exchange Traded Products (ETPs) into very broad categories based on its legal structure.",
|
| 125 |
+
)
|
| 126 |
+
legal_structure: Optional[str] = Field(
|
| 127 |
+
default=None,
|
| 128 |
+
description="Legal structure of the fund.",
|
| 129 |
+
)
|
| 130 |
+
issuer: Optional[str] = Field(
|
| 131 |
+
default=None,
|
| 132 |
+
description="Issuer of the ETF.",
|
| 133 |
+
)
|
| 134 |
+
etn_issuing_bank: Optional[str] = Field(
|
| 135 |
+
default=None,
|
| 136 |
+
description="If the product is an Exchange Traded Note (ETN), this field identifies the issuing bank.",
|
| 137 |
+
)
|
| 138 |
+
fund_family: Optional[str] = Field(
|
| 139 |
+
default=None,
|
| 140 |
+
description="This field identifies the fund family to which the ETF belongs, as categorized by the ETF Sponsor.",
|
| 141 |
+
)
|
| 142 |
+
investment_style: Optional[str] = Field(
|
| 143 |
+
default=None,
|
| 144 |
+
description="Investment style of the ETF.",
|
| 145 |
+
)
|
| 146 |
+
derivatives_based: Optional[str] = Field(
|
| 147 |
+
default=None,
|
| 148 |
+
description="This field is populated if the ETF holds either"
|
| 149 |
+
+ " listed or over-the-counter derivatives in its portfolio.",
|
| 150 |
+
)
|
| 151 |
+
income_category: Optional[str] = Field(
|
| 152 |
+
default=None,
|
| 153 |
+
description="Identifies if an Exchange Traded Fund (ETF) falls into a category"
|
| 154 |
+
+ " that is specifically designed to provide a high yield or income",
|
| 155 |
+
)
|
| 156 |
+
asset_class: Optional[str] = Field(
|
| 157 |
+
default=None,
|
| 158 |
+
description="Captures the underlying nature of the securities in the Exchanged Traded Product (ETP).",
|
| 159 |
+
)
|
| 160 |
+
other_asset_types: Optional[str] = Field(
|
| 161 |
+
default=None,
|
| 162 |
+
description="If 'asset_class' field is classified as 'Other Asset Types'"
|
| 163 |
+
+ " this field captures the specific category of the underlying assets.",
|
| 164 |
+
)
|
| 165 |
+
single_category_designation: Optional[str] = Field(
|
| 166 |
+
default=None,
|
| 167 |
+
description="This categorization is created for those users who want every ETF to be 'forced'"
|
| 168 |
+
+ " into a single bucket, so that the assets for all categories will always sum to the total market.",
|
| 169 |
+
)
|
| 170 |
+
beta_type: Optional[str] = Field(
|
| 171 |
+
default=None,
|
| 172 |
+
description="This field identifies whether an ETF provides 'Traditional' beta exposure or 'Smart' beta exposure."
|
| 173 |
+
+ " ETFs that are active (i.e. non-indexed), leveraged / inverse or have a proprietary quant model"
|
| 174 |
+
+ " (i.e. that don't provide indexed exposure to a targeted factor) are classified separately.",
|
| 175 |
+
)
|
| 176 |
+
beta_details: Optional[str] = Field(
|
| 177 |
+
default=None,
|
| 178 |
+
description="This field provides further detail within the traditional and smart beta categories.",
|
| 179 |
+
)
|
| 180 |
+
market_cap_range: Optional[str] = Field(
|
| 181 |
+
default=None,
|
| 182 |
+
description="Equity ETFs are classified as falling into categories"
|
| 183 |
+
+ " based on the description of their investment strategy in the prospectus."
|
| 184 |
+
+ " Examples ('Mega Cap', 'Large Cap', 'Mid Cap', etc.)",
|
| 185 |
+
)
|
| 186 |
+
market_cap_weighting_type: Optional[str] = Field(
|
| 187 |
+
default=None,
|
| 188 |
+
description="For ETFs that take the value 'Market Cap Weighted' in the 'index_weighting_scheme' field,"
|
| 189 |
+
+ " this field provides detail on the market cap weighting type.",
|
| 190 |
+
)
|
| 191 |
+
index_weighting_scheme: Optional[str] = Field(
|
| 192 |
+
default=None,
|
| 193 |
+
description="For ETFs that track an underlying index,"
|
| 194 |
+
+ " this field provides detail on the index weighting type.",
|
| 195 |
+
)
|
| 196 |
+
index_linked: Optional[str] = Field(
|
| 197 |
+
default=None,
|
| 198 |
+
description="This field identifies whether an ETF is index linked or active.",
|
| 199 |
+
)
|
| 200 |
+
index_name: Optional[str] = Field(
|
| 201 |
+
default=None,
|
| 202 |
+
description="This field identifies the name of the underlying index tracked by the ETF, if applicable.",
|
| 203 |
+
)
|
| 204 |
+
index_symbol: Optional[str] = Field(
|
| 205 |
+
default=None,
|
| 206 |
+
description="This field identifies the OpenFIGI ticker for the Index underlying the ETF.",
|
| 207 |
+
)
|
| 208 |
+
parent_index: Optional[str] = Field(
|
| 209 |
+
default=None,
|
| 210 |
+
description="This field identifies the name of the parent index, which represents"
|
| 211 |
+
+ " the broader universe from which the index underlying the ETF is created, if applicable.",
|
| 212 |
+
)
|
| 213 |
+
index_family: Optional[str] = Field(
|
| 214 |
+
default=None,
|
| 215 |
+
description="This field identifies the index family to which the index underlying the ETF belongs."
|
| 216 |
+
+ " The index family is represented as categorized by the index provider.",
|
| 217 |
+
)
|
| 218 |
+
broader_index_family: Optional[str] = Field(
|
| 219 |
+
default=None,
|
| 220 |
+
description="This field identifies the broader index family to which the index underlying the ETF belongs."
|
| 221 |
+
+ " The broader index family is represented as categorized by the index provider.",
|
| 222 |
+
)
|
| 223 |
+
index_provider: Optional[str] = Field(
|
| 224 |
+
default=None,
|
| 225 |
+
description="This field identifies the Index provider for the index underlying the ETF, if applicable.",
|
| 226 |
+
)
|
| 227 |
+
index_provider_code: Optional[str] = Field(
|
| 228 |
+
default=None,
|
| 229 |
+
description="This field provides the First Bridge code for each Index provider,"
|
| 230 |
+
+ " corresponding to the index underlying the ETF if applicable.",
|
| 231 |
+
)
|
| 232 |
+
replication_structure: Optional[str] = Field(
|
| 233 |
+
default=None,
|
| 234 |
+
description="The replication structure of the Exchange Traded Product (ETP).",
|
| 235 |
+
)
|
| 236 |
+
growth_value_tilt: Optional[str] = Field(
|
| 237 |
+
default=None,
|
| 238 |
+
description="Classifies equity ETFs as either 'Growth' or Value' based on the stated style tilt"
|
| 239 |
+
+ " in the ETF prospectus. Equity ETFs that do not have a stated style tilt are classified as 'Core / Blend'.",
|
| 240 |
+
)
|
| 241 |
+
growth_type: Optional[str] = Field(
|
| 242 |
+
default=None,
|
| 243 |
+
description="For ETFs that are classified as 'Growth' in 'growth_value_tilt',"
|
| 244 |
+
+ " this field further identifies those where the stocks in the"
|
| 245 |
+
+ " ETF are both selected and weighted based on their growth (style factor) scores.",
|
| 246 |
+
)
|
| 247 |
+
value_type: Optional[str] = Field(
|
| 248 |
+
default=None,
|
| 249 |
+
description="For ETFs that are classified as 'Value' in 'growth_value_tilt',"
|
| 250 |
+
+ " this field further identifies those where the stocks in the"
|
| 251 |
+
+ " ETF are both selected and weighted based on their value (style factor) scores.",
|
| 252 |
+
)
|
| 253 |
+
sector: Optional[str] = Field(
|
| 254 |
+
default=None,
|
| 255 |
+
description="For equity ETFs that aim to provide targeted exposure to a sector or industry,"
|
| 256 |
+
+ " this field identifies the Sector that it provides the exposure to.",
|
| 257 |
+
)
|
| 258 |
+
industry: Optional[str] = Field(
|
| 259 |
+
default=None,
|
| 260 |
+
description="For equity ETFs that aim to provide targeted exposure to an industry,"
|
| 261 |
+
+ " this field identifies the Industry that it provides the exposure to.",
|
| 262 |
+
)
|
| 263 |
+
industry_group: Optional[str] = Field(
|
| 264 |
+
default=None,
|
| 265 |
+
description="For equity ETFs that aim to provide targeted exposure to a sub-industry,"
|
| 266 |
+
+ " this field identifies the sub-Industry that it provides the exposure to.",
|
| 267 |
+
)
|
| 268 |
+
cross_sector_theme: Optional[str] = Field(
|
| 269 |
+
default=None,
|
| 270 |
+
description="For equity ETFs that aim to provide targeted exposure to a specific investment theme"
|
| 271 |
+
+ " that cuts across GICS sectors, this field identifies the specific cross-sector theme."
|
| 272 |
+
+ " Examples ('Agri-business', 'Natural Resources', 'Green Investing', etc.)",
|
| 273 |
+
)
|
| 274 |
+
natural_resources_type: Optional[str] = Field(
|
| 275 |
+
default=None,
|
| 276 |
+
description="For ETFs that are classified as 'Natural Resources' in the 'cross_sector_theme' field,"
|
| 277 |
+
+ " this field provides further detail on the type of Natural Resources exposure.",
|
| 278 |
+
)
|
| 279 |
+
us_or_excludes_us: Optional[str] = Field(
|
| 280 |
+
default=None,
|
| 281 |
+
description="Takes the value of 'Domestic' for US exposure,"
|
| 282 |
+
+ " 'International' for non-US exposure and 'Global' for exposure that includes all regions including the US.",
|
| 283 |
+
)
|
| 284 |
+
developed_emerging: Optional[str] = Field(
|
| 285 |
+
default=None,
|
| 286 |
+
description="This field identifies the stage of development of the markets that the ETF provides exposure to.",
|
| 287 |
+
)
|
| 288 |
+
specialized_region: Optional[str] = Field(
|
| 289 |
+
default=None,
|
| 290 |
+
description="This field is populated if the ETF provides targeted"
|
| 291 |
+
+ " exposure to a specific type of geography-based grouping"
|
| 292 |
+
+ " that does not fall into a specific country or continent grouping."
|
| 293 |
+
+ " Examples ('BRIC', 'Chindia', etc.)",
|
| 294 |
+
)
|
| 295 |
+
continent: Optional[str] = Field(
|
| 296 |
+
default=None,
|
| 297 |
+
description="This field is populated if the ETF provides targeted exposure"
|
| 298 |
+
+ " to a specific continent or country within that Continent.",
|
| 299 |
+
)
|
| 300 |
+
latin_america_sub_group: Optional[str] = Field(
|
| 301 |
+
default=None,
|
| 302 |
+
description="For ETFs that are classified as 'Latin America' in the 'continent' field,"
|
| 303 |
+
+ " this field provides further detail on the type of regional exposure.",
|
| 304 |
+
)
|
| 305 |
+
europe_sub_group: Optional[str] = Field(
|
| 306 |
+
default=None,
|
| 307 |
+
description="For ETFs that are classified as 'Europe' in the 'continent' field,"
|
| 308 |
+
+ " this field provides further detail on the type of regional exposure.",
|
| 309 |
+
)
|
| 310 |
+
asia_sub_group: Optional[str] = Field(
|
| 311 |
+
default=None,
|
| 312 |
+
description="For ETFs that are classified as 'Asia' in the 'continent' field,"
|
| 313 |
+
+ " this field provides further detail on the type of regional exposure.",
|
| 314 |
+
)
|
| 315 |
+
specific_country: Optional[str] = Field(
|
| 316 |
+
default=None,
|
| 317 |
+
description="This field is populated if the ETF provides targeted exposure to a specific country.",
|
| 318 |
+
)
|
| 319 |
+
china_listing_location: Optional[str] = Field(
|
| 320 |
+
default=None,
|
| 321 |
+
description="For ETFs that are classified as 'China' in the 'country' field,"
|
| 322 |
+
+ " this field provides further detail on the type of exposure in the underlying securities.",
|
| 323 |
+
)
|
| 324 |
+
us_state: Optional[str] = Field(
|
| 325 |
+
default=None,
|
| 326 |
+
description="Takes the value of a US state if the ETF provides"
|
| 327 |
+
+ " targeted exposure to the municipal bonds or equities of companies.",
|
| 328 |
+
)
|
| 329 |
+
real_estate: Optional[str] = Field(
|
| 330 |
+
default=None,
|
| 331 |
+
description="For ETFs that provide targeted real estate exposure,"
|
| 332 |
+
+ " this field is populated if the ETF provides targeted"
|
| 333 |
+
+ " exposure to a specific segment of the real estate market.",
|
| 334 |
+
)
|
| 335 |
+
|
| 336 |
+
fundamental_weighting_type: Optional[str] = Field(
|
| 337 |
+
default=None,
|
| 338 |
+
description="For ETFs that take the value 'Fundamental Weighted' in the 'index_weighting_scheme' field,"
|
| 339 |
+
+ " this field provides detail on the fundamental weighting methodology.",
|
| 340 |
+
)
|
| 341 |
+
dividend_weighting_type: Optional[str] = Field(
|
| 342 |
+
default=None,
|
| 343 |
+
description="For ETFs that take the value 'Dividend Weighted' in the 'index_weighting_scheme' field,"
|
| 344 |
+
+ " this field provides detail on the dividend weighting methodology.",
|
| 345 |
+
)
|
| 346 |
+
bond_type: Optional[str] = Field(
|
| 347 |
+
default=None,
|
| 348 |
+
description="For ETFs where 'asset_class_type' is 'Bonds',"
|
| 349 |
+
+ " this field provides detail on the type of bonds held in the ETF.",
|
| 350 |
+
)
|
| 351 |
+
government_bond_types: Optional[str] = Field(
|
| 352 |
+
default=None,
|
| 353 |
+
description="For bond ETFs that take the value 'Treasury & Government' in 'bond_type',"
|
| 354 |
+
+ " this field provides detail on the exposure.",
|
| 355 |
+
)
|
| 356 |
+
municipal_bond_region: Optional[str] = Field(
|
| 357 |
+
default=None,
|
| 358 |
+
description="For bond ETFs that take the value 'Municipal' in 'bond_type',"
|
| 359 |
+
+ " this field provides additional detail on the geographic exposure.",
|
| 360 |
+
)
|
| 361 |
+
municipal_vrdo: Optional[bool] = Field(
|
| 362 |
+
default=None,
|
| 363 |
+
description="For bond ETFs that take the value 'Municipal' in 'bond_type',"
|
| 364 |
+
+ " this field identifies those ETFs that specifically provide exposure to Variable Rate Demand Obligations.",
|
| 365 |
+
)
|
| 366 |
+
mortgage_bond_types: Optional[str] = Field(
|
| 367 |
+
default=None,
|
| 368 |
+
description="For bond ETFs that take the value 'Mortgage' in 'bond_type',"
|
| 369 |
+
+ " this field provides additional detail on the type of underlying securities.",
|
| 370 |
+
)
|
| 371 |
+
bond_tax_status: Optional[str] = Field(
|
| 372 |
+
default=None,
|
| 373 |
+
description="For all US bond ETFs, this field provides additional"
|
| 374 |
+
+ " detail on the tax treatment of the underlying securities.",
|
| 375 |
+
)
|
| 376 |
+
credit_quality: Optional[str] = Field(
|
| 377 |
+
default=None,
|
| 378 |
+
description="For all bond ETFs, this field helps to identify if the ETF"
|
| 379 |
+
+ " provides targeted exposure to securities of a specific credit quality range.",
|
| 380 |
+
)
|
| 381 |
+
average_maturity: Optional[str] = Field(
|
| 382 |
+
default=None,
|
| 383 |
+
description="For all bond ETFs, this field helps to identify if the ETF"
|
| 384 |
+
+ " provides targeted exposure to securities of a specific maturity range.",
|
| 385 |
+
)
|
| 386 |
+
specific_maturity_year: Optional[int] = Field(
|
| 387 |
+
default=None,
|
| 388 |
+
description="For all bond ETFs that take the value 'Specific Maturity Year' in the 'average_maturity' field,"
|
| 389 |
+
+ " this field specifies the calendar year.",
|
| 390 |
+
)
|
| 391 |
+
commodity_types: Optional[str] = Field(
|
| 392 |
+
default=None,
|
| 393 |
+
description="For ETFs where 'asset_class_type' is 'Commodities',"
|
| 394 |
+
+ " this field provides detail on the type of commodities held in the ETF.",
|
| 395 |
+
)
|
| 396 |
+
energy_type: Optional[str] = Field(
|
| 397 |
+
default=None,
|
| 398 |
+
description="For ETFs where 'commodity_type' is 'Energy',"
|
| 399 |
+
+ " this field provides detail on the type of energy exposure provided by the ETF.",
|
| 400 |
+
)
|
| 401 |
+
agricultural_type: Optional[str] = Field(
|
| 402 |
+
default=None,
|
| 403 |
+
description="For ETFs where 'commodity_type' is 'Agricultural',"
|
| 404 |
+
+ " this field provides detail on the type of agricultural exposure provided by the ETF.",
|
| 405 |
+
)
|
| 406 |
+
livestock_type: Optional[str] = Field(
|
| 407 |
+
default=None,
|
| 408 |
+
description="For ETFs where 'commodity_type' is 'Livestock',"
|
| 409 |
+
+ " this field provides detail on the type of livestock exposure provided by the ETF.",
|
| 410 |
+
)
|
| 411 |
+
metal_type: Optional[str] = Field(
|
| 412 |
+
default=None,
|
| 413 |
+
description="For ETFs where 'commodity_type' is 'Gold & Metals',"
|
| 414 |
+
+ " this field provides detail on the type of exposure provided by the ETF.",
|
| 415 |
+
)
|
| 416 |
+
inverse_leveraged: Optional[str] = Field(
|
| 417 |
+
default=None,
|
| 418 |
+
description="This field is populated if the ETF provides inverse or leveraged exposure.",
|
| 419 |
+
)
|
| 420 |
+
target_date_multi_asset_type: Optional[str] = Field(
|
| 421 |
+
default=None,
|
| 422 |
+
description="For ETFs where 'asset_class_type' is 'Target Date / MultiAsset',"
|
| 423 |
+
+ " this field provides detail on the type of commodities held in the ETF.",
|
| 424 |
+
)
|
| 425 |
+
currency_pair: Optional[str] = Field(
|
| 426 |
+
default=None,
|
| 427 |
+
description="This field is populated if the ETF's strategy involves providing exposure to"
|
| 428 |
+
+ " the movements of a currency or involves hedging currency exposure.",
|
| 429 |
+
)
|
| 430 |
+
social_environmental_type: Optional[str] = Field(
|
| 431 |
+
default=None,
|
| 432 |
+
description="This field is populated if the ETF's strategy involves providing"
|
| 433 |
+
+ " exposure to a specific social or environmental theme.",
|
| 434 |
+
)
|
| 435 |
+
clean_energy_type: Optional[str] = Field(
|
| 436 |
+
default=None,
|
| 437 |
+
description="This field is populated if the ETF has a value of 'Clean Energy'"
|
| 438 |
+
+ " in the 'social_environmental_type' field.",
|
| 439 |
+
)
|
| 440 |
+
dividend_type: Optional[str] = Field(
|
| 441 |
+
default=None,
|
| 442 |
+
description="This field is populated if the ETF has an intended"
|
| 443 |
+
+ " investment objective of holding dividend-oriented stocks as stated in the prospectus.",
|
| 444 |
+
)
|
| 445 |
+
regular_dividend_payor_type: Optional[str] = Field(
|
| 446 |
+
default=None,
|
| 447 |
+
description="This field is populated if the ETF has a value of"
|
| 448 |
+
+ "'Dividend - Regular Payors' in the 'dividend_type' field.",
|
| 449 |
+
)
|
| 450 |
+
quant_strategies_type: Optional[str] = Field(
|
| 451 |
+
default=None,
|
| 452 |
+
description="This field is populated if the ETF has either an index-linked"
|
| 453 |
+
+ " or active strategy that is based on a proprietary quantitative strategy.",
|
| 454 |
+
)
|
| 455 |
+
other_quant_models: Optional[str] = Field(
|
| 456 |
+
default=None,
|
| 457 |
+
description="For ETFs where 'quant_strategies_type' is 'Other Quant Model',"
|
| 458 |
+
+ " this field provides the name of the specific proprietary quant model"
|
| 459 |
+
+ " used as the underlying strategy for the ETF.",
|
| 460 |
+
)
|
| 461 |
+
hedge_fund_type: Optional[str] = Field(
|
| 462 |
+
default=None,
|
| 463 |
+
description="For ETFs where 'other_asset_types' is 'Hedge Fund Replication',"
|
| 464 |
+
+ " this field provides detail on the type of hedge fund replication strategy.",
|
| 465 |
+
)
|
| 466 |
+
excludes_financials: Optional[bool] = Field(
|
| 467 |
+
default=None,
|
| 468 |
+
description="For equity ETFs, identifies those ETFs"
|
| 469 |
+
+ " where the underlying fund holdings will not hold financials stocks,"
|
| 470 |
+
+ " based on the funds intended objective.",
|
| 471 |
+
)
|
| 472 |
+
excludes_technology: Optional[bool] = Field(
|
| 473 |
+
default=None,
|
| 474 |
+
description="For equity ETFs, identifies those ETFs"
|
| 475 |
+
+ " where the underlying fund holdings will not hold technology stocks,"
|
| 476 |
+
+ " based on the funds intended objective.",
|
| 477 |
+
)
|
| 478 |
+
holds_only_nyse_stocks: Optional[bool] = Field(
|
| 479 |
+
default=None,
|
| 480 |
+
description="If true, the ETF is an equity ETF and holds only stocks listed on NYSE.",
|
| 481 |
+
)
|
| 482 |
+
holds_only_nasdaq_stocks: Optional[bool] = Field(
|
| 483 |
+
default=None,
|
| 484 |
+
description="If true, the ETF is an equity ETF and holds only stocks listed on Nasdaq.",
|
| 485 |
+
)
|
| 486 |
+
holds_mlp: Optional[bool] = Field(
|
| 487 |
+
default=None,
|
| 488 |
+
description="If true, the ETF's investment objective explicitly specifies"
|
| 489 |
+
+ " that it holds MLPs as an intended part of its investment strategy.",
|
| 490 |
+
)
|
| 491 |
+
holds_preferred_stock: Optional[bool] = Field(
|
| 492 |
+
default=None,
|
| 493 |
+
description="If true, the ETF's investment objective explicitly specifies"
|
| 494 |
+
+ " that it holds preferred stock as an intended part of its investment strategy.",
|
| 495 |
+
)
|
| 496 |
+
holds_closed_end_funds: Optional[bool] = Field(
|
| 497 |
+
default=None,
|
| 498 |
+
description="If true, the ETF's investment objective explicitly specifies"
|
| 499 |
+
+ " that it holds closed end funds as an intended part of its investment strategy.",
|
| 500 |
+
)
|
| 501 |
+
holds_adr: Optional[bool] = Field(
|
| 502 |
+
default=None,
|
| 503 |
+
description="If true, he ETF's investment objective explicitly specifies that it holds"
|
| 504 |
+
+ " American Depositary Receipts (ADRs) as an intended part of its investment strategy.",
|
| 505 |
+
)
|
| 506 |
+
laddered: Optional[bool] = Field(
|
| 507 |
+
default=None,
|
| 508 |
+
description="For bond ETFs, this field identifies those ETFs that"
|
| 509 |
+
+ " specifically hold bonds in a laddered structure,"
|
| 510 |
+
+ " where the bonds are scheduled to mature in an annual, sequential structure.",
|
| 511 |
+
)
|
| 512 |
+
zero_coupon: Optional[bool] = Field(
|
| 513 |
+
default=None,
|
| 514 |
+
description="For bond ETFs, this field identifies those ETFs that specifically hold zero coupon Treasury Bills.",
|
| 515 |
+
)
|
| 516 |
+
floating_rate: Optional[bool] = Field(
|
| 517 |
+
default=None,
|
| 518 |
+
description="For bond ETFs, this field identifies those ETFs that specifically hold floating rate bonds.",
|
| 519 |
+
)
|
| 520 |
+
build_america_bonds: Optional[bool] = Field(
|
| 521 |
+
default=None,
|
| 522 |
+
description="For municipal bond ETFs, this field identifies those"
|
| 523 |
+
+ " ETFs that specifically hold Build America Bonds.",
|
| 524 |
+
)
|
| 525 |
+
dynamic_futures_roll: Optional[bool] = Field(
|
| 526 |
+
default=None,
|
| 527 |
+
description="If the product holds futures contracts, this field identifies those products where the roll strategy"
|
| 528 |
+
+ " is dynamic (rather than entirely rules based), so as to minimize roll costs.",
|
| 529 |
+
)
|
| 530 |
+
currency_hedged: Optional[bool] = Field(
|
| 531 |
+
default=None,
|
| 532 |
+
description="This field is populated if the ETF's strategy involves hedging currency exposure.",
|
| 533 |
+
)
|
| 534 |
+
includes_short_exposure: Optional[bool] = Field(
|
| 535 |
+
default=None,
|
| 536 |
+
description="This field is populated if the ETF has short exposure"
|
| 537 |
+
+ " in any of its holdings e.g. in a long/short or inverse ETF.",
|
| 538 |
+
)
|
| 539 |
+
ucits: Optional[bool] = Field(
|
| 540 |
+
default=None,
|
| 541 |
+
description="If true, the Exchange Traded Product (ETP) is Undertakings for the Collective Investment"
|
| 542 |
+
+ " in Transferable Securities (UCITS) compliant",
|
| 543 |
+
)
|
| 544 |
+
registered_countries: Optional[str] = Field(
|
| 545 |
+
default=None,
|
| 546 |
+
description="The list of countries where the ETF is legally registered for sale."
|
| 547 |
+
+ " This may differ from where the ETF is domiciled or traded, particularly in Europe.",
|
| 548 |
+
)
|
| 549 |
+
issuer_country: Optional[str] = Field(
|
| 550 |
+
default=None,
|
| 551 |
+
description="2 letter ISO country code for the country where the issuer is located.",
|
| 552 |
+
)
|
| 553 |
+
domicile: Optional[str] = Field(
|
| 554 |
+
default=None,
|
| 555 |
+
description="2 letter ISO country code for the country where the ETP is domiciled.",
|
| 556 |
+
)
|
| 557 |
+
listing_country: Optional[str] = Field(
|
| 558 |
+
default=None,
|
| 559 |
+
description="2 letter ISO country code for the country of the primary listing.",
|
| 560 |
+
alias="listing_country_code",
|
| 561 |
+
)
|
| 562 |
+
listing_region: Optional[str] = Field(
|
| 563 |
+
default=None,
|
| 564 |
+
description="Geographic region in the country of the primary listing falls.",
|
| 565 |
+
)
|
| 566 |
+
bond_currency_denomination: Optional[str] = Field(
|
| 567 |
+
default=None,
|
| 568 |
+
description="For all bond ETFs, this field provides additional"
|
| 569 |
+
+ " detail on the currency denomination of the underlying securities.",
|
| 570 |
+
)
|
| 571 |
+
base_currency: Optional[str] = Field(
|
| 572 |
+
default=None,
|
| 573 |
+
description="Base currency in which NAV is reported.",
|
| 574 |
+
)
|
| 575 |
+
listing_currency: Optional[str] = Field(
|
| 576 |
+
default=None,
|
| 577 |
+
description="Listing currency of the Exchange Traded Product (ETP) in which it is traded."
|
| 578 |
+
+ " Reported using the 3-digit ISO currency code.",
|
| 579 |
+
)
|
| 580 |
+
number_of_holdings: Optional[int] = Field(
|
| 581 |
+
default=None,
|
| 582 |
+
description="The number of holdings in the ETF.",
|
| 583 |
+
)
|
| 584 |
+
month_end_assets: Optional[float] = Field(
|
| 585 |
+
default=None,
|
| 586 |
+
description="Net assets in millions of dollars as of the most recent month end.",
|
| 587 |
+
)
|
| 588 |
+
net_expense_ratio: Optional[float] = Field(
|
| 589 |
+
default=None,
|
| 590 |
+
description="Gross expense net of Fee Waivers, as a percentage of net assets"
|
| 591 |
+
+ " as published by the ETF issuer.",
|
| 592 |
+
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
|
| 593 |
+
)
|
| 594 |
+
etf_portfolio_turnover: Optional[float] = Field(
|
| 595 |
+
default=None,
|
| 596 |
+
description="The percentage of positions turned over in the last 12 months.",
|
| 597 |
+
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
|
| 598 |
+
)
|
| 599 |
+
|
| 600 |
+
|
| 601 |
+
class IntrinioEtfInfoFetcher(
|
| 602 |
+
Fetcher[IntrinioEtfInfoQueryParams, List[IntrinioEtfInfoData]]
|
| 603 |
+
):
|
| 604 |
+
"""Intrinio ETF Info Fetcher."""
|
| 605 |
+
|
| 606 |
+
@staticmethod
|
| 607 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioEtfInfoQueryParams:
|
| 608 |
+
"""Transform query."""
|
| 609 |
+
return IntrinioEtfInfoQueryParams(**params)
|
| 610 |
+
|
| 611 |
+
@staticmethod
|
| 612 |
+
async def aextract_data(
|
| 613 |
+
query: IntrinioEtfInfoQueryParams,
|
| 614 |
+
credentials: Optional[Dict[str, str]],
|
| 615 |
+
**kwargs: Any,
|
| 616 |
+
) -> List[Dict]:
|
| 617 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 618 |
+
|
| 619 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 620 |
+
base_url = "https://api-v2.intrinio.com/etfs/"
|
| 621 |
+
symbols = query.symbol.split(",")
|
| 622 |
+
symbols = [
|
| 623 |
+
symbol + ":US" if ":" not in symbol else symbol for symbol in symbols
|
| 624 |
+
]
|
| 625 |
+
urls = [f"{base_url}{symbol}?api_key={api_key}" for symbol in symbols]
|
| 626 |
+
|
| 627 |
+
results = []
|
| 628 |
+
|
| 629 |
+
async def response_callback(response, _):
|
| 630 |
+
"""Response callback."""
|
| 631 |
+
result = await response.json()
|
| 632 |
+
if "error" in result:
|
| 633 |
+
warn(f"Symbol Error: {result['error']} for {response.url.parts[-1]}")
|
| 634 |
+
return
|
| 635 |
+
_ = result.pop("messages", None)
|
| 636 |
+
results.append(result)
|
| 637 |
+
|
| 638 |
+
await amake_requests(urls, response_callback, **kwargs) # type: ignore
|
| 639 |
+
|
| 640 |
+
if not results:
|
| 641 |
+
raise EmptyDataError("No data was returned.")
|
| 642 |
+
|
| 643 |
+
return sorted(
|
| 644 |
+
results,
|
| 645 |
+
key=(lambda item: (symbols.index(item.get("figi_ticker", len(symbols))))),
|
| 646 |
+
)
|
| 647 |
+
|
| 648 |
+
@staticmethod
|
| 649 |
+
def transform_data(
|
| 650 |
+
query: IntrinioEtfInfoQueryParams,
|
| 651 |
+
data: List[Dict],
|
| 652 |
+
**kwargs: Any,
|
| 653 |
+
) -> List[IntrinioEtfInfoData]:
|
| 654 |
+
"""Transform data."""
|
| 655 |
+
return [IntrinioEtfInfoData.model_validate(d) for d in data]
|
openbb_platform/providers/intrinio/openbb_intrinio/models/etf_price_performance.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio ETF Performance Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
import asyncio
|
| 6 |
+
from datetime import date as dateType
|
| 7 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 8 |
+
from warnings import warn
|
| 9 |
+
|
| 10 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 11 |
+
from openbb_core.provider.standard_models.recent_performance import (
|
| 12 |
+
RecentPerformanceData,
|
| 13 |
+
RecentPerformanceQueryParams,
|
| 14 |
+
)
|
| 15 |
+
from openbb_core.provider.utils.descriptions import DATA_DESCRIPTIONS
|
| 16 |
+
from openbb_core.provider.utils.errors import EmptyDataError
|
| 17 |
+
from openbb_core.provider.utils.helpers import amake_request
|
| 18 |
+
from openbb_intrinio.utils.references import ETF_PERFORMANCE_MAP
|
| 19 |
+
from pydantic import Field
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class IntrinioEtfPricePerformanceQueryParams(RecentPerformanceQueryParams):
|
| 23 |
+
"""
|
| 24 |
+
Intrinio ETF Performance Query Params.
|
| 25 |
+
|
| 26 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_etf_stats_v2
|
| 27 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_etf_analytics_v2
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
__json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
|
| 31 |
+
|
| 32 |
+
return_type: Literal["trailing", "calendar"] = Field(
|
| 33 |
+
default="trailing",
|
| 34 |
+
description="The type of returns to return, a trailing or calendar window.",
|
| 35 |
+
)
|
| 36 |
+
adjustment: Literal["splits_only", "splits_and_dividends"] = Field(
|
| 37 |
+
default="splits_and_dividends",
|
| 38 |
+
description="The adjustment factor, 'splits_only' will return pure price performance.",
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class IntrinioEtfPricePerformanceData(RecentPerformanceData):
|
| 43 |
+
"""Intrinio ETF Performance Data."""
|
| 44 |
+
|
| 45 |
+
__alias_dict__ = {
|
| 46 |
+
"updated": "date",
|
| 47 |
+
"year_high": "fifty_two_week_high",
|
| 48 |
+
"year_low": "fifty_two_week_low",
|
| 49 |
+
"volume": "volume_traded",
|
| 50 |
+
"volume_avg_30": "average_daily_volume_one_month",
|
| 51 |
+
"volume_avg_90": "average_daily_volume_three_month",
|
| 52 |
+
"volume_avg_180": "average_daily_volume_six_month",
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
max_annualized: Optional[float] = Field(
|
| 56 |
+
default=None,
|
| 57 |
+
description="Annualized rate of return from inception.",
|
| 58 |
+
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
|
| 59 |
+
)
|
| 60 |
+
volatility_one_year: Optional[float] = Field(
|
| 61 |
+
default=None,
|
| 62 |
+
description="Trailing one-year annualized volatility.",
|
| 63 |
+
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
|
| 64 |
+
)
|
| 65 |
+
volatility_three_year: Optional[float] = Field(
|
| 66 |
+
default=None,
|
| 67 |
+
description="Trailing three-year annualized volatility.",
|
| 68 |
+
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
|
| 69 |
+
)
|
| 70 |
+
volatility_five_year: Optional[float] = Field(
|
| 71 |
+
default=None,
|
| 72 |
+
description="Trailing five-year annualized volatility.",
|
| 73 |
+
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
|
| 74 |
+
)
|
| 75 |
+
volume: Optional[int] = Field(
|
| 76 |
+
default=None,
|
| 77 |
+
description=DATA_DESCRIPTIONS.get("volume", ""),
|
| 78 |
+
)
|
| 79 |
+
volume_avg_30: Optional[float] = Field(
|
| 80 |
+
default=None,
|
| 81 |
+
description="The one-month average daily volume.",
|
| 82 |
+
)
|
| 83 |
+
volume_avg_90: Optional[float] = Field(
|
| 84 |
+
default=None,
|
| 85 |
+
description="The three-month average daily volume.",
|
| 86 |
+
)
|
| 87 |
+
volume_avg_180: Optional[float] = Field(
|
| 88 |
+
default=None,
|
| 89 |
+
description="The six-month average daily volume.",
|
| 90 |
+
)
|
| 91 |
+
beta: Optional[float] = Field(
|
| 92 |
+
default=None,
|
| 93 |
+
description="Beta compared to the S&P 500.",
|
| 94 |
+
)
|
| 95 |
+
nav: Optional[float] = Field(
|
| 96 |
+
default=None,
|
| 97 |
+
description="Net asset value per share.",
|
| 98 |
+
)
|
| 99 |
+
year_high: Optional[float] = Field(
|
| 100 |
+
default=None,
|
| 101 |
+
description="The 52-week high price.",
|
| 102 |
+
)
|
| 103 |
+
year_low: Optional[float] = Field(
|
| 104 |
+
default=None,
|
| 105 |
+
description="The 52-week low price.",
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
market_cap: Optional[float] = Field(
|
| 109 |
+
default=None,
|
| 110 |
+
description="The market capitalization.",
|
| 111 |
+
)
|
| 112 |
+
shares_outstanding: Optional[int] = Field(
|
| 113 |
+
default=None,
|
| 114 |
+
description="The number of shares outstanding.",
|
| 115 |
+
)
|
| 116 |
+
updated: Optional[dateType] = Field(
|
| 117 |
+
default=None,
|
| 118 |
+
description=DATA_DESCRIPTIONS.get("date", ""),
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class IntrinioEtfPricePerformanceFetcher(
|
| 123 |
+
Fetcher[
|
| 124 |
+
IntrinioEtfPricePerformanceQueryParams, List[IntrinioEtfPricePerformanceData]
|
| 125 |
+
]
|
| 126 |
+
):
|
| 127 |
+
"""Intrinio ETF Performance Fetcher."""
|
| 128 |
+
|
| 129 |
+
@staticmethod
|
| 130 |
+
def transform_query(
|
| 131 |
+
params: Dict[str, Any]
|
| 132 |
+
) -> IntrinioEtfPricePerformanceQueryParams:
|
| 133 |
+
"""Transform query."""
|
| 134 |
+
return IntrinioEtfPricePerformanceQueryParams(**params)
|
| 135 |
+
|
| 136 |
+
@staticmethod
|
| 137 |
+
async def aextract_data(
|
| 138 |
+
query: IntrinioEtfPricePerformanceQueryParams,
|
| 139 |
+
credentials: Optional[Dict[str, str]],
|
| 140 |
+
**kwargs: Any,
|
| 141 |
+
) -> List[Dict]:
|
| 142 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 143 |
+
|
| 144 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 145 |
+
base_url = "https://api-v2.intrinio.com/etfs/"
|
| 146 |
+
symbols = query.symbol.split(",")
|
| 147 |
+
symbols = [
|
| 148 |
+
symbol + ":US" if ":" not in symbol else symbol for symbol in symbols
|
| 149 |
+
]
|
| 150 |
+
|
| 151 |
+
adjustment = (
|
| 152 |
+
"split_only"
|
| 153 |
+
if query.adjustment == "splits_and_dividends"
|
| 154 |
+
else "split_and_dividend"
|
| 155 |
+
)
|
| 156 |
+
|
| 157 |
+
return_type = "trailing" if query.return_type == "calendar" else "calendar"
|
| 158 |
+
|
| 159 |
+
results = []
|
| 160 |
+
|
| 161 |
+
async def get_one(symbol: str, **kwargs):
|
| 162 |
+
"""Get data for one symbol."""
|
| 163 |
+
|
| 164 |
+
url = f"{base_url}{symbol}/stats?api_key={api_key}"
|
| 165 |
+
result = await amake_request(url, **kwargs)
|
| 166 |
+
|
| 167 |
+
if "message" in result and result["message"] != []: # type: ignore
|
| 168 |
+
warn(f"Symbol Error: {symbol} - {result['message']}") # type: ignore
|
| 169 |
+
return
|
| 170 |
+
_ = result.pop("message", None) # type: ignore
|
| 171 |
+
_ = result.pop("messages", None) # type: ignore
|
| 172 |
+
|
| 173 |
+
data = {}
|
| 174 |
+
etf = result.pop("etf", {}) # type: ignore
|
| 175 |
+
data["symbol"] = etf.get("ticker")
|
| 176 |
+
# These items will be kept regardless of the adjustment and return_type.
|
| 177 |
+
keep = ["volatility", "month", "year_to_date"]
|
| 178 |
+
for k, v in result.copy().items(): # type: ignore
|
| 179 |
+
if not any(substring in k for substring in keep):
|
| 180 |
+
_ = result.pop(k, None) if adjustment in k else None # type: ignore
|
| 181 |
+
_ = result.pop(k, None) if return_type in k else None # type: ignore
|
| 182 |
+
if k in result:
|
| 183 |
+
data[ETF_PERFORMANCE_MAP.get(k, k)] = v
|
| 184 |
+
# Get an additional set of data to combine with the first set.
|
| 185 |
+
analytics_url = (
|
| 186 |
+
f"https://api-v2.intrinio.com/etfs/{symbol}/analytics?api_key={api_key}"
|
| 187 |
+
)
|
| 188 |
+
if data:
|
| 189 |
+
analytics = await amake_request(analytics_url, **kwargs)
|
| 190 |
+
if "messages" in analytics and analytics["messages"] != []: # type: ignore
|
| 191 |
+
warn(
|
| 192 |
+
f"Symbol Error: {analytics['messages']}" # type: ignore
|
| 193 |
+
+ f"for {etf.get('ticker')}" # type: ignore
|
| 194 |
+
)
|
| 195 |
+
return
|
| 196 |
+
# Remove the duplicate data from the analytics response.
|
| 197 |
+
_ = analytics.pop("messages", None) # type: ignore
|
| 198 |
+
_ = analytics.pop("etf", None) # type: ignore
|
| 199 |
+
_ = analytics.pop("date", None) # type: ignore
|
| 200 |
+
|
| 201 |
+
data.update(analytics) # type: ignore
|
| 202 |
+
|
| 203 |
+
results.append(data)
|
| 204 |
+
|
| 205 |
+
tasks = [get_one(symbol, **kwargs) for symbol in symbols]
|
| 206 |
+
|
| 207 |
+
await asyncio.gather(*tasks)
|
| 208 |
+
|
| 209 |
+
if not results:
|
| 210 |
+
raise EmptyDataError("No data was returned.")
|
| 211 |
+
|
| 212 |
+
# Undo any formatting changes made to the symbols before sorting.
|
| 213 |
+
symbols = query.symbol.replace(":US", "").split(",")
|
| 214 |
+
|
| 215 |
+
return sorted(
|
| 216 |
+
results,
|
| 217 |
+
key=(lambda item: (symbols.index(item.get("symbol", len(symbols))))),
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
@staticmethod
|
| 221 |
+
def transform_data(
|
| 222 |
+
query: IntrinioEtfPricePerformanceQueryParams,
|
| 223 |
+
data: List[Dict],
|
| 224 |
+
**kwargs: Any,
|
| 225 |
+
) -> List[IntrinioEtfPricePerformanceData]:
|
| 226 |
+
"""Transform data."""
|
| 227 |
+
return [IntrinioEtfPricePerformanceData.model_validate(d) for d in data]
|
openbb_platform/providers/intrinio/openbb_intrinio/models/etf_search.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio ETF Search Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
from typing import Any, Dict, List, Optional, Union
|
| 6 |
+
|
| 7 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 8 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 9 |
+
from openbb_core.provider.standard_models.etf_search import (
|
| 10 |
+
EtfSearchData,
|
| 11 |
+
EtfSearchQueryParams,
|
| 12 |
+
)
|
| 13 |
+
from openbb_core.provider.utils.errors import EmptyDataError
|
| 14 |
+
from openbb_intrinio.utils.references import ETF_EXCHANGES
|
| 15 |
+
from pydantic import Field
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class IntrinioEtfSearchQueryParams(EtfSearchQueryParams):
|
| 19 |
+
"""
|
| 20 |
+
Intrinio ETF Search Query Params.
|
| 21 |
+
|
| 22 |
+
Source: https://docs.intrinio.com/documentation/web_api/search_etfs_v2
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
exchange: Union[None, ETF_EXCHANGES] = Field(
|
| 26 |
+
default=None,
|
| 27 |
+
description="Target a specific exchange by providing the MIC code.",
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class IntrinioEtfSearchData(EtfSearchData):
|
| 32 |
+
"""Intrinio ETF Search Data."""
|
| 33 |
+
|
| 34 |
+
__alias_dict__ = {
|
| 35 |
+
"intrinio_id": "id",
|
| 36 |
+
"symbol": "ticker",
|
| 37 |
+
"exchange": "exchange_mic",
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
exchange: Optional[str] = Field(
|
| 41 |
+
default=None,
|
| 42 |
+
description="The exchange MIC code.",
|
| 43 |
+
)
|
| 44 |
+
figi_ticker: Optional[str] = Field(
|
| 45 |
+
None,
|
| 46 |
+
description="The OpenFIGI ticker.",
|
| 47 |
+
)
|
| 48 |
+
ric: Optional[str] = Field(
|
| 49 |
+
None,
|
| 50 |
+
description="The Reuters Instrument Code.",
|
| 51 |
+
)
|
| 52 |
+
isin: Optional[str] = Field(
|
| 53 |
+
None,
|
| 54 |
+
description="The International Securities Identification Number.",
|
| 55 |
+
)
|
| 56 |
+
sedol: Optional[str] = Field(
|
| 57 |
+
None,
|
| 58 |
+
description="The Stock Exchange Daily Official List.",
|
| 59 |
+
)
|
| 60 |
+
intrinio_id: Optional[str] = Field(
|
| 61 |
+
None,
|
| 62 |
+
description="The unique Intrinio ID for the security.",
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class IntrinioEtfSearchFetcher(
|
| 67 |
+
Fetcher[IntrinioEtfSearchQueryParams, List[IntrinioEtfSearchData]]
|
| 68 |
+
):
|
| 69 |
+
"""Intrinio ETF Search Fetcher."""
|
| 70 |
+
|
| 71 |
+
@staticmethod
|
| 72 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioEtfSearchQueryParams:
|
| 73 |
+
"""Transform query."""
|
| 74 |
+
return IntrinioEtfSearchQueryParams(**params)
|
| 75 |
+
|
| 76 |
+
@staticmethod
|
| 77 |
+
async def aextract_data(
|
| 78 |
+
query: IntrinioEtfSearchQueryParams,
|
| 79 |
+
credentials: Optional[Dict[str, str]],
|
| 80 |
+
**kwargs: Any,
|
| 81 |
+
) -> List[Dict]:
|
| 82 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 83 |
+
# pylint: disable=import-outside-toplevel
|
| 84 |
+
from openbb_core.provider.utils.helpers import (
|
| 85 |
+
ClientResponse,
|
| 86 |
+
ClientSession,
|
| 87 |
+
amake_request,
|
| 88 |
+
)
|
| 89 |
+
|
| 90 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 91 |
+
BASE = "https://api-v2.intrinio.com/etfs"
|
| 92 |
+
if query.exchange is not None:
|
| 93 |
+
url = f"{BASE}?exchange={query.exchange.upper()}&page_size=10000&api_key={api_key}"
|
| 94 |
+
elif query.query:
|
| 95 |
+
url = f"{BASE}/search?query={query.query}&page_size=10000&api_key={api_key}"
|
| 96 |
+
else:
|
| 97 |
+
url = f"{BASE}?page_size=10000&api_key={api_key}"
|
| 98 |
+
|
| 99 |
+
data: List = []
|
| 100 |
+
|
| 101 |
+
async def response_callback(response: ClientResponse, session: ClientSession):
|
| 102 |
+
"""Async response callback."""
|
| 103 |
+
results = await response.json()
|
| 104 |
+
|
| 105 |
+
if results.get("messages"): # type: ignore
|
| 106 |
+
messages = results.get("messages") # type: ignore
|
| 107 |
+
raise OpenBBError(str(messages))
|
| 108 |
+
|
| 109 |
+
if results.get("etfs") and len(results.get("etfs")) > 0: # type: ignore
|
| 110 |
+
data.extend(results.get("etfs")) # type: ignore
|
| 111 |
+
while results.get("next_page"): # type: ignore
|
| 112 |
+
next_page = results["next_page"] # type: ignore
|
| 113 |
+
next_url = f"{url}&next_page={next_page}"
|
| 114 |
+
results = await amake_request(next_url, session=session, **kwargs)
|
| 115 |
+
if (
|
| 116 |
+
"etfs" in results
|
| 117 |
+
and len(results.get("etfs")) > 0 # type: ignore
|
| 118 |
+
):
|
| 119 |
+
data.extend(results.get("etfs")) # type: ignore
|
| 120 |
+
return data
|
| 121 |
+
|
| 122 |
+
return await amake_request(url, response_callback=response_callback, **kwargs) # type: ignore
|
| 123 |
+
|
| 124 |
+
@staticmethod
|
| 125 |
+
def transform_data(
|
| 126 |
+
query: IntrinioEtfSearchQueryParams,
|
| 127 |
+
data: List[Dict],
|
| 128 |
+
**kwargs: Any,
|
| 129 |
+
) -> List[IntrinioEtfSearchData]:
|
| 130 |
+
"""Transform data."""
|
| 131 |
+
# pylint: disable=import-outside-toplevel
|
| 132 |
+
import re # noqa
|
| 133 |
+
from pandas import DataFrame # noqa
|
| 134 |
+
|
| 135 |
+
if not data:
|
| 136 |
+
raise EmptyDataError("No data found.")
|
| 137 |
+
|
| 138 |
+
results = DataFrame(data)
|
| 139 |
+
if query.query:
|
| 140 |
+
pattern = f".*{re.escape(query.query)}.*"
|
| 141 |
+
results = results[
|
| 142 |
+
results["name"].str.contains(pattern, case=False, regex=True)
|
| 143 |
+
]
|
| 144 |
+
|
| 145 |
+
return [
|
| 146 |
+
IntrinioEtfSearchData.model_validate(d)
|
| 147 |
+
for d in results.to_dict(orient="records")
|
| 148 |
+
]
|
openbb_platform/providers/intrinio/openbb_intrinio/models/financial_attributes.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Financial Attributes Model."""
|
| 2 |
+
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from typing import Any, Dict, List, Optional
|
| 5 |
+
|
| 6 |
+
from dateutil.relativedelta import relativedelta
|
| 7 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 8 |
+
from openbb_core.provider.standard_models.financial_attributes import (
|
| 9 |
+
FinancialAttributesData,
|
| 10 |
+
FinancialAttributesQueryParams,
|
| 11 |
+
)
|
| 12 |
+
from openbb_core.provider.utils.helpers import get_querystring
|
| 13 |
+
from openbb_intrinio.utils.helpers import get_data_many
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class IntrinioFinancialAttributesQueryParams(FinancialAttributesQueryParams):
|
| 17 |
+
"""Intrinio Financial Attributes Query."""
|
| 18 |
+
|
| 19 |
+
__alias_dict__ = {"sort": "sort_order", "limit": "page_size"}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class IntrinioFinancialAttributesData(FinancialAttributesData):
|
| 23 |
+
"""Intrinio Financial Attributes Data."""
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class IntrinioFinancialAttributesFetcher(
|
| 27 |
+
Fetcher[
|
| 28 |
+
IntrinioFinancialAttributesQueryParams,
|
| 29 |
+
List[IntrinioFinancialAttributesData],
|
| 30 |
+
]
|
| 31 |
+
):
|
| 32 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 33 |
+
|
| 34 |
+
@staticmethod
|
| 35 |
+
def transform_query(
|
| 36 |
+
params: Dict[str, Any]
|
| 37 |
+
) -> IntrinioFinancialAttributesQueryParams:
|
| 38 |
+
"""Transform the query params."""
|
| 39 |
+
transformed_params = params
|
| 40 |
+
|
| 41 |
+
now = datetime.now().date()
|
| 42 |
+
if params.get("start_date") is None:
|
| 43 |
+
transformed_params["start_date"] = now - relativedelta(years=5)
|
| 44 |
+
|
| 45 |
+
if params.get("end_date") is None:
|
| 46 |
+
transformed_params["end_date"] = now
|
| 47 |
+
|
| 48 |
+
return IntrinioFinancialAttributesQueryParams(**transformed_params)
|
| 49 |
+
|
| 50 |
+
@staticmethod
|
| 51 |
+
async def aextract_data(
|
| 52 |
+
query: IntrinioFinancialAttributesQueryParams, # pylint: disable=unused-argument
|
| 53 |
+
credentials: Optional[Dict[str, str]],
|
| 54 |
+
**kwargs: Any,
|
| 55 |
+
) -> List[Dict]:
|
| 56 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 57 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 58 |
+
frequency = "yearly" if query.period == "annual" else "quarterly"
|
| 59 |
+
data: List[Dict] = []
|
| 60 |
+
|
| 61 |
+
base_url = "https://api-v2.intrinio.com"
|
| 62 |
+
query_str = get_querystring(query.model_dump(by_alias=True), ["frequency"])
|
| 63 |
+
query_str = f"{query_str}&frequency={frequency}"
|
| 64 |
+
|
| 65 |
+
url = f"{base_url}/historical_data/{query.symbol}/{query.tag}?{query_str}&api_key={api_key}"
|
| 66 |
+
# data = get_data_one(url).get("historical_data", [])
|
| 67 |
+
data = await get_data_many(url, "historical_data")
|
| 68 |
+
|
| 69 |
+
return data
|
| 70 |
+
|
| 71 |
+
@staticmethod
|
| 72 |
+
def transform_data(
|
| 73 |
+
query: IntrinioFinancialAttributesQueryParams,
|
| 74 |
+
data: List[Dict],
|
| 75 |
+
**kwargs: Any,
|
| 76 |
+
) -> List[IntrinioFinancialAttributesData]:
|
| 77 |
+
"""Return the transformed data."""
|
| 78 |
+
return [IntrinioFinancialAttributesData.model_validate(item) for item in data]
|
openbb_platform/providers/intrinio/openbb_intrinio/models/financial_ratios.py
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Financial Ratios Model."""
|
| 2 |
+
|
| 3 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 4 |
+
from warnings import warn
|
| 5 |
+
|
| 6 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 7 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 8 |
+
from openbb_core.provider.standard_models.financial_ratios import (
|
| 9 |
+
FinancialRatiosData,
|
| 10 |
+
FinancialRatiosQueryParams,
|
| 11 |
+
)
|
| 12 |
+
from openbb_core.provider.utils.descriptions import QUERY_DESCRIPTIONS
|
| 13 |
+
from openbb_core.provider.utils.helpers import ClientResponse, amake_requests
|
| 14 |
+
from openbb_intrinio.utils.helpers import get_data_one
|
| 15 |
+
from pydantic import Field, field_validator
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class IntrinioFinancialRatiosQueryParams(FinancialRatiosQueryParams):
|
| 19 |
+
"""Intrinio Financial Ratios Query.
|
| 20 |
+
|
| 21 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_company_fundamentals_v2
|
| 22 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_fundamental_standardized_financials_v2
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
__json_schema_extra__ = {
|
| 26 |
+
"period": {
|
| 27 |
+
"choices": ["annual", "quarter", "ttm", "ytd"],
|
| 28 |
+
}
|
| 29 |
+
}
|
| 30 |
+
period: Literal["annual", "quarter", "ttm", "ytd"] = Field(
|
| 31 |
+
default="annual",
|
| 32 |
+
description=QUERY_DESCRIPTIONS.get("period", ""),
|
| 33 |
+
)
|
| 34 |
+
fiscal_year: Optional[int] = Field(
|
| 35 |
+
default=None,
|
| 36 |
+
description="The specific fiscal year. Reports do not go beyond 2008.",
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
@field_validator("symbol", mode="after", check_fields=False)
|
| 40 |
+
@classmethod
|
| 41 |
+
def handle_symbol(cls, v) -> str:
|
| 42 |
+
"""Handle symbols with a dash and replace it with a dot for Intrinio."""
|
| 43 |
+
return v.replace("-", ".")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class IntrinioFinancialRatiosData(FinancialRatiosData):
|
| 47 |
+
"""Intrinio Financial Ratios Data."""
|
| 48 |
+
|
| 49 |
+
__alias_dict__ = {
|
| 50 |
+
"net_operating_profit_after_tax_margin": "nopatmargin",
|
| 51 |
+
"invested_capital_turnover": "investedcapitalturnover",
|
| 52 |
+
"book_value_per_share": "bookvaluepershare",
|
| 53 |
+
"tangible_book_value": "tangiblebookvaluepershare",
|
| 54 |
+
"price_to_book_ratio": "pricetobook",
|
| 55 |
+
"price_to_tangible_book_ratio": "pricetotangiblebook",
|
| 56 |
+
"price_to_revenue": "pricetorevenue",
|
| 57 |
+
"price_to_earnings": "pricetoearnings",
|
| 58 |
+
"dividend_yield": "dividendyield",
|
| 59 |
+
"earnings_yield": "earningsyield",
|
| 60 |
+
"ev_to_invested_capital": "evtoinvestedcapital",
|
| 61 |
+
"ev_to_sales": "evtorevenue",
|
| 62 |
+
"ev_to_ebitda": "evtoebitda",
|
| 63 |
+
"ev_to_ebit": "evtoebit",
|
| 64 |
+
"ev_to_nopat": "evtonopat",
|
| 65 |
+
"ev_to_operating_cash_flow": "evtoocf",
|
| 66 |
+
"ev_to_free_cash_flow": "evtofcff",
|
| 67 |
+
"gross_margin": "grossmargin",
|
| 68 |
+
"ebitda_margin": "ebitdamargin",
|
| 69 |
+
"operating_margin": "operatingmargin",
|
| 70 |
+
"ebit_margin": "ebitmargin",
|
| 71 |
+
"net_profit_margin": "profitmargin",
|
| 72 |
+
"cost_of_rev_to_revenue": "costofrevtorevenue",
|
| 73 |
+
"sga_expense_to_revenue": "sgaextorevenue",
|
| 74 |
+
"rd_expense_to_revenue": "rdextorevenue",
|
| 75 |
+
"op_expense_to_revenue": "opextorevenue",
|
| 76 |
+
"tax_burden_percent": "taxburdenpct",
|
| 77 |
+
"interest_burden_percent": "interestburdenpct",
|
| 78 |
+
"effective_tax_rate": "efftaxrate",
|
| 79 |
+
"asset_turnover": "assetturnover",
|
| 80 |
+
"receivables_turnover": "arturnover",
|
| 81 |
+
"inventory_turnover": "invturnover",
|
| 82 |
+
"fixed_asset_turnover": "faturnover",
|
| 83 |
+
"payables_turnover": "apturnover",
|
| 84 |
+
"days_of_sales_outstanding": "dso",
|
| 85 |
+
"days_of_inventory_outstanding": "dio",
|
| 86 |
+
"days_payable_outstanding": "dpo",
|
| 87 |
+
"cash_conversion_cycle": "ccc",
|
| 88 |
+
"financial_leverage": "finleverage",
|
| 89 |
+
"leverage_ratio": "leverageratio",
|
| 90 |
+
"compound_leverage_factor": "compoundleveragefactor",
|
| 91 |
+
"long_term_debt_equity_ratio": "ltdebttoequity",
|
| 92 |
+
"debt_equity_ratio": "debttoequity",
|
| 93 |
+
"return_on_invested_capital": "roic",
|
| 94 |
+
"net_non_operating_expense_percent": "nnep",
|
| 95 |
+
"roic_nnep_spread": "roicnnepspread",
|
| 96 |
+
"return_on_net_non_operating_assets": "rnnoa",
|
| 97 |
+
"return_on_equity": "roe",
|
| 98 |
+
"cash_returned_on_invested_capitals": "croic",
|
| 99 |
+
"operating_return_on_assets": "oroa",
|
| 100 |
+
"return_on_assets": "roa",
|
| 101 |
+
"non_controlling_interest_sharing_ratio": "noncontrollinginterestsharingratio",
|
| 102 |
+
"return_on_common_equity": "roce",
|
| 103 |
+
"dividend_payout_ratio": "divpayoutratio",
|
| 104 |
+
"augmented_payout_ratio": "augmentedpayoutratio",
|
| 105 |
+
"operating_cash_flow_to_capex": "ocftocapex",
|
| 106 |
+
"short_term_debt_to_capitalization": "stdebttocap",
|
| 107 |
+
"long_term_debt_to_capitalization": "ltdebttocap",
|
| 108 |
+
"debt_to_capitalization": "debttototalcapital",
|
| 109 |
+
"preferred_equity_to_capitalization": "preferredtocap",
|
| 110 |
+
"non_controlling_interests_to_capitalization": "noncontrolinttocap",
|
| 111 |
+
"equity_to_capitalization": "commontocap",
|
| 112 |
+
"debt_to_ebitda": "debttoebitda",
|
| 113 |
+
"net_debt_to_ebitda": "netdebttoebitda",
|
| 114 |
+
"long_term_debt_to_ebita": "ltdebttoebitda",
|
| 115 |
+
"debt_to_nopat": "debttonopat",
|
| 116 |
+
"net_debt_to_nopat": "netdebttonopat",
|
| 117 |
+
"long_term_debt_to_nopat": "ltdebttonopat",
|
| 118 |
+
"altman_z_score": "altmanzscore",
|
| 119 |
+
"ebit_to_interest_expense": "ebittointerestex",
|
| 120 |
+
"nopat_to_intersest_expense": "nopattointerestex",
|
| 121 |
+
"ebit_less_capex_to_interest_expense": "ebitlesscapextointerestex",
|
| 122 |
+
"nopat_less_capex_to_interest_expense": "nopatlesscapextointex",
|
| 123 |
+
"operating_cash_flow_to_interest_expense": "ocftointerestex",
|
| 124 |
+
"operating_cash_flow_less_capex_to_interest_expense": "ocflesscapextointerestex",
|
| 125 |
+
"free_cash_flow_to_interest_expense": "fcfftointerestex",
|
| 126 |
+
"current_ratio": "curratio",
|
| 127 |
+
"quick_ratio": "quickratio",
|
| 128 |
+
"debt_free_cash_free_nwc_to_revenue": "dfcfnwctorev",
|
| 129 |
+
"debt_free_nwc_to_revenue": "dfnwctorev",
|
| 130 |
+
"net_working_capital_to_revenue": "nwctorev",
|
| 131 |
+
"normalized_nopat_margin": "normalizednopatmargin",
|
| 132 |
+
"pre_tax_income_margin": "pretaxincomemargin",
|
| 133 |
+
"adjusted_basic_eps": "adjbasiceps",
|
| 134 |
+
"adjusted_diluted_eps": "adjdilutedeps",
|
| 135 |
+
"adjusted_basic_diluted_eps": "adjbasicdilutedeps",
|
| 136 |
+
"return_on_equity_simple": "roe_simple",
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
class IntrinioFinancialRatiosFetcher(
|
| 141 |
+
Fetcher[
|
| 142 |
+
IntrinioFinancialRatiosQueryParams,
|
| 143 |
+
List[IntrinioFinancialRatiosData],
|
| 144 |
+
]
|
| 145 |
+
):
|
| 146 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 147 |
+
|
| 148 |
+
@staticmethod
|
| 149 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioFinancialRatiosQueryParams:
|
| 150 |
+
"""Transform the query params."""
|
| 151 |
+
return IntrinioFinancialRatiosQueryParams(**params)
|
| 152 |
+
|
| 153 |
+
@staticmethod
|
| 154 |
+
async def aextract_data(
|
| 155 |
+
query: IntrinioFinancialRatiosQueryParams,
|
| 156 |
+
credentials: Optional[Dict[str, str]],
|
| 157 |
+
**kwargs: Any,
|
| 158 |
+
) -> List[Dict]:
|
| 159 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 160 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 161 |
+
statement_code = "calculations"
|
| 162 |
+
if query.period in ["quarter", "annual"]:
|
| 163 |
+
period_type = "FY" if query.period == "annual" else "QTR"
|
| 164 |
+
elif query.period in ["ttm", "ytd"]:
|
| 165 |
+
period_type = query.period.upper()
|
| 166 |
+
else:
|
| 167 |
+
raise OpenBBError(f"Period '{query.period}' not supported.")
|
| 168 |
+
|
| 169 |
+
fundamentals_data: Dict = {}
|
| 170 |
+
|
| 171 |
+
base_url = "https://api-v2.intrinio.com"
|
| 172 |
+
fundamentals_url = (
|
| 173 |
+
f"{base_url}/companies/{query.symbol}/fundamentals?"
|
| 174 |
+
f"statement_code={statement_code}&type={period_type}"
|
| 175 |
+
)
|
| 176 |
+
if query.fiscal_year is not None:
|
| 177 |
+
if query.fiscal_year < 2008:
|
| 178 |
+
warn("Financials data is only available from 2008 and later.")
|
| 179 |
+
query.fiscal_year = 2008
|
| 180 |
+
fundamentals_url = fundamentals_url + f"&fiscal_year={query.fiscal_year}"
|
| 181 |
+
fundamentals_url = fundamentals_url + f"&api_key={api_key}"
|
| 182 |
+
fundamentals_data = (await get_data_one(fundamentals_url, **kwargs)).get(
|
| 183 |
+
"fundamentals", []
|
| 184 |
+
)
|
| 185 |
+
ids = [item["id"] for item in fundamentals_data]
|
| 186 |
+
ids = ids[: query.limit]
|
| 187 |
+
|
| 188 |
+
async def callback(response: ClientResponse, _: Any) -> Dict:
|
| 189 |
+
"""Return the response."""
|
| 190 |
+
statement_data = await response.json()
|
| 191 |
+
return {
|
| 192 |
+
"period_ending": statement_data["fundamental"]["end_date"], # type: ignore
|
| 193 |
+
"fiscal_year": statement_data["fundamental"]["fiscal_year"], # type: ignore
|
| 194 |
+
"fiscal_period": statement_data["fundamental"]["fiscal_period"], # type: ignore
|
| 195 |
+
"calculations": statement_data["standardized_financials"], # type: ignore
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
urls = [
|
| 199 |
+
f"https://api-v2.intrinio.com/fundamentals/{id}/standardized_financials?api_key={api_key}"
|
| 200 |
+
for id in ids
|
| 201 |
+
]
|
| 202 |
+
|
| 203 |
+
return await amake_requests(urls, callback, **kwargs) # type: ignore
|
| 204 |
+
|
| 205 |
+
@staticmethod
|
| 206 |
+
def transform_data(
|
| 207 |
+
query: IntrinioFinancialRatiosQueryParams, data: List[Dict], **kwargs: Any
|
| 208 |
+
) -> List[IntrinioFinancialRatiosData]:
|
| 209 |
+
"""Return the transformed data."""
|
| 210 |
+
transformed_data: List[IntrinioFinancialRatiosData] = []
|
| 211 |
+
|
| 212 |
+
tags = [
|
| 213 |
+
"nopatmargin",
|
| 214 |
+
"investedcapitalturnover",
|
| 215 |
+
"bookvaluepershare",
|
| 216 |
+
"tangiblebookvaluepershare",
|
| 217 |
+
"pricetobook",
|
| 218 |
+
"pricetotangiblebook",
|
| 219 |
+
"pricetorevenue",
|
| 220 |
+
"pricetoearnings",
|
| 221 |
+
"dividendyield",
|
| 222 |
+
"earningsyield",
|
| 223 |
+
"evtoinvestedcapital",
|
| 224 |
+
"evtorevenue",
|
| 225 |
+
"evtoebitda",
|
| 226 |
+
"evtoebit",
|
| 227 |
+
"evtonopat",
|
| 228 |
+
"evtoocf",
|
| 229 |
+
"evtofcff",
|
| 230 |
+
"grossmargin",
|
| 231 |
+
"ebitdamargin",
|
| 232 |
+
"operatingmargin",
|
| 233 |
+
"ebitmargin",
|
| 234 |
+
"profitmargin",
|
| 235 |
+
"costofrevtorevenue",
|
| 236 |
+
"sgaextorevenue",
|
| 237 |
+
"rdextorevenue",
|
| 238 |
+
"opextorevenue",
|
| 239 |
+
"taxburdenpct",
|
| 240 |
+
"interestburdenpct",
|
| 241 |
+
"efftaxrate",
|
| 242 |
+
"assetturnover",
|
| 243 |
+
"arturnover",
|
| 244 |
+
"invturnover",
|
| 245 |
+
"faturnover",
|
| 246 |
+
"apturnover",
|
| 247 |
+
"dso",
|
| 248 |
+
"dio",
|
| 249 |
+
"dpo",
|
| 250 |
+
"ccc",
|
| 251 |
+
"finleverage",
|
| 252 |
+
"leverageratio",
|
| 253 |
+
"compoundleveragefactor",
|
| 254 |
+
"ltdebttoequity",
|
| 255 |
+
"debttoequity",
|
| 256 |
+
"roic",
|
| 257 |
+
"nnep",
|
| 258 |
+
"roicnnepspread",
|
| 259 |
+
"rnnoa",
|
| 260 |
+
"roe",
|
| 261 |
+
"croic",
|
| 262 |
+
"oroa",
|
| 263 |
+
"roa",
|
| 264 |
+
"noncontrollinginterestsharingratio",
|
| 265 |
+
"roce",
|
| 266 |
+
"divpayoutratio",
|
| 267 |
+
"augmentedpayoutratio",
|
| 268 |
+
"ocftocapex",
|
| 269 |
+
"stdebttocap",
|
| 270 |
+
"ltdebttocap",
|
| 271 |
+
"debttototalcapital",
|
| 272 |
+
"preferredtocap",
|
| 273 |
+
"noncontrolinttocap",
|
| 274 |
+
"commontocap",
|
| 275 |
+
"debttoebitda",
|
| 276 |
+
"netdebttoebitda",
|
| 277 |
+
"ltdebttoebitda",
|
| 278 |
+
"debttonopat",
|
| 279 |
+
"netdebttonopat",
|
| 280 |
+
"ltdebttonopat",
|
| 281 |
+
"altmanzscore",
|
| 282 |
+
"ebittointerestex",
|
| 283 |
+
"nopattointerestex",
|
| 284 |
+
"ebitlesscapextointerestex",
|
| 285 |
+
"nopatlesscapextointex",
|
| 286 |
+
"ocftointerestex",
|
| 287 |
+
"ocflesscapextointerestex",
|
| 288 |
+
"fcfftointerestex",
|
| 289 |
+
"curratio",
|
| 290 |
+
"quickratio",
|
| 291 |
+
"dfcfnwctorev",
|
| 292 |
+
"dfnwctorev",
|
| 293 |
+
"nwctorev",
|
| 294 |
+
"normalizednopatmargin",
|
| 295 |
+
"pretaxincomemargin",
|
| 296 |
+
"adjbasiceps",
|
| 297 |
+
"adjdilutedeps",
|
| 298 |
+
"adjbasicdilutedeps",
|
| 299 |
+
"roe_simple",
|
| 300 |
+
]
|
| 301 |
+
|
| 302 |
+
for item in data:
|
| 303 |
+
sub_dict: Dict[str, Any] = {}
|
| 304 |
+
|
| 305 |
+
for sub_item in item["calculations"]:
|
| 306 |
+
field_name = sub_item["data_tag"]["tag"]
|
| 307 |
+
if field_name in tags:
|
| 308 |
+
sub_dict[field_name] = (
|
| 309 |
+
float(sub_item["value"])
|
| 310 |
+
if sub_item["value"] and sub_item["value"] != 0
|
| 311 |
+
else None
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
sub_dict["period_ending"] = item["period_ending"]
|
| 315 |
+
sub_dict["fiscal_year"] = item["fiscal_year"]
|
| 316 |
+
sub_dict["fiscal_period"] = item["fiscal_period"]
|
| 317 |
+
|
| 318 |
+
transformed_data.append(IntrinioFinancialRatiosData(**sub_dict))
|
| 319 |
+
|
| 320 |
+
return transformed_data
|
openbb_platform/providers/intrinio/openbb_intrinio/models/forward_ebitda_estimates.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Forward EBITDA Estimates Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
import asyncio
|
| 6 |
+
from typing import Any, Dict, List, Literal, Optional
|
| 7 |
+
from warnings import warn
|
| 8 |
+
|
| 9 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 10 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 11 |
+
from openbb_core.provider.standard_models.forward_ebitda_estimates import (
|
| 12 |
+
ForwardEbitdaEstimatesData,
|
| 13 |
+
ForwardEbitdaEstimatesQueryParams,
|
| 14 |
+
)
|
| 15 |
+
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
|
| 16 |
+
from openbb_core.provider.utils.helpers import (
|
| 17 |
+
amake_request,
|
| 18 |
+
get_querystring,
|
| 19 |
+
)
|
| 20 |
+
from openbb_intrinio.utils.helpers import response_callback
|
| 21 |
+
from pydantic import Field
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class IntrinioForwardEbitdaEstimatesQueryParams(ForwardEbitdaEstimatesQueryParams):
|
| 25 |
+
"""Intrinio Forward EBITDA Estimates Query.
|
| 26 |
+
|
| 27 |
+
https://docs.intrinio.com/documentation/web_api/get_zacks_sales_estimates_v2
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
__json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
|
| 31 |
+
__alias_dict__ = {"estimate_type": "type"}
|
| 32 |
+
|
| 33 |
+
fiscal_period: Optional[Literal["annual", "quarter"]] = Field(
|
| 34 |
+
default=None, description="Filter for only full-year or quarterly estimates."
|
| 35 |
+
)
|
| 36 |
+
estimate_type: Optional[
|
| 37 |
+
Literal[
|
| 38 |
+
"ebitda",
|
| 39 |
+
"ebit",
|
| 40 |
+
"enterprise_value",
|
| 41 |
+
"cash_flow_per_share",
|
| 42 |
+
"pretax_income",
|
| 43 |
+
]
|
| 44 |
+
] = Field(
|
| 45 |
+
default=None,
|
| 46 |
+
description="Limit the EBITDA estimates to this type.",
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class IntrinioForwardEbitdaEstimatesData(ForwardEbitdaEstimatesData):
|
| 51 |
+
"""Intrinio Forward EBITDA Estimates Data."""
|
| 52 |
+
|
| 53 |
+
__alias_dict__ = {
|
| 54 |
+
"last_updated": "updated_date",
|
| 55 |
+
"symbol": "ticker",
|
| 56 |
+
"calendar_period": "estimate_month",
|
| 57 |
+
"name": "company_name",
|
| 58 |
+
"fiscal_year": "estimate_year",
|
| 59 |
+
"fiscal_period": "period",
|
| 60 |
+
"low_estimate": "low",
|
| 61 |
+
"high_estimate": "high",
|
| 62 |
+
"number_of_analysts": "estimate_count",
|
| 63 |
+
"standard_deviation": "std_dev",
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
conensus_type: Optional[
|
| 67 |
+
Literal[
|
| 68 |
+
"ebitda",
|
| 69 |
+
"ebitda",
|
| 70 |
+
"ebit",
|
| 71 |
+
"enterprise_value",
|
| 72 |
+
"cash_flow_per_share",
|
| 73 |
+
"pretax_income",
|
| 74 |
+
]
|
| 75 |
+
] = Field(
|
| 76 |
+
default=None,
|
| 77 |
+
description="The type of estimate.",
|
| 78 |
+
)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class IntrinioForwardEbitdaEstimatesFetcher(
|
| 82 |
+
Fetcher[
|
| 83 |
+
IntrinioForwardEbitdaEstimatesQueryParams,
|
| 84 |
+
List[IntrinioForwardEbitdaEstimatesData],
|
| 85 |
+
]
|
| 86 |
+
):
|
| 87 |
+
"""Intrinio Forward EBITDA Estimates Fetcher."""
|
| 88 |
+
|
| 89 |
+
@staticmethod
|
| 90 |
+
def transform_query(
|
| 91 |
+
params: Dict[str, Any]
|
| 92 |
+
) -> IntrinioForwardEbitdaEstimatesQueryParams:
|
| 93 |
+
"""Transform the query params."""
|
| 94 |
+
return IntrinioForwardEbitdaEstimatesQueryParams(**params)
|
| 95 |
+
|
| 96 |
+
@staticmethod
|
| 97 |
+
async def aextract_data(
|
| 98 |
+
query: IntrinioForwardEbitdaEstimatesQueryParams,
|
| 99 |
+
credentials: Optional[Dict[str, str]],
|
| 100 |
+
**kwargs: Any,
|
| 101 |
+
) -> List[Dict]:
|
| 102 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 103 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 104 |
+
BASE_URL = (
|
| 105 |
+
"https://api-v2.intrinio.com/zacks/ebitda_consensus?"
|
| 106 |
+
+ f"page_size=10000&api_key={api_key}"
|
| 107 |
+
)
|
| 108 |
+
symbols = query.symbol.split(",") if query.symbol else None
|
| 109 |
+
query_str = get_querystring(query.model_dump(by_alias=True), ["symbol"])
|
| 110 |
+
results: List[Dict] = []
|
| 111 |
+
|
| 112 |
+
async def get_one(symbol):
|
| 113 |
+
"""Get the data for one symbol."""
|
| 114 |
+
url = f"{BASE_URL}&identifier={symbol}"
|
| 115 |
+
url = url + f"&{query_str}" if query_str else url
|
| 116 |
+
data = await amake_request(
|
| 117 |
+
url, response_callback=response_callback, **kwargs
|
| 118 |
+
)
|
| 119 |
+
consensus = (
|
| 120 |
+
data.get("ebitda_consensus")
|
| 121 |
+
if isinstance(data, dict) and "ebitda_consensus" in data
|
| 122 |
+
else []
|
| 123 |
+
)
|
| 124 |
+
if not data or not consensus:
|
| 125 |
+
warn(f"Symbol Error: No data found for {symbol}")
|
| 126 |
+
if consensus:
|
| 127 |
+
results.extend(consensus)
|
| 128 |
+
|
| 129 |
+
if symbols:
|
| 130 |
+
await asyncio.gather(*[get_one(symbol) for symbol in symbols])
|
| 131 |
+
if not results:
|
| 132 |
+
raise EmptyDataError(f"No results were found. -> {query.symbol}")
|
| 133 |
+
return results
|
| 134 |
+
|
| 135 |
+
async def fetch_callback(response, session):
|
| 136 |
+
"""Use callback for pagination."""
|
| 137 |
+
data = await response.json()
|
| 138 |
+
error = data.get("error", None)
|
| 139 |
+
if error:
|
| 140 |
+
message = data.get("message", "")
|
| 141 |
+
if "api key" in message.lower():
|
| 142 |
+
raise UnauthorizedError(
|
| 143 |
+
f"Unauthorized Intrinio request -> {message}"
|
| 144 |
+
)
|
| 145 |
+
raise OpenBBError(f"Error: {error} -> {message}")
|
| 146 |
+
|
| 147 |
+
estimates = data.get("ebitda_consensus", []) # type: ignore
|
| 148 |
+
if estimates and len(estimates) > 0:
|
| 149 |
+
results.extend(estimates)
|
| 150 |
+
while data.get("next_page"): # type: ignore
|
| 151 |
+
next_page = data["next_page"] # type: ignore
|
| 152 |
+
next_url = f"{url}&next_page={next_page}"
|
| 153 |
+
data = await amake_request(next_url, session=session, **kwargs)
|
| 154 |
+
consensus = (
|
| 155 |
+
data.get("ebitda_consensus")
|
| 156 |
+
if isinstance(data, dict) and "ebitda_consensus" in data
|
| 157 |
+
else []
|
| 158 |
+
)
|
| 159 |
+
if consensus:
|
| 160 |
+
results.extend(consensus) # type: ignore
|
| 161 |
+
return results
|
| 162 |
+
|
| 163 |
+
url = f"{BASE_URL}&{query_str}" if query_str else BASE_URL
|
| 164 |
+
|
| 165 |
+
results = await amake_request(url, response_callback=fetch_callback, **kwargs) # type: ignore
|
| 166 |
+
|
| 167 |
+
if not results:
|
| 168 |
+
raise EmptyDataError("The request was successful but was returned empty.")
|
| 169 |
+
|
| 170 |
+
return results
|
| 171 |
+
|
| 172 |
+
@staticmethod
|
| 173 |
+
def transform_data(
|
| 174 |
+
query: IntrinioForwardEbitdaEstimatesQueryParams,
|
| 175 |
+
data: List[Dict],
|
| 176 |
+
**kwargs: Any,
|
| 177 |
+
) -> List[IntrinioForwardEbitdaEstimatesData]:
|
| 178 |
+
"""Transform the raw data into the standard format."""
|
| 179 |
+
if not data:
|
| 180 |
+
raise EmptyDataError()
|
| 181 |
+
results: List[IntrinioForwardEbitdaEstimatesData] = []
|
| 182 |
+
fiscal_period = None
|
| 183 |
+
if query.fiscal_period is not None:
|
| 184 |
+
fiscal_period = "fy" if query.fiscal_period == "annual" else "fq"
|
| 185 |
+
for item in data:
|
| 186 |
+
estimate_count = item.get("estimate_count")
|
| 187 |
+
if (
|
| 188 |
+
not estimate_count
|
| 189 |
+
or estimate_count == 0
|
| 190 |
+
or not item.get("updated_date")
|
| 191 |
+
):
|
| 192 |
+
continue
|
| 193 |
+
if fiscal_period and item.get("period") != fiscal_period:
|
| 194 |
+
continue
|
| 195 |
+
results.append(IntrinioForwardEbitdaEstimatesData.model_validate(item))
|
| 196 |
+
if not results:
|
| 197 |
+
raise EmptyDataError()
|
| 198 |
+
|
| 199 |
+
return sorted(
|
| 200 |
+
results, key=lambda x: (x.fiscal_year, x.last_updated), reverse=True
|
| 201 |
+
)
|
openbb_platform/providers/intrinio/openbb_intrinio/models/forward_eps_estimates.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Forward EPS Estimates Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
import asyncio
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import Any, Dict, List, Literal, Optional, Union
|
| 8 |
+
from warnings import warn
|
| 9 |
+
|
| 10 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 11 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 12 |
+
from openbb_core.provider.standard_models.forward_eps_estimates import (
|
| 13 |
+
ForwardEpsEstimatesData,
|
| 14 |
+
ForwardEpsEstimatesQueryParams,
|
| 15 |
+
)
|
| 16 |
+
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
|
| 17 |
+
from openbb_core.provider.utils.helpers import (
|
| 18 |
+
amake_request,
|
| 19 |
+
get_querystring,
|
| 20 |
+
)
|
| 21 |
+
from openbb_intrinio.utils.helpers import response_callback
|
| 22 |
+
from pydantic import Field, field_validator, model_validator
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class IntrinioForwardEpsEstimatesQueryParams(ForwardEpsEstimatesQueryParams):
|
| 26 |
+
"""Intrinio Forward EPS Estimates Query.
|
| 27 |
+
|
| 28 |
+
https://docs.intrinio.com/documentation/web_api/get_zacks_sales_estimates_v2
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
__json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
|
| 32 |
+
|
| 33 |
+
fiscal_year: Optional[int] = Field(
|
| 34 |
+
default=None,
|
| 35 |
+
description="The future fiscal year to retrieve estimates for."
|
| 36 |
+
+ " When no symbol and year is supplied the current calendar year is used.",
|
| 37 |
+
)
|
| 38 |
+
fiscal_period: Optional[Literal["fy", "q1", "q2", "q3", "q4"]] = Field(
|
| 39 |
+
default=None,
|
| 40 |
+
description="The future fiscal period to retrieve estimates for.",
|
| 41 |
+
)
|
| 42 |
+
calendar_year: Optional[int] = Field(
|
| 43 |
+
default=None,
|
| 44 |
+
description="The future calendar year to retrieve estimates for."
|
| 45 |
+
+ " When no symbol and year is supplied the current calendar year is used.",
|
| 46 |
+
)
|
| 47 |
+
calendar_period: Optional[Literal["q1", "q2", "q3", "q4"]] = Field(
|
| 48 |
+
default=None,
|
| 49 |
+
description="The future calendar period to retrieve estimates for.",
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
@model_validator(mode="after")
|
| 53 |
+
@classmethod
|
| 54 |
+
def validate_choices(cls, values):
|
| 55 |
+
"""Validate the model and set a safe default state."""
|
| 56 |
+
if values.symbol is None and (
|
| 57 |
+
values.calendar_year is None and values.fiscal_year is None
|
| 58 |
+
):
|
| 59 |
+
values.calendar_year = datetime.now().year
|
| 60 |
+
return values
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class IntrinioForwardEpsEstimatesData(ForwardEpsEstimatesData):
|
| 64 |
+
"""Intrinio Forward EPS Estimates Data."""
|
| 65 |
+
|
| 66 |
+
__alias_dict__ = {
|
| 67 |
+
"high_estimate": "high",
|
| 68 |
+
"low_estimate": "low",
|
| 69 |
+
"number_of_analysts": "count",
|
| 70 |
+
"mean": "estimated_sales_mean",
|
| 71 |
+
"revisions_change_percent": "percent_change",
|
| 72 |
+
"mean_1w": "mean_7_days_ago",
|
| 73 |
+
"mean_1m": "mean_30_days_ago",
|
| 74 |
+
"mean_2m": "mean_60_days_ago",
|
| 75 |
+
"mean_3m": "mean_90_days_ago",
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
revisions_change_percent: Optional[float] = Field(
|
| 79 |
+
default=None,
|
| 80 |
+
description="The earnings per share (EPS) percent change in estimate for the period.",
|
| 81 |
+
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
|
| 82 |
+
)
|
| 83 |
+
mean_1w: Optional[float] = Field(
|
| 84 |
+
default=None,
|
| 85 |
+
description="The mean estimate for the period one week ago.",
|
| 86 |
+
)
|
| 87 |
+
mean_1m: Optional[float] = Field(
|
| 88 |
+
default=None,
|
| 89 |
+
description="The mean estimate for the period one month ago.",
|
| 90 |
+
)
|
| 91 |
+
mean_2m: Optional[float] = Field(
|
| 92 |
+
default=None,
|
| 93 |
+
description="The mean estimate for the period two months ago.",
|
| 94 |
+
)
|
| 95 |
+
mean_3m: Optional[float] = Field(
|
| 96 |
+
default=None,
|
| 97 |
+
description="The mean estimate for the period three months ago.",
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
@field_validator(
|
| 101 |
+
"revisions_change_percent",
|
| 102 |
+
mode="before",
|
| 103 |
+
check_fields=False,
|
| 104 |
+
)
|
| 105 |
+
@classmethod
|
| 106 |
+
def normalize_percent(
|
| 107 |
+
cls, v: Optional[Union[int, float]]
|
| 108 |
+
) -> Optional[Union[int, float]]:
|
| 109 |
+
"""Normalize percent values."""
|
| 110 |
+
return v / 100 if v else None
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class IntrinioForwardEpsEstimatesFetcher(
|
| 114 |
+
Fetcher[
|
| 115 |
+
IntrinioForwardEpsEstimatesQueryParams, List[IntrinioForwardEpsEstimatesData]
|
| 116 |
+
]
|
| 117 |
+
):
|
| 118 |
+
"""Intrinio Forward EPS Estimates Fetcher."""
|
| 119 |
+
|
| 120 |
+
@staticmethod
|
| 121 |
+
def transform_query(
|
| 122 |
+
params: Dict[str, Any]
|
| 123 |
+
) -> IntrinioForwardEpsEstimatesQueryParams:
|
| 124 |
+
"""Transform the query params."""
|
| 125 |
+
return IntrinioForwardEpsEstimatesQueryParams(**params)
|
| 126 |
+
|
| 127 |
+
@staticmethod
|
| 128 |
+
async def aextract_data(
|
| 129 |
+
query: IntrinioForwardEpsEstimatesQueryParams,
|
| 130 |
+
credentials: Optional[Dict[str, str]],
|
| 131 |
+
**kwargs: Any,
|
| 132 |
+
) -> List[Dict]:
|
| 133 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 134 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 135 |
+
|
| 136 |
+
BASE_URL = "https://api-v2.intrinio.com/zacks/eps_estimates?page_size=10000"
|
| 137 |
+
|
| 138 |
+
symbols = query.symbol.split(",") if query.symbol else None
|
| 139 |
+
|
| 140 |
+
query_str = get_querystring(
|
| 141 |
+
query.model_dump(by_alias=True),
|
| 142 |
+
["symbol", "calendar_period", "fiscal_period", "limit"],
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
results: List[Dict] = []
|
| 146 |
+
|
| 147 |
+
async def get_one(symbol):
|
| 148 |
+
"""Get the data for one symbol."""
|
| 149 |
+
url = f"{BASE_URL}&identifier={symbol}&{query_str}&api_key={api_key}"
|
| 150 |
+
new_data: List[Dict] = []
|
| 151 |
+
data = await amake_request(
|
| 152 |
+
url, response_callback=response_callback, **kwargs
|
| 153 |
+
)
|
| 154 |
+
if not data or not isinstance(data, dict) or not data.get("estimates"):
|
| 155 |
+
warn(f"Symbol Error: No data found for {symbol}")
|
| 156 |
+
if isinstance(data, dict) and data.get("estimates"):
|
| 157 |
+
new_data = data.get("estimates") # type: ignore
|
| 158 |
+
if new_data:
|
| 159 |
+
results.extend(new_data)
|
| 160 |
+
|
| 161 |
+
if symbols:
|
| 162 |
+
await asyncio.gather(*[get_one(symbol) for symbol in symbols])
|
| 163 |
+
return results
|
| 164 |
+
|
| 165 |
+
async def fetch_callback(response, session):
|
| 166 |
+
"""Use callback for pagination."""
|
| 167 |
+
data = await response.json()
|
| 168 |
+
error = data.get("error", None)
|
| 169 |
+
if error:
|
| 170 |
+
message = data.get("message", "")
|
| 171 |
+
if "api key" in message.lower():
|
| 172 |
+
raise UnauthorizedError(
|
| 173 |
+
f"Unauthorized Intrinio request -> {message}"
|
| 174 |
+
)
|
| 175 |
+
raise OpenBBError(f"Error: {error} -> {message}")
|
| 176 |
+
|
| 177 |
+
if data.get("estimates") and len(data.get("estimates")) > 0: # type: ignore
|
| 178 |
+
results.extend(data.get("estimates")) # type: ignore
|
| 179 |
+
while data.get("next_page"): # type: ignore
|
| 180 |
+
next_page = data["next_page"] # type: ignore
|
| 181 |
+
next_url = f"{url}&next_page={next_page}"
|
| 182 |
+
data = await amake_request(next_url, session=session, **kwargs)
|
| 183 |
+
if (
|
| 184 |
+
"estimates" in data
|
| 185 |
+
and len(data.get("estimates")) > 0 # type: ignore
|
| 186 |
+
):
|
| 187 |
+
results.extend(data.get("estimates")) # type: ignore
|
| 188 |
+
return results
|
| 189 |
+
|
| 190 |
+
url = f"{BASE_URL}&{query_str}&api_key={api_key}"
|
| 191 |
+
|
| 192 |
+
results = await amake_request(url, response_callback=fetch_callback, **kwargs) # type: ignore
|
| 193 |
+
|
| 194 |
+
if not results:
|
| 195 |
+
raise EmptyDataError("The request was successful but was returned empty.")
|
| 196 |
+
|
| 197 |
+
return results
|
| 198 |
+
|
| 199 |
+
@staticmethod
|
| 200 |
+
def transform_data(
|
| 201 |
+
query: IntrinioForwardEpsEstimatesQueryParams,
|
| 202 |
+
data: List[Dict],
|
| 203 |
+
**kwargs: Any,
|
| 204 |
+
) -> List[IntrinioForwardEpsEstimatesData]:
|
| 205 |
+
"""Transform the raw data into the standard format."""
|
| 206 |
+
symbols = query.symbol.split(",") if query.symbol else []
|
| 207 |
+
results: List[IntrinioForwardEpsEstimatesData] = []
|
| 208 |
+
for item in sorted(
|
| 209 |
+
data,
|
| 210 |
+
key=lambda item: ( # type: ignore
|
| 211 |
+
(
|
| 212 |
+
symbols.index(item.get("symbol")) if item.get("symbol") in symbols else len(symbols), # type: ignore
|
| 213 |
+
item.get("date"),
|
| 214 |
+
)
|
| 215 |
+
if symbols
|
| 216 |
+
else item.get("date")
|
| 217 |
+
),
|
| 218 |
+
):
|
| 219 |
+
temp: Dict[str, Any] = {}
|
| 220 |
+
company = item.pop("company")
|
| 221 |
+
if company.get("ticker") is None:
|
| 222 |
+
continue
|
| 223 |
+
temp["symbol"] = company.get("ticker")
|
| 224 |
+
temp["name"] = company.get("name")
|
| 225 |
+
if query.fiscal_period and query.fiscal_period.upper() != item.get(
|
| 226 |
+
"fiscal_period"
|
| 227 |
+
):
|
| 228 |
+
continue
|
| 229 |
+
if query.calendar_period and query.calendar_period.upper() != item.get(
|
| 230 |
+
"calendar_period"
|
| 231 |
+
):
|
| 232 |
+
continue
|
| 233 |
+
temp.update(item)
|
| 234 |
+
results.append(IntrinioForwardEpsEstimatesData.model_validate(temp))
|
| 235 |
+
|
| 236 |
+
return results
|
openbb_platform/providers/intrinio/openbb_intrinio/models/forward_pe_estimates.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Forward PE Estimates Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
from datetime import date as dateType
|
| 6 |
+
from typing import Any, Optional
|
| 7 |
+
|
| 8 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 9 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 10 |
+
from openbb_core.provider.standard_models.forward_pe_estimates import (
|
| 11 |
+
ForwardPeEstimatesData,
|
| 12 |
+
ForwardPeEstimatesQueryParams,
|
| 13 |
+
)
|
| 14 |
+
from pydantic import Field
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class IntrinioForwardPeEstimatesQueryParams(ForwardPeEstimatesQueryParams):
|
| 18 |
+
"""Intrinio Forward PE Estimates Query.
|
| 19 |
+
|
| 20 |
+
https://api-v2.intrinio.com/zacks/forward_pe?
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
__json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class IntrinioForwardPeEstimatesData(ForwardPeEstimatesData):
|
| 27 |
+
"""Intrinio Forward PE Estimates Data."""
|
| 28 |
+
|
| 29 |
+
__alias_dict__ = {
|
| 30 |
+
"symbol": "ticker",
|
| 31 |
+
"name": "company_name",
|
| 32 |
+
"year1": "forward_pe_year1",
|
| 33 |
+
"year2": "forward_pe_year2",
|
| 34 |
+
"year3": "forward_pe_year3",
|
| 35 |
+
"year4": "forward_pe_year4",
|
| 36 |
+
"year5": "forward_pe_year5",
|
| 37 |
+
"peg_ratio_year1": "forward_peg_ratio_year1",
|
| 38 |
+
"eps_ttm": "latest_ttm_eps",
|
| 39 |
+
"last_updated": "updated_date",
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
peg_ratio_year1: Optional[float] = Field(
|
| 43 |
+
default=None,
|
| 44 |
+
description="Estimated Forward PEG ratio for the next fiscal year.",
|
| 45 |
+
)
|
| 46 |
+
eps_ttm: Optional[float] = Field(
|
| 47 |
+
default=None,
|
| 48 |
+
description="The latest trailing twelve months earnings per share.",
|
| 49 |
+
)
|
| 50 |
+
last_updated: Optional[dateType] = Field(
|
| 51 |
+
default=None,
|
| 52 |
+
description="The date the data was last updated.",
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class IntrinioForwardPeEstimatesFetcher(
|
| 57 |
+
Fetcher[IntrinioForwardPeEstimatesQueryParams, list[IntrinioForwardPeEstimatesData]]
|
| 58 |
+
):
|
| 59 |
+
"""Intrinio Forward PE Estimates Fetcher."""
|
| 60 |
+
|
| 61 |
+
@staticmethod
|
| 62 |
+
def transform_query(
|
| 63 |
+
params: dict[str, Any],
|
| 64 |
+
) -> IntrinioForwardPeEstimatesQueryParams:
|
| 65 |
+
"""Transform the query params."""
|
| 66 |
+
return IntrinioForwardPeEstimatesQueryParams(**params)
|
| 67 |
+
|
| 68 |
+
@staticmethod
|
| 69 |
+
async def aextract_data(
|
| 70 |
+
query: IntrinioForwardPeEstimatesQueryParams,
|
| 71 |
+
credentials: Optional[dict[str, str]],
|
| 72 |
+
**kwargs: Any,
|
| 73 |
+
) -> list[dict]:
|
| 74 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 75 |
+
# pylint: disable=import-outside-toplevel
|
| 76 |
+
import asyncio # noqa
|
| 77 |
+
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
|
| 78 |
+
from openbb_core.provider.utils.helpers import amake_request
|
| 79 |
+
from openbb_intrinio.utils.helpers import response_callback
|
| 80 |
+
|
| 81 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 82 |
+
BASE_URL = "https://api-v2.intrinio.com/zacks/forward_pe"
|
| 83 |
+
symbols = query.symbol.split(",") if query.symbol else None
|
| 84 |
+
results: list[dict] = []
|
| 85 |
+
|
| 86 |
+
async def get_one(symbol):
|
| 87 |
+
"""Get the data for one symbol."""
|
| 88 |
+
url = f"{BASE_URL}/{symbol}?api_key={api_key}"
|
| 89 |
+
try:
|
| 90 |
+
data = await amake_request(
|
| 91 |
+
url, response_callback=response_callback, **kwargs
|
| 92 |
+
)
|
| 93 |
+
except Exception as e:
|
| 94 |
+
raise OpenBBError(e) from e
|
| 95 |
+
|
| 96 |
+
if data:
|
| 97 |
+
results.append(data) # type: ignore
|
| 98 |
+
|
| 99 |
+
if symbols:
|
| 100 |
+
try:
|
| 101 |
+
gather_results = await asyncio.gather(
|
| 102 |
+
*[get_one(symbol) for symbol in symbols], return_exceptions=True
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
for result in gather_results:
|
| 106 |
+
if isinstance(result, UnauthorizedError):
|
| 107 |
+
raise result
|
| 108 |
+
if isinstance(result, OpenBBError):
|
| 109 |
+
raise result
|
| 110 |
+
|
| 111 |
+
if not results:
|
| 112 |
+
raise EmptyDataError(
|
| 113 |
+
f"There were no results found for any of the given symbols. -> {symbols}"
|
| 114 |
+
)
|
| 115 |
+
return results
|
| 116 |
+
except Exception as e:
|
| 117 |
+
raise OpenBBError(
|
| 118 |
+
f"Error in Intrinio request -> {e} -> {symbols}"
|
| 119 |
+
) from e
|
| 120 |
+
|
| 121 |
+
async def fetch_callback(response, session):
|
| 122 |
+
"""Use callback for pagination."""
|
| 123 |
+
data = await response.json()
|
| 124 |
+
error = data.get("error", None)
|
| 125 |
+
|
| 126 |
+
if error:
|
| 127 |
+
message = data.get("message", "")
|
| 128 |
+
if "api key" in message.lower() or "view this data" in error.lower():
|
| 129 |
+
raise UnauthorizedError(
|
| 130 |
+
f"Unauthorized Intrinio request -> {message} -> {error}"
|
| 131 |
+
)
|
| 132 |
+
raise OpenBBError(f"Error: {error} -> {message}")
|
| 133 |
+
|
| 134 |
+
forward_pe = data.get("forward_pe")
|
| 135 |
+
|
| 136 |
+
if forward_pe and len(forward_pe) > 0: # type: ignore
|
| 137 |
+
results.extend(forward_pe) # type: ignore
|
| 138 |
+
|
| 139 |
+
return results
|
| 140 |
+
|
| 141 |
+
url = f"{BASE_URL}?page_size=10000&api_key={api_key}"
|
| 142 |
+
results = await amake_request(url, response_callback=fetch_callback, **kwargs) # type: ignore
|
| 143 |
+
|
| 144 |
+
if not results:
|
| 145 |
+
raise EmptyDataError("The request was successful but was returned empty.")
|
| 146 |
+
|
| 147 |
+
return results
|
| 148 |
+
|
| 149 |
+
@staticmethod
|
| 150 |
+
def transform_data(
|
| 151 |
+
query: IntrinioForwardPeEstimatesQueryParams,
|
| 152 |
+
data: list[dict],
|
| 153 |
+
**kwargs: Any,
|
| 154 |
+
) -> list[IntrinioForwardPeEstimatesData]:
|
| 155 |
+
"""Transform the raw data into the standard format."""
|
| 156 |
+
symbols = query.symbol.split(",") if query.symbol else []
|
| 157 |
+
if symbols:
|
| 158 |
+
data.sort(
|
| 159 |
+
key=lambda item: (
|
| 160 |
+
symbols.index(item.get("ticker")) # type: ignore
|
| 161 |
+
if item.get("ticker") in symbols
|
| 162 |
+
else len(symbols)
|
| 163 |
+
)
|
| 164 |
+
)
|
| 165 |
+
return [IntrinioForwardPeEstimatesData.model_validate(d) for d in data]
|
openbb_platform/providers/intrinio/openbb_intrinio/models/forward_sales_estimates.py
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Forward Sales Estimates Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable=unused-argument
|
| 4 |
+
|
| 5 |
+
import asyncio
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import Any, Dict, List, Literal, Optional, Union
|
| 8 |
+
from warnings import warn
|
| 9 |
+
|
| 10 |
+
from openbb_core.app.model.abstract.error import OpenBBError
|
| 11 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 12 |
+
from openbb_core.provider.standard_models.forward_sales_estimates import (
|
| 13 |
+
ForwardSalesEstimatesData,
|
| 14 |
+
ForwardSalesEstimatesQueryParams,
|
| 15 |
+
)
|
| 16 |
+
from openbb_core.provider.utils.errors import EmptyDataError, UnauthorizedError
|
| 17 |
+
from openbb_core.provider.utils.helpers import (
|
| 18 |
+
amake_request,
|
| 19 |
+
get_querystring,
|
| 20 |
+
)
|
| 21 |
+
from openbb_intrinio.utils.helpers import response_callback
|
| 22 |
+
from pydantic import Field, field_validator, model_validator
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class IntrinioForwardSalesEstimatesQueryParams(ForwardSalesEstimatesQueryParams):
|
| 26 |
+
"""Intrinio Forward Sales Estimates Query.
|
| 27 |
+
|
| 28 |
+
https://docs.intrinio.com/documentation/web_api/get_zacks_sales_estimates_v2
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
__json_schema_extra__ = {"symbol": {"multiple_items_allowed": True}}
|
| 32 |
+
|
| 33 |
+
fiscal_year: Optional[int] = Field(
|
| 34 |
+
default=None,
|
| 35 |
+
description="The future fiscal year to retrieve estimates for."
|
| 36 |
+
+ " When no symbol and year is supplied the current calendar year is used.",
|
| 37 |
+
)
|
| 38 |
+
fiscal_period: Optional[Literal["fy", "q1", "q2", "q3", "q4"]] = Field(
|
| 39 |
+
default=None,
|
| 40 |
+
description="The future fiscal period to retrieve estimates for.",
|
| 41 |
+
)
|
| 42 |
+
calendar_year: Optional[int] = Field(
|
| 43 |
+
default=None,
|
| 44 |
+
description="The future calendar year to retrieve estimates for."
|
| 45 |
+
+ " When no symbol and year is supplied the current calendar year is used.",
|
| 46 |
+
)
|
| 47 |
+
calendar_period: Optional[Literal["q1", "q2", "q3", "q4"]] = Field(
|
| 48 |
+
default=None,
|
| 49 |
+
description="The future calendar period to retrieve estimates for.",
|
| 50 |
+
)
|
| 51 |
+
|
| 52 |
+
@model_validator(mode="after")
|
| 53 |
+
@classmethod
|
| 54 |
+
def validate_choices(cls, values):
|
| 55 |
+
"""Validate the model and set a safe default state."""
|
| 56 |
+
if values.symbol is None and (
|
| 57 |
+
values.calendar_year is None and values.fiscal_year is None
|
| 58 |
+
):
|
| 59 |
+
values.calendar_year = datetime.now().year
|
| 60 |
+
return values
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
class IntrinioForwardSalesEstimatesData(ForwardSalesEstimatesData):
|
| 64 |
+
"""Intrinio Forward Sales Estimates Data."""
|
| 65 |
+
|
| 66 |
+
__alias_dict__ = {
|
| 67 |
+
"low_estimate": "low",
|
| 68 |
+
"high_estimate": "high",
|
| 69 |
+
"number_of_analysts": "count",
|
| 70 |
+
"mean": "estimated_sales_mean",
|
| 71 |
+
"revisions_1w_up": "analyst_revisions_up_1w",
|
| 72 |
+
"revisions_1w_down": "analyst_revisions_down_1w",
|
| 73 |
+
"revisions_1w_change_percent": "analyst_revisions_percent_change_1w",
|
| 74 |
+
"revisions_1m_up": "analyst_revisions_up_1m",
|
| 75 |
+
"revisions_1m_down": "analyst_revisions_down_1m",
|
| 76 |
+
"revisions_1m_change_percent": "analyst_revisions_percent_change_1m",
|
| 77 |
+
"revisions_3m_up": "analyst_revisions_up_3m",
|
| 78 |
+
"revisions_3m_down": "analyst_revisions_down_3m",
|
| 79 |
+
"revisions_3m_change_percent": "analyst_revisions_percent_change_3m",
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
revisions_1w_up: Optional[int] = Field(
|
| 83 |
+
default=None, description="Number of revisions up in the last week."
|
| 84 |
+
)
|
| 85 |
+
revisions_1w_down: Optional[int] = Field(
|
| 86 |
+
default=None, description="Number of revisions down in the last week."
|
| 87 |
+
)
|
| 88 |
+
revisions_1w_change_percent: Optional[float] = Field(
|
| 89 |
+
default=None,
|
| 90 |
+
description="The analyst revisions percent change in estimate for the period of 1 week.",
|
| 91 |
+
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
|
| 92 |
+
)
|
| 93 |
+
revisions_1m_up: Optional[int] = Field(
|
| 94 |
+
default=None, description="Number of revisions up in the last month."
|
| 95 |
+
)
|
| 96 |
+
revisions_1m_down: Optional[int] = Field(
|
| 97 |
+
default=None, description="Number of revisions down in the last month."
|
| 98 |
+
)
|
| 99 |
+
revisions_1m_change_percent: Optional[float] = Field(
|
| 100 |
+
default=None,
|
| 101 |
+
description="The analyst revisions percent change in estimate for the period of 1 month.",
|
| 102 |
+
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
|
| 103 |
+
)
|
| 104 |
+
revisions_3m_up: Optional[int] = Field(
|
| 105 |
+
default=None, description="Number of revisions up in the last 3 months."
|
| 106 |
+
)
|
| 107 |
+
revisions_3m_down: Optional[int] = Field(
|
| 108 |
+
default=None, description="Number of revisions down in the last 3 months."
|
| 109 |
+
)
|
| 110 |
+
revisions_3m_change_percent: Optional[float] = Field(
|
| 111 |
+
default=None,
|
| 112 |
+
description="The analyst revisions percent change in estimate for the period of 3 months.",
|
| 113 |
+
json_schema_extra={"x-unit_measurement": "percent", "x-frontend_multiply": 100},
|
| 114 |
+
)
|
| 115 |
+
|
| 116 |
+
@field_validator(
|
| 117 |
+
"revisions_1w_change_percent",
|
| 118 |
+
"revisions_1m_change_percent",
|
| 119 |
+
"revisions_3m_change_percent",
|
| 120 |
+
mode="before",
|
| 121 |
+
check_fields=False,
|
| 122 |
+
)
|
| 123 |
+
@classmethod
|
| 124 |
+
def normalize_percent(
|
| 125 |
+
cls, v: Optional[Union[int, float]]
|
| 126 |
+
) -> Optional[Union[int, float]]:
|
| 127 |
+
"""Normalize percent values."""
|
| 128 |
+
return v / 100 if v else None
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
class IntrinioForwardSalesEstimatesFetcher(
|
| 132 |
+
Fetcher[
|
| 133 |
+
IntrinioForwardSalesEstimatesQueryParams,
|
| 134 |
+
List[IntrinioForwardSalesEstimatesData],
|
| 135 |
+
]
|
| 136 |
+
):
|
| 137 |
+
"""Intrinio Forward Sales Estimates Fetcher."""
|
| 138 |
+
|
| 139 |
+
@staticmethod
|
| 140 |
+
def transform_query(
|
| 141 |
+
params: Dict[str, Any]
|
| 142 |
+
) -> IntrinioForwardSalesEstimatesQueryParams:
|
| 143 |
+
"""Transform the query params."""
|
| 144 |
+
return IntrinioForwardSalesEstimatesQueryParams(**params)
|
| 145 |
+
|
| 146 |
+
@staticmethod
|
| 147 |
+
async def aextract_data(
|
| 148 |
+
query: IntrinioForwardSalesEstimatesQueryParams,
|
| 149 |
+
credentials: Optional[Dict[str, str]],
|
| 150 |
+
**kwargs: Any,
|
| 151 |
+
) -> List[Dict]:
|
| 152 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 153 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 154 |
+
|
| 155 |
+
BASE_URL = "https://api-v2.intrinio.com/zacks/sales_estimates?page_size=10000"
|
| 156 |
+
|
| 157 |
+
symbols = query.symbol.split(",") if query.symbol else None
|
| 158 |
+
|
| 159 |
+
query_str = get_querystring(
|
| 160 |
+
query.model_dump(by_alias=True),
|
| 161 |
+
["symbol", "calendar_period", "fiscal_period", "limit"],
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
results: List[Dict] = []
|
| 165 |
+
|
| 166 |
+
async def get_one(symbol):
|
| 167 |
+
"""Get the data for one symbol."""
|
| 168 |
+
url = f"{BASE_URL}&identifier={symbol}&{query_str}&api_key={api_key}"
|
| 169 |
+
new_data: List[Dict] = []
|
| 170 |
+
data = await amake_request(
|
| 171 |
+
url, response_callback=response_callback, **kwargs
|
| 172 |
+
)
|
| 173 |
+
if not data or not isinstance(data, dict) or not data.get("estimates"):
|
| 174 |
+
warn(f"Symbol Error: No data found for {symbol}")
|
| 175 |
+
if isinstance(data, dict) and data.get("estimates"):
|
| 176 |
+
new_data = data.get("estimates") # type: ignore
|
| 177 |
+
if new_data:
|
| 178 |
+
results.extend(new_data)
|
| 179 |
+
|
| 180 |
+
if symbols:
|
| 181 |
+
await asyncio.gather(*[get_one(symbol) for symbol in symbols])
|
| 182 |
+
return results
|
| 183 |
+
|
| 184 |
+
async def fetch_callback(response, session):
|
| 185 |
+
"""Use callback for pagination."""
|
| 186 |
+
data = await response.json()
|
| 187 |
+
error = data.get("error", None)
|
| 188 |
+
if error:
|
| 189 |
+
message = data.get("message", "")
|
| 190 |
+
if "api key" in message.lower():
|
| 191 |
+
raise UnauthorizedError(
|
| 192 |
+
f"Unauthorized Intrinio request -> {message}"
|
| 193 |
+
)
|
| 194 |
+
raise OpenBBError(f"Error: {error} -> {message}")
|
| 195 |
+
if data.get("estimates") and len(data.get("estimates")) > 0: # type: ignore
|
| 196 |
+
results.extend(data.get("estimates")) # type: ignore
|
| 197 |
+
while data.get("next_page"): # type: ignore
|
| 198 |
+
next_page = data["next_page"] # type: ignore
|
| 199 |
+
next_url = f"{url}&next_page={next_page}"
|
| 200 |
+
data = await amake_request(next_url, session=session, **kwargs)
|
| 201 |
+
if (
|
| 202 |
+
"estimates" in data
|
| 203 |
+
and len(data.get("estimates")) > 0 # type: ignore
|
| 204 |
+
):
|
| 205 |
+
results.extend(data.get("estimates")) # type: ignore
|
| 206 |
+
return results
|
| 207 |
+
|
| 208 |
+
url = f"{BASE_URL}&{query_str}&api_key={api_key}"
|
| 209 |
+
|
| 210 |
+
results = await amake_request(url, response_callback=fetch_callback, **kwargs) # type: ignore
|
| 211 |
+
|
| 212 |
+
if not results:
|
| 213 |
+
raise EmptyDataError("The request was successful but was returned empty.")
|
| 214 |
+
|
| 215 |
+
return results
|
| 216 |
+
|
| 217 |
+
@staticmethod
|
| 218 |
+
def transform_data(
|
| 219 |
+
query: IntrinioForwardSalesEstimatesQueryParams,
|
| 220 |
+
data: List[Dict],
|
| 221 |
+
**kwargs: Any,
|
| 222 |
+
) -> List[IntrinioForwardSalesEstimatesData]:
|
| 223 |
+
"""Transform the raw data into the standard format."""
|
| 224 |
+
symbols = query.symbol.split(",") if query.symbol else []
|
| 225 |
+
results: List[IntrinioForwardSalesEstimatesData] = []
|
| 226 |
+
for item in sorted(
|
| 227 |
+
data,
|
| 228 |
+
key=lambda item: ( # type: ignore
|
| 229 |
+
(
|
| 230 |
+
symbols.index(item.get("symbol")) if item.get("symbol") in symbols else len(symbols), # type: ignore
|
| 231 |
+
item.get("date"),
|
| 232 |
+
)
|
| 233 |
+
if symbols
|
| 234 |
+
else item.get("date")
|
| 235 |
+
),
|
| 236 |
+
):
|
| 237 |
+
temp: Dict[str, Any] = {}
|
| 238 |
+
company = item.pop("company")
|
| 239 |
+
if company.get("ticker") is None:
|
| 240 |
+
continue
|
| 241 |
+
temp["symbol"] = company.get("ticker")
|
| 242 |
+
temp["name"] = company.get("name")
|
| 243 |
+
if query.fiscal_period and query.fiscal_period.upper() != item.get(
|
| 244 |
+
"fiscal_period"
|
| 245 |
+
):
|
| 246 |
+
continue
|
| 247 |
+
if query.calendar_period and query.calendar_period.upper() != item.get(
|
| 248 |
+
"calendar_period"
|
| 249 |
+
):
|
| 250 |
+
continue
|
| 251 |
+
temp.update(item)
|
| 252 |
+
results.append(IntrinioForwardSalesEstimatesData.model_validate(temp))
|
| 253 |
+
|
| 254 |
+
return results
|
openbb_platform/providers/intrinio/openbb_intrinio/models/fred_series.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio FRED Series Model."""
|
| 2 |
+
|
| 3 |
+
import asyncio
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
from typing import Any, Dict, List, Optional
|
| 6 |
+
|
| 7 |
+
from dateutil.relativedelta import relativedelta
|
| 8 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 9 |
+
from openbb_core.provider.standard_models.fred_series import (
|
| 10 |
+
SeriesData,
|
| 11 |
+
SeriesQueryParams,
|
| 12 |
+
)
|
| 13 |
+
from openbb_core.provider.utils.helpers import (
|
| 14 |
+
ClientResponse,
|
| 15 |
+
ClientSession,
|
| 16 |
+
amake_requests,
|
| 17 |
+
get_querystring,
|
| 18 |
+
)
|
| 19 |
+
from pydantic import Field
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class IntrinioFredSeriesQueryParams(SeriesQueryParams):
|
| 23 |
+
"""Intrinio FRED Series Query.
|
| 24 |
+
|
| 25 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_economic_index_historical_data_v2
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
__alias_dict__ = {"limit": "page_size"}
|
| 29 |
+
|
| 30 |
+
all_pages: Optional[bool] = Field(
|
| 31 |
+
default=False,
|
| 32 |
+
description="Returns all pages of data from the API call at once.",
|
| 33 |
+
)
|
| 34 |
+
sleep: Optional[float] = Field(
|
| 35 |
+
default=1.0,
|
| 36 |
+
description="Time to sleep between requests to avoid rate limiting.",
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class IntrinioFredSeriesData(SeriesData):
|
| 41 |
+
"""Intrinio FRED Series Data."""
|
| 42 |
+
|
| 43 |
+
value: Optional[float] = Field(default=None, description="Value of the index.")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
class IntrinioFredSeriesFetcher(
|
| 47 |
+
Fetcher[
|
| 48 |
+
IntrinioFredSeriesQueryParams,
|
| 49 |
+
List[IntrinioFredSeriesData],
|
| 50 |
+
]
|
| 51 |
+
):
|
| 52 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 53 |
+
|
| 54 |
+
@staticmethod
|
| 55 |
+
def transform_query(params: Dict[str, Any]) -> IntrinioFredSeriesQueryParams:
|
| 56 |
+
"""Transform the query params."""
|
| 57 |
+
transformed_params = params
|
| 58 |
+
|
| 59 |
+
now = datetime.now().date()
|
| 60 |
+
if params.get("start_date") is None:
|
| 61 |
+
transformed_params["start_date"] = now - relativedelta(years=1)
|
| 62 |
+
|
| 63 |
+
if params.get("end_date") is None:
|
| 64 |
+
transformed_params["end_date"] = now
|
| 65 |
+
|
| 66 |
+
return IntrinioFredSeriesQueryParams(**transformed_params)
|
| 67 |
+
|
| 68 |
+
@staticmethod
|
| 69 |
+
async def aextract_data(
|
| 70 |
+
query: IntrinioFredSeriesQueryParams,
|
| 71 |
+
credentials: Optional[Dict[str, str]],
|
| 72 |
+
**kwargs: Any,
|
| 73 |
+
) -> List[Dict]:
|
| 74 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 75 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 76 |
+
|
| 77 |
+
base_url = "https://api-v2.intrinio.com"
|
| 78 |
+
query_str = get_querystring(
|
| 79 |
+
query.model_dump(), ["symbol", "all_pages", "sleep"]
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
url = (
|
| 83 |
+
f"{base_url}/indices/economic/${query.symbol.replace('$', '')}/historical_data/level"
|
| 84 |
+
f"?{query_str}&api_key={api_key}"
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
async def callback(response: ClientResponse, session: ClientSession) -> dict:
|
| 88 |
+
"""Return the response."""
|
| 89 |
+
init_response = await response.json()
|
| 90 |
+
|
| 91 |
+
all_data: list = init_response.get("historical_data", [])
|
| 92 |
+
|
| 93 |
+
if query.all_pages:
|
| 94 |
+
next_page = init_response.get("next_page", None)
|
| 95 |
+
while next_page:
|
| 96 |
+
if query.limit > 100:
|
| 97 |
+
await asyncio.sleep(query.sleep)
|
| 98 |
+
|
| 99 |
+
url = response.url.update_query(next_page=next_page).human_repr()
|
| 100 |
+
response_data = await session.get_json(url)
|
| 101 |
+
|
| 102 |
+
all_data.extend(response_data.get("historical_data", []))
|
| 103 |
+
next_page = response_data.get("next_page", None)
|
| 104 |
+
|
| 105 |
+
return all_data
|
| 106 |
+
|
| 107 |
+
return await amake_requests([url], callback, **kwargs)
|
| 108 |
+
|
| 109 |
+
@staticmethod
|
| 110 |
+
def transform_data(
|
| 111 |
+
query: IntrinioFredSeriesQueryParams, data: List[Dict], **kwargs: Any
|
| 112 |
+
) -> List[IntrinioFredSeriesData]:
|
| 113 |
+
"""Return the transformed data."""
|
| 114 |
+
return [IntrinioFredSeriesData.model_validate(d) for d in data]
|
openbb_platform/providers/intrinio/openbb_intrinio/models/historical_attributes.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Intrinio Historical Attributes Model."""
|
| 2 |
+
|
| 3 |
+
# pylint: disable = unused-argument
|
| 4 |
+
|
| 5 |
+
import warnings
|
| 6 |
+
from datetime import datetime
|
| 7 |
+
from typing import Any, Dict, List, Optional
|
| 8 |
+
|
| 9 |
+
from dateutil.relativedelta import relativedelta
|
| 10 |
+
from openbb_core.app.model.abstract.warning import OpenBBWarning
|
| 11 |
+
from openbb_core.provider.abstract.fetcher import Fetcher
|
| 12 |
+
from openbb_core.provider.standard_models.historical_attributes import (
|
| 13 |
+
HistoricalAttributesData,
|
| 14 |
+
HistoricalAttributesQueryParams,
|
| 15 |
+
)
|
| 16 |
+
from openbb_core.provider.utils.helpers import (
|
| 17 |
+
ClientResponse,
|
| 18 |
+
ClientSession,
|
| 19 |
+
amake_requests,
|
| 20 |
+
get_querystring,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class IntrinioHistoricalAttributesQueryParams(HistoricalAttributesQueryParams):
|
| 25 |
+
"""Intrinio Historical Attributes Query.
|
| 26 |
+
|
| 27 |
+
Source: https://docs.intrinio.com/documentation/web_api/get_historical_data_v2
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
__alias_dict__ = {"sort": "sort_order", "limit": "page_size", "tag_type": "type"}
|
| 31 |
+
__json_schema_extra__ = {
|
| 32 |
+
"tag": {"multiple_items_allowed": True},
|
| 33 |
+
"symbol": {"multiple_items_allowed": True},
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class IntrinioHistoricalAttributesData(HistoricalAttributesData):
|
| 38 |
+
"""Intrinio Historical Attributes Data."""
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class IntrinioHistoricalAttributesFetcher(
|
| 42 |
+
Fetcher[
|
| 43 |
+
IntrinioHistoricalAttributesQueryParams,
|
| 44 |
+
List[IntrinioHistoricalAttributesData],
|
| 45 |
+
]
|
| 46 |
+
):
|
| 47 |
+
"""Transform the query, extract and transform the data from the Intrinio endpoints."""
|
| 48 |
+
|
| 49 |
+
@staticmethod
|
| 50 |
+
def transform_query(
|
| 51 |
+
params: Dict[str, Any]
|
| 52 |
+
) -> IntrinioHistoricalAttributesQueryParams:
|
| 53 |
+
"""Transform the query params."""
|
| 54 |
+
transformed_params = params
|
| 55 |
+
|
| 56 |
+
now = datetime.now().date()
|
| 57 |
+
if params.get("start_date") is None:
|
| 58 |
+
transformed_params["start_date"] = now - relativedelta(years=5)
|
| 59 |
+
|
| 60 |
+
if params.get("end_date") is None:
|
| 61 |
+
transformed_params["end_date"] = now
|
| 62 |
+
|
| 63 |
+
return IntrinioHistoricalAttributesQueryParams(**transformed_params)
|
| 64 |
+
|
| 65 |
+
@staticmethod
|
| 66 |
+
async def aextract_data(
|
| 67 |
+
query: IntrinioHistoricalAttributesQueryParams,
|
| 68 |
+
credentials: Optional[Dict[str, str]],
|
| 69 |
+
**kwargs: Any,
|
| 70 |
+
) -> List[Dict]:
|
| 71 |
+
"""Return the raw data from the Intrinio endpoint."""
|
| 72 |
+
api_key = credentials.get("intrinio_api_key") if credentials else ""
|
| 73 |
+
|
| 74 |
+
base_url = "https://api-v2.intrinio.com"
|
| 75 |
+
query_str = get_querystring(query.model_dump(by_alias=True), ["symbol", "tag"])
|
| 76 |
+
|
| 77 |
+
def generate_url(symbol: str, tag: str) -> str:
|
| 78 |
+
"""Return the url for the given symbol and tag."""
|
| 79 |
+
url_params = f"{symbol}/{tag}?{query_str}&api_key={api_key}"
|
| 80 |
+
url = f"{base_url}/historical_data/{url_params}"
|
| 81 |
+
return url
|
| 82 |
+
|
| 83 |
+
async def callback(
|
| 84 |
+
response: ClientResponse, session: ClientSession
|
| 85 |
+
) -> List[Dict]:
|
| 86 |
+
"""Return the response."""
|
| 87 |
+
init_response = await response.json()
|
| 88 |
+
|
| 89 |
+
if message := init_response.get( # type: ignore
|
| 90 |
+
"error"
|
| 91 |
+
) or init_response.get( # type: ignore
|
| 92 |
+
"message"
|
| 93 |
+
):
|
| 94 |
+
warnings.warn(message=str(message), category=OpenBBWarning)
|
| 95 |
+
return []
|
| 96 |
+
|
| 97 |
+
symbol = response.url.parts[-2] # type: ignore
|
| 98 |
+
tag = response.url.parts[-1] # type: ignore
|
| 99 |
+
|
| 100 |
+
all_data: List = init_response.get("historical_data", []) # type: ignore
|
| 101 |
+
all_data = [{**item, "symbol": symbol, "tag": tag} for item in all_data]
|
| 102 |
+
|
| 103 |
+
next_page = init_response.get("next_page", None) # type: ignore
|
| 104 |
+
while next_page:
|
| 105 |
+
url = response.url.update_query( # type: ignore
|
| 106 |
+
next_page=next_page
|
| 107 |
+
).human_repr()
|
| 108 |
+
response_data = await session.get_json(url)
|
| 109 |
+
|
| 110 |
+
if message := response_data.get("error") or response_data.get( # type: ignore
|
| 111 |
+
"message"
|
| 112 |
+
):
|
| 113 |
+
warnings.warn(message=message, category=OpenBBWarning)
|
| 114 |
+
return []
|
| 115 |
+
|
| 116 |
+
symbol = response.url.parts[-2] # type: ignore
|
| 117 |
+
tag = response_data.url.parts[-1] # type: ignore
|
| 118 |
+
|
| 119 |
+
response_data = response_data.get("historical_data", []) # type: ignore
|
| 120 |
+
response_data = [
|
| 121 |
+
{**item, "symbol": symbol, "tag": tag} for item in response_data
|
| 122 |
+
]
|
| 123 |
+
|
| 124 |
+
all_data.extend(response_data)
|
| 125 |
+
next_page = response_data.get("next_page", None) # type: ignore
|
| 126 |
+
|
| 127 |
+
return all_data
|
| 128 |
+
|
| 129 |
+
urls = [
|
| 130 |
+
generate_url(symbol, tag)
|
| 131 |
+
for symbol in query.symbol.split(",")
|
| 132 |
+
for tag in query.tag.split(",")
|
| 133 |
+
]
|
| 134 |
+
|
| 135 |
+
return await amake_requests(urls, callback, **kwargs)
|
| 136 |
+
|
| 137 |
+
@staticmethod
|
| 138 |
+
def transform_data(
|
| 139 |
+
query: IntrinioHistoricalAttributesQueryParams,
|
| 140 |
+
data: List[Dict],
|
| 141 |
+
**kwargs: Any,
|
| 142 |
+
) -> List[IntrinioHistoricalAttributesData]:
|
| 143 |
+
"""Return the transformed data."""
|
| 144 |
+
return [IntrinioHistoricalAttributesData.model_validate(d) for d in data]
|