diff --git a/backend/routers/wavelet.py b/backend/routers/wavelet.py index edd9fb8..7b4e6f4 100644 --- a/backend/routers/wavelet.py +++ b/backend/routers/wavelet.py @@ -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), diff --git a/backend/services/wavelet_engine.py b/backend/services/wavelet_engine.py index cb7d8b5..66b957c 100644 --- a/backend/services/wavelet_engine.py +++ b/backend/services/wavelet_engine.py @@ -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, }) diff --git a/frontend/src/pages/InstrumentDashboard.tsx b/frontend/src/pages/InstrumentDashboard.tsx index 2b7671b..b30b6cb 100644 --- a/frontend/src/pages/InstrumentDashboard.tsx +++ b/frontend/src/pages/InstrumentDashboard.tsx @@ -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(null) const [loadingWavelet, setLoadingWavelet] = useState(false) const [waveletReliability, setWaveletReliability] = useState(null) @@ -2155,7 +2155,7 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i )} @@ -2387,6 +2387,8 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i Bande Cycle moyen Horizon max + Délai réel moyen + Écart calibration Retournements testés Confirmés Indice de confiance @@ -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 ( {b.label} {b.avg_cycle_days != null ? `${b.avg_cycle_days}j` : '—'} {b.confirm_horizon != null ? `+${b.confirm_horizon}j` : '—'} + {b.avg_actual_delay_days != null ? `${b.avg_actual_delay_days}j` : '—'} + {gap != null ? `${gap}j` : '—'} {b.n_tested} {b.n_confirmed} @@ -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 - où 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. + où 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.

)}