Files
All-in-one-Financial-Analysis/atlas-terminal/server/services/oecd_cycle.py
T

129 lines
3.8 KiB
Python
Raw Normal View History

"""OECD leading indicator service via DBnomics."""
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional
from server.services.cache import cached
OECD_COUNTRIES = {
"USA": "United States",
"EA": "Euro Area",
"GBR": "United Kingdom",
"DEU": "Germany",
"FRA": "France",
"JPN": "Japan",
"KOR": "South Korea",
"CHN": "China",
"IND": "India",
"BRA": "Brazil",
}
_DATASET = "DSD_STES@DF_CLI"
def _fetch_cli(iso: str, limit: int = 36) -> List[Dict[str, Any]]:
"""Fetch CLI (normalized) for a country from DBnomics."""
code = f"{iso}.M.LI.IX._Z.NOR.IX._Z.H"
return _fetch_dbn(_DATASET, code, limit)
def _fetch_bci(iso: str) -> Optional[float]:
code = f"{iso}.M.BCICP.IX._Z.AA.IX._Z.H"
series = _fetch_dbn(_DATASET, code, 3)
return series[-1]["value"] if series else None
def _fetch_cci(iso: str) -> Optional[float]:
code = f"{iso}.M.CCICP.IX._Z.AA.IX._Z.H"
series = _fetch_dbn(_DATASET, code, 3)
return series[-1]["value"] if series else None
def _fetch_dbn(dataset: str, series_code: str, limit: int) -> List[Dict[str, Any]]:
"""Fetch from DBnomics, with REST fallback."""
try:
import dbnomics
df = dbnomics.fetch_series(
provider_code="OECD", dataset_code=dataset, series_code=series_code,
)
if df is None or df.empty:
return []
df = df.dropna(subset=["value"]).tail(limit)
return [
{"date": str(row["period"])[:7], "value": round(float(row["value"]), 4)}
for _, row in df.iterrows()
]
except Exception:
return _fetch_dbn_rest(dataset, series_code, limit)
def _fetch_dbn_rest(dataset: str, series_code: str, limit: int) -> List[Dict[str, Any]]:
"""REST fallback when the dbnomics package fails."""
import requests
url = f"https://api.db.nomics.world/v22/series/OECD/{dataset}/{series_code}?observations=1&limit=1"
try:
resp = requests.get(url, timeout=15)
data = resp.json()
docs = data.get("series", {}).get("docs", [])
if not docs:
return []
obs = docs[0]
periods = obs.get("period", [])
values = obs.get("value", [])
result = []
for p, v in zip(periods[-limit:], values[-limit:]):
if v is not None and str(v) != "NA":
try:
result.append({"date": str(p)[:7], "value": round(float(v), 4)})
except (TypeError, ValueError):
continue
return result
except Exception:
return []
def _direction(series: List[Dict[str, Any]]) -> str:
if len(series) < 2:
return "unknown"
last = series[-1]["value"]
prev = series[-2]["value"]
if last > 100 and last > prev:
return "expanding"
if last < 100 and last < prev:
return "contracting"
if last > prev:
return "recovering"
return "slowing"
@cached("oecd_cli_snapshot", ttl_seconds=21600)
def get_oecd_cli_snapshot() -> Dict[str, Any]:
"""Fetch OECD CLI for major economies via DBnomics."""
countries: List[Dict[str, Any]] = []
for iso, name in OECD_COUNTRIES.items():
series = _fetch_cli(iso, limit=36)
cli_current = series[-1]["value"] if series else None
cli_prev = series[-2]["value"] if len(series) >= 2 else None
bci = _fetch_bci(iso)
cci = _fetch_cci(iso)
countries.append({
"country": name,
"iso": iso,
"cli_current": cli_current,
"cli_prev": cli_prev,
"direction": _direction(series) if series else "unknown",
"bci": bci,
"cci": cci,
"series": series,
})
return {
"updated_at": datetime.utcnow().isoformat() + "Z",
"countries": countries,
}