research(exit-lab): 34 agenti su exit dinamiche → EXIT-16 close-confirm SL PROMOSSO a livello PORT06
23 famiglie esplorate (harness condiviso exit_lab, train/OOS embargo nov-2023, tutto lo storico 1h 2018-2026) + 10 verifiche avversariali + test PORT06. 'Cavalcare il prezzo' non esiste (4a conferma: oltre il TP=media non c'e' coda). Scoperta: lo SL intrabar fisso e' il distruttore di valore n.1 delle fade (stop da wick = falsi negativi). Forma robusta: SL solo su CHIUSURA oltre sl0±0.5·ATR14 — PORT06 FULL Sharpe 6.47→7.84 DD 4.10→2.60, OOS 8.82→10.06. Collaterali: bias gap-through dell'engine sugli stop stretti; ramo -2% del worker morto con sl=0. Diario: docs/diary/2026-06-04-exit-lab.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
"""VERIFY EXIT-16 close_confirm_sl — lente OVERFIT/ROBUSTEZZA (avversariale).
|
||||
|
||||
Ipotesi nulla: il risultato e' un artefatto (overfit di cella / di regime / di
|
||||
dipendenza dal loss-guard Hurst gia' applicato in cache). Tre test:
|
||||
|
||||
(1) JITTER PARAMETRI: buffer fuori griglia {0.4, 0.6, 0.75, 1.0} + ponte verso la
|
||||
base con SL fisso a 3x/4x ATR (no_sl come limite). Il plateau tiene?
|
||||
(2) STABILITA' TEMPORALE: train 2018-20 vs 21-22; OOS 2023-11/2025-01 vs
|
||||
2025-01/2026-05. Il miglioramento e' in OGNI finestra o concentrato?
|
||||
(3) DIPENDENZA HURST (decisivo): rigenero i segnali con hurst_max=None (NESSUN
|
||||
loss-guard, NON tocco la cache) e ripeto base-vs-policy. Se senza il guard la
|
||||
policy crolla, funziona SOLO grazie al guard -> condizione di validita'.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
|
||||
|
||||
import exit_lab # noqa: E402
|
||||
from exit_lab import ExitPolicy, simulate, OOS_START_MS, _atr14 # noqa: E402
|
||||
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"cc16", str(Path(__file__).resolve().parent / "16_close_confirm_sl.py"))
|
||||
cc16 = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(cc16)
|
||||
CloseConfirmSl = cc16.CloseConfirmSl
|
||||
|
||||
from src.data.downloader import load_data # noqa: E402
|
||||
from src.live.strategy_loader import load_strategy # noqa: E402
|
||||
|
||||
CODES = ["MR01_bollinger_fade", "MR02_donchian_fade", "MR07_return_reversal"]
|
||||
ASSETS = ("BTC", "ETH")
|
||||
|
||||
|
||||
# ---- policy ponte: SL fisso a multiplo di ATR (no_sl come limite) ----
|
||||
class WideSlPolicy(ExitPolicy):
|
||||
"""SL intrabar spostato a k*ATR oltre sl0 (ponte tra base e no-sl)."""
|
||||
name = "wide_sl"
|
||||
|
||||
def __init__(self, ctx, i, d, entry, tp0, sl0, mb, **params):
|
||||
super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params)
|
||||
self.k = float(params.get("k_atr", 2.0))
|
||||
self.atr = ctx["atr14"]
|
||||
|
||||
def levels(self, j: int):
|
||||
a = self.atr[j - 1] if np.isfinite(self.atr[j - 1]) else 0.0
|
||||
# sl0 e' sotto (long) / sopra (short) l'entry; allarga di k*atr
|
||||
sl = self.sl0 - self.k * a if self.d == 1 else self.sl0 + self.k * a
|
||||
return self.tp0, sl, 1.0
|
||||
|
||||
|
||||
def sub(cls, sleeve, g, s, e):
|
||||
return simulate(cls, sleeve, g, start_ms=s, end_ms=e)
|
||||
|
||||
|
||||
def fmt(r):
|
||||
if not r:
|
||||
return " n/a"
|
||||
return (f"ret{r['ret_pct']:>7.0f}% dd{r['dd_pct']:>5.1f} sh{r['sharpe_t']:>5.2f} "
|
||||
f"n{r['trades']:>4} bars{r['avg_bars']:>5.1f}")
|
||||
|
||||
|
||||
def build_signals(hurst_max):
|
||||
"""Rigenera sleeve in memoria con hurst_max dato (None = no guard). NON tocca cache."""
|
||||
out = {}
|
||||
params = dict(trend_max=3.0, ema_long=200, hurst_max=hurst_max, min_tp_frac=0.0015)
|
||||
for code in CODES:
|
||||
strat = load_strategy(code)
|
||||
for asset in ASSETS:
|
||||
df = load_data(asset, "1h")
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
sigs = strat.generate_signals(df, ts, **params)
|
||||
h = df["high"].values.astype(float)
|
||||
l = df["low"].values.astype(float)
|
||||
c = df["close"].values.astype(float)
|
||||
out[(code, asset)] = {
|
||||
"signals": [(int(s.idx), int(s.direction), float(s.metadata["tp"]),
|
||||
float(s.metadata["sl"]), int(s.metadata["max_bars"]))
|
||||
for s in sigs],
|
||||
"open": df["open"].values.astype(float),
|
||||
"high": h, "low": l, "close": c,
|
||||
"ts_ms": df["timestamp"].values.astype(np.int64),
|
||||
"atr14": _atr14(h, l, c),
|
||||
}
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
data = exit_lab.load_sleeves()
|
||||
keys = list(data.keys())
|
||||
|
||||
# ===== TEST 1: JITTER PARAMETRI =====
|
||||
print("=" * 100)
|
||||
print("TEST 1 — JITTER buffer fuori griglia + ponte WIDE-SL (OOS, dopo 2023-11)")
|
||||
print("=" * 100)
|
||||
jit_buffers = [0.4, 0.6, 0.75, 1.0]
|
||||
all_pos = True
|
||||
for (code, asset), sleeve in data.items():
|
||||
key = f"{code.split('_')[0]} {asset}"
|
||||
base = sub(ExitPolicy, sleeve, {}, OOS_START_MS, None)
|
||||
line = f"{key:<10} BASE {fmt(base)}"
|
||||
print(line)
|
||||
for b in jit_buffers:
|
||||
r = sub(CloseConfirmSl, sleeve, {"buffer": b}, OOS_START_MS, None)
|
||||
better = r and base and r["sharpe_t"] >= base["sharpe_t"] - 0.10
|
||||
all_pos &= bool(better)
|
||||
print(f" buf={b:<4} {fmt(r)} {'OK' if better else 'WORSE'}")
|
||||
print()
|
||||
print(f"JITTER buffer: tutte >= base-0.10 sharpe? {all_pos}\n")
|
||||
|
||||
print("-" * 100)
|
||||
print("PONTE WIDE-SL: SL intrabar fisso allargato a k*ATR (k grande -> verso no-sl)")
|
||||
print("-" * 100)
|
||||
for (code, asset), sleeve in data.items():
|
||||
key = f"{code.split('_')[0]} {asset}"
|
||||
base = sub(ExitPolicy, sleeve, {}, OOS_START_MS, None)
|
||||
print(f"{key:<10} BASE(k=0) {fmt(base)}")
|
||||
for k in [1.5, 3.0, 4.0]:
|
||||
r = sub(WideSlPolicy, sleeve, {"k_atr": k}, OOS_START_MS, None)
|
||||
print(f" k={k:<4} {fmt(r)}")
|
||||
print()
|
||||
|
||||
# ===== TEST 2: STABILITA' TEMPORALE =====
|
||||
print("=" * 100)
|
||||
print("TEST 2 — STABILITA' TEMPORALE (base vs policy buffer=0.5)")
|
||||
print("=" * 100)
|
||||
ms = lambda d: int(pd.Timestamp(d, tz="UTC").value // 1e6)
|
||||
windows = [
|
||||
("TRAIN 2018-20", None, ms("2021-01-01")),
|
||||
("TRAIN 2021-22", ms("2021-01-01"), OOS_START_MS),
|
||||
("OOS 23/11-25/01", OOS_START_MS, ms("2025-01-01")),
|
||||
("OOS 25/01-26/05", ms("2025-01-01"), None),
|
||||
]
|
||||
win_verdict = {w[0]: 0 for w in windows}
|
||||
win_total = {w[0]: 0 for w in windows}
|
||||
for (code, asset), sleeve in data.items():
|
||||
key = f"{code.split('_')[0]} {asset}"
|
||||
print(f"\n{key}")
|
||||
for wname, s, e in windows:
|
||||
b = sub(ExitPolicy, sleeve, {}, s, e)
|
||||
p = sub(CloseConfirmSl, sleeve, {"buffer": 0.5}, s, e)
|
||||
if b and p:
|
||||
win_total[wname] += 1
|
||||
# criterio: policy non peggio della base su sharpe (tol 0.15)
|
||||
imp = p["sharpe_t"] >= b["sharpe_t"] - 0.15
|
||||
win_verdict[wname] += int(imp)
|
||||
tag = "OK " if imp else "BAD"
|
||||
else:
|
||||
tag = "n/a"
|
||||
print(f" {wname:<18} base {fmt(b)}")
|
||||
print(f" {'':<18} pol {fmt(p)} -> {tag}")
|
||||
print("\nPer-finestra (policy >= base-0.15 sharpe):")
|
||||
for w in windows:
|
||||
wn = w[0]
|
||||
print(f" {wn:<18} {win_verdict[wn]}/{win_total[wn]} sleeve OK")
|
||||
|
||||
# ===== TEST 3: DIPENDENZA HURST (DECISIVO) =====
|
||||
print("\n" + "=" * 100)
|
||||
print("TEST 3 — DIPENDENZA dal loss-guard HURST (DECISIVO)")
|
||||
print("Rigenero i segnali con hurst_max=None (NO guard, regime trending incluso).")
|
||||
print("Se la policy crolla -> funziona SOLO grazie al guard.")
|
||||
print("=" * 100)
|
||||
print("Generazione segnali SENZA hurst (puo' richiedere ~1-2 min)...")
|
||||
data_nohurst = build_signals(hurst_max=None)
|
||||
|
||||
n_guard = sum(len(s["signals"]) for s in data.values())
|
||||
n_nohurst = sum(len(s["signals"]) for s in data_nohurst.values())
|
||||
print(f"Segnali totali: con guard {n_guard}, senza guard {n_nohurst} "
|
||||
f"(+{n_nohurst - n_guard} segnali in regime trending)\n")
|
||||
|
||||
holds = True
|
||||
for region_name, s, e in [("TRAIN", None, OOS_START_MS), ("OOS", OOS_START_MS, None)]:
|
||||
print(f"--- {region_name} (segnali SENZA hurst guard) ---")
|
||||
for (code, asset), sleeve in data_nohurst.items():
|
||||
key = f"{code.split('_')[0]} {asset}"
|
||||
b = sub(ExitPolicy, sleeve, {}, s, e)
|
||||
p = sub(CloseConfirmSl, sleeve, {"buffer": 0.5}, s, e)
|
||||
if b and p:
|
||||
imp = p["sharpe_t"] >= b["sharpe_t"] - 0.15
|
||||
ddimp = p["dd_pct"] <= b["dd_pct"] + 1.0
|
||||
holds &= bool(imp)
|
||||
tag = "OK " if imp else "POLICY WORSE"
|
||||
else:
|
||||
tag = "n/a"
|
||||
print(f" {key:<10} base {fmt(b)}")
|
||||
print(f" {'':<10} pol {fmt(p)} -> {tag}")
|
||||
print()
|
||||
print(f"TEST 3 verdict: policy regge SENZA il guard (>= base-0.15 sharpe ovunque)? {holds}")
|
||||
print("Se False -> la tesi 'SL dannoso' dipende dal guard (condizione di validita').")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user