feat: wavelets
This commit is contained in:
@@ -73,7 +73,7 @@ def wavelet_analyze(
|
|||||||
def wavelet_rolling(
|
def wavelet_rolling(
|
||||||
symbol: str = Query("SPY"),
|
symbol: str = Query("SPY"),
|
||||||
period: str = Query("1y", description="how much of the causal output range to return"),
|
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),
|
levels: int = Query(4, ge=2, le=6),
|
||||||
wavelet: str = Query("gmw"),
|
wavelet: str = Query("gmw"),
|
||||||
step: int = Query(1, ge=1),
|
step: int = Query(1, ge=1),
|
||||||
@@ -114,7 +114,7 @@ def wavelet_rolling(
|
|||||||
def wavelet_reliability_endpoint(
|
def wavelet_reliability_endpoint(
|
||||||
symbol: str = Query("SPY"),
|
symbol: str = Query("SPY"),
|
||||||
period: str = Query("1y", description="how much of the causal output range to scan for turning points"),
|
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),
|
levels: int = Query(4, ge=2, le=6),
|
||||||
wavelet: str = Query("gmw"),
|
wavelet: str = Query("gmw"),
|
||||||
step: int = Query(1, ge=1),
|
step: int = Query(1, ge=1),
|
||||||
|
|||||||
@@ -610,11 +610,20 @@ def wavelet_reliability(
|
|||||||
# last index already IS "original date + horizon"; no separate bound needed.
|
# last index already IS "original date + horizon"; no separate bound needed.
|
||||||
d_pos = g_idx - window_start
|
d_pos = g_idx - window_start
|
||||||
hindsight_turns = _turning_points(h_series, smooth_days)
|
hindsight_turns = _turning_points(h_series, smooth_days)
|
||||||
confirmed = any(idx >= d_pos and new_dir == direction for idx, new_dir in hindsight_turns)
|
matches = [idx for idx, new_dir in hindsight_turns if idx >= d_pos and new_dir == direction]
|
||||||
tested.append({"date": d_date, "confirmed": confirmed})
|
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_tested = len(tested)
|
||||||
n_confirmed = sum(1 for t in tested if t["confirmed"])
|
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({
|
bands_out.append({
|
||||||
"label": band["label"],
|
"label": band["label"],
|
||||||
"n_tested": n_tested,
|
"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,
|
"confidence_pct": round(n_confirmed / n_tested * 100, 1) if n_tested else None,
|
||||||
"avg_cycle_days": round(avg_cycle_days, 1),
|
"avg_cycle_days": round(avg_cycle_days, 1),
|
||||||
"confirm_horizon": confirm_horizon,
|
"confirm_horizon": confirm_horizon,
|
||||||
|
"avg_actual_delay_days": avg_actual_delay,
|
||||||
|
"calibration_gap_days": calibration_gap,
|
||||||
"turns": tested,
|
"turns": tested,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1430,7 +1430,7 @@ export default function InstrumentDashboard({ instrumentIdProp, isVisible }: { i
|
|||||||
const [waveletFamily, setWaveletFamily] = useState<'gmw' | 'morlet' | 'bump'>('gmw')
|
const [waveletFamily, setWaveletFamily] = useState<'gmw' | 'morlet' | 'bump'>('gmw')
|
||||||
const [waveletMethod, setWaveletMethod] = useState<'cwt' | 'ssq'>('cwt')
|
const [waveletMethod, setWaveletMethod] = useState<'cwt' | 'ssq'>('cwt')
|
||||||
const [waveletCausal, setWaveletCausal] = useState(false)
|
const [waveletCausal, setWaveletCausal] = useState(false)
|
||||||
const [waveletLookback, setWaveletLookback] = useState(130)
|
const [waveletLookback, setWaveletLookback] = useState(260)
|
||||||
const [waveletData, setWaveletData] = useState<any | null>(null)
|
const [waveletData, setWaveletData] = useState<any | null>(null)
|
||||||
const [loadingWavelet, setLoadingWavelet] = useState(false)
|
const [loadingWavelet, setLoadingWavelet] = useState(false)
|
||||||
const [waveletReliability, setWaveletReliability] = useState<any | null>(null)
|
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">
|
<label className="flex items-center gap-1 text-slate-400">
|
||||||
Lookback
|
Lookback
|
||||||
<input type="number" min={32} value={waveletLookback}
|
<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" />
|
className="w-14 bg-dark-900 border border-slate-700/40 rounded px-1.5 py-0.5 text-white" />
|
||||||
</label>
|
</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">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="É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="= 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">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">Confirmés</th>
|
||||||
<th className="py-1 pr-3 font-normal text-right">Indice de confiance</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) => {
|
{waveletReliability.bands.map((b: any, i: number) => {
|
||||||
const pct = b.confidence_pct
|
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 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 (
|
return (
|
||||||
<tr key={b.label} className="border-t border-slate-800/60">
|
<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" 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.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.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_tested}</td>
|
||||||
<td className="py-1 pr-3 text-right text-slate-300 font-mono">{b.n_confirmed}</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)}>
|
<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),
|
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
|
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
|
(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
|
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é
|
||||||
(peu fiable pile au bord de la fenêtre disponible), pas à un vrai signal.
|
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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user