# 2026-06-01 — SH01 live eseguiva la strategia SBAGLIATA (squeeze scartato), non shape-ML > Scoperto verificando perché SH01 continuava a chiudere a `hold_limit/3` **anche dopo** > il rebuild col fix horizon-exit. Il fix era corretto ma in un **ramo morto**: SH01 live > non passava da `StrategyWorker.tick()`. ## Sintomo Dopo il deploy del fix SH01 (exit a H=12), un close SH01_BTC delle 12:00 era ancora `reason=hold_limit bars=3` (perdita −1,27%). Il fix non aveva effetto sul path reale. ## Causa (bug di wiring, più grave del previsto) `src/portfolio/runner.py` importava `MLWorkerWrapper` da **`src/live/multi_runner.py`** e ci avvolgeva lo sleeve SH01: ```python if spec.kind == "ml": return MLWorkerWrapper(worker, {"retrain_hours": 24}) ``` Ma quel wrapper è **legacy, per la famiglia squeeze ML01** (scartata, vedi CLAUDE.md): - usa `SignalEngine` = squeeze-detection + GradientBoosting (NON SH01_shape_ml); - ha una `tick()` propria che apre con un `Signal` **nudo** (niente tp/sl/max_bars) ed esce con `if bars_held >= hold_bars: close("hold_limit")` → ignora del tutto la strategia caricata e il fix horizon. Quindi lo sleeve "SH01" del portafoglio live **non eseguiva shape-ML**: eseguiva il motore squeeze scartato. I log `TRAIN OK / oos_signal_precision` venivano da lì. Il `worker` con strategy=SH01_shape_ml era costruito ma la sua `generate_signals` non veniva **mai** chiamata. ## Fix SH01 (`kind="ml"`) ora gira come **StrategyWorker normale**: `SH01_shape_ml.generate_signals` fa il walk-forward (retraining) **internamente** ad ogni tick (`ml_wf_entries`) ed emette `metadata.max_bars=H=12` → gli exit passano per `StrategyWorker.tick()` e il fix horizon si applica davvero. ```python # runner.py: niente più MLWorkerWrapper per kind="ml" return StrategyWorker(strategy=strategy, asset=spec.asset, tf=spec.tf, ...) ``` **Lookback dati.** `ml_wf_entries` ha `train_min=4000` → servono ≥4000 barre 1h prima di produrre segnali (con 90g/2160 barre → 0 segnali, runtime 0.01s — il falso "muto"). Le candele 1h di BTC/ETH già arrivano a 440g (le richiede TSM01/ROT02 a 1d), ma per non dipendere da quella coincidenza ho aggiunto `_ML_LOOKBACK_DAYS=365`: gli asset usati da sleeve ml fetchano ≥365g (~8760 barre). Costo `generate_signals` su 365g: **0,17–0,24s** (modello logit) → trascurabile sul poll 60s. **Verifica.** Build SH01 → `StrategyWorker` con `strategy.name=="SH01_shape_ml"`, niente attributo `engine` (regression test `test_build_ml_sh01_is_plain_strategyworker`). Smoke su 365g: 766–1786 segnali, tutti `max_bars=12`; tick live 0,17s. `ml_wf_entries` non predice mai l'ultima barra (`n-1`) ma fino a `n-2` = esattamente la condizione di apertura del worker (`idx >= last_idx-1`) → apre quando il segnale è fresco. Suite: 51 passed. **Stato live.** SH01 BTC/ETH erano flat: contatori resettati a 0 (capitale preservato 58,76/58,78), vecchi trade squeeze archiviati in `trades_squeeze_archive.jsonl`. Rebuild + recreate: 14 worker RESUME puliti, container healthy, nessun log TRAIN/squeeze, zero errori. ## Lezione 1. **Verificare il path REALE, non solo il codice del fix.** Il fix horizon era giusto ma SH01 non lo attraversava. Un fix non testato end-to-end sul percorso vivo è un fix presunto. (Mi ero fidato del rebuild senza confermare il reason dei close SH01.) 2. Riusare un wrapper legacy "perché c'è" è un rischio: `MLWorkerWrapper` di multi_runner era per la famiglia squeeze scartata, non per shape-ML. 3. Un modello ML "muto" può essere solo **fame di dati** (train_min), non un bug logico: controllare sempre la dimensione della finestra prima di concludere.