de72e3ce1f
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>
226 lines
11 KiB
Python
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Ð 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Ð) ##########")
|
|
|
|
# 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Ð 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))
|