"""RSK06 — Time-stop momentum HYPOTHESIS: Enter long on a breakout of the N-bar Donchian high, then EXIT after exactly M bars (hard time-stop), no trailing. Tests whether momentum has a fixed horizon with a clean carry/decay structure. Signal style: al.study_signals (discrete entry/exit, 1d only). Grid (<=4 param sets, total backtests = 4 * 2 assets = 8 <= 12 max): We test (breakout_window, hold_bars) pairs: A: (20, 10) — mid-term breakout, short hold B: (20, 20) — mid-term breakout, mid hold C: (40, 10) — longer breakout, short hold D: (40, 20) — longer breakout, mid hold Entry: close[i] breaks above the prior `bk_win`-bar high (Donchian, causal, shifted). Fill: close[i] (executable; NOT a high/low extreme, it's the close price). Exit: close[i + hold_bars] — hard time-stop, no TP/SL. Direction: long only (momentum = price breaks out above prior range). No vol-targeting (discrete signal framework does not support it natively). Fee: 0.10% RT Deribit taker baseline. """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np # --------------------------------------------------------------------------- # Signal builder # --------------------------------------------------------------------------- def make_entries(df, bk_win: int, hold_bars: int): """Return entries list: signal at i if close[i] > prior bk_win-bar high. Uses donchian() which shifts by 1 to prevent look-ahead. Entry price = close[i] (not high/low extreme). Hard exit after hold_bars bars (max_bars param in harness). """ hi, _lo = al.donchian(df, bk_win) # hi[i] = max high over [i-bk_win, i-1] — causal c = df["close"].values n = len(df) entries = [] for i in range(n): if np.isnan(hi[i]): entries.append(None) continue # Breakout: current close exceeds the prior-window high if c[i] > hi[i]: entries.append({"dir": +1, "tp": None, "sl": None, "max_bars": hold_bars}) else: entries.append(None) return entries # --------------------------------------------------------------------------- # Grid search: pick best config by min-asset hold-out Sharpe # --------------------------------------------------------------------------- GRID = [ (20, 10), (20, 20), (40, 10), (40, 20), ] best_rep = None best_score = -999.0 best_label = "" for bk_win, hold_bars in GRID: label = f"RSK06 bk={bk_win} hold={hold_bars}" print(f"\n--- Testing {label} ---") rep = al.study_signals( label, lambda df, bw=bk_win, hb=hold_bars: make_entries(df, bw, hb), tfs=("1d",), ) print(al.fmt(rep)) # Score by min-asset hold-out Sharpe (conservative) best_cell = max(rep["cells"], key=lambda c: c.get("min_asset_holdout_sharpe", -9)) score = best_cell.get("min_asset_holdout_sharpe", -9.0) if score > best_score: best_score = score best_rep = rep best_label = label # --------------------------------------------------------------------------- # Final report on best config # --------------------------------------------------------------------------- print("\n" + "=" * 60) print(f"BEST CONFIG: {best_label}") print(al.fmt(best_rep)) print("JSON:", al.as_json(best_rep))