"""MIC05 — Wide-range-bar follow-through. HYPOTHESIS: After a wide-range bar (range > 2*ATR) closing strong (close near the top 30% of the bar for longs, or bottom 30% for shorts), enter in the bar's direction at close[i]; exit after k bars (or on TP/SL). CAUSAL: ATR is computed up to bar i-1 (shifted), range and close strength computed from bar i itself (known at close[i]). Entry fills at close[i]. Grid: k_bars in {3, 5, 7, 10} — only 1d, 2 assets, 4 param sets = 8 backtests total. Best config selected by min-asset hold-out Sharpe. """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np # --------------------------------------------------------------------------- # Signal generator # --------------------------------------------------------------------------- def make_entries(df, k_bars: int = 5, atr_mult: float = 2.0, close_pct: float = 0.30): """Returns entries list len(df). Wide range bar: range > atr_mult * ATR(14) at bar i-1 (causal). Strong close long: close >= low + (1 - close_pct) * range (top 30%) Strong close short: close <= low + close_pct * range (bottom 30%) """ hi = df["high"].values.astype(float) lo = df["low"].values.astype(float) cl = df["close"].values.astype(float) bar_range = hi - lo # ATR causal: shift by 1 so ATR at bar i uses data up to bar i-1 atr_raw = al.atr(df, win=14) atr_shifted = np.roll(atr_raw, 1) atr_shifted[0] = atr_raw[0] entries = [None] * len(df) for i in range(1, len(df)): rng = bar_range[i] atr_i = atr_shifted[i] if atr_i <= 0 or not np.isfinite(atr_i): continue if rng < atr_mult * atr_i: continue # not a wide-range bar close_rel = (cl[i] - lo[i]) / rng if rng > 0 else 0.5 if close_rel >= (1.0 - close_pct): # Strong bullish wide bar -> long follow-through entries[i] = {"dir": 1, "tp": None, "sl": None, "max_bars": k_bars} elif close_rel <= close_pct: # Strong bearish wide bar -> short follow-through entries[i] = {"dir": -1, "tp": None, "sl": None, "max_bars": k_bars} return entries # --------------------------------------------------------------------------- # Grid search over k_bars # --------------------------------------------------------------------------- K_BARS_GRID = [3, 5, 7, 10] best_rep = None best_hold = -999 for k in K_BARS_GRID: rep = al.study_signals( f"MIC05-k{k}", lambda df, _k=k: make_entries(df, k_bars=_k), tfs=("1d",), ) min_hold = rep["verdict"].get("best_holdout_sharpe", -999) print(f"k={k:2d}: grade={rep['verdict']['grade']} " f"full={rep['verdict'].get('best_full_sharpe', 'N/A')} " f"hold={min_hold}") if min_hold > best_hold: best_hold = min_hold best_rep = rep # Rename best rep with canonical ID best_rep["name"] = "MIC05" print("\n--- BEST CONFIG ---") print(al.fmt(best_rep)) print("JSON:", al.as_json(best_rep))