chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
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>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
"""SH01 EXIT policy 01 — atr_fixed_intrabar.
|
||||
|
||||
SL fisso ad ATR, INTRABAR. In open_trade fissiamo il livello una volta sola:
|
||||
sl = entry - d * k * ATR14[i] (entry = close[i], ATR14[i] noto a close[i])
|
||||
levels() restituisce (sl, "intrabar") costante per tutta la vita del trade.
|
||||
Il fill è gap-aware (worse(sl, open[j])) nell'engine — realistico sui crash a
|
||||
gap (es. 2026-06-05: feed flat 2h -> gap ETH 1655->1600).
|
||||
|
||||
ANTI-LOOK-AHEAD: il livello usa SOLO dati <= i (ATR14[i], close[i]); levels usa
|
||||
quel valore congelato (nessun dato del bar j). OK.
|
||||
|
||||
PROTOCOLLO: grid su k SOLO sul train (t_hi=OOS_START_MS). Plateau >=3 celle
|
||||
adiacenti migliorative. Poi OOS una volta sulla config scelta + 2 vicine.
|
||||
|
||||
cd /opt/docker/PythagorasGoal && uv run python scripts/analysis/sh01_exit_policies/01_atr_fixed_intrabar.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 AtrFixedIntrabar(ExitPolicy):
|
||||
def __init__(self, k: float):
|
||||
self.k = float(k)
|
||||
self.name = f"atr_fixed_intrabar k={k:.1f}"
|
||||
|
||||
def open_trade(self, ctx, i, d):
|
||||
atr = ctx["atr14"][i]
|
||||
entry = ctx["close"][i]
|
||||
# se atr nan/0 (early bars) -> nessuno stop attivo
|
||||
sl = entry - d * self.k * atr if atr == atr and atr > 0 else None
|
||||
return {"sl": sl}
|
||||
|
||||
def levels(self, ctx, i, d, j, st):
|
||||
return st["sl"], "intrabar"
|
||||
|
||||
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)},
|
||||
}
|
||||
|
||||
|
||||
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 main():
|
||||
sleeves = load_sleeves()
|
||||
KS = [1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0]
|
||||
|
||||
print("=" * 78)
|
||||
print("TRAIN GRID (selezione SOLO sul train, t_hi=OOS_START)")
|
||||
print("=" * 78)
|
||||
# baseline train
|
||||
print(" baseline (orizzonte puro):")
|
||||
base = evaluate(ExitPolicy(), sleeves=sleeves)
|
||||
print()
|
||||
|
||||
train = {} # k -> {asset: result}
|
||||
for k in KS:
|
||||
pol = AtrFixedIntrabar(k)
|
||||
row = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
ctx = sleeves[a]
|
||||
row[a] = simulate(ctx, pol, t_hi=OOS_START_MS)
|
||||
train[k] = row
|
||||
print(f" k={k:.1f}")
|
||||
_row("TRAIN", "BTC", row["BTC"])
|
||||
_row("TRAIN", "ETH", row["ETH"])
|
||||
|
||||
print()
|
||||
print("=" * 78)
|
||||
print("PLATEAU CHECK (train): per ogni k, ETH sharpe up & dd down & worst up,")
|
||||
print(" BTC sharpe>=95% & ret>=80% baseline")
|
||||
print("=" * 78)
|
||||
b_btc, b_eth = BASELINE["BTC"]["train"], BASELINE["ETH"]["train"]
|
||||
improving = []
|
||||
for k in KS:
|
||||
bt, et = train[k]["BTC"], train[k]["ETH"]
|
||||
eth_ok = (et["sharpe"] > b_eth["sharpe"] and et["dd"] < b_eth["dd"]
|
||||
and et["worst"] > b_eth["worst"])
|
||||
btc_ok = (bt["sharpe"] >= 0.95 * b_btc["sharpe"]
|
||||
and bt["ret"] >= 0.80 * b_btc["ret"])
|
||||
ok = eth_ok and btc_ok
|
||||
if ok:
|
||||
improving.append(k)
|
||||
print(f" k={k:.1f} ETH_ok={eth_ok} BTC_ok={btc_ok} -> "
|
||||
f"{'IMPROVING' if ok else '-'}")
|
||||
print(f" improving cells (train): {improving}")
|
||||
|
||||
# plateau = >=3 k adiacenti improving
|
||||
plateau = []
|
||||
for idx in range(len(KS)):
|
||||
run = []
|
||||
for j in range(idx, len(KS)):
|
||||
if KS[j] in improving:
|
||||
run.append(KS[j])
|
||||
else:
|
||||
break
|
||||
if len(run) >= 3 and len(run) > len(plateau):
|
||||
plateau = run
|
||||
print(f" longest adjacent improving run: {plateau} "
|
||||
f"(plateau={'YES' if len(plateau) >= 3 else 'NO'})")
|
||||
|
||||
# scelgo centro del plateau (o miglior ETH sharpe fra gli improving)
|
||||
chosen = None
|
||||
if len(plateau) >= 3:
|
||||
chosen = plateau[len(plateau) // 2]
|
||||
elif improving:
|
||||
chosen = max(improving, key=lambda k: train[k]["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": plateau, "improving": improving,
|
||||
"train": train, "oos": None}
|
||||
|
||||
print(f"CHOSEN k={chosen:.1f} -> OOS (config + 2 vicine), guardato UNA volta")
|
||||
print("=" * 78)
|
||||
ci = KS.index(chosen)
|
||||
neigh = [KS[x] for x in (ci - 1, ci, ci + 1) if 0 <= x < len(KS)]
|
||||
oos = {}
|
||||
for k in neigh:
|
||||
pol = AtrFixedIntrabar(k)
|
||||
row = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
ctx = sleeves[a]
|
||||
row[a] = {
|
||||
"train": train[k][a],
|
||||
"oos": simulate(ctx, pol, t_lo=OOS_START_MS),
|
||||
}
|
||||
oos[k] = row
|
||||
print(f" k={k:.1f}")
|
||||
_row("TRAIN", "BTC", row["BTC"]["train"])
|
||||
_row("OOS", "BTC", row["BTC"]["oos"])
|
||||
_row("TRAIN", "ETH", row["ETH"]["train"])
|
||||
_row("OOS", "ETH", row["ETH"]["oos"])
|
||||
|
||||
# gate finale sulla config scelta
|
||||
print()
|
||||
print("=" * 78)
|
||||
print(f"GATE finale (k={chosen:.1f}):")
|
||||
bt_tr, et_tr = oos[chosen]["BTC"]["train"], oos[chosen]["ETH"]["train"]
|
||||
bt_oo, et_oo = oos[chosen]["BTC"]["oos"], oos[chosen]["ETH"]["oos"]
|
||||
Bb_o, Be_o = BASELINE["BTC"]["oos"], BASELINE["ETH"]["oos"]
|
||||
|
||||
# a) ETH: sharpe up & dd down & worst up, train E oos
|
||||
a_train = (et_tr["sharpe"] > b_eth["sharpe"] and et_tr["dd"] < b_eth["dd"]
|
||||
and et_tr["worst"] > b_eth["worst"])
|
||||
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) BTC sharpe>=95% & ret>=80% baseline (train e oos)
|
||||
b_tr = (bt_tr["sharpe"] >= 0.95 * b_btc["sharpe"]
|
||||
and bt_tr["ret"] >= 0.80 * b_btc["ret"])
|
||||
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
|
||||
# c) ret ETH oos >= 80% baseline
|
||||
cond_c = et_oo["ret"] >= 0.80 * Be_o["ret"]
|
||||
# d) plateau
|
||||
cond_d = len(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} ({plateau})")
|
||||
passes = cond_a and cond_b and cond_c and cond_d
|
||||
print(f" PASSES GATE: {passes}")
|
||||
print("=" * 78)
|
||||
|
||||
return {"chosen": chosen, "plateau": plateau, "improving": improving,
|
||||
"passes": passes, "oos": oos, "conds": (cond_a, cond_b, cond_c, cond_d)}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,94 @@
|
||||
"""SH01 EXIT POLICY 02 — ATR-fixed stop, CLOSE-CONFIRM (stile EXIT-16 delle fade).
|
||||
|
||||
Stesso livello di stop fisso della policy 01 (intrabar):
|
||||
sl = entry - d * k * ATR14[i] (fissato all'ingresso, dati <= i)
|
||||
ma `levels` ritorna mode="close" → lo stop scatta SOLO se il CLOSE del bar j
|
||||
sfonda il livello, con uscita al close (immune ai wick). E' il trasferimento a
|
||||
SH01 della lezione EXIT-16 sulle fade: l'overshoot che buca lo stop e rientra e'
|
||||
un falso negativo; aspettare la conferma del CLOSE evita di farsi stoppare dai
|
||||
wick di un crash che poi rimbalza dentro l'orizzonte.
|
||||
|
||||
Griglia k in {1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0}.
|
||||
|
||||
ANTI-LOOK-AHEAD: sl usa SOLO atr14[i] e c[i] (dati <= i); mode="close" decide
|
||||
sul close del bar j (dati <= j, eseguibile al poll). Nessun indicatore al bar j.
|
||||
|
||||
cd /opt/docker/PythagorasGoal && uv run python scripts/analysis/sh01_exit_policies/02_atr_fixed_close_confirm.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/opt/docker/PythagorasGoal")
|
||||
|
||||
from scripts.analysis.sh01_exit_lab import ( # noqa: E402
|
||||
ExitPolicy, evaluate, load_sleeves, simulate, OOS_START_MS,
|
||||
)
|
||||
|
||||
|
||||
class AtrFixedCloseConfirm(ExitPolicy):
|
||||
def __init__(self, k: float):
|
||||
self.k = float(k)
|
||||
self.name = f"atr_fixed_close k={k:g}"
|
||||
|
||||
def open_trade(self, ctx: dict, i: int, d: int) -> dict:
|
||||
atr = ctx["atr14"][i]
|
||||
entry = ctx["close"][i]
|
||||
sl = entry - d * self.k * atr
|
||||
return {"sl": float(sl)}
|
||||
|
||||
def levels(self, ctx: dict, i: int, d: int, j: int, st: dict):
|
||||
return st["sl"], "close"
|
||||
|
||||
|
||||
GRID = [1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0]
|
||||
|
||||
|
||||
def _fmt(m):
|
||||
return (f"ret={m['ret']:>+7.0f}% dd={m['dd']:>4.0f}% shrp={m['sharpe']:>5.2f} "
|
||||
f"worst={m['worst']:>+5.1f}% stop={m['stop_rate']:>4.1f}% n={m['trades']}")
|
||||
|
||||
|
||||
def main():
|
||||
sleeves = load_sleeves()
|
||||
|
||||
# baseline (orizzonte puro) per riferimento
|
||||
base = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
ctx = sleeves[a]
|
||||
base[a] = {
|
||||
"train": simulate(ctx, ExitPolicy(), t_hi=OOS_START_MS),
|
||||
"oos": simulate(ctx, ExitPolicy(), t_lo=OOS_START_MS),
|
||||
}
|
||||
|
||||
print("=" * 110)
|
||||
print("BASELINE (exit orizzonte puro):")
|
||||
for a in ("BTC", "ETH"):
|
||||
print(f" {a} TRAIN {_fmt(base[a]['train'])}")
|
||||
print(f" {a} OOS {_fmt(base[a]['oos'])}")
|
||||
|
||||
print("=" * 110)
|
||||
print("GRID — TRAIN ONLY (selezione parametri):")
|
||||
train_res = {}
|
||||
for k in GRID:
|
||||
pol = AtrFixedCloseConfirm(k)
|
||||
row = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
row[a] = simulate(sleeves[a], pol, t_hi=OOS_START_MS)
|
||||
train_res[k] = row
|
||||
print(f" k={k:>3g} | BTC {_fmt(row['BTC'])}")
|
||||
print(f" | ETH {_fmt(row['ETH'])}")
|
||||
|
||||
# verdetto OOS sulla config scelta + vicine (guardato una volta sola)
|
||||
print("=" * 110)
|
||||
print("OOS (verdetto, config scelta + vicine):")
|
||||
for k in GRID:
|
||||
pol = AtrFixedCloseConfirm(k)
|
||||
print(f" k={k:>3g} | BTC TRAIN {_fmt(train_res[k]['BTC'])}")
|
||||
for a in ("BTC", "ETH"):
|
||||
oo = simulate(sleeves[a], pol, t_lo=OOS_START_MS)
|
||||
print(f" | {a} OOS {_fmt(oo)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,203 @@
|
||||
"""SH01 exit policy 03 — pct_fixed.
|
||||
|
||||
SL fisso in PERCENTUALE del prezzo d'ingresso: sl = entry * (1 - d*p).
|
||||
Griglia p in {0.01, 0.015, 0.02, 0.03, 0.04, 0.05}, modalita' {intrabar, close}
|
||||
-> 12 celle. Il livello e' FISSO (deciso a open_trade su close[i]) -> nessun
|
||||
look-ahead nei bar successivi (i livelli usano solo dati <= i).
|
||||
|
||||
Protocollo: grid SOLO sul train; plateau (>=3 celle adiacenti migliorative);
|
||||
poi OOS una volta per la config scelta + le 2 vicine.
|
||||
|
||||
cd /opt/docker/PythagorasGoal && uv run python scripts/analysis/sh01_exit_policies/03_pct_fixed.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/opt/docker/PythagorasGoal")
|
||||
|
||||
from scripts.analysis.sh01_exit_lab import ( # noqa: E402
|
||||
ASSETS, OOS_START_MS, ExitPolicy, load_sleeves, simulate,
|
||||
)
|
||||
|
||||
|
||||
class PctFixed(ExitPolicy):
|
||||
"""SL fisso a una frazione p del prezzo d'ingresso."""
|
||||
|
||||
def __init__(self, p: float, mode: str = "intrabar"):
|
||||
self.p = p
|
||||
self.mode = mode
|
||||
self.name = f"pct_fixed p={p:.3f} {mode}"
|
||||
|
||||
def open_trade(self, ctx, i, d):
|
||||
entry = ctx["close"][i]
|
||||
sl = entry * (1.0 - d * self.p) # long: sotto; short: sopra
|
||||
return {"sl": sl}
|
||||
|
||||
def levels(self, ctx, i, d, j, st):
|
||||
return st["sl"], self.mode
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- grid
|
||||
|
||||
P_GRID = [0.01, 0.015, 0.02, 0.03, 0.04, 0.05]
|
||||
MODES = ["intrabar", "close"]
|
||||
|
||||
|
||||
def _row(m):
|
||||
return (f"ret={m['ret']:>+7.0f}% dd={m['dd']:>4.0f}% shrp={m['sharpe']:>5.2f} "
|
||||
f"worst={m['worst']:>+5.1f}% stop={m['stop_rate']:>4.1f}%")
|
||||
|
||||
|
||||
def main():
|
||||
sleeves = load_sleeves()
|
||||
|
||||
# baseline (no stop)
|
||||
print("=" * 110)
|
||||
print("BASELINE (orizzonte puro, no SL) — TRAIN:")
|
||||
base = {}
|
||||
for a in ASSETS:
|
||||
m = simulate(sleeves[a], ExitPolicy(), t_hi=OOS_START_MS)
|
||||
base[a] = m
|
||||
print(f" {a}: {_row(m)}")
|
||||
print()
|
||||
|
||||
# ---------------- grid TRAIN only
|
||||
print("=" * 110)
|
||||
print("GRID — TRAIN ONLY (selezione qui):")
|
||||
train = {}
|
||||
for mode in MODES:
|
||||
print(f"\n mode={mode}")
|
||||
for p in P_GRID:
|
||||
pol = PctFixed(p, mode)
|
||||
row = {}
|
||||
for a in ASSETS:
|
||||
m = simulate(sleeves[a], pol, t_hi=OOS_START_MS)
|
||||
row[a] = m
|
||||
train[(mode, p)] = row
|
||||
print(f" p={p:.3f} | BTC {_row(row['BTC'])}")
|
||||
print(f" | ETH {_row(row['ETH'])}")
|
||||
|
||||
# improvement flags vs baseline on TRAIN: ETH gate (sharpe up, dd down, worst less neg)
|
||||
# + BTC not degraded (sharpe>=0.95x, ret>=0.80x)
|
||||
print("\n" + "=" * 110)
|
||||
print("TRAIN improvement check (cell = migliorativa se ETH sharpe^ dd v worst^ AND BTC sharpe>=95% ret>=80%):")
|
||||
bE, bB = base["ETH"], base["BTC"]
|
||||
improved = {}
|
||||
for mode in MODES:
|
||||
flags = []
|
||||
for p in P_GRID:
|
||||
r = train[(mode, p)]
|
||||
eth, btc = r["ETH"], r["BTC"]
|
||||
eth_ok = (eth["sharpe"] > bE["sharpe"] and eth["dd"] < bE["dd"]
|
||||
and eth["worst"] > bE["worst"])
|
||||
btc_ok = (btc["sharpe"] >= 0.95 * bB["sharpe"]
|
||||
and btc["ret"] >= 0.80 * bB["ret"])
|
||||
cell = eth_ok and btc_ok
|
||||
improved[(mode, p)] = cell
|
||||
flags.append("Y" if cell else (".|E" if not eth_ok else ".|B"))
|
||||
print(f" mode={mode:<9s} " + " ".join(f"p={p:.3f}:{f}" for p, f in zip(P_GRID, flags)))
|
||||
|
||||
# plateau detection: >=3 adjacent p's (same mode) all improved
|
||||
print("\nPLATEAU (>=3 p adiacenti migliorativi nella stessa modalita'):")
|
||||
plateau_cells = []
|
||||
for mode in MODES:
|
||||
run = []
|
||||
runs = []
|
||||
for p in P_GRID:
|
||||
if improved[(mode, p)]:
|
||||
run.append(p)
|
||||
else:
|
||||
if len(run) >= 1:
|
||||
runs.append(run)
|
||||
run = []
|
||||
if run:
|
||||
runs.append(run)
|
||||
for run in runs:
|
||||
mark = " <-- PLATEAU" if len(run) >= 3 else ""
|
||||
print(f" mode={mode}: run {run} (len {len(run)}){mark}")
|
||||
if len(run) >= 3:
|
||||
plateau_cells.extend((mode, p) for p in run)
|
||||
|
||||
if not plateau_cells:
|
||||
print("\nNESSUN PLATEAU sul train -> famiglia NON passa. OOS solo informativo.")
|
||||
else:
|
||||
print(f"\nplateau cells: {plateau_cells}")
|
||||
|
||||
# ---------------- pick best cell on TRAIN within plateau (or best overall if no plateau)
|
||||
def score(cell):
|
||||
r = train[cell]
|
||||
# ETH train e' il banco di prova (baseline negativo) -> max ETH sharpe,
|
||||
# tie-break ETH dd minore, poi BTC sharpe.
|
||||
return (r["ETH"]["sharpe"], -r["ETH"]["dd"], r["BTC"]["sharpe"])
|
||||
|
||||
pool = plateau_cells if plateau_cells else list(train.keys())
|
||||
best = max(pool, key=score)
|
||||
print(f"\nCHOSEN (train): mode={best[0]} p={best[1]:.3f}")
|
||||
|
||||
# neighbors (same mode, adjacent p)
|
||||
mode_b, p_b = best
|
||||
idx = P_GRID.index(p_b)
|
||||
neigh = [(mode_b, P_GRID[k]) for k in (idx - 1, idx, idx + 1) if 0 <= k < len(P_GRID)]
|
||||
|
||||
# ---------------- OOS verdict (chosen + 2 neighbors) — looked at ONCE
|
||||
print("\n" + "=" * 110)
|
||||
print("OOS VERDICT (config scelta + 2 vicine) — guardato UNA volta:")
|
||||
print("\nBaseline OOS:")
|
||||
base_oos = {}
|
||||
for a in ASSETS:
|
||||
m = simulate(sleeves[a], ExitPolicy(), t_lo=OOS_START_MS)
|
||||
base_oos[a] = m
|
||||
print(f" {a}: {_row(m)}")
|
||||
|
||||
chosen_oos = None
|
||||
for cell in neigh:
|
||||
pol = PctFixed(cell[1], cell[0])
|
||||
tag = " <== CHOSEN" if cell == best else ""
|
||||
print(f"\n mode={cell[0]} p={cell[1]:.3f}{tag}")
|
||||
res = {}
|
||||
for a in ASSETS:
|
||||
tr = simulate(sleeves[a], pol, t_hi=OOS_START_MS)
|
||||
oo = simulate(sleeves[a], pol, t_lo=OOS_START_MS)
|
||||
res[a] = {"train": tr, "oos": oo}
|
||||
print(f" {a} TRAIN {_row(tr)}")
|
||||
print(f" {a} OOS {_row(oo)}")
|
||||
if cell == best:
|
||||
chosen_oos = res
|
||||
|
||||
# ---------------- gate evaluation on chosen
|
||||
print("\n" + "=" * 110)
|
||||
print("GATE (tutte e 4, train E oos):")
|
||||
r = chosen_oos
|
||||
bE_o, bB_o = base_oos["ETH"], base_oos["BTC"]
|
||||
|
||||
def g(label, cond):
|
||||
print(f" [{'PASS' if cond else 'FAIL'}] {label}")
|
||||
return cond
|
||||
|
||||
# a) ETH: sharpe^ dd v worst^ su train E oos
|
||||
a_tr = (r["ETH"]["train"]["sharpe"] > bE["sharpe"]
|
||||
and r["ETH"]["train"]["dd"] < bE["dd"]
|
||||
and r["ETH"]["train"]["worst"] > bE["worst"])
|
||||
a_oo = (r["ETH"]["oos"]["sharpe"] > bE_o["sharpe"]
|
||||
and r["ETH"]["oos"]["dd"] < bE_o["dd"]
|
||||
and r["ETH"]["oos"]["worst"] > bE_o["worst"])
|
||||
A = g("a) ETH sharpe^ dd v worst^ (train E oos)", a_tr and a_oo)
|
||||
# b) BTC sharpe>=95% ret>=80% baseline (train E oos)
|
||||
b_tr = (r["BTC"]["train"]["sharpe"] >= 0.95 * bB["sharpe"]
|
||||
and r["BTC"]["train"]["ret"] >= 0.80 * bB["ret"])
|
||||
b_oo = (r["BTC"]["oos"]["sharpe"] >= 0.95 * bB_o["sharpe"]
|
||||
and r["BTC"]["oos"]["ret"] >= 0.80 * bB_o["ret"])
|
||||
B = g("b) BTC sharpe>=95% ret>=80% (train E oos)", b_tr and b_oo)
|
||||
# c) ret ETH oos >= 80% baseline
|
||||
C = g("c) ret ETH oos >= 80% baseline", r["ETH"]["oos"]["ret"] >= 0.80 * bE_o["ret"])
|
||||
# d) plateau
|
||||
D = g("d) plateau confermato", bool(plateau_cells) and best in plateau_cells)
|
||||
|
||||
passes = A and B and C and D
|
||||
print(f"\n ==> GATE {'PASS' if passes else 'FAIL'}")
|
||||
return passes
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,238 @@
|
||||
"""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()
|
||||
@@ -0,0 +1,267 @@
|
||||
"""SH01 EXIT policy 05 — breakeven_ratchet.
|
||||
|
||||
Disaster-stop ampio + ratchet a breakeven. Idea: NON tagliare i loser presto
|
||||
(quello distrugge l'edge, lezione exit-lab sulle fade), ma proteggere SOLO i
|
||||
trade gia' andati in profitto, alzando lo stop a entry (o entry+b*ATR) una volta
|
||||
che il move e' partito. Il disaster-stop iniziale (4*ATR14[i]) taglia la coda
|
||||
estrema (il crash ETH -15.6%) senza interferire coi trade normali.
|
||||
|
||||
Logica:
|
||||
long :
|
||||
sl_init = entry - 4 * ATR14[i]
|
||||
quando close[<=j-1] >= entry + a * ATR14[i] -> sl = entry + b * ATR14[i]
|
||||
short: specchiato.
|
||||
RATCHET: una volta alzato lo stop a breakeven, NON riscende (st["armed"]).
|
||||
|
||||
Lo stop iniziale (4*ATR14[i]) e' FISSO sul valore noto a close[i] (open_trade);
|
||||
il ratchet si arma leggendo close[j-1] (gia' chiuso quando il worker fissa il
|
||||
livello per il bar j) -> nessun dato del bar j. ATR14[i] e' causale.
|
||||
|
||||
ANTI-LOOK-AHEAD: open_trade usa solo close[i]/ATR14[i]; levels(j) legge solo
|
||||
close[j-1] per decidere l'arming e ATR14[i] (gia' fissato). OK.
|
||||
|
||||
Griglia: a in {0.5, 1.0, 1.5, 2.0} (soglia di arming in ATR) x b in {0, 0.25}
|
||||
(dove va lo stop una volta armato: entry o entry+0.25 ATR) x mode {intrabar,
|
||||
close}. Il disaster-stop 4*ATR e' fisso (la coda da tagliare e' a -15%, ~3 ATR).
|
||||
|
||||
Profilo SH01: hold a orizzonte, win ~50%, edge nell'asimmetria. Il rischio del
|
||||
breakeven e' di chiudere a 0 i winner che vanno prima in drawdown leggero e poi
|
||||
recuperano -> pronti a un NO se BTC degrada.
|
||||
|
||||
PROTOCOLLO: grid SOLO sul train (t_hi=OOS_START_MS). Plateau >=3 celle adiacenti
|
||||
migliorative (adiacenza su a, con b/mode fissi). Poi OOS una volta su config +
|
||||
2 vicine.
|
||||
|
||||
cd /opt/docker/PythagorasGoal && uv run python scripts/analysis/sh01_exit_policies/05_breakeven_ratchet.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,
|
||||
)
|
||||
|
||||
DISASTER_ATR = 4.0
|
||||
|
||||
|
||||
class BreakevenRatchet(ExitPolicy):
|
||||
def __init__(self, a: float, b: float = 0.0, mode: str = "intrabar"):
|
||||
self.a = float(a)
|
||||
self.b = float(b)
|
||||
self.mode = mode
|
||||
self.name = f"be_ratchet a={a:.1f} b={b:.2f} {mode}"
|
||||
|
||||
def open_trade(self, ctx, i, d):
|
||||
entry = ctx["close"][i]
|
||||
a14 = ctx["atr14"][i]
|
||||
if not (a14 == a14 and a14 > 0):
|
||||
# nessun ATR valido -> nessuno stop (degenera a baseline su quel trade)
|
||||
return {"entry": entry, "atr": None, "sl_disaster": None, "armed": False}
|
||||
if d == 1:
|
||||
sl_dis = entry - DISASTER_ATR * a14
|
||||
else:
|
||||
sl_dis = entry + DISASTER_ATR * a14
|
||||
return {"entry": entry, "atr": a14, "sl_disaster": sl_dis, "armed": False}
|
||||
|
||||
def levels(self, ctx, i, d, j, st):
|
||||
a14 = st["atr"]
|
||||
if a14 is None:
|
||||
return None, self.mode
|
||||
entry = st["entry"]
|
||||
cprev = ctx["close"][j - 1] # gia' chiuso quando si fissa il livello per j
|
||||
# arming del ratchet (una volta armato resta armato)
|
||||
if not st["armed"]:
|
||||
if d == 1:
|
||||
if cprev >= entry + self.a * a14:
|
||||
st["armed"] = True
|
||||
else:
|
||||
if cprev <= entry - self.a * a14:
|
||||
st["armed"] = True
|
||||
if st["armed"]:
|
||||
if d == 1:
|
||||
sl = entry + self.b * a14
|
||||
else:
|
||||
sl = entry - self.b * a14
|
||||
else:
|
||||
sl = st["sl_disaster"]
|
||||
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)},
|
||||
}
|
||||
|
||||
A_VALS = [0.5, 1.0, 1.5, 2.0]
|
||||
B_VALS = [0.0, 0.25]
|
||||
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,b,a)] -> {asset: result}
|
||||
train = {}
|
||||
for mode in MODES:
|
||||
for b in B_VALS:
|
||||
print(f" --- mode={mode} b={b:.2f} ---")
|
||||
for a in A_VALS:
|
||||
pol = BreakevenRatchet(a, b, mode)
|
||||
row = {}
|
||||
for asset in ("BTC", "ETH"):
|
||||
row[asset] = simulate(sleeves[asset], pol, t_hi=OOS_START_MS)
|
||||
train[(mode, b, a)] = row
|
||||
print(f" a={a:.1f} b={b:.2f} ({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,b)] -> [a...]
|
||||
improving = {}
|
||||
for mode in MODES:
|
||||
for b in B_VALS:
|
||||
imp = []
|
||||
for a in A_VALS:
|
||||
bt, et = train[(mode, b, a)]["BTC"], train[(mode, b, a)]["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(a)
|
||||
print(f" {mode:<9s} b={b:.2f} a={a:.1f} ETH_ok={eth_ok} "
|
||||
f"BTC_ok={btc_ok} -> {'IMPROVING' if ok else '-'}")
|
||||
improving[(mode, b)] = imp
|
||||
print(f" improving cells ({mode}, b={b:.2f}): {imp}")
|
||||
|
||||
# plateau = >=3 a adiacenti improving in QUALCHE (mode,b)
|
||||
best_plateau, best_key = [], None
|
||||
for key in improving:
|
||||
imp = improving[key]
|
||||
for idx in range(len(A_VALS)):
|
||||
run = []
|
||||
for jj in range(idx, len(A_VALS)):
|
||||
if A_VALS[jj] in imp:
|
||||
run.append(A_VALS[jj])
|
||||
else:
|
||||
break
|
||||
if len(run) >= 3 and len(run) > len(best_plateau):
|
||||
best_plateau, best_key = run, key
|
||||
print(f" longest adjacent improving run: {best_plateau} (key={best_key}) "
|
||||
f"plateau={'YES' if len(best_plateau) >= 3 else 'NO'}")
|
||||
|
||||
chosen = None
|
||||
if len(best_plateau) >= 3:
|
||||
chosen_a = best_plateau[len(best_plateau) // 2]
|
||||
chosen = (best_key[0], best_key[1], chosen_a)
|
||||
else:
|
||||
cands = [(m, b, a) for (m, b) in improving for a in improving[(m, b)]]
|
||||
if cands:
|
||||
chosen = max(cands, key=lambda mba: train[mba]["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_b, c_a = chosen
|
||||
print(f"CHOSEN a={c_a:.1f} b={c_b:.2f} mode={c_mode} -> OOS (config + 2 vicine a)")
|
||||
print("=" * 78)
|
||||
ai = A_VALS.index(c_a)
|
||||
neigh = [A_VALS[x] for x in (ai - 1, ai, ai + 1) if 0 <= x < len(A_VALS)]
|
||||
oos = {}
|
||||
for a in neigh:
|
||||
pol = BreakevenRatchet(a, c_b, c_mode)
|
||||
row = {}
|
||||
for asset in ("BTC", "ETH"):
|
||||
row[asset] = {"train": train[(c_mode, c_b, a)][asset],
|
||||
"oos": simulate(sleeves[asset], pol, t_lo=OOS_START_MS)}
|
||||
oos[a] = row
|
||||
print(f" a={a:.1f} b={c_b:.2f} ({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 (a={c_a:.1f} b={c_b:.2f} mode={c_mode}):")
|
||||
bt_tr, et_tr = oos[c_a]["BTC"]["train"], oos[c_a]["ETH"]["train"]
|
||||
bt_oo, et_oo = oos[c_a]["BTC"]["oos"], oos[c_a]["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} key={best_key})")
|
||||
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()
|
||||
@@ -0,0 +1,260 @@
|
||||
"""SH01 EXIT policy 06 — giveback (profit-protection).
|
||||
|
||||
Protezione del profitto via after_bar (mode "close" implicito: uscita sempre al
|
||||
close del bar j). Lo state traccia il PEAK FAVOREVOLE dei close da i (per long il
|
||||
max close; per short il min close, specchiato). Si esce al close del bar j se:
|
||||
|
||||
giveback = (peak_fav - close[j]) * d >= g * ATR14[j-1] (retrace)
|
||||
profit_at_peak = (peak_fav - entry) * d >= m * ATR_ref (era in gain)
|
||||
|
||||
cioe' il trade aveva raggiunto un profitto di almeno m*ATR e poi ha ritracciato
|
||||
di g*ATR dal massimo favorevole. Idea: lascia correre il momentum SH01 finche'
|
||||
sale, ma protegge il guadagno quando rifiata — senza toccare i trade che non sono
|
||||
mai andati in profitto (quelli muoiono a orizzonte come nel baseline, cosi' non
|
||||
si crea un trailing-stop mascherato che taglia i winner-in-drawdown).
|
||||
|
||||
Griglia g in {1.0, 1.5, 2.0, 3.0} x m in {0.5, 1.0}.
|
||||
|
||||
ANTI-LOOK-AHEAD: after_bar(j) decide sul CLOSE del bar j (dato <= j, eseguibile
|
||||
al poll). Il peak favorevole include close[j] (gia' chiuso quando si decide).
|
||||
ATR di riferimento: usiamo ATR14[j-1] per la soglia di giveback (causale, come
|
||||
i livelli) e ATR14[i] per la soglia di profit-at-peak (noto a close[i], cioe'
|
||||
all'apertura del trade). open_trade usa solo close[i]/ATR14[i]. Nessun dato di
|
||||
un bar futuro. OK.
|
||||
|
||||
Profilo SH01: hold a orizzonte (momentum), win ~50%, edge nell'asimmetria dei
|
||||
winner. La famiglia "ride/trailing" sulle fade e' stata SCARTATA; il giveback e'
|
||||
una variante condizionata-al-profitto, pensata per NON toccare i loser-che-
|
||||
recuperano. Pronti a un NO se taglia comunque l'edge.
|
||||
|
||||
PROTOCOLLO: grid (g x m) SOLO sul train (t_hi=OOS_START_MS). Plateau >=3 celle
|
||||
adiacenti migliorative (adiacenza su g, m fisso). Poi OOS una volta sulla config
|
||||
scelta + 2 vicine.
|
||||
|
||||
cd /opt/docker/PythagorasGoal && uv run python scripts/analysis/sh01_exit_policies/06_giveback.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 Giveback(ExitPolicy):
|
||||
def __init__(self, g: float, m: float):
|
||||
self.g = float(g)
|
||||
self.m = float(m)
|
||||
self.name = f"giveback g={g:.1f} m={m:.1f}"
|
||||
|
||||
def open_trade(self, ctx, i, d):
|
||||
entry = ctx["close"][i]
|
||||
a0 = ctx["atr14"][i]
|
||||
a0 = float(a0) if (a0 == a0 and a0 > 0) else 0.0
|
||||
# peak favorevole inizializzato all'entry; atr_ref per il profit-at-peak.
|
||||
return {"entry": entry, "peak": entry, "atr0": a0}
|
||||
|
||||
def levels(self, ctx, i, d, j, st):
|
||||
# nessuno stop a livello: il giveback e' tutto in after_bar.
|
||||
return None, "close"
|
||||
|
||||
def after_bar(self, ctx, i, d, j, st):
|
||||
close = ctx["close"]
|
||||
atr = ctx["atr14"]
|
||||
cj = close[j]
|
||||
# aggiorna il peak FAVOREVOLE con close[j] (gia' chiuso quando decidiamo).
|
||||
# per long: max close; per short: min close (= peak favorevole specchiato).
|
||||
if d == 1:
|
||||
if cj > st["peak"]:
|
||||
st["peak"] = cj
|
||||
else:
|
||||
if cj < st["peak"]:
|
||||
st["peak"] = cj
|
||||
|
||||
a_gb = atr[j - 1]
|
||||
if not (a_gb == a_gb and a_gb > 0):
|
||||
return False
|
||||
a_pk = st["atr0"]
|
||||
if a_pk <= 0:
|
||||
return False
|
||||
|
||||
# profitto raggiunto al peak favorevole (in direzione del trade).
|
||||
profit_at_peak = (st["peak"] - st["entry"]) * d
|
||||
if profit_at_peak < self.m * a_pk:
|
||||
return False
|
||||
# ritracciamento dal peak favorevole fino al close corrente.
|
||||
giveback = (st["peak"] - cj) * d
|
||||
return giveback >= self.g * a_gb
|
||||
|
||||
|
||||
# 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)},
|
||||
}
|
||||
|
||||
GS = [1.0, 1.5, 2.0, 3.0]
|
||||
MS = [0.5, 1.0]
|
||||
|
||||
|
||||
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[(m,g)] -> {asset: result}
|
||||
train = {}
|
||||
for m in MS:
|
||||
print(f" --- m={m:.1f} (profit-at-peak threshold) ---")
|
||||
for g in GS:
|
||||
pol = Giveback(g, m)
|
||||
row = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
row[a] = simulate(sleeves[a], pol, t_hi=OOS_START_MS)
|
||||
train[(m, g)] = row
|
||||
print(f" g={g:.1f} m={m:.1f}")
|
||||
_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 = {} # m -> [g...]
|
||||
for m in MS:
|
||||
imp = []
|
||||
for g in GS:
|
||||
bt, et = train[(m, g)]["BTC"], train[(m, g)]["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(g)
|
||||
print(f" m={m:.1f} g={g:.1f} ETH_ok={eth_ok} BTC_ok={btc_ok} -> "
|
||||
f"{'IMPROVING' if ok else '-'}")
|
||||
improving[m] = imp
|
||||
print(f" improving cells (m={m:.1f}): {imp}")
|
||||
|
||||
# plateau = >=3 g adiacenti improving in QUALCHE m
|
||||
best_plateau, best_m = [], None
|
||||
for m in MS:
|
||||
imp = improving[m]
|
||||
for idx in range(len(GS)):
|
||||
run = []
|
||||
for j in range(idx, len(GS)):
|
||||
if GS[j] in imp:
|
||||
run.append(GS[j])
|
||||
else:
|
||||
break
|
||||
if len(run) >= 3 and len(run) > len(best_plateau):
|
||||
best_plateau, best_m = run, m
|
||||
print(f" longest adjacent improving run: {best_plateau} (m={best_m}) "
|
||||
f"plateau={'YES' if len(best_plateau) >= 3 else 'NO'}")
|
||||
|
||||
chosen = None
|
||||
if len(best_plateau) >= 3:
|
||||
chosen_g = best_plateau[len(best_plateau) // 2]
|
||||
chosen = (best_m, chosen_g)
|
||||
else:
|
||||
cands = [(m, g) for m in MS for g in improving[m]]
|
||||
if cands:
|
||||
chosen = max(cands, key=lambda mg: train[mg]["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_m, c_g = chosen
|
||||
print(f"CHOSEN g={c_g:.1f} m={c_m:.1f} -> OOS (config + 2 vicine g), 1 volta")
|
||||
print("=" * 78)
|
||||
gi = GS.index(c_g)
|
||||
neigh = [GS[x] for x in (gi - 1, gi, gi + 1) if 0 <= x < len(GS)]
|
||||
oos = {}
|
||||
for g in neigh:
|
||||
pol = Giveback(g, c_m)
|
||||
row = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
row[a] = {"train": train[(c_m, g)][a],
|
||||
"oos": simulate(sleeves[a], pol, t_lo=OOS_START_MS)}
|
||||
oos[g] = row
|
||||
print(f" g={g:.1f} m={c_m:.1f}")
|
||||
_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 (g={c_g:.1f} m={c_m:.1f}):")
|
||||
bt_tr, et_tr = oos[c_g]["BTC"]["train"], oos[c_g]["ETH"]["train"]
|
||||
bt_oo, et_oo = oos[c_g]["BTC"]["oos"], oos[c_g]["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} m={best_m})")
|
||||
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()
|
||||
@@ -0,0 +1,107 @@
|
||||
"""SH01 EXIT POLICY 07 — LOSER time-stop condizionale (after_bar).
|
||||
|
||||
Idea: SH01 esce a orizzonte fisso H=12. Se a un check-point intermedio j=i+m il
|
||||
trade e' GIA' in perdita oltre una soglia, esci subito al close invece di tenere
|
||||
fino a H. L'ipotesi (lezione exit-lab fade: tagliare i loser presto rischia di
|
||||
tagliare anche i winner in drawdown temporaneo): su un orizzonte FISSO di 12 bar
|
||||
forse un loser conclamato a meta' corsa raramente recupera, mentre i winner del
|
||||
modello partono subito (asimmetria). Il time-stop e' UNA volta sola (al bar m),
|
||||
non un trailing: non insegue il prezzo, condiziona solo l'uscita a un istante.
|
||||
|
||||
Regola (after_bar):
|
||||
al bar j == i + m: se (close[j]-entry)/entry * d < -x * ATR14[i] / entry
|
||||
esci al close del bar j.
|
||||
Equivalente: directional_move[j] < -x*ATR14[i]. x=0.0 => esci se in QUALSIASI
|
||||
perdita direzionale al bar m.
|
||||
|
||||
Griglia m in {2, 3, 4, 6} x x in {0.0, 0.5, 1.0}.
|
||||
|
||||
ANTI-LOOK-AHEAD: ATR14[i] e entry=close[i] fissati all'ingresso (dati <= i);
|
||||
after_bar decide sul close del bar j (dati <= j, eseguibile al poll del tick).
|
||||
Nessun indicatore al bar j stesso.
|
||||
|
||||
cd /opt/docker/PythagorasGoal && uv run python scripts/analysis/sh01_exit_policies/07_loser_timestop.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/opt/docker/PythagorasGoal")
|
||||
|
||||
from scripts.analysis.sh01_exit_lab import ( # noqa: E402
|
||||
ExitPolicy, evaluate, load_sleeves, simulate, OOS_START_MS,
|
||||
)
|
||||
|
||||
|
||||
class LoserTimestop(ExitPolicy):
|
||||
def __init__(self, m: int, x: float):
|
||||
self.m = int(m)
|
||||
self.x = float(x)
|
||||
self.name = f"loser_timestop m={m} x={x:g}"
|
||||
|
||||
def open_trade(self, ctx: dict, i: int, d: int) -> dict:
|
||||
return {
|
||||
"entry": float(ctx["close"][i]),
|
||||
"atr": float(ctx["atr14"][i]),
|
||||
}
|
||||
|
||||
def after_bar(self, ctx: dict, i: int, d: int, j: int, st: dict) -> bool:
|
||||
if j != i + self.m:
|
||||
return False
|
||||
move = (ctx["close"][j] - st["entry"]) * d # directional, in price
|
||||
thresh = -self.x * st["atr"]
|
||||
return move < thresh
|
||||
|
||||
|
||||
GRID_M = [2, 3, 4, 6]
|
||||
GRID_X = [0.0, 0.5, 1.0]
|
||||
|
||||
|
||||
def _fmt(m):
|
||||
return (f"ret={m['ret']:>+7.0f}% dd={m['dd']:>4.0f}% shrp={m['sharpe']:>5.2f} "
|
||||
f"worst={m['worst']:>+5.1f}% stop={m['stop_rate']:>4.1f}% n={m['trades']}")
|
||||
|
||||
|
||||
def main():
|
||||
sleeves = load_sleeves()
|
||||
|
||||
base = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
ctx = sleeves[a]
|
||||
base[a] = {
|
||||
"train": simulate(ctx, ExitPolicy(), t_hi=OOS_START_MS),
|
||||
"oos": simulate(ctx, ExitPolicy(), t_lo=OOS_START_MS),
|
||||
}
|
||||
|
||||
print("=" * 110)
|
||||
print("BASELINE (exit orizzonte puro):")
|
||||
for a in ("BTC", "ETH"):
|
||||
print(f" {a} TRAIN {_fmt(base[a]['train'])}")
|
||||
print(f" {a} OOS {_fmt(base[a]['oos'])}")
|
||||
|
||||
print("=" * 110)
|
||||
print("GRID — TRAIN ONLY (selezione parametri): m x rows")
|
||||
train_res = {}
|
||||
for mm in GRID_M:
|
||||
for xx in GRID_X:
|
||||
pol = LoserTimestop(mm, xx)
|
||||
row = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
row[a] = simulate(sleeves[a], pol, t_hi=OOS_START_MS)
|
||||
train_res[(mm, xx)] = row
|
||||
print(f" m={mm} x={xx:g} | BTC {_fmt(row['BTC'])}")
|
||||
print(f" | ETH {_fmt(row['ETH'])}")
|
||||
|
||||
print("=" * 110)
|
||||
print("OOS (verdetto, intera griglia per ispezione plateau):")
|
||||
for mm in GRID_M:
|
||||
for xx in GRID_X:
|
||||
pol = LoserTimestop(mm, xx)
|
||||
line_b = simulate(sleeves["BTC"], pol, t_lo=OOS_START_MS)
|
||||
line_e = simulate(sleeves["ETH"], pol, t_lo=OOS_START_MS)
|
||||
print(f" m={mm} x={xx:g} | BTC OOS {_fmt(line_b)}")
|
||||
print(f" | ETH OOS {_fmt(line_e)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,159 @@
|
||||
"""SH01 EXIT POLICY 08 — DISASTER-CAP LARGO, close-confirm (minimal intervention).
|
||||
|
||||
Ipotesi: SH01 (exit a orizzonte puro, niente TP/SL) si fa massacrare dalle code
|
||||
rare (crash ETH 2026-06-05 −15.6% in un trade, ETH 2020). Uno stop LARGO a
|
||||
k·ATR14[i] (k grande) dovrebbe toccare SOLO quei pochi trade-disastro, lasciando
|
||||
intatto il resto della distribuzione — e quindi l'edge asimmetrico dei winner.
|
||||
|
||||
sl = entry - d * k * ATR14[i] (fissato all'ingresso, dati <= i)
|
||||
mode = "close" (stop solo se il CLOSE sfonda, stile EXIT-16)
|
||||
|
||||
Griglia LARGA: k in {3.0, 4.0, 5.0, 6.0}. E' il complemento "wide-only" della
|
||||
policy 02 (che spazzava anche stop stretti): qui l'intento e' la NON-interferenza.
|
||||
|
||||
Strumentazione extra (richiesta dal mandato): per ogni k riporto
|
||||
- stop_rate (quanti trade vengono stoppati),
|
||||
- la DISTRIBUZIONE dei trade tagliati: erano tutti loser? quanti winner uccisi?
|
||||
Per ogni trade stoppato confronto il suo ret (post-stop, ⇒ negativo) con il
|
||||
ret che AVREBBE avuto a orizzonte puro (baseline, senza stop) → conto quanti
|
||||
sarebbero finiti winner (stop "dannoso") vs loser (stop "utile").
|
||||
|
||||
ANTI-LOOK-AHEAD: sl usa SOLO atr14[i] e c[i] (dati <= i); mode="close" decide sul
|
||||
close del bar j (dati <= j). Nessun indicatore valutato al bar j.
|
||||
|
||||
cd /opt/docker/PythagorasGoal && uv run python scripts/analysis/sh01_exit_policies/08_disaster_wide_close.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/opt/docker/PythagorasGoal")
|
||||
|
||||
import numpy as np # noqa: E402
|
||||
|
||||
from scripts.analysis.sh01_exit_lab import ( # noqa: E402
|
||||
ExitPolicy, load_sleeves, simulate, OOS_START_MS, FEE_RT, LEV, POS,
|
||||
)
|
||||
|
||||
|
||||
class DisasterWideClose(ExitPolicy):
|
||||
def __init__(self, k: float):
|
||||
self.k = float(k)
|
||||
self.name = f"disaster_wide_close k={k:g}"
|
||||
|
||||
def open_trade(self, ctx: dict, i: int, d: int) -> dict:
|
||||
atr = ctx["atr14"][i]
|
||||
entry = ctx["close"][i]
|
||||
sl = entry - d * self.k * atr
|
||||
return {"sl": float(sl)}
|
||||
|
||||
def levels(self, ctx: dict, i: int, d: int, j: int, st: dict):
|
||||
return st["sl"], "close"
|
||||
|
||||
|
||||
GRID = [3.0, 4.0, 5.0, 6.0]
|
||||
|
||||
|
||||
def _fmt(m):
|
||||
return (f"ret={m['ret']:>+7.0f}% dd={m['dd']:>4.0f}% shrp={m['sharpe']:>5.2f} "
|
||||
f"worst={m['worst']:>+5.1f}% stop={m['stop_rate']:>4.1f}% n={m['trades']}")
|
||||
|
||||
|
||||
def _baseline_ret_by_entry(ctx, t_lo=None, t_hi=None):
|
||||
"""Mappa entry-i -> ret a orizzonte puro (baseline, nessuno stop), stesso
|
||||
engine, stesso slice. Serve a classificare i trade stoppati."""
|
||||
base = simulate(ctx, ExitPolicy(), t_lo=t_lo, t_hi=t_hi)
|
||||
return {r["i"]: r["ret"] for r in base["_trades"]}
|
||||
|
||||
|
||||
def _stop_breakdown(ctx, policy, t_lo=None, t_hi=None):
|
||||
"""Esegue la policy e analizza SOLO i trade con reason=='stop'.
|
||||
Ritorna (n_stop, n_winner_killed, n_loser_cut, dettaglio_list)."""
|
||||
res = simulate(ctx, policy, t_lo=t_lo, t_hi=t_hi)
|
||||
base_ret = _baseline_ret_by_entry(ctx, t_lo=t_lo, t_hi=t_hi)
|
||||
killed = cut = 0
|
||||
detail = []
|
||||
for r in res["_trades"]:
|
||||
if r["reason"] != "stop":
|
||||
continue
|
||||
br = base_ret.get(r["i"])
|
||||
would_win = (br is not None and br > 0)
|
||||
killed += would_win
|
||||
cut += (not would_win)
|
||||
detail.append((r["i"], r["d"], r["ret"], br, would_win))
|
||||
return res, len(detail), killed, cut, detail
|
||||
|
||||
|
||||
def main():
|
||||
sleeves = load_sleeves()
|
||||
|
||||
# baseline orizzonte puro
|
||||
base = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
ctx = sleeves[a]
|
||||
base[a] = {
|
||||
"train": simulate(ctx, ExitPolicy(), t_hi=OOS_START_MS),
|
||||
"oos": simulate(ctx, ExitPolicy(), t_lo=OOS_START_MS),
|
||||
}
|
||||
|
||||
print("=" * 118)
|
||||
print("BASELINE (exit orizzonte puro):")
|
||||
for a in ("BTC", "ETH"):
|
||||
print(f" {a} TRAIN {_fmt(base[a]['train'])}")
|
||||
print(f" {a} OOS {_fmt(base[a]['oos'])}")
|
||||
|
||||
print("=" * 118)
|
||||
print("GRID — TRAIN ONLY (selezione parametri):")
|
||||
train_res = {}
|
||||
for k in GRID:
|
||||
pol = DisasterWideClose(k)
|
||||
row = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
row[a] = simulate(sleeves[a], pol, t_hi=OOS_START_MS)
|
||||
train_res[k] = row
|
||||
print(f" k={k:>3g} | BTC {_fmt(row['BTC'])}")
|
||||
print(f" | ETH {_fmt(row['ETH'])}")
|
||||
|
||||
# ---- breakdown dei trade stoppati (TRAIN), per la domanda "minimal intervention"
|
||||
print("=" * 118)
|
||||
print("STOP BREAKDOWN — TRAIN (chi viene tagliato? winner uccisi vs loser tagliati):")
|
||||
for k in GRID:
|
||||
pol = DisasterWideClose(k)
|
||||
for a in ("BTC", "ETH"):
|
||||
ctx = sleeves[a]
|
||||
res, ns, killed, cut, detail = _stop_breakdown(ctx, pol, t_hi=OOS_START_MS)
|
||||
print(f" k={k:>3g} {a} TRAIN: stop n={ns:>2d}/{res['trades']} "
|
||||
f"({res['stop_rate']:.1f}%) -> loser_tagliati={cut} winner_UCCISI={killed}")
|
||||
for (i, d, ret, br, ww) in detail:
|
||||
tag = "WINNER-KILLED" if ww else "loser-cut"
|
||||
brs = f"{br*100:>+6.1f}%" if br is not None else " n/a "
|
||||
print(f" i={i:>6d} d={d:>+d} stop_ret={ret*100:>+6.1f}% "
|
||||
f"baseline_ret={brs} [{tag}]")
|
||||
|
||||
# ---- verdetto OOS (config scelta + vicine, guardato una volta)
|
||||
print("=" * 118)
|
||||
print("OOS (verdetto):")
|
||||
for k in GRID:
|
||||
pol = DisasterWideClose(k)
|
||||
print(f" k={k:>3g} | BTC TRAIN {_fmt(train_res[k]['BTC'])}")
|
||||
for a in ("BTC", "ETH"):
|
||||
oo = simulate(sleeves[a], pol, t_lo=OOS_START_MS)
|
||||
print(f" | {a} OOS {_fmt(oo)}")
|
||||
print("=" * 118)
|
||||
print("STOP BREAKDOWN — OOS:")
|
||||
for k in GRID:
|
||||
pol = DisasterWideClose(k)
|
||||
for a in ("BTC", "ETH"):
|
||||
ctx = sleeves[a]
|
||||
res, ns, killed, cut, detail = _stop_breakdown(ctx, pol, t_lo=OOS_START_MS)
|
||||
print(f" k={k:>3g} {a} OOS : stop n={ns:>2d}/{res['trades']} "
|
||||
f"({res['stop_rate']:.1f}%) -> loser_tagliati={cut} winner_UCCISI={killed}")
|
||||
for (i, d, ret, br, ww) in detail:
|
||||
tag = "WINNER-KILLED" if ww else "loser-cut"
|
||||
brs = f"{br*100:>+6.1f}%" if br is not None else " n/a "
|
||||
print(f" i={i:>6d} d={d:>+d} stop_ret={ret*100:>+6.1f}% "
|
||||
f"baseline_ret={brs} [{tag}]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,256 @@
|
||||
"""SH01 EXIT policy 09 — swing_stop.
|
||||
|
||||
Stop STRUTTURALE sullo swing recente, fissato all'ingresso:
|
||||
long : sl = min(low[i-N+1 .. i]) - pad * ATR14[i]
|
||||
short: sl = max(high[i-N+1 .. i]) + pad * ATR14[i]
|
||||
Specchiato per d=-1. Il livello e' congelato in open_trade (SOLO dati <= i:
|
||||
low/high della finestra fino a i incluso, ATR14[i] noto a close[i]). levels()
|
||||
restituisce quel livello costante per tutta la vita del trade -> nessun dato del
|
||||
bar j -> anti-look-ahead OK.
|
||||
|
||||
Idea: invece di uno stop a distanza fissa (ATR/%), ancora lo stop alla STRUTTURA
|
||||
del prezzo (minimo/massimo dello swing recente). Un long viene stoppato solo se
|
||||
rompe il supporto strutturale che lo ha generato; il pad in ATR da' un cuscinetto
|
||||
sotto il livello per evitare i wick (mode intrabar) o per richiedere conferma sul
|
||||
close (mode close, stile EXIT-16).
|
||||
|
||||
Griglia: N in {6, 12, 24} x pad in {0.0, 0.25, 0.5} x mode {intrabar, close}.
|
||||
|
||||
cd /opt/docker/PythagorasGoal && uv run python scripts/analysis/sh01_exit_policies/09_swing_stop.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 SwingStop(ExitPolicy):
|
||||
def __init__(self, n: int, pad: float, mode: str):
|
||||
self.n = int(n)
|
||||
self.pad = float(pad)
|
||||
self.mode = str(mode)
|
||||
self.name = f"swing n={n} pad={pad:.2f} {mode}"
|
||||
|
||||
def open_trade(self, ctx, i, d):
|
||||
lo, hi = ctx["low"], ctx["high"]
|
||||
atr = ctx["atr14"][i]
|
||||
lo0 = max(0, i - self.n + 1)
|
||||
if atr != atr or atr <= 0: # nan/0 (early bars) -> nessuno stop
|
||||
return {"sl": None}
|
||||
if d == 1:
|
||||
swing = float(lo[lo0:i + 1].min())
|
||||
sl = swing - self.pad * atr
|
||||
else:
|
||||
swing = float(hi[lo0:i + 1].max())
|
||||
sl = swing + self.pad * atr
|
||||
return {"sl": sl}
|
||||
|
||||
def levels(self, ctx, i, d, j, st):
|
||||
return st["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)},
|
||||
}
|
||||
|
||||
NS = [6, 12, 24]
|
||||
PADS = [0.0, 0.25, 0.5]
|
||||
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, n, pad) -> {asset: result}
|
||||
train = {}
|
||||
for mode in MODES:
|
||||
print(f" --- mode={mode} ---")
|
||||
for n in NS:
|
||||
for pad in PADS:
|
||||
pol = SwingStop(n, pad, mode)
|
||||
row = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
row[a] = simulate(sleeves[a], pol, t_hi=OOS_START_MS)
|
||||
train[(mode, n, pad)] = row
|
||||
print(f" n={n:<2d} pad={pad:.2f}")
|
||||
_row("TRAIN", "BTC", row["BTC"])
|
||||
_row("TRAIN", "ETH", row["ETH"])
|
||||
print()
|
||||
|
||||
print("=" * 78)
|
||||
print("PLATEAU CHECK (train): per ogni cella, ETH(shrp up & dd down & worst up)")
|
||||
print(" & BTC(shrp>=95% & ret>=80% baseline)")
|
||||
print("=" * 78)
|
||||
improving = []
|
||||
grid_imp = {} # (mode,n,pad) -> bool
|
||||
for mode in MODES:
|
||||
for n in NS:
|
||||
for pad in PADS:
|
||||
bt, et = train[(mode, n, pad)]["BTC"], train[(mode, n, pad)]["ETH"]
|
||||
ok = _eth_ok(et, b_eth) and _btc_ok(bt, b_btc)
|
||||
grid_imp[(mode, n, pad)] = ok
|
||||
if ok:
|
||||
improving.append((mode, n, pad))
|
||||
print(f" {mode:<8s} n={n:<2d} pad={pad:.2f} "
|
||||
f"ETH_ok={_eth_ok(et, b_eth)!s:<5} BTC_ok={_btc_ok(bt, b_btc)!s:<5} "
|
||||
f"-> {'IMPROVING' if ok else '-'}")
|
||||
print(f" improving cells (train): {len(improving)}/{len(train)} -> {improving}")
|
||||
|
||||
# PLATEAU = adiacenza nella griglia N x pad (stesso mode). Adiacenti = vicini
|
||||
# nelle liste NS/PADS. Cerco il blocco contiguo piu' grande di celle improving.
|
||||
def adjacent_block_size(mode):
|
||||
cells = [(NS.index(n), PADS.index(p))
|
||||
for (m, n, p) in improving if m == mode]
|
||||
cells_set = set(cells)
|
||||
best = []
|
||||
for start in cells:
|
||||
# BFS sul reticolo 4-connesso
|
||||
seen, stack = set(), [start]
|
||||
while stack:
|
||||
cur = stack.pop()
|
||||
if cur in seen:
|
||||
continue
|
||||
seen.add(cur)
|
||||
ci, cj = cur
|
||||
for di, dj in ((1, 0), (-1, 0), (0, 1), (0, -1)):
|
||||
nb = (ci + di, cj + dj)
|
||||
if nb in cells_set and nb not in seen:
|
||||
stack.append(nb)
|
||||
if len(seen) > len(best):
|
||||
best = list(seen)
|
||||
return best
|
||||
|
||||
plateau_cells = []
|
||||
plateau_mode = None
|
||||
for mode in MODES:
|
||||
blk = adjacent_block_size(mode)
|
||||
if len(blk) > len(plateau_cells):
|
||||
plateau_cells = blk
|
||||
plateau_mode = mode
|
||||
plateau_ok = len(plateau_cells) >= 3
|
||||
if plateau_mode is not None:
|
||||
readable = [(plateau_mode, NS[i], PADS[j]) for (i, j) in plateau_cells]
|
||||
else:
|
||||
readable = []
|
||||
print(f" largest adjacent improving block: {len(plateau_cells)} cells "
|
||||
f"mode={plateau_mode} -> {readable} (plateau={'YES' if plateau_ok else 'NO'})")
|
||||
|
||||
# scelta: centro del plateau (miglior ETH sharpe fra le celle del blocco),
|
||||
# altrimenti miglior ETH sharpe fra gli improving.
|
||||
chosen = None
|
||||
if plateau_ok:
|
||||
chosen = max(readable, key=lambda c: train[c]["ETH"]["sharpe"])
|
||||
elif improving:
|
||||
chosen = max(improving, key=lambda c: train[c]["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": readable, "improving": improving,
|
||||
"passes": False}
|
||||
|
||||
print(f"CHOSEN {chosen} -> OOS (config + vicine), guardato UNA volta")
|
||||
print("=" * 78)
|
||||
mode, n, pad = chosen
|
||||
# vicine: stesso mode, pad +-1 step e n +-1 step (se esistono e improving o no)
|
||||
ni, pi = NS.index(n), PADS.index(pad)
|
||||
neigh = set([chosen])
|
||||
for di, dj in ((0, 0), (1, 0), (-1, 0), (0, 1), (0, -1)):
|
||||
a, b = ni + di, pi + dj
|
||||
if 0 <= a < len(NS) and 0 <= b < len(PADS):
|
||||
neigh.add((mode, NS[a], PADS[b]))
|
||||
oos = {}
|
||||
for c in sorted(neigh, key=lambda c: (c[1], c[2])):
|
||||
m, nn, pp = c
|
||||
pol = SwingStop(nn, pp, m)
|
||||
row = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
row[a] = {"train": train[c][a],
|
||||
"oos": simulate(sleeves[a], pol, t_lo=OOS_START_MS)}
|
||||
oos[c] = row
|
||||
print(f" {m} n={nn} pad={pp:.2f}")
|
||||
_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 ({chosen}):")
|
||||
bt_tr, et_tr = oos[chosen]["BTC"]["train"], oos[chosen]["ETH"]["train"]
|
||||
bt_oo, et_oo = oos[chosen]["BTC"]["oos"], oos[chosen]["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
|
||||
cond_b = _btc_ok(bt_tr, b_btc) and (bt_oo["sharpe"] >= 0.95 * Bb_o["sharpe"]
|
||||
and bt_oo["ret"] >= 0.80 * Bb_o["ret"])
|
||||
cond_c = et_oo["ret"] >= 0.80 * Be_o["ret"]
|
||||
cond_d = plateau_ok
|
||||
|
||||
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} ({len(plateau_cells)} cells)")
|
||||
passes = cond_a and cond_b and cond_c and cond_d
|
||||
print(f" PASSES GATE: {passes}")
|
||||
print("=" * 78)
|
||||
|
||||
return {"chosen": chosen, "plateau": readable, "improving": improving,
|
||||
"passes": passes, "oos": oos,
|
||||
"conds": (cond_a, cond_b, cond_c, cond_d)}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,157 @@
|
||||
"""SH01 EXIT POLICY 10 — vol_regime_stop.
|
||||
|
||||
Stop CONDIZIONALE al regime di volatilita': lo SL esiste solo quando la vol
|
||||
sta esplodendo. Razionale: il danno (2020 ETH, crash live 2026-06-05) avviene
|
||||
in vol-expansion; quando la vol e' normale lo SL taglierebbe winner in
|
||||
drawdown temporaneo (l'edge SH01 e' nell'asimmetria, win ~50%).
|
||||
|
||||
Regime causale: vr[j] = ATR14[j] / SMA100(ATR14)[j]. Nel bar j si guarda
|
||||
vr[j-1] (dati <= j-1). Se vr[j-1] > r -> SL = entry - d*k1*ATR14[i]
|
||||
(ATR all'entry = dati <= i). Altrimenti nessuno stop.
|
||||
|
||||
r in {1.2, 1.5}
|
||||
k1 in {1.5, 2.0, 3.0}
|
||||
mode in {intrabar, close}
|
||||
|
||||
ANTI-LOOK-AHEAD:
|
||||
- vr e' un array precomputato module-level (SMA100 causale, no centering).
|
||||
- levels(j) usa vr[j-1] e atr14[i] (entry), entrambi <= j-1.
|
||||
- mode "close": stop solo se il CLOSE sfonda (stile EXIT-16).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/opt/docker/PythagorasGoal")
|
||||
|
||||
import numpy as np
|
||||
|
||||
from scripts.analysis.sh01_exit_lab import (
|
||||
ExitPolicy, evaluate, load_sleeves, simulate, OOS_START_MS,
|
||||
)
|
||||
|
||||
_VR_CACHE: dict[int, np.ndarray] = {}
|
||||
|
||||
|
||||
def _vol_ratio(atr14: np.ndarray, win: int = 100) -> np.ndarray:
|
||||
"""vr[j] = atr14[j] / SMA(atr14, win)[j], causale. NaN dove non definito."""
|
||||
key = id(atr14)
|
||||
if key in _VR_CACHE:
|
||||
return _VR_CACHE[key]
|
||||
a = np.asarray(atr14, dtype=float)
|
||||
n = len(a)
|
||||
sma = np.full(n, np.nan)
|
||||
# rolling mean causale (include il bar corrente j: e' OK perche' in levels
|
||||
# consumiamo vr[j-1], cioe' dati fino a j-1).
|
||||
csum = np.nancumsum(np.where(np.isnan(a), 0.0, a))
|
||||
cnt = np.cumsum(~np.isnan(a))
|
||||
for j in range(n):
|
||||
lo = j - win + 1
|
||||
if lo < 0:
|
||||
continue
|
||||
s = csum[j] - (csum[lo - 1] if lo > 0 else 0.0)
|
||||
k = cnt[j] - (cnt[lo - 1] if lo > 0 else 0)
|
||||
if k > 0:
|
||||
sma[j] = s / k
|
||||
vr = np.where((sma > 0) & ~np.isnan(a), a / sma, np.nan)
|
||||
_VR_CACHE[key] = vr
|
||||
return vr
|
||||
|
||||
|
||||
class VolRegimeStop(ExitPolicy):
|
||||
def __init__(self, r: float, k1: float, mode: str):
|
||||
self.r = float(r)
|
||||
self.k1 = float(k1)
|
||||
self.mode = mode
|
||||
self.name = f"volreg r{r} k{k1} {mode[:3]}"
|
||||
|
||||
def open_trade(self, ctx: dict, i: int, d: int) -> dict:
|
||||
atr_i = ctx["atr14"][i]
|
||||
if not np.isfinite(atr_i) or atr_i <= 0:
|
||||
atr_i = 0.0
|
||||
return {"vr": _vol_ratio(ctx["atr14"]), "atr_i": float(atr_i)}
|
||||
|
||||
def levels(self, ctx: dict, i: int, d: int, j: int, st: dict):
|
||||
if j - 1 < 0:
|
||||
return None, self.mode
|
||||
vr_prev = st["vr"][j - 1]
|
||||
if not np.isfinite(vr_prev) or vr_prev <= self.r:
|
||||
return None, self.mode # regime calmo -> nessuno stop
|
||||
atr_i = st["atr_i"]
|
||||
if atr_i <= 0:
|
||||
return None, self.mode
|
||||
entry = ctx["close"][i]
|
||||
sl = entry - d * self.k1 * atr_i
|
||||
return sl, self.mode
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------- grid
|
||||
|
||||
def _fmt(m: dict) -> str:
|
||||
return (f"ret={m['ret']:>+7.0f}% dd={m['dd']:>4.0f}% shrp={m['sharpe']:>5.2f} "
|
||||
f"worst={m['worst']:>+5.1f}% stop={m['stop_rate']:>4.1f}%")
|
||||
|
||||
|
||||
def main():
|
||||
sleeves = load_sleeves()
|
||||
|
||||
# baseline
|
||||
print("=== BASELINE (orizzonte puro) ===")
|
||||
base = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
ctx = sleeves[a]
|
||||
tr = simulate(ctx, ExitPolicy(), t_hi=OOS_START_MS)
|
||||
oo = simulate(ctx, ExitPolicy(), t_lo=OOS_START_MS)
|
||||
base[a] = {"train": tr, "oos": oo}
|
||||
print(f" {a} TRAIN {_fmt(tr)}")
|
||||
print(f" {a} OOS {_fmt(oo)}")
|
||||
|
||||
rs = [1.2, 1.5]
|
||||
ks = [1.5, 2.0, 3.0]
|
||||
modes = ["intrabar", "close"]
|
||||
|
||||
print("\n=== GRID (TRAIN only) ===")
|
||||
grid = {}
|
||||
for mode in modes:
|
||||
print(f"\n--- mode={mode} ---")
|
||||
for r in rs:
|
||||
for k1 in ks:
|
||||
pol = VolRegimeStop(r, k1, mode)
|
||||
btc = simulate(sleeves["BTC"], pol, t_hi=OOS_START_MS)
|
||||
eth = simulate(sleeves["ETH"], pol, t_hi=OOS_START_MS)
|
||||
grid[(mode, r, k1)] = (btc, eth)
|
||||
print(f" r={r} k1={k1}: BTC {_fmt(btc)} | ETH {_fmt(eth)}")
|
||||
|
||||
# plateau check sul train: cella migliorativa se
|
||||
# ETH sharpe up & dd down & worst less-neg, BTC sharpe>=95% & ret>=80%
|
||||
bB_tr = base["BTC"]["train"]; bE_tr = base["ETH"]["train"]
|
||||
print("\n=== TRAIN improvement map (cell = ETH sh^ dd_v worst^ AND BTC ok) ===")
|
||||
improved = {}
|
||||
for key, (btc, eth) in grid.items():
|
||||
eth_ok = (eth["sharpe"] > bE_tr["sharpe"] and eth["dd"] < bE_tr["dd"]
|
||||
and eth["worst"] > bE_tr["worst"])
|
||||
btc_ok = (btc["sharpe"] >= 0.95 * bB_tr["sharpe"]
|
||||
and btc["ret"] >= 0.80 * bB_tr["ret"])
|
||||
improved[key] = eth_ok and btc_ok
|
||||
flag = "YES" if improved[key] else " . "
|
||||
print(f" {key}: {flag} (ethSh {eth['sharpe']:+.2f} vs {bE_tr['sharpe']:+.2f}, "
|
||||
f"ethDD {eth['dd']:.0f} vs {bE_tr['dd']:.0f}, ethW {eth['worst']:+.1f} vs {bE_tr['worst']:+.1f}, "
|
||||
f"btcSh {btc['sharpe']:.2f} btcRet {btc['ret']:+.0f})")
|
||||
|
||||
n_imp = sum(improved.values())
|
||||
print(f"\nTRAIN improving cells: {n_imp}/{len(grid)}")
|
||||
|
||||
# OOS verdict on improving cells (guardato UNA volta)
|
||||
print("\n=== OOS verdict (improving train cells) ===")
|
||||
for key, ok in improved.items():
|
||||
if not ok:
|
||||
continue
|
||||
mode, r, k1 = key
|
||||
pol = VolRegimeStop(r, k1, mode)
|
||||
btc = simulate(sleeves["BTC"], pol, t_lo=OOS_START_MS)
|
||||
eth = simulate(sleeves["ETH"], pol, t_lo=OOS_START_MS)
|
||||
print(f" {key}: BTC {_fmt(btc)} | ETH {_fmt(eth)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,105 @@
|
||||
"""SH01 EXIT policy 11 — disaster_wide_intrabar (COMPLETENESS PROBE).
|
||||
|
||||
Le 10 policy precedenti hanno tutte fallito. Diagnosi ricorrente:
|
||||
- close-confirm (02,08) ALLARGA la coda su momentum-continuation (caso live
|
||||
ETH 2026-06-05): il close corre oltre il livello.
|
||||
- intrabar fisso (01) cappa AL livello (worst limitato) ma degrada BTC anche a k=5.
|
||||
|
||||
QUESTA probe chiude il buco: intrabar cap MOLTO LARGO (k=6..12), gap-aware,
|
||||
il cui UNICO scopo e' tagliare la coda catastrofica (la -14.9% ETH / il crash
|
||||
live -15.6%) SENZA mai toccare i normali pullback. E' la domanda diretta:
|
||||
"esiste un k cosi' largo che NON tocca BTC ma cappa la coda ETH?".
|
||||
|
||||
Anti-look-ahead: sl = entry - d*k*ATR14[i], congelato in open_trade (dati<=i);
|
||||
levels restituisce il livello costante, fill gap-aware nell'engine. mode=intrabar
|
||||
cappa AL livello (a differenza del close-confirm che lascia correre il close).
|
||||
|
||||
cd /opt/docker/PythagorasGoal && uv run python scripts/analysis/sh01_exit_policies/11_disaster_wide_intrabar.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, load_sleeves, simulate,
|
||||
)
|
||||
|
||||
|
||||
class DisasterWideIntrabar(ExitPolicy):
|
||||
def __init__(self, k: float):
|
||||
self.k = float(k)
|
||||
self.name = f"disaster_wide_intrabar k={k:.1f}"
|
||||
|
||||
def open_trade(self, ctx, i, d):
|
||||
atr = ctx["atr14"][i]
|
||||
entry = ctx["close"][i]
|
||||
sl = entry - d * self.k * atr if atr == atr and atr > 0 else None
|
||||
return {"sl": sl}
|
||||
|
||||
def levels(self, ctx, i, d, j, st):
|
||||
return st["sl"], "intrabar"
|
||||
|
||||
|
||||
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)},
|
||||
}
|
||||
|
||||
|
||||
def _row(tag, a, r):
|
||||
print(f" {tag:<7s} {a}: ret={r['ret']:>+7.0f}% dd={r['dd']:>4.0f}% "
|
||||
f"shrp={r['sharpe']:>5.2f} worst={r['worst']:>+6.1f}% "
|
||||
f"stop={r['stop_rate']:>4.1f}% trades={r['trades']}")
|
||||
|
||||
|
||||
def main():
|
||||
sleeves = load_sleeves()
|
||||
KS = [5.0, 6.0, 7.0, 8.0, 10.0, 12.0]
|
||||
|
||||
print("=" * 78)
|
||||
print("TRAIN GRID (intrabar cap LARGO, fill gap-aware)")
|
||||
print("=" * 78)
|
||||
train = {}
|
||||
for k in KS:
|
||||
pol = DisasterWideIntrabar(k)
|
||||
row = {a: simulate(sleeves[a], pol, t_hi=OOS_START_MS) for a in ("BTC", "ETH")}
|
||||
train[k] = row
|
||||
print(f" k={k:.1f}")
|
||||
_row("TRAIN", "BTC", row["BTC"])
|
||||
_row("TRAIN", "ETH", row["ETH"])
|
||||
|
||||
b_btc, b_eth = BASELINE["BTC"]["train"], BASELINE["ETH"]["train"]
|
||||
improving = []
|
||||
for k in KS:
|
||||
bt, et = train[k]["BTC"], train[k]["ETH"]
|
||||
eth_ok = (et["sharpe"] > b_eth["sharpe"] and et["dd"] < b_eth["dd"]
|
||||
and et["worst"] > b_eth["worst"])
|
||||
btc_ok = (bt["sharpe"] >= 0.95 * b_btc["sharpe"]
|
||||
and bt["ret"] >= 0.80 * b_btc["ret"])
|
||||
if eth_ok and btc_ok:
|
||||
improving.append(k)
|
||||
print(f" k={k:.1f} ETH_ok={eth_ok} BTC_ok={btc_ok} "
|
||||
f"(BTC shrp={bt['sharpe']:.2f} ret={bt['ret']:.0f} | "
|
||||
f"ETH shrp={et['sharpe']:.2f} dd={et['dd']:.0f} worst={et['worst']:.1f})")
|
||||
print(f"\n improving cells (train): {improving}")
|
||||
|
||||
if not improving:
|
||||
print(" -> NESSUNA cella migliorativa: NO pulito, OOS non guardato.")
|
||||
return
|
||||
|
||||
# plateau >=3 adiacenti? poi OOS
|
||||
print("\n Plateau candidate -> OOS verdetto:")
|
||||
for k in improving:
|
||||
oos = {a: simulate(sleeves[a], DisasterWideIntrabar(k), t_lo=OOS_START_MS)
|
||||
for a in ("BTC", "ETH")}
|
||||
print(f" k={k:.1f}")
|
||||
_row("OOS", "BTC", oos["BTC"])
|
||||
_row("OOS", "ETH", oos["ETH"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user