feat(DIP01): EXIT-16 close-confirm SL (gate gap-aware 36/36 BTC, PORT06 promosso)
Punto 9 roadmap sweep. DIP01 era l'unico sleeve BTC con esecuzione reale round-trip ancora sul branch SL intrabar wick-sensitive. Gate dip01_exit16_impact.py (parita' canonica 1.00000; engine GAP-AWARE: orig filla lo SL a worse(livello, open) per rimuovere il bias pro-stop-intrabar dell'engine canonico sui gap-through): - grid 3x3x2 BTC: EXIT-16 >= orig in 36/36 (train E oos), OOS Sharpe ~2-4x (canonica 1.47->3.48); ETH 35/36 (robustezza). - plateau buffer piatto 0.4-1.0 (OOS DD 6.4% identico) -> 0.5 come le fade. - PORT06: FULL Sh 6.43->6.61 DD 3.96->3.58 | OOS Sh 8.58->8.77 DD 1.36->1.34. 5a conferma del principio EXIT-16 (wick-stop = falsi negativi per mean-reversion). hurst_max non valutato (ridondante-dannoso post-EXIT-16, gate trendmax). Deploy: "sl_confirm_atr": 0.5 nei params DIP01_BTC (il worker legge gia'). Trappola di metodo documentata: la prima parita' falliva per l'ancora bfill di _daily_equity (la serie a punti-trade reindexata su IDX aggancia il primo valore al PRIMO trade in finestra, non al capitale portato avanti) — replicata per parita'; finding aperto perche' tocca le metriche canoniche di tutti gli sleeve a punti-trade. Diario docs/diary/2026-06-07-dip01-exit16.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
"""GATE DIP01 + PORT06: estendere EXIT-16 (close-confirm SL) a DIP01 (sweep punto 9).
|
||||
|
||||
DIP01 e' l'unico sleeve BTC con esecuzione REALE round-trip, e gira ancora col
|
||||
branch SL intrabar wick-sensitive. EXIT-16 e' stato validato SULLE FADE: estenderlo
|
||||
a una strategia honest richiede la validazione sul grid proprio di DIP01, con
|
||||
engine GAP-AWARE (lezione exit-lab: l'engine canonico filla gli stop "al livello"
|
||||
anche su gap-through -> bias PRO stop intrabar stretti; il confronto onesto filla
|
||||
lo SL a worse(livello, open)).
|
||||
|
||||
Protocollo:
|
||||
[1] parita': replay engine 'orig' (fill al livello) == equity canonica DIP01_BTC
|
||||
[2] grid 3x3x2 (z_in x sl_atr x max_bars) su BTC (deployato) ed ETH (robustezza):
|
||||
orig GAP-AWARE vs EXIT-16(buf 0.5), ret/DD/Sharpe train (pre-OOS) e OOS
|
||||
[3] plateau buffer {0.4, 0.5, 0.75, 1.0} sulla cella canonica
|
||||
[4] gate PORT06: DIP01_BTC exit16 innestato nel canonico, pesi cap
|
||||
-> PROMOSSO se OOS Sharpe non peggiora E FULL/DD non degradano materialmente.
|
||||
|
||||
NB hurst_max NON valutato: il gate trendmax (2026-06-07) ha mostrato che il
|
||||
loss-guard Hurst e' ridondante-dannoso POST-EXIT-16 (stesso regime target).
|
||||
|
||||
uv run python scripts/analysis/dip01_exit16_impact.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 src.data.downloader import load_data
|
||||
from scripts.analysis.strategy_research import atr
|
||||
from scripts.analysis.combine_portfolio import (
|
||||
_norm, IDX, port_returns, metrics, SPLIT, OOS_DATE,
|
||||
)
|
||||
from scripts.portfolios._defs import PORTFOLIOS
|
||||
from src.portfolio import weighting as W
|
||||
|
||||
FEE_RT, LEV, POS, INIT = 0.001, 3.0, 0.15, 1000.0
|
||||
BUFFER = 0.5
|
||||
GRID_Z = (2.0, 2.5, 3.0)
|
||||
GRID_SL = (2.0, 2.5, 3.0)
|
||||
GRID_MB = (24, 48)
|
||||
CANON = dict(n=50, z_in=2.5, sl_atr=2.5, max_bars=24)
|
||||
|
||||
|
||||
def dip_entries(df, n=50, z_in=2.5, sl_atr=2.5, max_bars=24):
|
||||
"""Entries DIP01 == honest_improve2.dip_market_gated (market_n=0): crossing
|
||||
di z sotto -z_in. Ritorna [{i, tp, sl, mb}] (long-only)."""
|
||||
c = df["close"].values
|
||||
ma = pd.Series(c).rolling(n).mean().values
|
||||
sd = pd.Series(c).rolling(n).std().values
|
||||
a = atr(df, 14)
|
||||
z = (c - ma) / np.where(sd == 0, np.nan, sd)
|
||||
out = []
|
||||
for i in range(n + 14, len(c)):
|
||||
if np.isnan(z[i]) or np.isnan(a[i]):
|
||||
continue
|
||||
if z[i] <= -z_in and z[i - 1] > -z_in:
|
||||
out.append({"i": i, "tp": ma[i], "sl": c[i] - sl_atr * a[i],
|
||||
"mb": max_bars})
|
||||
return out
|
||||
|
||||
|
||||
def dip_trades(ents, df, mode, buffer=BUFFER):
|
||||
"""Engine exit DIP01 (long-only), non-overlap come il canonico.
|
||||
|
||||
mode="orig" : SL intrabar fill AL LIVELLO (== canonico, per la parita')
|
||||
mode="orig_gap" : SL intrabar fill a worse(livello, open[j]) — gap-aware
|
||||
mode="exit16" : SL intrabar OFF; TP intrabar al livello (priorita' nel bar);
|
||||
stop solo se close[j] < sl - buffer*ATR14[j], fill a close[j]
|
||||
"""
|
||||
h, l, c, o = df["high"].values, df["low"].values, df["close"].values, df["open"].values
|
||||
n = len(c)
|
||||
a = atr(df, 14)
|
||||
fee = FEE_RT * LEV
|
||||
out = []
|
||||
last = -1
|
||||
for e in ents:
|
||||
i = e["i"]
|
||||
if i <= last or i + 1 >= n:
|
||||
continue
|
||||
tp, sl, mb = e["tp"], e["sl"], e["mb"]
|
||||
exit_p = c[min(i + mb, n - 1)]
|
||||
j = min(i + mb, n - 1)
|
||||
for k in range(1, mb + 1):
|
||||
j = i + k
|
||||
if j >= n:
|
||||
j = n - 1
|
||||
exit_p = c[j]
|
||||
break
|
||||
if mode in ("orig", "orig_gap"):
|
||||
if l[j] <= sl:
|
||||
exit_p = sl if mode == "orig" else min(sl, o[j])
|
||||
break
|
||||
if h[j] >= tp:
|
||||
exit_p = tp
|
||||
break
|
||||
if k == mb:
|
||||
exit_p = c[j]
|
||||
else: # exit16
|
||||
if h[j] >= tp:
|
||||
exit_p = tp
|
||||
break
|
||||
aj = a[j] if np.isfinite(a[j]) else 0.0
|
||||
if c[j] < sl - buffer * aj:
|
||||
exit_p = c[j]
|
||||
break
|
||||
if k == mb:
|
||||
exit_p = c[j]
|
||||
ret = (exit_p - c[i]) / c[i] * LEV - fee
|
||||
out.append((i, j, ret))
|
||||
last = j
|
||||
return out
|
||||
|
||||
|
||||
def daily_equity(df, trades):
|
||||
"""Equity giornaliera con la convenzione CANONICA honest (_daily_equity su punti
|
||||
trade-exit). NB: la serie a punti-trade reindexata su IDX ancora il primo valore
|
||||
al PRIMO trade dentro IDX (bfill), non al capitale portato avanti da prima —
|
||||
convenzione discutibile ma e' quella di build_everything: per la parita' (e il
|
||||
confronto col PORT06 canonico) va replicata esattamente."""
|
||||
from scripts.analysis.honest_improve2 import _daily_equity
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
cap = INIT
|
||||
eq_ts, eq_v = [], []
|
||||
for i, j, ret in sorted(trades, key=lambda t: t[1]):
|
||||
cap = max(cap + cap * POS * ret, 10.0)
|
||||
eq_ts.append(ts.iloc[j])
|
||||
eq_v.append(cap)
|
||||
return _norm(_daily_equity(eq_ts, eq_v, IDX))
|
||||
|
||||
|
||||
def cell_metrics(eq):
|
||||
dr = eq.pct_change().fillna(0.0)
|
||||
return metrics(dr), metrics(dr, lo=SPLIT)
|
||||
|
||||
|
||||
def port_metrics(members, p):
|
||||
ids = p.sleeve_ids
|
||||
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
|
||||
w = W.weight_vector(p.weighting, ids, dr, weights=p.weights,
|
||||
caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
|
||||
drp = port_returns({i: members[i] for i in ids}, w)
|
||||
return metrics(drp), metrics(drp, lo=SPLIT)
|
||||
|
||||
|
||||
def main():
|
||||
p = PORTFOLIOS["PORT06"]
|
||||
print("=" * 104)
|
||||
print(" GATE DIP01 EXIT-16 (close-confirm 0.5 ATR) — grid gap-aware + PORT06")
|
||||
print(f" OOS da {OOS_DATE} | fee {FEE_RT*100:.2f}%RT x lev{LEV:.0f} | pos {POS}")
|
||||
print("=" * 104)
|
||||
|
||||
print("\n[1] build_everything() canonico (cache)...")
|
||||
from src.portfolio.sleeves import all_sleeve_equities
|
||||
eq_base = dict(all_sleeve_equities())
|
||||
|
||||
dfs = {a: load_data(a, "1h") for a in ("BTC", "ETH")}
|
||||
|
||||
# --- parita' ---
|
||||
ents = dip_entries(dfs["BTC"], **CANON)
|
||||
rep = daily_equity(dfs["BTC"], dip_trades(ents, dfs["BTC"], "orig"))
|
||||
base = eq_base["DIP01_BTC"]
|
||||
corr = base.pct_change().fillna(0).corr(rep.pct_change().fillna(0))
|
||||
rb = (base.iloc[-1] / base.iloc[0] - 1) * 100
|
||||
rr = (rep.iloc[-1] / rep.iloc[0] - 1) * 100
|
||||
print(f"\n[1] PARITA' orig vs canonico: corr={corr:.5f} ret {rb:+.0f}% vs {rr:+.0f}%")
|
||||
if not (corr > 0.999 and abs(rr - rb) <= max(1.0, abs(rb) * 0.01)):
|
||||
print(" >>> PARITA' FALLITA: STOP.")
|
||||
return
|
||||
|
||||
# --- [2] grid gap-aware ---
|
||||
for asset in ("BTC", "ETH"):
|
||||
df = dfs[asset]
|
||||
print(f"\n[2] GRID {asset} — orig GAP-AWARE vs EXIT-16 (train | OOS: ret% e Sharpe)")
|
||||
print(f" {'cella':<16s}{'tr retO':>9s}{'tr retE':>9s} {'oos retO':>9s}{'oos retE':>9s}"
|
||||
f" {'oos ShO':>8s}{'oos ShE':>8s} {'ddO':>6s}{'ddE':>6s} esito")
|
||||
wins_tr = wins_oos = cells = 0
|
||||
for z in GRID_Z:
|
||||
for slm in GRID_SL:
|
||||
for mb in GRID_MB:
|
||||
ents = dip_entries(df, n=50, z_in=z, sl_atr=slm, max_bars=mb)
|
||||
eo = daily_equity(df, dip_trades(ents, df, "orig_gap"))
|
||||
ee = daily_equity(df, dip_trades(ents, df, "exit16"))
|
||||
fo, oo = cell_metrics(eo)
|
||||
fe, oe = cell_metrics(ee)
|
||||
tr_o = fo["ret"] - oo["ret"]; tr_e = fe["ret"] - oe["ret"] # ~train (full-oos, approssimato su ret composti: usare segni)
|
||||
# train ret esatto: equity al SPLIT
|
||||
tr_o = (eo.iloc[SPLIT] / eo.iloc[0] - 1) * 100
|
||||
tr_e = (ee.iloc[SPLIT] / ee.iloc[0] - 1) * 100
|
||||
cells += 1
|
||||
w_tr = tr_e >= tr_o
|
||||
w_oos = oe["ret"] >= oo["ret"]
|
||||
wins_tr += w_tr
|
||||
wins_oos += w_oos
|
||||
tag = ("OK" if (w_tr and w_oos) else "tr-" if w_oos else "oos-" if w_tr else "KO")
|
||||
print(f" z{z} sl{slm} mb{mb:<3d}{tr_o:>9.0f}{tr_e:>9.0f} "
|
||||
f"{oo['ret']:>9.0f}{oe['ret']:>9.0f} {oo['sharpe']:>8.2f}{oe['sharpe']:>8.2f}"
|
||||
f" {fo['dd']:>6.1f}{fe['dd']:>6.1f} {tag}")
|
||||
print(f" -> EXIT-16 >= orig-gap: train {wins_tr}/{cells}, OOS {wins_oos}/{cells}")
|
||||
|
||||
# --- [3] plateau buffer (BTC, cella canonica) ---
|
||||
print("\n[3] Plateau buffer EXIT-16 (BTC, cella canonica):")
|
||||
ents = dip_entries(dfs["BTC"], **CANON)
|
||||
for buf in (0.4, 0.5, 0.75, 1.0):
|
||||
ee = daily_equity(dfs["BTC"], dip_trades(ents, dfs["BTC"], "exit16", buffer=buf))
|
||||
fe, oe = cell_metrics(ee)
|
||||
print(f" buf {buf:<5}FULL ret {fe['ret']:>+7.0f}% DD {fe['dd']:>5.1f} Sh {fe['sharpe']:>5.2f}"
|
||||
f" | OOS ret {oe['ret']:>+6.0f}% DD {oe['dd']:>5.1f} Sh {oe['sharpe']:>5.2f}")
|
||||
|
||||
# --- [4] gate PORT06 ---
|
||||
ee = daily_equity(dfs["BTC"], dip_trades(ents, dfs["BTC"], "exit16"))
|
||||
members_b = dict(eq_base)
|
||||
members_e = dict(eq_base)
|
||||
members_e["DIP01_BTC"] = ee
|
||||
f_b, o_b = port_metrics(members_b, p)
|
||||
f_e, o_e = port_metrics(members_e, p)
|
||||
print("\n" + "=" * 104)
|
||||
print(f" [4] PORT06 (pesi cap {p.caps}) — DIP01_BTC orig vs EXIT-16")
|
||||
print("=" * 104)
|
||||
print(f" {'variante':<10s}{'FULL Sh':>9s}{'FULL DD%':>10s}{'CAGR':>6s} | {'OOS Sh':>7s}{'OOS DD%':>8s}{'CAGR':>6s}")
|
||||
for nm, (f, o) in (("BASE", (f_b, o_b)), ("EXIT-16", (f_e, o_e))):
|
||||
print(f" {nm:<10s}{f['sharpe']:>9.2f}{f['dd']:>10.2f}{f['cagr']:>5.0f}% | "
|
||||
f"{o['sharpe']:>7.2f}{o['dd']:>8.2f}{o['cagr']:>5.0f}%")
|
||||
|
||||
oos_ok = o_e["sharpe"] >= o_b["sharpe"] - 0.02 and o_e["dd"] <= o_b["dd"] + 0.20
|
||||
full_ok = f_e["sharpe"] >= f_b["sharpe"] - 0.02 and f_e["dd"] <= f_b["dd"] + 0.20
|
||||
promoted = oos_ok and full_ok
|
||||
print(f"\n GATE: OOS {'OK' if oos_ok else 'KO'} | FULL {'OK' if full_ok else 'KO'}")
|
||||
print(" VERDETTO: " + (">>> PROMOSSO <<<" if promoted else ">>> BOCCIATO <<<"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -46,8 +46,12 @@ FADE = [SleeveSpec(kind="single", name=c, sid=f"{c}_{a}", asset=a, cluster=f"{a}
|
||||
for a in ("BTC", "ETH") for c in ("MR01", "MR02", "MR07")]
|
||||
HONEST = [
|
||||
# DIP01: single-asset 1h -> StrategyWorker (Strategy DIP01_dip_buy). TR01/ROT02: multi-asset.
|
||||
# EXIT-16 esteso a DIP01 (2026-06-07, sweep punto 9): gate dip01_exit16_impact.py con
|
||||
# engine GAP-AWARE — grid 36/36 BTC e 35/36 ETH (train E oos), plateau buffer piatto,
|
||||
# PORT06 FULL Sh 6.43->6.61 DD 3.96->3.58, OOS 8.58->8.77. Era l'unico sleeve BTC con
|
||||
# esecuzione reale ancora sul branch SL intrabar wick-sensitive.
|
||||
SleeveSpec(kind="single", name="DIP01", sid="DIP01_BTC", asset="BTC", cluster="BTC-rev",
|
||||
params={"min_tp_frac": MIN_TP_FRAC}),
|
||||
params={"min_tp_frac": MIN_TP_FRAC, "sl_confirm_atr": SL_CONFIRM_ATR}),
|
||||
SleeveSpec(kind="basket", name="TR01", sid="TR01_basket", cluster="trend",
|
||||
params={"universe": ["BNB", "BTC", "DOGE", "SOL", "XRP"], "tf": "4h"}),
|
||||
SleeveSpec(kind="rotation", name="ROT02", sid="ROT02_rot", cluster="rotation",
|
||||
|
||||
Reference in New Issue
Block a user