"""TRD08 — Hull MA slope strategy. HYPOTHESIS: HMA(n); long when HMA rising (slope > 0), flat when falling. Grid: n in {20, 50, 100}. Hull Moving Average (causal): WMA(n) = weighted moving average with linear weights HMA(n) = WMA(sqrt(n), 2*WMA(n//2) - WMA(n)) Position sizing: vol-targeted (20% target, 2x cap), long-flat only. """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np from numpy.lib.stride_tricks import as_strided def wma_vectorized(x: np.ndarray, win: int) -> np.ndarray: """Causal weighted moving average — vectorized via cumsum trick.""" n = len(x) # Use pandas for clean rolling WMA: sum(w_i * x_i) / sum(w_i) # weights = 1, 2, ..., win # We can compute via cumsum: WMA = (sum(i * x[t-i]) for i=1..win) / (win*(win+1)/2) # Use a numerator via weighted cumsum weights = np.arange(1, win + 1, dtype=float) total_w = weights.sum() result = np.full(n, np.nan) # Efficient: build a 2D sliding window using stride tricks, then dot with weights if n < win: return result # pad at start for alignment # shape: (n - win + 1, win) shape = (n - win + 1, win) strides = (x.strides[0], x.strides[0]) windows = as_strided(x, shape=shape, strides=strides) result[win - 1:] = windows @ weights / total_w return result def hma(x: np.ndarray, n: int) -> np.ndarray: """Causal Hull Moving Average.""" half_n = max(2, n // 2) sqrt_n = max(2, int(round(np.sqrt(n)))) wma_full = wma_vectorized(x, n) wma_half = wma_vectorized(x, half_n) # 2 * WMA(n//2) - WMA(n) raw = 2.0 * wma_half - wma_full # Apply WMA(sqrt(n)) to the raw series return wma_vectorized(raw, sqrt_n) def make_target(n: int): """Return a lambda that computes vol-targeted HMA slope signal.""" def target(df): c = df["close"].values.astype(float) h = hma(c, n) # slope: hma[i] > hma[i-1] => rising => long slope = np.zeros(len(h)) slope[1:] = np.where(h[1:] > h[:-1], 1.0, 0.0) # NaN protection: flat when HMA not yet valid or slope undefined nan_mask = np.isnan(h) | np.isnan(np.concatenate([[np.nan], h[:-1]])) slope[nan_mask] = 0.0 # Vol-target return al.vol_target(slope, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) return target # Grid: n in {20, 50, 100} across timeframes {1d, 12h} # 3 param sets × 2 TFs = 6 total backtests (within limit) tfs = ("1d", "12h") grid_n = [20, 50, 100] best_rep = None best_score = -999.0 best_n = grid_n[0] for n in grid_n: name = f"TRD08-HMA{n}" rep = al.study_weights(name, make_target(n), tfs=tfs) print(al.fmt(rep)) print("JSON:", al.as_json(rep)) # Score by best_holdout_sharpe score = rep["verdict"].get("best_holdout_sharpe", rep["verdict"].get("min_asset_holdout_sharpe", -999)) if score > best_score: best_score = score best_rep = rep best_n = n print("\n" + "="*60) print(f"BEST CONFIG: n={best_n}") print(al.fmt(best_rep)) print("JSON:", al.as_json(best_rep))