""" Gauge Sync — mappe les dernières données de marché (macro_regime_history) vers les nœuds manuels des modèles instruments. Logique : - Récupère les derniers gauges (VIX, DXY, US10Y, Brent, LQD, cuivre...) - Pour chaque instrument, propose une valeur pour chaque nœud mappable - Confidence : HIGH (direct), MEDIUM (dérivé simple), LOW (proxy) - L'utilisateur review et confirme dans le frontend """ import json import math from typing import Optional # ── Structures de suggestion ─────────────────────────────────────────────────── def _conf(level: str, value: float, node_id: str, label: str, unit: str, source: str, note: str) -> dict: return { "node_id": node_id, "label": label, "value": round(value, 2), "unit": unit, "source": source, "confidence": level, # HIGH | MEDIUM | LOW "note": note, } # ── Dérivations communes ─────────────────────────────────────────────────────── def _us_real_rate_bps(g: dict) -> float: """Taux réel US approximé : 10Y nominal - breakeven estimé. TIPS ETF 109.76 → taux TIPS ≈ 4.487% - 2.0% = ~2.0% → 200bps. Approximation : 10Y - 2.2% (breakeven historique moyen). """ us10y = g.get("us10y", {}).get("value", 4.5) breakeven_est = 2.2 # % return (us10y - breakeven_est) * 100 # bps def _risk_appetite_score(g: dict) -> float: """Score appétit risque -5 à +5. VIX < 15 = risk-on (+), VIX > 25 = risk-off (−). """ vix = g.get("vix", {}).get("value", 20.0) spx_200d = g.get("spx_vs_200d", {}).get("value", 0.0) # Base from VIX score = 3.0 - vix / 8.0 # Adjust from SPX vs 200d MA score += spx_200d * 0.05 return max(-5.0, min(5.0, round(score, 2))) def _ig_spread_bps(g: dict) -> float: """Proxy spread IG depuis prix LQD ETF. LQD à 109+ = spreads très serrés (~80bps). Chaque point de LQD ≈ 5bps spread. Baseline : LQD=109 → 80bps spreads. """ lqd = g.get("lqd", {}).get("value", 109.0) return max(0, round((110.0 - lqd) * 8 + 80, 0)) def _recession_prob_pct(g: dict) -> float: """Probabilité récession % depuis pente 10Y-3M. Pente positive (0.87%) → faible risque récession ~20%. Pente inversée (<0) → risque élevé. """ slope = g.get("slope_10y3m", {}).get("value", 1.0) prob = 50.0 - slope * 28.0 return max(0.0, min(95.0, round(prob, 1))) def _energy_delta(g: dict, baseline: float = 70.0) -> float: """Delta prix énergie (Brent) vs baseline ($/bbl).""" brent = g.get("brent", {}).get("value", baseline) return round(brent - baseline, 2) def _copper_score(g: dict) -> float: """Score cycle cuivre -5 à +5. Baseline ~$4.5/lb.""" copper = g.get("copper", {}).get("value", 4.5) return round(max(-5, min(5, (copper - 4.5) * 2)), 2) def _dxy_level(g: dict) -> float: return g.get("dxy", {}).get("value", 100.0) def _vix(g: dict) -> float: return g.get("vix", {}).get("value", 18.0) def _us10y_bps(g: dict) -> float: return round(g.get("us10y", {}).get("value", 4.5) * 100, 0) def _us30y_bps(g: dict) -> float: """Approxime 30Y = 10Y + 25bps prime terme.""" return round((_us10y_bps(g) / 100 + 0.25) * 100, 0) def _yield_diff_usdjpy_bps(g: dict) -> float: """Différentiel 10Y US-Japon (JGB ≈ 1.0% depuis politique BoJ).""" us10y = g.get("us10y", {}).get("value", 4.5) jgb10y_approx = 1.0 # Policy-controlled, approximate return round((us10y - jgb10y_approx) * 100, 0) def _slope_bps(g: dict) -> float: return round(g.get("slope_10y3m", {}).get("value", 1.0) * 100, 0) # ── Mappings par instrument ──────────────────────────────────────────────────── def _suggest_eurusd(g: dict) -> list[dict]: vix = _vix(g) real = _us_real_rate_bps(g) risk = _risk_appetite_score(g) energy = _energy_delta(g, baseline=70.0) return [ _conf("HIGH", vix, "m_vix", "Niveau VIX", "pts", "gauge:vix", f"VIX actuel = {vix:.1f}"), _conf("HIGH", real, "m_us_real_rate", "Taux réel US 10Y", "bps", "gauge:us10y+tips", f"10Y {g.get('us10y',{}).get('value',4.5):.2f}% - 2.2% breakeven ≈ {real:.0f}bps"), _conf("MEDIUM", risk, "m_risk_appetite","Appétit risque mondial", "score", "gauge:vix+spx", f"Score = 3 - VIX/8 + SPX200d×0.05 = {risk:.2f}"), _conf("MEDIUM", energy, "m_energy_price", "Prix énergie delta", "$/bbl", "gauge:brent", f"Brent {g.get('brent',{}).get('value',70):.1f}$ - baseline 70$ = {energy:+.1f}"), _conf("LOW", _dxy_level(g), "m_dollar_reserve", "Demande réserves USD", "score", "gauge:dxy", f"DXY {_dxy_level(g):.1f} → proxy demand USD (>100 = fort)"), ] def _suggest_usdjpy(g: dict) -> list[dict]: vix = _vix(g) risk = _risk_appetite_score(g) yd = _yield_diff_usdjpy_bps(g) slope = _slope_bps(g) return [ _conf("HIGH", vix, "m_vix", "Niveau VIX", "pts", "gauge:vix", f"VIX = {vix:.1f}"), _conf("HIGH", yd, "m_yield_diff", "Diff 10Y US-JP", "bps", "gauge:us10y", f"US10Y {g.get('us10y',{}).get('value',4.5):.2f}% - JGB≈1.0% = {yd:.0f}bps"), _conf("MEDIUM", _us_real_rate_bps(g), "m_us_real_rate", "Taux réel US 10Y", "bps", "gauge:us10y", f"≈ {_us_real_rate_bps(g):.0f}bps"), _conf("MEDIUM", risk, "m_risk_appetite", "Appétit risque", "score", "gauge:vix", f"Score = {risk:.2f}"), _conf("LOW", slope,"m_carry_momentum","Momentum carry", "score", "gauge:slope", f"Pente 10Y-3M = {slope:.0f}bps → carry actif"), ] def _suggest_xauusd(g: dict) -> list[dict]: vix = _vix(g) dxy = _dxy_level(g) real = _us_real_rate_bps(g) return [ _conf("HIGH", vix, "m_vix", "Niveau VIX", "pts", "gauge:vix", f"VIX = {vix:.1f}"), _conf("HIGH", dxy, "m_dxy", "DXY (indice dollar)", "pts", "gauge:dxy", f"DXY = {dxy:.1f}"), _conf("HIGH", real, "m_us_real_rate", "Taux réel US 10Y", "bps", "gauge:us10y+tips", f"≈ {real:.0f}bps (10Y - 2.2% breakeven)"), _conf("MEDIUM", _energy_delta(g, 70), "m_fiscal_risk", "Risque fiscal US", "score", "gauge:dxy", f"DXY < 100 = doutes USD → score {round(max(0, (100-dxy)/5), 1)}"), _conf("LOW", _copper_score(g), "m_india_china", "Demande physique Asie", "score", "gauge:copper", f"Cuivre {g.get('copper',{}).get('value',4.5):.2f}$/lb → proxy Asie"), ] def _suggest_sp500(g: dict) -> list[dict]: vix = _vix(g) real = _us_real_rate_bps(g) ig = _ig_spread_bps(g) risk = _risk_appetite_score(g) spx200d = g.get("spx_vs_200d", {}).get("value", 0) rec_prob = _recession_prob_pct(g) return [ _conf("HIGH", vix, "m_vix", "Niveau VIX", "pts", "gauge:vix", f"VIX = {vix:.1f}"), _conf("HIGH", real, "m_real_rate", "Taux réel US 10Y", "bps", "gauge:us10y+tips", f"≈ {real:.0f}bps"), _conf("HIGH", ig, "m_ig_spread", "Spread IG (LQD proxy)", "bps", "gauge:lqd", f"LQD {g.get('lqd',{}).get('value',109):.2f} → spread ≈ {ig:.0f}bps"), _conf("MEDIUM", risk, "m_fin_cond", "Conditions financières", "score", "gauge:vix+lqd", f"Score = {risk:.2f}"), _conf("MEDIUM", rec_prob,"m_gdp", "Croissance PIB US", "%", "gauge:slope", f"Pente 10Y-3M = {_slope_bps(g):.0f}bps → récession {rec_prob:.0f}% (inversé → PIB)"), ] def _suggest_tlt(g: dict) -> list[dict]: us10y = _us10y_bps(g) us30y = _us30y_bps(g) vix = _vix(g) slope = _slope_bps(g) rec = _recession_prob_pct(g) tp = round(slope * 0.6, 0) # term premium proxy return [ _conf("HIGH", us10y, "m_us_10y", "Rendement UST 10Y", "bps", "gauge:us10y", f"UST 10Y = {us10y/100:.3f}%"), _conf("HIGH", us30y, "m_us_30y", "Rendement UST 30Y", "bps", "gauge:us10y", f"≈ 10Y + 25bps = {us30y/100:.3f}%"), _conf("HIGH", vix, "m_vix", "Niveau VIX", "pts", "gauge:vix", f"VIX = {vix:.1f}"), _conf("MEDIUM", tp, "m_term_prem", "Prime de terme", "bps", "gauge:slope", f"Pente 10Y-3M {slope:.0f}bps × 0.6 ≈ {tp:.0f}bps"), _conf("MEDIUM", rec, "m_recession_prob", "Prob. récession 12m", "%", "gauge:slope", f"Pente {slope:.0f}bps → prob ≈ {rec:.0f}%"), _conf("LOW", round(_ig_spread_bps(g)/10, 1), "m_foreign_demand", "Demande étrangère", "Mds$", "gauge:lqd", f"LQD sain → proxy demand bonds = {round(_ig_spread_bps(g)/10,1)}"), ] def _suggest_gbpusd(g: dict) -> list[dict]: vix = _vix(g) risk = _risk_appetite_score(g) real = _us_real_rate_bps(g) return [ _conf("HIGH", vix, "m_vix", "Niveau VIX", "pts", "gauge:vix", f"VIX = {vix:.1f}"), _conf("MEDIUM", risk, "m_risk_appetite", "Appétit risque", "score", "gauge:vix", f"GBP devise cyclique, risk-on = {risk:.2f}"), _conf("LOW", real, "m_fed_path", "Anticipation Fed 12m", "bps", "gauge:us10y", f"Proxy Fed path depuis taux réels ≈ {real:.0f}bps"), ] def _suggest_eem(g: dict) -> list[dict]: dxy = _dxy_level(g) vix = _vix(g) real = _us_real_rate_bps(g) risk = _risk_appetite_score(g) em_spread = round(_ig_spread_bps(g) * 1.8, 0) # EM spreads ≈ 1.8× IG cop = _copper_score(g) return [ _conf("HIGH", dxy, "m_dxy_inv", "Dollar DXY", "pts", "gauge:dxy", f"DXY = {dxy:.1f}"), _conf("HIGH", vix, "m_vix", "Niveau VIX", "pts", "gauge:vix", f"VIX = {vix:.1f}"), _conf("HIGH", real, "m_us_real_rate", "Taux réel US 10Y", "bps", "gauge:us10y", f"≈ {real:.0f}bps"), _conf("MEDIUM", risk, "m_risk_appetite", "Appétit risque", "score", "gauge:vix", f"Score = {risk:.2f}"), _conf("MEDIUM", em_spread, "m_em_spread", "Spread souverain EM", "bps", "gauge:lqd", f"LQD proxy × 1.8 ≈ {em_spread:.0f}bps"), _conf("MEDIUM", cop, "m_commodity_index","Indice commodités", "score", "gauge:copper",f"Cuivre {g.get('copper',{}).get('value',4.5):.2f} → score {cop:.2f}"), _conf("LOW", round(cop * 0.5, 2), "m_china_pmi", "PMI manuf. Chine", "pts", "gauge:copper",f"Proxy cuivre → PMI-like = {round(50+cop*0.5,1)}"), ] def _suggest_qqq(g: dict) -> list[dict]: vix = _vix(g) real = _us_real_rate_bps(g) risk = _risk_appetite_score(g) spx_vs200 = g.get("spx_vs_200d", {}).get("value", 0) return [ _conf("HIGH", vix, "m_vix", "Niveau VIX", "pts", "gauge:vix", f"VIX = {vix:.1f} (beta élevé QQQ)"), _conf("HIGH", real, "m_real_rate", "Taux réel US 10Y (duration)","bps","gauge:us10y", f"≈ {real:.0f}bps → taux d'actualisation tech"), _conf("MEDIUM", risk, "m_retail_options", "Flux options retail", "score", "gauge:spx", f"Risk appetite {risk:.2f} → proxy momentum options"), _conf("LOW", round(spx_vs200 * 0.8, 2), "m_tech_pe", "Multiple PE tech (NTM)", "x", "gauge:spx_vs_200d", f"SPX vs 200d = {spx_vs200:.1f}% → PE trend"), ] # ── Registry ─────────────────────────────────────────────────────────────────── _SUGGEST_FN = { "EURUSD": _suggest_eurusd, "USDJPY": _suggest_usdjpy, "XAUUSD": _suggest_xauusd, "SP500": _suggest_sp500, "TLT": _suggest_tlt, "GBPUSD": _suggest_gbpusd, "EEM": _suggest_eem, "QQQ": _suggest_qqq, } # ── Public API ───────────────────────────────────────────────────────────────── def get_latest_gauges(conn, at_date: Optional[str] = None) -> tuple[dict, str]: """Retourne (gauges_dict, snapshot_date) depuis macro_regime_history. Si at_date fourni, retourne le snapshot le plus proche <= at_date.""" if at_date: row = conn.execute( "SELECT gauges_summary_json, timestamp FROM macro_regime_history " "WHERE timestamp <= ? ORDER BY timestamp DESC LIMIT 1", (at_date,) ).fetchone() else: row = conn.execute( "SELECT gauges_summary_json, timestamp FROM macro_regime_history " "ORDER BY timestamp DESC LIMIT 1" ).fetchone() if row: r = dict(row) gauges = json.loads(r.get("gauges_summary_json") or "{}") date = str(r.get("timestamp", ""))[:10] if gauges: return gauges, date # Fallback : macro_gauge_snapshots if at_date: row2 = conn.execute( "SELECT gauges_json, snapshot_date FROM macro_gauge_snapshots " "WHERE snapshot_date <= ? ORDER BY snapshot_date DESC LIMIT 1", (at_date,) ).fetchone() else: row2 = conn.execute( "SELECT gauges_json, snapshot_date FROM macro_gauge_snapshots " "ORDER BY snapshot_date DESC LIMIT 1" ).fetchone() if row2: return json.loads(row2["gauges_json"] or "{}"), str(row2["snapshot_date"]) return {}, "" def suggest_from_gauges(instrument: str, gauges: dict) -> list[dict]: """ Retourne les suggestions de valeurs pour les nœuds manuels d'un instrument depuis les gauges de marché actuels. """ fn = _SUGGEST_FN.get(instrument.upper()) if not fn or not gauges: return [] suggestions = fn(gauges) # Filtre les NaN / Inf return [ s for s in suggestions if math.isfinite(s.get("value", 0)) ]