Repository Public
1"""Volatility breakout: follow a move that is large against the volatility that preceded it, for a fixed time."""23import numpy as np45META = {6 "name": "vol_breakout_perp",7 "version": "1",8 "kind": "perp",9 "universe": ["BTC", "ETH", "SOL", "HYPE"],10 "interval": "1h",11 "lookback": 500,12 "max_leverage": 2.0,13 "max_gross_exposure": 2.0,14 "params": {"move_window": 24, "vol_window": 168, "k": 2.0, "hold": 48, "gross": 1.0},15}161718def _last_breakout(close, move_window, vol_window, k, hold):19 """Side (+1 / -1) of the most recent breakout among the last `hold` bars, 0 if there is none.2021 A breakout at bar t: |log(close[t] / close[t - move_window])| > k x sigma, where sigma is the standard22 deviation of the `vol_window` one-bar log-returns that END where the move starts, scaled to the move's length.23 """24 logp = np.log(close)25 n = len(logp)26 for t in range(n - 1, max(n - 1 - hold, move_window + vol_window - 1), -1):27 start = t - move_window28 move = logp[t] - logp[start]29 returns = np.diff(logp[start - vol_window:start + 1])30 sigma = returns.std(ddof=1) * np.sqrt(move_window)31 if sigma > 0 and abs(move) > k * sigma:32 return 1.0 if move > 0 else -1.033 return 0.0343536def target_weights(data, params):37 move_window, vol_window, hold = int(params["move_window"]), int(params["vol_window"]), int(params["hold"])38 k, gross = float(params["k"]), float(params["gross"])39 n = len(data)40 weights = {}41 for symbol, df in data.items():42 close = df["close"].to_numpy()43 if move_window < 1 or vol_window < 3 or hold < 1 or k <= 0 or len(close) < move_window + vol_window + 1:44 weights[symbol] = 0.045 continue46 weights[symbol] = _last_breakout(close, move_window, vol_window, k, hold) * gross / n47 return weights48