"""MRV02 — BB reversion in calm regime (1d, discrete signals). HYPOTHESIS: Buy lower BB(20,2) ONLY when realized vol is in low expanding-percentile (calm regime). Exit at mid-BB. The gate is the alpha: filter out high-vol / volatile periods; only trade the gentle reversions. Style: al.study_signals (discrete entry/exit, 1d only) Gate: RV <= expanding percentile of RV (calm = low expanding percentile threshold) Entry: close <= lower BB(20,2) TP: mid-BB (dynamic, recomputed each bar in the trade management) SL: 2 * ATR below entry Max bars: 20 days """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np import pandas as pd def make_entries(df: pd.DataFrame, bb_win: int = 20, bb_k: float = 2.0, rv_win_days: int = 20, rv_pct_thresh: float = 30.0, atr_win: int = 14, max_bars: int = 20): """ Causal entry logic for MRV02. Entry conditions at close[i]: 1. close[i] <= lower_BB(20,2) — price touched/crossed lower band 2. rv_percentile(i) <= rv_pct_thresh — calm regime (low expanding RV percentile) TP: mid_BB at entry time (static target for the trade) SL: entry - 2*ATR (static) max_bars: 20 days """ c = df["close"].values.astype(float) n = len(c) bpd = al.bars_per_day(df) bpy = bpd * 365.25 # Bollinger Bands (causal: value at i uses data <= i) upper_bb, mid_bb, lower_bb = al.bbands(c, win=bb_win, k=bb_k) # Realized vol (annualized), window = rv_win_days bars rv_win = max(2, rv_win_days * bpd) r = al.simple_returns(c) rv = al.realized_vol(r, rv_win, bpy) # Expanding percentile of RV (causal: percentile of all RV values seen up to i) rv_series = pd.Series(rv) rv_pct = rv_series.expanding().rank(pct=True) * 100.0 # 0-100 percentile rv_pct = rv_pct.values # ATR for SL atr_vals = al.atr(df, win=atr_win) entries = [None] * n warmup = max(bb_win, rv_win, atr_win) + 1 for i in range(warmup, n): # Gate: RV must be in calm regime if not np.isfinite(rv_pct[i]) or rv_pct[i] > rv_pct_thresh: continue # Gate: lower BB must be defined if not np.isfinite(lower_bb[i]) or not np.isfinite(mid_bb[i]): continue # Entry: close touches or crosses lower BB if c[i] > lower_bb[i]: continue # ATR must be defined if not np.isfinite(atr_vals[i]) or atr_vals[i] <= 0: continue tp_price = mid_bb[i] # exit at mid-band (static target) sl_price = c[i] - 2.0 * atr_vals[i] # SL: 2 ATR below entry # Only take trade if TP > entry price (there's room to profit) if tp_price <= c[i]: continue entries[i] = { "dir": +1, "tp": tp_price, "sl": sl_price, "max_bars": max_bars, } return entries # ---------------------------------------------------------------- # Small parameter grid: bb_win x rv_pct_thresh (4 combos max) # ---------------------------------------------------------------- GRID = [ # (bb_win, rv_pct_thresh) (20, 30), # canonical (20, 40), # slightly more permissive gate (30, 30), # wider bands (30, 40), # wider bands + more permissive gate ] print("MRV02 — BB reversion in calm regime") print(f"Grid: {GRID}") print() best_rep = None best_score = -999.0 for bb_win, rv_pct_thresh in GRID: label = f"MRV02[BB{bb_win},RVp{rv_pct_thresh}]" print(f"--- Testing {label} ---") def make_fn(bw=bb_win, rp=rv_pct_thresh): def entries_fn(df): return make_entries(df, bb_win=bw, rv_pct_thresh=rp) return entries_fn rep = al.study_signals(label, make_fn(), tfs=("1d",)) print(al.fmt(rep)) print() v = rep["verdict"] score = v.get("best_holdout_sharpe", -999.0) or -999.0 if score > best_score: best_score = score best_rep = rep best_rep["_config"] = dict(bb_win=bb_win, rv_pct_thresh=rv_pct_thresh) print("\n=== BEST CONFIG ===") print(al.fmt(best_rep)) print() print("JSON:", al.as_json(best_rep))