From 27ee4410cfd457aad56fa9bb51460cdab2bd8331 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Fri, 26 Jun 2026 13:48:14 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20separate=20Sentiment=20and=20Techni?= =?UTF-8?q?cal=20desks=20=E2=80=94=20no=20signal=20overlap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Mark 6 price signals (ma_cross, rsi_extreme, bb_squeeze, new_52w_extreme, price_gap, volume_spike) as desk_type=technical so they no longer appear in the Sentiment desk - Sentiment desk now shows MacroGaugeSelector (32 gauges grouped by bloc: Liquidité, Crédit, Volatilité, Métaux…) instead of the stock/ETF instrument picker - Sentiment desk instruments seed updated to macro gauge keys (vix, vvix, skew, hyg, dxy, slope_10y3m, gold_copper_ratio) - Signal init useEffect extended to cover sentiment desk as well as technical Co-Authored-By: Claude Sonnet 4.6 --- backend/routers/ai_desks.py | 6 ++ backend/services/database.py | 2 +- frontend/src/pages/AIDesks.tsx | 121 +++++++++++++++++++++++++++++++-- 3 files changed, 121 insertions(+), 8 deletions(-) diff --git a/backend/routers/ai_desks.py b/backend/routers/ai_desks.py index 21644a3..b6bec93 100644 --- a/backend/routers/ai_desks.py +++ b/backend/routers/ai_desks.py @@ -19,6 +19,7 @@ SIGNAL_CATALOG: List[Dict[str, Any]] = [ "id": "ma_cross", "label": "Croisement de moyennes mobiles", "description": "Détecte les croisements bullish/bearish entre deux MAs", + "desk_type": "technical", "params": { "pairs": { "type": "pairs", "label": "Paires MA", @@ -31,6 +32,7 @@ SIGNAL_CATALOG: List[Dict[str, Any]] = [ "id": "rsi_extreme", "label": "RSI extrême", "description": "Signal quand le RSI dépasse les seuils de surachat/survente", + "desk_type": "technical", "params": { "period": {"type": "int", "label": "Période", "default": 14, "min": 5, "max": 50}, "oversold": {"type": "int", "label": "Survente", "default": 30, "min": 10, "max": 45}, @@ -41,6 +43,7 @@ SIGNAL_CATALOG: List[Dict[str, Any]] = [ "id": "bb_squeeze", "label": "Squeeze Bollinger Bands", "description": "Détecte quand les bandes BB se resserrent en dessous d'un seuil", + "desk_type": "technical", "params": { "period": {"type": "int", "label": "Période", "default": 20, "min": 10, "max": 50}, "std": {"type": "float", "label": "Std dev", "default": 2.0, "min": 1.0, "max": 3.0}, @@ -51,6 +54,7 @@ SIGNAL_CATALOG: List[Dict[str, Any]] = [ "id": "new_52w_extreme", "label": "Nouveau 52 semaines extrême", "description": "Nouveau plus haut ou plus bas sur 52 semaines (avec buffer)", + "desk_type": "technical", "params": { "buffer_pct": {"type": "float", "label": "Buffer %", "default": 0.5, "min": 0.0, "max": 5.0}, }, @@ -59,6 +63,7 @@ SIGNAL_CATALOG: List[Dict[str, Any]] = [ "id": "price_gap", "label": "Gap de prix", "description": "Gap d'ouverture significatif par rapport à la clôture précédente", + "desk_type": "technical", "params": { "min_gap_pct": {"type": "float", "label": "Gap min %", "default": 1.5, "min": 0.5, "max": 10.0}, }, @@ -67,6 +72,7 @@ SIGNAL_CATALOG: List[Dict[str, Any]] = [ "id": "volume_spike", "label": "Spike de volume", "description": "Volume anormalement élevé par rapport à la moyenne mobile", + "desk_type": "technical", "params": { "ma_period": {"type": "int", "label": "Période MA vol", "default": 20, "min": 5, "max": 50}, "spike_factor": {"type": "float", "label": "Facteur spike", "default": 2.5, "min": 1.5, "max": 10.0}, diff --git a/backend/services/database.py b/backend/services/database.py index c91f49b..ca15087 100644 --- a/backend/services/database.py +++ b/backend/services/database.py @@ -1098,7 +1098,7 @@ def init_db(): "SKEW > 140 = marché paye cher pour la protection downside. " "Inversion terme structure VIX (front > back) = peur concentrée court terme." ), - "instruments": json.dumps(["SPY","QQQ","VXX","TLT","HYG","GLD"]), + "instruments": json.dumps(["vix","vvix","skew","hyg","dxy","slope_10y3m","gold_copper_ratio"]), "config": json.dumps({ "lookback_days": 5, "signals": { diff --git a/frontend/src/pages/AIDesks.tsx b/frontend/src/pages/AIDesks.tsx index a6c6fbc..2c25b27 100644 --- a/frontend/src/pages/AIDesks.tsx +++ b/frontend/src/pages/AIDesks.tsx @@ -38,6 +38,54 @@ const ALL_INSTRUMENTS = [ 'VXX','AAPL','NVDA','GS','XOM','BTC-USD', ] +// Macro regime gauges from MACRO_GAUGE_CONFIG (backend data_fetcher.py) +const MACRO_GAUGE_LIST: { id: string; label: string; bloc: string }[] = [ + // Liquidité / Taux + { id: 'dxy', label: 'Dollar DXY', bloc: 'Liquidité' }, + { id: 'us10y', label: 'UST 10Y', bloc: 'Liquidité' }, + { id: 'us3m', label: 'UST 3M', bloc: 'Liquidité' }, + { id: 'tips', label: 'TIPS ETF', bloc: 'Liquidité' }, + { id: 'tlt', label: 'TLT 20Y+', bloc: 'Liquidité' }, + // Crédit + { id: 'vix', label: 'VIX', bloc: 'Crédit' }, + { id: 'hyg', label: 'HYG (HY Bonds)', bloc: 'Crédit' }, + { id: 'lqd', label: 'LQD (IG Bonds)', bloc: 'Crédit' }, + { id: 'ief', label: 'IEF (7-10Y)', bloc: 'Crédit' }, + // Énergie + { id: 'brent', label: 'Brent', bloc: 'Énergie' }, + { id: 'ng', label: 'Gaz naturel', bloc: 'Énergie' }, + // Métaux + { id: 'gold', label: 'Or', bloc: 'Métaux' }, + { id: 'silver', label: 'Argent', bloc: 'Métaux' }, + { id: 'copper', label: 'Cuivre', bloc: 'Métaux' }, + // Croissance + { id: 'spx', label: 'S&P 500', bloc: 'Croissance' }, + { id: 'iwm', label: 'Russell 2000', bloc: 'Croissance' }, + { id: 'xli', label: 'Industriels XLI', bloc: 'Croissance' }, + // Secteurs + { id: 'xlk', label: 'Tech XLK', bloc: 'Secteurs' }, + { id: 'xlf', label: 'Financières XLF', bloc: 'Secteurs' }, + { id: 'xlp', label: 'Conso. défensif', bloc: 'Secteurs' }, + { id: 'xlu', label: 'Utilities XLU', bloc: 'Secteurs' }, + // Volatilité + { id: 'vvix', label: 'VVIX (vol-of-vol)', bloc: 'Volatilité' }, + { id: 'skew', label: 'CBOE SKEW', bloc: 'Volatilité' }, + { id: 'ovx', label: 'OVX (Oil Vol)', bloc: 'Volatilité' }, + { id: 'gvz', label: 'GVZ (Gold Vol)', bloc: 'Volatilité' }, + // Global / EM + { id: 'eem', label: 'EM Actions EEM', bloc: 'Global' }, + { id: 'emb', label: 'EM Bonds EMB', bloc: 'Global' }, + { id: 'fxi', label: 'Chine FXI', bloc: 'Global' }, + // Forex risk-off + { id: 'usdjpy', label: 'USD/JPY', bloc: 'Forex' }, + // Dérivés calculés + { id: 'slope_10y3m', label: 'Slope 10Y-3M', bloc: 'Dérivés' }, + { id: 'gold_copper_ratio',label: 'Ratio Or/Cuivre', bloc: 'Dérivés' }, + { id: 'spx_vs_200d', label: 'SPX vs MA 200j', bloc: 'Dérivés' }, +] + +const MACRO_BLOC_ORDER = ['Liquidité','Crédit','Volatilité','Métaux','Énergie','Croissance','Secteurs','Global','Forex','Dérivés'] + const TYPE_LABELS: Record = { news: 'News', fundamental: 'Fondamental', @@ -101,6 +149,58 @@ function InstrumentSelector({ } +function MacroGaugeSelector({ + selected, + onChange, +}: { + selected: string[] + onChange: (v: string[]) => void +}) { + const toggle = (id: string) => + onChange(selected.includes(id) ? selected.filter(x => x !== id) : [...selected, id]) + + const blocs = MACRO_BLOC_ORDER.filter(b => MACRO_GAUGE_LIST.some(g => g.bloc === b)) + + return ( +
+
+ + +
+ {blocs.map(bloc => { + const gauges = MACRO_GAUGE_LIST.filter(g => g.bloc === bloc) + return ( +
+
{bloc}
+
+ {gauges.map(g => ( + + ))} +
+
+ ) + })} +
+ ) +} + + function SignalToggle({ signal, value, @@ -405,9 +505,9 @@ function DeskEditor({ const set = (k: keyof AIDesk, v: any) => setD(prev => ({ ...prev, [k]: v })) const dirty = JSON.stringify(d) !== JSON.stringify(desk) - // Ensure signals config is initialized from catalog for technical desks + // Ensure signals config is initialized from catalog for technical + sentiment desks useEffect(() => { - if (d.type !== 'technical' || !catalog.length) return + if ((d.type !== 'technical' && d.type !== 'sentiment') || !catalog.length) return const signals = d.config.signals ?? {} let changed = false const next = { ...signals } @@ -477,11 +577,18 @@ function DeskEditor({ /> - {/* Instruments */} - set('instruments', v)} - /> + {/* Instruments / Gauges */} + {d.type === 'sentiment' ? ( + set('instruments', v)} + /> + ) : ( + set('instruments', v)} + /> + )} {/* Type-specific config */}