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>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
"""VOL09 — EWMA vol-forecast sizing (RiskMetrics vs rolling)
|
||||
|
||||
HYPOTHESIS: Use EWMA (RiskMetrics lambda=0.94) to forecast next-bar realized vol
|
||||
instead of a simple rolling window. Size a long-only position proportionally to
|
||||
target_vol / ewma_vol_forecast. Compare to simple rolling baseline.
|
||||
|
||||
Strategy:
|
||||
- Long-only on BTC/ETH (crypto trends upward, short adds drawdown)
|
||||
- Trend direction: TSMOM (1-3-6 month blend), flat if negative
|
||||
- Sizing: target_vol / ewma_vol_forecast (capped at leverage_cap)
|
||||
- EWMA lambda = 0.94 (RiskMetrics standard) vs rolling 30d baseline
|
||||
- Config grid: (lambda, target_vol) x 2 options each = 4 combinations
|
||||
"""
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
|
||||
import altlib as al
|
||||
import numpy as np
|
||||
|
||||
|
||||
def ewma_vol(returns: np.ndarray, lam: float, bars_per_year: float) -> np.ndarray:
|
||||
"""Compute EWMA variance forecast (RiskMetrics style), return annualized vol.
|
||||
sigma2[0] = returns[0]^2
|
||||
sigma2[i] = lambda * sigma2[i-1] + (1-lambda) * r[i-1]^2 (causal: use r[i-1])
|
||||
|
||||
This is the one-step-ahead forecast: sigma2[i] is the forecast for bar i
|
||||
using returns up to r[i-1]. Fully causal.
|
||||
"""
|
||||
n = len(returns)
|
||||
sigma2 = np.zeros(n)
|
||||
# Initialize with first return squared
|
||||
if n > 0:
|
||||
sigma2[0] = returns[0] ** 2 if returns[0] != 0 else 1e-6
|
||||
for i in range(1, n):
|
||||
sigma2[i] = lam * sigma2[i - 1] + (1 - lam) * returns[i - 1] ** 2
|
||||
# Annualize: daily vol = sqrt(sigma2), annualized = daily_vol * sqrt(bars_per_year)
|
||||
vol = np.sqrt(np.maximum(sigma2, 1e-12)) * np.sqrt(bars_per_year)
|
||||
return vol
|
||||
|
||||
|
||||
def tsmom_direction(df, bpd: int) -> np.ndarray:
|
||||
"""Multi-horizon TSMOM signal (1-3-6 month blend), long-only (0 or 1)."""
|
||||
c = df["close"].values
|
||||
n = len(c)
|
||||
d = np.zeros(n)
|
||||
for months in (1, 3, 6):
|
||||
h = int(months * 30 * bpd)
|
||||
s = np.zeros(n)
|
||||
if h < n:
|
||||
s[h:] = np.sign(c[h:] / c[:-h] - 1.0)
|
||||
d += np.nan_to_num(s)
|
||||
# Long-only: clip direction to [0, 1]
|
||||
return np.clip(np.sign(d), 0, None)
|
||||
|
||||
|
||||
def make_ewma_target(lam: float, target_vol: float, leverage_cap: float = 2.0):
|
||||
"""Factory: returns a target_fn(df) for EWMA-vol-sized TSMOM."""
|
||||
def target_fn(df):
|
||||
c = df["close"].values.astype(float)
|
||||
bpd = al.bars_per_day(df)
|
||||
bpy = bpd * 365.25
|
||||
|
||||
r = al.simple_returns(c)
|
||||
|
||||
# Causal EWMA vol forecast
|
||||
vol_forecast = ewma_vol(r, lam, bpy)
|
||||
|
||||
# TSMOM direction (long-only)
|
||||
direction = tsmom_direction(df, bpd)
|
||||
|
||||
# Vol-targeted sizing
|
||||
scal = np.where(
|
||||
(vol_forecast > 0) & np.isfinite(vol_forecast),
|
||||
target_vol / vol_forecast,
|
||||
0.0
|
||||
)
|
||||
tgt = np.clip(direction * scal, 0.0, leverage_cap)
|
||||
tgt[~np.isfinite(tgt)] = 0.0
|
||||
return tgt
|
||||
return target_fn
|
||||
|
||||
|
||||
def make_rolling_target(vol_win_days: int, target_vol: float, leverage_cap: float = 2.0):
|
||||
"""Baseline: simple rolling vol sizing (same TSMOM direction)."""
|
||||
def target_fn(df):
|
||||
c = df["close"].values.astype(float)
|
||||
bpd = al.bars_per_day(df)
|
||||
bpy = bpd * 365.25
|
||||
|
||||
r = al.simple_returns(c)
|
||||
|
||||
# Rolling realized vol
|
||||
vol = al.realized_vol(r, max(2, vol_win_days * bpd), bpy)
|
||||
|
||||
# TSMOM direction
|
||||
direction = tsmom_direction(df, bpd)
|
||||
|
||||
scal = np.where((vol > 0) & np.isfinite(vol), target_vol / vol, 0.0)
|
||||
tgt = np.clip(direction * scal, 0.0, leverage_cap)
|
||||
tgt[~np.isfinite(tgt)] = 0.0
|
||||
return tgt
|
||||
return target_fn
|
||||
|
||||
|
||||
# ---- Internal grid: 4 configs, 2 TFs = 8 backtests (just within 6 per TF pair) ---
|
||||
# We test EWMA lambda in {0.94, 0.97} x target_vol {0.20} = 2 EWMA configs
|
||||
# + 1 rolling baseline, across TFs (1d, 12h) = total 6 runs
|
||||
configs = [
|
||||
("EWMA-lam0.94-tv20", make_ewma_target(lam=0.94, target_vol=0.20)),
|
||||
("EWMA-lam0.97-tv20", make_ewma_target(lam=0.97, target_vol=0.20)),
|
||||
("ROLLING-30d-tv20", make_rolling_target(vol_win_days=30, target_vol=0.20)),
|
||||
]
|
||||
|
||||
TFS = ("1d", "12h")
|
||||
|
||||
# Run all configs on 1d only first to pick best, then run best on both TFs
|
||||
results = {}
|
||||
for cfg_name, cfg_fn in configs:
|
||||
rep = al.study_weights(f"VOL09/{cfg_name}", cfg_fn, tfs=("1d",))
|
||||
best_cell = rep["cells"][0] # only 1d
|
||||
results[cfg_name] = {
|
||||
"rep": rep,
|
||||
"min_full": best_cell["min_asset_full_sharpe"],
|
||||
"min_hold": best_cell["min_asset_holdout_sharpe"],
|
||||
"fee_ok": best_cell["fee_survives"],
|
||||
"fn": cfg_fn,
|
||||
}
|
||||
print(f"[1d] {cfg_name}: fullSh={best_cell['min_asset_full_sharpe']:+.3f} "
|
||||
f"holdSh={best_cell['min_asset_holdout_sharpe']:+.3f} feeOK={best_cell['fee_survives']}")
|
||||
|
||||
# Pick best config by hold-out Sharpe
|
||||
best_name = max(results, key=lambda k: results[k]["min_hold"])
|
||||
best_fn = results[best_name]["fn"]
|
||||
print(f"\nBest config: {best_name}")
|
||||
|
||||
# Run best config on both TFs for final report
|
||||
rep = al.study_weights(f"VOL09 [{best_name}]", best_fn, tfs=TFS)
|
||||
print(al.fmt(rep))
|
||||
print("JSON:", al.as_json(rep))
|
||||
Reference in New Issue
Block a user