Files
All-in-one-Financial-Analysis/atlas-terminal/server/services/ecos_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

178 lines
4.8 KiB
Python

"""Korea Bank (ECOS) economic data fetcher."""
from __future__ import annotations
from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
from server.services.cache import cached
ECOS_BASE = "https://ecos.bok.or.kr/api"
KOREA_INDICATORS = [
{
"key": "policy_rate",
"label": "\ud55c\uad6d\uc740\ud589 \uae30\uc900\uae08\ub9ac",
"table": "722Y001",
"item": "0101000",
"cycle": "M",
"unit": "%",
},
{
"key": "cpi_yoy",
"label": "\uc18c\ube44\uc790\ubb3c\uac00 \uc0c1\uc2b9\ub960",
"table": "901Y009",
"item": "0",
"cycle": "M",
"unit": "%",
},
{
"key": "gdp_growth",
"label": "\uc2e4\uc9c8 GDP \uc131\uc7a5\ub960",
"table": "200Y002",
"item": "10111",
"cycle": "Q",
"unit": "%",
},
{
"key": "exports_yoy",
"label": "\uc218\ucd9c \uc99d\uac10\ub960",
"table": "403Y001",
"item": "1",
"cycle": "M",
"unit": "%",
},
{
"key": "usd_krw",
"label": "\uc6d0/\ub2ec\ub7ec \ud658\uc728",
"table": "731Y003",
"item": "0000001",
"cycle": "M",
"unit": "\u20a9",
},
{
"key": "cli",
"label": "\uacbd\uae30\uc120\ud589\uc9c0\uc218",
"table": "101Y018",
"item": "I16A",
"cycle": "M",
"unit": "pt",
},
]
def _fetch_ecos_indicator(
api_key: str,
table_code: str,
item_code: str,
cycle: str,
count: int = 3,
) -> List[Dict[str, Any]]:
"""Fetch the latest values from ECOS API."""
import requests
now = datetime.utcnow()
if cycle == "Q":
end_date = now.strftime("%Y%m")
start_date = (now - timedelta(days=365 * 2)).strftime("%Y%m")
freq = "Q"
else:
end_date = now.strftime("%Y%m")
start_date = (now - timedelta(days=365)).strftime("%Y%m")
freq = "M"
url = (
f"{ECOS_BASE}/StatisticSearch/{api_key}/json/kr/1/{count * 4}"
f"/{table_code}/{freq}/{start_date}/{end_date}/{item_code}"
)
try:
resp = requests.get(url, timeout=12)
data = resp.json()
rows = data.get("StatisticSearch", {}).get("row", [])
result = []
for row in rows[-count:]:
val = row.get("DATA_VALUE")
time_str = row.get("TIME", "")
try:
result.append({"date": time_str, "value": float(val)})
except (TypeError, ValueError):
continue
return result
except Exception:
return []
def _ecos_fallback_yfinance() -> Dict[str, Any]:
"""Minimal fallback using yfinance for USD/KRW and KOSPI."""
import yfinance as yf
indicators: List[Dict[str, Any]] = []
try:
krw = yf.Ticker("KRW=X")
hist = krw.history(period="5d")
if hist is not None and not hist.empty:
rate = float(hist["Close"].iloc[-1])
indicators.append({
"key": "usd_krw",
"label": "\uc6d0/\ub2ec\ub7ec \ud658\uc728",
"value": round(rate, 2),
"unit": "\u20a9",
"prev": None,
"direction": None,
})
except Exception:
pass
return {
"updated_at": datetime.utcnow().isoformat() + "Z",
"indicators": indicators,
"source": "yfinance_fallback",
}
@cached("korea_ecos_snapshot", ttl_seconds=3600)
def _fetch_korea_cached(api_key: str) -> Dict[str, Any]:
indicators: List[Dict[str, Any]] = []
for spec in KOREA_INDICATORS:
values = _fetch_ecos_indicator(
api_key, spec["table"], spec["item"], spec["cycle"], count=3,
)
if not values:
continue
latest = values[-1]
prev = values[-2] if len(values) >= 2 else None
direction = None
if prev is not None:
if latest["value"] > prev["value"]:
direction = "up"
elif latest["value"] < prev["value"]:
direction = "down"
else:
direction = "flat"
indicators.append({
"key": spec["key"],
"label": spec["label"],
"value": round(latest["value"], 2),
"unit": spec["unit"],
"prev": round(prev["value"], 2) if prev else None,
"direction": direction,
})
return {
"updated_at": datetime.utcnow().isoformat() + "Z",
"indicators": indicators,
"source": "ecos",
}
def get_korea_snapshot(api_key: Optional[str] = None) -> Dict[str, Any]:
"""Return Korean economic indicators from ECOS, or yfinance fallback."""
if not api_key:
return _ecos_fallback_yfinance()
return _fetch_korea_cached(api_key)