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:
Adriano Dal Pastro
2026-06-07 16:48:22 +00:00
parent be85a454e0
commit 8c5372c487
8 changed files with 268 additions and 6 deletions
+10
View File
@@ -357,6 +357,16 @@ queste fade, ma va confermato col paper trader live prima di rischiare capitale
- **SH01 SENZA STOP-LOSS — by design, CONFERMATO da ricerca (2026-06-05).** Dopo il crash ETH (15.6% su un trade SH01 live), ricerca multi-agente con harness dedicato `scripts/analysis/sh01_exit_lab.py` (cache segnali walk-forward, engine con **fill gap-aware** `worse(livello, open)`, parity esatta con explore_lab, protocollo train≤2023-11-01/OOS): **11 famiglie di stop testate (ATR intrabar/close-confirm, %, chandelier, breakeven, giveback, loser-timestop, disaster-cap close+intrabar, swing, vol-regime), 0 sopravvissute** al gate (ETH migliorato senza degradare BTC, train E oos, plateau). Pattern: ogni stop stretto abbastanza da toccare la coda ETH rompe BTC; ogni stop largo non arriva alla coda; nei crash il fill è al gap, non al livello (lo stop "protettivo" PEGGIORA la coda OOS). Mitigazione adottata: **cap famiglia SHAPE a 0.0588 in PORT06** (≈ dimezzata; costo OOS Sharpe 0.24, FULL DD 0.14pp) — la prossima coda impatta il conto per metà. NON impostare mai `sl`/`sl_confirm_atr` su SH_BTC/SH_ETH. Direzione futura: liquidity-gate sull'entry (skip dopo feed flat). Diario `docs/diary/2026-06-05-sh01-sl-research.md`. - **SH01 SENZA STOP-LOSS — by design, CONFERMATO da ricerca (2026-06-05).** Dopo il crash ETH (15.6% su un trade SH01 live), ricerca multi-agente con harness dedicato `scripts/analysis/sh01_exit_lab.py` (cache segnali walk-forward, engine con **fill gap-aware** `worse(livello, open)`, parity esatta con explore_lab, protocollo train≤2023-11-01/OOS): **11 famiglie di stop testate (ATR intrabar/close-confirm, %, chandelier, breakeven, giveback, loser-timestop, disaster-cap close+intrabar, swing, vol-regime), 0 sopravvissute** al gate (ETH migliorato senza degradare BTC, train E oos, plateau). Pattern: ogni stop stretto abbastanza da toccare la coda ETH rompe BTC; ogni stop largo non arriva alla coda; nei crash il fill è al gap, non al livello (lo stop "protettivo" PEGGIORA la coda OOS). Mitigazione adottata: **cap famiglia SHAPE a 0.0588 in PORT06** (≈ dimezzata; costo OOS Sharpe 0.24, FULL DD 0.14pp) — la prossima coda impatta il conto per metà. NON impostare mai `sl`/`sl_confirm_atr` su SH_BTC/SH_ETH. Direzione futura: liquidity-gate sull'entry (skip dopo feed flat). Diario `docs/diary/2026-06-05-sh01-sl-research.md`.
- **Data layer Cerbero v2:** `get_historical_v2` unificato + `get_instruments` (naming robusto) + `get_ticker_batch`. Trading su Deribit. - **Data layer Cerbero v2:** `get_historical_v2` unificato + `get_instruments` (naming robusto) + `get_ticker_batch`. Trading su Deribit.
- **SCOPE LIVE (fase 2 completata):** il runner esegue TUTTI gli sleeve di PORT06. Worker: single `StrategyWorker` (fade MR01/02/07, DIP01, **e SH01**), `PairsWorker` (PR01 2 gambe), e i multi-asset dedicati `BasketTrendWorker` (TR01 4h), `RotationWorker` (ROT02 1d), `TsmomWorker` (TSM01 1d). Il runner fetcha 1h da Cerbero v2 e **resampla a 4h/1d** (lookback dimensionato sui daily: TSM01 usa 252g). Validazione: runner pool/ribilancio/ledger == backtest (`validate_portfolio_runner.py`, identico); worker multi-asset == reference (`validate_honest_workers.py`: TSM01 esatto, ROT02 +1303% canonico, TR01 stesso ordine — differenza di convenzione capitale-unico vs media-equity). - **SCOPE LIVE (fase 2 completata):** il runner esegue TUTTI gli sleeve di PORT06. Worker: single `StrategyWorker` (fade MR01/02/07, DIP01, **e SH01**), `PairsWorker` (PR01 2 gambe), e i multi-asset dedicati `BasketTrendWorker` (TR01 4h), `RotationWorker` (ROT02 1d), `TsmomWorker` (TSM01 1d). Il runner fetcha 1h da Cerbero v2 e **resampla a 4h/1d** (lookback dimensionato sui daily: TSM01 usa 252g). Validazione: runner pool/ribilancio/ledger == backtest (`validate_portfolio_runner.py`, identico); worker multi-asset == reference (`validate_honest_workers.py`: TSM01 esatto, ROT02 +1303% canonico, TR01 stesso ordine — differenza di convenzione capitale-unico vs media-equity).
- **SH01 bootstrap full-history (punto-10, 2026-06-07).** La ri-validazione col train-window del
regime live (`sh01_trainwindow_validate.py`) ha mostrato che SH01 a train 365g **NON è robusto**
(BTC fee-2x FULL 42%, ETH Sharpe 0.02, trade-rate 22-26% vs 10% validato: LogReg over-confident,
soglia inerte — la diagnosi sweep era esatta). L'edge è MONOTONO nella memoria: solo l'expanding
full-history passa il gate. Fix live: il runner passa agli sleeve `ml` la storia FULL
(`_with_history`: parquet locale + feed, gap-guard con WARN) e `ml_wf_entries(last_block_only=True)`
fitta SOLO l'ultimo blocco del walk-forward → segnali **identici per costruzione** al WF completo
(parity test esatto), 0.6 s/tick su 73k barre. Manutenzione: tenere fresco il parquet
(`download_all()`). MAI ri-tunare la soglia nel regime corto (instabile/incoerente fra asset).
Diario `docs/diary/2026-06-07-sh01-trainwindow.md`.
- **FIX SH01 wiring (2026-06-01).** SH01 gira come **`StrategyWorker` NORMALE** (NON il vecchio `MLWorkerWrapper` di `multi_runner`, che usava il `SignalEngine` **squeeze SCARTATO**: apriva senza metadata ed usciva a `hold_bars=3`, ignorando del tutto SH01_shape_ml). `SH01_shape_ml.generate_signals` fa il walk-forward (retraining) internamente ad ogni tick ed emette `metadata.max_bars=12` → exit a orizzonte via `StrategyWorker.tick`. Serve ≥4000 barre 1h (`_ML_LOOKBACK_DAYS=365`). Vedi `docs/diary/2026-06-01-sh01-wiring-squeeze-bug.md`. - **FIX SH01 wiring (2026-06-01).** SH01 gira come **`StrategyWorker` NORMALE** (NON il vecchio `MLWorkerWrapper` di `multi_runner`, che usava il `SignalEngine` **squeeze SCARTATO**: apriva senza metadata ed usciva a `hold_bars=3`, ignorando del tutto SH01_shape_ml). `SH01_shape_ml.generate_signals` fa il walk-forward (retraining) internamente ad ogni tick ed emette `metadata.max_bars=12` → exit a orizzonte via `StrategyWorker.tick`. Serve ≥4000 barre 1h (`_ML_LOOKBACK_DAYS=365`). Vedi `docs/diary/2026-06-01-sh01-wiring-squeeze-bug.md`.
- **Altri fix StrategyWorker (2026-06-01).** Exit a orizzonte puro per strategie senza TP/SL (`elif self.max_bars`, SH01 esce a H=12 non hold_bars=3); `is_win = net > 0` (win NETTO fee, non lordo); filtro `min_tp_frac` (salta micro-scalp col TP entro le fee); loss-guard `hurst_max=0.55` sulle fade (vedi sopra). - **Altri fix StrategyWorker (2026-06-01).** Exit a orizzonte puro per strategie senza TP/SL (`elif self.max_bars`, SH01 esce a H=12 non hold_bars=3); `is_win = net > 0` (win NETTO fee, non lordo); filtro `min_tp_frac` (salta micro-scalp col TP entro le fee); loss-guard `hurst_max=0.55` sulle fade (vedi sopra).
- **Exit intrabar (fase 3, risolto):** lo `StrategyWorker` ora esce sui TP/SL toccati INTRABAR (high/low della barra, al livello, SL prioritario) come il backtest — non più solo sul close. Allinea fade/DIP01 live al backtest intrabar (`tests/portfolio/test_intrabar_exit.py`). Caveat residuo onesto: nel paper trading l'high/low usato è quello della barra in corso al poll; su un fill reale conterebbe il momento del tocco. - **Exit intrabar (fase 3, risolto):** lo `StrategyWorker` ora esce sui TP/SL toccati INTRABAR (high/low della barra, al livello, SL prioritario) come il backtest — non più solo sul close. Allinea fade/DIP01 live al backtest intrabar (`tests/portfolio/test_intrabar_exit.py`). Caveat residuo onesto: nel paper trading l'high/low usato è quello della barra in corso al poll; su un fill reale conterebbe il momento del tocco.
+51
View File
@@ -0,0 +1,51 @@
# 2026-06-07 — SH01 punto-10: il regime live (train 365g) NON è robusto → bootstrap full-history
Punto 10 della roadmap sweep: ri-validare SH01 col train-window del regime live
(~8760 barre, il `_ML_LOOKBACK_DAYS=365` del runner) invece dell'expanding
full-history usato da tutta la validazione storica.
## Ri-validazione (`scripts/analysis/sh01_trainwindow_validate.py`)
`ml_wf_entries(train_window=...)` rolling su storia piena ≈ il regime live.
Protocollo explore_lab (FULL / OOS 30% / sweep fee / anni positivi), W24 H12 th0.58.
| BTC train | trade-rate | FULL / OOS | Sharpe | fee 0.2%: FULL/OOS | anni+ | robusto |
|---|---|---|---|---|---|---|
| 8760 (live) | **21.7%** | +82 / +74 | 1.31 | **42** / +20 | 6/9 | ❌ |
| 17520 (2y) | 15.4% | +166 / +96 | 2.05 | +9 / +47 | 6/9 | ❌ |
| 26280 (3y) | 11.8% | +348 / +95 | 3.09 | +109 / +61 | 7/9 | ❌ |
| EXPANDING | 9.8% | +219 / +42 | 2.72 | +60 / +26 | 8/9 | ✅ |
ETH a tw=8760: **FULL 33%, Sharpe 0.02** (lancio di moneta che paga fee).
Conferme: (a) la diagnosi del sweep era esatta (trade-rate 22-26% live vs 10%
validato — soglia inerte per over-confidence su train piccolo); (b) la
progressione è MONOTONA: l'edge di SH01 *è* la memoria lunga; (c) lo sweep della
soglia nel regime corto dà risultati instabili e incoerenti fra asset → ri-tunare
th sarebbe curve-fitting sul rumore di calibrazione (come da warning del piano).
## Decisione (utente): bootstrap full-history
Ripristinare in live ESATTAMENTE il regime validato (expanding full-history):
1. **`ml_wf_entries(last_block_only=True)`** — fitta/predice SOLO l'ultimo blocco
del walk-forward. I confini dei blocchi sono deterministici (start + k·retrain)
e al worker servono solo i segnali recenti → le entries dell'ultimo blocco sono
**identiche per costruzione** al tail del WF completo (parity test esatto in
`tests/portfolio/test_sh01_last_block.py`). Costo live: **0.6 s/tick** su 73k
barre (1 fit LogReg) vs ~140 fit del WF completo.
2. **Bootstrap storia nel runner**: per gli sleeve `ml` il tick riceve
`concat(parquet_locale < feed_start, feed_live)` (`runner._with_history`).
Gap-guard: se il parquet è stantio oltre il lookback del feed → solo feed +
WARN una-tantum (meglio il regime corto dichiarato che una serie col buco).
Manutenzione: tenere fresco il parquet con `download_all()` (oggi al 2026-05-28,
il feed copre 365g → margine ~11 mesi).
3. `_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 da ~25% a ~10% delle barre, selettività
della soglia ripristinata, WR atteso ≈ validazione (acc OOS ~56% BTC).
Il monitor naturale è il report orario (SH01 farà MENO trade).
Nota: `_ML_LOOKBACK_DAYS` resta 365 (il feed serve comunque per le barre recenti
e come fallback se il parquet è stantio).
@@ -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()
+11 -2
View File
@@ -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, 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, train_min=4000, retrain=500, n_estimators=80, max_depth=3,
tp_atr=None, sl_atr=None, trend_max=None, ema_long=200, 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 """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. 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}. 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 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 c = df["close"].values
n = len(c) n = len(c)
a = atr(df, 14) a = atr(df, 14)
@@ -269,6 +275,9 @@ def ml_wf_entries(df, W=24, H=12, model="gb", thresh=0.58,
blk = start blk = start
while blk < n - 1: while blk < n - 1:
blk_end = min(blk + retrain, 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 # 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) # 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) lo_end = (blk - 1 - H - train_window) if train_window is not None else (W - 1)
+6 -1
View File
@@ -67,7 +67,12 @@ PAIRS = [
TSM = [SleeveSpec(kind="tsmom", name="TSM01", sid="TSM01", cluster="trend", TSM = [SleeveSpec(kind="tsmom", name="TSM01", sid="TSM01", cluster="trend",
params={"universe": UNIVERSE8, "tf": "1d", params={"universe": UNIVERSE8, "tf": "1d",
"horizons": [63, 126, 252], "thr": 1.0, "gross": 0.30})] "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")] for a in ("BTC", "ETH")]
PORTFOLIOS = { PORTFOLIOS = {
+8 -1
View File
@@ -75,7 +75,14 @@ class ShapeMLStrategy(Strategy):
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex, **params) -> list[Signal]: 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}} 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 c = df["close"].values
out: list[Signal] = [] out: list[Signal] = []
for e in ents: for e in ents:
+47 -2
View File
@@ -116,6 +116,32 @@ def rebalance_allocations(ledger: PortfolioLedger, workers: dict, weights: dict[
ledger.save() ledger.save()
_OHLCV = ["timestamp", "open", "high", "low", "close", "volume"]
def _with_history(hist: pd.DataFrame | None, live: pd.DataFrame,
warned: set | None = None, asset: str = "") -> pd.DataFrame:
"""Bootstrap storia per SH01 (punto-10, 2026-06-07): parquet locale PRIMA del
feed live (dedup sul timestamp). La ri-validazione ha mostrato che l'edge SH01
richiede il training EXPANDING full-history: col solo lookback live (365g) la
LogReg e' over-confident e la strategia NON e' robusta. Se c'e' un gap fra
parquet e feed (parquet stantio oltre il lookback) si usa il SOLO feed con
WARN: meglio il regime corto dichiarato che una serie con un buco."""
if hist is None or live is None or live.empty:
return live
lo = int(live["timestamp"].iloc[0])
h = hist[hist["timestamp"] < lo]
if h.empty:
return live
if int(h["timestamp"].iloc[-1]) < lo - 2 * 3_600_000: # buco > 1 barra 1h
if warned is not None and asset not in warned:
warned.add(asset)
print(f"[runner] WARN: gap fra parquet e feed live per {asset} "
f"(parquet stantio? rilanciare download_all) — SH01 senza bootstrap")
return live
return pd.concat([h[_OHLCV], live[_OHLCV]], ignore_index=True)
def _resample(df: pd.DataFrame, tf: str) -> pd.DataFrame: def _resample(df: pd.DataFrame, tf: str) -> pd.DataFrame:
"""Resampla candele 1h -> 4h/1d mantenendo timestamp ms reale (come get_df del backtest).""" """Resampla candele 1h -> 4h/1d mantenendo timestamp ms reale (come get_df del backtest)."""
if tf == "1h": if tf == "1h":
@@ -247,6 +273,21 @@ def run(config_path: str = "portfolios.yml"):
if ps_family: if ps_family:
print(f"[runner] position_size globale={position_size} override famiglia={ps_family}") print(f"[runner] position_size globale={position_size} override famiglia={ps_family}")
# bootstrap storia full per gli sleeve ML (SH01): parquet locale + feed live.
# L'edge SH01 richiede train expanding full-history (sh01_trainwindow_validate);
# il path live fitta solo l'ultimo blocco (last_block_only nei params SHAPE).
ml_hist: dict[str, pd.DataFrame] = {}
ml_gap_warned: set[str] = set()
for a in sorted({s.asset for s in live_specs if s.kind == "ml"}):
try:
from src.data.downloader import load_data
ml_hist[a] = load_data(a, "1h")
last = pd.to_datetime(ml_hist[a]["timestamp"].iloc[-1], unit="ms", utc=True)
print(f"[runner] bootstrap storia SH01 {a}: {len(ml_hist[a])} barre parquet "
f"(fino a {last:%Y-%m-%d %H:%M})")
except Exception as e:
print(f"[runner] WARN bootstrap storia {a} fallito: {e} — SH01 col solo feed")
# lookback (giorni) richiesto per ogni asset = max sui worker che lo usano # lookback (giorni) richiesto per ogni asset = max sui worker che lo usano
asset_days: dict[str, int] = {} asset_days: dict[str, int] = {}
for s in live_specs: for s in live_specs:
@@ -322,9 +363,13 @@ def run(config_path: str = "portfolios.yml"):
w.tick(res[s.a], res[s.b]) w.tick(res[s.a], res[s.b])
elif s.kind in _MULTI_KINDS: elif s.kind in _MULTI_KINDS:
w.tick(res) w.tick(res)
elif s.kind == "ml":
# SH01: storia full (parquet bootstrap + feed) -> il walk-forward
# interno fitta solo l'ultimo blocco (last_block_only nei params).
w.tick(_with_history(ml_hist.get(s.asset), res[s.asset],
ml_gap_warned, s.asset))
else: else:
# single (fade/dip) e ml (SH01): StrategyWorker. SH01 retraina dentro # single (fade/dip): StrategyWorker su feed live.
# generate_signals (walk-forward) -> nessun training esterno.
w.tick(res[s.asset]) w.tick(res[s.asset])
ledger.update_equity({sid: _worker_equity(wk) for sid, wk in workers.items()}) ledger.update_equity({sid: _worker_equity(wk) for sid, wk in workers.items()})
+60
View File
@@ -0,0 +1,60 @@
"""SH01 path live: last_block_only == tail del walk-forward completo (parity by
construction) + merge storia parquet/feed del runner."""
import numpy as np
import pandas as pd
import pytest
from src.portfolio.runner import _with_history
def _df(n=6500, seed=0, t0=1_600_000_000_000):
rng = np.random.default_rng(seed)
c = 100 * np.exp(np.cumsum(rng.normal(0, 0.01, n)))
h = c * (1 + np.abs(rng.normal(0, 0.004, n)))
l = c * (1 - np.abs(rng.normal(0, 0.004, n)))
o = np.roll(c, 1); o[0] = c[0]
ts = t0 + 3_600_000 * np.arange(n)
return pd.DataFrame({"timestamp": ts, "open": o, "high": h, "low": l,
"close": c, "volume": 1.0})
def test_last_block_only_matches_full_wf_tail():
from scripts.analysis.shape_ml_research import ml_wf_entries
df = _df()
cfg = dict(W=24, H=12, model="logit", thresh=0.55, train_min=4000, retrain=500)
full = ml_wf_entries(df, **cfg)
last = ml_wf_entries(df, last_block_only=True, **cfg)
n = len(df)
# confine dell'ultimo blocco: start + k*retrain (deterministico)
start = max(cfg["train_min"], cfg["W"] - 1)
B = start
while B + cfg["retrain"] < n - 1:
B += cfg["retrain"]
full_tail = [e for e in full if e["i"] >= B]
assert last == full_tail # identita' esatta, non solo simile
assert all(e["i"] >= B for e in last)
def test_with_history_merges_contiguous():
hist = _df(n=100)
live = _df(n=50, t0=int(hist["timestamp"].iloc[60])) # overlap: live parte dentro hist
out = _with_history(hist, live)
assert len(out) == 60 + 50 # hist pre-overlap + live completo
ts = out["timestamp"].values
assert (np.diff(ts) > 0).all() # serie strettamente crescente
def test_with_history_gap_falls_back_to_live():
hist = _df(n=100)
gap_start = int(hist["timestamp"].iloc[-1]) + 10 * 3_600_000 # buco di 10 barre
live = _df(n=50, t0=gap_start)
warned = set()
out = _with_history(hist, live, warned, "BTC")
assert len(out) == 50 and "BTC" in warned # solo feed + warn deduplicato
out2 = _with_history(hist, live, warned, "BTC")
assert len(out2) == 50 # secondo giro: nessun nuovo warn
def test_with_history_none_hist_passthrough():
live = _df(n=50)
assert _with_history(None, live) is live