feat(skyhook): SKH01-V2-DD — asymmetric %-exits cut standalone DD <30% (2-wave agent research)
Second agent wave (skyhook-improve-v2, 14 DD-reduction families, each adversarially verified by 2 skeptics) beats the prior winner on the only unmet goal (DD<30%). Winner = ASYM_LS -> promoted to engine as SKH01_V2_DD: same signal (ptn_n=45, vola[35,95], vol_lo=0, exit-bars 24/16) but exits switched from ATR to FIXED-PCT ASYMMETRIC — long sl4%/tp10%, short sl2%(tighter)/tp8%. The tight short %-SL caps the per-trade loss that forms the maxDD in vol spikes. Verified (sk.study, independent re-run): standalone maxDD BTC 21.4% / ETH 27.4% (<30%), minFull +0.99, minHold +1.26, causality 0/400 both assets, fee-surviving to 0.40%RT, marginal vs TP01 ADDS (corr 0.09, in-sample edge, robust_oos, multicut, clean-year +0.57), blend 0.75*TP01+0.25*SKH uplift_hold +0.87; blend 50/50 full 1.84/hold 1.59/DD 10.7%. Plateau (not knife-edge); both skeptics holds_up=high, killer=null. Engine: per-direction short exit overrides (exit_mode_short/sl_*_short/tp_*_short), backward-compatible (None -> symmetric, V1/intermediate-winner unchanged). +3 tests (8/8 pass). Lessons: DD is cut by changing the exit MECHANISM (%-SL, L/S asymmetry, ensembles), NOT by entry-only kill-switch / vol-target / cadence. PATTERN_CONF killed as overfit (knife-edge). PCTL_DD unverified (rate-limit) and ENS_PARAM/TPSL_DD recency/hedge-loaded -> forward-monitor. NOT yet wired to live sleeves: re-verify blend@0.25 + causality on execution code before deploy. Includes both waves' research scripts (runs/SKH_* wave 1, runs/SKH2_* wave 2). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
"""SKH2_TPSL_DD: RR / stop fine grid around the V2 winner to push standalone maxDD < 30%
|
||||
while holding min-asset HOLD-OUT >= ~0.70 and earns_slot True.
|
||||
|
||||
Winner baseline:
|
||||
SkyhookParams(ptn_n=45, sl_atr=2.5, tp_atr=7.0, uscitalong=24, uscitashort=16,
|
||||
vola_lo=35.0, vola_hi=95.0, vol_lo=0.0)
|
||||
-> minFull +0.83, minHold +0.81, DD BTC 34% / ETH 31% (THE PROBLEM), earns_slot True.
|
||||
|
||||
Family task: sl_atr in {1.75,2.0,2.25,2.5}, tp_atr in {5,6,7,8}, exit_mode 'pct' vs 'atr'.
|
||||
Tighter SL cuts DD but can lower hold-out. Find DD<30 cell + minHold>=0.7 + plateau.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/skyhook")
|
||||
import numpy as np # noqa: E402
|
||||
import skyhooklib as sk # noqa: E402
|
||||
from src.strategies.skyhook import SkyhookParams # noqa: E402
|
||||
|
||||
# Winner fixed (non-RR) fields:
|
||||
BASE = dict(ptn_n=45, uscitalong=24, uscitashort=16, vola_lo=35.0, vola_hi=95.0, vol_lo=0.0)
|
||||
|
||||
|
||||
def mk(sl_atr=None, tp_atr=None, exit_mode="atr", sl_pct=None, tp_pct=None):
|
||||
kw = dict(BASE)
|
||||
kw["exit_mode"] = exit_mode
|
||||
if exit_mode == "atr":
|
||||
kw["sl_atr"] = sl_atr
|
||||
kw["tp_atr"] = tp_atr
|
||||
else:
|
||||
kw["sl_pct"] = sl_pct
|
||||
kw["tp_pct"] = tp_pct
|
||||
return SkyhookParams(**kw)
|
||||
|
||||
|
||||
def metrics(p):
|
||||
"""FULL/HOLD/DD min-asset + fee@0.30 + trades, both assets."""
|
||||
pa = {}
|
||||
fee_ok = True
|
||||
for a in ("BTC", "ETH"):
|
||||
r = sk.run_asset(a, p)
|
||||
# fee sweep at 0.30%
|
||||
rf = sk.run_asset(a, p, fee_rt=0.003)
|
||||
fee_ok = fee_ok and (rf["full"]["sharpe"] > 0)
|
||||
pa[a] = dict(full=r["full"], hold=r["holdout"], yearly=r["yearly"],
|
||||
fee30=rf["full"]["sharpe"])
|
||||
minFull = min(pa[a]["full"]["sharpe"] for a in pa)
|
||||
minHold = min(pa[a]["hold"]["sharpe"] for a in pa)
|
||||
minTr = min(pa[a]["full"]["n_trades"] for a in pa)
|
||||
maxDD = max(pa[a]["full"]["maxdd"] for a in pa)
|
||||
return dict(pa=pa, minFull=minFull, minHold=minHold, minTr=minTr, maxDD=maxDD, fee_ok=fee_ok)
|
||||
|
||||
|
||||
def grade_of(m):
|
||||
enough = m["minTr"] >= 20
|
||||
if enough and m["minFull"] >= 0.5 and m["minHold"] >= 0.2 and m["fee_ok"]:
|
||||
return "PASS"
|
||||
if enough and m["minFull"] >= 0.3 and m["minHold"] >= 0.0:
|
||||
return "WEAK"
|
||||
return "FAIL"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# ---- STAGE 1: coarse ATR grid (the family core) ----
|
||||
sl_grid = [1.75, 2.0, 2.25, 2.5]
|
||||
tp_grid = [5.0, 6.0, 7.0, 8.0]
|
||||
rows = []
|
||||
print("##### STAGE 1: ATR grid (sl_atr x tp_atr) #####")
|
||||
print(f"{'sl':>5} {'tp':>4} | {'minFull':>8} {'minHold':>8} {'maxDD%':>7} {'minTr':>6} "
|
||||
f"{'BTC_DD':>7} {'ETH_DD':>7} {'feeOK':>5} {'grade':>5}")
|
||||
for sl in sl_grid:
|
||||
for tp in tp_grid:
|
||||
p = mk(sl_atr=sl, tp_atr=tp, exit_mode="atr")
|
||||
m = metrics(p)
|
||||
g = grade_of(m)
|
||||
bdd = m["pa"]["BTC"]["full"]["maxdd"]
|
||||
edd = m["pa"]["ETH"]["full"]["maxdd"]
|
||||
rows.append((sl, tp, "atr", m, g))
|
||||
print(f"{sl:>5} {tp:>4} | {m['minFull']:>+8.2f} {m['minHold']:>+8.2f} "
|
||||
f"{m['maxDD']*100:>7.1f} {m['minTr']:>6} {bdd*100:>7.1f} {edd*100:>7.1f} "
|
||||
f"{str(m['fee_ok']):>5} {g:>5}")
|
||||
|
||||
# ---- STAGE 2: pct exit grid (a few sensible RR pairs ~ matching ATR ratios) ----
|
||||
# ATR LTF ~ a few % of price; pct exit gives a HARD dollar cap on DD per trade.
|
||||
print("\n##### STAGE 2: PCT exit grid (sl_pct x tp_pct) #####")
|
||||
print(f"{'slP':>6} {'tpP':>6} | {'minFull':>8} {'minHold':>8} {'maxDD%':>7} {'minTr':>6} "
|
||||
f"{'BTC_DD':>7} {'ETH_DD':>7} {'feeOK':>5} {'grade':>5}")
|
||||
pct_pairs = [(0.02, 0.06), (0.025, 0.07), (0.03, 0.075), (0.03, 0.09),
|
||||
(0.035, 0.10), (0.04, 0.10),
|
||||
# dense neighbourhood around the DD<30 winner (0.025,0.07) to prove a plateau:
|
||||
(0.0225, 0.065), (0.0225, 0.07), (0.0225, 0.075),
|
||||
(0.025, 0.0625), (0.025, 0.065), (0.025, 0.075), (0.025, 0.08),
|
||||
(0.0275, 0.065), (0.0275, 0.07), (0.0275, 0.075)]
|
||||
for slp, tpp in pct_pairs:
|
||||
p = mk(exit_mode="pct", sl_pct=slp, tp_pct=tpp)
|
||||
m = metrics(p)
|
||||
g = grade_of(m)
|
||||
bdd = m["pa"]["BTC"]["full"]["maxdd"]
|
||||
edd = m["pa"]["ETH"]["full"]["maxdd"]
|
||||
rows.append((slp, tpp, "pct", m, g))
|
||||
print(f"{slp:>6} {tpp:>6} | {m['minFull']:>+8.2f} {m['minHold']:>+8.2f} "
|
||||
f"{m['maxDD']*100:>7.1f} {m['minTr']:>6} {bdd*100:>7.1f} {edd*100:>7.1f} "
|
||||
f"{str(m['fee_ok']):>5} {g:>5}")
|
||||
|
||||
# ---- Pick best: DD<30, minHold>=0.7, grade!=FAIL; tie-break by minHold then minFull ----
|
||||
def ok_dd(r):
|
||||
return r[3]["maxDD"] < 0.30 and r[3]["minHold"] >= 0.70 and r[4] != "FAIL"
|
||||
cands = [r for r in rows if ok_dd(r)]
|
||||
if not cands:
|
||||
# relax: DD<30 and minHold>=0.65
|
||||
cands = [r for r in rows if r[3]["maxDD"] < 0.30 and r[3]["minHold"] >= 0.65 and r[4] != "FAIL"]
|
||||
relaxed = True
|
||||
else:
|
||||
relaxed = False
|
||||
if not cands:
|
||||
# fall back to lowest DD among non-FAIL with decent hold
|
||||
cands = [r for r in rows if r[4] != "FAIL"]
|
||||
# rank: among DD<30 cells, maximize a balanced score (minHold + minFull) so we don't pick a
|
||||
# low-DD-but-weak-Sharpe corner. DD is already gated < 0.30 above, so optimise value next.
|
||||
cands_sorted = sorted(cands, key=lambda r: -(r[3]["minHold"] + r[3]["minFull"]))
|
||||
best = cands_sorted[0]
|
||||
print(f"\n##### BEST PICK (relaxed={relaxed if cands else 'fallback'}): "
|
||||
f"{'sl/tp' if best[2]=='atr' else 'slP/tpP'}=({best[0]},{best[1]}) mode={best[2]} #####")
|
||||
|
||||
# Build best params
|
||||
if best[2] == "atr":
|
||||
bp = mk(sl_atr=best[0], tp_atr=best[1], exit_mode="atr")
|
||||
best_cfg = dict(ptn_n=45, sl_atr=best[0], tp_atr=best[1], uscitalong=24,
|
||||
uscitashort=16, vola_lo=35.0, vola_hi=95.0, vol_lo=0.0,
|
||||
exit_mode="atr")
|
||||
else:
|
||||
bp = mk(exit_mode="pct", sl_pct=best[0], tp_pct=best[1])
|
||||
best_cfg = dict(ptn_n=45, sl_pct=best[0], tp_pct=best[1], uscitalong=24,
|
||||
uscitashort=16, vola_lo=35.0, vola_hi=95.0, vol_lo=0.0,
|
||||
exit_mode="pct")
|
||||
|
||||
# ---- Full verification of best: study + causality + marginal ----
|
||||
print("\n##### FULL STUDY of BEST #####")
|
||||
rep = sk.study("SKH2_TPSL_DD-BEST", bp)
|
||||
print(sk.fmt(rep))
|
||||
caus = sk.causality(bp, "BTC")
|
||||
caus_eth = sk.causality(bp, "ETH")
|
||||
print(f"\ncausality BTC: {caus}")
|
||||
print(f"causality ETH: {caus_eth}")
|
||||
mg = sk.marginal(bp)
|
||||
|
||||
m = best[3]
|
||||
g = rep["verdict"]["grade"]
|
||||
earns = (g != "FAIL" and mg.get("marginal_verdict") == "ADDS"
|
||||
and bool(mg.get("robust_oos")) and not bool(mg.get("is_hedge")))
|
||||
w25 = mg.get("blends", {}).get("w25", {})
|
||||
w50 = mg.get("blends", {}).get("w50", {})
|
||||
beats = (earns and m["maxDD"] < 0.30
|
||||
and (w25.get("uplift_hold") or -9) >= 0.55 and m["minHold"] >= 0.65)
|
||||
|
||||
print("\n========== FINAL BLOCK ==========")
|
||||
print(f"best_cfg = {best_cfg}")
|
||||
print(f"exit_mode = {best[2]}")
|
||||
print(f"minFull = {m['minFull']:+.3f}")
|
||||
print(f"minHold = {m['minHold']:+.3f}")
|
||||
print(f"max_dd (BTC/ETH) = {m['maxDD']:.4f} (BTC {m['pa']['BTC']['full']['maxdd']:.4f} / "
|
||||
f"ETH {m['pa']['ETH']['full']['maxdd']:.4f})")
|
||||
print(f"n_trades_min = {m['minTr']}")
|
||||
print(f"fee@0.30 OK = {m['fee_ok']} (BTC {m['pa']['BTC']['fee30']:+.2f} / "
|
||||
f"ETH {m['pa']['ETH']['fee30']:+.2f})")
|
||||
print(f"causality_ok = {caus['ok'] and caus_eth['ok']} "
|
||||
f"(BTC mism={caus['mismatches']} ETH mism={caus_eth['mismatches']})")
|
||||
print(f"grade = {g}")
|
||||
print("--- marginal vs TP01 ---")
|
||||
print(f"corr_full = {mg.get('corr_full')}")
|
||||
print(f"corr_hold = {mg.get('corr_hold')}")
|
||||
print(f"marginal_verdict = {mg.get('marginal_verdict')}")
|
||||
print(f"has_insample_edge = {mg.get('has_insample_edge')}")
|
||||
print(f"is_hedge = {mg.get('is_hedge')}")
|
||||
print(f"robust_oos = {mg.get('robust_oos')}")
|
||||
print(f"multicut_persist = {mg.get('multicut_persistent')}")
|
||||
print(f"clean_year_uplift = {mg.get('clean_year_uplift')}")
|
||||
print(f"jackknife_min_upl = {mg.get('jackknife_min_uplift')}")
|
||||
print(f"cand_insample_sh = {mg.get('cand_insample_sharpe')}")
|
||||
print(f"blend w25 = {w25}")
|
||||
print(f"blend w50 = {w50}")
|
||||
print(f"earns_slot = {earns}")
|
||||
print(f"BEATS_WINNER = {beats}")
|
||||
|
||||
# ---- plateau report: neighbors of best in the same mode ----
|
||||
print("\n##### PLATEAU (neighbors of best) #####")
|
||||
nbrs = [r for r in rows if r[2] == best[2]]
|
||||
nbrs_sorted = sorted(nbrs, key=lambda r: (r[3]["maxDD"]))
|
||||
for r in nbrs_sorted[:8]:
|
||||
tag = f"({r[0]},{r[1]})"
|
||||
print(f" {r[2]} {tag:>14}: DD={r[3]['maxDD']*100:5.1f}% minFull={r[3]['minFull']:+.2f} "
|
||||
f"minHold={r[3]['minHold']:+.2f} grade={r[4]}")
|
||||
Reference in New Issue
Block a user