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,100 @@
|
||||
"""SKH_P_EXITBARS — sweep the asymmetric time-exit horizons on the V1 base.
|
||||
|
||||
V1 base: SkyhookParams(ptn_n=55, sl_atr=2.5, tp_atr=6.0, vola_lo=35, vola_hi=95, vol_lo=0.0)
|
||||
defaults uscitalong=24, uscitashort=18 -> minFull +0.69, HOLD +0.64 (BTC 0.64/ETH 0.64),
|
||||
DD ~40-49% (HIGH).
|
||||
|
||||
The asymmetry (long held longer than short) is core to Skyhook. Sweep:
|
||||
uscitalong in {16,20,24,30,40} (LTF 230m bars max-hold for longs)
|
||||
uscitashort in {10,14,18,24} (LTF 230m bars max-hold for shorts)
|
||||
|
||||
Objective (priority): maximize min-asset HOLD-OUT subject to minFull>=0.5, minTrades>=20 BOTH
|
||||
assets, fee survives 0.30%RT, causality ok. Secondary: cut standalone DD toward <30%.
|
||||
Compare to V1 (minHold +0.64, DD ~40-49%).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/skyhook")
|
||||
import skyhooklib as sk
|
||||
from src.strategies.skyhook import SkyhookParams
|
||||
|
||||
# V1 base (everything except the two exit horizons we sweep)
|
||||
BASE = dict(ptn_n=55, sl_atr=2.5, tp_atr=6.0, vola_lo=35.0, vola_hi=95.0, vol_lo=0.0)
|
||||
|
||||
UL_GRID = [16, 20, 24, 30, 40] # uscitalong
|
||||
US_GRID = [10, 14, 18, 24] # uscitashort
|
||||
|
||||
|
||||
def cell(ul, us):
|
||||
p = SkyhookParams(uscitalong=ul, uscitashort=us, **BASE)
|
||||
out = {}
|
||||
for a in ("BTC", "ETH"):
|
||||
out[a] = sk.run_asset(a, p, fee_rt=sk.FEE_RT)
|
||||
min_full = min(out[a]["full"]["sharpe"] for a in out)
|
||||
min_hold = min(out[a]["holdout"]["sharpe"] for a in out)
|
||||
min_tr = min(out[a]["full"]["n_trades"] for a in out)
|
||||
max_dd = max(out[a]["full"]["maxdd"] for a in out)
|
||||
return dict(ul=ul, us=us, min_full=min_full, min_hold=min_hold,
|
||||
min_tr=min_tr, max_dd=max_dd,
|
||||
btc_full=out["BTC"]["full"]["sharpe"], eth_full=out["ETH"]["full"]["sharpe"],
|
||||
btc_hold=out["BTC"]["holdout"]["sharpe"], eth_hold=out["ETH"]["holdout"]["sharpe"],
|
||||
btc_dd=out["BTC"]["full"]["maxdd"], eth_dd=out["ETH"]["full"]["maxdd"])
|
||||
|
||||
|
||||
print("=== SKH_P_EXITBARS sweep (V1 base ptn_n=55 sl2.5 tp6) — fee=0.10%RT ===")
|
||||
print(f"{'uL':>4} {'uS':>4} | {'minFull':>7} {'minHold':>7} {'minTr':>5} {'maxDD':>6} | "
|
||||
f"{'btcF':>5} {'ethF':>5} {'btcH':>5} {'ethH':>5} {'btcDD':>5} {'ethDD':>5}")
|
||||
results = []
|
||||
for ul in UL_GRID:
|
||||
for us in US_GRID:
|
||||
c = cell(ul, us)
|
||||
results.append(c)
|
||||
flag = " *" if (c["min_full"] >= 0.5 and c["min_tr"] >= 20) else ""
|
||||
marker = " <V1" if (ul == 24 and us == 18) else ""
|
||||
print(f"{ul:>4} {us:>4} | {c['min_full']:>+7.2f} {c['min_hold']:>+7.2f} "
|
||||
f"{c['min_tr']:>5} {c['max_dd']*100:>5.0f}% | "
|
||||
f"{c['btc_full']:>+5.2f} {c['eth_full']:>+5.2f} "
|
||||
f"{c['btc_hold']:>+5.2f} {c['eth_hold']:>+5.2f} "
|
||||
f"{c['btc_dd']*100:>4.0f}% {c['eth_dd']*100:>4.0f}%{flag}{marker}")
|
||||
|
||||
# Eligible = minFull>=0.5 AND minTrades>=20. Rank by min_hold, tie-break lower maxDD.
|
||||
elig = [c for c in results if c["min_full"] >= 0.5 and c["min_tr"] >= 20]
|
||||
print(f"\nEligible cells (minFull>=0.5, minTr>=20): {len(elig)}")
|
||||
if not elig:
|
||||
print("No eligible cell — V1 exit horizons may already be at/near the frontier.")
|
||||
sys.exit(0)
|
||||
|
||||
elig_hold = sorted(elig, key=lambda c: (-round(c["min_hold"], 3), c["max_dd"]))
|
||||
print("Top by minHold (tie-break lower maxDD):")
|
||||
for c in elig_hold[:6]:
|
||||
print(f" uL={c['ul']} uS={c['us']}: minHold={c['min_hold']:+.2f} "
|
||||
f"minFull={c['min_full']:+.2f} maxDD={c['max_dd']*100:.0f}% minTr={c['min_tr']}")
|
||||
|
||||
dd_cands = sorted(elig, key=lambda c: (c["max_dd"], -round(c["min_hold"], 3)))
|
||||
print("\nTop by lowest maxDD (DD-cut objective):")
|
||||
for c in dd_cands[:6]:
|
||||
print(f" uL={c['ul']} uS={c['us']}: maxDD={c['max_dd']*100:.0f}% "
|
||||
f"minHold={c['min_hold']:+.2f} minFull={c['min_full']:+.2f} minTr={c['min_tr']}")
|
||||
|
||||
best = elig_hold[0]
|
||||
print(f"\n=== STUDY on best-by-minHold (uL={best['ul']} uS={best['us']}) ===")
|
||||
pbest = SkyhookParams(uscitalong=best["ul"], uscitashort=best["us"], **BASE)
|
||||
rep = sk.study(f"P_EXITBARS_uL{best['ul']}_uS{best['us']}", pbest)
|
||||
print(sk.fmt(rep))
|
||||
caus = sk.causality(pbest)
|
||||
print("causality:", caus)
|
||||
mg = sk.marginal(pbest)
|
||||
print("marginal:", {k: v for k, v in mg.items()
|
||||
if k in ("corr_full", "marginal_verdict", "has_insample_edge",
|
||||
"is_hedge", "robust_oos")})
|
||||
print("blend w25 uplift_hold:", mg.get("blends", {}).get("w25", {}).get("uplift_hold"))
|
||||
print("\nAS_JSON_STUDY:", sk.as_json(rep))
|
||||
|
||||
# If the DD-cut frontier differs from the headline pick, study it too (cheap, one config).
|
||||
ddbest = dd_cands[0]
|
||||
if (ddbest["ul"], ddbest["us"]) != (best["ul"], best["us"]) and ddbest["min_hold"] >= 0.2:
|
||||
print(f"\n=== STUDY on lowest-DD eligible (uL={ddbest['ul']} uS={ddbest['us']}) ===")
|
||||
pdd = SkyhookParams(uscitalong=ddbest["ul"], uscitashort=ddbest["us"], **BASE)
|
||||
repdd = sk.study(f"P_EXITBARS_DDcut_uL{ddbest['ul']}_uS{ddbest['us']}", pdd)
|
||||
print(sk.fmt(repdd))
|
||||
print("causality:", sk.causality(pdd))
|
||||
Reference in New Issue
Block a user