Files
PythagorasGoal/docs/diary/2026-06-01-sh01-horizon-exit-bug.md
T
Adriano Dal Pastro b5d277478c docs(diary): bugfix exit a orizzonte SH01 (3 barre -> H=12)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 09:06:49 +00:00

3.4 KiB
Raw Blame History

2026-06-01 — Bugfix: SH01 usciva a 3 barre invece di H=12 (exit a orizzonte)

Diagnosi partita da un check sulla debolezza apparente di SH01_BTC nel paper trading live PORT06 (accuratezza 33,3% su 3 trade). Non era sfortuna statistica: era un bug di exit nello StrategyWorker.

Sintomo

Nel live PORT06 (Docker pythagoras-portfolio), SH01_BTC mostrava 3 trade tutti long, tutti chiusi con reason: "hold_limit" a bars_held: 3, con tp: null sl: null:

# entry exit bars net % esito
1 73529.5 73433.0 3 0,46%
2 73759.5 73839.5 3 +0,02%
3 73811.5 73766.0 3 0,32%

oos_signal_precision nei log di TRAIN scendeva 55,6% → 50,0% → 43,3%.

Causa

SH01 (scripts/strategies/SH01_shape_ml.py, config W24 H12 th0.58) è una strategia horizon-only: predice il segno del rendimento a H=12 barre ed esce a H barre. I suoi Signal portano metadata={"max_bars": H} (=12) e nessun TP/SL.

Nello StrategyWorker.tick() la logica di uscita era:

if self.tp and self.sl:          # SH01: False (tp=sl=0)
    ... usa self.max_bars ...     #   -> max_bars=12 consultato SOLO qui
elif self.bars_held >= self.hold_bars:   # fallback legacy hold_bars=3
    self._close_position(..., "hold_limit")   # SH01 finiva QUI

self.max_bars (=12, settato correttamente in _open_position) era onorato solo dentro il ramo tp and sl. Senza TP/SL, SH01 cadeva sul fallback hold_bars=3 e chiudeva a 3 barre. L'edge di SH01 — per CLAUDE.md è nell'asimmetria sull'orizzonte H, non nella frequenza (win-rate ~50%) — non aveva tempo di realizzarsi: tagliato a 3/12, degenera in rumore.

Solo SH01 (BTC+ETH) era colpito: tutte le fade (MR01/MR02/MR07, DIP01) portano tp+sl+max_bars e usano il ramo intrabar corretto.

Fix

src/live/strategy_worker.py: aggiunto un ramo per l'exit a orizzonte puro, prima del fallback hold_bars:

elif self.max_bars:
    # Exit puro a orizzonte (strategie senza TP/SL, es. SH01 shape-ML H=12):
    # onora max_bars dalla metadata del Signal, non il fallback hold_bars=3.
    if self.bars_held >= self.max_bars:
        self._close_position(current_price, "time_limit")

Le fade restano invariate (entrano nel ramo tp and sl).

Verifica

  • Nuovo test tests/portfolio/test_horizon_exit.py (2 casi): con max_bars=12 resta in posizione a 3 barre; esce a 12 con reason: "time_limit" e bars_held: 12.
  • Suite completa: 43 passed.
  • Container riavviato: tutti i 17 sleeve RESUME puliti, inclusa una posizione SH01_ETH short aperta che ora seguirà l'exit a 12 barre.

Atteso d'ora in poi

I trade SH01 nei log mostreranno reason: "time_limit" con bars_held: 12 invece di hold_limit / 3. Il 33% di accuratezza era un artefatto dell'exit prematuro; ora la strategia gira sull'orizzonte su cui è validata (BTC OOS Sharpe 2,72, expanding). Resta comunque un diversificatore del MASTER, non un motore di ritorno standalone.

Lezione

Il backtest di SH01 (fade_base/engine onesto) esce a H barre via max_bars; il worker live deve replicarlo. Quando una strategia non porta TP/SL ma solo un orizzonte, il fallback hold_bars del worker la falsa silenziosamente. Verificare sempre che la convenzione di exit del worker live coincida con quella del backtest validato — non solo l'ingresso.