diff --git a/backend/routers/simulator.py b/backend/routers/simulator.py index 35cf163..4eed2cd 100644 --- a/backend/routers/simulator.py +++ b/backend/routers/simulator.py @@ -15,7 +15,9 @@ _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, @@ -79,10 +81,11 @@ def simulator_baseline(): result["oil"] = round(v, 1) sources["oil"] = f"snapshot {snap_date}" - # us10y raw value is the yield in % → keep as reference + # US 10Y from gauge snapshot (yfinance will override if available) v = _gauge_val(gauges, "us10y") if v: - result["_us10y_gauge"] = round(v, 2) + 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}") @@ -135,8 +138,12 @@ def simulator_baseline(): 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 - # ^TNX = 10Y nominal; ^FVX gives us a rough real yield proxy 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) @@ -152,6 +159,16 @@ def simulator_baseline(): 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") @@ -170,7 +187,6 @@ def simulator_baseline(): logger.debug(f"[simulator/baseline] yfinance block: {e}") # ── Cleanup & return ──────────────────────────────────────────────────── - result.pop("_us10y_gauge", None) result["sources"] = sources result["fetched_at"] = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") return result diff --git a/frontend/src/pages/EuroSimulator.tsx b/frontend/src/pages/EuroSimulator.tsx index 5e3731f..0b63fbf 100644 --- a/frontend/src/pages/EuroSimulator.tsx +++ b/frontend/src/pages/EuroSimulator.tsx @@ -20,24 +20,31 @@ interface Params { vix: number // 10–60 oil: number // 40–130 real_yield_us: number // –1 … 4 % - // Base anchors (not displayed as sliders — set from live data) - us_2y: number // US 2Y yield reference - eu_2y: number // EU 2Y yield reference - eurusd: number // EURUSD base price + // Base anchors (set from live data, not displayed as sliders) + us_2y: number + us_10y: number + eu_2y: number + eu_10y: number + eurusd: number } interface ModelResult { - fed_pressure: number - ecb_pressure: number + fed_rate_pressure: number // rate channel signal (→ 2Y primarily) + fed_fwd_signal: number // tone/data channel signal (→ 10Y primarily) + ecb_rate_pressure: number + ecb_fwd_signal: number us_2y_implied: number + us_10y_implied: number eu_2y_implied: number - rate_diff: number + eu_10y_implied: number + rate_diff_2y: number + rate_diff_10y: number eurusd: number delta_pips: number contribs: { label: string; pips: number; color: 'red' | 'green' | 'slate' }[] } -// ── Hardcoded fallback (used before live data arrives) ───────────────────────── +// ── Hardcoded fallback ───────────────────────────────────────────────────────── const FALLBACK: Params = { fed_rate: 4.25, ecb_rate: 3.65, @@ -45,84 +52,101 @@ const FALLBACK: Params = { cpi_us_surprise: 0, cpi_eu_surprise: 0, nfp_surprise: 0, pmi_us: 50, pmi_eu: 50, - vix: 18, oil: 80, - real_yield_us: 2.1, - us_2y: 4.50, eu_2y: 2.80, + vix: 18, oil: 80, real_yield_us: 2.1, + us_2y: 4.50, us_10y: 4.30, + eu_2y: 2.80, eu_10y: 2.60, eurusd: 1.1450, } // ── Causal model ─────────────────────────────────────────────────────────────── -// All deltas are computed vs the base anchors embedded in `base`. -// Surprises / tones / PMI are always centered at their neutral value (0 / 50). +// +// Two distinct transmission channels: +// 1. Rate channel (fait accompli → anchors short end): +// fed_rate change → US 2Y (coefficient ~0.085) +// 2. Expectation/tone channel (forward guidance → long end): +// fed_tone + CPI/NFP surprises → US 10Y (coefficient ~0.070) +// with residual bleed into 2Y (~0.030) and 10Y from rate (~0.035) +// +// EUR/USD pip contributions: +// Δ 2Y differential → −500 pips / 1% spread widening +// Δ 10Y differential → −200 pips / 1% spread widening +// PMI diff, VIX, real yield, oil as secondary channels function compute(p: Params, base: Params): ModelResult { - const BASE_US_2Y = base.us_2y - const BASE_EU_2Y = base.eu_2y - const BASE_EURUSD = base.eurusd + // ── Rate pressure (current policy rate, already priced → hits 2Y hard) ───── + const fed_rate_pressure = (p.fed_rate - base.fed_rate) / 0.25 // units of 25bps + const ecb_rate_pressure = (p.ecb_rate - base.ecb_rate) / 0.25 - // Fed pressure: positive = hawkish (USD up, EUR/USD down) - const fed_pressure = - (p.fed_rate - base.fed_rate) / 0.25 * 1.0 - - p.fed_tone * 1.5 - + p.cpi_us_surprise / 0.1 * 0.7 - + p.nfp_surprise / 100 * 0.45 + // ── Forward guidance / expectations (tone + data → shapes future path) ────── + // Positive = hawkish signal for USD (tone: +3 dovish → signal = −3*1.2 → negative → dovish) + const fed_fwd_signal = + -p.fed_tone * 1.2 + + p.cpi_us_surprise / 0.1 * 0.50 // hot CPI → more hikes expected + + p.nfp_surprise / 100 * 0.35 // strong jobs → Fed stays restrictive - // ECB pressure: positive = hawkish (EUR up, EUR/USD up) - const ecb_pressure = - (p.ecb_rate - base.ecb_rate) / 0.25 * 1.0 - - p.ecb_tone * 1.5 - + p.cpi_eu_surprise / 0.1 * 0.55 - + (p.pmi_eu - 50) / 5 * 0.25 + const ecb_fwd_signal = + -p.ecb_tone * 1.2 + + p.cpi_eu_surprise / 0.1 * 0.45 + + (p.pmi_eu - 50) / 5 * 0.22 - // Implied 2Y yields - const us_2y_implied = BASE_US_2Y + fed_pressure * 0.09 - const eu_2y_implied = BASE_EU_2Y + ecb_pressure * 0.08 + // ── Implied yields ───────────────────────────────────────────────────────── + // 2Y: 90% anchored by current rate, 10% by near-term guidance + const us_2y_delta = fed_rate_pressure * 0.085 + fed_fwd_signal * 0.030 + const eu_2y_delta = ecb_rate_pressure * 0.080 + ecb_fwd_signal * 0.025 - // Rate differential delta vs baseline - const rate_diff_delta = (us_2y_implied - eu_2y_implied) - (BASE_US_2Y - BASE_EU_2Y) - const rate_diff = us_2y_implied - eu_2y_implied + // 10Y: 35% by current rate (level shift), 65% by long-term expectations + const us_10y_delta = fed_rate_pressure * 0.035 + fed_fwd_signal * 0.070 + (p.pmi_us - 50) / 50 * 0.012 + const eu_10y_delta = ecb_rate_pressure * 0.030 + ecb_fwd_signal * 0.060 + (p.pmi_eu - 50) / 50 * 0.012 - // PMI growth differential (positive = EU stronger) + const us_2y_implied = base.us_2y + us_2y_delta + const us_10y_implied = base.us_10y + us_10y_delta + const eu_2y_implied = base.eu_2y + eu_2y_delta + const eu_10y_implied = base.eu_10y + eu_10y_delta + + const rate_diff_2y = us_2y_implied - eu_2y_implied + const rate_diff_10y = us_10y_implied - eu_10y_implied + + const delta_diff_2y = rate_diff_2y - (base.us_2y - base.eu_2y) + const delta_diff_10y = rate_diff_10y - (base.us_10y - base.eu_10y) + + // ── Secondary channels ───────────────────────────────────────────────────── const pmi_diff = (p.pmi_eu - 50) - (p.pmi_us - 50) - - // VIX / risk-off (positive vix_dev = risk-off = USD up = EUR/USD down) const vix_dev = p.vix - base.vix const ry_dev = p.real_yield_us - base.real_yield_us const oil_dev = p.oil - base.oil - // ── Pip contributions ───────────────────────────────────────────────────── - const c_rate = Math.round(-rate_diff_delta * 650) // ~650 pips per 1% spread - const c_pmi = Math.round(pmi_diff * 8) - const c_vix = Math.round(-vix_dev * 4) - const c_ry = Math.round(-ry_dev * 120) - const c_oil = Math.round(oil_dev * 0.5) - const c_nfp = Math.round(-(p.nfp_surprise / 100) * 30) - const c_cpi_us = Math.round(-(p.cpi_us_surprise / 0.1) * 20) - const c_cpi_eu = Math.round((p.cpi_eu_surprise / 0.1) * 15) + // ── Pip contributions ────────────────────────────────────────────────────── + const c_2y = Math.round(-delta_diff_2y * 500) // rate channel (dominant) + const c_10y = Math.round(-delta_diff_10y * 200) // tone/expectations channel + const c_pmi = Math.round(pmi_diff * 7) + const c_vix = Math.round(-vix_dev * 4) + const c_ry = Math.round(-ry_dev * 100) + const c_oil = Math.round(oil_dev * 0.5) - const total_pips = c_rate + c_pmi + c_vix + c_ry + c_oil + c_nfp + c_cpi_us + c_cpi_eu + const total_pips = c_2y + c_10y + c_pmi + c_vix + c_ry + c_oil const contribs = ([ - { label: 'Δ Taux (US 2Y – Bund)', pips: c_rate, color: c_rate < 0 ? 'red' : c_rate > 0 ? 'green' : 'slate' }, - { label: 'CPI US (surprise)', pips: c_cpi_us, color: c_cpi_us < 0 ? 'red' : c_cpi_us > 0 ? 'green' : 'slate' }, - { label: 'NFP (surprise)', pips: c_nfp, color: c_nfp < 0 ? 'red' : c_nfp > 0 ? 'green' : 'slate' }, - { label: 'CPI EU (surprise)', pips: c_cpi_eu, color: c_cpi_eu > 0 ? 'green' : c_cpi_eu < 0 ? 'red' : 'slate' }, - { label: 'PMI diff (EU−US)', pips: c_pmi, color: c_pmi > 0 ? 'green' : c_pmi < 0 ? 'red' : 'slate' }, - { label: 'VIX / Risk off', pips: c_vix, color: c_vix < 0 ? 'red' : c_vix > 0 ? 'green' : 'slate' }, - { label: 'Taux réel US', pips: c_ry, color: c_ry < 0 ? 'red' : c_ry > 0 ? 'green' : 'slate' }, - { label: 'Pétrole', pips: c_oil, color: c_oil > 0 ? 'green' : c_oil < 0 ? 'red' : 'slate' }, + { label: 'Δ 2Y — taux directeurs', pips: c_2y, color: c_2y < 0 ? 'red' : c_2y > 0 ? 'green' : 'slate' }, + { label: 'Δ 10Y — anticipations/ton', pips: c_10y, color: c_10y < 0 ? 'red' : c_10y > 0 ? 'green' : 'slate' }, + { label: 'PMI diff (EU−US)', pips: c_pmi, color: c_pmi > 0 ? 'green' : c_pmi < 0 ? 'red' : 'slate' }, + { label: 'VIX / Risk-off', pips: c_vix, color: c_vix < 0 ? 'red' : c_vix > 0 ? 'green' : 'slate' }, + { label: 'Taux réel US', pips: c_ry, color: c_ry < 0 ? 'red' : c_ry > 0 ? 'green' : 'slate' }, + { label: 'Pétrole', pips: c_oil, color: c_oil > 0 ? 'green' : c_oil < 0 ? 'red' : 'slate' }, ] as ModelResult['contribs']).sort((a, b) => Math.abs(b.pips) - Math.abs(a.pips)) return { - fed_pressure, ecb_pressure, - us_2y_implied, eu_2y_implied, rate_diff, - eurusd: BASE_EURUSD + total_pips / 10000, + fed_rate_pressure, fed_fwd_signal, + ecb_rate_pressure, ecb_fwd_signal, + us_2y_implied, us_10y_implied, + eu_2y_implied, eu_10y_implied, + rate_diff_2y, rate_diff_10y, + eurusd: base.eurusd + total_pips / 10000, delta_pips: total_pips, contribs, } } -// ── Slider ───────────────────────────────────────────────────────────────────── +// ── Slider (with click-to-edit value) ───────────────────────────────────────── function Slider({ label, value, min, max, step, format, onChange, @@ -132,9 +156,12 @@ function Slider({ format: (v: number) => string; onChange: (v: number) => void colorize?: boolean; reverse?: boolean; center?: number }) { - const pct = ((value - min) / (max - min)) * 100 - const pivot = center ?? 0 - const mid = Math.max(0, Math.min(100, ((pivot - min) / (max - min)) * 100)) + const [editing, setEditing] = useState(false) + const [draft, setDraft] = useState('') + + const pct = ((value - min) / (max - min)) * 100 + const pivot = center ?? 0 + const mid = Math.max(0, Math.min(100, ((pivot - min) / (max - min)) * 100)) const atBase = Math.abs(value - pivot) < step / 2 let trackColor = 'bg-blue-500' @@ -143,15 +170,53 @@ function Slider({ trackColor = atBase ? 'bg-slate-600' : pos ? 'bg-emerald-500' : 'bg-rose-500' } + const startEdit = () => { setDraft(String(value)); setEditing(true) } + + const commitEdit = () => { + const v = parseFloat(draft.replace(',', '.')) + if (!isNaN(v)) { + const clamped = Math.max(min, Math.min(max, v)) + const snapped = Math.round((clamped - min) / step) * step + min + onChange(parseFloat(snapped.toFixed(10))) + } + setEditing(false) + } + + const isColored = colorize && !atBase + const valueColor = isColored + ? (reverse ? value < pivot : value > pivot) ? 'text-emerald-400' : 'text-rose-400' + : 'text-white' + return (
-
- {label} - pivot) ? 'text-emerald-400' : 'text-rose-400' - : 'text-white', - )}>{format(value)} +
+ {label} + {editing ? ( + setDraft(e.target.value)} + onBlur={commitEdit} + onKeyDown={e => { + if (e.key === 'Enter') commitEdit() + if (e.key === 'Escape') setEditing(false) + }} + className="w-20 bg-dark-900 border border-blue-500 rounded px-2 py-0.5 text-xs font-mono text-white text-right outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none" + /> + ) : ( + + )}
v > 0 ? '+' : '' - const fedC = r.fed_pressure > 0.4 ? '#f87171' : r.fed_pressure < -0.4 ? '#34d399' : '#94a3b8' - const ecbC = r.ecb_pressure > 0.4 ? '#34d399' : r.ecb_pressure < -0.4 ? '#f87171' : '#94a3b8' - const us2C = r.us_2y_implied > base.us_2y + 0.04 ? '#f87171' : r.us_2y_implied < base.us_2y - 0.04 ? '#34d399' : '#94a3b8' - const eu2C = r.eu_2y_implied > base.eu_2y + 0.04 ? '#34d399' : r.eu_2y_implied < base.eu_2y - 0.04 ? '#f87171' : '#94a3b8' - const diffC = r.rate_diff > (base.us_2y - base.eu_2y) + 0.04 ? '#f87171' : '#34d399' - const fxC = r.delta_pips < -5 ? '#f87171' : r.delta_pips > 5 ? '#34d399' : '#94a3b8' + const delta2yVal = r.rate_diff_2y - (base.us_2y - base.eu_2y) + const delta10yVal = r.rate_diff_10y - (base.us_10y - base.eu_10y) - const aw = (v: number) => Math.max(1, Math.min(3.5, Math.abs(v) * 0.7 + 1)) + const col3 = (v: number, threshold = 0.03): string => + Math.abs(v) < threshold ? '#94a3b8' : v > 0 ? '#f87171' : '#34d399' - const W = 360, H = 420 - const usX = 80, euX = 280 - const y1 = 42, y2 = 82, y3 = 118 - const yCB = 178, yYld = 258, yDiff = 338, yFX = 403 - const riskX = 38 + const fedRateC = col3(r.fed_rate_pressure, 0.3) + const fedFwdC = col3(r.fed_fwd_signal, 0.5) + const ecbRateC = col3(-r.ecb_rate_pressure, 0.3) // flipped: ECB hawkish → EUR up → green + const ecbFwdC = col3(-r.ecb_fwd_signal, 0.5) - const mk = (id: string, col: string) => ( - - + const us2C = col3(r.us_2y_implied - base.us_2y) + const us10C = col3(r.us_10y_implied - base.us_10y) + const eu2C = col3(-(r.eu_2y_implied - base.eu_2y)) // EU yield up → EUR/USD up → green + const eu10C = col3(-(r.eu_10y_implied - base.eu_10y)) + + const diff2C = col3(delta2yVal, 0.02) + const diff10C = col3(delta10yVal, 0.02) + const fxC = r.delta_pips < -5 ? '#f87171' : r.delta_pips > 5 ? '#34d399' : '#94a3b8' + + const aw = (v: number) => Math.max(1, Math.min(3.5, Math.abs(v) * 1.2 + 1)) + + const W = 400, H = 490 + const fedX = 100, ecbX = 300 + const us2X = 52, us10X = 155 + const eu2X = 348, eu10X = 245 + const diff2X = 140, diff10X = 260 + const vixX = 28 + const cX = 200 + + const yIn1 = 38, yIn2 = 74 + const yCB = 132 + const yYld = 220 + const yDiff = 310 + const yFX = 400 + + const COLORS = ['#f87171', '#34d399', '#94a3b8'] + const mk = (id: string, c: string) => ( + + ) - const colors = ['#f87171','#34d399','#94a3b8'] - const Node = ({ x, y, label, sub, col, w = 100 }: { x: number; y: number; label: string; sub?: string; col: string; w?: number }) => ( + const Node = ({ x, y, label, sub, col, w = 88 }: { x: number; y: number; label: string; sub?: string; col: string; w?: number }) => ( - - {label} - {sub && {sub}} + + {label} + {sub && {sub}} ) - const Arr = ({ x1, y1, x2, y2, col, w }: { x1: number; y1: number; x2: number; y2: number; col: string; w: number }) => ( - ( + ) return ( - {colors.map(c => mk(`a${c.replace('#', '')}`, c))} + {COLORS.map(c => mk(`a${c.replace('#', '')}`, c))} - {/* US chain */} - 0.3 ? '#f87171' : '#94a3b8'} w={88} /> - 0.3 ? '#f87171' : '#94a3b8'} w={88} /> - - - - - - + {/* US inputs */} + 0.4 ? '#f87171' : '#94a3b8'} w={98} /> + + - {/* EU chain */} - 0.3 ? '#34d399' : '#94a3b8'} w={88} /> - 0.3 ? 'fort' : 'faible'} col={r.ecb_pressure > 0.3 ? '#34d399' : '#94a3b8'} w={88} /> - - - - - + {/* EU inputs */} + 0.4 ? '#34d399' : '#94a3b8'} w={98} /> + - {/* Rate differential */} - - + {/* CB nodes */} + + - {/* VIX */} - - + {/* FED → US 2Y (solid, rate) and FED → US 10Y (dashed, tone) */} + + - {/* EURUSD */} - - EURUSD - {r.eurusd.toFixed(4)} + {/* BCE → EU 2Y (solid) and BCE → EU 10Y (dashed) */} + + + + {/* Yield nodes */} + + + + + + {/* Yields → differentials */} + + + + + + {/* Differential nodes */} + + + + {/* VIX (left) */} + + + + {/* Differentials → EURUSD */} + + + + {/* EURUSD box */} + + EURUSD + {r.eurusd.toFixed(4)} + + {/* Legend */} + + taux (2Y) + + ton (10Y) ) } @@ -282,7 +400,7 @@ function CausalChain({ r, base }: { r: ModelResult; base: Params }) { function SensBar({ label, pips, maxAbs }: { label: string; pips: number; maxAbs: number }) { const pct = maxAbs > 0 ? (Math.abs(pips) / maxAbs) * 100 : 0 return ( -
+
{label}
0 ? 'bg-emerald-500' : 'bg-rose-500')} @@ -306,15 +424,21 @@ function Section({ title, children, color }: { title: string; children: React.Re // ── Main page ───────────────────────────────────────────────────────────────── -interface Baseline { fed_rate: number; ecb_rate: number; us_2y: number; eu_2y: number; eurusd: number; vix: number; oil: number; real_yield_us: number; fetched_at?: string; sources?: Record } +interface Baseline { + fed_rate: number; ecb_rate: number + us_2y: number; us_10y: number + eu_2y: number; eu_10y: number + eurusd: number; vix: number; oil: number; real_yield_us: number + fetched_at?: string; sources?: Record +} export default function EuroSimulator() { - const [base, setBase] = useState(FALLBACK) - const [p, setP] = useState(FALLBACK) - const [liveStatus, setLive] = useState<'loading' | 'ok' | 'fallback'>('loading') - const [fetchedAt, setAt] = useState(null) - const [sources, setSources] = useState>({}) - const fetched = useRef(false) + const [base, setBase] = useState(FALLBACK) + const [p, setP] = useState(FALLBACK) + const [liveStatus, setLive] = useState<'loading' | 'ok' | 'fallback'>('loading') + const [fetchedAt, setAt] = useState(null) + const [sources, setSources] = useState>({}) + const fetched = useRef(false) const set = (k: keyof Params, v: number) => setP(prev => ({ ...prev, [k]: v })) @@ -325,11 +449,13 @@ export default function EuroSimulator() { .then(r => r.json()) .then((data: Baseline) => { const newBase: Params = { - ...FALLBACK, // keep surprise/tone/PMI at neutral + ...FALLBACK, fed_rate: data.fed_rate ?? FALLBACK.fed_rate, ecb_rate: data.ecb_rate ?? FALLBACK.ecb_rate, us_2y: data.us_2y ?? FALLBACK.us_2y, + us_10y: data.us_10y ?? FALLBACK.us_10y, eu_2y: data.eu_2y ?? FALLBACK.eu_2y, + eu_10y: data.eu_10y ?? FALLBACK.eu_10y, eurusd: data.eurusd ?? FALLBACK.eurusd, vix: data.vix ?? FALLBACK.vix, oil: data.oil ?? FALLBACK.oil, @@ -346,8 +472,8 @@ export default function EuroSimulator() { const r = useMemo(() => compute(p, base), [p, base]) - const maxAbs = useMemo(() => Math.max(1, ...r.contribs.map(c => Math.abs(c.pips))), [r.contribs]) - const eurusdColor = r.delta_pips < -5 ? 'text-rose-400' : r.delta_pips > 5 ? 'text-emerald-400' : 'text-slate-300' + const maxAbs = useMemo(() => Math.max(1, ...r.contribs.map(c => Math.abs(c.pips))), [r.contribs]) + const eurusdColor = r.delta_pips < -5 ? 'text-rose-400' : r.delta_pips > 5 ? 'text-emerald-400' : 'text-slate-300' return (
@@ -358,22 +484,19 @@ export default function EuroSimulator() {

Simulateur EUR/USD

-

Modèle causal — sans données historiques, purement simulé

+

Modèle causal 2 canaux — taux directeurs → 2Y · ton/données → 10Y

- {/* Live status badge */}
- {liveStatus === 'ok' - ? - : } + {liveStatus === 'ok' ? : } {liveStatus === 'loading' ? 'Chargement…' : - liveStatus === 'ok' ? `Live — base ${fetchedAt ? fetchedAt.slice(0,10) : ''}` : + liveStatus === 'ok' ? `Live — base ${fetchedAt ? fetchedAt.slice(0, 10) : ''}` : 'Fallback — données hardcodées'}
@@ -385,7 +508,7 @@ export default function EuroSimulator() {
-
+
{/* ── Controls ──────────────────────────────────────────────────────── */}
@@ -444,14 +567,24 @@ export default function EuroSimulator() {
Chaîne de transmission causale
- {/* Live metric strip */} -
- {[ - { label: 'US 2Y', val: `${r.us_2y_implied.toFixed(2)}%`, col: r.us_2y_implied > base.us_2y + 0.01 ? 'text-rose-400' : r.us_2y_implied < base.us_2y - 0.01 ? 'text-emerald-400' : 'text-slate-400' }, - { label: 'Bund 2Y', val: `${r.eu_2y_implied.toFixed(2)}%`, col: r.eu_2y_implied > base.eu_2y + 0.01 ? 'text-emerald-400' : r.eu_2y_implied < base.eu_2y - 0.01 ? 'text-rose-400' : 'text-slate-400' }, - { label: 'Δ Taux', val: `${r.rate_diff.toFixed(2)}%`, col: r.rate_diff > (base.us_2y - base.eu_2y) + 0.01 ? 'text-rose-400' : 'text-emerald-400' }, - { label: 'VIX', val: p.vix.toFixed(0), col: p.vix > base.vix + 2 ? 'text-rose-400' : p.vix < base.vix - 2 ? 'text-emerald-400' : 'text-slate-400' }, - ].map(it => ( + {/* Metric strip — 6 cells in 3×2 grid */} +
+ {(() => { + const d2 = r.us_2y_implied - base.us_2y + const d10 = r.us_10y_implied - base.us_10y + const ed2 = r.eu_2y_implied - base.eu_2y + const ed10 = r.eu_10y_implied - base.eu_10y + const baseDiff2 = base.us_2y - base.eu_2y + const baseDiff10 = base.us_10y - base.eu_10y + return [ + { label: 'US 2Y', val: `${r.us_2y_implied.toFixed(2)}%`, col: Math.abs(d2) < 0.01 ? 'text-slate-400' : d2 > 0 ? 'text-rose-400' : 'text-emerald-400' }, + { label: 'US 10Y', val: `${r.us_10y_implied.toFixed(2)}%`, col: Math.abs(d10) < 0.01 ? 'text-slate-400' : d10 > 0 ? 'text-rose-400' : 'text-emerald-400' }, + { label: 'Bund 2Y', val: `${r.eu_2y_implied.toFixed(2)}%`, col: Math.abs(ed2) < 0.01 ? 'text-slate-400' : ed2 > 0 ? 'text-emerald-400' : 'text-rose-400' }, + { label: 'Bund 10Y', val: `${r.eu_10y_implied.toFixed(2)}%`, col: Math.abs(ed10) < 0.01 ? 'text-slate-400' : ed10 > 0 ? 'text-emerald-400' : 'text-rose-400' }, + { label: 'Δ 2Y', val: `${r.rate_diff_2y.toFixed(2)}%`, col: r.rate_diff_2y > baseDiff2 + 0.01 ? 'text-rose-400' : r.rate_diff_2y < baseDiff2 - 0.01 ? 'text-emerald-400' : 'text-slate-400' }, + { label: 'Δ 10Y', val: `${r.rate_diff_10y.toFixed(2)}%`, col: r.rate_diff_10y > baseDiff10 + 0.01 ? 'text-rose-400' : r.rate_diff_10y < baseDiff10 - 0.01 ? 'text-emerald-400' : 'text-slate-400' }, + ] + })().map(it => (
{it.label}
{it.val}
@@ -462,7 +595,7 @@ export default function EuroSimulator() {
- Base live: EUR/USD {base.eurusd.toFixed(4)} · US 2Y {base.us_2y.toFixed(2)}% · Bund 2Y {base.eu_2y.toFixed(2)}% · VIX {base.vix.toFixed(1)} + Base: EURUSD {base.eurusd.toFixed(4)} · US 2Y {base.us_2y.toFixed(2)}% · US 10Y {base.us_10y.toFixed(2)}% · Bund 10Y {base.eu_10y.toFixed(2)}%
@@ -480,18 +613,20 @@ export default function EuroSimulator() {
vs base {base.eurusd.toFixed(4)}
- {/* Pressure gauges */} + {/* 4 signal gauges: FED rate/tone + BCE rate/tone */}
{[ - { label: 'Pression FED', v: r.fed_pressure, posLabel: 'Hawkish', negLabel: 'Dovish', posCol: 'text-rose-400', negCol: 'text-emerald-400' }, - { label: 'Pression BCE', v: r.ecb_pressure, posLabel: 'Hawkish', negLabel: 'Dovish', posCol: 'text-emerald-400', negCol: 'text-rose-400' }, + { label: 'FED — taux', v: r.fed_rate_pressure, posL: 'Hawkish', negL: 'Dovish', posC: 'text-rose-400', negC: 'text-emerald-400' }, + { label: 'FED — ton', v: r.fed_fwd_signal, posL: 'Hawkish', negL: 'Dovish', posC: 'text-rose-400', negC: 'text-emerald-400' }, + { label: 'BCE — taux', v: r.ecb_rate_pressure, posL: 'Hawkish', negL: 'Dovish', posC: 'text-emerald-400', negC: 'text-rose-400' }, + { label: 'BCE — ton', v: r.ecb_fwd_signal, posL: 'Hawkish', negL: 'Dovish', posC: 'text-emerald-400', negC: 'text-rose-400' }, ].map(g => (
{g.label}
0.2 ? g.posCol : g.v < -0.2 ? g.negCol : 'text-slate-400', + g.v > 0.2 ? g.posC : g.v < -0.2 ? g.negC : 'text-slate-400', )}> - {g.v > 0.2 ? g.posLabel : g.v < -0.2 ? g.negLabel : 'Neutre'} + {g.v > 0.2 ? g.posL : g.v < -0.2 ? g.negL : 'Neutre'}
{g.v > 0 ? '+' : ''}{g.v.toFixed(1)}
@@ -521,10 +656,10 @@ export default function EuroSimulator() { ))}
-
- Note: Modèle linéarisé heuristique. - La base est chargée en live (taux, VIX, Brent, EURUSD spot) au démarrage. - Les coefficients de transmission restent heuristiques — non calibrés sur données historiques. +
+
Canal taux (solid) : variations des taux directeurs → principalement US/Bund 2Y. Réponse rapide, déjà pricée.
+
Canal ton (tirets) : discours des CBs + surprises CPI/NFP → principalement 10Y. Anticipe la trajectoire future des taux.
+
Coefficients heuristiques — non calibrés sur données historiques.