feat: curve regime

This commit is contained in:
OpenSquared
2026-07-25 09:23:26 +02:00
parent 3addea6850
commit ad7ab35d1d
8 changed files with 177 additions and 31 deletions

View File

@@ -1164,6 +1164,16 @@ export const useWaveletWatchlistSignals = () =>
staleTime: 5 * 60_000,
})
// Instrument-level Curve Regime (services.curve_regime) per Watchlist instrument — cached
// on the same refresh cadence as the wavelet cache (services.wavelet_scheduler), NOT the
// global macro regime. Feeds the Dashboard's "Curve Regime" card.
export const useWatchlistCurveRegimes = () =>
useQuery({
queryKey: ['watchlist-curve-regimes'],
queryFn: () => api.get('/watchlist/curve-regimes').then(r => r.data as { items: any[] }),
staleTime: 5 * 60_000,
})
export const useLogCycles = () =>
useQuery({
queryKey: ['log-cycles'],

View File

@@ -6,7 +6,7 @@ import {
useRiskDashboard, useRiskRadar, useGeoNews,
useCycleStatus,
useInstrumentsWatchlist, useInstrumentsWatchlistQuotes, useWatchlistHistory, useQuickAddInstrument, useSaxoIvWatchlist, useLatestCycleReport,
useWaveletWatchlistSignals,
useWatchlistCurveRegimes,
} from '../hooks/useApi'
import { Clock, Globe, ArrowUpRight, Newspaper, Waves, Link2 } from 'lucide-react'
import { Link, useNavigate } from 'react-router-dom'
@@ -132,7 +132,7 @@ export default function Dashboard() {
const activeWatchlistName = ((watchlistItems as any) ?? []).find((w: any) => w.ticker === activeWatchlistTicker)?.name || activeWatchlistTicker
const { data: watchlistHistoryData, isLoading: watchlistHistoryLoading } = useWatchlistHistory(activeWatchlistTicker, watchlistChartPeriod)
const { data: latestCycleReportData } = useLatestCycleReport()
const { data: waveletSignalsData } = useWaveletWatchlistSignals()
const { data: curveRegimesData } = useWatchlistCurveRegimes()
const { data: saxoIvWatchlistData } = useSaxoIvWatchlist()
// Double-click on a Watchlist row opens it in Instrument Analysis. Always quick-adds
@@ -1013,50 +1013,51 @@ export default function Dashboard() {
<TradeRankList trades={openPositions} mode="recent" title="🏆 Trades Overview" linkTo="/portfolio" tickerLabel={underlyingDisplayName} />
{/* Wavelets Signal — latest per-ticker signal from the watchlist scan (computed each
cycle). Single click on the card = Wavelets Simulation overview; double-click a
row = that instrument's Analysis page, Wavelets panel open directly. */}
{/* Curve Regime — instrument-level regime classification (services.curve_regime),
cached per-instrument on the same refresh cadence as the wavelet cache. Distinct
from the global macro regime shown elsewhere. Double-click a row = that
instrument's Analysis page, Curve Regime tab open directly. */}
{(() => {
const signals: any[] = waveletSignalsData?.signals ?? []
const regimes: any[] = curveRegimesData?.items ?? []
const nameByTicker: Record<string, string> = {}
for (const w of ((watchlistItems as any[]) ?? [])) nameByTicker[w.ticker] = w.name || w.ticker
const scoreColor = (s: number | null) => s == null ? 'text-slate-600' : s >= 0.75 ? 'text-emerald-400' : s >= 0.5 ? 'text-amber-400' : 'text-slate-400'
return (
<div className="card">
<Link to="/wavelets-simulation" className="flex items-center justify-between mb-1 hover:opacity-80 transition-opacity">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px] flex items-center gap-1">
<Waves className="w-2.5 h-2.5" /> Wavelets Signal
<Waves className="w-2.5 h-2.5" /> Curve Regime
</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</Link>
{signals.length > 0 ? (
</div>
{regimes.length > 0 ? (
<div className="space-y-1.5 mt-1 max-h-[190px] overflow-y-auto pr-0.5">
{signals.map((s: any, i: number) => (
{regimes.map((r: any, i: number) => (
<div
key={i}
onDoubleClick={() => quickAddInstrument.mutate(s.ticker, {
onSuccess: ({ id }) => navigate(`/instruments/${encodeURIComponent(id)}?tab=wavelets`),
onError: (e) => console.error(`[WaveletsSignal] failed to open ${s.ticker} in Instrument Analysis:`, e),
onDoubleClick={() => quickAddInstrument.mutate(r.ticker, {
onSuccess: ({ id }) => navigate(`/instruments/${encodeURIComponent(id)}?tab=regime`),
onError: (e) => console.error(`[CurveRegime] failed to open ${r.ticker} in Instrument Analysis:`, e),
})}
title="Double-click to open in Instrument Analysis → Wavelets"
title="Double-click to open in Instrument Analysis → Curve Regime"
className="flex items-center gap-1.5 text-[10px] rounded px-1 -mx-1 py-0.5 cursor-pointer hover:bg-dark-700/40 transition-colors"
>
<span className={clsx('shrink-0',
s.direction === 'up' ? 'text-emerald-400' : s.direction === 'down' ? 'text-red-400' : 'text-slate-700'
r.direction === 'up' ? 'text-emerald-400' : r.direction === 'down' ? 'text-red-400' : 'text-slate-700'
)}>
{s.direction === 'up' ? '↑' : s.direction === 'down' ? '↓' : '·'}
{r.direction === 'up' ? '↑' : r.direction === 'down' ? '↓' : '·'}
</span>
<span className="text-slate-200 font-bold shrink-0 truncate max-w-[90px]" title={s.ticker}>
{nameByTicker[s.ticker] || s.ticker}
<span className="text-slate-200 font-bold shrink-0 truncate max-w-[90px]" title={r.ticker}>
{nameByTicker[r.ticker] || r.ticker}
</span>
<span className="text-slate-500 truncate flex-1 text-[9px]">
{s.band_label ? `${s.band_label} · ${s.signal_kind ?? 'veille'}` : 'Pas de données'}
<span className={clsx('truncate flex-1 text-[9px] font-semibold', scoreColor(r.score))}>
{r.regime_label ?? 'Pas de données'}
</span>
{s.computed_at && <span className="text-[8px] text-slate-700 shrink-0 font-mono">{s.computed_at.slice(0, 10)}</span>}
{r.computed_at && <span className="text-[8px] text-slate-700 shrink-0 font-mono">{r.computed_at.slice(0, 10)}</span>}
</div>
))}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">No wavelet signal detected le prochain cycle en calculera</div>
<div className="text-[10px] text-slate-600 mt-1">Aucun régime calculé le prochain rafraîchissement s'en chargera</div>
)}
</div>
)

View File

@@ -1799,9 +1799,16 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
const waveletChartData = useMemo(() => {
if (!waveletData) return []
const { dates, original, bands, mean } = waveletData
// mean is a single scalar for a whole-range decomposition (/wavelet/analyze), but a
// per-day series for the causal/rolling one (/wavelet/rolling and the cached decomposition
// this tab loads by default) — each trailing window is de-meaned independently before
// decomposing it, and that mean drifts as the window slides forward, so there's no single
// constant that reconstructs every day correctly. Without picking the right one per day,
// the reconstruction (and the shared price axis it's plotted on) comes out silently
// missing the entire price level, not just slightly off.
return dates.map((d: string, i: number) => {
const row: any = { date: d.slice(0, 10), original: original[i] }
let recon = mean ?? 0
let recon = Array.isArray(mean) ? (mean[i] ?? 0) : (mean ?? 0)
for (const b of bands) { row[b.label] = b.series[i]; recon += b.series[i] }
row.reconstruction = recon
return row