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:
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user