Files
Adriano Dal Pastro de72e3ce1f 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>
2026-06-23 16:10:38 +00:00

164 lines
8.5 KiB
Python

"""SKH2_REGIME_TIGHT — DD-reduction wave, family: tighter regime selectivity.
Hypothesis: make the regime band MORE selective (narrow vola band, add a volume floor)
so only the cleanest setups trade -> fewer, higher-quality entries -> lower standalone DD,
while keeping the winner's asymmetric exits (sl 2.5 / tp 7.0, uscitalong 24 / short 16).
Baseline winner to beat (V2):
SkyhookParams(ptn_n=45, sl_atr=2.5, tp_atr=7.0, uscitalong=24, uscitashort=16,
vola_lo=35, vola_hi=95, vol_lo=0)
minFull +0.83 minHold +0.81 ; DD BTC 34% / ETH 31% (the unmet goal: DD<30%) ;
marginal ADDS, corr_full 0.05, blend w25 uplift_hold +0.58, w50 full 1.59 / hold 1.04 / DD 12.5%.
BEATS-THE-WINNER iff: earns_slot AND max_dd<0.30 AND blend_w25_uplift_hold>=0.55 AND min_hold>=0.65.
Everything is param-based (no new feature) -> sk.study/sk.marginal/sk.causality are directly valid.
"""
from __future__ import annotations
import sys
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/skyhook")
import itertools
import skyhooklib as sk
from src.strategies.skyhook import SkyhookParams
# --- the winner's exits, frozen ----------------------------------------------------------
EXITS = dict(ptn_n=45, sl_atr=2.5, tp_atr=7.0, uscitalong=24, uscitashort=16)
def mk(vola_lo, vola_hi, vol_lo, **extra):
return SkyhookParams(vola_lo=vola_lo, vola_hi=vola_hi, vol_lo=vol_lo, **EXITS, **extra)
def earns_slot(rep, mg):
return (rep["verdict"]["grade"] != "FAIL"
and mg.get("marginal_verdict") == "ADDS"
and bool(mg.get("robust_oos"))
and not bool(mg.get("is_hedge")))
def beats(rep, mg, max_dd):
es = earns_slot(rep, mg)
upl = mg.get("blends", {}).get("w25", {}).get("uplift_hold")
mh = rep["verdict"]["min_asset_holdout_sharpe"]
return (es and max_dd < 0.30 and (upl is not None and upl >= 0.55) and mh >= 0.65)
def summarize(name, p):
rep = sk.study(name, p)
v = rep["verdict"]
dd = max(rep["per_asset"][a]["full"]["maxdd"] for a in rep["per_asset"])
nt = v["min_trades"]
print(sk.fmt(rep))
print(f" >> maxDD(BTC,ETH) = {dd*100:.1f}% minTrades={nt}")
return rep, dd
if __name__ == "__main__":
# ---- STAGE 1: coarse grid over the family levers -----------------------------------
vola_los = [35.0, 40.0, 45.0, 50.0] # 35 = winner; 40/45/50 = tighter floor
vola_his = [85.0, 90.0, 95.0] # 95 = winner; 85/90 = clip blow-off harder
vol_los = [0.0, 30.0, 40.0, 50.0] # 0 = winner; floor = require live volume
rows = []
print("########## STAGE 1: coarse DD scan (study, both assets) ##########")
for vlo, vhi, vol in itertools.product(vola_los, vola_his, vol_los):
p = mk(vlo, vhi, vol)
rep = sk.study(f"vlo{vlo:.0f}_vhi{vhi:.0f}_vol{vol:.0f}", p)
v = rep["verdict"]
dd = max(rep["per_asset"][a]["full"]["maxdd"] for a in rep["per_asset"])
nt = v["min_trades"]
ddb = rep["per_asset"]["BTC"]["full"]["maxdd"]
dde = rep["per_asset"]["ETH"]["full"]["maxdd"]
rows.append(dict(vlo=vlo, vhi=vhi, vol=vol, grade=v["grade"],
mf=v["min_asset_full_sharpe"], mh=v["min_asset_holdout_sharpe"],
dd=dd, ddb=ddb, dde=dde, nt=nt, fee=v["fee_survives"]))
print(f" vlo{vlo:.0f} vhi{vhi:.0f} vol{vol:.0f}: {v['grade']:4s} "
f"mF={v['min_asset_full_sharpe']:+.2f} mH={v['min_asset_holdout_sharpe']:+.2f} "
f"DD={dd*100:4.1f}% (B{ddb*100:.0f}/E{dde*100:.0f}) nT={nt:3d} feeOK={v['fee_survives']}")
# ---- rank: candidates with DD<30%, minTrades>=20, fee OK, holdout>=0.65 -------------
print("\n########## STAGE 1 RANK (filter DD<30, nT>=20, fee OK, mH>=0.65, not FAIL) ##########")
cand = [r for r in rows if r["dd"] < 0.30 and r["nt"] >= 20 and r["fee"]
and r["mh"] >= 0.65 and r["grade"] != "FAIL"]
cand.sort(key=lambda r: (-r["mh"], r["dd"])) # prefer high holdout then low DD
if not cand:
print(" (none met all hard filters; falling back to lowest-DD with nT>=20 & not FAIL)")
cand = [r for r in rows if r["nt"] >= 20 and r["grade"] != "FAIL"]
cand.sort(key=lambda r: (r["dd"], -r["mh"]))
for r in cand[:8]:
print(f" vlo{r['vlo']:.0f} vhi{r['vhi']:.0f} vol{r['vol']:.0f}: {r['grade']} "
f"mF={r['mf']:+.2f} mH={r['mh']:+.2f} DD={r['dd']*100:.1f}% nT={r['nt']}")
# ---- STAGE 2: full diligence (causality + marginal) on top few ---------------------
print("\n########## STAGE 2: causality + marginal on top candidates ##########")
best = None
top = cand[:4]
for r in top:
p = mk(r["vlo"], r["vhi"], r["vol"])
name = f"TIGHT vlo{r['vlo']:.0f}_vhi{r['vhi']:.0f}_vol{r['vol']:.0f}"
print(f"\n----- {name} -----")
rep, dd = summarize(name, p)
cau = sk.causality(p, "BTC")
cau_e = sk.causality(p, "ETH")
cau_ok = bool(cau["ok"] and cau_e["ok"])
mg = sk.marginal(p)
es = earns_slot(rep, mg)
bw = beats(rep, mg, dd) and cau_ok
w25 = mg.get("blends", {}).get("w25", {})
w50 = mg.get("blends", {}).get("w50", {})
print(f" causality BTC={cau} ETH={cau_e} -> ok={cau_ok}")
print(f" marginal: verdict={mg.get('marginal_verdict')} corr_full={mg.get('corr_full')} "
f"corr_hold={mg.get('corr_hold')} insample_edge={mg.get('has_insample_edge')} "
f"(cand_is_sh={mg.get('cand_insample_sharpe')}) hedge={mg.get('is_hedge')} "
f"robust_oos={mg.get('robust_oos')} multicut={mg.get('multicut_persistent')} "
f"clean_year_uplift={mg.get('clean_year_uplift')}")
print(f" blend w25: uplift_hold={w25.get('uplift_hold')} uplift_full={w25.get('uplift_full')} "
f"| w50: full={w50.get('full')} hold={w50.get('hold')} dd={w50.get('dd')}")
print(f" >> earns_slot={es} beats_winner={bw}")
cand_obj = dict(name=name, p=p, rep=rep, dd=dd, mg=mg, cau_ok=cau_ok,
es=es, bw=bw, w25=w25, w50=w50,
mf=rep["verdict"]["min_asset_full_sharpe"],
mh=rep["verdict"]["min_asset_holdout_sharpe"],
nt=rep["verdict"]["min_trades"],
fee=rep["verdict"]["fee_survives"])
# pick best: prefer beats_winner, then earns_slot+lowDD, then lowDD
def keyf(c):
return (c["bw"], c["es"], -c["dd"], c["mh"])
if best is None or keyf(cand_obj) > keyf(best):
best = cand_obj
# ---- FINAL BLOCK -------------------------------------------------------------------
print("\n\n==================== FINAL — REGIME_TIGHT BEST ====================")
if best is None:
print("NO CANDIDATE — no config passed even the soft filter.")
sys.exit(0)
b = best
pf = {k: getattr(b["p"], k) for k in b["p"].__dataclass_fields__}
mg = b["mg"]
print(f"config: vola_lo={b['p'].vola_lo} vola_hi={b['p'].vola_hi} vol_lo={b['p'].vol_lo} "
f"ptn_n={b['p'].ptn_n} sl_atr={b['p'].sl_atr} tp_atr={b['p'].tp_atr} "
f"uscitalong={b['p'].uscitalong} uscitashort={b['p'].uscitashort}")
print(f"minFull={b['mf']:+.3f} minHold={b['mh']:+.3f} max_dd={b['dd']*100:.1f}% "
f"n_trades_min={b['nt']} fee@0.30%OK={b['fee']} causality_ok={b['cau_ok']}")
print(f"earns_slot={b['es']} beats_winner={b['bw']}")
print("FULL marginal dict:")
for k in ("corr_full", "corr_hold", "marginal_verdict", "has_insample_edge",
"cand_insample_sharpe", "is_hedge", "robust_oos", "multicut_persistent",
"clean_year_uplift", "jackknife_min_uplift", "multicut_uplift"):
print(f" {k} = {mg.get(k)}")
print(f" blend w25: {mg.get('blends', {}).get('w25')}")
print(f" blend w50: {mg.get('blends', {}).get('w50')}")
print("PARAMS:", pf)
# machine-readable tail for me to parse
import json
print("\nRESULT_JSON " + json.dumps(dict(
family="REGIME_TIGHT", params=pf,
minFull=b["mf"], minHold=b["mh"], max_dd=b["dd"], n_trades_min=b["nt"],
fee_ok=b["fee"], causality_ok=b["cau_ok"], earns_slot=b["es"], beats=b["bw"],
corr_full=mg.get("corr_full"), marginal_verdict=mg.get("marginal_verdict"),
has_insample_edge=mg.get("has_insample_edge"), is_hedge=mg.get("is_hedge"),
robust_oos=mg.get("robust_oos"), multicut_persistent=mg.get("multicut_persistent"),
clean_year_uplift=mg.get("clean_year_uplift"),
w25_uplift_hold=mg.get("blends", {}).get("w25", {}).get("uplift_hold"),
), default=str))