"""LADDER SL/TP STUDY (2026-06-18) — i 3 passi pre-deploy + studio di SL e TP da aggiungere. Contesto: dopo clean_feed.py i Price Ladder BTC/ETH sono candidati VERI (PROMOSSO, DD gate 2021+ ~11-15%), MA il tail REALE e' il 2018 (-44/-52%) che il gate (IDX 2021+) NON vede. SL/TP sono la leva per domarlo. Prior del progetto: gli stop su mean-reversion sono falsi negativi (EXIT-16/SH01/pairs z-stop) -> ma un grid in un BEAR sostenuto (2018, niente rimbalzo) e' il caso in cui un catastrophe-SL genuinamente aiuta. Questo studio distingue i due regimi. Fa i 3 passi: 1. VALUTAZIONE 2018-INCLUSIVE: metriche standalone su TUTTA la storia (2018+) + DD per anno (il gate del progetto e' cieco al 2018; qui no). 2. FILL maker vs taker: il grid e' LIMIT -> su Deribit fill MAKER ~0%; confronto 0% vs 0.10% RT (la harness e' conservativa). E' la preparazione backtest dello shadow ledger (la parte live = deploy operativo a parte). 3. HALF-SIZE: il candidato finale a meta' size (prudenza coda). E studia SL (sl_buf, = catastrophe stop sotto il range) x TP (tp_buf) sul tail 2018 vs edge 2021+. uv run python scripts/analysis/ladder_sltp_study.py """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) from scripts.analysis.ladder_search import regime_mask, _gate from scripts.analysis.grid_game_gate import grid_mtm OOS_DATE = pd.Timestamp("2024-10-12", tz="UTC") def fm(eqd: pd.Series) -> dict: """Metriche su TUTTA la storia (2018+), niente reindex a IDX 2021+.""" def sh(s): r = s.pct_change().fillna(0.0) return float(r.mean() / r.std() * np.sqrt(365)) if r.std() > 0 else 0.0 def dd(s): c = s / s.iloc[0] return float(((c - c.cummax()) / c.cummax()).min() * 100) oos = eqd[eqd.index >= OOS_DATE] peryear = {int(y): round(dd(g), 1) for y, g in eqd.groupby(eqd.index.year)} return {"full_sh": round(sh(eqd), 2), "full_dd": round(dd(eqd), 1), "oos_sh": round(sh(oos), 2) if len(oos) > 5 else 0.0, "dd2018": peryear.get(2018, 0.0), "dd2022": peryear.get(2022, 0.0), "peryear": peryear} def run(asset, tf, rd, ru, levels, sl_buf, tp_buf, max_bars, regime, tmax, fee_side=0.0005): mask = regime_mask(asset, tf, trend_max=tmax) if regime == "range" else None eqd, st = grid_mtm(asset, tf=tf, range_down=rd, range_up=ru, levels=levels, sl_buf=sl_buf, tp_buf=tp_buf, max_bars=max_bars, deploy_mask=mask, fee_side=fee_side) m = fm(eqd); m["trades"] = st["trades"]; m["eqd"] = eqd return m # candidati base (i migliori del re-gate pulito) BASES = { "BTC 1h L6 range1.5": dict(asset="BTC", tf="1h", rd=0.20, ru=0.06, levels=6, max_bars=720, regime="range", tmax=1.5), "BTC 1h L3 none": dict(asset="BTC", tf="1h", rd=0.08, ru=0.06, levels=3, max_bars=720, regime="none", tmax=2.0), } def sltp_sweep(name, base): print(f"\n{'='*104}\n SL/TP SWEEP — {name} (full=2018+, dd2018=tail vero, oos=2024-10+; fee 0.10% RT)\n{'='*104}") print(f" {'sl_buf':>7}{'tp_buf':>7}{'trades':>8}{'full_sh':>9}{'full_dd':>9}{'dd2018':>8}{'dd2022':>8}{'oos_sh':>8}") best = None for slb in (0.06, 0.08, 0.10, 0.12, 0.15, 0.20): for tpb in (0.03, 0.05, 0.08): m = run(**base, sl_buf=slb, tp_buf=tpb) star = "" # criterio: tail 2018 contenuto (>-25%) E oos edge preservato (sh>3) E full edge ok if m["dd2018"] > -25 and m["oos_sh"] > 3 and m["full_sh"] > 1.5: star = " <-- tail-capped + edge" if best is None or m["dd2018"] > best[1]["dd2018"]: best = ((slb, tpb), m) print(f" {slb:>7.2f}{tpb:>7.2f}{m['trades']:>8}{m['full_sh']:>9.2f}" f"{m['full_dd']:>9.1f}{m['dd2018']:>8.1f}{m['dd2022']:>8.1f}{m['oos_sh']:>8.2f}{star}") return best def main(): print("LADDER SL/TP STUDY — 3 passi pre-deploy + SL/TP da aggiungere\n") # passo 1: valutazione 2018-inclusive dei due base (sl/tp correnti) print("[1] VALUTAZIONE 2018-INCLUSIVE (sl/tp correnti) — DD per anno (il gate IDX2021+ e' cieco al 2018):") for name, base in BASES.items(): m = run(**base, sl_buf=0.12, tp_buf=0.05) print(f" {name:<22} full_sh {m['full_sh']:>5.2f} full_dd {m['full_dd']:>6.1f} " f"oos_sh {m['oos_sh']:>5.2f} | DD/anno {m['peryear']}") # passo SL/TP: sweep su entrambi winners = {} for name, base in BASES.items(): winners[name] = sltp_sweep(name, base) # passo 2+3: maker vs taker + half-size + gate 2021+, sul miglior (sl,tp) del candidato regime-gated name = "BTC 1h L6 range1.5" w = winners.get(name) print(f"\n{'='*104}\n [2+3] FILL maker/taker + HALF-SIZE + GATE 2021+ — {name}\n{'='*104}") if w is None: print(" nessun (sl,tp) ha cappato il tail 2018 sotto -25% mantenendo l'edge: vedi sweep sopra.") # usa comunque lo sl piu' stretto per il confronto fill (slb, tpb) = (0.06, 0.05) else: (slb, tpb), _ = w print(f" miglior (sl_buf,tp_buf) per tail 2018 + edge = ({slb}, {tpb})") base = BASES[name] for fee, lab in ((0.0005, "taker 0.10% RT"), (0.0, "maker 0% (Deribit limit)")): m = run(**base, sl_buf=slb, tp_buf=tpb, fee_side=fee) g = _gate(m["eqd"]) print(f" fee={lab:<26} oos_sh {m['oos_sh']:>5.2f} dd2018 {m['dd2018']:>6.1f} " f"gate½ {g['verdict_half']} (OOS {g['base_oos_sh']}->{g['half_oos_sh']}, corr {g['max_corr_existing']})") print("\n NB half-size: il gate 'half' E' gia' a meta' size (vedi grid_game_gate). La coda 2018") print(" standalone va dimezzata sul book a half. Lo shadow ledger reale (fill intrabar/maker)") print(" resta il passo OPERATIVO finale, non backtestabile qui.") if __name__ == "__main__": main()