mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-17 20:38:06 +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>
211 lines
6.2 KiB
Python
211 lines
6.2 KiB
Python
"""Global macro quadrant: growth vs inflation momentum (Z-scores) for major economies."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
|
|
from server.services.macro_fetcher import fetch_fred_series
|
|
from server.services.oecd_cycle import _fetch_cli
|
|
|
|
_LOOKBACK_MOM = 60
|
|
_MIN_MOMENTS = 6
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _GrowthSource:
|
|
kind: str # "fred_level" | "oecd_cli"
|
|
fred_id: Optional[str] = None
|
|
oecd_iso: Optional[str] = None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _InflationSource:
|
|
fred_id: str
|
|
is_index: bool
|
|
|
|
|
|
# US, Eurozone, Japan, China, South Korea — FRED where available; OECD CLI fallback for growth.
|
|
_COUNTRY_SPECS: List[Dict[str, Any]] = [
|
|
{
|
|
"id": "US",
|
|
"label": "United States",
|
|
"growth": _GrowthSource("fred_level", fred_id="ISM/MAN_PMI"),
|
|
"inflation": _InflationSource("CPIAUCSL", True),
|
|
},
|
|
{
|
|
"id": "EU",
|
|
"label": "Eurozone",
|
|
"growth": _GrowthSource("fred_level", fred_id="EMUSTRM"),
|
|
"inflation": _InflationSource("CP0000EZ19M086NEST", True),
|
|
},
|
|
{
|
|
"id": "JP",
|
|
"label": "Japan",
|
|
"growth": _GrowthSource("fred_level", fred_id="JPNPMIMA", oecd_iso="JPN"),
|
|
"inflation": _InflationSource("JPNCPIALLMINMEI", True),
|
|
},
|
|
{
|
|
"id": "CN",
|
|
"label": "China",
|
|
"growth": _GrowthSource("oecd_cli", oecd_iso="CHN"),
|
|
"inflation": _InflationSource("CHNCPIALLMINMEI", True),
|
|
},
|
|
{
|
|
"id": "KR",
|
|
"label": "South Korea",
|
|
"growth": _GrowthSource("oecd_cli", oecd_iso="KOR"),
|
|
"inflation": _InflationSource("KORCPIALLMINMEI", True),
|
|
},
|
|
]
|
|
|
|
|
|
def _fred_to_series(rows: List[Dict[str, Any]]) -> pd.Series:
|
|
if not rows:
|
|
return pd.Series(dtype=float)
|
|
df = pd.DataFrame(rows)
|
|
df["dt"] = pd.to_datetime(df["date"])
|
|
df = df.sort_values("dt").drop_duplicates("dt", keep="last")
|
|
s = pd.Series(df["value"].astype(float).values, index=df["dt"])
|
|
return s.sort_index()
|
|
|
|
|
|
def _pct_yoy_monthly(s: pd.Series) -> pd.Series:
|
|
if s.empty or len(s) < 13:
|
|
return pd.Series(dtype=float)
|
|
return s.pct_change(12) * 100.0
|
|
|
|
|
|
def _oecd_cli_series(iso: str, limit: int = 120) -> pd.Series:
|
|
raw = _fetch_cli(iso, limit=limit)
|
|
if not raw:
|
|
return pd.Series(dtype=float)
|
|
recs = []
|
|
for r in raw:
|
|
p = str(r.get("date", ""))[:7]
|
|
if len(p) < 7:
|
|
continue
|
|
dt = pd.Timestamp(p + "-01")
|
|
v = r.get("value")
|
|
if v is None:
|
|
continue
|
|
try:
|
|
recs.append((dt, float(v)))
|
|
except (TypeError, ValueError):
|
|
continue
|
|
if not recs:
|
|
return pd.Series(dtype=float)
|
|
recs.sort(key=lambda x: x[0])
|
|
idx = [x[0] for x in recs]
|
|
vals = [x[1] for x in recs]
|
|
return pd.Series(vals, index=idx).sort_index()
|
|
|
|
|
|
def _three_month_momentum(s: pd.Series) -> pd.Series:
|
|
if s.empty or len(s) < 4:
|
|
return pd.Series(dtype=float)
|
|
return s - s.shift(3)
|
|
|
|
|
|
def _zscore_last(momentum: pd.Series, lookback: int = _LOOKBACK_MOM) -> Tuple[Optional[float], Optional[float]]:
|
|
"""Return (z-score of last momentum, last momentum value)."""
|
|
mom = momentum.dropna()
|
|
if len(mom) < _MIN_MOMENTS:
|
|
return None, None
|
|
tail = mom.iloc[-lookback:]
|
|
last = float(tail.iloc[-1])
|
|
arr = tail.values.astype(float)
|
|
mean = float(np.mean(arr))
|
|
std = float(np.std(arr))
|
|
if std == 0 or np.isnan(std):
|
|
return 0.0, last
|
|
z = float((last - mean) / std)
|
|
return z, last
|
|
|
|
|
|
def _quadrant_label(gz: float, iz: float) -> str:
|
|
gpos = gz > 0
|
|
ipos = iz > 0
|
|
if gpos and ipos:
|
|
return "Reflation"
|
|
if gpos and not ipos:
|
|
return "Recovery"
|
|
if not gpos and ipos:
|
|
return "Stagflation"
|
|
return "Overheat"
|
|
|
|
|
|
def _growth_series_resolved(spec: _GrowthSource) -> pd.Series:
|
|
if spec.kind == "fred_level" and spec.fred_id:
|
|
s = _fred_to_series(fetch_fred_series(spec.fred_id))
|
|
if not s.empty:
|
|
return s
|
|
if spec.kind == "oecd_cli" and spec.oecd_iso:
|
|
return _oecd_cli_series(spec.oecd_iso)
|
|
if spec.oecd_iso:
|
|
return _oecd_cli_series(spec.oecd_iso)
|
|
return pd.Series(dtype=float)
|
|
|
|
|
|
def _inflation_series(src: _InflationSource) -> pd.Series:
|
|
rows = fetch_fred_series(src.fred_id)
|
|
s = _fred_to_series(rows)
|
|
if s.empty:
|
|
return pd.Series(dtype=float)
|
|
if src.is_index:
|
|
return _pct_yoy_monthly(s)
|
|
return s
|
|
|
|
|
|
def _compute_country_point(spec_row: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
gspec: _GrowthSource = spec_row["growth"]
|
|
ispec: _InflationSource = spec_row["inflation"]
|
|
|
|
g = _growth_series_resolved(gspec)
|
|
inf = _inflation_series(ispec)
|
|
if g.empty or inf.empty:
|
|
return None
|
|
|
|
g_mom = _three_month_momentum(g)
|
|
i_mom = _three_month_momentum(inf)
|
|
|
|
gz, g_last = _zscore_last(g_mom)
|
|
iz, i_last = _zscore_last(i_mom)
|
|
if gz is None or iz is None:
|
|
return None
|
|
|
|
return {
|
|
"id": spec_row["id"],
|
|
"label": spec_row["label"],
|
|
"growth_z": round(gz, 4),
|
|
"inflation_z": round(iz, 4),
|
|
"growth_momentum": round(g_last, 4) if g_last is not None else None,
|
|
"inflation_momentum": round(i_last, 4) if i_last is not None else None,
|
|
"quadrant": _quadrant_label(gz, iz),
|
|
}
|
|
|
|
|
|
def get_global_macro_quadrant() -> Dict[str, Any]:
|
|
"""Return scatter payload for global growth/inflation quadrant."""
|
|
points: List[Dict[str, Any]] = []
|
|
order = {row["id"]: i for i, row in enumerate(_COUNTRY_SPECS)}
|
|
with ThreadPoolExecutor(max_workers=5) as pool:
|
|
futures = {pool.submit(_compute_country_point, row): row for row in _COUNTRY_SPECS}
|
|
for fut in as_completed(futures):
|
|
try:
|
|
p = fut.result()
|
|
if p:
|
|
points.append(p)
|
|
except Exception:
|
|
continue
|
|
points.sort(key=lambda x: order.get(x.get("id", ""), 99))
|
|
return {
|
|
"updated_at": datetime.utcnow().isoformat() + "Z",
|
|
"points": points,
|
|
}
|