"""MRV05 — Williams %R Mean-Reversion HYPOTHESIS: Buy when %R(14) < -90 (oversold) with trend filter (close > SMA200); exit (go flat) when %R > -50 (momentum restored). Long-flat only. Williams %R = (Highest High(n) - Close) / (Highest High(n) - Lowest Low(n)) * -100 Range: -100 (most oversold) to 0 (most overbought). %R < -80 = oversold zone; %R > -20 = overbought zone. The exit condition (%R > -50) is causal: we check %R[i] and decide position for bar i+1. This maps naturally to study_weights (continuous hold logic): - position[i] = 1 if %R[i] < -90 AND close[i] > SMA200[i] (buy signal) - position[i] = 0 if %R[i] > -50 (exit signal) - else hold previous position Variants (small grid, 4 configs): V1: %R entry -90, exit -50, SMA200 trend filter, long-flat V2: %R entry -85, exit -50, SMA200 trend filter, long-flat (slightly less oversold entry) V3: %R entry -90, exit -50, SMA50 trend filter, long-flat (shorter trend filter) V4: %R entry -90, exit -40, SMA200 trend filter, long-flat (later exit) Best variant selected by min-asset hold-out Sharpe. All positions are vol-targeted (20% annualized, 2x leverage cap). """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np import pandas as pd # --------------------------------------------------------------------------- # Williams %R calculation (causal: uses data <= bar i) # --------------------------------------------------------------------------- def williams_r(df: pd.DataFrame, win: int = 14) -> np.ndarray: """Causal Williams %R. Value at i uses data[i-win+1 .. i]. %R = (HH - Close) / (HH - LL) * -100 Range: -100 (oversold) to 0 (overbought). """ h = df["high"].values.astype(float) l = df["low"].values.astype(float) c = df["close"].values.astype(float) n = len(c) wr = np.full(n, np.nan) # Vectorized rolling using pandas hh = pd.Series(h).rolling(win, min_periods=win).max().values ll = pd.Series(l).rolling(win, min_periods=win).min().values rng = hh - ll # Avoid division by zero valid = rng > 0 wr[valid] = (hh[valid] - c[valid]) / rng[valid] * -100.0 return wr # --------------------------------------------------------------------------- # Strategy factory # --------------------------------------------------------------------------- def make_wrpct_target(wr_entry: float = -90.0, wr_exit: float = -50.0, sma_win: int = 200, wr_win: int = 14): """Williams %R long-flat mean-reversion with trend filter. Entry: %R[i] < wr_entry AND close[i] > SMA(sma_win)[i] -> go long Exit: %R[i] > wr_exit -> go flat Hold: otherwise, maintain current position Causal: position decided using data <= close[i], held during bar i+1. Vol-targeted: 20% annualized, 2x leverage cap. """ def target_fn(df): c = df["close"].values.astype(float) wr = williams_r(df, wr_win) sma_trend = al.sma(c, sma_win) # Vectorized state machine using ffill # Signal: 1 = enter long, 0 = exit to flat, NaN = hold # Priority: exit takes precedence over entry sig = np.where( wr > wr_exit, # exit condition 0.0, np.where( (wr < wr_entry) & (c > sma_trend), # entry condition 1.0, np.nan # hold ) ) # Start flat sig[0] = 0.0 # Forward-fill NaN (hold previous position) pos = pd.Series(sig).ffill().fillna(0.0).values # Vol-target return al.vol_target(pos, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) return target_fn # --------------------------------------------------------------------------- # Run all variants and pick best # --------------------------------------------------------------------------- if __name__ == "__main__": TFS = ("1d",) variants = [ ("MRV05-V1-WR90-exit50-SMA200", make_wrpct_target(-90.0, -50.0, 200, 14)), ("MRV05-V2-WR85-exit50-SMA200", make_wrpct_target(-85.0, -50.0, 200, 14)), ("MRV05-V3-WR90-exit50-SMA50", make_wrpct_target(-90.0, -50.0, 50, 14)), ("MRV05-V4-WR90-exit40-SMA200", make_wrpct_target(-90.0, -40.0, 200, 14)), ] results = [] for name, fn in variants: print(f"\nRunning {name} ...") rep = al.study_weights(name, fn, tfs=TFS) print(al.fmt(rep)) results.append(rep) # Pick best by min_asset_holdout_sharpe best = max(results, key=lambda r: r["verdict"].get("best_holdout_sharpe", -99)) print("\n" + "=" * 60) print(f"BEST VARIANT: {best['name']}") print(al.fmt(best)) print("JSON:", al.as_json(best))