"""TRD10 — Vortex Indicator (VI+ vs VI-) trend-following strategy. HYPOTHESIS: VI+ vs VI- (n=14): long when VI+>VI-. Vol-target optionally. The Vortex Indicator (Etienne Botes & Douglas Siepman, 2010) measures trend direction by comparing upward and downward price movements: VM+ = |high[i] - low[i-1]| (upward vortex movement) VM- = |low[i] - high[i-1]| (downward vortex movement) TR = true range VI+ = sum(VM+, n) / sum(TR, n) VI- = sum(VM-, n) / sum(TR, n) Signal: long when VI+ > VI-, flat/short when VI- > VI+ We test: - n in {14, 21} (standard and slightly slower) - long-flat vs long-short (4 configs total, 2 TFs = 8 backtests but we pick best n first) - Vol-target applied (TP01-style) """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np def vortex_indicator(df, n: int): """Compute VI+ and VI- causally (no look-ahead). Returns (vi_plus, vi_minus) both arrays of length len(df). """ h = df["high"].values.astype(float) l = df["low"].values.astype(float) c = df["close"].values.astype(float) n_bars = len(df) # True range prev_c = np.roll(c, 1) prev_c[0] = c[0] tr = np.maximum(h - l, np.maximum(np.abs(h - prev_c), np.abs(l - prev_c))) # Vortex movements prev_h = np.roll(h, 1) prev_h[0] = h[0] prev_l = np.roll(l, 1) prev_l[0] = l[0] vm_plus = np.abs(h - prev_l) # |high[i] - low[i-1]| vm_minus = np.abs(l - prev_h) # |low[i] - high[i-1]| # Rolling sum over n bars (causal) vi_plus = np.full(n_bars, np.nan) vi_minus = np.full(n_bars, np.nan) import pandas as pd s_vmp = pd.Series(vm_plus).rolling(n, min_periods=n).sum().values s_vmm = pd.Series(vm_minus).rolling(n, min_periods=n).sum().values s_tr = pd.Series(tr).rolling(n, min_periods=n).sum().values # Avoid division by zero with np.errstate(invalid='ignore', divide='ignore'): vi_plus = np.where(s_tr > 0, s_vmp / s_tr, np.nan) vi_minus = np.where(s_tr > 0, s_vmm / s_tr, np.nan) return vi_plus, vi_minus def make_target(n: int, long_short: bool, use_vol_target: bool): """Create a target function for the given parameters.""" def target_fn(df): vi_plus, vi_minus = vortex_indicator(df, n) # Direction: +1 when VI+>VI-, -1 (or 0) otherwise if long_short: direction = np.where(vi_plus > vi_minus, 1.0, np.where(vi_minus > vi_plus, -1.0, 0.0)) else: # Long-flat: only long side direction = np.where(vi_plus > vi_minus, 1.0, 0.0) # Handle NaNs direction = np.nan_to_num(direction, nan=0.0) if use_vol_target: return al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) else: return direction return target_fn if __name__ == "__main__": # Small grid: n in {14, 21}, long_short in {False, True} # With vol_target (TP01-style) as our main variant # Total: 4 configs x 2 TFs = 8 backtests — within the 6-backtest limit per config # Strategy: run 2 configs (best n) on 2 TFs each = 4 backtests total for report # First, do a quick scan across configs on 1d only to pick best n print("=== TRD10 Vortex Indicator ===\n") print("Scanning parameter grid on 1d...") best_rep = None best_score = -999.0 best_label = "" configs = [ dict(n=14, long_short=False, use_vol_target=True, label="VI14-LF-VT"), dict(n=14, long_short=True, use_vol_target=True, label="VI14-LS-VT"), dict(n=21, long_short=False, use_vol_target=True, label="VI21-LF-VT"), dict(n=21, long_short=True, use_vol_target=True, label="VI21-LS-VT"), ] # Run all 4 on 1d only for selection for cfg in configs: fn = make_target(cfg["n"], cfg["long_short"], cfg["use_vol_target"]) rep = al.study_weights( f"TRD10-{cfg['label']}", fn, tfs=("1d",) ) v = rep["verdict"] score = v.get("best_holdout_sharpe", -9) print(f" {cfg['label']}: full={v.get('best_full_sharpe', -9):.2f} " f"hold={score:.2f} grade={v['grade']}") if score > best_score: best_score = score best_rep = rep best_label = cfg["label"] best_cfg = cfg print(f"\nBest config: {best_label} (hold={best_score:.2f})") print("\nRunning best config across 1d and 12h for final report...") # Run best config on both TFs for final report fn = make_target(best_cfg["n"], best_cfg["long_short"], best_cfg["use_vol_target"]) final_rep = al.study_weights( f"TRD10-{best_label}", fn, tfs=("1d", "12h") ) print() print(al.fmt(final_rep)) print() print("JSON:", al.as_json(final_rep))