feat: wavelets
This commit is contained in:
@@ -516,6 +516,19 @@ def _turning_points(series: list[float], smooth_days: int) -> list[tuple[int, in
|
||||
return turns
|
||||
|
||||
|
||||
def _average_cycle_days(turn_points: list[tuple[int, int]]) -> float | None:
|
||||
"""Empirical average gap (in days) between consecutive SAME-direction turning points —
|
||||
peak-to-peak or trough-to-trough, i.e. one full oscillation — measured from the actual
|
||||
reconstructed series rather than assumed from the band's theoretical period_low/high
|
||||
labels (which describe the wavelet scale bucket, not necessarily the real cycle length
|
||||
this specific window's price action produced). None if there aren't at least 2
|
||||
same-direction turns to measure a gap from."""
|
||||
peaks = [t for t, d in turn_points if d == -1] # a downturn = just passed a peak
|
||||
troughs = [t for t, d in turn_points if d == 1] # an upturn = just passed a trough
|
||||
gaps = [seq[i + 1] - seq[i] for seq in (peaks, troughs) for i in range(len(seq) - 1)]
|
||||
return sum(gaps) / len(gaps) if gaps else None
|
||||
|
||||
|
||||
def wavelet_reliability(
|
||||
values: list[float],
|
||||
dates: list[str],
|
||||
@@ -526,18 +539,27 @@ def wavelet_reliability(
|
||||
method: str = "cwt",
|
||||
step: int = 1,
|
||||
smooth_days: int = 3,
|
||||
tolerance_days: int = 5,
|
||||
confirm_horizon: int = 10,
|
||||
tolerance_pct: float = 0.10,
|
||||
min_confirm_horizon: int = 3,
|
||||
) -> dict:
|
||||
"""
|
||||
For every turning point a CAUSAL (walk-forward, no look-ahead) decomposition flags —
|
||||
i.e. what a live signal would have shown on that date, using only data available up to
|
||||
then — checks whether redoing the decomposition `confirm_horizon` days later (with that
|
||||
much more real data, so the date in question is no longer sitting at the unstable tip of
|
||||
the window) still shows a same-direction reversal within `tolerance_days` of the original
|
||||
date. The fraction confirmed is a per-band confidence score: CWT/SSQ reconstructions are
|
||||
well known to be least reliable right at the edge of the available data — exactly where a
|
||||
live "the band just turned" reading gets made.
|
||||
then — checks whether redoing the decomposition later still shows a same-direction
|
||||
reversal near the original date. The fraction confirmed is a per-band confidence score:
|
||||
CWT/SSQ reconstructions are well known to be least reliable right at the edge of the
|
||||
available data — exactly where a live "the band just turned" reading gets made.
|
||||
|
||||
A fixed confirm horizon (e.g. always +10 days) is meaningless across bands whose natural
|
||||
oscillation period can range from ~1 day to several weeks: 10 days is many cycles for a
|
||||
fast band (trivially "confirmed", inflating its score) but less than half a cycle for a
|
||||
slow one (never has time to actually turn back, deflating its score) — the exact pattern
|
||||
that shows up as fast bands scoring ~100% and the slow band scoring ~10% for no real
|
||||
reliability reason. Instead, per band: measure its own historical average peak-to-peak
|
||||
(or trough-to-trough) period from the causal series, and use `avg_cycle * (1 +
|
||||
tolerance_pct)` as a MAXIMUM horizon — a same-direction reversal confirms the original one
|
||||
if it shows up ANYWHERE in the forward window [original date, original date + that
|
||||
horizon], not just near one specific point in it.
|
||||
"""
|
||||
rolling_decomposer = rolling_causal_bands_ssq if method == "ssq" else rolling_causal_bands
|
||||
batch_decompose = band_decompose_ssq if method == "ssq" else band_decompose
|
||||
@@ -552,6 +574,16 @@ def wavelet_reliability(
|
||||
bands_out = []
|
||||
for band in causal["bands"]:
|
||||
turn_points = _turning_points(band["series"], smooth_days)
|
||||
avg_cycle_days = _average_cycle_days(turn_points)
|
||||
|
||||
if avg_cycle_days is None:
|
||||
bands_out.append({
|
||||
"label": band["label"], "n_tested": 0, "n_confirmed": 0, "confidence_pct": None,
|
||||
"avg_cycle_days": None, "confirm_horizon": None, "turns": [],
|
||||
})
|
||||
continue
|
||||
|
||||
confirm_horizon = max(min_confirm_horizon, round(avg_cycle_days * (1 + tolerance_pct)))
|
||||
|
||||
tested = []
|
||||
for t, direction in turn_points:
|
||||
@@ -572,10 +604,13 @@ def wavelet_reliability(
|
||||
continue
|
||||
h_series = hindsight["bands"][band["index"]]["series"]
|
||||
|
||||
# Forward-only window [original date, original date + horizon] — the reversal
|
||||
# can be confirmed anywhere in it, not just near a specific point. window_values
|
||||
# was built to end exactly at end_idx = g_idx + confirm_horizon, so h_series's
|
||||
# last index already IS "original date + horizon"; no separate bound needed.
|
||||
d_pos = g_idx - window_start
|
||||
lo, hi = d_pos - tolerance_days, d_pos + tolerance_days
|
||||
hindsight_turns = _turning_points(h_series, smooth_days)
|
||||
confirmed = any(lo <= idx <= hi and new_dir == direction for idx, new_dir in hindsight_turns)
|
||||
confirmed = any(idx >= d_pos and new_dir == direction for idx, new_dir in hindsight_turns)
|
||||
tested.append({"date": d_date, "confirmed": confirmed})
|
||||
|
||||
n_tested = len(tested)
|
||||
@@ -585,13 +620,14 @@ def wavelet_reliability(
|
||||
"n_tested": n_tested,
|
||||
"n_confirmed": n_confirmed,
|
||||
"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,
|
||||
"turns": tested,
|
||||
})
|
||||
|
||||
return {
|
||||
"bands": bands_out,
|
||||
"smooth_days": smooth_days,
|
||||
"tolerance_days": tolerance_days,
|
||||
"confirm_horizon": confirm_horizon,
|
||||
"tolerance_pct": tolerance_pct,
|
||||
"method": method,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user