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",
|
"id": "ma_cross",
|
||||||
"label": "Croisement de moyennes mobiles",
|
"label": "Croisement de moyennes mobiles",
|
||||||
"description": "Détecte les croisements bullish/bearish entre deux MAs",
|
"description": "Détecte les croisements bullish/bearish entre deux MAs",
|
||||||
|
"desk_type": "technical",
|
||||||
"params": {
|
"params": {
|
||||||
"pairs": {
|
"pairs": {
|
||||||
"type": "pairs", "label": "Paires MA",
|
"type": "pairs", "label": "Paires MA",
|
||||||
@@ -31,6 +32,7 @@ SIGNAL_CATALOG: List[Dict[str, Any]] = [
|
|||||||
"id": "rsi_extreme",
|
"id": "rsi_extreme",
|
||||||
"label": "RSI extrême",
|
"label": "RSI extrême",
|
||||||
"description": "Signal quand le RSI dépasse les seuils de surachat/survente",
|
"description": "Signal quand le RSI dépasse les seuils de surachat/survente",
|
||||||
|
"desk_type": "technical",
|
||||||
"params": {
|
"params": {
|
||||||
"period": {"type": "int", "label": "Période", "default": 14, "min": 5, "max": 50},
|
"period": {"type": "int", "label": "Période", "default": 14, "min": 5, "max": 50},
|
||||||
"oversold": {"type": "int", "label": "Survente", "default": 30, "min": 10, "max": 45},
|
"oversold": {"type": "int", "label": "Survente", "default": 30, "min": 10, "max": 45},
|
||||||
@@ -41,6 +43,7 @@ SIGNAL_CATALOG: List[Dict[str, Any]] = [
|
|||||||
"id": "bb_squeeze",
|
"id": "bb_squeeze",
|
||||||
"label": "Squeeze Bollinger Bands",
|
"label": "Squeeze Bollinger Bands",
|
||||||
"description": "Détecte quand les bandes BB se resserrent en dessous d'un seuil",
|
"description": "Détecte quand les bandes BB se resserrent en dessous d'un seuil",
|
||||||
|
"desk_type": "technical",
|
||||||
"params": {
|
"params": {
|
||||||
"period": {"type": "int", "label": "Période", "default": 20, "min": 10, "max": 50},
|
"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},
|
"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",
|
"id": "new_52w_extreme",
|
||||||
"label": "Nouveau 52 semaines extrême",
|
"label": "Nouveau 52 semaines extrême",
|
||||||
"description": "Nouveau plus haut ou plus bas sur 52 semaines (avec buffer)",
|
"description": "Nouveau plus haut ou plus bas sur 52 semaines (avec buffer)",
|
||||||
|
"desk_type": "technical",
|
||||||
"params": {
|
"params": {
|
||||||
"buffer_pct": {"type": "float", "label": "Buffer %", "default": 0.5, "min": 0.0, "max": 5.0},
|
"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",
|
"id": "price_gap",
|
||||||
"label": "Gap de prix",
|
"label": "Gap de prix",
|
||||||
"description": "Gap d'ouverture significatif par rapport à la clôture précédente",
|
"description": "Gap d'ouverture significatif par rapport à la clôture précédente",
|
||||||
|
"desk_type": "technical",
|
||||||
"params": {
|
"params": {
|
||||||
"min_gap_pct": {"type": "float", "label": "Gap min %", "default": 1.5, "min": 0.5, "max": 10.0},
|
"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",
|
"id": "volume_spike",
|
||||||
"label": "Spike de volume",
|
"label": "Spike de volume",
|
||||||
"description": "Volume anormalement élevé par rapport à la moyenne mobile",
|
"description": "Volume anormalement élevé par rapport à la moyenne mobile",
|
||||||
|
"desk_type": "technical",
|
||||||
"params": {
|
"params": {
|
||||||
"ma_period": {"type": "int", "label": "Période MA vol", "default": 20, "min": 5, "max": 50},
|
"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},
|
"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. "
|
"SKEW > 140 = marché paye cher pour la protection downside. "
|
||||||
"Inversion terme structure VIX (front > back) = peur concentrée court terme."
|
"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({
|
"config": json.dumps({
|
||||||
"lookback_days": 5,
|
"lookback_days": 5,
|
||||||
"signals": {
|
"signals": {
|
||||||
|
|||||||
@@ -38,6 +38,54 @@ const ALL_INSTRUMENTS = [
|
|||||||
'VXX','AAPL','NVDA','GS','XOM','BTC-USD',
|
'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> = {
|
const TYPE_LABELS: Record<string, string> = {
|
||||||
news: 'News',
|
news: 'News',
|
||||||
fundamental: 'Fondamental',
|
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({
|
function SignalToggle({
|
||||||
signal,
|
signal,
|
||||||
value,
|
value,
|
||||||
@@ -405,9 +505,9 @@ function DeskEditor({
|
|||||||
const set = (k: keyof AIDesk, v: any) => setD(prev => ({ ...prev, [k]: v }))
|
const set = (k: keyof AIDesk, v: any) => setD(prev => ({ ...prev, [k]: v }))
|
||||||
const dirty = JSON.stringify(d) !== JSON.stringify(desk)
|
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(() => {
|
useEffect(() => {
|
||||||
if (d.type !== 'technical' || !catalog.length) return
|
if ((d.type !== 'technical' && d.type !== 'sentiment') || !catalog.length) return
|
||||||
const signals = d.config.signals ?? {}
|
const signals = d.config.signals ?? {}
|
||||||
let changed = false
|
let changed = false
|
||||||
const next = { ...signals }
|
const next = { ...signals }
|
||||||
@@ -477,11 +577,18 @@ function DeskEditor({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Instruments */}
|
{/* Instruments / Gauges */}
|
||||||
<InstrumentSelector
|
{d.type === 'sentiment' ? (
|
||||||
selected={d.instruments}
|
<MacroGaugeSelector
|
||||||
onChange={v => set('instruments', v)}
|
selected={d.instruments}
|
||||||
/>
|
onChange={v => set('instruments', v)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<InstrumentSelector
|
||||||
|
selected={d.instruments}
|
||||||
|
onChange={v => set('instruments', v)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Type-specific config */}
|
{/* Type-specific config */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user