Modèle causal refactorisé en deux canaux distincts : - Canal taux (solid) : variations taux directeurs → principalement 2Y (ancrage court terme) - Canal ton/anticipations (tirets) : discours CB + surprises CPI/NFP → principalement 10Y (anticipations long terme) Slider : clic sur la valeur affichée → input éditable (Enter/Blur pour valider, Escape pour annuler) Backend : endpoint /api/simulator/baseline expose us_10y et eu_10y (yfinance ^TNX + GE10YT=RR) SVG mis à jour : 4 nœuds de taux (2Y/10Y US+EU), 2 nœuds différentiels, flèches solides/tirets Strip métrique : 6 cellules (US 2Y, US 10Y, Bund 2Y, Bund 10Y, Δ 2Y, Δ 10Y) Décomposition : séparation "Δ 2Y — taux directeurs" / "Δ 10Y — anticipations/ton" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
193 lines
7.0 KiB
Python
193 lines
7.0 KiB
Python
"""
|
|
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
|