bd6232dc00
Crash ETH 2026-06-05: SH01 ETH −15.6% su un trade (exit solo a orizzonte, nessuna protezione). Ricerca con harness dedicato sh01_exit_lab (cache walk-forward, engine fill gap-aware worse(livello,open), parity esatta con explore_lab, train<=2023-11-01): ATR intrabar/close-confirm, %, chandelier, breakeven, giveback, loser-timestop, disaster-cap close+intrabar, swing, vol-regime — NESSUNA passa il gate (ogni stop stretto rompe BTC, ogni stop largo non tocca la coda ETH; nei crash il fill e' al gap). Mitigazione: peso famiglia SHAPE 11.8%->5.9% in PORT06 (FULL 6.47->6.43 DD 4.10->3.96, OOS 8.82->8.58 DD 1.30->1.36) — la prossima coda impatta il conto per meta'. Regression-lock test aggiornato. Diario: docs/diary/2026-06-05-sh01-sl-research.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
239 lines
8.9 KiB
Python
239 lines
8.9 KiB
Python
"""SH01 EXIT policy 04 — chandelier_trail.
|
|
|
|
Trailing chandelier CAUSALE. Lo state tiene il running peak dei CLOSE da i a
|
|
j-1; lo stop per il bar j e':
|
|
long : sl = peak - k * ATR14[j-1]
|
|
short: sl = trough + k * ATR14[j-1] (specchiato)
|
|
Il peak/trough viene aggiornato dentro levels() usando SOLO close[j-1] (dato
|
|
gia' chiuso quando il worker fissa il livello per il bar j). ATR14[j-1] e'
|
|
causale. Griglia k x mode {intrabar, close}.
|
|
|
|
ANTI-LOOK-AHEAD: levels(j) usa peak su close[<=j-1] e ATR14[j-1] -> nessun dato
|
|
del bar j. open_trade usa solo close[i]/ATR14[i]. OK.
|
|
|
|
Profilo SH01: hold a orizzonte (momentum), win ~50%, edge nell'asimmetria dei
|
|
winner. Sulle fade la famiglia trailing e' stata SCARTATA (taglia i winner che
|
|
vanno in drawdown e poi recuperano) -> qui si testa se su SH01 va diversamente,
|
|
pronti a un NO.
|
|
|
|
PROTOCOLLO: grid (k x mode) SOLO sul train (t_hi=OOS_START_MS). Plateau >=3
|
|
celle adiacenti migliorative (adiacenza su k, mode fisso). Poi OOS una volta
|
|
sulla config scelta + 2 vicine.
|
|
|
|
cd /opt/docker/PythagorasGoal && uv run python scripts/analysis/sh01_exit_policies/04_chandelier_trail.py
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
|
|
sys.path.insert(0, "/opt/docker/PythagorasGoal")
|
|
|
|
from scripts.analysis.sh01_exit_lab import ( # noqa: E402
|
|
ExitPolicy, OOS_START_MS, evaluate, load_sleeves, simulate,
|
|
)
|
|
|
|
|
|
class ChandelierTrail(ExitPolicy):
|
|
def __init__(self, k: float, mode: str = "intrabar"):
|
|
self.k = float(k)
|
|
self.mode = mode
|
|
self.name = f"chandelier_trail k={k:.1f} {mode}"
|
|
|
|
def open_trade(self, ctx, i, d):
|
|
# peak/trough inizializzato all'entry (close[i]); atr14[i] noto a close[i].
|
|
entry = ctx["close"][i]
|
|
return {"peak": entry, "trough": entry}
|
|
|
|
def levels(self, ctx, i, d, j, st):
|
|
close = ctx["close"]
|
|
atr = ctx["atr14"]
|
|
# aggiorna il running peak/trough con close[j-1] (gia' chiuso). j>=i+1
|
|
# sempre nell'engine, quindi j-1>=i e' definito.
|
|
cprev = close[j - 1]
|
|
if cprev > st["peak"]:
|
|
st["peak"] = cprev
|
|
if cprev < st["trough"]:
|
|
st["trough"] = cprev
|
|
a = atr[j - 1]
|
|
if not (a == a and a > 0): # nan/0 -> nessuno stop attivo
|
|
return None, self.mode
|
|
if d == 1:
|
|
sl = st["peak"] - self.k * a
|
|
else:
|
|
sl = st["trough"] + self.k * a
|
|
return sl, self.mode
|
|
|
|
def after_bar(self, ctx, i, d, j, st):
|
|
return False
|
|
|
|
|
|
# baseline numbers (exit a orizzonte puro) — dal prompt/harness
|
|
BASELINE = {
|
|
"BTC": {"train": dict(ret=127, dd=23, sharpe=2.09, worst=-5.5),
|
|
"oos": dict(ret=41, dd=8, sharpe=2.18, worst=-3.1)},
|
|
"ETH": {"train": dict(ret=-26, dd=61, sharpe=-0.16, worst=-14.9),
|
|
"oos": dict(ret=143, dd=7, sharpe=3.60, worst=-4.6)},
|
|
}
|
|
|
|
KS = [2.0, 2.5, 3.0, 4.0, 5.0]
|
|
MODES = ["intrabar", "close"]
|
|
|
|
|
|
def _row(tag, a, r):
|
|
print(f" {tag:<10s} {a}: ret={r['ret']:>+7.0f}% dd={r['dd']:>4.0f}% "
|
|
f"shrp={r['sharpe']:>5.2f} worst={r['worst']:>+5.1f}% "
|
|
f"stop={r['stop_rate']:>4.1f}% trades={r['trades']}")
|
|
|
|
|
|
def _eth_ok(et, b_eth):
|
|
return (et["sharpe"] > b_eth["sharpe"] and et["dd"] < b_eth["dd"]
|
|
and et["worst"] > b_eth["worst"])
|
|
|
|
|
|
def _btc_ok(bt, b_btc):
|
|
return (bt["sharpe"] >= 0.95 * b_btc["sharpe"]
|
|
and bt["ret"] >= 0.80 * b_btc["ret"])
|
|
|
|
|
|
def main():
|
|
sleeves = load_sleeves()
|
|
b_btc, b_eth = BASELINE["BTC"]["train"], BASELINE["ETH"]["train"]
|
|
|
|
print("=" * 78)
|
|
print("TRAIN GRID (selezione SOLO sul train, t_hi=OOS_START)")
|
|
print("=" * 78)
|
|
print(" baseline (orizzonte puro):")
|
|
evaluate(ExitPolicy(), sleeves=sleeves)
|
|
print()
|
|
|
|
# train[(mode,k)] -> {asset: result}
|
|
train = {}
|
|
for mode in MODES:
|
|
print(f" --- mode={mode} ---")
|
|
for k in KS:
|
|
pol = ChandelierTrail(k, mode)
|
|
row = {}
|
|
for a in ("BTC", "ETH"):
|
|
row[a] = simulate(sleeves[a], pol, t_hi=OOS_START_MS)
|
|
train[(mode, k)] = row
|
|
print(f" k={k:.1f} ({mode})")
|
|
_row("TRAIN", "BTC", row["BTC"])
|
|
_row("TRAIN", "ETH", row["ETH"])
|
|
print()
|
|
|
|
print("=" * 78)
|
|
print("PLATEAU CHECK (train): ETH sharpe up & dd down & worst up,")
|
|
print(" BTC sharpe>=95% & ret>=80% baseline")
|
|
print("=" * 78)
|
|
improving = {} # mode -> [k...]
|
|
for mode in MODES:
|
|
imp = []
|
|
for k in KS:
|
|
bt, et = train[(mode, k)]["BTC"], train[(mode, k)]["ETH"]
|
|
eth_ok = _eth_ok(et, b_eth)
|
|
btc_ok = _btc_ok(bt, b_btc)
|
|
ok = eth_ok and btc_ok
|
|
if ok:
|
|
imp.append(k)
|
|
print(f" {mode:<9s} k={k:.1f} ETH_ok={eth_ok} BTC_ok={btc_ok} -> "
|
|
f"{'IMPROVING' if ok else '-'}")
|
|
improving[mode] = imp
|
|
print(f" improving cells ({mode}): {imp}")
|
|
|
|
# plateau = >=3 k adiacenti improving in QUALCHE mode
|
|
best_plateau, best_mode = [], None
|
|
for mode in MODES:
|
|
imp = improving[mode]
|
|
for idx in range(len(KS)):
|
|
run = []
|
|
for j in range(idx, len(KS)):
|
|
if KS[j] in imp:
|
|
run.append(KS[j])
|
|
else:
|
|
break
|
|
if len(run) >= 3 and len(run) > len(best_plateau):
|
|
best_plateau, best_mode = run, mode
|
|
print(f" longest adjacent improving run: {best_plateau} (mode={best_mode}) "
|
|
f"plateau={'YES' if len(best_plateau) >= 3 else 'NO'}")
|
|
|
|
chosen = None
|
|
if len(best_plateau) >= 3:
|
|
chosen_k = best_plateau[len(best_plateau) // 2]
|
|
chosen = (best_mode, chosen_k)
|
|
else:
|
|
# fallback: miglior ETH sharpe fra tutti gli improving (per diagnosi OOS)
|
|
cands = [(m, k) for m in MODES for k in improving[m]]
|
|
if cands:
|
|
chosen = max(cands, key=lambda mk: train[mk]["ETH"]["sharpe"])
|
|
|
|
print()
|
|
print("=" * 78)
|
|
if chosen is None:
|
|
print("NESSUNA cella migliorativa sul train -> verdetto NO (niente OOS).")
|
|
print("=" * 78)
|
|
return {"chosen": None, "plateau": best_plateau, "improving": improving,
|
|
"passes": False, "train": train}
|
|
|
|
c_mode, c_k = chosen
|
|
print(f"CHOSEN k={c_k:.1f} mode={c_mode} -> OOS (config + 2 vicine k), 1 volta")
|
|
print("=" * 78)
|
|
ci = KS.index(c_k)
|
|
neigh = [KS[x] for x in (ci - 1, ci, ci + 1) if 0 <= x < len(KS)]
|
|
oos = {}
|
|
for k in neigh:
|
|
pol = ChandelierTrail(k, c_mode)
|
|
row = {}
|
|
for a in ("BTC", "ETH"):
|
|
row[a] = {"train": train[(c_mode, k)][a],
|
|
"oos": simulate(sleeves[a], pol, t_lo=OOS_START_MS)}
|
|
oos[k] = row
|
|
print(f" k={k:.1f} ({c_mode})")
|
|
_row("TRAIN", "BTC", row["BTC"]["train"])
|
|
_row("OOS", "BTC", row["BTC"]["oos"])
|
|
_row("TRAIN", "ETH", row["ETH"]["train"])
|
|
_row("OOS", "ETH", row["ETH"]["oos"])
|
|
|
|
print()
|
|
print("=" * 78)
|
|
print(f"GATE finale (k={c_k:.1f} mode={c_mode}):")
|
|
bt_tr, et_tr = oos[c_k]["BTC"]["train"], oos[c_k]["ETH"]["train"]
|
|
bt_oo, et_oo = oos[c_k]["BTC"]["oos"], oos[c_k]["ETH"]["oos"]
|
|
Bb_o, Be_o = BASELINE["BTC"]["oos"], BASELINE["ETH"]["oos"]
|
|
|
|
a_train = _eth_ok(et_tr, b_eth)
|
|
a_oos = (et_oo["sharpe"] > Be_o["sharpe"] and et_oo["dd"] < Be_o["dd"]
|
|
and et_oo["worst"] > Be_o["worst"])
|
|
cond_a = a_train and a_oos
|
|
b_tr = _btc_ok(bt_tr, b_btc)
|
|
b_oo = (bt_oo["sharpe"] >= 0.95 * Bb_o["sharpe"]
|
|
and bt_oo["ret"] >= 0.80 * Bb_o["ret"])
|
|
cond_b = b_tr and b_oo
|
|
cond_c = et_oo["ret"] >= 0.80 * Be_o["ret"]
|
|
cond_d = len(best_plateau) >= 3
|
|
|
|
print(f" a) ETH sharpe up & dd down & worst up (train&oos): {cond_a}")
|
|
print(f" train: shrp {et_tr['sharpe']:.2f} vs {b_eth['sharpe']:.2f} | "
|
|
f"dd {et_tr['dd']:.0f} vs {b_eth['dd']:.0f} | "
|
|
f"worst {et_tr['worst']:.1f} vs {b_eth['worst']:.1f}")
|
|
print(f" oos: shrp {et_oo['sharpe']:.2f} vs {Be_o['sharpe']:.2f} | "
|
|
f"dd {et_oo['dd']:.0f} vs {Be_o['dd']:.0f} | "
|
|
f"worst {et_oo['worst']:.1f} vs {Be_o['worst']:.1f}")
|
|
print(f" b) BTC sharpe>=95% & ret>=80% (train&oos): {cond_b}")
|
|
print(f" train: shrp {bt_tr['sharpe']:.2f} (>={0.95*b_btc['sharpe']:.2f}) | "
|
|
f"ret {bt_tr['ret']:.0f} (>={0.80*b_btc['ret']:.0f})")
|
|
print(f" oos: shrp {bt_oo['sharpe']:.2f} (>={0.95*Bb_o['sharpe']:.2f}) | "
|
|
f"ret {bt_oo['ret']:.0f} (>={0.80*Bb_o['ret']:.0f})")
|
|
print(f" c) ETH oos ret>=80% baseline ({0.80*Be_o['ret']:.0f}): {cond_c} "
|
|
f"(ret={et_oo['ret']:.0f})")
|
|
print(f" d) plateau: {cond_d} ({best_plateau} mode={best_mode})")
|
|
passes = cond_a and cond_b and cond_c and cond_d
|
|
print(f" PASSES GATE: {passes}")
|
|
print("=" * 78)
|
|
|
|
return {"chosen": chosen, "plateau": best_plateau, "improving": improving,
|
|
"passes": passes, "oos": oos, "conds": (cond_a, cond_b, cond_c, cond_d)}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|