5ac4e16af8
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>
131 lines
4.5 KiB
Python
131 lines
4.5 KiB
Python
"""STA08 — AR(1) residual reversion.
|
||
|
||
IDEA: Fit an expanding-window AR(1) on log returns. The AR(1) residual is
|
||
r[t] - (a0 + a1 * r[t-1]), where a0 and a1 are estimated causally from all
|
||
data up to t-1. Trade the mean-reversion of the residual: if residual is
|
||
positive (return exceeded AR(1) prediction) we expect reversion → short;
|
||
if negative → long.
|
||
|
||
Signal: z-score the residual over a rolling window, take the negative of it
|
||
as the continuous position (mean-reversion), then vol-target it.
|
||
|
||
Grid: 2 lookback windows for z-scoring (60, 120 bars), tested on 1d and 12h.
|
||
Total cells: 2 TFs × 2 params × 2 assets = 8 backtests — within limit.
|
||
|
||
We pick the best config by min-asset hold-out Sharpe.
|
||
"""
|
||
import sys
|
||
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
|
||
import altlib as al
|
||
import numpy as np
|
||
|
||
|
||
def ar1_residual_target(df, zscore_win: int = 60) -> np.ndarray:
|
||
"""
|
||
Causal AR(1) residual reversion target.
|
||
|
||
At each bar i:
|
||
- Use all returns r[0..i-1] to fit AR(1): regress r[t] on r[t-1]
|
||
(expanding OLS — efficient via running sums)
|
||
- Compute residual[i] = r[i] - (a0 + a1 * r[i-1]) (uses closed bar i)
|
||
- Z-score the residual over last zscore_win bars
|
||
- Position = -z (mean-reversion) → vol-targeted
|
||
|
||
Minimum warmup: 30 bars for stable OLS + zscore_win bars for z-score.
|
||
"""
|
||
c = df["close"].values.astype(float)
|
||
n = len(c)
|
||
r = al.log_returns(c) # r[0]=0, r[i] = log(c[i]/c[i-1])
|
||
|
||
# Expanding AR(1): for each bar i, estimate (a0, a1) from data up to i-1.
|
||
# We need: sum(r), sum(r^2), sum(r_t * r_{t-1}), sum(r_{t-1}), sum(r_{t-1}^2)
|
||
# for t in [1..i-1].
|
||
# Then OLS: regress r_t ~ a0 + a1*r_{t-1}.
|
||
# Normal equations:
|
||
# [n-1, sum_r1 ] [a0] [sum_r ]
|
||
# [sum_r1, sum_r1sq] [a1] = [sum_r_r1]
|
||
# where sum_r1 = sum(r[t-1]), sum_r = sum(r[t]), etc.
|
||
|
||
residuals = np.zeros(n)
|
||
min_warmup = 30 # minimum bars to fit AR(1)
|
||
|
||
# Running sums for expanding OLS (using pairs (r[t-1], r[t]) for t>=1)
|
||
S_n = 0.0 # count of pairs
|
||
S_x = 0.0 # sum of r[t-1]
|
||
S_y = 0.0 # sum of r[t]
|
||
S_xx = 0.0 # sum of r[t-1]^2
|
||
S_xy = 0.0 # sum of r[t-1]*r[t]
|
||
|
||
for i in range(1, n):
|
||
# Update running sums with pair (r[i-1], r[i]) but we use data up to i-1
|
||
# So at step i, we first compute residual using sums from [1..i-1],
|
||
# then update sums to include pair for t=i.
|
||
|
||
if S_n >= min_warmup:
|
||
# Fit AR(1) from expanding window up to t=i-1
|
||
denom = S_n * S_xx - S_x * S_x
|
||
if abs(denom) > 1e-14:
|
||
a1 = (S_n * S_xy - S_x * S_y) / denom
|
||
a0 = (S_y - a1 * S_x) / S_n
|
||
else:
|
||
a0, a1 = 0.0, 0.0
|
||
# Residual at bar i: actual r[i] minus AR(1) prediction
|
||
pred = a0 + a1 * r[i - 1]
|
||
residuals[i] = r[i] - pred
|
||
# else: residuals[i] remains 0
|
||
|
||
# Update running sums with the new observation pair (r[i-1], r[i])
|
||
# This is data point for t=i: x=r[i-1], y=r[i]
|
||
S_n += 1.0
|
||
S_x += r[i - 1]
|
||
S_y += r[i]
|
||
S_xx += r[i - 1] ** 2
|
||
S_xy += r[i - 1] * r[i]
|
||
|
||
# Z-score the residual with rolling window
|
||
z = al.zscore(residuals, zscore_win)
|
||
|
||
# Mean-reversion: negative of z-score
|
||
direction = -z
|
||
direction = np.nan_to_num(direction, nan=0.0)
|
||
|
||
# Vol-target to 20% annualized, cap at 2x leverage
|
||
target = al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
|
||
return target
|
||
|
||
|
||
def make_target(zscore_win: int):
|
||
return lambda df: ar1_residual_target(df, zscore_win=zscore_win)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# Small internal grid: 2 z-score windows × 2 TFs = 4 cells per config
|
||
# Pick best by min-asset holdout Sharpe
|
||
configs = [
|
||
{"zscore_win": 60, "label": "z60"},
|
||
{"zscore_win": 120, "label": "z120"},
|
||
]
|
||
tfs = ("1d", "12h")
|
||
|
||
best_rep = None
|
||
best_score = -9.0
|
||
|
||
for cfg in configs:
|
||
zw = cfg["zscore_win"]
|
||
rep = al.study_weights(
|
||
f"STA08-AR1resid-z{zw}",
|
||
make_target(zw),
|
||
tfs=tfs,
|
||
)
|
||
score = rep["verdict"].get("best_holdout_sharpe", -9.0)
|
||
if score > best_score:
|
||
best_score = score
|
||
best_rep = rep
|
||
# Print intermediate for debug
|
||
print(f"\n--- Config z{zw} ---")
|
||
print(al.fmt(rep))
|
||
|
||
print("\n\n=== BEST CONFIG ===")
|
||
print(al.fmt(best_rep))
|
||
print("JSON:", al.as_json(best_rep))
|