fix(live): exit a orizzonte per strategie senza TP/SL (SH01)
Lo StrategyWorker onorava max_bars (orizzonte del Signal) solo nel ramo `if self.tp and self.sl`. SH01 (shape-ML, H=12) non porta TP/SL, quindi cadeva sul fallback legacy hold_bars=3 e chiudeva a 3 barre invece delle 12 validate: l'edge (asimmetria sull'orizzonte, non frequenza) non aveva tempo di realizzarsi -> accuratezza live falsata (33%), tutti exit "hold_limit" a bars_held=3. Aggiunto un ramo `elif self.max_bars` che esce a "time_limit" quando bars_held>=max_bars, prima del fallback hold_bars. Tocca solo le strategie horizon-only (SH01); le fade con tp+sl+max_bars sono invariate. Test: tests/portfolio/test_horizon_exit.py (resta in posizione a 3 barre con max_bars=12; esce a 12 con reason time_limit). Suite: 43 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -237,6 +237,11 @@ class StrategyWorker:
|
|||||||
self._close_position(self.tp, "take_profit")
|
self._close_position(self.tp, "take_profit")
|
||||||
elif self.max_bars and self.bars_held >= self.max_bars:
|
elif self.max_bars and self.bars_held >= self.max_bars:
|
||||||
self._close_position(current_price, "time_limit")
|
self._close_position(current_price, "time_limit")
|
||||||
|
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")
|
||||||
elif self.bars_held >= self.hold_bars:
|
elif self.bars_held >= self.hold_bars:
|
||||||
self._close_position(current_price, "hold_limit")
|
self._close_position(current_price, "hold_limit")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""Fix exit a orizzonte puro: una strategia che porta solo `max_bars` nella metadata del
|
||||||
|
Signal (niente TP/SL, es. SH01 shape-ML con H=12) deve uscire a `max_bars` barre — NON sul
|
||||||
|
fallback legacy `hold_bars=3`. Prima del fix lo StrategyWorker chiudeva tali sleeve a 3 barre
|
||||||
|
(reason "hold_limit"), tagliando l'orizzonte su cui è validato l'edge di forma."""
|
||||||
|
import pandas as pd
|
||||||
|
from src.live.strategy_worker import StrategyWorker
|
||||||
|
from src.live.strategy_loader import load_strategy
|
||||||
|
|
||||||
|
|
||||||
|
def _df(n, price=100.0, last_ts=None):
|
||||||
|
c = [price] * n
|
||||||
|
ts = (pd.date_range("2024-01-01", periods=n, freq="1h", tz="UTC").astype("int64") // 10**6)
|
||||||
|
df = pd.DataFrame({"timestamp": ts, "open": c, "high": c, "low": c, "close": c, "volume": 1.0})
|
||||||
|
if last_ts is not None:
|
||||||
|
df.loc[df.index[-1], "timestamp"] = last_ts
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
def _horizon_worker(tmp, max_bars=12):
|
||||||
|
"""Worker con posizione aperta, solo max_bars (tp/sl=0) — come SH01."""
|
||||||
|
w = StrategyWorker(strategy=load_strategy("MR01_bollinger_fade"), asset="BTC", tf="1h",
|
||||||
|
capital=1000.0, data_dir=tmp)
|
||||||
|
w._notify = lambda *a, **k: None
|
||||||
|
w.in_position = True
|
||||||
|
w.direction = 1
|
||||||
|
w.entry_price = 100.0
|
||||||
|
w.tp = 0.0
|
||||||
|
w.sl = 0.0
|
||||||
|
w.max_bars = max_bars
|
||||||
|
w.bars_held = 0
|
||||||
|
w.last_bar_ts = 0
|
||||||
|
return w
|
||||||
|
|
||||||
|
|
||||||
|
def _tick_bars(w, k, base_n=120):
|
||||||
|
"""Avanza k barre nuove (ogni tick incrementa bars_held di 1 col nuovo timestamp)."""
|
||||||
|
for b in range(1, k + 1):
|
||||||
|
w.tick(_df(base_n, last_ts=b))
|
||||||
|
|
||||||
|
|
||||||
|
def test_holds_until_horizon_not_hold_bars(tmp_path):
|
||||||
|
"""max_bars=12: a 3 barre (vecchio hold_bars) NON deve chiudere; deve restare in posizione."""
|
||||||
|
w = _horizon_worker(tmp_path, max_bars=12)
|
||||||
|
_tick_bars(w, 3)
|
||||||
|
assert w.in_position
|
||||||
|
assert w.bars_held == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_exits_at_max_bars_with_time_limit(tmp_path):
|
||||||
|
"""A max_bars barre chiude con reason 'time_limit' (non 'hold_limit')."""
|
||||||
|
w = _horizon_worker(tmp_path, max_bars=12)
|
||||||
|
_tick_bars(w, 12)
|
||||||
|
assert not w.in_position
|
||||||
|
# leggi l'ultimo CLOSE dal log (formato piatto) e verificane reason/bars_held
|
||||||
|
import json
|
||||||
|
rows = [json.loads(l) for l in w.trades_path.read_text().strip().splitlines()]
|
||||||
|
close = [r for r in rows if r.get("event") == "CLOSE"]
|
||||||
|
assert close and close[-1]["reason"] == "time_limit"
|
||||||
|
assert close[-1]["bars_held"] == 12
|
||||||
Reference in New Issue
Block a user