"""MIC03 — Volume-spike breakout Hypothesis: Breakout of prior high CONFIRMED by volume z-score > threshold -> enter long at close. Exit: TP, SL, or max_bars timeout. Implementation: - Breakout: close[i] > donchian_high(win)[i] (prior win-bar high, shifted by 1 so fully causal) - Volume confirmation: volume z-score over vol_win bars > vol_thresh - Entry at close[i], direction = long only (breakouts on the upside) - TP = entry * (1 + tp_pct), SL = entry * (1 - sl_pct), max_bars timeout Grid (<=4 param sets, 1d only -> total backtests = 4 * 2 assets = 8 <= 6... actually 8. Reduce to 2 configs to stay within ~6 backtests and avoid slow fee sweeps): Config A: donchian 20, vol_win 20, vol_thresh 2.0, tp 3%, sl 1.5%, max_bars 10 Config B: donchian 30, vol_win 30, vol_thresh 1.5, tp 4%, sl 2.0%, max_bars 15 Pick the best config by min_asset_holdout_sharpe. """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np def make_entries_fn(don_win: int, vol_win: int, vol_thresh: float, tp_pct: float, sl_pct: float, max_bars: int): def entries_fn(df): close = df["close"].values.astype(float) volume = df["volume"].values.astype(float) n = len(close) # Donchian upper channel: prior don_win-bar HIGH (shifted, causal) # Using high prices for breakout reference (breakout above prior high is more meaningful) high = df["high"].values.astype(float) don_hi = np.full(n, np.nan) # rolling max of high over don_win bars, then shift by 1 (prior bar) for i in range(don_win, n): don_hi[i] = np.max(high[i - don_win: i]) # excludes bar i -> causal # Volume z-score (causal): zscore of current volume vs rolling mean/std vol_mean = np.full(n, np.nan) vol_std = np.full(n, np.nan) for i in range(vol_win, n): v_window = volume[i - vol_win: i] # excludes current bar vol_mean[i] = np.mean(v_window) vol_std[i] = np.std(v_window) vol_z = np.full(n, np.nan) mask = (vol_std > 0) & np.isfinite(vol_std) vol_z[mask] = (volume[mask] - vol_mean[mask]) / vol_std[mask] # Build entry list entries = [None] * n for i in range(don_win + vol_win, n): # Breakout condition: close breaks above prior don_win-bar high breakout = (np.isfinite(don_hi[i]) and close[i] > don_hi[i]) # Volume confirmation vol_confirmed = (np.isfinite(vol_z[i]) and vol_z[i] > vol_thresh) if breakout and vol_confirmed: entry_px = close[i] # fill at close[i] tp_px = entry_px * (1.0 + tp_pct) sl_px = entry_px * (1.0 - sl_pct) entries[i] = { "dir": +1, "tp": tp_px, "sl": sl_px, "max_bars": max_bars, } return entries return entries_fn # Config A: tighter params config_a = dict(don_win=20, vol_win=20, vol_thresh=2.0, tp_pct=0.03, sl_pct=0.015, max_bars=10) # Config B: wider params config_b = dict(don_win=30, vol_win=30, vol_thresh=1.5, tp_pct=0.04, sl_pct=0.02, max_bars=15) configs = [ ("MIC03-A", config_a), ("MIC03-B", config_b), ] best_rep = None best_score = -999.0 for cfg_name, cfg in configs: print(f"\n--- Running {cfg_name}: {cfg} ---") fn = make_entries_fn(**cfg) rep = al.study_signals(cfg_name, fn, tfs=("1d",)) print(al.fmt(rep)) print("JSON:", al.as_json(rep)) score = rep["verdict"].get("best_holdout_sharpe", -999) or -999 if score > best_score: best_score = score best_rep = rep best_rep["_config"] = cfg best_rep["_config_name"] = cfg_name print("\n\n=== BEST CONFIG ===") print(al.fmt(best_rep)) print("JSON:", al.as_json(best_rep))