feat: market event
This commit is contained in:
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user