feat: 6 AI desks complets — fundamental, report, sentiment (options triggers)

- Fundamental Desk: filtre corporate news (layoffs, M&A, earnings, credit)
  avec prompt dédié + dedup sémantique
- Report Desk: wiring desk config (days, min_importance, system_prompt)
- Sentiment Desk: 5 signaux VIX/SKEW pour options lab
  (vix_level thresholds, vix_spike %, vix_term_structure, vvix_extreme, skew_extreme)
  Chaque event contient options_note actionnable (vente puts, straddles, calendar spreads)
- check_new_market_events() couvre les 6 sources, charge desk configs dynamiquement
- Signal catalog: 12 signaux (7 technical + 5 sentiment), filtrés par desk_type dans UI
- AIDesks.tsx: FundamentalConfig + ReportConfig + SignalToggle filtré par desk type
- 3 nouveaux desks seedés dans init_db (idempotent)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-26 09:21:29 +02:00
parent 97706dea7b
commit a12d7a1ef3
4 changed files with 670 additions and 90 deletions

View File

@@ -17,6 +17,7 @@ interface SignalDef {
id: string
label: string
description: string
desk_type?: string
params: Record<string, SignalParam>
}
@@ -38,17 +39,21 @@ const ALL_INSTRUMENTS = [
]
const TYPE_LABELS: Record<string, string> = {
news: 'News',
technical: 'Technical',
eco: 'Économique',
report: 'Report',
news: 'News',
fundamental: 'Fondamental',
technical: 'Technical',
eco: 'Économique',
report: 'Report',
sentiment: 'Sentiment',
}
const TYPE_COLORS: Record<string, string> = {
news: 'text-blue-400 bg-blue-900/20 border-blue-700/40',
technical: 'text-cyan-400 bg-cyan-900/20 border-cyan-700/40',
eco: 'text-emerald-400 bg-emerald-900/20 border-emerald-700/40',
report: 'text-violet-400 bg-violet-900/20 border-violet-700/40',
news: 'text-blue-400 bg-blue-900/20 border-blue-700/40',
fundamental: 'text-amber-400 bg-amber-900/20 border-amber-700/40',
technical: 'text-cyan-400 bg-cyan-900/20 border-cyan-700/40',
eco: 'text-emerald-400 bg-emerald-900/20 border-emerald-700/40',
report: 'text-violet-400 bg-violet-900/20 border-violet-700/40',
sentiment: 'text-rose-400 bg-rose-900/20 border-rose-700/40',
}
// ── Subcomponents ─────────────────────────────────────────────────────────────
@@ -271,6 +276,118 @@ function EcoConfig({
}
function FundamentalConfig({
config,
onChange,
}: {
config: Record<string, any>
onChange: (c: Record<string, any>) => void
}) {
const set = (k: string, v: any) => onChange({ ...config, [k]: v })
const FOCUS_TYPES = ['layoffs', 'earnings', 'ma', 'credit', 'regulatory', 'guidance']
const focus: string[] = config.focus_types ?? FOCUS_TYPES
const toggleFocus = (t: string) => {
const next = focus.includes(t) ? focus.filter(x => x !== t) : [...focus, t]
set('focus_types', next)
}
return (
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-slate-400 block mb-1">Score min d'impact</label>
<input
type="number" min={0} max={1} step={0.05}
value={config.min_impact ?? 0.45}
onChange={e => set('min_impact', parseFloat(e.target.value))}
className="w-full bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-sm text-white"
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">Lookback heures</label>
<input
type="number" min={24} max={168}
value={config.lookback_hours ?? 72}
onChange={e => set('lookback_hours', parseInt(e.target.value))}
className="w-full bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-sm text-white"
/>
</div>
</div>
<div>
<div className="text-xs text-slate-400 mb-2">Types d'événements filtrés</div>
<div className="flex flex-wrap gap-1.5">
{FOCUS_TYPES.map(t => (
<button
key={t}
onClick={() => toggleFocus(t)}
className={clsx(
'px-2 py-0.5 rounded text-xs border transition-colors capitalize',
focus.includes(t)
? 'bg-amber-900/40 border-amber-600/60 text-amber-300'
: 'bg-dark-800 border-slate-700/40 text-slate-500 hover:border-slate-600',
)}
>
{t}
</button>
))}
</div>
</div>
<div className="flex items-center gap-3">
<button onClick={() => set('dedup_enabled', !config.dedup_enabled)} className="shrink-0">
{config.dedup_enabled
? <ToggleRight className="w-5 h-5 text-amber-400" />
: <ToggleLeft className="w-5 h-5 text-slate-600" />}
</button>
<span className="text-sm text-slate-300">Déduplication sémantique</span>
{config.dedup_enabled && (
<div className="flex items-center gap-2 ml-auto">
<span className="text-xs text-slate-500">Fenêtre ± jours</span>
<input
type="number" min={1} max={7}
value={config.dedup_lookback_days ?? 3}
onChange={e => set('dedup_lookback_days', parseInt(e.target.value))}
className="w-14 bg-dark-900 border border-slate-700/40 rounded px-2 py-1 text-xs text-white"
/>
</div>
)}
</div>
</div>
)
}
function ReportConfig({
config,
onChange,
}: {
config: Record<string, any>
onChange: (c: Record<string, any>) => void
}) {
const set = (k: string, v: any) => onChange({ ...config, [k]: v })
return (
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-xs text-slate-400 block mb-1">Jours lookback</label>
<input
type="number" min={1} max={30}
value={config.days ?? 7}
onChange={e => set('days', parseInt(e.target.value))}
className="w-full bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-sm text-white"
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">Importance min (1-5)</label>
<input
type="number" min={1} max={5}
value={config.min_importance ?? 3}
onChange={e => set('min_importance', parseInt(e.target.value))}
className="w-full bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-sm text-white"
/>
</div>
</div>
)
}
// ── Desk editor ───────────────────────────────────────────────────────────────
function DeskEditor({
@@ -369,39 +486,55 @@ function DeskEditor({
{/* Type-specific config */}
<div>
<div className="text-xs text-slate-400 mb-2">Configuration</div>
{d.type === 'news' && (
<NewsConfig
config={d.config}
onChange={c => set('config', c)}
/>
{(d.type === 'news') && (
<NewsConfig config={d.config} onChange={c => set('config', c)} />
)}
{d.type === 'fundamental' && (
<FundamentalConfig config={d.config} onChange={c => set('config', c)} />
)}
{d.type === 'eco' && (
<EcoConfig
config={d.config}
onChange={c => set('config', c)}
/>
<EcoConfig config={d.config} onChange={c => set('config', c)} />
)}
{d.type === 'technical' && catalog.length > 0 && (
<div className="space-y-2">
{catalog.map(sig => (
<SignalToggle
key={sig.id}
signal={sig}
value={d.config.signals?.[sig.id] ?? { enabled: false }}
onChange={v => updateSignal(sig.id, v)}
/>
))}
<div className="pt-1">
<label className="text-xs text-slate-400 block mb-1">Lookback jours</label>
<input
type="number" min={1} max={30}
value={d.config.lookback_days ?? 7}
onChange={e => set('config', { ...d.config, lookback_days: parseInt(e.target.value) })}
className="w-24 bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-sm text-white"
/>
{d.type === 'report' && (
<ReportConfig config={d.config} onChange={c => set('config', c)} />
)}
{(d.type === 'technical' || d.type === 'sentiment') && catalog.length > 0 && (() => {
const deskSignals = catalog.filter(s => !s.desk_type || s.desk_type === d.type)
return (
<div className="space-y-2">
{deskSignals.map(sig => (
<SignalToggle
key={sig.id}
signal={sig}
value={d.config.signals?.[sig.id] ?? { enabled: false }}
onChange={v => updateSignal(sig.id, v)}
/>
))}
{d.type === 'technical' && (
<div className="pt-1">
<label className="text-xs text-slate-400 block mb-1">Lookback jours</label>
<input
type="number" min={1} max={30}
value={d.config.lookback_days ?? 7}
onChange={e => set('config', { ...d.config, lookback_days: parseInt(e.target.value) })}
className="w-24 bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-sm text-white"
/>
</div>
)}
{d.type === 'sentiment' && (
<div className="pt-1">
<label className="text-xs text-slate-400 block mb-1">Lookback jours</label>
<input
type="number" min={1} max={14}
value={d.config.lookback_days ?? 5}
onChange={e => set('config', { ...d.config, lookback_days: parseInt(e.target.value) })}
className="w-24 bg-dark-900 border border-slate-700/40 rounded px-2 py-1.5 text-sm text-white"
/>
</div>
)}
</div>
</div>
)}
)
})()}
</div>
{/* Actions */}