feat: causal lab
This commit is contained in:
@@ -24,6 +24,12 @@ interface ChartEvent {
|
||||
description?: string
|
||||
}
|
||||
|
||||
interface TheoPoint {
|
||||
date: string
|
||||
cumulative_pips: number
|
||||
contributions: { template_name: string; event_name: string; event_date: string; pips: number; decay_factor: number }[]
|
||||
}
|
||||
|
||||
interface Props {
|
||||
priceData: PriceCandle[]
|
||||
indicators: Record<string, LinePoint[]>
|
||||
@@ -31,8 +37,11 @@ interface Props {
|
||||
height?: number
|
||||
chartType?: 'candles' | 'line'
|
||||
onDateHover?: (date: string | null) => void
|
||||
theoryCurve?: TheoPoint[]
|
||||
}
|
||||
|
||||
export type { TheoPoint }
|
||||
|
||||
const MA_COLORS: Record<string, string> = {
|
||||
ma20: '#f59e0b',
|
||||
ma50: '#3b82f6',
|
||||
@@ -58,7 +67,7 @@ const CAT_LABELS: Record<string, string> = {
|
||||
technical: 'Tech.',
|
||||
}
|
||||
|
||||
export default function InstrumentChart({ priceData, indicators, events = [], height = 420, chartType = 'candles', onDateHover }: Props) {
|
||||
export default function InstrumentChart({ priceData, indicators, events = [], height = 420, chartType = 'candles', onDateHover, theoryCurve }: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const cleanupRef = useRef<(() => void) | null>(null)
|
||||
const onHoverRef = useRef(onDateHover)
|
||||
@@ -173,6 +182,30 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
||||
chart.addLineSeries(bbOpts).setData(indicators.bb_lower)
|
||||
}
|
||||
|
||||
// ── Theoretical curve — left scale (pips) ─────────────────────────────
|
||||
if (theoryCurve?.length) {
|
||||
chart.priceScale('left').applyOptions({
|
||||
visible: true,
|
||||
borderColor: 'rgba(167,139,250,0.25)',
|
||||
textColor: '#a78bfa',
|
||||
scaleMargins: { top: 0.1, bottom: 0.1 },
|
||||
})
|
||||
const theorySeries = chart.addLineSeries({
|
||||
color: 'rgba(167,139,250,0.75)',
|
||||
lineWidth: 1.5,
|
||||
priceScaleId: 'left',
|
||||
title: 'Δ pips théorique',
|
||||
priceLineVisible: false,
|
||||
lastValueVisible: true,
|
||||
crosshairMarkerVisible: true,
|
||||
crosshairMarkerRadius: 3,
|
||||
})
|
||||
const theoryData = theoryCurve
|
||||
.filter(p => p.contributions.length > 0)
|
||||
.map(p => ({ time: p.date as any, value: p.cumulative_pips }))
|
||||
if (theoryData.length) theorySeries.setData(theoryData)
|
||||
}
|
||||
|
||||
chart.timeScale().fitContent()
|
||||
|
||||
// ── Star overlay — ★ icons positioned via chart coordinate API ────────
|
||||
@@ -293,7 +326,7 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
||||
cleanupRef.current?.()
|
||||
cleanupRef.current = null
|
||||
}
|
||||
}, [priceData, indicators, events, height, chartType])
|
||||
}, [priceData, indicators, events, height, chartType, theoryCurve])
|
||||
|
||||
return (
|
||||
<div className="bg-dark-900/60 rounded-xl border border-slate-700/40 overflow-hidden">
|
||||
@@ -307,6 +340,12 @@ export default function InstrumentChart({ priceData, indicators, events = [], he
|
||||
<span className="flex items-center gap-1.5 opacity-40">
|
||||
<span className="inline-block w-5 border-t border-dashed border-slate-400" />BB(20,2)
|
||||
</span>
|
||||
{theoryCurve?.some(p => p.contributions.length > 0) && (
|
||||
<span className="flex items-center gap-1.5" style={{ color: '#a78bfa' }}>
|
||||
<span className="inline-block w-5 h-0.5 rounded" style={{ background: '#a78bfa' }} />
|
||||
Théorie (pips)
|
||||
</span>
|
||||
)}
|
||||
<span className="ml-auto flex items-center gap-2.5">
|
||||
{Object.entries(CAT_COLORS).map(([cat, c]) => (
|
||||
<span key={cat} className="flex items-center gap-0.5" style={{ color: c }}>
|
||||
|
||||
@@ -380,6 +380,8 @@ function TabLibrary({ initialTemplateId }: { initialTemplateId?: number | null }
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
const [savingLag, setSavingLag] = useState(false)
|
||||
const [savingTheory, setSavingTheory] = useState(false)
|
||||
const [genTheory, setGenTheory] = useState(false)
|
||||
const [editCoefs, setEditCoefs] = useState<Record<string, number>>({})
|
||||
const [editEdgesLag, setEdgesLag] = useState<CausalEdge[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
@@ -440,6 +442,30 @@ function TabLibrary({ initialTemplateId }: { initialTemplateId?: number | null }
|
||||
} finally { setSavingLag(false) }
|
||||
}
|
||||
|
||||
async function generateTheory() {
|
||||
if (!selected) return
|
||||
setGenTheory(true)
|
||||
try {
|
||||
await api(`/api/causal-lab/template/${selected.id}/generate-theory`, { method: 'POST' })
|
||||
const fresh = await api(`/api/causal-lab/template/${selected.id}`)
|
||||
setSelected(fresh)
|
||||
} finally { setGenTheory(false) }
|
||||
}
|
||||
|
||||
async function saveTheoryParams(updates: Record<string, unknown>) {
|
||||
if (!selected) return
|
||||
setSavingTheory(true)
|
||||
try {
|
||||
const newCalib = { ...(selected.calibration_json || {}), ...updates }
|
||||
await api(`/api/causal-lab/template/${selected.id}`, {
|
||||
method: 'PATCH', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ calibration_json: newCalib }),
|
||||
})
|
||||
const fresh = await api(`/api/causal-lab/template/${selected.id}`)
|
||||
setSelected(fresh)
|
||||
} finally { setSavingTheory(false) }
|
||||
}
|
||||
|
||||
function updateEdgeLag(i: number, field: 'lag_min' | 'diffusion_min' | 'decay_days', val: string) {
|
||||
setEdgesLag(prev => prev.map((e, idx) => idx !== i ? e : {
|
||||
...e,
|
||||
@@ -609,6 +635,82 @@ function TabLibrary({ initialTemplateId }: { initialTemplateId?: number | null }
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Paramètres théoriques */}
|
||||
<div className="bg-dark-700 rounded-lg p-4 border border-violet-800/30">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<h4 className="text-slate-300 text-xs font-semibold uppercase tracking-wider flex items-center gap-2 flex-1">
|
||||
<span className="text-violet-400">⟁</span> Paramètres théoriques
|
||||
</h4>
|
||||
<button
|
||||
onClick={generateTheory}
|
||||
disabled={genTheory}
|
||||
className="px-3 py-1 bg-violet-700 hover:bg-violet-600 disabled:opacity-50 rounded text-xs font-medium text-white flex items-center gap-1.5"
|
||||
>
|
||||
{genTheory ? (
|
||||
<><span className="animate-spin inline-block">↻</span> Génération…</>
|
||||
) : (
|
||||
<><span>✦</span> Générer IA</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
const calib = selected.calibration_json || {}
|
||||
const absorption = calib.absorption_days as number | undefined
|
||||
const decay = calib.decay_type as string | undefined
|
||||
const conf = calib.confidence as number | undefined
|
||||
const rationale = calib.theory_rationale as string | undefined
|
||||
const genAt = calib.theory_generated_at as string | undefined
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{rationale && (
|
||||
<p className="text-xs text-violet-300 italic border-l-2 border-violet-700/60 pl-2">{rationale}</p>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 mb-1">Durée absorption (j)</div>
|
||||
<input
|
||||
type="number" min={1} max={60} step={1}
|
||||
value={absorption ?? ''}
|
||||
placeholder="—"
|
||||
onChange={e => saveTheoryParams({ absorption_days: parseInt(e.target.value) || 7 })}
|
||||
className="w-full bg-dark-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 text-center"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 mb-1">Type de décroissance</div>
|
||||
<select
|
||||
value={decay ?? 'exp'}
|
||||
onChange={e => saveTheoryParams({ decay_type: e.target.value })}
|
||||
className="w-full bg-dark-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200"
|
||||
>
|
||||
<option value="step">step — tout ou rien</option>
|
||||
<option value="linear">linear — déclin linéaire</option>
|
||||
<option value="exp">exp — déclin exponentiel</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-xs text-slate-500">
|
||||
{conf !== undefined && (
|
||||
<span className="flex items-center gap-1">
|
||||
Confiance IA :
|
||||
<span className={`font-mono font-semibold ${conf >= 0.7 ? 'text-emerald-400' : conf >= 0.4 ? 'text-yellow-400' : 'text-red-400'}`}>
|
||||
{Math.round(conf * 100)}%
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
{genAt && (
|
||||
<span className="ml-auto opacity-60">
|
||||
{new Date(genAt).toLocaleDateString('fr-FR')}
|
||||
</span>
|
||||
)}
|
||||
{savingTheory && <span className="text-violet-400 animate-pulse">Sauvegarde…</span>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center text-slate-600 text-sm">
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from 'lucide-react'
|
||||
import axios from 'axios'
|
||||
import clsx from 'clsx'
|
||||
import InstrumentChart from '../components/InstrumentChart'
|
||||
import InstrumentChart, { TheoPoint } from '../components/InstrumentChart'
|
||||
|
||||
const api = axios.create({ baseURL: '/api' })
|
||||
|
||||
@@ -665,110 +665,25 @@ function MacroGaugePanel({ snap, dateLabel }: { snap: MacroGaugeSnap; dateLabel:
|
||||
)
|
||||
}
|
||||
|
||||
// ── EventTimeline ─────────────────────────────────────────────────────────────
|
||||
|
||||
function EventTimeline({
|
||||
events, priceData, selectedDate, templates, causalInsts,
|
||||
}: {
|
||||
events: SnapshotEvent[]
|
||||
priceData: PriceCandle[]
|
||||
selectedDate: string | null
|
||||
templates: CausalTemplate[]
|
||||
causalInsts: string[]
|
||||
}) {
|
||||
const navigate = useNavigate()
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [contWidth, setContWidth] = useState(800)
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current; if (!el) return
|
||||
const obs = new ResizeObserver(entries => setContWidth(entries[0].contentRect.width))
|
||||
obs.observe(el)
|
||||
return () => obs.disconnect()
|
||||
}, [])
|
||||
|
||||
// Only events that have an instantiated analysis for this instrument
|
||||
// Uses analyzed_instruments (actual DB analyses) — not template.instruments (theoretical)
|
||||
const linked = events.filter(ev => {
|
||||
if (!ev.analyzed_instruments) return false
|
||||
const insts = ev.analyzed_instruments.split(',')
|
||||
return causalInsts.some(ci => insts.includes(ci))
|
||||
})
|
||||
|
||||
if (!priceData.length || !linked.length) {
|
||||
return (
|
||||
<div ref={containerRef} className="h-16 flex items-center justify-center text-xs text-slate-600 italic">
|
||||
Aucun événement avec graphe causal pour cet instrument
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const minDate = priceData[0].time
|
||||
const maxDate = priceData[priceData.length - 1].time
|
||||
const minTs = new Date(minDate).getTime()
|
||||
const maxTs = new Date(maxDate).getTime()
|
||||
const PAD = 24
|
||||
const usable = Math.max(contWidth - PAD * 2, 1)
|
||||
|
||||
function tsToX(d: string): number {
|
||||
return PAD + ((new Date(d).getTime() - minTs) / (maxTs - minTs)) * usable
|
||||
}
|
||||
|
||||
const visible = linked.filter(ev => ev.date >= minDate && ev.date <= maxDate)
|
||||
|
||||
const HALF_W = 48
|
||||
const ROW_H = 28
|
||||
const MAX_ROWS = 5
|
||||
const rowEnds: number[] = new Array(MAX_ROWS).fill(-Infinity)
|
||||
|
||||
const placed = visible.map(ev => {
|
||||
const x = tsToX(ev.date)
|
||||
let row = 0
|
||||
for (let r = 0; r < MAX_ROWS; r++) {
|
||||
if (rowEnds[r] <= x - HALF_W) { row = r; break }
|
||||
row = r
|
||||
// ── Shared: trading-day-index x-coordinate ────────────────────────────────────
|
||||
// Uses the same time scale as LightweightCharts (trading days, weekends skipped).
|
||||
// For a date D, snaps to the nearest available trading day index.
|
||||
function makeTdToX(priceData: PriceCandle[], width: number): (d: string) => number {
|
||||
const dates = priceData.map(c => c.time)
|
||||
const N = dates.length
|
||||
if (N < 2) return () => 0
|
||||
function snap(d: string): number {
|
||||
if (d <= dates[0]) return 0
|
||||
if (d >= dates[N - 1]) return N - 1
|
||||
let lo = 0, hi = N - 1
|
||||
while (lo < hi) {
|
||||
const mid = (lo + hi) >> 1
|
||||
if (dates[mid] < d) lo = mid + 1
|
||||
else hi = mid
|
||||
}
|
||||
rowEnds[row] = x + HALF_W
|
||||
return { ev, x, row }
|
||||
})
|
||||
|
||||
const maxRow = placed.length ? Math.max(...placed.map(p => p.row)) : 0
|
||||
const containerH = (maxRow + 1) * ROW_H + 28
|
||||
const crossX = selectedDate && selectedDate >= minDate && selectedDate <= maxDate
|
||||
? tsToX(selectedDate) : null
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative w-full overflow-hidden select-none" style={{ height: containerH }}>
|
||||
<div className="absolute bottom-7 left-0 right-0 h-px bg-slate-700/40" />
|
||||
{crossX !== null && (
|
||||
<div className="absolute top-0 bottom-0 w-px bg-blue-400/40 pointer-events-none" style={{ left: crossX }} />
|
||||
)}
|
||||
{placed.map(({ ev, x, row }) => {
|
||||
const active = isActiveAt(ev, selectedDate)
|
||||
return (
|
||||
<div
|
||||
key={`${ev.date}-${ev.title}`}
|
||||
onClick={() => ev.id && navigate(`/market-events?event=${ev.id}`)}
|
||||
title={`${ev.title}\n${fmtDateFR(ev.date)}`}
|
||||
className="absolute flex flex-col items-center cursor-pointer group"
|
||||
style={{ left: x, top: row * ROW_H, transform: 'translateX(-50%)' }}
|
||||
>
|
||||
<span className={clsx('text-sm leading-none', active ? 'text-amber-400' : 'text-slate-600 group-hover:text-slate-400')}>★</span>
|
||||
<span className={clsx(
|
||||
'absolute top-full mt-0.5 text-[9px] whitespace-nowrap px-1 rounded bg-dark-800/90 border border-slate-700/40 opacity-0 group-hover:opacity-100 transition-opacity z-10 pointer-events-none',
|
||||
active ? 'text-amber-300 border-amber-800/40' : 'text-slate-400'
|
||||
)}>
|
||||
{ev.title.length > 24 ? ev.title.slice(0, 24) + '…' : ev.title}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="absolute bottom-0 left-0 right-0 flex justify-between px-6">
|
||||
<span className="text-[10px] text-slate-700">{fmtDateFR(minDate)}</span>
|
||||
<span className="text-[10px] text-slate-700">{fmtDateFR(maxDate)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return lo
|
||||
}
|
||||
return (d: string) => (snap(d) / (N - 1)) * width
|
||||
}
|
||||
|
||||
// ── CausalFrise ───────────────────────────────────────────────────────────────
|
||||
@@ -829,12 +744,8 @@ function CausalFrise({
|
||||
|
||||
const minDate = priceData[0].time
|
||||
const maxDate = priceData[priceData.length - 1].time
|
||||
const minTs = new Date(minDate).getTime()
|
||||
const maxTs = new Date(maxDate).getTime()
|
||||
const usable = Math.max(contWidth, 1)
|
||||
|
||||
const clampTs = (d: string) => Math.min(Math.max(new Date(d).getTime(), minTs), maxTs)
|
||||
const dateToX = (d: string) => ((clampTs(d) - minTs) / (maxTs - minTs)) * usable
|
||||
const tdToX = makeTdToX(priceData, usable)
|
||||
|
||||
// Build chips (one per event × template)
|
||||
type Chip = {
|
||||
@@ -850,8 +761,8 @@ function CausalFrise({
|
||||
const d = new Date(ev.date); d.setDate(d.getDate() + 30)
|
||||
return d.toISOString().slice(0, 10)
|
||||
})()
|
||||
const x1 = dateToX(ev.date)
|
||||
const raw = dateToX(endDate) - x1
|
||||
const x1 = tdToX(ev.date)
|
||||
const raw = tdToX(endDate) - x1
|
||||
const w = Math.max(raw, FRISE_MIN_W)
|
||||
return { ev, tmpl, x1, x2: x1 + w, w, active: isActiveAt(ev, selectedDate) }
|
||||
})
|
||||
@@ -882,7 +793,7 @@ function CausalFrise({
|
||||
month: 'short',
|
||||
...(cur.getFullYear() !== start.getFullYear() ? { year: '2-digit' } : {}),
|
||||
}),
|
||||
x: dateToX(cur.toISOString().slice(0, 10)),
|
||||
x: tdToX(cur.toISOString().slice(0, 10)),
|
||||
})
|
||||
cur = new Date(cur.getFullYear(), cur.getMonth() + 1, 1)
|
||||
}
|
||||
@@ -892,7 +803,7 @@ function CausalFrise({
|
||||
const visTicks = ticks.filter((_, i) => i % tickStep === 0)
|
||||
|
||||
const crossX = selectedDate && selectedDate >= minDate && selectedDate <= maxDate
|
||||
? dateToX(selectedDate) : null
|
||||
? tdToX(selectedDate) : null
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -1090,6 +1001,9 @@ export default function InstrumentDashboard() {
|
||||
const [tabUnder, setTabUnder] = useState<'counters' | 'analyse'>('counters')
|
||||
const [templates, setTemplates] = useState<CausalTemplate[]>([])
|
||||
const [macroAtDate, setMacroAtDate] = useState<MacroGaugeSnap | null>(null)
|
||||
const [theoryCurve, setTheoryCurve] = useState<TheoPoint[] | null>(null)
|
||||
const [loadingTheory, setLoadingTheory] = useState(false)
|
||||
const [showTheory, setShowTheory] = useState(false)
|
||||
|
||||
const instrumentId = id.toUpperCase()
|
||||
|
||||
@@ -1104,6 +1018,8 @@ export default function InstrumentDashboard() {
|
||||
setNarrative('')
|
||||
setSelectedDate(null)
|
||||
setMacroAtDate(null)
|
||||
setTheoryCurve(null)
|
||||
setShowTheory(false)
|
||||
api.get(`/instruments/${instrumentId}/snapshot?period=${period}`)
|
||||
.then(r => {
|
||||
setSnapshot(r.data)
|
||||
@@ -1126,6 +1042,20 @@ export default function InstrumentDashboard() {
|
||||
if (date) setSelectedDate(date)
|
||||
}, [])
|
||||
|
||||
const toggleTheory = useCallback(() => {
|
||||
if (showTheory) {
|
||||
setShowTheory(false)
|
||||
setTheoryCurve(null)
|
||||
return
|
||||
}
|
||||
if (theoryCurve) { setShowTheory(true); return }
|
||||
setLoadingTheory(true)
|
||||
api.get(`/instruments/${instrumentId}/theoretical-curve?period=${period}`)
|
||||
.then(r => { setTheoryCurve(r.data); setShowTheory(true) })
|
||||
.catch(() => {})
|
||||
.finally(() => setLoadingTheory(false))
|
||||
}, [showTheory, theoryCurve, instrumentId, period])
|
||||
|
||||
const { priceMap, indMap, sortedDates, dateIndex } = useMemo(() => {
|
||||
if (!snapshot) return { priceMap: {} as Record<string, PriceCandle>, indMap: {} as Record<string, Record<string, number>>, sortedDates: [] as string[], dateIndex: {} as Record<string, number> }
|
||||
const priceMap: Record<string, PriceCandle> = {}
|
||||
@@ -1322,6 +1252,7 @@ export default function InstrumentDashboard() {
|
||||
height={420}
|
||||
chartType={chartStyle}
|
||||
onDateHover={handleDateHover}
|
||||
theoryCurve={showTheory && theoryCurve ? theoryCurve : undefined}
|
||||
/>
|
||||
|
||||
{/* Date badge */}
|
||||
@@ -1383,30 +1314,27 @@ export default function InstrumentDashboard() {
|
||||
|
||||
{tabUnder === 'analyse' && (() => {
|
||||
const causalInsts = CAT_TO_CAUSAL_INST[selected?.category ?? ''] ?? []
|
||||
const theoPt = effectiveDate && theoryCurve
|
||||
? theoryCurve.find(p => p.date === effectiveDate) ?? null
|
||||
: null
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{/* Zone haute — frise des événements (seulement ceux avec graphe causal) */}
|
||||
<div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Calendar className="w-4 h-4 text-amber-400" />
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Frise des événements</span>
|
||||
<span className="text-xs text-slate-600 ml-auto">survol = détail · clic = market event</span>
|
||||
</div>
|
||||
<EventTimeline
|
||||
events={snapshot.events}
|
||||
priceData={snapshot.price_data}
|
||||
selectedDate={effectiveDate}
|
||||
templates={templates}
|
||||
causalInsts={causalInsts}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Zone basse — frise des graphes causaux */}
|
||||
{/* Frise des graphes causaux (inclut l'event) */}
|
||||
<div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<BarChart2 className="w-4 h-4 text-violet-400" />
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Frise des graphes</span>
|
||||
<span className="text-xs text-slate-600 ml-auto">largeur ∝ durée · clic = détail</span>
|
||||
<button
|
||||
onClick={toggleTheory}
|
||||
disabled={loadingTheory}
|
||||
className={`ml-auto px-2.5 py-1 rounded text-xs font-medium border transition-colors ${
|
||||
showTheory
|
||||
? 'bg-violet-700/60 border-violet-600/60 text-violet-200'
|
||||
: 'bg-dark-900/40 border-slate-600/40 text-slate-400 hover:border-violet-600/40 hover:text-violet-300'
|
||||
}`}
|
||||
>
|
||||
{loadingTheory ? '↻ Chargement…' : showTheory ? '⟁ Théorie ON' : '⟁ Courbe théorique'}
|
||||
</button>
|
||||
</div>
|
||||
<CausalFrise
|
||||
events={snapshot.events}
|
||||
@@ -1417,6 +1345,44 @@ export default function InstrumentDashboard() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Décomposition théorique au curseur */}
|
||||
{showTheory && theoPt && (
|
||||
<div className="rounded-xl border border-violet-800/40 bg-violet-950/20 p-4">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<span className="text-violet-400 text-xs">⟁</span>
|
||||
<span className="text-xs font-semibold text-violet-300 uppercase tracking-wide">
|
||||
Contributions théoriques — {dateLabel}
|
||||
</span>
|
||||
<span className={`ml-auto text-sm font-mono font-bold ${theoPt.cumulative_pips >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{theoPt.cumulative_pips >= 0 ? '+' : ''}{theoPt.cumulative_pips} pips
|
||||
</span>
|
||||
</div>
|
||||
{theoPt.contributions.length === 0 ? (
|
||||
<p className="text-xs text-slate-500 italic">Aucun événement actif à cette date.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{theoPt.contributions.map((c, i) => (
|
||||
<div key={i} className="flex items-center gap-2 text-xs">
|
||||
<div className="flex-1 min-w-0">
|
||||
<span className="text-slate-300 font-medium truncate block">{c.event_name}</span>
|
||||
<span className="text-slate-500">{c.template_name} · depuis {c.event_date} · {Math.round(c.decay_factor * 100)}% actif</span>
|
||||
</div>
|
||||
<span className={`font-mono font-semibold shrink-0 ${c.pips >= 0 ? 'text-emerald-400' : 'text-red-400'}`}>
|
||||
{c.pips >= 0 ? '+' : ''}{c.pips} pip
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showTheory && !theoPt && theoryCurve && (
|
||||
<div className="rounded-xl border border-violet-800/30 bg-violet-950/10 p-3 text-xs text-slate-500 text-center">
|
||||
⟁ Aucune contribution théorique pour cette date
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Note globale */}
|
||||
<ExplanationScore
|
||||
events={snapshot.events}
|
||||
|
||||
Reference in New Issue
Block a user