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

72 lines
3.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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,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.