mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-20 14:18:05 +00:00
- 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>
178 lines
4.8 KiB
Python
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)
|