feat: Sentiment desk — gauge threshold alerts (Option B)

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 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-26 13:59:40 +02:00
parent 27ee4410cf
commit d0816fd9ff
2 changed files with 212 additions and 0 deletions

View File

@@ -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