tesorotools-python 0.0.44__py3-none-any.whl → 0.0.45__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tesorotools/providers/__init__.py +11 -4
- tesorotools/providers/imf_irfcl.py +242 -0
- {tesorotools_python-0.0.44.dist-info → tesorotools_python-0.0.45.dist-info}/METADATA +4 -1
- {tesorotools_python-0.0.44.dist-info → tesorotools_python-0.0.45.dist-info}/RECORD +5 -4
- {tesorotools_python-0.0.44.dist-info → tesorotools_python-0.0.45.dist-info}/WHEEL +0 -0
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"""Public provider API.
|
|
2
2
|
|
|
3
|
-
``BdeProvider``, ``EcbProvider
|
|
4
|
-
the optional ``[bde]`` / ``[ecb]`` /
|
|
5
|
-
imported lazily through
|
|
6
|
-
``tesorotools.providers`` itself does not
|
|
3
|
+
``BdeProvider``, ``EcbProvider``, ``ImfIrfclProvider`` and
|
|
4
|
+
``LSEGProvider`` depend on the optional ``[bde]`` / ``[ecb]`` /
|
|
5
|
+
``[imf]`` / ``[lseg]`` extras and are imported lazily through
|
|
6
|
+
``__getattr__``; importing ``tesorotools.providers`` itself does not
|
|
7
|
+
require those extras.
|
|
7
8
|
"""
|
|
8
9
|
|
|
9
10
|
from typing import TYPE_CHECKING, Any
|
|
@@ -17,12 +18,14 @@ from tesorotools.providers.base import (
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
19
|
from tesorotools.providers.bde import BdeProvider
|
|
19
20
|
from tesorotools.providers.ecb import EcbProvider
|
|
21
|
+
from tesorotools.providers.imf_irfcl import ImfIrfclProvider
|
|
20
22
|
from tesorotools.providers.lseg import LSEGProvider
|
|
21
23
|
|
|
22
24
|
__all__ = [
|
|
23
25
|
"BdeProvider",
|
|
24
26
|
"DataProvider",
|
|
25
27
|
"EcbProvider",
|
|
28
|
+
"ImfIrfclProvider",
|
|
26
29
|
"LSEGProvider",
|
|
27
30
|
"RegistryProtocol",
|
|
28
31
|
"bootstrap_providers",
|
|
@@ -38,6 +41,10 @@ def __getattr__(name: str) -> Any:
|
|
|
38
41
|
from tesorotools.providers.ecb import EcbProvider
|
|
39
42
|
|
|
40
43
|
return EcbProvider
|
|
44
|
+
if name == "ImfIrfclProvider":
|
|
45
|
+
from tesorotools.providers.imf_irfcl import ImfIrfclProvider
|
|
46
|
+
|
|
47
|
+
return ImfIrfclProvider
|
|
41
48
|
if name == "LSEGProvider":
|
|
42
49
|
from tesorotools.providers.lseg import LSEGProvider
|
|
43
50
|
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""IMF IRFCL data provider via the official SDMX 3.0 REST API.
|
|
2
|
+
|
|
3
|
+
Downloads time series from the IMF's IRFCL dataflow (International
|
|
4
|
+
Reserves and Foreign Currency Liquidity), which publishes official
|
|
5
|
+
reserve assets per country, gold included. No authentication
|
|
6
|
+
required.
|
|
7
|
+
|
|
8
|
+
Install with the ``imf`` optional extra::
|
|
9
|
+
|
|
10
|
+
uv pip install "tesorotools-python[imf]"
|
|
11
|
+
|
|
12
|
+
Indicators (verified against the API in jun-2026):
|
|
13
|
+
|
|
14
|
+
* ``IRFCLDT1_IRFCL56V_FTO`` — gold, VOLUME in fine troy ounces. This
|
|
15
|
+
is the **physical stock** (despite the IMF label saying "millions of
|
|
16
|
+
ounces", the raw value comes in ounces: Spain ≈ 9,053,750 oz ≈
|
|
17
|
+
281.6 t).
|
|
18
|
+
* ``IRFCLDT1_IRFCL56_USD`` — gold value in USD (approx. market value).
|
|
19
|
+
* ``IRFCLDT1_IRFCL65_USD`` — TOTAL official reserve assets in USD
|
|
20
|
+
(denominator of the % gold represents over reserves).
|
|
21
|
+
|
|
22
|
+
API base
|
|
23
|
+
--------
|
|
24
|
+
``https://api.imf.org/external/sdmx/3.0/data/dataflow/IMF.STA/IRFCL/~/{key}``
|
|
25
|
+
|
|
26
|
+
Code convention (4 dimensions of DSD_IRFCL_PUB)
|
|
27
|
+
-----------------------------------------------
|
|
28
|
+
::
|
|
29
|
+
|
|
30
|
+
{COUNTRY}.{INDICATOR}.{SECTOR}.{FREQUENCY}
|
|
31
|
+
|
|
32
|
+
* COUNTRY ISO-3 country code (``ESP``, ``USA``, ``DEU`` …).
|
|
33
|
+
* INDICATOR the IRFCL indicator (``IRFCLDT1_IRFCL56V_FTO`` …).
|
|
34
|
+
* SECTOR ``*`` (wildcard). Countries report gold under different
|
|
35
|
+
sectors (Spain ``S1X``, the US ``S1XS1311`` …); the
|
|
36
|
+
wildcard captures them all. When a key returns several
|
|
37
|
+
series (same gold total under different sectors) they are
|
|
38
|
+
collapsed by date in :meth:`_fetch_one` (last write wins;
|
|
39
|
+
values coincide).
|
|
40
|
+
* FREQUENCY ``M`` (monthly; reserves are published every month).
|
|
41
|
+
|
|
42
|
+
Keys are passed verbatim to the API; this provider does not parse them.
|
|
43
|
+
|
|
44
|
+
Date label
|
|
45
|
+
----------
|
|
46
|
+
IRFCL labels the period and the observation is the end-of-period
|
|
47
|
+
balance. We map to the real end of period:
|
|
48
|
+
|
|
49
|
+
* ``"2026"`` → 2026-12-31
|
|
50
|
+
* ``"2026-Q1"`` → 2026-03-31
|
|
51
|
+
* ``"2026-M04"`` → 2026-04-30
|
|
52
|
+
|
|
53
|
+
Corporate TLS
|
|
54
|
+
-------------
|
|
55
|
+
``truststore`` is enabled on import (if installed) so the provider
|
|
56
|
+
works behind MINECO's TLS proxy without ``verify=False``.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
import logging
|
|
62
|
+
from typing import ClassVar
|
|
63
|
+
|
|
64
|
+
import pandas as pd
|
|
65
|
+
import requests
|
|
66
|
+
|
|
67
|
+
from tesorotools.providers.base import DataProvider
|
|
68
|
+
|
|
69
|
+
logger = logging.getLogger(__name__)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _enable_truststore_once() -> bool:
|
|
73
|
+
"""Enable truststore (the OS cert store) if available."""
|
|
74
|
+
try:
|
|
75
|
+
import truststore # type: ignore[import-not-found]
|
|
76
|
+
except ImportError:
|
|
77
|
+
return False
|
|
78
|
+
truststore.inject_into_ssl() # pyright: ignore[reportUnknownMemberType]
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
_TRUSTSTORE_ACTIVE = _enable_truststore_once()
|
|
83
|
+
|
|
84
|
+
_API_BASE = (
|
|
85
|
+
"https://api.imf.org/external/sdmx/3.0/data/dataflow/IMF.STA/IRFCL/~"
|
|
86
|
+
)
|
|
87
|
+
_DEFAULT_TIMEOUT = 60
|
|
88
|
+
_DEFAULT_START = "2000"
|
|
89
|
+
_HEADERS = {"Accept": "application/vnd.sdmx.data+json"}
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ImfIrfclProvider(DataProvider):
|
|
93
|
+
"""Provider that downloads IMF IRFCL series via SDMX 3.0.
|
|
94
|
+
|
|
95
|
+
One HTTP request per key. Series with no observations (a country
|
|
96
|
+
that does not report that indicator) are skipped with a warning
|
|
97
|
+
instead of aborting the whole download.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
timeout
|
|
102
|
+
Max seconds per HTTP request.
|
|
103
|
+
session
|
|
104
|
+
Optional pre-built ``requests.Session``. Useful for tests or
|
|
105
|
+
for custom retry policies.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
PROVIDER_NAME: ClassVar[str] = "imf_irfcl"
|
|
109
|
+
|
|
110
|
+
def __init__(
|
|
111
|
+
self,
|
|
112
|
+
*,
|
|
113
|
+
timeout: int = _DEFAULT_TIMEOUT,
|
|
114
|
+
session: requests.Session | None = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
self._timeout = timeout
|
|
117
|
+
self._session = session or requests.Session()
|
|
118
|
+
|
|
119
|
+
def fetch(
|
|
120
|
+
self,
|
|
121
|
+
codes: list[str],
|
|
122
|
+
start: str | None = None,
|
|
123
|
+
end: str | None = None,
|
|
124
|
+
) -> pd.DataFrame:
|
|
125
|
+
"""Download a list of IRFCL keys and return a wide DataFrame.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
codes
|
|
130
|
+
List of IRFCL keys (``"{COUNTRY}.{INDICATOR}.{SECTOR}.{FREQ}"``).
|
|
131
|
+
start
|
|
132
|
+
Start period (e.g. ``"2024"``). If ``None`` defaults to
|
|
133
|
+
``"2000"``.
|
|
134
|
+
end
|
|
135
|
+
End date as ISO string. If ``None`` up to latest.
|
|
136
|
+
|
|
137
|
+
Returns
|
|
138
|
+
-------
|
|
139
|
+
pd.DataFrame
|
|
140
|
+
Wide DataFrame. Index = dates (tz-naive), columns = the
|
|
141
|
+
requested codes. Missing observations are NaN.
|
|
142
|
+
"""
|
|
143
|
+
if not codes:
|
|
144
|
+
return pd.DataFrame()
|
|
145
|
+
|
|
146
|
+
cols: dict[str, pd.Series[float]] = {}
|
|
147
|
+
for code in codes:
|
|
148
|
+
try:
|
|
149
|
+
series = self._fetch_one(code, start=start)
|
|
150
|
+
except requests.RequestException as e:
|
|
151
|
+
logger.warning("IRFCL: failed downloading %s (%s)", code, e)
|
|
152
|
+
continue
|
|
153
|
+
if series is not None and not series.empty:
|
|
154
|
+
cols[code] = series
|
|
155
|
+
|
|
156
|
+
if not cols:
|
|
157
|
+
return pd.DataFrame()
|
|
158
|
+
|
|
159
|
+
df = pd.DataFrame(cols).sort_index()
|
|
160
|
+
df.index.name = "date"
|
|
161
|
+
if end:
|
|
162
|
+
df = df.loc[df.index <= pd.Timestamp(end)]
|
|
163
|
+
return df
|
|
164
|
+
|
|
165
|
+
def is_available(self) -> bool:
|
|
166
|
+
"""Check whether the IMF API responds.
|
|
167
|
+
|
|
168
|
+
Hits a small ESP gold-volume query.
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
r = self._session.get(
|
|
172
|
+
f"{_API_BASE}/ESP.IRFCLDT1_IRFCL56V_FTO.*.M?startPeriod=2024",
|
|
173
|
+
timeout=10,
|
|
174
|
+
headers=_HEADERS,
|
|
175
|
+
)
|
|
176
|
+
return r.status_code == 200
|
|
177
|
+
except requests.RequestException:
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
def _fetch_one(
|
|
181
|
+
self, code: str, start: str | None = None
|
|
182
|
+
) -> "pd.Series[float] | None":
|
|
183
|
+
"""Download a single IRFCL series and return it as a Series."""
|
|
184
|
+
url = f"{_API_BASE}/{code}?startPeriod={start or _DEFAULT_START}"
|
|
185
|
+
r = self._session.get(url, timeout=self._timeout, headers=_HEADERS)
|
|
186
|
+
if r.status_code != 200:
|
|
187
|
+
logger.warning("IRFCL: HTTP %d for %s", r.status_code, code)
|
|
188
|
+
return None
|
|
189
|
+
payload = r.json()
|
|
190
|
+
data_sets = payload.get("data", {}).get("dataSets", [])
|
|
191
|
+
structures = payload.get("data", {}).get("structures", [])
|
|
192
|
+
if not data_sets or not structures:
|
|
193
|
+
return None
|
|
194
|
+
time_dim = structures[0].get("dimensions", {}).get("observation", [])
|
|
195
|
+
if not time_dim:
|
|
196
|
+
return None
|
|
197
|
+
time_values = [t["value"] for t in time_dim[0].get("values", [])]
|
|
198
|
+
series_dict = data_sets[0].get("series", {})
|
|
199
|
+
|
|
200
|
+
data: dict[pd.Timestamp, float] = {}
|
|
201
|
+
for _, series in series_dict.items():
|
|
202
|
+
observations = series.get("observations", {})
|
|
203
|
+
for idx_str, payload_obs in observations.items():
|
|
204
|
+
if not payload_obs or payload_obs[0] is None:
|
|
205
|
+
continue
|
|
206
|
+
try:
|
|
207
|
+
value = float(payload_obs[0])
|
|
208
|
+
except (TypeError, ValueError):
|
|
209
|
+
continue
|
|
210
|
+
idx = int(idx_str)
|
|
211
|
+
if idx >= len(time_values):
|
|
212
|
+
continue
|
|
213
|
+
ts = _parse_period(time_values[idx])
|
|
214
|
+
if ts is not None:
|
|
215
|
+
data[ts] = value
|
|
216
|
+
|
|
217
|
+
if not data:
|
|
218
|
+
return None
|
|
219
|
+
return pd.Series(data, dtype="float64").sort_index()
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _parse_period(label: str) -> pd.Timestamp | None:
|
|
223
|
+
"""Convert an IRFCL label to the **real end of period**.
|
|
224
|
+
|
|
225
|
+
* ``"YYYY"`` → December 31 of that year.
|
|
226
|
+
* ``"YYYY-QN"`` → last day of the quarter.
|
|
227
|
+
* ``"YYYY-MNN"`` → last day of the month.
|
|
228
|
+
"""
|
|
229
|
+
try:
|
|
230
|
+
if "-Q" in label:
|
|
231
|
+
period = pd.Period(label.replace("-Q", "Q"), freq="Q")
|
|
232
|
+
return period.end_time.normalize()
|
|
233
|
+
if "-M" in label: # "YYYY-MNN"
|
|
234
|
+
year_str, month_str = label.split("-M")
|
|
235
|
+
period = pd.Period(
|
|
236
|
+
freq="M", year=int(year_str), month=int(month_str)
|
|
237
|
+
)
|
|
238
|
+
return period.end_time.normalize()
|
|
239
|
+
year = int(label)
|
|
240
|
+
return pd.Timestamp(year=year, month=12, day=31)
|
|
241
|
+
except (ValueError, TypeError):
|
|
242
|
+
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tesorotools-python
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.45
|
|
4
4
|
Requires-Python: >=3.13
|
|
5
5
|
Requires-Dist: babel>=2.17
|
|
6
6
|
Requires-Dist: matplotlib>=3.10
|
|
@@ -16,5 +16,8 @@ Provides-Extra: bde
|
|
|
16
16
|
Requires-Dist: requests>=2.31; extra == 'bde'
|
|
17
17
|
Provides-Extra: ecb
|
|
18
18
|
Requires-Dist: requests>=2.31; extra == 'ecb'
|
|
19
|
+
Provides-Extra: imf
|
|
20
|
+
Requires-Dist: requests>=2.31; extra == 'imf'
|
|
21
|
+
Requires-Dist: truststore>=0.10; extra == 'imf'
|
|
19
22
|
Provides-Extra: lseg
|
|
20
23
|
Requires-Dist: lseg-data>=2.1; extra == 'lseg'
|
|
@@ -39,10 +39,11 @@ tesorotools/pipeline/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
|
|
|
39
39
|
tesorotools/pipeline/diagnose.py,sha256=Ji_T3rAn-z4NhSdkpyPsnwQ1CTl4u5-1gnJZC8d1Sxg,1470
|
|
40
40
|
tesorotools/pipeline/engine.py,sha256=D4gKIv1bO6pw2gUMWJ7CVRv7e_kWWrWpZCzupeZCx0k,2929
|
|
41
41
|
tesorotools/pipeline/rules.py,sha256=OXi6dk2Gx63aO-SGjYnblhAah7tQIynNq2DKtfMGIEk,10041
|
|
42
|
-
tesorotools/providers/__init__.py,sha256=
|
|
42
|
+
tesorotools/providers/__init__.py,sha256=7iEs_BeNeVSMwzjsVd16b6GRmj3bgzMD30rNLEKwupo,1495
|
|
43
43
|
tesorotools/providers/base.py,sha256=6PA1HKhbc-Ehy27Ekm4wn-jWthEdL39Q9qxLCoqH_Jo,7666
|
|
44
44
|
tesorotools/providers/bde.py,sha256=wVfaKzgInTjThJ_twQ-ycA6FUMAKLx3BWL19P03LR0g,7226
|
|
45
45
|
tesorotools/providers/ecb.py,sha256=8UIW-NFLlGVIM7E2Sd_9UEWryt5wwilY3Hv13dnjY68,6422
|
|
46
|
+
tesorotools/providers/imf_irfcl.py,sha256=jGUDFGJBpNRsWKRI90BzI5AzIOmrSj6XwBS6ZUThc8A,8041
|
|
46
47
|
tesorotools/providers/lseg.py,sha256=paKdSxb1tWv26YCwMD0dEUfF4GLFJWzHukKII9WPges,30183
|
|
47
48
|
tesorotools/render/__init__.py,sha256=cqC5hoOwt_ZEE7gNG8nBbvf59YI99Lro8e18WsMKVT0,589
|
|
48
49
|
tesorotools/render/report.py,sha256=_7QnmDgUScRtUWSuaKYoTOVk2TTGUCkRWxAo1haxnek,1051
|
|
@@ -64,6 +65,6 @@ tesorotools/utils/matplotlib.py,sha256=lItlkJwJEDGALt0J1UZioTz2GOOFPPe7ffQ4Hzxl7
|
|
|
64
65
|
tesorotools/utils/series.py,sha256=AEf5DhneYjzQ0nvZD5b1IU3hop0Xgb3Bw2xWs_G3Lhw,1207
|
|
65
66
|
tesorotools/utils/shortcuts.py,sha256=9fpTLgfDFm6_1kh2W0Zk2atn3ZIZMvPfuOjlPpjVy4c,1718
|
|
66
67
|
tesorotools/utils/template.py,sha256=owYVB_dDlkIcKGmT6muH_4UjKDD7JYn_I1pxjQ9VIB8,5211
|
|
67
|
-
tesorotools_python-0.0.
|
|
68
|
-
tesorotools_python-0.0.
|
|
69
|
-
tesorotools_python-0.0.
|
|
68
|
+
tesorotools_python-0.0.45.dist-info/METADATA,sha256=fnpAvg45bw2GqNyvh3-V05ssOs4pYYfvMF3dzsyj36k,724
|
|
69
|
+
tesorotools_python-0.0.45.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
70
|
+
tesorotools_python-0.0.45.dist-info/RECORD,,
|
|
File without changes
|