Files
PythagorasGoal/scripts/research/alt/runs/STA01.py
T
Adriano Dal Pastro 5ac4e16af8 research(alt): sweep 104 strategie alternative su Deribit (153 agenti) + marginal scorer
Ondata di ricerca onesta a largo spettro su BTC/ETH+DVOL certificati: 104 ipotesi
distinte (11 famiglie), un agente-finder per ipotesi, verifica avversariale a 3
scettici sui promettenti, sintesi (153 agenti totali). Esito: NIENTE di nuovo regge
-> conferma del soffitto strutturale ~1.3 BTC/ETH-direzionale; lo stack
TP01+XS01+VRP01 resta imbattuto.

- altlib.py: harness condiviso vettoriale leak-free (eval_weights/study_weights,
  fee-sweep, both-asset + hold-out 2025+). Riproduce i numeri canonici di TP01.
- MARGINAL SCORER (study_marginal/marginal_vs_tp01): Sharpe INCREMENTALE vs baseline
  TP01 (corr, blend uplift OOS, alpha residua) + jackknife OOS (clean-year +
  drop-best-month). earns_slot = abs!=FAIL & ADDS & robust_oos. Smaschera gli overlay
  su TSMOM con PASS assoluti fasulli (CMB04, VOL11, ...) e il falso positivo KAMA
  (ADDS ma muore al jackknife).
- runs/*.py (104) script riproducibili per ipotesi; wf_altstrat.js workflow.
- Verdetto: 0 candidati deployabili; 2 LEAD fragili (VOL08, STA05_LS) da forward-monitor.
- test_marginal_scorer.py blocca baseline + invarianti. Suite: 32 verde.

Diario: docs/diary/2026-06-20-alt-strategies-100agent-sweep.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 19:50:39 +00:00

159 lines
6.2 KiB
Python

"""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))