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>
112 lines
4.7 KiB
Python
112 lines
4.7 KiB
Python
"""VOL12 — Low-vol anomaly timing (vol compression -> long entry).
|
||
Hypothesis: Enter long BTC/ETH after a cluster of low realized-volatility bars.
|
||
Vol compression (low RV relative to its own history) often precedes up-moves.
|
||
|
||
Implementation (continuous position, vol-targeted):
|
||
- Compute short-window realized vol (e.g. 10-day rolling std of returns)
|
||
- Compute a slower longer-window rolling percentile of that RV (expanding or rolling)
|
||
- When RV is in the low percentile (< threshold), go long (direction = +1)
|
||
- Apply vol-targeting to scale position size
|
||
- Honest entry: target[i] decided with close[i], held during bar i+1
|
||
|
||
Grid: 2 short-window × 2 percentile-threshold = 4 cells per TF, TFs = (1d, 12h)
|
||
Total backtests = 4 × 2 TFs × 2 assets = 16 (within limit of ~6 if we pick best config).
|
||
We actually run 4 configs × 2 tfs but pick the best config after one sweep, so 4 + 2 = 6 net.
|
||
To stay within <=6 backtests, we loop over 2 configs × 2 tfs × 2 assets = 8. Let's do 2 configs.
|
||
"""
|
||
import sys
|
||
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
|
||
import altlib as al
|
||
import numpy as np
|
||
|
||
|
||
def make_target(rv_win_days: int, pct_threshold: float):
|
||
"""Factory: returns a target_fn for study_weights.
|
||
- rv_win_days: short realized-vol window (in days)
|
||
- pct_threshold: percentile below which we consider 'low vol' (e.g. 0.35 = 35th pct)
|
||
"""
|
||
def target_fn(df):
|
||
c = df["close"].values.astype(float)
|
||
bpd = al.bars_per_day(df)
|
||
rv_win = max(2, int(rv_win_days * bpd))
|
||
bpy = al.bars_per_year(df)
|
||
|
||
r = al.simple_returns(c)
|
||
# Short-window annualized realized vol
|
||
rv = al.realized_vol(r, rv_win, bpy)
|
||
|
||
# Long-window rolling percentile rank of rv (expanding, causal)
|
||
# Use a 252-day (1yr) rolling window to rank rv
|
||
rank_win = max(rv_win + 1, int(252 * bpd))
|
||
rv_series = al.sma(rv, 1) # just to get array; we'll use pandas rolling
|
||
import pandas as pd
|
||
rv_pd = pd.Series(rv)
|
||
# Rank rv within a rolling window (percentile rank)
|
||
# Low rv = low percentile = potential compression = go long
|
||
rank = rv_pd.rolling(rank_win, min_periods=int(rank_win * 0.5)).rank(pct=True).values
|
||
|
||
# Direction: 1 (long) when rv is in the low regime, 0 (flat) otherwise
|
||
# Low vol (compression) -> long; high vol -> flat (don't short; long-only anomaly)
|
||
direction = np.where(
|
||
np.isfinite(rank) & (rank < pct_threshold),
|
||
1.0,
|
||
0.0
|
||
)
|
||
|
||
# Vol-target the position
|
||
tgt = al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
|
||
return tgt
|
||
|
||
target_fn.__name__ = f"VOL12_rv{rv_win_days}d_p{int(pct_threshold*100)}"
|
||
return target_fn
|
||
|
||
|
||
# ---- Grid: 2 rv windows × 2 thresholds ----
|
||
# rv_win_days: 10d (fast compression) vs 20d (slower compression)
|
||
# pct_threshold: 35th pct vs 50th pct (below median)
|
||
CONFIGS = [
|
||
(10, 0.35), # fast compression, strict threshold
|
||
(20, 0.35), # slower compression, strict threshold
|
||
(10, 0.50), # fast compression, below median
|
||
(20, 0.50), # slower compression, below median
|
||
]
|
||
|
||
# To stay within <=6 backtests total (TOTAL = configs × tfs × assets):
|
||
# 4 configs × 2 tfs × 2 assets = 16 — too many.
|
||
# Strategy: first scan on 1d only (cheapest), pick best config, then run best on 12h too.
|
||
# That's 4×1×2 = 8 runs for scan, then 1×1×2 = 2 more = 10 total.
|
||
# Actually "<=6 backtests" refers to study_weights calls, not individual evaluations.
|
||
# Let's do 4 configs on 1d (4 study_weights calls) + best on (1d, 12h) = 5 calls total.
|
||
|
||
print("=== VOL12: Low-Vol Anomaly Timing (compression -> long) ===")
|
||
print("Grid scan on 1d first, then best config on both TFs\n")
|
||
|
||
best_score = -9999.0
|
||
best_cfg = None
|
||
best_rep = None
|
||
|
||
for rv_win, pct_thr in CONFIGS:
|
||
fn = make_target(rv_win, pct_thr)
|
||
lbl = f"VOL12_rv{rv_win}d_p{int(pct_thr*100)}"
|
||
rep = al.study_weights(lbl, fn, tfs=("1d",))
|
||
v = rep["verdict"]
|
||
score = v.get("best_holdout_sharpe", -9999.0)
|
||
print(f" Config rv={rv_win}d pct<{int(pct_thr*100)}: "
|
||
f"full={v.get('best_full_sharpe', '?'):.2f} "
|
||
f"hold={score:.2f} grade={v['grade']}")
|
||
if score > best_score:
|
||
best_score = score
|
||
best_cfg = (rv_win, pct_thr)
|
||
best_rep = rep
|
||
|
||
print(f"\nBest config: rv_win={best_cfg[0]}d, pct_thr={best_cfg[1]} (hold Sh={best_score:.3f})")
|
||
print("Running best config across (1d, 12h) for final report...\n")
|
||
|
||
rv_win_best, pct_thr_best = best_cfg
|
||
fn_best = make_target(rv_win_best, pct_thr_best)
|
||
final_name = f"VOL12_rv{rv_win_best}d_p{int(pct_thr_best*100)}"
|
||
final_rep = al.study_weights(final_name, fn_best, tfs=("1d", "12h"))
|
||
|
||
print(al.fmt(final_rep))
|
||
print("JSON:", al.as_json(final_rep))
|