feat: curve regime
This commit is contained in:
@@ -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'],
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user