feat(SH01): bootstrap storia full-history + last_block_only (punto-10: regime live non robusto)
Ri-validazione sh01_trainwindow_validate.py (rolling train_window == regime live):
- tw=8760 (live 365g): BTC FULL +82% ma fee-2x -42% (6/9 anni), ETH Sharpe -0.02,
trade-rate 21.7-26.4% vs 9.8% validato -> NON robusto. Diagnosi sweep confermata
(LogReg over-confident su train corto, th 0.58 inerte).
- progressione MONOTONA con la memoria (2y: Sh 2.05; 3y: 3.09; expanding: ROBUSTO
8/9) -> l'edge di SH01 e' la memoria lunga.
- sweep soglia nel regime corto: instabile/incoerente fra asset -> NON ri-tunare.
Fix (decisione utente): ripristinare in live il regime validato.
- ml_wf_entries(last_block_only=True): fitta/predice solo l'ultimo blocco del WF
(confini deterministici start+k*retrain) -> entries IDENTICHE per costruzione
al tail del WF completo (parity test esatto); 0.6s/tick su 73k barre vs ~140 fit.
- runner._with_history: tick degli sleeve ml su parquet locale + feed live
(dedup timestamp, gap-guard con WARN una-tantum se parquet stantio).
- _defs.py: params {last_block_only: true} sugli sleeve SHAPE (solo path live;
il backtest canonico resta WF completo).
Effetto atteso live: trade-rate SH01 ~25% -> ~10% delle barre, selettivita'
ripristinata. Manutenzione: parquet fresco via download_all (oggi 2026-05-28,
margine ~11 mesi col feed 365g).
Test: 87/87 (4 nuovi: parity last-block esatta, merge storia, gap fallback).
Diario docs/diary/2026-06-07-sh01-trainwindow.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""Ri-validazione SH01 col train-window del REGIME LIVE (sweep punto 10).
|
||||
|
||||
Diagnosi (improvement-sweep 2026-06-06): tutta la validazione SH01
|
||||
(`shape_ml_validate.py`) usa training EXPANDING full-history (~73k barre dal 2018),
|
||||
ma il worker live fetcha solo `_ML_LOOKBACK_DAYS=365` (~8760 barre 1h) -> la LogReg
|
||||
allenata su train piccolo e' OVER-CONFIDENT: frac(proba>=0.58) 44.8% vs 3.8%,
|
||||
trade-rate live 25.1% delle barre vs 2.9% validato, WR live 1/9. La soglia th=0.58
|
||||
e' di fatto inerte nel regime live.
|
||||
|
||||
Questo harness misura l'edge SH01 (W24 H12 logit th0.58, protocollo explore_lab:
|
||||
FULL / OOS ultimo 30% / sweep fee / anni positivi) in funzione del train-window
|
||||
ROLLING (`ml_wf_entries(train_window=...)`):
|
||||
|
||||
8760 = regime live ATTUALE (365g) <- la validazione mai fatta
|
||||
17520 = 2 anni (gia' noto: BTC regge, +166%/+96%)
|
||||
26280 = 3 anni
|
||||
None = EXPANDING full-history <- la validazione canonica (riferimento)
|
||||
|
||||
+ diagnostica trade-rate per variante e, per il regime live, sweep della soglia
|
||||
(SOLO diagnostica: la lezione del sweep e' di NON ri-tunare th a forza — se
|
||||
l'edge non sopravvive a 8760 la risposta e' cambiare il regime di training del
|
||||
live, non la soglia).
|
||||
|
||||
Esiti possibili:
|
||||
a) edge OK a 8760 -> il live va bene cosi' (e la diagnosi WR 1/9 e' altro);
|
||||
b) edge OK da un lookback X > 8760 -> portare il train live a X
|
||||
(bootstrap storico dal parquet locale + append feed, o lookback maggiore);
|
||||
c) edge solo expanding -> bootstrap parquet full-history o riconsiderare SH01.
|
||||
|
||||
uv run python scripts/analysis/sh01_trainwindow_validate.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from scripts.analysis.explore_lab import get_df, evaluate, robust # noqa: E402
|
||||
from scripts.analysis.shape_ml_research import ml_wf_entries # noqa: E402
|
||||
|
||||
CFG = dict(W=24, H=12, model="logit", thresh=0.58)
|
||||
WINDOWS = [8760, 17520, 26280, None] # None = expanding (canonico)
|
||||
TH_DIAG = [0.58, 0.62, 0.66, 0.70] # sweep soglia SOLO per il regime live
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 108)
|
||||
print(" SH01 — edge vs train-window ROLLING (W24 H12 logit th0.58, protocollo explore_lab)")
|
||||
print(" regime live = 8760 barre (365g); canonico = expanding full-history")
|
||||
print("=" * 108)
|
||||
|
||||
for asset in ("BTC", "ETH"):
|
||||
df = get_df(asset, "1h")
|
||||
nbars = len(df)
|
||||
print(f"\n--- {asset} ({nbars} barre 1h) ---")
|
||||
for tw in WINDOWS:
|
||||
tag = f"tw={tw}" if tw else "EXPANDING"
|
||||
ents = ml_wf_entries(df, train_window=tw, **CFG)
|
||||
rate = len(ents) / nbars * 100
|
||||
print(f" [{tag:<10s}] trade-rate {rate:4.1f}% delle barre")
|
||||
res = evaluate(f"{asset} {tag}", ents, df)
|
||||
print(f" -> {'ROBUSTO' if robust(res) else 'NON robusto'}")
|
||||
|
||||
print(f"\n diagnostica soglia nel REGIME LIVE (tw=8760), {asset}:")
|
||||
for th in TH_DIAG:
|
||||
ents = ml_wf_entries(df, train_window=8760, **{**CFG, "thresh": th})
|
||||
rate = len(ents) / nbars * 100
|
||||
print(f" [th={th:<5}] trade-rate {rate:4.1f}%")
|
||||
evaluate(f"{asset} tw8760 th{th}", ents, df)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -245,12 +245,18 @@ def analog_feat_entries(df, W=24, H=12, K=60, rebuild=300, min_lib=1500,
|
||||
def ml_wf_entries(df, W=24, H=12, model="gb", thresh=0.58,
|
||||
train_min=4000, retrain=500, n_estimators=80, max_depth=3,
|
||||
tp_atr=None, sl_atr=None, trend_max=None, ema_long=200,
|
||||
train_window=None) -> list[dict]:
|
||||
train_window=None, last_block_only=False) -> list[dict]:
|
||||
"""Walk-forward: a blocchi di `retrain` barre, allena sul passato il cui esito
|
||||
e' noto, predice il blocco corrente. Scaler+modello fittati solo sul train.
|
||||
Entra a close[i] se proba della classe predetta >= thresh. model in {gb, logit}.
|
||||
train_window: se None -> expanding (tutto il passato); se int -> ROLLING (solo le
|
||||
ultime train_window barre prima del blocco) -> test di robustezza piu' severo."""
|
||||
ultime train_window barre prima del blocco) -> test di robustezza piu' severo.
|
||||
last_block_only: PATH LIVE (2026-06-07) — fitta e predice SOLO l'ultimo blocco
|
||||
(un fit invece di ~n/retrain): al worker servono solo i segnali sulla barra
|
||||
corrente e i confini dei blocchi sono deterministici (start + k*retrain), quindi
|
||||
le entries dell'ultimo blocco sono IDENTICHE per costruzione al tail del
|
||||
walk-forward completo (parity test in tests/portfolio/test_sh01_last_block.py).
|
||||
Rende sostenibile il train EXPANDING full-history nel runner (~73k barre/tick)."""
|
||||
c = df["close"].values
|
||||
n = len(c)
|
||||
a = atr(df, 14)
|
||||
@@ -269,6 +275,9 @@ def ml_wf_entries(df, W=24, H=12, model="gb", thresh=0.58,
|
||||
blk = start
|
||||
while blk < n - 1:
|
||||
blk_end = min(blk + retrain, n - 1)
|
||||
if last_block_only and blk_end < n - 1:
|
||||
blk = blk_end # salta i blocchi storici (stessi confini del WF completo)
|
||||
continue
|
||||
# TRAIN: finestre la cui forma E il cui esito (e+H) sono < blk
|
||||
# cioe' e <= blk-1-H (esito realizzato prima del primo test del blocco)
|
||||
lo_end = (blk - 1 - H - train_window) if train_window is not None else (W - 1)
|
||||
|
||||
@@ -67,7 +67,12 @@ PAIRS = [
|
||||
TSM = [SleeveSpec(kind="tsmom", name="TSM01", sid="TSM01", cluster="trend",
|
||||
params={"universe": UNIVERSE8, "tf": "1d",
|
||||
"horizons": [63, 126, 252], "thr": 1.0, "gross": 0.30})]
|
||||
SHAPE = [SleeveSpec(kind="ml", name="SH01", sid=f"SH_{a}", asset=a, cluster="shape")
|
||||
# SH01 live (punto-10, 2026-06-07): il runner passa la storia FULL (parquet+feed) e
|
||||
# last_block_only fitta/predice solo l'ultimo blocco del walk-forward (segnali ==
|
||||
# WF completo per costruzione). Il regime corto train-365g NON e' robusto (BTC
|
||||
# negativo a fee 2x, trade-rate 22% vs 10% validato): sh01_trainwindow_validate.py.
|
||||
SHAPE = [SleeveSpec(kind="ml", name="SH01", sid=f"SH_{a}", asset=a, cluster="shape",
|
||||
params={"last_block_only": True})
|
||||
for a in ("BTC", "ETH")]
|
||||
|
||||
PORTFOLIOS = {
|
||||
|
||||
@@ -75,7 +75,14 @@ class ShapeMLStrategy(Strategy):
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex, **params) -> list[Signal]:
|
||||
cfg = {**CONFIG, **{k: params[k] for k in ("W", "H", "model", "thresh") if k in params}}
|
||||
ents = ml_wf_entries(df, W=cfg["W"], H=cfg["H"], model=cfg["model"], thresh=cfg["thresh"])
|
||||
# last_block_only (live, 2026-06-07): il runner passa la storia FULL (parquet
|
||||
# bootstrap + feed) e qui si fitta/predice solo l'ultimo blocco del walk-forward
|
||||
# (segnali identici al WF completo per costruzione, 1 fit invece di ~140).
|
||||
# La ri-validazione punto-10 ha mostrato che il regime corto (train 365g) NON
|
||||
# e' robusto (BTC negativo a fee 2x, trade-rate 22% vs 10%): l'edge richiede
|
||||
# il training expanding full-history. Vedi sh01_trainwindow_validate.py.
|
||||
ents = ml_wf_entries(df, W=cfg["W"], H=cfg["H"], model=cfg["model"], thresh=cfg["thresh"],
|
||||
last_block_only=bool(params.get("last_block_only")))
|
||||
c = df["close"].values
|
||||
out: list[Signal] = []
|
||||
for e in ents:
|
||||
|
||||
Reference in New Issue
Block a user