14522262e6
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
199 lines
8.1 KiB
Python
199 lines
8.1 KiB
Python
"""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()
|