feat: market event

This commit is contained in:
OpenSquared
2026-06-30 12:22:14 +02:00
parent 12c298cf83
commit 2b1a43a093

View File

@@ -383,6 +383,7 @@ function EventDetail({
const [editData, setEditData] = useState<Partial<MarketEvent>>({})
const [saving, setSaving] = useState(false)
const [analyses, setAnalyses] = useState<CausalAnalysis[]>([])
const [expandedAnalysis, setExpandedAnalysis] = useState<number | null>(null)
// Inline analysis
const [templates, setTemplates] = useState<TemplateRef[]>([])
const [selTmpl, setSelTmpl] = useState(0)
@@ -760,39 +761,137 @@ function EventDetail({
.filter(([k]) => !k.includes('error'))
.sort(([, v1], [, v2]) => Math.abs(v2) - Math.abs(v1))
.slice(0, 3)
const isExpanded = expandedAnalysis === a.id
const gNodes = a.graph_json?.nodes || []
const gEdges = a.graph_json?.edges || []
const allVals: Record<string, number> = { ...(a.inputs_json || {}), ...(a.prediction_json || {}) }
const inputNodes = gNodes.filter(n => n.type === 'input')
const outputNodes = gNodes.filter(n => n.type === 'market_asset' || n.type === 'output')
const midNodes = gNodes.filter(n => n.type !== 'input' && n.type !== 'market_asset' && n.type !== 'output')
return (
<div key={a.id} className="flex items-stretch gap-1">
<button
onClick={() => navigate(`/causal-lab?template=${a.template_id}`)}
className="flex-1 text-left bg-slate-800/40 hover:bg-slate-800/70 border border-slate-700/40 hover:border-blue-700/40 rounded-lg px-3 py-2 transition-colors"
>
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-slate-200 truncate">{a.template_name}</span>
{score != null && (
<span className={clsx(
'text-xs font-mono px-1.5 py-0.5 rounded shrink-0 ml-2',
score >= 70 ? 'bg-emerald-500/20 text-emerald-400' :
score >= 40 ? 'bg-amber-500/20 text-amber-400' :
'bg-slate-700/40 text-slate-400'
)}>{score}%</span>
<div key={a.id} className="space-y-0.5">
<div className="flex items-stretch gap-1">
<button
onClick={() => setExpandedAnalysis(isExpanded ? null : a.id)}
className={clsx(
'flex-1 text-left border rounded-lg px-3 py-2 transition-colors',
isExpanded
? 'bg-slate-800/70 border-blue-700/50'
: 'bg-slate-800/40 hover:bg-slate-800/70 border-slate-700/40 hover:border-blue-700/40'
)}
>
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-slate-200 truncate">{a.template_name}</span>
<div className="flex items-center gap-1.5 shrink-0 ml-2">
{score != null && (
<span className={clsx(
'text-xs font-mono px-1.5 py-0.5 rounded',
score >= 70 ? 'bg-emerald-500/20 text-emerald-400' :
score >= 40 ? 'bg-amber-500/20 text-amber-400' :
'bg-slate-700/40 text-slate-400'
)}>{score}%</span>
)}
<span className="text-slate-600 text-[10px]">{isExpanded ? '' : ''}</span>
</div>
</div>
<div className="flex items-center gap-2 flex-wrap mt-0.5">
<span className="text-xs text-slate-500">{a.instrument}</span>
{topPreds.map(([k, v]) => (
<span key={k} className={clsx('text-xs font-mono', v > 0 ? 'text-emerald-500' : 'text-red-500')}>
{k}: {v > 0 ? '+' : ''}{Math.round(v)}
</span>
))}
</div>
</button>
<button
onClick={() => deleteAnalysis(a.id)}
className="px-2 rounded-lg text-slate-600 hover:text-red-400 hover:bg-red-900/20 border border-slate-700/40 transition-colors shrink-0"
title="Supprimer cette instanciation"
>
<Trash2 size={11} />
</button>
</div>
{/* Détail du calcul */}
{isExpanded && (
<div className="rounded-lg border border-slate-700/40 bg-dark-900/80 p-2.5 space-y-2.5">
{/* Mini graphe */}
{gNodes.length > 0 && (
<CausalGraphMini nodes={gNodes} edges={gEdges} nodeValues={allVals} />
)}
{/* Breakdown par couche */}
<div className="space-y-1.5 text-xs">
{inputNodes.length > 0 && (
<div>
<div className="text-[10px] text-slate-500 uppercase tracking-wider mb-1">Entrées</div>
<div className="flex flex-wrap gap-1">
{inputNodes.map(n => {
const v = allVals[n.id]
if (v === undefined) return null
return (
<span key={n.id} className="bg-blue-900/20 border border-blue-700/30 text-blue-300 px-1.5 py-0.5 rounded font-mono text-[10px]">
{n.label}: <span className={v > 0 ? 'text-blue-200' : 'text-red-300'}>{v > 0 ? '+' : ''}{v.toFixed(2)}</span>
</span>
)
})}
</div>
</div>
)}
{midNodes.length > 0 && (
<div>
<div className="text-[10px] text-slate-500 uppercase tracking-wider mb-1">Propagation</div>
<div className="flex flex-wrap gap-1">
{midNodes.map(n => {
const v = allVals[n.id]
if (v === undefined) return null
const fmt = n.unit === 'pips' ? Math.round(v).toString()
: n.unit === '%' ? v.toFixed(2) + '%'
: v.toFixed(3)
return (
<span key={n.id} className="bg-slate-800/60 border border-slate-700/40 text-slate-300 px-1.5 py-0.5 rounded font-mono text-[10px]">
{n.label}: <span className={v > 0 ? 'text-teal-400' : v < 0 ? 'text-orange-400' : 'text-slate-500'}>{v > 0 ? '+' : ''}{fmt}</span>
</span>
)
})}
</div>
</div>
)}
{outputNodes.length > 0 && (
<div>
<div className="text-[10px] text-slate-500 uppercase tracking-wider mb-1">Prédictions vs Réel</div>
<div className="flex flex-wrap gap-1">
{outputNodes.map(n => {
const pred = allVals[n.id]
const actual = (a.actual_json || {})[n.id] ?? (a.actual_json || {})[n.instrument || '']
const fmt = (x: number) => `${x > 0 ? '+' : ''}${Math.round(x)}${n.unit === 'pips' ? 'p' : ''}`
return (
<span key={n.id} className={clsx('border px-2 py-0.5 rounded font-mono text-[10px]',
pred > 0 ? 'bg-emerald-900/20 border-emerald-700/30 text-emerald-400' :
pred < 0 ? 'bg-red-900/20 border-red-700/30 text-red-400' :
'bg-slate-800/60 border-slate-700/40 text-slate-400')}>
{n.label}: {pred !== undefined ? fmt(pred) : ''}
{actual !== undefined && (
<span className="text-slate-500 ml-1">/ réel {fmt(actual)}</span>
)}
</span>
)
})}
</div>
</div>
)}
</div>
<button
onClick={() => navigate(`/causal-lab?template=${a.template_id}`)}
className="text-[10px] text-blue-400 hover:text-blue-300 transition-colors"
>
→ Ouvrir dans l'éditeur Causal Lab
</button>
</div>
<div className="flex items-center gap-2 flex-wrap mt-0.5">
<span className="text-xs text-slate-500">{a.instrument}</span>
{topPreds.map(([k, v]) => (
<span key={k} className={clsx('text-xs font-mono', v > 0 ? 'text-emerald-500' : 'text-red-500')}>
{k}: {v > 0 ? '+' : ''}{Math.round(v)}
</span>
))}
</div>
</button>
<button
onClick={() => deleteAnalysis(a.id)}
className="px-2 rounded-lg text-slate-600 hover:text-red-400 hover:bg-red-900/20 border border-slate-700/40 transition-colors shrink-0"
title="Supprimer cette instanciation"
>
<Trash2 size={11} />
</button>
)}
</div>
)
})}