Files
PythagorasGoal/scripts/research/skyhook/runs/SKH_P_CHANDE.py
T
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

103 lines
4.8 KiB
Python

"""SKH_P_CHANDE — Chande-window sweep on the V1 base.
TASK: on the V1 base, sweep n_vola and n_volume (the Chande momentum windows that drive
BuzVola=Chande01(ATR) and BuzVolume=Chande01(volume)) in {8,13,21,34,55}. Does a different
vol/volume CYCLE window help the REGIME gate out-of-sample? Maximize min HOLD-OUT Sharpe.
V1 reference: SkyhookParams(ptn_n=55, sl_atr=2.5, tp_atr=6.0, vola_lo=35, vola_hi=95, vol_lo=0.0)
with n_vola=n_volume=13 (defaults) -> minFull +0.69, minHold +0.64 (BTC 0.64 / ETH 0.64).
We keep EVERYTHING from V1 fixed (bands, pattern, exits) and vary only the two Chande windows.
Rank by min HOLD-OUT subject to minFull>=0.5 and >=20 trades both assets, then plateau-check
the winner (neighbors in the n_vola x n_volume grid must also be good), then full study +
causality + marginal.
"""
from __future__ import annotations
import sys, itertools
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/skyhook")
import skyhooklib as sk
from src.strategies.skyhook import SkyhookParams
ASSETS = ("BTC", "ETH")
# V1 base: everything except the two Chande windows stays fixed.
BASE = dict(ptn_n=55, sl_atr=2.5, tp_atr=6.0, vola_lo=35, vola_hi=95, vol_lo=0.0)
WINDOWS = [8, 13, 21, 34, 55] # Fibonacci-ish cycle lengths for the Chande momentum
def mk(n_vola, n_volume):
return SkyhookParams(n_vola=n_vola, n_volume=n_volume, **BASE)
def eval_combo(n_vola, n_volume):
p = mk(n_vola, n_volume)
res = {a: sk.run_asset(a, p, sk.FEE_RT) for a in ASSETS}
min_full = min(res[a]["full"]["sharpe"] for a in ASSETS)
min_hold = min(res[a]["holdout"]["sharpe"] for a in ASSETS)
min_tr = min(res[a]["full"]["n_trades"] for a in ASSETS)
max_dd = max(res[a]["full"]["maxdd"] for a in ASSETS)
return dict(n_vola=n_vola, n_volume=n_volume,
min_full=min_full, min_hold=min_hold, min_tr=min_tr, max_dd=max_dd,
btc_hold=res["BTC"]["holdout"]["sharpe"], eth_hold=res["ETH"]["holdout"]["sharpe"],
btc_full=res["BTC"]["full"]["sharpe"], eth_full=res["ETH"]["full"]["sharpe"])
def main():
rows = []
combos = list(itertools.product(WINDOWS, WINDOWS))
print(f"Sweeping {len(combos)} (n_vola x n_volume) combos x 2 assets = {len(combos)*2} run_asset calls")
for nv, nvol in combos:
rows.append(eval_combo(nv, nvol))
# V1 reference cell (n_vola=n_volume=13) for explicit comparison
v1 = next(r for r in rows if r["n_vola"] == 13 and r["n_volume"] == 13)
print(f"\nV1 cell (n_vola=13,n_volume=13): minF={v1['min_full']:+.2f} minH={v1['min_hold']:+.2f} "
f"tr={v1['min_tr']} DD={v1['max_dd']*100:.0f}% (btcH={v1['btc_hold']:+.2f} ethH={v1['eth_hold']:+.2f})")
valid = [r for r in rows if r["min_full"] >= 0.5 and r["min_tr"] >= 20]
valid.sort(key=lambda r: r["min_hold"], reverse=True)
print("\n=== ALL combos by min HOLD-OUT (minF>=0.5 & tr>=20 marked OK) ===")
print(f"{'n_vola':>7}{'n_vol':>6} {'minF':>6}{'minH':>6}{'minTr':>6}{'maxDD':>7} {'btcH':>6}{'ethH':>6} ok")
for r in sorted(rows, key=lambda r: r["min_hold"], reverse=True):
ok = "OK" if (r["min_full"] >= 0.5 and r["min_tr"] >= 20) else "."
mark = " <-V1" if (r["n_vola"] == 13 and r["n_volume"] == 13) else ""
print(f"{r['n_vola']:>7}{r['n_volume']:>6} {r['min_full']:>6.2f}{r['min_hold']:>6.2f}"
f"{r['min_tr']:>6}{r['max_dd']*100:>6.0f}% {r['btc_hold']:>6.2f}{r['eth_hold']:>6.2f} {ok}{mark}")
if not valid:
print("\nNo valid combo (minFull>=0.5 & >=20 trades). Best raw by minHold:")
print(sorted(rows, key=lambda r: r["min_hold"], reverse=True)[0])
return
top = valid[0]
print(f"\n=== WINNER: n_vola={top['n_vola']} n_volume={top['n_volume']} ===")
print(f" minFull={top['min_full']:+.2f} minHold={top['min_hold']:+.2f} minTr={top['min_tr']} maxDD={top['max_dd']*100:.0f}%")
# plateau grid (full minHold table laid out as n_vola rows x n_volume cols)
def find(nv, nvol):
return next((r for r in rows if r["n_vola"] == nv and r["n_volume"] == nvol), None)
print("\n Plateau grid (minHold; rows=n_vola, cols=n_volume):")
print(" " + "".join(f"{nvol:>7}" for nvol in WINDOWS))
for nv in WINDOWS:
cells = []
for nvol in WINDOWS:
r = find(nv, nvol)
tag = "*" if (nv == top['n_vola'] and nvol == top['n_volume']) else " "
cells.append(f"{r['min_hold']:>6.2f}{tag}")
print(f" nv={nv:>3} " + "".join(cells))
# final study + causality + marginal on the winner
p = mk(top['n_vola'], top['n_volume'])
print("\n=== STUDY (winner) ===")
rep = sk.study(f"SKH_P_CHANDE_nv{top['n_vola']}_nvol{top['n_volume']}", p)
print(sk.fmt(rep))
print("\ncausality:", sk.causality(p))
print("\nmarginal:", sk.marginal(p))
print("\nas_json:", sk.as_json(rep))
if __name__ == "__main__":
main()