feat: curve regime
This commit is contained in:
@@ -1449,7 +1449,10 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
const [searchParams] = useSearchParams()
|
||||
// ?tab=wavelets lets other pages (e.g. Dashboard's Wavelets Signal card) deep-link
|
||||
// straight into the Wavelets panel instead of always landing on Counters.
|
||||
const [tabUnder, setTabUnder] = useState<'counters' | 'analyse' | 'wavelets'>(() => (searchParams.get('tab') === 'wavelets' ? 'wavelets' : 'counters'))
|
||||
const [tabUnder, setTabUnder] = useState<'counters' | 'analyse' | 'wavelets' | 'regime'>(() => {
|
||||
const t = searchParams.get('tab')
|
||||
return t === 'counters' || t === 'analyse' || t === 'regime' ? t : 'wavelets'
|
||||
})
|
||||
const [waveletLevels, setWaveletLevels] = useState(4)
|
||||
const [waveletFamily, setWaveletFamily] = useState<'gmw' | 'morlet' | 'bump'>('gmw')
|
||||
const [waveletMethod, setWaveletMethod] = useState<'cwt' | 'ssq'>('cwt')
|
||||
@@ -1466,6 +1469,8 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
const [waveletReliability, setWaveletReliability] = useState<any | null>(null)
|
||||
const [loadingReliability, setLoadingReliability] = useState(false)
|
||||
const [hiddenBands, setHiddenBands] = useState<Set<string>>(new Set())
|
||||
const [curveRegime, setCurveRegime] = useState<any | null>(null)
|
||||
const [loadingCurveRegime, setLoadingCurveRegime] = useState(false)
|
||||
const [hoveredWaveletIdx, setHoveredWaveletIdx] = useState<number | null>(null)
|
||||
const [templates, setTemplates] = useState<CausalTemplate[]>([])
|
||||
const [causalScores, setCausalScores] = useState<Record<number, number | null>>({}) // eventId → activation_score*100
|
||||
@@ -1716,6 +1721,22 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tabUnder, instrumentId, selected])
|
||||
|
||||
// Curve Regime tab — auto-loads once per instrument, same pattern as the Wavelet tab
|
||||
// above (reuses the period selector's current snapshot, no separate live trigger).
|
||||
const curveRegimeAutoFetchKey = useRef<string | null>(null)
|
||||
useEffect(() => {
|
||||
if (tabUnder !== 'regime' || !selected) return
|
||||
if (curveRegimeAutoFetchKey.current === `${instrumentId}|${period}`) return
|
||||
curveRegimeAutoFetchKey.current = `${instrumentId}|${period}`
|
||||
setCurveRegime(null)
|
||||
setLoadingCurveRegime(true)
|
||||
api.get(`/instruments/${encodeURIComponent(instrumentId)}/curve-regime`, { params: { period } })
|
||||
.then(({ data }) => setCurveRegime(data))
|
||||
.catch((e) => console.error(`[CurveRegime] fetch failed for ${instrumentId}:`, e))
|
||||
.finally(() => setLoadingCurveRegime(false))
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [tabUnder, instrumentId, selected, period])
|
||||
|
||||
const runWaveletReliability = async () => {
|
||||
if (!selected) return
|
||||
setLoadingReliability(true)
|
||||
@@ -2120,9 +2141,10 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
{/* ── Tabs sous la courbe ── */}
|
||||
<div className="border-b border-slate-700/40 flex gap-1">
|
||||
{([
|
||||
{ key: 'wavelets', label: 'Wavelet' },
|
||||
{ key: 'regime', label: 'Curve Regime' },
|
||||
{ key: 'counters', label: 'Compteurs' },
|
||||
{ key: 'analyse', label: 'Analyse de la courbe' },
|
||||
{ key: 'wavelets', label: 'Wavelet' },
|
||||
] as const).map(t => (
|
||||
<button
|
||||
key={t.key}
|
||||
@@ -2577,6 +2599,110 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tabUnder === 'regime' && (
|
||||
<div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Curve Regime — état de marché de l'instrument</span>
|
||||
<span className="text-[10px] text-slate-600" title="Distinct du régime macro global (Inflation Shock, Goldilocks...) affiché ailleurs dans le Cockpit — celui-ci ne concerne que cet instrument.">
|
||||
≠ régime macro global
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{loadingCurveRegime ? (
|
||||
<div className="text-xs text-slate-500 text-center py-10">Calcul en cours…</div>
|
||||
) : !curveRegime ? (
|
||||
<div className="text-xs text-slate-500 text-center py-10">Aucune donnée — réessayez ou vérifiez que l'instrument a un historique de prix.</div>
|
||||
) : (() => {
|
||||
const top = curveRegime.top_match
|
||||
const profile = curveRegime.profile
|
||||
const vol = profile?.volatilite ?? {}
|
||||
const scoreColorFor = (s: number) => s >= 0.75 ? 'text-emerald-400' : s >= 0.5 ? 'text-amber-400' : 'text-slate-400'
|
||||
const AXIS_LABELS: Record<string, string> = { direction: 'Direction', dynamique: 'Dynamique', volatilite: 'Volatilité', structure_iv: 'Structure IV' }
|
||||
return (
|
||||
<>
|
||||
{/* Top match */}
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<div>
|
||||
<div className={clsx('text-2xl font-bold', top ? scoreColorFor(top.score) : 'text-slate-500')}>
|
||||
{top ? top.label : 'Indéterminé'}
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-600">
|
||||
Confiance {top ? `${(top.score * 100).toFixed(0)}%` : '—'} · basé sur {top ? Object.keys(top.axis_hits).length : 0} axes calculables
|
||||
</div>
|
||||
</div>
|
||||
{curveRegime.facteur_dominant_bias && (
|
||||
<div className="text-[10px] px-2 py-1 rounded bg-dark-900/60 border border-slate-700/30 text-slate-400">
|
||||
Biais macro ({curveRegime.macro_regime_label}) sur cette classe d'actif : <span className="font-semibold text-slate-300">{curveRegime.facteur_dominant_bias}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!curveRegime.has_options_data && (
|
||||
<div className="text-[10px] text-amber-400/80 bg-amber-900/10 border border-amber-700/20 rounded px-2 py-1.5">
|
||||
Structure IV / Liquidité indisponibles — lie l'option chain de cet instrument (Config → Instruments Watchlist → "Option") pour les inclure dans le classement.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Axis breakdown */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
{[
|
||||
{ key: 'direction', value: profile?.direction },
|
||||
{ key: 'dynamique', value: profile?.dynamique },
|
||||
{ key: 'volatilite', value: vol.level ? `${vol.level}${vol.trend && vol.trend !== 'stable' ? ` (${vol.trend})` : ''}` : null },
|
||||
{ key: 'structure_iv', value: profile?.structure_iv },
|
||||
].map(({ key, value }) => {
|
||||
const hit = top?.axis_hits?.[key]
|
||||
return (
|
||||
<div key={key} className="bg-dark-900/50 rounded px-2 py-1.5">
|
||||
<div className="text-slate-500 text-[10px]">{AXIS_LABELS[key]}</div>
|
||||
<div className={clsx('font-mono font-semibold text-xs',
|
||||
value == null ? 'text-slate-600' : hit === true ? 'text-emerald-400' : hit === false ? 'text-slate-300' : 'text-slate-300'
|
||||
)}>
|
||||
{value ?? 'n/a'}{hit === true && ' ✓'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="bg-dark-900/50 rounded px-2 py-1.5">
|
||||
<div className="text-slate-500 text-[10px]">Liquidité</div>
|
||||
<div className="font-mono font-semibold text-xs text-slate-300">{profile?.liquidite ?? 'n/a'}</div>
|
||||
</div>
|
||||
<div className="bg-dark-900/50 rounded px-2 py-1.5" title="Aucune source de données de corrélation cross-actifs par instrument n'existe encore dans le Cockpit — axe non noté.">
|
||||
<div className="text-slate-500 text-[10px]">Corrélation cross-actifs</div>
|
||||
<div className="font-mono font-semibold text-xs text-slate-600">n/a</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Reference info for the matched regime */}
|
||||
{top && (
|
||||
<div className="text-xs space-y-1.5 border-t border-slate-700/30 pt-3">
|
||||
<div><span className="text-slate-500">Facteur dominant type : </span><span className="text-slate-300">{top.facteur_dominant_ref}</span></div>
|
||||
<div><span className="text-slate-500">Déclencheurs typiques : </span><span className="text-slate-300">{top.declencheurs}</span></div>
|
||||
<div><span className="text-slate-500">Stratégies privilégiées : </span><span className="text-slate-300">{top.strategies}</span></div>
|
||||
<div><span className="text-slate-500">Exemple : </span><span className="text-slate-300">{top.exemple}</span></div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Runner-up regimes — visible when the top match isn't a clean, unambiguous winner */}
|
||||
{curveRegime.ranked?.length > 1 && (
|
||||
<div className="border-t border-slate-700/30 pt-3">
|
||||
<div className="text-[10px] text-slate-500 uppercase tracking-wide mb-1.5">Autres régimes candidats</div>
|
||||
<div className="space-y-1">
|
||||
{curveRegime.ranked.slice(1, 4).map((r: any) => (
|
||||
<div key={r.key} className="flex items-center justify-between text-[10px]">
|
||||
<span className="text-slate-400">{r.label}</span>
|
||||
<span className={clsx('font-mono', scoreColorFor(r.score))}>{(r.score * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<NarrativeCard
|
||||
narrative={narrative}
|
||||
loading={loadingNarr}
|
||||
|
||||
Reference in New Issue
Block a user