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

141 lines
3.9 KiB
Python

"""Copper/Gold ratio and RORO-style risk composite (VIX + bond vol proxy)."""
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional
import numpy as np
import pandas as pd
from server.services.cache import cached
_TICK_VIX = "^VIX"
_TICK_MOVE = "^MOVE"
_TICK_TLT = "TLT"
_TICK_HG = "HG=F"
_TICK_GC = "GC=F"
_ROLL_Z = 252
_MA20 = 20
def _yf_close(ticker: str, period: str = "2y") -> pd.Series:
try:
import yfinance as yf
t = yf.Ticker(ticker)
hist = t.history(period=period, auto_adjust=True)
if hist is None or hist.empty or "Close" not in hist:
return pd.Series(dtype=float)
s = hist["Close"].astype(float)
s.index = pd.to_datetime(s.index).tz_localize(None)
return s.sort_index()
except Exception:
return pd.Series(dtype=float)
def _rolling_zscore_last(s: pd.Series, window: int = _ROLL_Z) -> Optional[float]:
if s.empty or len(s) < window:
return None
tail = s.iloc[-window:]
last = float(tail.iloc[-1])
mu = float(tail.mean())
sigma = float(tail.std())
if sigma == 0 or np.isnan(sigma):
return 0.0
z = (last - mu) / sigma
return float(z)
def _tlt_realized_vol_z() -> Optional[float]:
px = _yf_close(_TICK_TLT)
if px.empty or len(px) < _ROLL_Z + 5:
return None
ret = px.pct_change().dropna()
rv = ret.rolling(20).std() * (252 ** 0.5) * 100.0
rv = rv.dropna()
return _rolling_zscore_last(rv)
def _roro_label(z: float) -> str:
z = max(-3.0, min(3.0, z))
if z <= -1.5:
return "Extreme Fear"
if z <= -0.5:
return "Fear"
if z <= 0.5:
return "Neutral"
if z <= 1.5:
return "Greed"
return "Extreme Greed"
@cached("smart_money_snapshot", ttl_seconds=900)
def get_smart_money_snapshot() -> Dict[str, Any]:
"""Copper/Gold trend + RORO Z from VIX and MOVE (or TLT vol Z)."""
out: Dict[str, Any] = {
"updated_at": datetime.utcnow().isoformat() + "Z",
"roro_z": None,
"roro_label": None,
"copper_gold": [],
"components": {"vix_z": None, "bond_vol_z": None, "bond_source": None},
"error": None,
}
vix = _yf_close(_TICK_VIX)
move = _yf_close(_TICK_MOVE)
vix_z = _rolling_zscore_last(vix) if not vix.empty else None
bond_z: Optional[float] = None
bond_src = None
if not move.empty and len(move) >= _ROLL_Z:
bond_z = _rolling_zscore_last(move)
bond_src = "MOVE"
if bond_z is None:
bond_z = _tlt_realized_vol_z()
bond_src = "TLT_RV"
out["components"]["vix_z"] = round(vix_z, 4) if vix_z is not None else None
out["components"]["bond_vol_z"] = round(bond_z, 4) if bond_z is not None else None
out["components"]["bond_source"] = bond_src
if vix_z is not None and bond_z is not None:
roro = (vix_z + bond_z) / 2.0
elif vix_z is not None:
roro = vix_z
elif bond_z is not None:
roro = bond_z
else:
out["error"] = "insufficient_data"
return out
out["roro_z"] = round(float(roro), 4)
out["roro_label"] = _roro_label(float(roro))
hg = _yf_close(_TICK_HG)
gc = _yf_close(_TICK_GC)
if hg.empty or gc.empty:
return out
df = pd.DataFrame({"hg": hg, "gc": gc}).dropna()
if df.empty:
return out
df["ratio"] = df["hg"] / df["gc"]
df["ratio_ma20"] = df["ratio"].rolling(_MA20).mean()
cg: List[Dict[str, Any]] = []
for idx, row in df.iterrows():
if pd.isna(row["ratio"]):
continue
item: Dict[str, Any] = {
"date": idx.strftime("%Y-%m-%d"),
"ratio": round(float(row["ratio"]), 6),
}
if not pd.isna(row.get("ratio_ma20")):
item["ratio_ma20"] = round(float(row["ratio_ma20"]), 6)
cg.append(item)
out["copper_gold"] = cg[-520:] if len(cg) > 520 else cg
return out