feat: macro gauge DB + regime triggers + date-aware instrument snapshot
DB: - New table macro_gauge_snapshots (daily snapshot of all 28+ gauges + dominant + scores) - save_macro_gauge_snapshot / get_macro_gauge_snapshot_at / get_macro_gauge_history - Auto-save once per calendar day on every macro-regime fetch (not just force=True) API: - GET /api/market/macro-gauges/at?date=YYYY-MM-DD — nearest snapshot ≤ date - GET /api/market/macro-gauges/history?days=N Detector (_check_macro_gauges in Eco Desk): - Regime transition events (goldilocks→stagflation etc.) with severity scoring - Yield curve inversion / désinversion (slope_10y3m sign change) - DXY shock (% change over lookback window) - Credit stress (HYG drop threshold) - Gold/Copper ratio regime crossings InstrumentDashboard: - macroAtDate state: fetches /api/market/macro-gauges/at when crosshair date ≠ last date - RegimeCard uses historical macro regime when on a past date - MacroGaugePanel: full breakdown of all gauges by bloc (liquidité, crédit, énergie...) visible only when on a historical date — shows value + change_pct + regime scores bar AIDesks: added fundamental + sentiment to AIDesk type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -123,6 +123,8 @@ def macro_regime(force: bool = False):
|
|||||||
_macro_cache["data"] = result
|
_macro_cache["data"] = result
|
||||||
_macro_cache["ts"] = now
|
_macro_cache["ts"] = now
|
||||||
|
|
||||||
|
today = now.strftime("%Y-%m-%d")
|
||||||
|
|
||||||
if force:
|
if force:
|
||||||
# Build a compact gauge summary (key → value + change_pct) for the journal
|
# Build a compact gauge summary (key → value + change_pct) for the journal
|
||||||
gauges_summary = {
|
gauges_summary = {
|
||||||
@@ -138,4 +140,31 @@ def macro_regime(force: bool = False):
|
|||||||
gauges_summary=gauges_summary,
|
gauges_summary=gauges_summary,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Always persist full snapshot once per calendar day (all gauges + regime)
|
||||||
|
from services.database import save_macro_gauge_snapshot, macro_gauge_snapshot_exists_today
|
||||||
|
if force or not macro_gauge_snapshot_exists_today():
|
||||||
|
save_macro_gauge_snapshot(
|
||||||
|
snapshot_date=today,
|
||||||
|
gauges=gauges,
|
||||||
|
dominant=scenarios.get("dominant", "incertain"),
|
||||||
|
regime_scores=scenarios.get("scores", {}),
|
||||||
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/macro-gauges/at")
|
||||||
|
def macro_gauges_at(date: str = Query(..., description="YYYY-MM-DD")):
|
||||||
|
"""Return the macro gauge snapshot at or before a given date."""
|
||||||
|
from services.database import get_macro_gauge_snapshot_at
|
||||||
|
snap = get_macro_gauge_snapshot_at(date)
|
||||||
|
if not snap:
|
||||||
|
return {"snapshot_date": None, "gauges": {}, "dominant": "incertain", "regime_scores": {}}
|
||||||
|
return snap
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/macro-gauges/history")
|
||||||
|
def macro_gauges_history(days: int = Query(30, ge=1, le=365)):
|
||||||
|
"""Return gauge snapshots for the last N days (daily, most recent first)."""
|
||||||
|
from services.database import get_macro_gauge_history
|
||||||
|
return get_macro_gauge_history(days=days)
|
||||||
|
|||||||
@@ -238,6 +238,20 @@ def init_db():
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
c.execute("""CREATE TABLE IF NOT EXISTS macro_gauge_snapshots (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
snapshot_date TEXT NOT NULL,
|
||||||
|
gauges_json TEXT NOT NULL DEFAULT '{}',
|
||||||
|
dominant TEXT,
|
||||||
|
regime_scores_json TEXT DEFAULT '{}',
|
||||||
|
created_at TEXT DEFAULT (datetime('now')),
|
||||||
|
UNIQUE(snapshot_date)
|
||||||
|
)""")
|
||||||
|
try:
|
||||||
|
c.execute("CREATE INDEX IF NOT EXISTS idx_mgs_date ON macro_gauge_snapshots(snapshot_date DESC)")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
c.execute("""CREATE TABLE IF NOT EXISTS geo_alert_history (
|
c.execute("""CREATE TABLE IF NOT EXISTS geo_alert_history (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
timestamp TEXT NOT NULL,
|
timestamp TEXT NOT NULL,
|
||||||
@@ -1028,7 +1042,18 @@ def init_db():
|
|||||||
"active": 1,
|
"active": 1,
|
||||||
"system_prompt": "Tu analyses les surprises économiques des données macro US (FRED).",
|
"system_prompt": "Tu analyses les surprises économiques des données macro US (FRED).",
|
||||||
"instruments": json.dumps(["SPY","TLT","GLD","EURUSD=X","USO","HYG"]),
|
"instruments": json.dumps(["SPY","TLT","GLD","EURUSD=X","USO","HYG"]),
|
||||||
"config": json.dumps({"z_threshold": 1.5, "days": 7}),
|
"config": json.dumps({
|
||||||
|
"z_threshold": 1.5,
|
||||||
|
"days": 7,
|
||||||
|
"lookback_days": 7,
|
||||||
|
"gauge_signals": {
|
||||||
|
"regime_transition": {"enabled": True},
|
||||||
|
"yield_curve_inversion": {"enabled": True, "threshold": 0.0},
|
||||||
|
"dxy_shock": {"enabled": True, "pct_threshold": 2.0},
|
||||||
|
"credit_stress": {"enabled": True, "pct_threshold": -1.5},
|
||||||
|
"gold_copper_ratio": {"enabled": True, "fear_threshold": 700, "growth_threshold": 500},
|
||||||
|
},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Fundamental Desk",
|
"name": "Fundamental Desk",
|
||||||
@@ -1570,6 +1595,85 @@ def get_macro_regime_history(days: int = 15) -> List[Dict[str, Any]]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ── Macro gauge snapshots (daily persistence of all 28+ gauges) ──────────────
|
||||||
|
|
||||||
|
def save_macro_gauge_snapshot(
|
||||||
|
snapshot_date: str,
|
||||||
|
gauges: Dict[str, Any],
|
||||||
|
dominant: Optional[str] = None,
|
||||||
|
regime_scores: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""Save (or replace) a full gauge snapshot for a given date. Returns True if new."""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT id FROM macro_gauge_snapshots WHERE snapshot_date=?", (snapshot_date,)
|
||||||
|
).fetchone()
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO macro_gauge_snapshots "
|
||||||
|
"(snapshot_date, gauges_json, dominant, regime_scores_json, created_at) "
|
||||||
|
"VALUES (?, ?, ?, ?, datetime('now'))",
|
||||||
|
(snapshot_date, json.dumps(gauges), dominant or "",
|
||||||
|
json.dumps(regime_scores or {}))
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
return existing is None
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_macro_gauge_snapshot_at(date: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""Return the most recent snapshot whose date is <= `date`."""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT * FROM macro_gauge_snapshots WHERE snapshot_date <= ? "
|
||||||
|
"ORDER BY snapshot_date DESC LIMIT 1",
|
||||||
|
(date[:10],)
|
||||||
|
).fetchone()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
d = dict(row)
|
||||||
|
d["gauges"] = json.loads(d.pop("gauges_json", "{}"))
|
||||||
|
d["regime_scores"] = json.loads(d.pop("regime_scores_json", "{}"))
|
||||||
|
return d
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def get_macro_gauge_history(days: int = 30) -> List[Dict[str, Any]]:
|
||||||
|
"""Return gauge snapshots for the last N days, most recent first."""
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT * FROM macro_gauge_snapshots "
|
||||||
|
"WHERE snapshot_date >= date('now', ?) "
|
||||||
|
"ORDER BY snapshot_date DESC",
|
||||||
|
(f"-{days} days",)
|
||||||
|
).fetchall()
|
||||||
|
result = []
|
||||||
|
for r in rows:
|
||||||
|
d = dict(r)
|
||||||
|
d["gauges"] = json.loads(d.pop("gauges_json", "{}"))
|
||||||
|
d["regime_scores"] = json.loads(d.pop("regime_scores_json", "{}"))
|
||||||
|
result.append(d)
|
||||||
|
return result
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def macro_gauge_snapshot_exists_today() -> bool:
|
||||||
|
today = datetime.utcnow().strftime("%Y-%m-%d")
|
||||||
|
conn = get_conn()
|
||||||
|
try:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT id FROM macro_gauge_snapshots WHERE snapshot_date=?", (today,)
|
||||||
|
).fetchone()
|
||||||
|
return row is not None
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def log_geo_alert(geo_score: int, top_patterns: List[Dict[str, Any]], news_count: int, run_id: str):
|
def log_geo_alert(geo_score: int, top_patterns: List[Dict[str, Any]], news_count: int, run_id: str):
|
||||||
"""Append a geo alert snapshot tied to a scoring run."""
|
"""Append a geo alert snapshot tied to a scoring run."""
|
||||||
conn = get_conn()
|
conn = get_conn()
|
||||||
|
|||||||
@@ -1058,6 +1058,232 @@ def _check_reports(desk_cfg: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|||||||
return created
|
return created
|
||||||
|
|
||||||
|
|
||||||
|
# ── Source 7: Macro gauge transitions (Eco Desk extension) ───────────────────
|
||||||
|
|
||||||
|
# Scenarios ordered by risk level for determining transition severity
|
||||||
|
_REGIME_SEVERITY = {
|
||||||
|
"goldilocks": 1, "desinflation": 2, "soft_landing": 2,
|
||||||
|
"reflation": 3, "stagflation": 4, "inflation_shock": 5,
|
||||||
|
"recession": 5, "crise_liquidite": 6, "incertain": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
_REGIME_LABELS = {
|
||||||
|
"goldilocks": "Goldilocks", "desinflation": "Désinflation",
|
||||||
|
"soft_landing": "Soft Landing", "reflation": "Reflation",
|
||||||
|
"stagflation": "Stagflation", "inflation_shock": "Choc Inflationniste",
|
||||||
|
"recession": "Récession", "crise_liquidite": "Crise de liquidité",
|
||||||
|
"incertain": "Incertain",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _check_macro_gauges(desk_cfg: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Detect macro regime transitions and key gauge threshold crossings.
|
||||||
|
Uses macro_gauge_snapshots table (daily persistence) as source.
|
||||||
|
Falls back to macro_regime_history for regime transitions if no snapshots yet.
|
||||||
|
"""
|
||||||
|
from services.database import get_macro_gauge_history, get_macro_regime_history
|
||||||
|
|
||||||
|
gauge_signals = desk_cfg.get("gauge_signals", {})
|
||||||
|
|
||||||
|
def sig_on(k: str) -> Optional[Dict]:
|
||||||
|
c = gauge_signals.get(k, {})
|
||||||
|
return c if c.get("enabled", True) else None
|
||||||
|
|
||||||
|
regime_cfg = sig_on("regime_transition")
|
||||||
|
curve_cfg = sig_on("yield_curve_inversion")
|
||||||
|
dxy_cfg = sig_on("dxy_shock")
|
||||||
|
credit_cfg = sig_on("credit_stress")
|
||||||
|
gcr_cfg = sig_on("gold_copper_ratio")
|
||||||
|
|
||||||
|
existing = _existing_event_keys()
|
||||||
|
created: List[Dict] = []
|
||||||
|
|
||||||
|
def _emit(name: str, date_str: str, category: str, sub_type: str,
|
||||||
|
score: float, level: str, desc: str, assets: List[str]):
|
||||||
|
if _is_dup(name, existing):
|
||||||
|
return
|
||||||
|
ev = {
|
||||||
|
"name": name,
|
||||||
|
"start_date": date_str,
|
||||||
|
"level": level,
|
||||||
|
"category": category,
|
||||||
|
"sub_type": sub_type,
|
||||||
|
"description": desc,
|
||||||
|
"market_impact": "",
|
||||||
|
"affected_assets": assets,
|
||||||
|
"impact_score": score,
|
||||||
|
"source_refs": [{
|
||||||
|
"title": f"Macro signal: {name}",
|
||||||
|
"source": "MacroRegime/DB",
|
||||||
|
"url": "",
|
||||||
|
"date": date_str,
|
||||||
|
"original_score": score,
|
||||||
|
}],
|
||||||
|
"origin": "detector_macro_gauge",
|
||||||
|
}
|
||||||
|
result = _save_and_evaluate(ev, existing)
|
||||||
|
if result:
|
||||||
|
result["source"] = "eco"
|
||||||
|
created.append(result)
|
||||||
|
|
||||||
|
# ── Regime transition ─────────────────────────────────────────────────────
|
||||||
|
if regime_cfg:
|
||||||
|
history = get_macro_gauge_history(days=14)
|
||||||
|
if len(history) >= 2:
|
||||||
|
latest = history[0]
|
||||||
|
prev = history[1]
|
||||||
|
dom_new = latest.get("dominant") or "incertain"
|
||||||
|
dom_old = prev.get("dominant") or "incertain"
|
||||||
|
if dom_new != dom_old and dom_new != "incertain":
|
||||||
|
date_str = latest["snapshot_date"]
|
||||||
|
sev_old = _REGIME_SEVERITY.get(dom_old, 0)
|
||||||
|
sev_new = _REGIME_SEVERITY.get(dom_new, 0)
|
||||||
|
direction = "bearish" if sev_new > sev_old else "bullish"
|
||||||
|
score = min(0.90, 0.55 + abs(sev_new - sev_old) * 0.07)
|
||||||
|
level = "long" if abs(sev_new - sev_old) >= 3 else "medium"
|
||||||
|
lbl_old = _REGIME_LABELS.get(dom_old, dom_old)
|
||||||
|
lbl_new = _REGIME_LABELS.get(dom_new, dom_new)
|
||||||
|
name = f"Transition Régime Macro: {lbl_old} → {lbl_new} ({date_str[:7]})"
|
||||||
|
desc = (
|
||||||
|
f"Le régime macro dominant passe de {lbl_old} à {lbl_new}. "
|
||||||
|
f"Sévérité: {sev_old}→{sev_new}/6. "
|
||||||
|
f"Révision des biais d'actifs recommandée."
|
||||||
|
)
|
||||||
|
scores = latest.get("regime_scores", {})
|
||||||
|
top3 = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||||
|
if top3:
|
||||||
|
desc += " Top 3 scénarios: " + ", ".join(
|
||||||
|
f"{_REGIME_LABELS.get(k,k)} ({v:.0%})" for k, v in top3
|
||||||
|
)
|
||||||
|
_emit(name, date_str, "event_calendar", "RegimeTransition",
|
||||||
|
score, level, desc,
|
||||||
|
["SPY","TLT","GLD","VXX","HYG","EURUSD=X"])
|
||||||
|
|
||||||
|
# Fallback: use macro_regime_history if no snapshots yet
|
||||||
|
elif not history:
|
||||||
|
hist = get_macro_regime_history(days=14)
|
||||||
|
if len(hist) >= 2:
|
||||||
|
latest = hist[0]
|
||||||
|
prev = hist[1]
|
||||||
|
dom_new = latest.get("dominant") or "incertain"
|
||||||
|
dom_old = prev.get("dominant") or "incertain"
|
||||||
|
if dom_new != dom_old and dom_new != "incertain":
|
||||||
|
date_str = latest["timestamp"][:10]
|
||||||
|
sev_old = _REGIME_SEVERITY.get(dom_old, 0)
|
||||||
|
sev_new = _REGIME_SEVERITY.get(dom_new, 0)
|
||||||
|
score = min(0.90, 0.55 + abs(sev_new - sev_old) * 0.07)
|
||||||
|
level = "long" if abs(sev_new - sev_old) >= 3 else "medium"
|
||||||
|
lbl_old = _REGIME_LABELS.get(dom_old, dom_old)
|
||||||
|
lbl_new = _REGIME_LABELS.get(dom_new, dom_new)
|
||||||
|
name = f"Transition Régime Macro: {lbl_old} → {lbl_new} ({date_str[:7]})"
|
||||||
|
_emit(name, date_str, "event_calendar", "RegimeTransition",
|
||||||
|
score, level,
|
||||||
|
f"Transition macro: {lbl_old} → {lbl_new}. Source: macro_regime_history.",
|
||||||
|
["SPY","TLT","GLD","VXX","HYG"])
|
||||||
|
|
||||||
|
# ── Gauge threshold crossings (from saved snapshots) ──────────────────────
|
||||||
|
history = get_macro_gauge_history(days=desk_cfg.get("lookback_days", 7) + 2)
|
||||||
|
if len(history) < 2:
|
||||||
|
return created
|
||||||
|
|
||||||
|
latest_snap = history[0]
|
||||||
|
oldest_snap = history[-1]
|
||||||
|
latest_gauges = latest_snap.get("gauges", {})
|
||||||
|
oldest_gauges = oldest_snap.get("gauges", {})
|
||||||
|
latest_date = latest_snap["snapshot_date"]
|
||||||
|
|
||||||
|
def gauge_val(snap_gauges: Dict, gid: str) -> Optional[float]:
|
||||||
|
g = snap_gauges.get(gid, {})
|
||||||
|
v = g.get("value")
|
||||||
|
return float(v) if v is not None else None
|
||||||
|
|
||||||
|
# ── Yield curve inversion ─────────────────────────────────────────────────
|
||||||
|
if curve_cfg:
|
||||||
|
threshold = float(curve_cfg.get("threshold", 0.0))
|
||||||
|
# slope_10y3m is a derived gauge: positive = normal, negative = inverted
|
||||||
|
slope_now = gauge_val(latest_gauges, "slope_10y3m")
|
||||||
|
slope_old = gauge_val(oldest_gauges, "slope_10y3m")
|
||||||
|
if slope_now is not None and slope_old is not None:
|
||||||
|
if slope_old > threshold >= slope_now:
|
||||||
|
name = f"Inversion Courbe 10Y-3M ({latest_date[:7]})"
|
||||||
|
_emit(name, latest_date, "event_calendar", "YieldCurveInversion",
|
||||||
|
0.85, "long",
|
||||||
|
f"La courbe des taux US (10Y-3M) s'inverse à {slope_now:+.2f} pts "
|
||||||
|
f"(précédent: {slope_old:+.2f} pts). "
|
||||||
|
f"Signal historique de récession dans 12-18 mois.",
|
||||||
|
["TLT","SPY","HYG","GLD","EURUSD=X"])
|
||||||
|
elif slope_old <= threshold < slope_now:
|
||||||
|
name = f"Désincurve 10Y-3M — Reflation ({latest_date[:7]})"
|
||||||
|
_emit(name, latest_date, "event_calendar", "YieldCurveDesinversion",
|
||||||
|
0.70, "medium",
|
||||||
|
f"La courbe 10Y-3M revient positive à {slope_now:+.2f} pts. "
|
||||||
|
f"Signal de détente des craintes de récession.",
|
||||||
|
["SPY","XLF","IWM","TLT"])
|
||||||
|
|
||||||
|
# ── DXY shock ────────────────────────────────────────────────────────────
|
||||||
|
if dxy_cfg:
|
||||||
|
pct_thr = float(dxy_cfg.get("pct_threshold", 2.0))
|
||||||
|
dxy_now = gauge_val(latest_gauges, "dxy")
|
||||||
|
dxy_old = gauge_val(oldest_gauges, "dxy")
|
||||||
|
if dxy_now and dxy_old and dxy_old > 0:
|
||||||
|
pct_chg = (dxy_now - dxy_old) / dxy_old * 100
|
||||||
|
if abs(pct_chg) >= pct_thr:
|
||||||
|
sign = "+" if pct_chg > 0 else ""
|
||||||
|
direct = "bullish" if pct_chg > 0 else "bearish"
|
||||||
|
name = f"DXY choc {sign}{pct_chg:.1f}% ({latest_date[:7]})"
|
||||||
|
_emit(name, latest_date, "event_calendar", "DXYShock",
|
||||||
|
min(0.80, 0.45 + abs(pct_chg) * 0.07), "medium",
|
||||||
|
f"Dollar DXY {sign}{pct_chg:.1f}% sur la période "
|
||||||
|
f"({dxy_old:.1f} → {dxy_now:.1f}). "
|
||||||
|
f"{'Appréciation USD: pression sur EM et matières premières.' if pct_chg > 0 else 'Dépréciation USD: favorable aux matières premières et EM.'}",
|
||||||
|
["GLD","EEM","USO","EURUSD=X","USDJPY=X"])
|
||||||
|
|
||||||
|
# ── Credit stress (HYG) ───────────────────────────────────────────────────
|
||||||
|
if credit_cfg:
|
||||||
|
pct_thr = float(credit_cfg.get("pct_threshold", -1.5))
|
||||||
|
hyg_chg = gauge_val(latest_gauges, "hyg")
|
||||||
|
if hyg_chg is None:
|
||||||
|
# Fallback: compute from values
|
||||||
|
hyg_now = gauge_val(latest_gauges, "hyg")
|
||||||
|
hyg_old = gauge_val(oldest_gauges, "hyg")
|
||||||
|
if hyg_now and hyg_old and hyg_old > 0:
|
||||||
|
hyg_chg = (hyg_now - hyg_old) / hyg_old * 100
|
||||||
|
else:
|
||||||
|
hyg_chg = float(latest_gauges.get("hyg", {}).get("change_pct") or 0)
|
||||||
|
if hyg_chg is not None and hyg_chg <= pct_thr:
|
||||||
|
name = f"Stress Crédit HYG ({latest_date[:7]})"
|
||||||
|
_emit(name, latest_date, "event_calendar", "CreditStress",
|
||||||
|
min(0.80, 0.45 + abs(hyg_chg) * 0.1), "medium",
|
||||||
|
f"HYG (High Yield) chute de {hyg_chg:.1f}% — signal de stress crédit. "
|
||||||
|
f"Spreads HY en élargissement. Surveiller LQD et TLT pour contagion.",
|
||||||
|
["HYG","LQD","SPY","TLT","VXX"])
|
||||||
|
|
||||||
|
# ── Gold/Copper ratio regime ──────────────────────────────────────────────
|
||||||
|
if gcr_cfg:
|
||||||
|
fear_thr = float(gcr_cfg.get("fear_threshold", 700))
|
||||||
|
growth_thr = float(gcr_cfg.get("growth_threshold", 500))
|
||||||
|
gcr_now = gauge_val(latest_gauges, "gold_copper_ratio")
|
||||||
|
gcr_old = gauge_val(oldest_gauges, "gold_copper_ratio")
|
||||||
|
if gcr_now and gcr_old:
|
||||||
|
if gcr_old < fear_thr <= gcr_now:
|
||||||
|
name = f"Ratio Or/Cuivre zone peur > {fear_thr:.0f} ({latest_date[:7]})"
|
||||||
|
_emit(name, latest_date, "event_calendar", "GoldCopperRatio",
|
||||||
|
0.70, "medium",
|
||||||
|
f"Ratio Or/Cuivre franchit {fear_thr:.0f} ({gcr_old:.0f} → {gcr_now:.0f}). "
|
||||||
|
f"L'or surperforme le cuivre — signal de risk-off, craintes de récession.",
|
||||||
|
["GLD","SPY","EEM","USO"])
|
||||||
|
elif gcr_old > growth_thr >= gcr_now:
|
||||||
|
name = f"Ratio Or/Cuivre zone croissance < {growth_thr:.0f} ({latest_date[:7]})"
|
||||||
|
_emit(name, latest_date, "event_calendar", "GoldCopperRatio",
|
||||||
|
0.60, "medium",
|
||||||
|
f"Ratio Or/Cuivre sous {growth_thr:.0f} ({gcr_old:.0f} → {gcr_now:.0f}). "
|
||||||
|
f"Le cuivre surperforme l'or — signal de risk-on, expansion économique.",
|
||||||
|
["EEM","XLI","SPY","GLD"])
|
||||||
|
|
||||||
|
return created
|
||||||
|
|
||||||
|
|
||||||
# ── Main entry point ──────────────────────────────────────────────────────────
|
# ── Main entry point ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def check_new_market_events(
|
def check_new_market_events(
|
||||||
@@ -1109,7 +1335,15 @@ def check_new_market_events(
|
|||||||
"dedup_enabled": True, "dedup_lookback_days": 3,
|
"dedup_enabled": True, "dedup_lookback_days": 3,
|
||||||
})
|
})
|
||||||
eco_cfg = _desk_cfg(eco_desk, {
|
eco_cfg = _desk_cfg(eco_desk, {
|
||||||
"z_threshold": eco_z_threshold, "days": eco_days,
|
"z_threshold": eco_z_threshold,
|
||||||
|
"days": eco_days,
|
||||||
|
"gauge_signals": {
|
||||||
|
"regime_transition": {"enabled": True},
|
||||||
|
"yield_curve_inversion": {"enabled": True, "threshold": 0.0},
|
||||||
|
"dxy_shock": {"enabled": True, "pct_threshold": 2.0},
|
||||||
|
"credit_stress": {"enabled": True, "pct_threshold": -1.5},
|
||||||
|
"gold_copper_ratio": {"enabled": True, "fear_threshold": 700, "growth_threshold": 500},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
tech_cfg = _desk_cfg(tech_desk, {
|
tech_cfg = _desk_cfg(tech_desk, {
|
||||||
"lookback_days": technical_lookback_days,
|
"lookback_days": technical_lookback_days,
|
||||||
@@ -1142,7 +1376,7 @@ def check_new_market_events(
|
|||||||
runners = [
|
runners = [
|
||||||
("news", lambda: _check_news(news_cfg)),
|
("news", lambda: _check_news(news_cfg)),
|
||||||
("fundamental", lambda: _check_fundamental(fundamental_cfg)),
|
("fundamental", lambda: _check_fundamental(fundamental_cfg)),
|
||||||
("eco", lambda: _check_eco(eco_cfg)),
|
("eco", lambda: _check_eco(eco_cfg) + _check_macro_gauges(eco_cfg)),
|
||||||
("technical", lambda: _check_technical(tech_cfg)),
|
("technical", lambda: _check_technical(tech_cfg)),
|
||||||
("reports", lambda: _check_reports(report_cfg)),
|
("reports", lambda: _check_reports(report_cfg)),
|
||||||
("sentiment", lambda: _check_sentiment(sentiment_cfg)),
|
("sentiment", lambda: _check_sentiment(sentiment_cfg)),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ interface SignalDef {
|
|||||||
interface AIDesk {
|
interface AIDesk {
|
||||||
id?: number
|
id?: number
|
||||||
name: string
|
name: string
|
||||||
type: 'news' | 'technical' | 'eco' | 'report'
|
type: 'news' | 'fundamental' | 'technical' | 'eco' | 'report' | 'sentiment'
|
||||||
active: boolean
|
active: boolean
|
||||||
system_prompt: string
|
system_prompt: string
|
||||||
instruments: string[]
|
instruments: string[]
|
||||||
|
|||||||
@@ -56,6 +56,18 @@ interface MacroRegime {
|
|||||||
asset_bias: Record<string, any>
|
asset_bias: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GaugeValue {
|
||||||
|
id: string; label: string; value: number | null; change_pct: number | null
|
||||||
|
unit: string; bloc: string; note?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MacroGaugeSnap {
|
||||||
|
snapshot_date: string
|
||||||
|
dominant: string
|
||||||
|
regime_scores: Record<string, number>
|
||||||
|
gauges: Record<string, GaugeValue>
|
||||||
|
}
|
||||||
|
|
||||||
interface Snapshot {
|
interface Snapshot {
|
||||||
instrument: InstrumentConfig
|
instrument: InstrumentConfig
|
||||||
price_data: PriceCandle[]
|
price_data: PriceCandle[]
|
||||||
@@ -621,6 +633,118 @@ function DriversPanel({ instrumentId, drivers, onSave, onClose }: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Macro gauge helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const SCENARIO_META: Record<string, { label: string; emoji: string; color: string }> = {
|
||||||
|
goldilocks: { label: 'Goldilocks', emoji: '🟢', color: '#10b981' },
|
||||||
|
desinflation: { label: 'Désinflation', emoji: '🔵', color: '#3b82f6' },
|
||||||
|
soft_landing: { label: 'Soft Landing', emoji: '🔷', color: '#06b6d4' },
|
||||||
|
reflation: { label: 'Reflation', emoji: '🟠', color: '#f97316' },
|
||||||
|
stagflation: { label: 'Stagflation', emoji: '🟡', color: '#f59e0b' },
|
||||||
|
inflation_shock: { label: 'Choc Inflationniste', emoji: '🔥', color: '#dc2626' },
|
||||||
|
recession: { label: 'Récession', emoji: '🔴', color: '#ef4444' },
|
||||||
|
crise_liquidite: { label: 'Crise de liquidité', emoji: '🟣', color: '#7c3aed' },
|
||||||
|
incertain: { label: 'Incertain', emoji: '⬜', color: '#64748b' },
|
||||||
|
}
|
||||||
|
|
||||||
|
function snapToMacroRegime(snap: MacroGaugeSnap): MacroRegime {
|
||||||
|
const scores = snap.regime_scores ?? {}
|
||||||
|
const meta = SCENARIO_META[snap.dominant] ?? SCENARIO_META.incertain
|
||||||
|
const ranked = Object.entries(scores)
|
||||||
|
.sort(([, a], [, b]) => b - a)
|
||||||
|
.map(([k]) => k)
|
||||||
|
return {
|
||||||
|
dominant: snap.dominant,
|
||||||
|
label: meta.label,
|
||||||
|
color: meta.color,
|
||||||
|
emoji: meta.emoji,
|
||||||
|
scores,
|
||||||
|
ranked,
|
||||||
|
asset_bias: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const BLOC_LABELS: Record<string, string> = {
|
||||||
|
liquidite: 'Liquidité / Taux',
|
||||||
|
credit: 'Crédit / Vol',
|
||||||
|
energie: 'Énergie',
|
||||||
|
metaux: 'Métaux',
|
||||||
|
croissance: 'Croissance US',
|
||||||
|
secteurs: 'Secteurs',
|
||||||
|
volatilite: 'Volatilité surf.',
|
||||||
|
global: 'Global / EM',
|
||||||
|
forex_ro: 'Forex Risk-Off',
|
||||||
|
derive: 'Dérivés',
|
||||||
|
}
|
||||||
|
|
||||||
|
function MacroGaugePanel({ snap, dateLabel }: { snap: MacroGaugeSnap; dateLabel: string }) {
|
||||||
|
const byBloc: Record<string, GaugeValue[]> = {}
|
||||||
|
for (const g of Object.values(snap.gauges)) {
|
||||||
|
if (g.value === null && g.change_pct === null) continue
|
||||||
|
const b = g.bloc ?? 'derive'
|
||||||
|
if (!byBloc[b]) byBloc[b] = []
|
||||||
|
byBloc[b].push(g)
|
||||||
|
}
|
||||||
|
const meta = SCENARIO_META[snap.dominant] ?? SCENARIO_META.incertain
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-dark-800/60 rounded-xl border border-slate-700/30 p-4 space-y-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-base">{meta.emoji}</span>
|
||||||
|
<span className="text-sm font-semibold text-white">{meta.label}</span>
|
||||||
|
<span className="text-xs text-slate-500">— contexte macro au {dateLabel}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-slate-600">{snap.snapshot_date}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Regime scores mini bar */}
|
||||||
|
<div className="flex gap-1 h-1.5">
|
||||||
|
{Object.entries(snap.regime_scores)
|
||||||
|
.sort(([,a],[,b]) => b - a)
|
||||||
|
.slice(0, 6)
|
||||||
|
.map(([k, v]) => (
|
||||||
|
<div key={k} title={`${SCENARIO_META[k]?.label ?? k}: ${(v*100).toFixed(0)}%`}
|
||||||
|
style={{ width: `${v*100}%`, background: SCENARIO_META[k]?.color ?? '#64748b' }}
|
||||||
|
className="rounded-full transition-all"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gauges by bloc */}
|
||||||
|
<div className="grid grid-cols-2 gap-x-6 gap-y-3">
|
||||||
|
{Object.entries(byBloc).map(([bloc, gauges]) => (
|
||||||
|
<div key={bloc}>
|
||||||
|
<div className="text-xs text-slate-600 uppercase tracking-wide mb-1">{BLOC_LABELS[bloc] ?? bloc}</div>
|
||||||
|
<div className="space-y-0.5">
|
||||||
|
{gauges.map(g => (
|
||||||
|
<div key={g.id} className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-slate-400 truncate max-w-[120px]" title={g.label}>{g.label}</span>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{g.value !== null && (
|
||||||
|
<span className="text-white font-mono">
|
||||||
|
{g.unit === '%' ? g.value.toFixed(2) + '%'
|
||||||
|
: g.unit === 'pts' ? g.value.toFixed(1)
|
||||||
|
: g.unit === 'ratio' ? g.value.toFixed(3)
|
||||||
|
: g.value.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{g.change_pct !== null && g.change_pct !== undefined && (
|
||||||
|
<span className={clsx('font-mono', g.change_pct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||||
|
{g.change_pct >= 0 ? '+' : ''}{g.change_pct.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Main page ─────────────────────────────────────────────────────────────────
|
// ── Main page ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const PERIODS = [
|
const PERIODS = [
|
||||||
@@ -641,6 +765,7 @@ export default function InstrumentDashboard() {
|
|||||||
const [selectedDate, setSelectedDate] = useState<string | null>(null)
|
const [selectedDate, setSelectedDate] = useState<string | null>(null)
|
||||||
const [editDrivers, setEditDrivers] = useState(false)
|
const [editDrivers, setEditDrivers] = useState(false)
|
||||||
const [localDrivers, setLocalDrivers] = useState<Driver[] | null>(null)
|
const [localDrivers, setLocalDrivers] = useState<Driver[] | null>(null)
|
||||||
|
const [macroAtDate, setMacroAtDate] = useState<MacroGaugeSnap | null>(null)
|
||||||
|
|
||||||
const instrumentId = id.toUpperCase()
|
const instrumentId = id.toUpperCase()
|
||||||
|
|
||||||
@@ -655,6 +780,7 @@ export default function InstrumentDashboard() {
|
|||||||
setSelectedDate(null)
|
setSelectedDate(null)
|
||||||
setLocalDrivers(null)
|
setLocalDrivers(null)
|
||||||
setEditDrivers(false)
|
setEditDrivers(false)
|
||||||
|
setMacroAtDate(null)
|
||||||
api.get(`/instruments/${instrumentId}/snapshot?period=${period}`)
|
api.get(`/instruments/${instrumentId}/snapshot?period=${period}`)
|
||||||
.then(r => {
|
.then(r => {
|
||||||
setSnapshot(r.data)
|
setSnapshot(r.data)
|
||||||
@@ -691,6 +817,16 @@ export default function InstrumentDashboard() {
|
|||||||
return { priceMap, indMap, sortedDates, dateIndex }
|
return { priceMap, indMap, sortedDates, dateIndex }
|
||||||
}, [snapshot])
|
}, [snapshot])
|
||||||
|
|
||||||
|
// Fetch macro gauge context when crosshair date changes (after sortedDates is available)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedDate || !sortedDates.length) return
|
||||||
|
const isLast = selectedDate === sortedDates[sortedDates.length - 1]
|
||||||
|
if (isLast) { setMacroAtDate(null); return }
|
||||||
|
api.get(`/market/macro-gauges/at?date=${selectedDate}`)
|
||||||
|
.then(r => { if (r.data?.snapshot_date) setMacroAtDate(r.data) })
|
||||||
|
.catch(() => {})
|
||||||
|
}, [selectedDate, sortedDates])
|
||||||
|
|
||||||
const effectiveDate = useMemo(() => {
|
const effectiveDate = useMemo(() => {
|
||||||
if (selectedDate && dateIndex[selectedDate] !== undefined) return selectedDate
|
if (selectedDate && dateIndex[selectedDate] !== undefined) return selectedDate
|
||||||
return sortedDates[sortedDates.length - 1] ?? null
|
return sortedDates[sortedDates.length - 1] ?? null
|
||||||
@@ -882,7 +1018,7 @@ export default function InstrumentDashboard() {
|
|||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<RegimeCard
|
<RegimeCard
|
||||||
regime={snapshot.regime}
|
regime={snapshot.regime}
|
||||||
macroRegime={snapshot.macro_regime ?? null}
|
macroRegime={macroAtDate ? snapToMacroRegime(macroAtDate) : (snapshot.macro_regime ?? null)}
|
||||||
signalsAt={dateSignals}
|
signalsAt={dateSignals}
|
||||||
dateLabel={dateLabel}
|
dateLabel={dateLabel}
|
||||||
/>
|
/>
|
||||||
@@ -896,6 +1032,11 @@ export default function InstrumentDashboard() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Macro gauge detail panel — shown when on a historical date */}
|
||||||
|
{macroAtDate && (
|
||||||
|
<MacroGaugePanel snap={macroAtDate} dateLabel={dateLabel} />
|
||||||
|
)}
|
||||||
|
|
||||||
{editDrivers && (
|
{editDrivers && (
|
||||||
<DriversPanel
|
<DriversPanel
|
||||||
instrumentId={instrumentId}
|
instrumentId={instrumentId}
|
||||||
|
|||||||
Reference in New Issue
Block a user