feat: wavelets
This commit is contained in:
@@ -6,10 +6,13 @@ import {
|
||||
} from 'lucide-react'
|
||||
import axios from 'axios'
|
||||
import clsx from 'clsx'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
||||
import InstrumentChart, { TheoPoint } from '../components/InstrumentChart'
|
||||
|
||||
const api = axios.create({ baseURL: '/api' })
|
||||
|
||||
const WAVELET_BAND_COLORS = ['#3b82f6', '#f59e0b', '#a855f7', '#ef4444', '#14b8a6', '#84cc16']
|
||||
|
||||
// Stable empty array — prevents InstrumentChart useEffect from re-running on every render
|
||||
const NO_CHART_EVENTS: never[] = []
|
||||
|
||||
@@ -1337,7 +1340,15 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
const [loadingNarr, setLoadingNarr] = useState(false)
|
||||
const [selectorOpen, setSelectorOpen] = useState(false)
|
||||
const [selectedDate, setSelectedDate] = useState<string | null>(null)
|
||||
const [tabUnder, setTabUnder] = useState<'counters' | 'analyse'>('counters')
|
||||
const [tabUnder, setTabUnder] = useState<'counters' | 'analyse' | 'wavelets'>('counters')
|
||||
const [waveletLevels, setWaveletLevels] = useState(4)
|
||||
const [waveletFamily, setWaveletFamily] = useState<'gmw' | 'morlet' | 'bump'>('gmw')
|
||||
const [waveletMethod, setWaveletMethod] = useState<'cwt' | 'ssq'>('cwt')
|
||||
const [waveletCausal, setWaveletCausal] = useState(false)
|
||||
const [waveletLookback, setWaveletLookback] = useState(130)
|
||||
const [waveletData, setWaveletData] = useState<any | null>(null)
|
||||
const [loadingWavelet, setLoadingWavelet] = useState(false)
|
||||
const [hiddenBands, setHiddenBands] = useState<Set<string>>(new Set())
|
||||
const [templates, setTemplates] = useState<CausalTemplate[]>([])
|
||||
const [causalScores, setCausalScores] = useState<Record<number, number | null>>({}) // eventId → activation_score*100
|
||||
const [macroAtDate, setMacroAtDate] = useState<MacroGaugeSnap | null>(null)
|
||||
@@ -1532,6 +1543,54 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
const dateLabel = effectiveDate ? fmtDateFR(effectiveDate) : '—'
|
||||
const displayPrice = dateTrend?.current_price ?? snapshot?.current_price
|
||||
|
||||
const runWaveletAnalysis = async () => {
|
||||
if (!selected) return
|
||||
setLoadingWavelet(true)
|
||||
try {
|
||||
const path = waveletCausal ? '/wavelet/rolling' : '/wavelet/analyze'
|
||||
const params: any = { symbol: selected.yf_ticker, period, levels: waveletLevels, wavelet: waveletFamily, method: waveletMethod }
|
||||
if (waveletCausal) params.lookback = waveletLookback
|
||||
const { data } = await api.get(path, { params })
|
||||
setWaveletData(data)
|
||||
setHiddenBands(new Set())
|
||||
} catch (e) {
|
||||
console.error('Wavelet analysis failed', e)
|
||||
setWaveletData(null)
|
||||
} finally {
|
||||
setLoadingWavelet(false)
|
||||
}
|
||||
}
|
||||
|
||||
const waveletChartData = useMemo(() => {
|
||||
if (!waveletData) return []
|
||||
const { dates, original, bands, mean } = waveletData
|
||||
return dates.map((d: string, i: number) => {
|
||||
const row: any = { date: d.slice(0, 10), original: original[i] }
|
||||
let recon = mean ?? 0
|
||||
for (const b of bands) { row[b.label] = b.series[i]; recon += b.series[i] }
|
||||
row.reconstruction = recon
|
||||
return row
|
||||
})
|
||||
}, [waveletData])
|
||||
|
||||
const waveletStats = useMemo(() => {
|
||||
if (!waveletData || waveletChartData.length < 2) return null
|
||||
const orig: number[] = waveletData.original
|
||||
const recon = waveletChartData.map((r: any) => r.reconstruction)
|
||||
const n = orig.length
|
||||
const meanO = orig.reduce((a: number, b: number) => a + b, 0) / n
|
||||
const meanR = recon.reduce((a: number, b: number) => a + b, 0) / n
|
||||
let cov = 0, varO = 0, varR = 0, sumAbs = 0, maxAbs = 0
|
||||
for (let i = 0; i < n; i++) {
|
||||
const dO = orig[i] - meanO, dR = recon[i] - meanR
|
||||
cov += dO * dR; varO += dO * dO; varR += dR * dR
|
||||
const err = Math.abs(orig[i] - recon[i])
|
||||
sumAbs += err; maxAbs = Math.max(maxAbs, err)
|
||||
}
|
||||
const correlation = varO > 0 && varR > 0 ? cov / Math.sqrt(varO * varR) : null
|
||||
return { correlation, meanAbsError: sumAbs / n, maxAbsError: maxAbs }
|
||||
}, [waveletData, waveletChartData])
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-screen-xl mx-auto space-y-4">
|
||||
|
||||
@@ -1666,6 +1725,7 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
{([
|
||||
{ key: 'counters', label: 'Compteurs' },
|
||||
{ key: 'analyse', label: 'Analyse de la courbe' },
|
||||
{ key: 'wavelets', label: 'Ondelettes' },
|
||||
] as const).map(t => (
|
||||
<button
|
||||
key={t.key}
|
||||
@@ -1799,6 +1859,96 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
||||
)
|
||||
})()}
|
||||
|
||||
{tabUnder === 'wavelets' && (
|
||||
<div className="rounded-xl border border-slate-700/40 bg-dark-800/60 p-4">
|
||||
<div className="flex items-center gap-2 mb-3 flex-wrap">
|
||||
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wide">Décomposition Ondelette</span>
|
||||
<div className="ml-auto flex items-center gap-2 flex-wrap text-xs">
|
||||
<label className="flex items-center gap-1 text-slate-400">
|
||||
Niveaux
|
||||
<input type="number" min={2} max={6} value={waveletLevels}
|
||||
onChange={e => setWaveletLevels(Math.max(2, Math.min(6, Number(e.target.value) || 4)))}
|
||||
className="w-12 bg-dark-900 border border-slate-700/40 rounded px-1.5 py-0.5 text-white" />
|
||||
</label>
|
||||
<select value={waveletFamily} onChange={e => setWaveletFamily(e.target.value as any)}
|
||||
className="bg-dark-900 border border-slate-700/40 rounded px-1.5 py-0.5 text-slate-300">
|
||||
<option value="gmw">gmw</option>
|
||||
<option value="morlet">morlet</option>
|
||||
<option value="bump">bump</option>
|
||||
</select>
|
||||
<select value={waveletMethod} onChange={e => setWaveletMethod(e.target.value as any)}
|
||||
className="bg-dark-900 border border-slate-700/40 rounded px-1.5 py-0.5 text-slate-300">
|
||||
<option value="cwt">CWT</option>
|
||||
<option value="ssq">SSQ</option>
|
||||
</select>
|
||||
<label className="flex items-center gap-1 text-slate-400">
|
||||
<input type="checkbox" checked={waveletCausal} onChange={e => setWaveletCausal(e.target.checked)} />
|
||||
Mode causal
|
||||
</label>
|
||||
{waveletCausal && (
|
||||
<label className="flex items-center gap-1 text-slate-400">
|
||||
Lookback
|
||||
<input type="number" min={32} value={waveletLookback}
|
||||
onChange={e => setWaveletLookback(Math.max(32, Number(e.target.value) || 130))}
|
||||
className="w-14 bg-dark-900 border border-slate-700/40 rounded px-1.5 py-0.5 text-white" />
|
||||
</label>
|
||||
)}
|
||||
<button onClick={runWaveletAnalysis} disabled={loadingWavelet}
|
||||
className="px-2.5 py-1 rounded text-xs font-medium border border-blue-600/40 bg-blue-700/30 text-blue-200 hover:bg-blue-700/50 transition-colors disabled:opacity-50">
|
||||
{loadingWavelet ? '↻ Calcul…' : "▶ Lancer l'analyse"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{waveletData ? (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-3 mb-2 text-[10px]">
|
||||
{waveletData.bands.map((b: any, i: number) => (
|
||||
<label key={b.label} className="flex items-center gap-1 cursor-pointer">
|
||||
<input type="checkbox" checked={!hiddenBands.has(b.label)}
|
||||
onChange={() => setHiddenBands(prev => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(b.label)) next.delete(b.label); else next.add(b.label)
|
||||
return next
|
||||
})}
|
||||
/>
|
||||
<span style={{ color: WAVELET_BAND_COLORS[i % WAVELET_BAND_COLORS.length] }}>{b.label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={280}>
|
||||
<LineChart data={waveletChartData} margin={{ top: 4, right: 8, left: -14, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" />
|
||||
<XAxis dataKey="date" tick={{ fontSize: 9, fill: '#64748b' }}
|
||||
interval={Math.max(0, Math.floor(waveletChartData.length / 8))} />
|
||||
<YAxis tick={{ fontSize: 9, fill: '#64748b' }} domain={['auto', 'auto']} />
|
||||
<Tooltip contentStyle={{ background: '#0f172a', border: '1px solid #334155', borderRadius: 6, fontSize: 10 }} />
|
||||
<Line type="monotone" dataKey="original" stroke="#94a3b8" strokeWidth={1.5} dot={false} name="Prix" isAnimationActive={false} />
|
||||
<Line type="monotone" dataKey="reconstruction" stroke="#22c55e" strokeWidth={1} strokeDasharray="4 2" dot={false} name="Reconstruction" isAnimationActive={false} />
|
||||
{waveletData.bands.map((b: any, i: number) => !hiddenBands.has(b.label) && (
|
||||
<Line key={b.label} type="monotone" dataKey={b.label}
|
||||
stroke={WAVELET_BAND_COLORS[i % WAVELET_BAND_COLORS.length]}
|
||||
strokeWidth={1} dot={false} name={b.label} isAnimationActive={false} />
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
{waveletStats && (
|
||||
<div className="mt-2 flex items-center gap-4 flex-wrap text-[10px] text-slate-500">
|
||||
<span>Corrélation reconstruction: <span className="text-slate-300 font-mono">{waveletStats.correlation != null ? waveletStats.correlation.toFixed(3) : '—'}</span></span>
|
||||
<span>Erreur moy.: <span className="text-slate-300 font-mono">{waveletStats.meanAbsError.toFixed(3)}</span></span>
|
||||
<span>Erreur max: <span className="text-slate-300 font-mono">{waveletStats.maxAbsError.toFixed(3)}</span></span>
|
||||
{waveletCausal && <span className="ml-auto text-blue-400/70">Mode causal (walk-forward) — {waveletData.recomputations} recalculs</span>}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-xs text-slate-500 text-center py-10">
|
||||
{loadingWavelet ? 'Calcul en cours…' : "Cliquez sur \"Lancer l'analyse\" pour décomposer le prix en bandes de fréquence"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<NarrativeCard
|
||||
narrative={narrative}
|
||||
loading={loadingNarr}
|
||||
|
||||
Reference in New Issue
Block a user