Files
PythagorasGoal/docs/diary/2026-06-01-sh01-wiring-squeeze-bug.md
T
Adriano Dal Pastro 83c4e7a334 fix(live): SH01 esegue shape-ML reale, non il wrapper squeeze scartato
Bug di wiring: runner.py avvolgeva lo sleeve SH01 nel MLWorkerWrapper legacy
di multi_runner, che usa SignalEngine (famiglia squeeze ML01 SCARTATA), apre
con Signal nudo ed esce a hold_bars=3 con tick() propria. Risultato: lo sleeve
"SH01" del portafoglio live NON eseguiva SH01_shape_ml (generate_signals mai
chiamata) e il fix horizon-exit era in un ramo morto -> SH01 continuava a
chiudere a hold_limit/3.

Fix: SH01 (kind="ml") gira come StrategyWorker normale. SH01_shape_ml.
generate_signals fa il walk-forward internamente ad ogni tick ed emette
metadata.max_bars=H=12 -> exit via StrategyWorker.tick (orizzonte H, fix
applicato). Rimosso l'import/uso di MLWorkerWrapper e il blocco train esterno.

ml_wf_entries ha train_min=4000 (>=4000 barre 1h per produrre segnali):
aggiunto _ML_LOOKBACK_DAYS=365 cosi gli asset di sleeve ml fetchano >=365g
(~8760 barre), senza dipendere dal fetch 440g di TSM01/ROT02. generate_signals
su 365g: 0,17-0,24s (logit) -> trascurabile sul poll 60s.

Test: test_build_ml_sh01_is_plain_strategyworker (StrategyWorker + strategy
SH01_shape_ml + niente engine squeeze). Suite: 51 passed.

Stato live: SH01 BTC/ETH flat -> contatori resettati (capitale preservato),
trade squeeze archiviati. Rebuild+recreate: 14 worker RESUME puliti, healthy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 12:24:13 +00:00

3.6 KiB
Raw Blame History

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:

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.

# 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,170,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: 7661786 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.