Files
PythagorasGoal/scripts/research/alt/runs/STA08.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

131 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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))