Files
All-in-one-Financial-Analysis/atlas-terminal/server/services/macro_fetcher.py
T
shawnkim1997andClaude Opus 4.6 51cbaf7f8d feat: major codebase audit — 21 routers, 37 services, 12 pages fully documented
- Add missing numpy, scipy, dbnomics to requirements.txt (fixes ImportError on fresh install)
- Sync claude.md with actual codebase: §3 file structure (37 services, 21 routers),
  §5 API endpoints (92 routes), §6 frontend pages (12), §13 TODO status
- Update README.md with current architecture (92 API routes, 21 routers, 37 services),
  multi-asset overview, research grid, macro dashboard, screener+backtest,
  multi-jurisdiction filings, and 2026-03-26 changelog entry
- Add new routers: dart, edinet, fmp, macro, research
- Add new services: cache, dart_fetcher, dart_filing_service, economic_calendar,
  ecos_fetcher, edinet_filing_service, fmp_client, global_macro_quadrant,
  kpi_history_service, macro_cycle, macro_fetcher, oecd_cycle,
  peer_comparison_service, research_dashboard, smart_money_service, yield_fx_service
- Add new frontend: macro page, screener+backtest, research grid components,
  overview (Equity/ETF/Commodity), filings (SEC/DART/EDINET), error boundaries
- Remove 6 unused services: copilot_context, crypto_fetcher, fx_fetcher,
  gemini_analysis, market_data, technical_analysis
- Remove obsolete docs: .agent/, AGENT.md, ATLAS_EVALUATION.md, docs/

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-26 21:39:07 +00:00

133 lines
3.8 KiB
Python

"""Macro series: FRED (public CSV), optional ECOS (Bank of Korea).
FRED uses ``https://fred.stlouisfed.org/graph/fredgraph.csv`` (no API key) so we
avoid pandas-datareader / legacy dependencies on Python 3.12+.
"""
from __future__ import annotations
import csv
import io
import os
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
from urllib.request import Request, urlopen
from server.utils.safe_float import _safe_float
_FRED_CSV = "https://fred.stlouisfed.org/graph/fredgraph.csv"
_USER_AGENT = "ATLAS-Terminal/1.0 (macro)"
def _parse_iso(s: Optional[str], default: datetime) -> datetime:
if not s:
return default
try:
return datetime.strptime(s[:10], "%Y-%m-%d")
except ValueError:
return default
def fetch_fred_series(
series_id: str,
start: Optional[str] = None,
end: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""Download a FRED series via the public graph CSV export."""
sid = (series_id or "").strip().upper()
if not sid:
return []
end_dt = _parse_iso(end, datetime.utcnow())
start_dt = _parse_iso(start, end_dt - timedelta(days=365 * 10))
url = f"{_FRED_CSV}?id={sid}"
try:
req = Request(url, headers={"User-Agent": _USER_AGENT})
with urlopen(req, timeout=45) as resp:
text = resp.read().decode("utf-8", errors="replace")
except Exception:
return []
rows: List[Dict[str, Any]] = []
reader = csv.DictReader(io.StringIO(text))
value_col = sid
for rec in reader:
ds = (rec.get("observation_date") or rec.get("DATE") or "").strip()[:10]
if not ds:
continue
try:
dt = datetime.strptime(ds, "%Y-%m-%d")
except ValueError:
continue
if dt < start_dt or dt > end_dt:
continue
raw = rec.get(value_col) or rec.get(sid) or next(
(rec[k] for k in rec if k not in ("observation_date", "DATE") and rec.get(k)),
"",
)
fv = _safe_float(str(raw).replace(",", ""))
if fv is not None:
rows.append({"date": ds, "value": fv})
return rows
def fetch_oecd_mei() -> List[Dict[str, Any]]:
"""OECD MEI via pandas-datareader was removed (Py3.13+). Return empty for now."""
return []
def fetch_ecos_series(
stat_code: str,
cycle: str = "M",
start_ym: str = "201501",
end_ym: Optional[str] = None,
) -> List[Dict[str, Any]]:
"""Bank of Korea ECOS Open API (requires ``ECOS_API_KEY``)."""
import json
import urllib.parse
import urllib.request
key = (os.getenv("ECOS_API_KEY") or "").strip()
if not key:
return []
code = (stat_code or "").strip()
if not code:
return []
end_m = end_ym or datetime.utcnow().strftime("%Y%m")
path = (
f"/api/StatisticSearch/{urllib.parse.quote(key, safe='')}/json/kr/1/1000/"
f"{code}/{cycle}/{start_ym}/{end_m}/"
)
url = "https://ecos.bok.or.kr" + path
try:
req = urllib.request.Request(url, headers={"User-Agent": _USER_AGENT})
with urllib.request.urlopen(req, timeout=30) as resp:
raw = resp.read().decode("utf-8", errors="replace")
except Exception:
return []
try:
payload = json.loads(raw)
except json.JSONDecodeError:
return []
row_block = (
payload.get("StatisticSearch", {}).get("row", [])
)
if not isinstance(row_block, list):
return []
out: List[Dict[str, Any]] = []
for r in row_block:
if not isinstance(r, dict):
continue
t = r.get("TIME") or r.get("time")
v = r.get("DATA_VALUE") or r.get("DATA")
fv = _safe_float(v)
if t and fv is not None:
out.append({"period": str(t), "value": fv})
return out