From d0816fd9ff9979694ed36f590b6afc8752aaa146 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Fri, 26 Jun 2026 13:59:40 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Sentiment=20desk=20=E2=80=94=20gauge=20?= =?UTF-8?q?threshold=20alerts=20(Option=20B)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each macro gauge selected in the Sentiment desk can now be independently configured with three alert criteria: - Seuil bas (≤): alert when value crosses below - Seuil haut (≥): alert when value crosses above - Variation % (Δ%): alert when N-day % change exceeds threshold Frontend: GaugeThresholdConfig component — one row per selected gauge, compact grid layout with enable toggle + 3 numeric inputs. Stored in config.gauge_thresholds[gauge_id]. Backend: _check_sentiment() extended — after CBOE signals, reads macro_gauge_snapshots history, checks each enabled gauge threshold, emits sentiment market_events with options_note for each breach. Gauge → affected_assets mapping covers all 32 gauge keys. Co-Authored-By: Claude Sonnet 4.6 --- backend/services/market_event_detector.py | 117 ++++++++++++++++++++++ frontend/src/pages/AIDesks.tsx | 95 ++++++++++++++++++ 2 files changed, 212 insertions(+) diff --git a/backend/services/market_event_detector.py b/backend/services/market_event_detector.py index ccc720f..c125c97 100644 --- a/backend/services/market_event_detector.py +++ b/backend/services/market_event_detector.py @@ -972,6 +972,123 @@ def _check_sentiment(desk_cfg: Dict[str, Any]) -> List[Dict[str, Any]]: ["VXX","SPY"], "Skew bas → acheter protection bon marché (puts OTM relativement peu chers).") + # ── Custom gauge threshold alerts ──────────────────────────────────────────── + gauge_thresholds = desk_cfg.get("gauge_thresholds", {}) + selected_gauges = desk_cfg.get("_instruments") or [] + + if gauge_thresholds and selected_gauges: + from services.database import get_macro_gauge_history + from services.data_fetcher import MACRO_GAUGE_CONFIG + gauge_label_map = {gid: label for gid, label, _, _, _ in MACRO_GAUGE_CONFIG} + gauge_label_map.update({ + "slope_10y3m": "Slope 10Y-3M", + "gold_copper_ratio": "Ratio Or/Cuivre", + "spx_vs_200d": "SPX vs MA 200j", + }) + _gauge_assets: Dict[str, List[str]] = { + "dxy": ["GLD", "EEM", "EURUSD=X"], + "us10y": ["TLT", "IEF", "SPY"], + "us3m": ["TLT", "IEF"], + "tips": ["TLT", "GLD"], + "tlt": ["TLT", "IEF", "SPY"], + "vix": ["VXX", "SPY", "QQQ"], + "hyg": ["HYG", "LQD", "SPY"], + "lqd": ["LQD", "HYG", "TLT"], + "ief": ["IEF", "TLT"], + "brent": ["USO", "XOM"], + "ng": ["UNG", "XOM"], + "gold": ["GLD", "SLV"], + "silver": ["SLV", "GLD"], + "copper": ["XLI", "EEM"], + "spx": ["SPY", "QQQ"], + "iwm": ["IWM", "SPY"], + "xli": ["XLI", "SPY"], + "xlk": ["XLK", "QQQ"], + "xlf": ["XLF", "SPY"], + "xlp": ["XLP", "SPY"], + "xlu": ["XLU", "SPY"], + "vvix": ["VXX", "SPY"], + "skew": ["SPY", "QQQ", "TLT"], + "ovx": ["USO", "XOM"], + "gvz": ["GLD", "SLV"], + "eem": ["EEM", "EFA"], + "emb": ["EMB", "EEM"], + "fxi": ["FXI", "EEM"], + "usdjpy": ["USDJPY=X", "GLD"], + "slope_10y3m": ["TLT", "SPY", "HYG"], + "gold_copper_ratio":["GLD", "EEM"], + "spx_vs_200d": ["SPY", "QQQ", "VXX"], + } + + history = get_macro_gauge_history(days=lookback_days + 2) + if len(history) >= 1: + latest_snap = history[0] + latest_gauges = latest_snap.get("gauges", {}) + latest_date = latest_snap["snapshot_date"] + oldest_gauges = history[-1].get("gauges", {}) if len(history) > 1 else {} + + for gauge_id in selected_gauges: + cfg_g = gauge_thresholds.get(gauge_id, {}) + if not cfg_g.get("enabled", False): + continue + + gauge_data = latest_gauges.get(gauge_id, {}) + value = gauge_data.get("value") + if value is None: + continue + value = float(value) + + label = gauge_label_map.get(gauge_id, gauge_id) + assets = _gauge_assets.get(gauge_id, []) + old_data = oldest_gauges.get(gauge_id, {}) + old_value = old_data.get("value") + old_value = float(old_value) if old_value is not None else None + + low_thr = cfg_g.get("low_threshold") + high_thr = cfg_g.get("high_threshold") + chg_thr = cfg_g.get("change_pct_threshold") + + # High threshold crossing (old below, now at or above) + if high_thr is not None: + high_thr = float(high_thr) + crossed = (old_value is not None and old_value < high_thr <= value) + at_level = (old_value is None and value >= high_thr) + if crossed or at_level: + name = f"{label} franchit {high_thr:.2g} à la hausse ({latest_date[:7]})" + prev_str = f" (précédent: {old_value:.2g})" if old_value is not None else "" + _emit(name, latest_date, "bearish", f"{gauge_id.upper()} High", + 0.65, + f"{label} dépasse le seuil haut {high_thr:.2g}{prev_str} → valeur: {value:.2g}.", + assets, + f"Niveau haut sur {label} — surveiller exposition options.") + + # Low threshold crossing (old above, now at or below) + if low_thr is not None: + low_thr = float(low_thr) + crossed = (old_value is not None and old_value > low_thr >= value) + at_level = (old_value is None and value <= low_thr) + if crossed or at_level: + name = f"{label} passe sous {low_thr:.2g} ({latest_date[:7]})" + prev_str = f" (précédent: {old_value:.2g})" if old_value is not None else "" + _emit(name, latest_date, "bullish", f"{gauge_id.upper()} Low", + 0.65, + f"{label} passe sous le seuil bas {low_thr:.2g}{prev_str} → valeur: {value:.2g}.", + assets, + f"Niveau bas sur {label} — opportunité ou signal de retournement.") + + # Change % threshold (absolute value) + if chg_thr is not None and old_value and old_value > 0: + pct_chg = (value - old_value) / old_value * 100 + if abs(pct_chg) >= abs(float(chg_thr)): + sign = "+" if pct_chg > 0 else "" + direction = "bullish" if pct_chg > 0 else "bearish" + name = f"{label} variation {sign}{pct_chg:.1f}% ({latest_date[:7]})" + _emit(name, latest_date, direction, f"{gauge_id.upper()} Move", + min(0.80, 0.45 + abs(pct_chg) * 0.02), + f"{label} {sign}{pct_chg:.1f}% sur la période ({old_value:.2g} → {value:.2g}). Mouvement significatif.", + assets, + f"Mouvement {sign}{pct_chg:.1f}% sur {label} — ajuster stratégie de vol.") + return created diff --git a/frontend/src/pages/AIDesks.tsx b/frontend/src/pages/AIDesks.tsx index 2c25b27..187850f 100644 --- a/frontend/src/pages/AIDesks.tsx +++ b/frontend/src/pages/AIDesks.tsx @@ -455,6 +455,89 @@ function FundamentalConfig({ } +function GaugeThresholdConfig({ + selectedGauges, + config, + onChange, +}: { + selectedGauges: string[] + config: Record + onChange: (c: Record) => void +}) { + if (!selectedGauges.length) return null + + const thresholds: Record = config.gauge_thresholds ?? {} + + const update = (gaugeId: string, field: string, val: any) => { + const current = thresholds[gaugeId] ?? { enabled: false } + onChange({ + ...config, + gauge_thresholds: { ...thresholds, [gaugeId]: { ...current, [field]: val } }, + }) + } + + const getGauge = (id: string) => MACRO_GAUGE_LIST.find(g => g.id === id) + + return ( +
+
+ Alertes par compteur macro sélectionné + — laisser vide pour désactiver le critère +
+
+ + Compteur + ≤ Seuil bas + ≥ Seuil haut + Δ% alerte +
+ {selectedGauges.map(gaugeId => { + const gauge = getGauge(gaugeId) + const cfg_g = thresholds[gaugeId] ?? { enabled: false } + const enabled = cfg_g.enabled ?? false + return ( +
+ + + {gauge?.label ?? gaugeId} + {gauge?.bloc} + + update(gaugeId, 'low_threshold', e.target.value === '' ? null : parseFloat(e.target.value))} + className="w-20 bg-dark-900 border border-slate-700/40 rounded px-2 py-1 text-xs text-white placeholder-slate-700 disabled:opacity-30" + /> + update(gaugeId, 'high_threshold', e.target.value === '' ? null : parseFloat(e.target.value))} + className="w-20 bg-dark-900 border border-slate-700/40 rounded px-2 py-1 text-xs text-white placeholder-slate-700 disabled:opacity-30" + /> + update(gaugeId, 'change_pct_threshold', e.target.value === '' ? null : parseFloat(e.target.value))} + className="w-16 bg-dark-900 border border-slate-700/40 rounded px-2 py-1 text-xs text-white placeholder-slate-700 disabled:opacity-30" + /> +
+ ) + })} +
+ ) +} + + function ReportConfig({ config, onChange, @@ -644,6 +727,18 @@ function DeskEditor({ })()} + {/* Gauge threshold alerts — Sentiment only */} + {d.type === 'sentiment' && d.instruments.length > 0 && ( +
+
Configuration des alertes par compteur
+ set('config', c)} + /> +
+ )} + {/* Actions */}