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>
262 lines
11 KiB
Python
262 lines
11 KiB
Python
"""VERIFY EXIT-16 close_confirm_sl — lente STRESS (avversariale).
|
|
|
|
Ipotesi nulla: l'edge della close-confirm-SL e' fragile a frizioni reali.
|
|
Quattro stress, tutti su segnali cache (params LIVE, hurst_max=0.55):
|
|
|
|
(1) FEE 2x: FEE_RT=0.002 (vs 0.001). Penalizza le policy che girano piu' capitale.
|
|
(2) BEAR/CRASH 2021-01..2022-12 (LUNA/FTX/19-mag-21): worst-trade + 5 peggiori
|
|
trade della policy vs base. Lo SL disattivato lascia correre le perdite?
|
|
(3) SLIPPAGE AVVERSO 20bps sulle uscite della policy: ogni fill di USCITA paga
|
|
+20bps contro la posizione (prezzo di uscita peggiorato). L'edge regge?
|
|
NB: lo applico SOLO alle uscite della POLICY (la sua tesi e' "esco al close":
|
|
il close-fill e' market, paga slippage; la base esce a livelli limite sl0/tp0).
|
|
(4) OVERLAP/TURNOVER: la policy allunga la permanenza (no stop intrabar). Conto
|
|
i segnali SALTATI per non-overlap (i <= last_exit) base vs policy, e quanto
|
|
capitale-tempo (somma bars in posizione) gira in piu'.
|
|
|
|
Tutto via simulate() con monkeypatch di FEE_RT e una sottoclasse engine per lo
|
|
slippage. Niente modifiche ad altri file.
|
|
"""
|
|
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, HARD_CAP, LEV, POS # 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
|
|
|
|
BUF = 0.5 # train-pick
|
|
|
|
|
|
def fmt(r):
|
|
if not r:
|
|
return " n/a"
|
|
return (f"ret{r['ret_pct']:>8.0f}% dd{r['dd_pct']:>5.1f} sh{r['sharpe_t']:>5.2f} "
|
|
f"n{r['trades']:>4} win{r['win_pct']:>4.0f} bars{r['avg_bars']:>5.1f}")
|
|
|
|
|
|
def sub(cls, sleeve, g, s, e):
|
|
return simulate(cls, sleeve, g, start_ms=s, end_ms=e)
|
|
|
|
|
|
def ms(d):
|
|
return int(pd.Timestamp(d, tz="UTC").value // 1e6)
|
|
|
|
|
|
# ===========================================================================
|
|
# Engine "instrumented" che riproduce simulate() ma:
|
|
# - applica uno slippage avverso (bps) su OGNI fill di USCITA (solo se policy)
|
|
# - raccoglie la lista dei ret per-trade e i segnali SALTATI per non-overlap
|
|
# - raccoglie capital-time (somma bars)
|
|
# Lo tengo allineato 1:1 con exit_lab.simulate (stesso ordine SL-prima-di-TP).
|
|
# ===========================================================================
|
|
def simulate_instr(policy_cls, sleeve, params=None, start_ms=None, end_ms=None,
|
|
exit_slip_bps=0.0):
|
|
params = params or {}
|
|
h, l, c, ts = sleeve["high"], sleeve["low"], sleeve["close"], sleeve["ts_ms"]
|
|
n = len(c)
|
|
ctx = dict(sleeve)
|
|
policy_cls.prepare(ctx, **params)
|
|
fee = exit_lab.FEE_RT * LEV
|
|
slip = exit_slip_bps * 1e-4
|
|
capital = peak = 1000.0
|
|
max_dd = 0.0
|
|
last_exit = -1
|
|
trades = wins = 0
|
|
bars_tot = 0
|
|
skipped_overlap = 0
|
|
rets = [] # (ret, ts_entry, bars)
|
|
for (i, d, tp0, sl0, mb) in sleeve["signals"]:
|
|
if start_ms is not None and ts[i] < start_ms:
|
|
continue
|
|
if end_ms is not None and ts[i] >= end_ms:
|
|
continue
|
|
if i + 1 >= n:
|
|
continue
|
|
if i <= last_exit:
|
|
skipped_overlap += 1
|
|
continue
|
|
entry = c[i]
|
|
pol = policy_cls(ctx, i, d, entry, tp0, sl0, mb, **params)
|
|
horizon = min(int(pol.horizon), HARD_CAP)
|
|
fills = []
|
|
remaining = 1.0
|
|
j = i
|
|
for step in range(1, horizon + 1):
|
|
j = i + step
|
|
if j >= n:
|
|
j = n - 1
|
|
fills.append((remaining, c[j])); remaining = 0.0
|
|
break
|
|
tp, sl, tpfrac = pol.levels(j)
|
|
hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
|
|
hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
|
|
if hit_sl:
|
|
fills.append((remaining, sl)); remaining = 0.0
|
|
break
|
|
if hit_tp:
|
|
f = min(max(tpfrac, 0.0), 1.0) * remaining
|
|
if f > 0:
|
|
fills.append((f, tp)); remaining -= f
|
|
if remaining <= 1e-9:
|
|
break
|
|
pol.on_partial(j, tp, remaining)
|
|
if pol.after_bar(j):
|
|
fills.append((remaining, c[j])); remaining = 0.0
|
|
break
|
|
if step == horizon:
|
|
fills.append((remaining, c[j])); remaining = 0.0
|
|
if remaining > 1e-9:
|
|
fills.append((remaining, c[j]))
|
|
# slippage avverso sull'uscita: il prezzo di uscita peggiora di slip,
|
|
# cioe' si vende piu' basso (long) / si ricompra piu' alto (short).
|
|
def adj(p):
|
|
return p * (1.0 - slip) if d == 1 else p * (1.0 + slip)
|
|
ret = sum(f * (adj(p) - entry) for f, p in fills) / entry * d * LEV - fee
|
|
capital = max(capital + capital * POS * ret, 10.0)
|
|
peak = max(peak, capital)
|
|
max_dd = max(max_dd, (peak - capital) / peak)
|
|
last_exit = j
|
|
trades += 1
|
|
wins += ret > 0
|
|
bars_tot += j - i
|
|
rets.append((ret, int(ts[i]), j - i))
|
|
if trades == 0:
|
|
return {}
|
|
r = np.array([x[0] for x in rets])
|
|
return {
|
|
"ret_pct": (capital / 1000.0 - 1) * 100,
|
|
"dd_pct": max_dd * 100,
|
|
"trades": trades,
|
|
"win_pct": wins / trades * 100,
|
|
"sharpe_t": float(r.mean() / r.std() * np.sqrt(len(r))) if r.std() else 0.0,
|
|
"avg_bars": bars_tot / trades,
|
|
"bars_tot": bars_tot,
|
|
"skipped_overlap": skipped_overlap,
|
|
"rets": rets,
|
|
"worst5": sorted(r.tolist())[:5],
|
|
}
|
|
|
|
|
|
def main():
|
|
data = exit_lab.load_sleeves()
|
|
|
|
# ===================================================================
|
|
print("=" * 104)
|
|
print("TEST 1 — FEE 2x (FEE_RT 0.001 -> 0.002). base vs policy buffer=0.5 (OOS, dopo 2023-11)")
|
|
print("=" * 104)
|
|
orig_fee = exit_lab.FEE_RT
|
|
survive_fee = True
|
|
for fee in (0.001, 0.002):
|
|
exit_lab.FEE_RT = fee
|
|
print(f"\n--- FEE_RT={fee} ---")
|
|
for (code, asset), sleeve in data.items():
|
|
key = f"{code.split('_')[0]} {asset}"
|
|
b = sub(ExitPolicy, sleeve, {}, OOS_START_MS, None)
|
|
p = sub(CloseConfirmSl, sleeve, {"buffer": BUF}, OOS_START_MS, None)
|
|
tag = ""
|
|
if fee == 0.002 and b and p:
|
|
# regge se sharpe policy >= base (la tesi e' che migliora)
|
|
ok = p["sharpe_t"] >= b["sharpe_t"] - 0.10
|
|
survive_fee &= ok
|
|
tag = "OK" if ok else "WORSE"
|
|
print(f" {key:<10} base {fmt(b)}")
|
|
print(f" {'':<10} pol {fmt(p)} {tag}")
|
|
exit_lab.FEE_RT = orig_fee
|
|
print(f"\nFEE 2x: policy regge (>= base-0.10 sh su tutti gli sleeve OOS)? {survive_fee}")
|
|
|
|
# ===================================================================
|
|
print("\n" + "=" * 104)
|
|
print("TEST 2 — BEAR/CRASH 2021-01..2022-12 (LUNA/FTX/19-mag): worst-trade + 5 peggiori")
|
|
print("=" * 104)
|
|
s2, e2 = ms("2021-01-01"), ms("2023-01-01")
|
|
tail_worse = 0
|
|
tail_total = 0
|
|
for (code, asset), sleeve in data.items():
|
|
key = f"{code.split('_')[0]} {asset}"
|
|
b = simulate_instr(ExitPolicy, sleeve, {}, s2, e2)
|
|
p = simulate_instr(CloseConfirmSl, sleeve, {"buffer": BUF}, s2, e2)
|
|
print(f"\n{key}")
|
|
print(f" base {fmt(b)}")
|
|
print(f" pol {fmt(p)}")
|
|
if b and p:
|
|
bw = [f"{x*100:+.1f}%" for x in b["worst5"]]
|
|
pw = [f"{x*100:+.1f}%" for x in p["worst5"]]
|
|
print(f" base 5 peggiori (ret netto): {bw}")
|
|
print(f" pol 5 peggiori (ret netto): {pw}")
|
|
tail_total += 1
|
|
# la policy peggiora la coda se il worst-trade e' piu' negativo
|
|
if p["worst5"][0] < b["worst5"][0] - 0.005:
|
|
tail_worse += 1
|
|
print(f" -> CODA PEGGIORE: worst {p['worst5'][0]*100:+.1f}% < base {b['worst5'][0]*100:+.1f}%")
|
|
else:
|
|
print(f" -> coda OK: worst {p['worst5'][0]*100:+.1f}% vs base {b['worst5'][0]*100:+.1f}%")
|
|
print(f" DD bear: base {b['dd_pct']:.1f}% pol {p['dd_pct']:.1f}%")
|
|
print(f"\nBEAR: sleeve con coda PEGGIORE (worst-trade > 0.5pt sotto base): {tail_worse}/{tail_total}")
|
|
|
|
# ===================================================================
|
|
print("\n" + "=" * 104)
|
|
print("TEST 3 — SLIPPAGE AVVERSO 20bps sulle uscite della POLICY (OOS). base senza slippage")
|
|
print("(la tesi della policy e' 'esco al close' = market fill -> paga slippage)")
|
|
print("=" * 104)
|
|
survive_slip = True
|
|
for (code, asset), sleeve in data.items():
|
|
key = f"{code.split('_')[0]} {asset}"
|
|
b = simulate_instr(ExitPolicy, sleeve, {}, OOS_START_MS, None, exit_slip_bps=0.0)
|
|
p0 = simulate_instr(CloseConfirmSl, sleeve, {"buffer": BUF}, OOS_START_MS, None, exit_slip_bps=0.0)
|
|
p20 = simulate_instr(CloseConfirmSl, sleeve, {"buffer": BUF}, OOS_START_MS, None, exit_slip_bps=20.0)
|
|
ok = p20 and b and p20["sharpe_t"] >= b["sharpe_t"] - 0.10
|
|
survive_slip &= bool(ok)
|
|
print(f"\n{key}")
|
|
print(f" base (no slip) {fmt(b)}")
|
|
print(f" pol (no slip) {fmt(p0)}")
|
|
print(f" pol (+20bps exit) {fmt(p20)} {'OK' if ok else 'WORSE vs base'}")
|
|
print(f"\nSLIPPAGE 20bps: policy ancora >= base-0.10 sh su tutti? {survive_slip}")
|
|
print("(test severo: lo slippage colpisce la policy ma NON la base — asimmetria pessimistica)")
|
|
|
|
# severita' extra: slippage anche sulla base (entrambe market) per fairness
|
|
print("\n--- fairness: 20bps anche sulle uscite della BASE ---")
|
|
fair = True
|
|
for (code, asset), sleeve in data.items():
|
|
key = f"{code.split('_')[0]} {asset}"
|
|
b20 = simulate_instr(ExitPolicy, sleeve, {}, OOS_START_MS, None, exit_slip_bps=20.0)
|
|
p20 = simulate_instr(CloseConfirmSl, sleeve, {"buffer": BUF}, OOS_START_MS, None, exit_slip_bps=20.0)
|
|
ok = p20 and b20 and p20["sharpe_t"] >= b20["sharpe_t"] - 0.10
|
|
fair &= bool(ok)
|
|
print(f" {key:<10} base+20 {fmt(b20)}")
|
|
print(f" {'':<10} pol +20 {fmt(p20)} {'OK' if ok else 'WORSE'}")
|
|
print(f"fairness (entrambe +20bps): policy >= base-0.10 sh? {fair}")
|
|
|
|
# ===================================================================
|
|
print("\n" + "=" * 104)
|
|
print("TEST 4 — OVERLAP/TURNOVER: segnali saltati per non-overlap + capital-time (OOS)")
|
|
print("=" * 104)
|
|
for (code, asset), sleeve in data.items():
|
|
key = f"{code.split('_')[0]} {asset}"
|
|
b = simulate_instr(ExitPolicy, sleeve, {}, OOS_START_MS, None)
|
|
p = simulate_instr(CloseConfirmSl, sleeve, {"buffer": BUF}, OOS_START_MS, None)
|
|
if b and p:
|
|
dskip = p["skipped_overlap"] - b["skipped_overlap"]
|
|
dbars = p["bars_tot"] - b["bars_tot"]
|
|
print(f" {key:<10} base: trades {b['trades']:>4} skip-overlap {b['skipped_overlap']:>4} "
|
|
f"bars_tot {b['bars_tot']:>6} avg {b['avg_bars']:.1f}")
|
|
print(f" {'':<10} pol : trades {p['trades']:>4} skip-overlap {p['skipped_overlap']:>4} "
|
|
f"bars_tot {p['bars_tot']:>6} avg {p['avg_bars']:.1f}")
|
|
print(f" {'':<10} -> +{dskip} segnali persi per overlap, "
|
|
f"+{dbars} bars in posizione ({dbars/max(b['bars_tot'],1)*100:+.0f}% capital-time)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|