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:
@@ -455,6 +455,89 @@ function FundamentalConfig({
|
||||
}
|
||||
|
||||
|
||||
function GaugeThresholdConfig({
|
||||
selectedGauges,
|
||||
config,
|
||||
onChange,
|
||||
}: {
|
||||
selectedGauges: string[]
|
||||
config: Record<string, any>
|
||||
onChange: (c: Record<string, any>) => void
|
||||
}) {
|
||||
if (!selectedGauges.length) return null
|
||||
|
||||
const thresholds: Record<string, any> = 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 (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-xs text-slate-500 mb-2 flex items-center gap-2">
|
||||
<span>Alertes par compteur macro sélectionné</span>
|
||||
<span className="text-slate-700">— laisser vide pour désactiver le critère</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-[auto_1fr_auto_auto_auto] gap-x-3 gap-y-1 items-center text-[10px] text-slate-600 px-1 mb-1">
|
||||
<span />
|
||||
<span>Compteur</span>
|
||||
<span className="text-center w-20">≤ Seuil bas</span>
|
||||
<span className="text-center w-20">≥ Seuil haut</span>
|
||||
<span className="text-center w-16">Δ% alerte</span>
|
||||
</div>
|
||||
{selectedGauges.map(gaugeId => {
|
||||
const gauge = getGauge(gaugeId)
|
||||
const cfg_g = thresholds[gaugeId] ?? { enabled: false }
|
||||
const enabled = cfg_g.enabled ?? false
|
||||
return (
|
||||
<div key={gaugeId} className={clsx(
|
||||
'grid grid-cols-[auto_1fr_auto_auto_auto] gap-x-3 items-center px-3 py-2 rounded-lg border transition-colors',
|
||||
enabled ? 'border-rose-700/40 bg-rose-900/10' : 'border-slate-700/30 bg-dark-800/60',
|
||||
)}>
|
||||
<button onClick={() => update(gaugeId, 'enabled', !enabled)} className="shrink-0">
|
||||
{enabled
|
||||
? <ToggleRight className="w-5 h-5 text-rose-400" />
|
||||
: <ToggleLeft className="w-5 h-5 text-slate-600" />}
|
||||
</button>
|
||||
<span className={clsx('text-sm', enabled ? 'text-white' : 'text-slate-500')}>
|
||||
{gauge?.label ?? gaugeId}
|
||||
<span className="ml-1.5 text-[10px] text-slate-600">{gauge?.bloc}</span>
|
||||
</span>
|
||||
<input
|
||||
type="number" step="any" placeholder="—"
|
||||
disabled={!enabled}
|
||||
value={cfg_g.low_threshold ?? ''}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
<input
|
||||
type="number" step="any" placeholder="—"
|
||||
disabled={!enabled}
|
||||
value={cfg_g.high_threshold ?? ''}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
<input
|
||||
type="number" step="any" placeholder="—"
|
||||
disabled={!enabled}
|
||||
value={cfg_g.change_pct_threshold ?? ''}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
function ReportConfig({
|
||||
config,
|
||||
onChange,
|
||||
@@ -644,6 +727,18 @@ function DeskEditor({
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Gauge threshold alerts — Sentiment only */}
|
||||
{d.type === 'sentiment' && d.instruments.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-400 mb-2">Configuration des alertes par compteur</div>
|
||||
<GaugeThresholdConfig
|
||||
selectedGauges={d.instruments}
|
||||
config={d.config}
|
||||
onChange={c => set('config', c)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-between pt-2 border-t border-slate-700/30">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user