refactor: separate Sentiment and Technical desks — no signal overlap
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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 (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs text-slate-400">Compteurs macro surveillés</label>
|
||||
<button
|
||||
onClick={() => onChange(selected.length === MACRO_GAUGE_LIST.length ? [] : MACRO_GAUGE_LIST.map(g => g.id))}
|
||||
className="text-xs text-rose-400 hover:text-rose-300"
|
||||
>
|
||||
{selected.length === MACRO_GAUGE_LIST.length ? 'Tout désélectionner' : 'Tout sélectionner'}
|
||||
</button>
|
||||
</div>
|
||||
{blocs.map(bloc => {
|
||||
const gauges = MACRO_GAUGE_LIST.filter(g => g.bloc === bloc)
|
||||
return (
|
||||
<div key={bloc}>
|
||||
<div className="text-[10px] text-slate-600 uppercase tracking-wider mb-1">{bloc}</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{gauges.map(g => (
|
||||
<button
|
||||
key={g.id}
|
||||
onClick={() => toggle(g.id)}
|
||||
className={clsx(
|
||||
'px-2 py-0.5 rounded text-xs border transition-colors',
|
||||
selected.includes(g.id)
|
||||
? 'bg-rose-900/40 border-rose-600/60 text-rose-300'
|
||||
: 'bg-dark-800 border-slate-700/40 text-slate-500 hover:border-slate-600',
|
||||
)}
|
||||
>
|
||||
{g.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
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({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Instruments */}
|
||||
<InstrumentSelector
|
||||
selected={d.instruments}
|
||||
onChange={v => set('instruments', v)}
|
||||
/>
|
||||
{/* Instruments / Gauges */}
|
||||
{d.type === 'sentiment' ? (
|
||||
<MacroGaugeSelector
|
||||
selected={d.instruments}
|
||||
onChange={v => set('instruments', v)}
|
||||
/>
|
||||
) : (
|
||||
<InstrumentSelector
|
||||
selected={d.instruments}
|
||||
onChange={v => set('instruments', v)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Type-specific config */}
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user