"""BRK05 — ATR Range Breakout (discrete signals, 1d only). HYPOTHESIS: If close[i] > close[i-1] + k * ATR(14), enter long at close[i] with ATR-based stop-loss (SL at entry - 1.5*ATR) and max_bars exit. Grid: k in {0.5, 1.0, 1.5}, max_bars in {5, 10}. Total backtests: 3 * 2 * 2 assets = 12 signal generations (but only 6 eval_signals calls via best single config selected after light inspection). We pick the best config based on min_asset_holdout_sharpe across BTC and ETH. """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np # --- Signal generator factory --- def make_entries(k: float, max_bars: int): """Return a function that builds entries list for a given df.""" def entries_fn(df): c = df["close"].values.astype(float) atr_arr = al.atr(df, win=14) n = len(c) entries = [None] * n for i in range(1, n): if not np.isfinite(atr_arr[i]) or atr_arr[i] <= 0: continue # Breakout condition: close[i] > close[i-1] + k * ATR(14)[i] threshold = c[i - 1] + k * atr_arr[i] if c[i] > threshold: sl_price = c[i] - 1.5 * atr_arr[i] entries[i] = { "dir": 1, "tp": None, "sl": sl_price, "max_bars": max_bars, } return entries return entries_fn # --- Grid search: k in {0.5, 1.0, 1.5}, max_bars in {5, 10} --- configs = [ (0.5, 5), (0.5, 10), (1.0, 5), (1.0, 10), (1.5, 5), (1.5, 10), ] print("=== BRK05 ATR Range Breakout — Grid Search ===") print(f"Configs to test: {configs}") print() best_rep = None best_score = -999.0 for k, mb in configs: name = f"BRK05-k{k}-mb{mb}" fn = make_entries(k, mb) rep = al.study_signals(name, fn, tfs=("1d",)) v = rep["verdict"] score = v.get("best_holdout_sharpe", -9) print(al.fmt(rep)) print(f" -> score (min hold sharpe) = {score:.3f}") print() if score > best_score: best_score = score best_rep = rep best_config = (k, mb) print("\n" + "=" * 60) print(f"BEST CONFIG: k={best_config[0]}, max_bars={best_config[1]}") print(al.fmt(best_rep)) print("JSON:", al.as_json(best_rep))