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

226 lines
11 KiB
Python

"""SKH2_CHANDE_WIN — DD-reduction wave: re-tune indicator WINDOWS (Chande/ATR) for DD.
Family task: smoother indicators -> more stable regime -> potentially lower standalone maxDD.
We hold the VERIFIED V2 winner's pattern/exits/bands FIXED and sweep ONLY the windows:
n_vola, n_volume in {7,13,21,34}
atr_win in {10,14,21}
ltf_atr_win in {10,14,21}
Everything is expressible via SkyhookParams -> the SHARED honest harness sk.study() applies
the exact leak-free FULL+HOLDOUT+fee-sweep+per-year machinery, and sk.causality / sk.marginal
give the same comparable numbers as every other agent.
WINNER (baseline to beat):
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 ; standalone DD BTC 34% / ETH 31% (>30% = the problem).
GOAL: max_dd < 0.30 while keeping minHold >= ~0.70 and earns_slot True, blend w25 uplift_hold >= 0.55.
"""
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
# ---- fixed winner spine (pattern / exits / bands) --------------------------
FIXED = dict(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)
def mk(n_vola, n_volume, atr_win, ltf_atr_win):
return SkyhookParams(n_vola=n_vola, n_volume=n_volume, atr_win=atr_win,
ltf_atr_win=ltf_atr_win, **FIXED)
def cheap_eval(p):
"""Fast standalone screen: FULL+HOLD on BTC&ETH only (no fee-sweep/marginal)."""
rb = sk.run_asset("BTC", p)
re = sk.run_asset("ETH", p)
min_full = min(rb["full"]["sharpe"], re["full"]["sharpe"])
min_hold = min(rb["holdout"]["sharpe"], re["holdout"]["sharpe"])
max_dd = max(rb["full"]["maxdd"], re["full"]["maxdd"])
min_tr = min(rb["full"]["n_trades"], re["full"]["n_trades"])
return dict(min_full=min_full, min_hold=min_hold, max_dd=max_dd, min_tr=min_tr,
btc_dd=rb["full"]["maxdd"], eth_dd=re["full"]["maxdd"],
btc_hold=rb["holdout"]["sharpe"], eth_hold=re["holdout"]["sharpe"])
def earns_slot(rep, marg):
return (rep["verdict"]["grade"] != "FAIL"
and marg.get("marginal_verdict") == "ADDS"
and bool(marg.get("robust_oos"))
and not bool(marg.get("is_hedge")))
def beats_winner(rep, marg, ev):
es = earns_slot(rep, marg)
w25 = marg.get("blends", {}).get("w25", {}).get("uplift_hold")
mh = rep["verdict"]["min_asset_holdout_sharpe"]
return bool(es and ev["max_dd"] < 0.30 and (w25 is not None and w25 >= 0.55) and mh >= 0.65)
# ---- WINNER reference (so DD comparison is apples-to-apples in THIS harness) ----
def winner_params():
return SkyhookParams(**FIXED)
if __name__ == "__main__":
print("########## STAGE 1: cheap window screen (FULL+HOLD+DD, BTC&ETH) ##########")
# winner reference in this harness
wev = cheap_eval(winner_params())
print(f"[WINNER ref] minFull={wev['min_full']:+.2f} minHold={wev['min_hold']:+.2f} "
f"maxDD={wev['max_dd']*100:.0f}% (BTC {wev['btc_dd']*100:.0f}% / ETH {wev['eth_dd']*100:.0f}%) "
f"minTr={wev['min_tr']}")
n_vola_grid = [7, 13, 21, 34]
n_volume_grid = [7, 13, 21, 34]
atr_grid = [10, 14, 21]
ltf_grid = [10, 14, 21]
rows = []
for nva, nvo, aw, law in itertools.product(n_vola_grid, n_volume_grid, atr_grid, ltf_grid):
p = mk(nva, nvo, aw, law)
ev = cheap_eval(p)
rows.append((nva, nvo, aw, law, ev))
# Sort by lowest DD among those that keep some hold-out edge & enough trades
def keyf(r):
ev = r[4]
return ev["max_dd"]
viable = [r for r in rows if r[4]["min_tr"] >= 20]
viable.sort(key=keyf)
print(f"\n--- {len(rows)} configs screened. Top 15 by LOWEST standalone maxDD "
f"(min_tr>=20) ---")
print(f"{'nva':>4}{'nvo':>4}{'aw':>4}{'law':>5} {'maxDD':>7} {'btcDD':>6} {'ethDD':>6} "
f"{'minFull':>8} {'minHold':>8} {'minTr':>6}")
for nva, nvo, aw, law, ev in viable[:15]:
print(f"{nva:>4}{nvo:>4}{aw:>4}{law:>5} {ev['max_dd']*100:>6.1f}% "
f"{ev['btc_dd']*100:>5.1f}% {ev['eth_dd']*100:>5.1f}% "
f"{ev['min_full']:>+8.2f} {ev['min_hold']:>+8.2f} {ev['min_tr']:>6}")
# STAGE-1 LEARNING (from broad probes): no window combo gets BOTH BTC&ETH sub-30%
# (BTC & ETH DD move in OPPOSITE directions vs n_vola/atr_win). The best DD-CUT that
# also keeps hold-out is the SLOWER-INDICATOR corner. Study the lowest-DD configs that
# still keep minHold>=0.50 (the real DD/hold tradeoff frontier), plus a couple extras
# found by the broad probe (n_vola=13, slower atr_win/ltf_atr_win).
extra = [(13, 13, 18, 18), (13, 13, 14, 18), (13, 13, 21, 18)] # (nva,nvo,aw,law)
# candidates that cut DD while keeping hold-out
cands = [r for r in viable
if r[4]["min_hold"] >= 0.50 and r[4]["min_full"] >= 0.50]
cands.sort(key=lambda r: r[4]["max_dd"]) # lowest DD first
study_keys = set()
study_list = []
for r in cands[:5]:
k = (r[0], r[1], r[2], r[3])
if k not in study_keys:
study_keys.add(k); study_list.append(r)
# ensure the broad-probe extras are studied (they may not be on the coarse grid)
for nva, nvo, aw, law in extra:
k = (nva, nvo, aw, law)
if k not in study_keys:
ev = cheap_eval(mk(nva, nvo, aw, law))
study_keys.add(k); study_list.append((nva, nvo, aw, law, ev))
if not study_list:
study_list = viable[:4]
print(f"\n########## STAGE 2: FULL study + causality + marginal on "
f"{len(study_list)} candidate(s) ##########")
results = []
for nva, nvo, aw, law, ev in study_list:
p = mk(nva, nvo, aw, law)
name = f"CW_nva{nva}_nvo{nvo}_aw{aw}_law{law}"
rep = sk.study(name, p)
caus_b = sk.causality(p, "BTC")
caus_e = sk.causality(p, "ETH")
marg = sk.marginal(p)
caus_ok = bool(caus_b["ok"] and caus_e["ok"])
es = earns_slot(rep, marg)
bw = beats_winner(rep, marg, ev) and caus_ok
w25 = marg.get("blends", {}).get("w25", {}).get("uplift_hold")
w50 = marg.get("blends", {}).get("w50", {})
results.append(dict(name=name, p=p, rep=rep, marg=marg, ev=ev,
caus_ok=caus_ok, es=es, bw=bw, w25=w25, w50=w50,
cfg=dict(n_vola=nva, n_volume=nvo, atr_win=aw, ltf_atr_win=law)))
print("\n" + sk.fmt(rep))
print(f" causality BTC={caus_b} ETH={caus_e}")
print(f" marginal: verdict={marg.get('marginal_verdict')} corr_full={marg.get('corr_full')} "
f"has_insample_edge={marg.get('has_insample_edge')} is_hedge={marg.get('is_hedge')} "
f"robust_oos={marg.get('robust_oos')} multicut_persistent={marg.get('multicut_persistent')}")
print(f" clean_year_uplift={marg.get('clean_year_uplift')} "
f"blend_w25_uplift_hold={w25} w50={w50}")
print(f" >> earns_slot={es} beats_winner={bw} standaloneDD={ev['max_dd']*100:.1f}%")
# ---- pick best: prefer beats_winner; else lowest DD among earns_slot; else lowest DD ----
def rank(r):
# higher is better. Priority: (1) beats_winner, (2) earns_slot, then PORTFOLIO VALUE
# (blend w25 uplift_hold + min-asset hold-out) which the wave objectives (2)&(3) reward,
# with DD as a final tiebreak. NOTE: no window combo reaches max_dd<0.30 (DD wall is
# structural: BTC & ETH DD move in OPPOSITE directions vs the vola window) so we report
# the strongest earns_slot config rather than chasing an unreachable DD gate.
w25 = r["w25"] if r["w25"] is not None else -9
return (1 if r["bw"] else 0,
1 if r["es"] else 0,
round(w25, 3),
r["rep"]["verdict"]["min_asset_holdout_sharpe"],
-r["ev"]["max_dd"])
if results:
best = sorted(results, key=rank, reverse=True)[0]
b = best
rep = b["rep"]; marg = b["marg"]; ev = b["ev"]
v = rep["verdict"]
print("\n" + "=" * 78)
print("FINAL BEST CONFIG (CHANDE_WIN family)")
print("=" * 78)
print(f" config = {b['cfg']} (+ fixed winner spine {FIXED})")
print(f" name = {b['name']}")
print(f" minFull = {v['min_asset_full_sharpe']:+.3f}")
print(f" minHold = {v['min_asset_holdout_sharpe']:+.3f} "
f"(BTC {rep['per_asset']['BTC']['holdout']['sharpe']:+.2f} / "
f"ETH {rep['per_asset']['ETH']['holdout']['sharpe']:+.2f})")
print(f" standalone max_dd = {ev['max_dd']:.4f} "
f"(BTC {ev['btc_dd']:.4f} / ETH {ev['eth_dd']:.4f})")
print(f" n_trades_min = {v['min_trades']}")
print(f" fee_survives 0.30%= {v['fee_survives']}")
print(f" causality_ok = {b['caus_ok']}")
print(f" grade = {v['grade']}")
print(f" --- marginal vs TP01 ---")
print(f" corr_full = {marg.get('corr_full')}")
print(f" marginal_verdict = {marg.get('marginal_verdict')}")
print(f" has_insample_edge = {marg.get('has_insample_edge')}")
print(f" is_hedge = {marg.get('is_hedge')}")
print(f" robust_oos = {marg.get('robust_oos')}")
print(f" multicut_persist = {marg.get('multicut_persistent')}")
print(f" clean_year_uplift = {marg.get('clean_year_uplift')}")
print(f" blend w25 uplift_hold = {b['w25']}")
print(f" blend w50 = {b['w50']}")
print(f" earns_slot = {b['es']}")
print(f" BEATS_WINNER = {b['bw']}")
print("=" * 78)
# machine-readable line for the harness operator
import json
out = dict(
family="CHANDE_WIN", best_config=b["cfg"], fixed=FIXED, name=b["name"],
grade=v["grade"], min_full=v["min_asset_full_sharpe"],
min_hold=v["min_asset_holdout_sharpe"], max_dd=ev["max_dd"],
btc_dd=ev["btc_dd"], eth_dd=ev["eth_dd"], n_trades_min=v["min_trades"],
fee_survives=v["fee_survives"], causality_ok=b["caus_ok"],
corr_full=marg.get("corr_full"), marginal_verdict=marg.get("marginal_verdict"),
has_insample_edge=marg.get("has_insample_edge"), is_hedge=marg.get("is_hedge"),
robust_oos=marg.get("robust_oos"),
multicut_persistent=marg.get("multicut_persistent"),
clean_year_uplift=marg.get("clean_year_uplift"),
blend_w25_uplift_hold=b["w25"], earns_slot=b["es"], beats_winner=b["bw"],
)
print("RESULT_JSON " + json.dumps(out, default=str))