""" Simulator baseline endpoint. Returns current market values to seed the EUR/USD causal simulator. Sources (priority): macro_gauge_snapshots → FRED economic_events → yfinance → hardcoded fallbacks. """ import json import logging from datetime import datetime from fastapi import APIRouter router = APIRouter() logger = logging.getLogger(__name__) _FALLBACK = { "fed_rate": 4.25, "ecb_rate": 3.65, "us_2y": 4.50, "us_10y": 4.30, "eu_2y": 2.80, "eu_10y": 2.60, "eurusd": 1.1450, "vix": 18.0, "oil": 80.0, "real_yield_us": 2.10, "pmi_us": 50.0, "pmi_eu": 50.0, } def _gauge_val(gauges: dict, key: str): g = gauges.get(key, {}) v = g.get("value") return float(v) if v is not None else None def _yf_last(sym: str): """Download last closing price for a single yfinance symbol.""" try: import yfinance as yf df = yf.download(sym, period="5d", interval="1d", progress=False, auto_adjust=True) if df is None or len(df) == 0: return None if hasattr(df.columns, "levels"): df.columns = df.columns.get_level_values(0) close = df["Close"].dropna() return float(close.iloc[-1]) if len(close) > 0 else None except Exception as e: logger.debug(f"[simulator/baseline] yf {sym}: {e}") return None @router.get("/api/simulator/baseline") def simulator_baseline(): """ Aggregate current market values for the EUR/USD simulator. Returns: fed_rate, ecb_rate, us_2y, eu_2y, eurusd, vix, oil, real_yield_us, sources{}, fetched_at. """ result = dict(_FALLBACK) sources: dict = {} # ── 1. Macro gauge snapshots (fast, already in DB) ────────────────────── try: from services.database import get_conn conn = get_conn() row = conn.execute( "SELECT gauges, snapshot_date FROM macro_gauge_snapshots ORDER BY snapshot_date DESC LIMIT 1" ).fetchone() conn.close() if row: raw = row["gauges"] gauges = raw if isinstance(raw, dict) else json.loads(raw or "{}") snap_date = (row["snapshot_date"] or "")[:10] v = _gauge_val(gauges, "vix") if v: result["vix"] = round(v, 1) sources["vix"] = f"snapshot {snap_date}" v = _gauge_val(gauges, "brent") if v: result["oil"] = round(v, 1) sources["oil"] = f"snapshot {snap_date}" # US 10Y from gauge snapshot (yfinance will override if available) v = _gauge_val(gauges, "us10y") if v: result["us_10y"] = round(v, 2) sources["us_10y"] = f"snapshot {snap_date}" except Exception as e: logger.debug(f"[simulator/baseline] gauge query: {e}") # ── 2. FRED economic_events (policy rates, real yield) ────────────────── try: from services.database import get_conn conn = get_conn() for series_id, field in [ ("FEDFUNDS", "fed_rate"), ("DFF", "fed_rate"), # daily effective fed funds (fallback) ("ECBDFR", "ecb_rate"), # ECB deposit facility rate ("DFII10", "real_yield_us"),# 10-year TIPS yield (real yield) ]: if field in sources: continue row = conn.execute( "SELECT actual_value FROM economic_events WHERE series_id=? ORDER BY event_date DESC LIMIT 1", (series_id,), ).fetchone() if row and row[0] is not None: result[field] = round(float(row[0]), 2) sources[field] = f"FRED {series_id}" conn.close() except Exception as e: logger.debug(f"[simulator/baseline] FRED query: {e}") # ── 3. yfinance live quotes ───────────────────────────────────────────── try: # EURUSD spot v = _yf_last("EURUSD=X") if v: result["eurusd"] = round(v, 4) sources["eurusd"] = "yfinance EURUSD=X" # US Treasury yields (yfinance returns annualised %) irx = _yf_last("^IRX") # 13-week T-bill (0.25Y) fvx = _yf_last("^FVX") # 5-year T-note tnx = _yf_last("^TNX") # 10-year T-note if irx and fvx: # Linear interpolation for 2Y on [0.25Y, 5Y] segment # position of 2Y: (2 - 0.25) / (5 - 0.25) ≈ 0.368 us2y = irx + 0.368 * (fvx - irx) result["us_2y"] = round(us2y, 2) sources["us_2y"] = "yfinance ^IRX+^FVX interp" elif tnx: result["us_2y"] = round(tnx * 0.92, 2) # rough proxy sources["us_2y"] = "yfinance ^TNX proxy" # US 10Y (^TNX) if tnx: result["us_10y"] = round(tnx, 2) sources["us_10y"] = "yfinance ^TNX" # Real yield: 10Y nominal minus 10Y breakeven inflation via TIPS if tnx and "real_yield_us" not in sources: # TIPS yield approximation: nominal - 2.3% (rough breakeven) result["real_yield_us"] = round(tnx - 2.3, 2) sources["real_yield_us"] = "yfinance ^TNX - breakeven approx" # EU 2Y — German Schatz (not always available on yfinance) eu2y = _yf_last("DE2YT=RR") if eu2y: result["eu_2y"] = round(eu2y, 2) sources["eu_2y"] = "yfinance DE2YT=RR" else: # Approximation: ECB rate + 15 bps market premium result["eu_2y"] = round(result["ecb_rate"] + 0.15, 2) sources["eu_2y"] = "ECB rate +15bps approx" # EU 10Y — German Bund 10Y eu10y = _yf_last("GE10YT=RR") if eu10y: result["eu_10y"] = round(eu10y, 2) sources["eu_10y"] = "yfinance GE10YT=RR" else: # Approximation: ECB rate + term premium ~200bps result["eu_10y"] = round(result["ecb_rate"] + 2.0, 2) sources["eu_10y"] = "ECB rate +200bps approx" # VIX — override gauge if not already set from DB if "vix" not in sources: v = _yf_last("^VIX") if v: result["vix"] = round(v, 1) sources["vix"] = "yfinance ^VIX" # Brent — override gauge if not already set if "oil" not in sources: v = _yf_last("BZ=F") if v: result["oil"] = round(v, 1) sources["oil"] = "yfinance BZ=F" except Exception as e: logger.debug(f"[simulator/baseline] yfinance block: {e}") # ── Cleanup & return ──────────────────────────────────────────────────── result["sources"] = sources result["fetched_at"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") return result