"""RSK04 — Momentum-of-Momentum Sizing HYPOTHESIS: Size the TSMOM (long-flat) position by the STABILITY/AGREEMENT of multi-horizon momentum signals. When all horizons agree (strong consensus), take a larger position. When signals disagree, reduce exposure. MECHANISM: - Compute TSMOM signals for 3 horizons: 1M, 3M, 6M (same as TP01 canonical) - Direction = go long only if net signal > 0 (majority bullish), else flat - SIZE = fraction of horizons that agree with the majority direction e.g. all 3 agree -> size=1.0, 2/3 agree -> size=0.667, 1/3 -> flat - Apply vol-targeting on top of the sized position INTERNAL GRID (<=4 configs x 2 assets x 2 TFs = <=16 backtests): A: horizons=(1M,3M,6M), size by fraction-agreement B: horizons=(1M,3M,6M,12M), size by fraction-agreement (4 horizons) Two TFs: 1d, 12h -> 2 configs x 2 tfs x 2 assets = 8 backtests total CAUSAL: all signals use close[i] for the past horizon -> no leakage. """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np def make_target(horizons_months, tf): """Return a target_fn(df) that implements momentum-of-momentum sizing.""" def target_fn(df): c = df["close"].values.astype(float) n = len(c) bpd = al.bars_per_day(df) # Compute per-horizon signals: +1 (bullish) or 0 (bearish/flat) # Signal at bar i: sign of return over last `h` bars signals = [] for months in horizons_months: h = int(round(months * 30.44 * bpd)) h = max(h, 2) sig = np.zeros(n) # causal: sig[i] uses close[i] vs close[i-h] sig[h:] = np.where(c[h:] / c[:n-h] > 1.0, 1.0, 0.0) # NaN guard: first h bars stay 0 signals.append(sig) signals = np.stack(signals, axis=1) # shape (n, num_horizons) num_horizons = len(horizons_months) # Net bullish count at each bar bullish_count = signals.sum(axis=1) # in [0, num_horizons] bearish_count = num_horizons - bullish_count # Direction: go long only if strict majority bullish direction = np.where(bullish_count > num_horizons / 2, 1.0, 0.0) # Size = fraction of horizons agreeing with the direction taken # If long: fraction_agree = bullish_count / num_horizons # If flat (direction=0): size = 0 fraction_agree = np.where( direction > 0, bullish_count / num_horizons, 0.0 ) # Apply vol-targeting with the agreement-sized direction # We pass the sized direction (0..1) into vol_target as if it were direction target = al.vol_target(fraction_agree, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) return target return target_fn # Config A: 3 horizons (1M, 3M, 6M) horizons_A = [1, 3, 6] # Config B: 4 horizons (1M, 3M, 6M, 12M) horizons_B = [1, 3, 6, 12] # Run on 1d and 12h timeframes rep_A = al.study_weights( "RSK04-A(1M3M6M)", make_target(horizons_A, "1d"), tfs=("1d", "12h") ) rep_B = al.study_weights( "RSK04-B(1M3M6M12M)", make_target(horizons_B, "1d"), tfs=("1d", "12h") ) print("=== RSK04: Momentum-of-Momentum Sizing ===\n") print(al.fmt(rep_A)) print() print(al.fmt(rep_B)) print() print("JSON:", al.as_json(rep_A)) print("JSON:", al.as_json(rep_B)) # Determine best config by holdout sharpe best_rep = max([rep_A, rep_B], key=lambda r: r["verdict"].get("best_holdout_sharpe", -99)) print("\n=== BEST CONFIG ===") print(al.fmt(best_rep)) print("JSON_BEST:", al.as_json(best_rep))