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:
Adriano Dal Pastro
2026-06-23 16:10:38 +00:00
parent 8e46a62e67
commit de72e3ce1f
31 changed files with 5768 additions and 13 deletions
@@ -0,0 +1,48 @@
"""SKH_P_LOCAL_final — full study + causality + marginal on the top local-search winners,
plus a small extra pass trying to push DD<35% while keeping minHold high (ptn45 + tighter exits)."""
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 = dict(ptn_n=55, sl_atr=2.5, tp_atr=6.0, vola_lo=35.0, vola_hi=95.0, vol_lo=0.0)
def evalp(**over):
p = SkyhookParams(**{**V1, **over})
out = {a: sk.run_asset(a, p, fee_rt=sk.FEE_RT) for a in ("BTC","ETH")}
return dict(over=over,
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))
# Small extra: ptn45 + tp7 + tighter SL or exit bars to chase DD<35 with hold>=0.5
print("=== extra DD-chase around ptn45 ===")
for over in (dict(ptn_n=45, sl_atr=2.5, tp_atr=7.0, uscitalong=24, uscitashort=16),
dict(ptn_n=45, sl_atr=2.25, tp_atr=7.0, uscitalong=24, uscitashort=16),
dict(ptn_n=45, sl_atr=2.5, tp_atr=7.0, vola_lo=40.0),
dict(ptn_n=45, sl_atr=2.5, tp_atr=8.0)):
r=evalp(**over)
print(f" {over} -> minFull={r['min_full']:+.2f} minHold={r['min_hold']:+.2f} maxDD={r['max_dd']*100:.0f}% minTr={r['min_tr']}")
# WINNER candidates -> full study
WINNERS = {
"P_LOCAL_ptn45_sl2.5_tp7.0": dict(ptn_n=45, sl_atr=2.5, tp_atr=7.0), # best balanced (DD36, Full0.83, Hold0.69)
"P_LOCAL_ptn45_sl2.25_tp7.0": dict(ptn_n=45, sl_atr=2.25, tp_atr=7.0), # best hold/clean-year (DD36, Hold0.77)
}
for name, over in WINNERS.items():
p = SkyhookParams(**{**V1, **over})
print(f"\n################ STUDY {name} over={over} ################")
rep = sk.study(name, p)
print(sk.fmt(rep))
print("causality:", sk.causality(p))
mg = sk.marginal(p)
keys=("corr_full","corr_hold","marginal_verdict","has_insample_edge","is_hedge",
"robust_oos","multicut_persistent","clean_year_uplift","jackknife_min_uplift",
"cand_insample_sharpe","cand_full_sharpe")
print("marginal:", {k: mg.get(k) for k in keys})
print("blend w25 uplift_hold:", mg.get("blends",{}).get("w25",{}).get("uplift_hold"),
"uplift_full:", mg.get("blends",{}).get("w25",{}).get("uplift_full"))
print("multicut:", mg.get("multicut_uplift"))
print("AS_JSON:", sk.as_json(rep))