feat(fade): EXIT-16 close-confirm SL — stop solo se il CLOSE sfonda sl∓0.5·ATR14 (immune ai wick)
Param sl_confirm_atr (None = comportamento storico) in FadeStrategy.backtest, MR01.backtest e StrategyWorker.tick; attivo live a 0.5 sulle 6 fade in _defs.py. TP intrabar al livello e max_bars invariati; in modalita' confirm il TP ha priorita' nel bar. Ricerca exit-lab (34 agenti, 3 lenti avversariali + tail audit): gli stop intrabar da wick sono falsi negativi per le fade. PORT06 canonico: FULL Sharpe 6.47->7.84 DD 4.10->2.60, OOS 8.82->10.06 DD 1.30->1.15. NB path grezzo non filtrato: ret esplode ma DD per-sleeve sale — la riduzione DD vive nel config live (trend_max+hurst) + diversificazione, come misurato. 5 test nuovi (59 verdi). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
"""EXIT-16 close-confirm SL (2026-06-04): col param `sl_confirm_atr` il wick che
|
||||
buca lo SL NON stoppa piu' (immune ai wick); lo stop scatta solo se il CLOSE
|
||||
sfonda sl ∓ buf*ATR14, con uscita al close. TP intrabar invariato.
|
||||
Senza param il comportamento resta quello storico (regressione)."""
|
||||
import pandas as pd
|
||||
from src.live.strategy_worker import StrategyWorker
|
||||
from src.live.strategy_loader import load_strategy
|
||||
|
||||
|
||||
def _df(last_high, last_low, last_close, n=120, price=100.0):
|
||||
c = [price] * n
|
||||
h = [price] * n
|
||||
l = [price] * n
|
||||
h[-1] = last_high
|
||||
l[-1] = last_low
|
||||
c[-1] = last_close
|
||||
ts = (pd.date_range("2024-01-01", periods=n, freq="1h", tz="UTC").astype("int64") // 10**6)
|
||||
return pd.DataFrame({"timestamp": ts, "open": [price] * n, "high": h, "low": l,
|
||||
"close": c, "volume": 1.0})
|
||||
|
||||
|
||||
def _long_worker(tmp, confirm=0.5):
|
||||
params = {"sl_confirm_atr": confirm} if confirm else {}
|
||||
w = StrategyWorker(strategy=load_strategy("MR01_bollinger_fade"), asset="BTC", tf="1h",
|
||||
capital=1000.0, params=params, data_dir=tmp)
|
||||
w._notify = lambda *a, **k: None
|
||||
w.in_position = True
|
||||
w.direction = 1
|
||||
w.entry_price = 100.0
|
||||
w.tp = 102.0
|
||||
w.sl = 98.0
|
||||
w.max_bars = 24
|
||||
w.bars_held = 1
|
||||
w.last_bar_ts = 0
|
||||
return w
|
||||
|
||||
|
||||
def test_wick_below_sl_does_not_stop(tmp_path):
|
||||
# low 97 buca lo SL(98) ma il CLOSE rientra a 100 -> resta in posizione
|
||||
w = _long_worker(tmp_path)
|
||||
w.tick(_df(last_high=100.5, last_low=97.0, last_close=100.0))
|
||||
assert w.in_position
|
||||
|
||||
|
||||
def test_same_wick_stops_without_param(tmp_path):
|
||||
# regressione: senza sl_confirm_atr lo stesso wick stoppa intrabar al livello
|
||||
w = _long_worker(tmp_path, confirm=None)
|
||||
w.tick(_df(last_high=100.5, last_low=97.0, last_close=100.0))
|
||||
assert not w.in_position
|
||||
|
||||
|
||||
def test_close_breach_stops_at_close(tmp_path):
|
||||
# close 96.5 sfonda sl-buf (ATR del df flat ~ TR ultimo bar /14 -> buf piccolo) -> stop
|
||||
w = _long_worker(tmp_path)
|
||||
cap_before = w.capital
|
||||
w.tick(_df(last_high=100.5, last_low=96.0, last_close=96.5))
|
||||
assert not w.in_position
|
||||
assert w.capital < cap_before # uscito al CLOSE (in perdita), non al livello
|
||||
|
||||
|
||||
def test_tp_intrabar_still_first(tmp_path):
|
||||
# high tocca il TP(102) intrabar -> esce al TP anche in modalita' close-confirm
|
||||
w = _long_worker(tmp_path)
|
||||
w.tick(_df(last_high=102.5, last_low=99.5, last_close=100.0))
|
||||
assert not w.in_position
|
||||
assert w.capital > 1000.0 # uscito al TP in profitto
|
||||
|
||||
|
||||
def test_small_breach_within_buffer_holds(tmp_path):
|
||||
# close appena sotto SL ma DENTRO il buffer ATR -> resta in posizione
|
||||
w = _long_worker(tmp_path)
|
||||
df = _df(last_high=100.0, last_low=94.0, last_close=97.95) # TR=6 -> ATR~0.43 -> buf~0.21
|
||||
w.tick(df)
|
||||
assert w.in_position
|
||||
Reference in New Issue
Block a user