Files
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

112 lines
4.7 KiB
Python
Raw Permalink 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.
"""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))