"""STA01 — Ridge on lagged returns (1d only). Walk-forward expanding-window Ridge regression that predicts next-bar return sign from lagged log-returns (lags 1..10). Position = sign(prediction) vol-targeted. Key causal rule: at bar i, we have log_return[i] = log(close[i]/close[i-1]). We predict return[i+1], so we build features from lags 1..10 ending at lag 1 relative to i, meaning we use returns[i-1], returns[i-2], ..., returns[i-10]. This is strictly causal: no return from bar i is used in the feature vector for the prediction that drives the position held during bar i+1. The lib's eval_weights shift handles the final no-lookahead guarantee: target[i] -> position held during bar i+1. We set target[i] = sign of prediction made at close[i] using lags ending at i-1. Grid (<=4 sets, 1 TF -> 4 total backtests, well within 6 limit): - min_train_years: 1 or 2 (warm-up before first prediction) - alpha: 1.0 or 10.0 (ridge regularization) Best config chosen by min(BTC,ETH) holdout Sharpe. """ import sys sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt") import altlib as al import numpy as np from sklearn.linear_model import Ridge N_LAGS = 10 # lags 1..10 (i.e. features use returns[i-1]..returns[i-10]) def ridge_target(df, min_train_years: float = 2.0, alpha: float = 1.0) -> np.ndarray: """ Walk-forward expanding-window Ridge: predict sign of next-bar log-return. Feature at bar i: [ret[i-1], ret[i-2], ..., ret[i-10]] <- strictly causal. Output target[i] = vol-targeted position decided at bar i. """ c = df["close"].values.astype(float) lr = al.log_returns(c) # lr[k] = log(close[k]/close[k-1]), lr[0]=0 n = len(lr) bpy = al.bars_per_year(df) min_train_bars = int(min_train_years * bpy) + N_LAGS # raw signal array (before vol targeting) direction = np.zeros(n, dtype=float) # Walk-forward: at each bar i, we have features built from lags 1..N_LAGS # i.e. X[i] = [lr[i-1], lr[i-2], ..., lr[i-N_LAGS]] # We predict lr[i+1] sign, so we train on (X[k], lr[k+1]) for all k < i # where we have N_LAGS lags available (k >= N_LAGS). # The first valid feature row is at k = N_LAGS (uses lr[N_LAGS-1]..lr[0]). # We need min_train_bars samples before making the first prediction. # Build full feature matrix: row k uses lr[k-1]..lr[k-N_LAGS] # valid for k >= N_LAGS # target for row k: lr[k] (we're predicting the return at bar k) # Training on pairs: (X[k], lr[k]) means we're predicting current bar return # from lagged features — used to predict what comes next. # Specifically: predict lr[i] using X[i] = [lr[i-1]..lr[i-N_LAGS]] # Position at bar i-1 (decided at close[i-1]) will hold during bar i. # So in altlib terms: target[i-1] = sign(predict lr[i]) via X[i] = [lr[i-1]..lr[i-N_LAGS]] # But X[i] uses lr[i-1] which is available at close[i-1]. # Therefore: at close[i-1], we have lr[i-1]..lr[i-N_LAGS] -> predict lr[i] -> target[i-1]. # Let's index: prediction at "decision bar" d means: # features: [lr[d], lr[d-1], ..., lr[d-N_LAGS+1]] (all available at close[d]) # prediction target: lr[d+1] # train on (X[k], lr[k+1]) for k = N_LAGS-1 .. d-1 # X[k] = [lr[k], lr[k-1], ..., lr[k-N_LAGS+1]] # First prediction: d = min_train_bars - 1 (0-indexed), need d >= N_LAGS-1 and d-1 >= N_LAGS-1+1 first_pred_d = max(N_LAGS, min_train_bars - 1) model = Ridge(alpha=alpha, fit_intercept=True) trained = False for d in range(first_pred_d, n - 1): # Build training set: samples k from (N_LAGS-1) to (d-1) # X[k] = [lr[k], lr[k-1], ..., lr[k-N_LAGS+1]], y[k] = lr[k+1] # We rebuild only when needed; for efficiency, fit incrementally isn't # trivial with sklearn, so we do a periodic refit every 'refit_every' bars # to keep runtime manageable. pass # Vectorized approach for speed: refit every refit_every bars refit_every = max(1, int(bpy / 4)) # quarterly refit last_refit = -refit_every # force first refit for d in range(first_pred_d, n - 1): if d - last_refit >= refit_every: # Build full training set up to d-1 # k ranges from N_LAGS-1 to d-1 k_start = N_LAGS - 1 k_end = d # exclusive (train up to d-1 inclusive) if k_end - k_start < 10: continue # Build X matrix rows = k_end - k_start X_train = np.zeros((rows, N_LAGS)) y_train = np.zeros(rows) for row_i, k in enumerate(range(k_start, k_end)): # X[k] = [lr[k], lr[k-1], ..., lr[k-N_LAGS+1]] X_train[row_i] = lr[k - N_LAGS + 1: k + 1][::-1] # lag1=lr[k], lag10=lr[k-N_LAGS+1] y_train[row_i] = lr[k + 1] model.fit(X_train, y_train) trained = True last_refit = d if not trained: continue # Predict lr[d+1] using [lr[d], lr[d-1], ..., lr[d-N_LAGS+1]] x_pred = lr[d - N_LAGS + 1: d + 1][::-1].reshape(1, -1) pred = model.predict(x_pred)[0] direction[d] = np.sign(pred) if pred != 0 else 0.0 # Vol-target the direction signal target = al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0) return target def run_grid(): configs = [ dict(min_train_years=1.0, alpha=1.0), dict(min_train_years=1.0, alpha=10.0), dict(min_train_years=2.0, alpha=1.0), dict(min_train_years=2.0, alpha=10.0), ] best_rep = None best_holdout = -999.0 for cfg in configs: name = f"STA01(train={cfg['min_train_years']}y,a={cfg['alpha']})" print(f"\n--- Running {name} ---") rep = al.study_weights( name, lambda df, c=cfg: ridge_target(df, **c), tfs=("1d",) ) print(al.fmt(rep)) # Extract min holdout Sharpe across assets/cells min_hold = rep["verdict"].get("best_holdout_sharpe", -999) if min_hold > best_holdout: best_holdout = min_hold best_rep = rep best_rep["_cfg"] = cfg return best_rep if __name__ == "__main__": best = run_grid() print("\n\n=== BEST CONFIG ===") print(al.fmt(best)) print("JSON:", al.as_json(best))