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,102 @@
|
||||
"""SKH_R_PCTL final: verify top configs with sk.study + marginal, refine for minFull/DD."""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/skyhook")
|
||||
import numpy as np, pandas as pd
|
||||
import skyhooklib as sk
|
||||
from src.backtest.harness import backtest_signals
|
||||
from src.strategies import skyhook as S
|
||||
from src.strategies.skyhook import SkyhookParams, HTF_MIN
|
||||
|
||||
# import the structural builder from the sweep script
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"skr", "/opt/docker/PythagorasGoal/scripts/research/skyhook/runs/SKH_R_PCTL.py")
|
||||
skr = importlib.util.module_from_spec(spec)
|
||||
# avoid running its __main__
|
||||
import builtins
|
||||
_orig_name = "__main__"
|
||||
spec.loader.exec_module(skr) # defines functions; __main__ guard prevents the sweep
|
||||
|
||||
HOLDOUT = sk.HOLDOUT
|
||||
FEE = sk.FEE_RT
|
||||
|
||||
|
||||
def study_struct(name, cfg, p):
|
||||
"""sk.study-equivalent for our structural variant: FULL+HOLD+fee-sweep+per-year BOTH assets."""
|
||||
per_asset = {}
|
||||
fee_ok_all = True
|
||||
for a in ("BTC", "ETH"):
|
||||
ltf, htf = sk.frames(a)
|
||||
ent = skr.pctl_entries(ltf, htf, p, **cfg)
|
||||
m = backtest_signals(ltf, ent, fee_rt=FEE, leverage=1.0, asset=a, tf="230m")
|
||||
eq = m.equity
|
||||
idx = pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True))
|
||||
hmask = np.asarray(idx >= HOLDOUT)
|
||||
full = dict(sharpe=round(m.sharpe, 3), ret=round(m.net_return, 4), maxdd=round(m.max_dd, 4),
|
||||
n_trades=int(m.n_trades), win_rate=round(m.win_rate, 1))
|
||||
hold = skr._split(eq, idx, hmask)
|
||||
sweep = {}
|
||||
for f in (0.0, 0.001, 0.002, 0.003):
|
||||
mf = backtest_signals(ltf, ent, fee_rt=f, leverage=1.0, asset=a, tf="230m")
|
||||
sweep[f"{f*100:.2f}%"] = round(mf.sharpe, 3)
|
||||
fee_ok_all = fee_ok_all and (sweep["0.30%"] > 0)
|
||||
per_asset[a] = dict(full=full, hold=hold, yearly={int(y): round(v, 4) for y, v in m.yearly.items()},
|
||||
fee_sweep=sweep)
|
||||
mf = min(per_asset[a]["full"]["sharpe"] for a in per_asset)
|
||||
mh = min(per_asset[a]["hold"]["sharpe"] for a in per_asset)
|
||||
mt = min(per_asset[a]["full"]["n_trades"] for a in per_asset)
|
||||
mdd = max(per_asset[a]["full"]["maxdd"] for a in per_asset)
|
||||
grade = "PASS" if (mt >= 20 and mf >= 0.5 and mh >= 0.2 and fee_ok_all) else \
|
||||
("WEAK" if (mt >= 20 and mf >= 0.3 and mh >= 0.0) else "FAIL")
|
||||
print(f"\n=== {name} -> {grade} (minFull={mf:+.2f} minHold={mh:+.2f} minTr={mt} maxDD={mdd*100:.0f}% feeOK={fee_ok_all})")
|
||||
for a in per_asset:
|
||||
pa = per_asset[a]
|
||||
yr = " ".join(f"{y}:{r*100:+.0f}%" for y, r in pa["yearly"].items())
|
||||
print(f" {a}: FULL Sh={pa['full']['sharpe']:+.2f} ret={pa['full']['ret']*100:+.0f}% DD={pa['full']['maxdd']*100:.0f}%"
|
||||
f" n={pa['full']['n_trades']} wr={pa['full']['win_rate']:.0f}% | HOLD Sh={pa['hold']['sharpe']:+.2f} ret={pa['hold']['ret']*100:+.0f}%")
|
||||
print(f" fee: " + " ".join(f"{k}={v:+.2f}" for k, v in pa["fee_sweep"].items()))
|
||||
print(f" yr: {yr}")
|
||||
return dict(grade=grade, minFull=mf, minHold=mh, minTr=mt, maxDD=mdd, fee_ok=fee_ok_all, per_asset=per_asset)
|
||||
|
||||
|
||||
def marginal_struct(cfg, p):
|
||||
import altlib as al
|
||||
def daily(a):
|
||||
ltf, htf = sk.frames(a)
|
||||
ent = skr.pctl_entries(ltf, htf, p, **cfg)
|
||||
m = backtest_signals(ltf, ent, fee_rt=FEE, leverage=1.0, asset=a, tf="230m")
|
||||
s = pd.Series(m.equity, index=pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True)))
|
||||
return s.resample("1D").last().ffill().pct_change().dropna()
|
||||
sb, se = daily("BTC"), daily("ETH")
|
||||
J = pd.concat({"BTC": sb, "ETH": se}, axis=1, join="inner").fillna(0.0)
|
||||
cand = 0.5 * J["BTC"] + 0.5 * J["ETH"]
|
||||
return al.marginal_vs_tp01(cand)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
p = skr.v1_like_params()
|
||||
|
||||
# Candidate A: best minHold (exp_volaHi_volHi) -- minFull 0.53
|
||||
cfgA = dict(vola_win=None, vol_win=None, vola_lo=0.35, vola_hi=0.95, vol_lo=0.40, vol_hi=1.0)
|
||||
# Candidate B: best minFull + lower DD (exp_volaLo_vol0) -- minFull 0.70, DD 39%
|
||||
cfgB = dict(vola_win=None, vol_win=None, vola_lo=0.0, vola_hi=0.70, vol_lo=0.0, vol_hi=1.0)
|
||||
|
||||
# Refinements to lift minFull on A while keeping hold-out: tighten vola band / add small vol floor
|
||||
cfgC = dict(vola_win=None, vol_win=None, vola_lo=0.10, vola_hi=0.80, vol_lo=0.30, vol_hi=1.0)
|
||||
# B + modest vol floor to keep DD low but lift hold
|
||||
cfgD = dict(vola_win=None, vol_win=None, vola_lo=0.0, vola_hi=0.70, vol_lo=0.30, vol_hi=1.0)
|
||||
|
||||
rA = study_struct("PCTL-A exp_volaHi_volHi", cfgA, p)
|
||||
rB = study_struct("PCTL-B exp_volaLo_vol0", cfgB, p)
|
||||
rC = study_struct("PCTL-C exp_volaLoMid_volFloor", cfgC, p)
|
||||
rD = study_struct("PCTL-D exp_volaLo_volFloor", cfgD, p)
|
||||
|
||||
print("\n\n##### MARGINAL vs TP01 #####")
|
||||
for tag, cfg, r in [("A", cfgA, rA), ("B", cfgB, rB), ("C", cfgC, rC), ("D", cfgD, rD)]:
|
||||
mg = marginal_struct(cfg, p)
|
||||
print(f"[{tag}] grade={r['grade']} minFull={r['minFull']:+.2f} minHold={r['minHold']:+.2f} DD={r['maxDD']*100:.0f}%"
|
||||
f" | corr_full={mg.get('corr_full')} upliftHold={mg.get('blends',{}).get('w25',{}).get('uplift_hold')}"
|
||||
f" verdict={mg.get('marginal_verdict')} robust_oos={mg.get('robust_oos')}"
|
||||
f" insample_edge={mg.get('has_insample_edge')} hedge={mg.get('is_hedge')}"
|
||||
f" cleanYr={mg.get('clean_year_uplift')}")
|
||||
Reference in New Issue
Block a user