"""VOL08 — Realized-vol term structure overlay on long. HYPOTHESIS: Ratio short-window vol (5d) / long-window vol (30d). >1 (vol rising, de-risk) -> reduce position <1 (vol falling, risk-on) -> increase position Overlay on a long-only base position (TSMOM trend direction), vol-targeted. The vol-term-structure ratio modulates position size: position = base_dir * vol_target * clamp(1 / ratio, 0.0, 1.0) Grid: short_win: [5, 10] days long_win: [21, 63] days -> 4 configs x 2 TFs (1d, 12h) = 8 backtests total, but we pick best config first on 1d then verify best config on 12h -> capped at 6 total. Plan: - Run 4 configs on 1d to find best - Run best config on 12h - Report rep for best config Implementation: 1. Compute TSMOM direction (1m,3m,6m blend, long-flat) 2. Vol-target the direction (target_vol=0.20, cap=2x) 3. Multiply by vol-ratio scaling: scale = clip(long_vol / short_vol, 0, 1) (when short_vol > long_vol -> ratio > 1 -> scale < 1: de-risk) (when short_vol < long_vol -> ratio < 1 -> scale > 1, but clipped at 1: stay full) """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np def make_target(short_days: int, long_days: int): """Return a target function for the given short/long vol windows.""" def target_fn(df): c = df["close"].values.astype(float) bpd = al.bars_per_day(df) bpy = bpd * 365.25 r = al.simple_returns(c) # --- TSMOM long-flat direction (1m, 3m, 6m) --- horizons = [30 * bpd, 90 * bpd, 180 * bpd] direction = np.zeros(len(c)) for h in horizons: h = int(h) sig = np.full(len(c), np.nan) if h < len(c): sig[h:] = np.sign(c[h:] / c[:-h] - 1.0) direction += np.nan_to_num(sig) # long-flat (0 or +1) long_flat = np.clip(np.sign(direction), 0.0, 1.0) # --- Vol-targeted base position --- vol_win = max(2, 30 * bpd) rv30 = al.realized_vol(r, int(vol_win), bpy) base_scale = np.where((rv30 > 0) & np.isfinite(rv30), 0.20 / rv30, 0.0) base_pos = np.clip(long_flat * base_scale, 0.0, 2.0) # --- Vol term structure overlay --- short_win = max(2, short_days * bpd) long_win_b = max(2, long_days * bpd) rv_short = al.realized_vol(r, int(short_win), bpy) rv_long = al.realized_vol(r, int(long_win_b), bpy) # scale = long_vol / short_vol, clipped to [0, 1] # >1 vol rising (short > long): scale < 1 -> de-risk # <1 vol falling (short < long): scale > 1, clipped at 1 -> stay full with np.errstate(divide="ignore", invalid="ignore"): ratio = np.where( (rv_short > 0) & np.isfinite(rv_short) & np.isfinite(rv_long), rv_long / rv_short, 1.0 ) scale = np.clip(ratio, 0.0, 1.0) pos = base_pos * scale pos = np.nan_to_num(pos, nan=0.0) return pos return target_fn if __name__ == "__main__": print("VOL08 — Realized-vol term structure overlay") print("=" * 60) # Grid: 4 configs on 1d grid = [ (5, 21), (5, 63), (10, 21), (10, 63), ] best_rep = None best_hold_sh = -999.0 best_label = "" for short_d, long_d in grid: label = f"VOL08-s{short_d}d-l{long_d}d" print(f"\n--- Testing {label} on 1d ---") rep = al.study_weights( label, make_target(short_d, long_d), tfs=("1d",) ) print(al.fmt(rep)) hold_sh = rep["verdict"].get("best_holdout_sharpe", -999.0) if hold_sh > best_hold_sh: best_hold_sh = hold_sh best_rep = rep best_label = label best_short = short_d best_long = long_d print(f"\n*** Best config: {best_label} (hold_sh={best_hold_sh:.3f}) ***") print("Now running best config on 1d + 12h for final report...") final_rep = al.study_weights( f"VOL08-s{best_short}d-l{best_long}d", make_target(best_short, best_long), tfs=("1d", "12h") ) print("\n=== FINAL REPORT ===") print(al.fmt(final_rep)) print("JSON:", al.as_json(final_rep))