feat: curve regime

This commit is contained in:
OpenSquared
2026-07-25 09:23:26 +02:00
parent 3addea6850
commit ad7ab35d1d
8 changed files with 177 additions and 31 deletions

View File

@@ -249,10 +249,12 @@ def rolling_causal_bands(
n = len(values)
out_dates: list[str] = []
out_original: list[float] = []
out_mean: list[float] = []
band_series: list[list[float]] = [[] for _ in range(num_levels)]
band_labels = [f"bande {i + 1}" for i in range(num_levels)]
band_failed = [False] * num_levels
last_tip: list[float] | None = None
last_tip_mean = 0.0
recomputations = 0
for t in range(start_idx, n):
@@ -264,12 +266,21 @@ def rolling_causal_bands(
window_dates = dates[window_start:t + 1]
result = band_decompose(window_values, window_dates, num_levels, wavelet)
last_tip = [band["series"][-1] for band in result["bands"]]
# band_decompose de-means each trailing window before decomposing it (see its
# own x_mean/xc) — reconstructing a day's price needs THAT window's mean added
# back, not one fixed constant for the whole causal run (unlike band_decompose's
# own single-window "mean" key): a lookback window slides forward every step, so
# its mean drifts along with any real trend. Losing this here (never captured)
# meant the frontend's mean+sum(bands) reconstruction was silently missing the
# entire price level — bands.sum() alone centers on ~0, not the real price.
last_tip_mean = result["mean"]
band_labels = [band["label"] for band in result["bands"]]
for i, band in enumerate(result["bands"]):
band_failed[i] = band_failed[i] or band.get("reconstruction_failed", False)
recomputations += 1
out_dates.append(dates[t])
out_original.append(values[t])
out_mean.append(last_tip_mean)
for i in range(num_levels):
band_series[i].append(last_tip[i])
@@ -288,6 +299,9 @@ def rolling_causal_bands(
return {
"dates": out_dates,
"original": out_original,
"mean": out_mean, # per-day series (the trailing window's mean drifts) — NOT a
# single scalar like band_decompose's own "mean" key; callers
# must add mean[i] (not a bare `mean`) to bands.sum() per day
"bands": bands,
"wavelet": wavelet,
"lookback": lookback,
@@ -442,6 +456,7 @@ def rolling_causal_bands_ssq(
n = len(values)
out_dates: list[str] = []
out_original: list[float] = []
out_mean: list[float] = []
band_series: list[list[float]] = [[] for _ in range(num_levels)]
energy_series: list[list[float]] = [[] for _ in range(num_levels)]
ridge_series: list[float | None] = []
@@ -450,6 +465,7 @@ def rolling_causal_bands_ssq(
last_tip: list[float] | None = None
last_energy_tip: list[float] | None = None
last_ridge_tip: float | None = None
last_tip_mean = 0.0
recomputations = 0
for t in range(start_idx, n):
@@ -463,12 +479,17 @@ def rolling_causal_bands_ssq(
last_tip = [band["series"][-1] for band in result["bands"]]
last_energy_tip = [band["energy"][-1] for band in result["bands"]]
last_ridge_tip = result["ridge_period_days"][-1]
# See rolling_causal_bands's identical fix for why this is a per-day series,
# not a single scalar: each trailing window is de-meaned independently before
# decomposition, and that mean drifts as the window slides forward.
last_tip_mean = result["mean"]
band_labels = [band["label"] for band in result["bands"]]
for i, band in enumerate(result["bands"]):
band_failed[i] = band_failed[i] or band.get("reconstruction_failed", False)
recomputations += 1
out_dates.append(dates[t])
out_original.append(values[t])
out_mean.append(last_tip_mean)
for i in range(num_levels):
band_series[i].append(last_tip[i])
energy_series[i].append(last_energy_tip[i])
@@ -490,6 +511,7 @@ def rolling_causal_bands_ssq(
return {
"dates": out_dates,
"original": out_original,
"mean": out_mean, # per-day series — see rolling_causal_bands
"bands": bands,
"wavelet": wavelet,
"lookback": lookback,