feat: wavelets

This commit is contained in:
OpenSquared
2026-07-19 16:03:09 +02:00
parent b900d77aa0
commit 9079d4e9c5
3 changed files with 27 additions and 8 deletions

View File

@@ -73,7 +73,7 @@ def wavelet_analyze(
def wavelet_rolling(
symbol: str = Query("SPY"),
period: str = Query("1y", description="how much of the causal output range to return"),
lookback: int = Query(130, ge=32),
lookback: int = Query(260, ge=32),
levels: int = Query(4, ge=2, le=6),
wavelet: str = Query("gmw"),
step: int = Query(1, ge=1),
@@ -114,7 +114,7 @@ def wavelet_rolling(
def wavelet_reliability_endpoint(
symbol: str = Query("SPY"),
period: str = Query("1y", description="how much of the causal output range to scan for turning points"),
lookback: int = Query(130, ge=32),
lookback: int = Query(260, ge=32),
levels: int = Query(4, ge=2, le=6),
wavelet: str = Query("gmw"),
step: int = Query(1, ge=1),

View File

@@ -610,11 +610,20 @@ def wavelet_reliability(
# last index already IS "original date + horizon"; no separate bound needed.
d_pos = g_idx - window_start
hindsight_turns = _turning_points(h_series, smooth_days)
confirmed = any(idx >= d_pos and new_dir == direction for idx, new_dir in hindsight_turns)
tested.append({"date": d_date, "confirmed": confirmed})
matches = [idx for idx, new_dir in hindsight_turns if idx >= d_pos and new_dir == direction]
confirmed = bool(matches)
# Actual delay = how many days after the original date the confirming reversal
# showed up (earliest match) — compared against avg_cycle_days below to check
# whether "one cycle" is actually a well-calibrated horizon, or systematically
# too long/short relative to how quickly a real reversal actually confirms.
actual_delay = (min(matches) - d_pos) if confirmed else None
tested.append({"date": d_date, "confirmed": confirmed, "actual_delay_days": actual_delay})
n_tested = len(tested)
n_confirmed = sum(1 for t in tested if t["confirmed"])
delays = [t["actual_delay_days"] for t in tested if t["actual_delay_days"] is not None]
avg_actual_delay = round(sum(delays) / len(delays), 1) if delays else None
calibration_gap = round(sum(abs(avg_cycle_days - d) for d in delays) / len(delays), 1) if delays else None
bands_out.append({
"label": band["label"],
"n_tested": n_tested,
@@ -622,6 +631,8 @@ def wavelet_reliability(
"confidence_pct": round(n_confirmed / n_tested * 100, 1) if n_tested else None,
"avg_cycle_days": round(avg_cycle_days, 1),
"confirm_horizon": confirm_horizon,
"avg_actual_delay_days": avg_actual_delay,
"calibration_gap_days": calibration_gap,
"turns": tested,
})

View File

@@ -1430,7 +1430,7 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
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 [waveletLookback, setWaveletLookback] = useState(260)
const [waveletData, setWaveletData] = useState<any | null>(null)
const [loadingWavelet, setLoadingWavelet] = useState(false)
const [waveletReliability, setWaveletReliability] = useState<any | null>(null)
@@ -2155,7 +2155,7 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
<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))}
onChange={e => setWaveletLookback(Math.max(32, Number(e.target.value) || 260))}
className="w-14 bg-dark-900 border border-slate-700/40 rounded px-1.5 py-0.5 text-white" />
</label>
)}
@@ -2387,6 +2387,8 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
<th className="py-1 pr-3 font-normal">Bande</th>
<th className="py-1 pr-3 font-normal text-right" title="Écart moyen entre deux pics (ou deux creux) consécutifs, mesuré sur l'historique — pas la période théorique de la bande.">Cycle moyen</th>
<th className="py-1 pr-3 font-normal text-right" title="= cycle moyen × 1.10 — le retournement peut être confirmé n'importe où entre la date d'origine et date+horizon, pas juste pile à cette date">Horizon max</th>
<th className="py-1 pr-3 font-normal text-right" title="Délai réel moyen (parmi les retournements confirmés) entre la date d'origine et l'apparition du retournement confirmant, en hindsight">Délai réel moyen</th>
<th className="py-1 pr-3 font-normal text-right" title="Écart absolu moyen entre le cycle moyen (l'horizon supposé) et le délai réel observé — petit = l'horizon est bien calibré, grand = il est trop long ou trop court par rapport à la réalité">Écart calibration</th>
<th className="py-1 pr-3 font-normal text-right">Retournements testés</th>
<th className="py-1 pr-3 font-normal text-right">Confirmés</th>
<th className="py-1 pr-3 font-normal text-right">Indice de confiance</th>
@@ -2396,11 +2398,15 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
{waveletReliability.bands.map((b: any, i: number) => {
const pct = b.confidence_pct
const color = pct == null ? 'text-slate-500' : pct >= 70 ? 'text-emerald-400' : pct >= 40 ? 'text-amber-400' : 'text-red-400'
const gap = b.calibration_gap_days
const gapColor = gap == null ? 'text-slate-600' : gap <= 2 ? 'text-emerald-400' : gap <= 8 ? 'text-amber-400' : 'text-red-400'
return (
<tr key={b.label} className="border-t border-slate-800/60">
<td className="py-1 pr-3" style={{ color: WAVELET_BAND_COLORS[i % WAVELET_BAND_COLORS.length] }}>{b.label}</td>
<td className="py-1 pr-3 text-right text-slate-400 font-mono">{b.avg_cycle_days != null ? `${b.avg_cycle_days}j` : '—'}</td>
<td className="py-1 pr-3 text-right text-slate-400 font-mono">{b.confirm_horizon != null ? `+${b.confirm_horizon}j` : '—'}</td>
<td className="py-1 pr-3 text-right text-slate-400 font-mono">{b.avg_actual_delay_days != null ? `${b.avg_actual_delay_days}j` : '—'}</td>
<td className={clsx('py-1 pr-3 text-right font-mono', gapColor)}>{gap != null ? `${gap}j` : '—'}</td>
<td className="py-1 pr-3 text-right text-slate-300 font-mono">{b.n_tested}</td>
<td className="py-1 pr-3 text-right text-slate-300 font-mono">{b.n_confirmed}</td>
<td className={clsx('py-1 pr-3 text-right font-mono font-semibold', color)}>
@@ -2415,8 +2421,10 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
Pour chaque retournement détecté en mode causal (pente lissée sur {waveletReliability.smooth_days}j qui change de signe, sans regarder le futur),
l'horizon de recalcul est propre à chaque bande — cycle moyen historique (pic-à-pic) × 1.10, pas un nombre de jours fixe identique pour toutes
(10 jours ne veut rien dire pour une bande à 2j de période comme pour une à 20j). Le retournement confirme l'original s'il réapparaît n'importe
entre la date d'origine et date+horizon. Un indice bas signale des retournements souvent dus à un effet de bord de la décomposition
(peu fiable pile au bord de la fenêtre disponible), pas à un vrai signal.
entre la date d'origine et date+horizon. "Écart calibration" compare l'horizon supposé (cycle moyen) au délai réel observé un écart élevé
signale que l'horizon utilisé n'est pas représentatif de la vitesse réelle de confirmation, indépendamment de l'indice de confiance lui-même.
Un indice bas signale des retournements souvent dus à un effet de bord de la décomposition (peu fiable pile au bord de la fenêtre disponible),
pas à un vrai signal.
</p>
</div>
)}