test(strategy2): VRP + filtri honest — 69% acc max, squeeze filter non aiuta

Regime filter migliore (+1% acc). Tutti gli anni positivi 2018-2026.
Max realistico: 69.3% acc, 84% ann, 3.2% DD.
80% accuracy non raggiungibile con VRP puro.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 10:51:42 +02:00
parent e7be299b27
commit f6e111f72d
+297
View File
@@ -0,0 +1,297 @@
"""S2-10: VRP + filtri multipli per alzare accuracy.
Filtri testati:
1. NO vol sell se squeeze attivo (compressione → breakout imminente, NON vendere vol)
2. NO vol sell se RV short-term > RV long-term (regime esplosivo)
3. NO vol sell se move delle ultime 4h > 2% (momentum in corso)
4. NO vol sell se volume spike > 2x media (evento in corso)
5. COMBINAZIONI dei filtri sopra
Test per-anno, NO compounding per PnL medio, compounding a fine report.
"""
from __future__ import annotations
import sys
sys.path.insert(0, ".")
import numpy as np
import pandas as pd
from src.data.downloader import load_data
FEE_ROUNDTRIP = 0.0052
INITIAL = 1000
def rv_ann(close, window):
lr = np.diff(np.log(np.where(close == 0, 1e-10, close)))
r = np.full(len(close), np.nan)
for i in range(window, len(lr)):
r[i + 1] = np.std(lr[i - window : i]) * np.sqrt(24 * 365)
return r
def keltner_ratio(close, high, low, window=14):
n = len(close)
result = np.full(n, np.nan)
for i in range(window, n):
wc = close[i - window : i]
wh = high[i - window : i]
wl = low[i - window : i]
ma = np.mean(wc)
bb_std = np.std(wc)
tr = np.maximum(wh - wl, np.maximum(np.abs(wh - np.roll(wc, 1)), np.abs(wl - np.roll(wc, 1))))
atr = np.mean(tr[1:])
kc_r = (ma + 1.5 * atr) - (ma - 1.5 * atr)
bb_r = (ma + 2 * bb_std) - (ma - 2 * bb_std)
if kc_r > 0:
result[i] = bb_r / kc_r
return result
def straddle_prem(iv, dte_h):
if iv <= 0 or dte_h <= 0:
return 0
return iv * np.sqrt(dte_h / (24 * 365)) * 0.8
def run_filtered(asset, dte=48):
print(f"\n{'='*75}")
print(f" {asset} — VRP + FILTRI (DTE={dte}h)")
print(f"{'='*75}")
df = load_data(asset, "1h")
close = df["close"].values
high = df["high"].values
low = df["low"].values
volume = df["volume"].values
n = len(close)
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
rv_24 = rv_ann(close, 24)
rv_168 = rv_ann(close, 168)
kcr = keltner_ratio(close, high, low, 14)
# Pre-calcolo filtri
vol_avg_48 = np.full(n, np.nan)
for i in range(48, n):
vol_avg_48[i] = np.mean(volume[i - 48 : i])
ret_4h = np.full(n, 0.0)
for i in range(4, n):
ret_4h[i] = abs(close[i] - close[i - 4]) / close[i - 4]
filter_configs = [
# (name, use_squeeze, use_regime, use_momentum, use_volume, squeeze_thr, regime_thr, mom_thr, vol_thr)
("BASELINE (no filter)", False, False, False, False, 0, 0, 0, 0),
("squeeze_only", True, False, False, False, 0.85, 0, 0, 0),
("regime_only", False, True, False, False, 0, 1.3, 0, 0),
("momentum_only", False, False, True, False, 0, 0, 0.02, 0),
("volume_only", False, False, False, True, 0, 0, 0, 2.0),
("squeeze+regime", True, True, False, False, 0.85, 1.3, 0, 0),
("squeeze+momentum", True, False, True, False, 0.85, 0, 0.02, 0),
("squeeze+volume", True, False, False, True, 0.85, 0, 0, 2.0),
("regime+momentum", False, True, True, False, 0, 1.3, 0.02, 0),
("ALL_FILTERS", True, True, True, True, 0.85, 1.3, 0.02, 2.0),
("squeeze+regime+mom", True, True, True, False, 0.85, 1.3, 0.02, 0),
("squeeze_tight", True, False, False, False, 0.80, 0, 0, 0),
("squeeze_tight+regime", True, True, False, False, 0.80, 1.3, 0, 0),
("regime_strict", False, True, False, False, 0, 1.2, 0, 0),
("mom_strict", False, False, True, False, 0, 0, 0.015, 0),
("squeeze_tight+mom_strict", True, False, True, False, 0.80, 0, 0.015, 0),
("ALL_TIGHT", True, True, True, True, 0.80, 1.2, 0.015, 1.8),
]
results_table = []
for name, f_sq, f_reg, f_mom, f_vol, sq_thr, reg_thr, mom_thr, vol_thr in filter_configs:
all_pnls = []
yearly = {}
for i in range(170, n - dte):
if ts.iloc[i].hour != 8:
continue
rv_s = rv_24[i]
rv_l = rv_168[i]
if np.isnan(rv_s) or np.isnan(rv_l) or rv_s < 0.05 or rv_l < 0.05:
continue
# === FILTRI ===
skip = False
if f_sq and not np.isnan(kcr[i]):
in_squeeze = kcr[i] < sq_thr
# Controlla se squeeze nelle ultime 5 barre
recent_squeeze = any(not np.isnan(kcr[j]) and kcr[j] < sq_thr for j in range(max(0, i - 5), i + 1))
if recent_squeeze:
skip = True
if f_reg and rv_l > 0:
if rv_s / rv_l > reg_thr:
skip = True
if f_mom:
if ret_4h[i] > mom_thr:
skip = True
if f_vol and not np.isnan(vol_avg_48[i]) and vol_avg_48[i] > 0:
if volume[i] > vol_avg_48[i] * vol_thr:
skip = True
if skip:
continue
# === TRADE ===
regime = rv_s / rv_l if rv_l > 0 else 1.0
if regime > 2.0:
iv_pf = 0.9
elif regime > 1.5:
iv_pf = 1.0
elif regime > 1.0:
iv_pf = 1.1
else:
iv_pf = 1.2
iv = rv_l * iv_pf
prem = straddle_prem(iv, dte)
spot = close[i]
exit_idx = min(i + dte, n - 1)
actual_move = abs(close[exit_idx] - spot) / spot
pos_size = INITIAL * 0.10
if actual_move <= prem:
raw = (prem - actual_move) * pos_size
else:
raw = -(actual_move - prem) * pos_size
raw = max(raw, -pos_size * 0.05)
net = raw - FEE_ROUNDTRIP * pos_size
all_pnls.append(net)
year = ts.iloc[i].year
if year not in yearly:
yearly[year] = []
yearly[year].append(net)
if len(all_pnls) < 50:
continue
wins = sum(1 for p in all_pnls if p > 0)
acc = wins / len(all_pnls) * 100
avg_pnl = np.mean(all_pnls)
tot_pnl = np.sum(all_pnls)
worst_trade = np.min(all_pnls)
avg_win = np.mean([p for p in all_pnls if p > 0]) if wins > 0 else 0
avg_loss = np.mean([p for p in all_pnls if p <= 0]) if wins < len(all_pnls) else 0
# Worst year
worst_year_acc = 100
worst_year_name = ""
for y, ypnls in sorted(yearly.items()):
yw = sum(1 for p in ypnls if p > 0) / len(ypnls) * 100 if ypnls else 0
if yw < worst_year_acc:
worst_year_acc = yw
worst_year_name = str(y)
# Compounded return
capital = float(INITIAL)
peak = capital
max_dd = 0
for pnl in all_pnls:
capital += pnl * (capital / INITIAL)
capital = max(capital, 10)
if capital > peak:
peak = capital
dd = (peak - capital) / peak
max_dd = max(max_dd, dd)
n_years = len(yearly)
ann = ((capital / INITIAL) ** (1 / n_years) - 1) * 100 if capital > 0 and n_years > 0 else -100
results_table.append({
"name": name,
"trades": len(all_pnls),
"acc": acc,
"avg_pnl": avg_pnl,
"avg_win": avg_win,
"avg_loss": avg_loss,
"ann": ann,
"max_dd": max_dd * 100,
"worst_year": f"{worst_year_name}({worst_year_acc:.0f}%)",
"capital": capital,
})
# Sort by accuracy
results_table.sort(key=lambda x: x["acc"], reverse=True)
print(f"\n {'Name':.<30s} {'Trades':>7s} {'Acc':>6s} {'AvgPnL':>8s} {'AvgWin':>8s} {'AvgLoss':>8s} {'Ann%':>7s} {'DD%':>5s} {'Worst_Yr':>12s} {'Capital':>10s}")
print(f" {'-'*105}")
for r in results_table:
tag = "✅✅" if r["acc"] >= 75 else "" if r["acc"] >= 70 else ""
print(f" {r['name']:.<30s} {r['trades']:>7d} {r['acc']:>5.1f}% {r['avg_pnl']:>+7.2f}{r['avg_win']:>+7.2f}{r['avg_loss']:>+7.2f}{r['ann']:>+6.1f}% {r['max_dd']:>4.1f}% {r['worst_year']:>12s}{r['capital']:>9,.0f} {tag}")
# Dettaglio per anno del migliore
best = results_table[0]
print(f"\n MIGLIORE: {best['name']}{best['acc']:.1f}% acc, {best['ann']:+.1f}% ann, DD {best['max_dd']:.1f}%")
# Rerun best per year
best_name = best["name"]
best_cfg = None
for cfg in filter_configs:
if cfg[0] == best_name:
best_cfg = cfg
break
if best_cfg:
_, f_sq, f_reg, f_mom, f_vol, sq_thr, reg_thr, mom_thr, vol_thr = best_cfg
yearly_detail = {}
for i in range(170, n - dte):
if ts.iloc[i].hour != 8:
continue
rv_s = rv_24[i]
rv_l = rv_168[i]
if np.isnan(rv_s) or np.isnan(rv_l) or rv_s < 0.05 or rv_l < 0.05:
continue
skip = False
if f_sq:
if any(not np.isnan(kcr[j]) and kcr[j] < sq_thr for j in range(max(0, i - 5), i + 1)):
skip = True
if f_reg and rv_l > 0 and rv_s / rv_l > reg_thr:
skip = True
if f_mom and ret_4h[i] > mom_thr:
skip = True
if f_vol and not np.isnan(vol_avg_48[i]) and vol_avg_48[i] > 0 and volume[i] > vol_avg_48[i] * vol_thr:
skip = True
if skip:
continue
regime = rv_s / rv_l if rv_l > 0 else 1.0
iv_pf = {True: 0.9, False: 1.2}[regime > 2.0] if regime > 2.0 else (1.0 if regime > 1.5 else (1.1 if regime > 1.0 else 1.2))
iv = rv_l * iv_pf
prem = straddle_prem(iv, dte)
spot = close[i]
exit_idx = min(i + dte, n - 1)
move = abs(close[exit_idx] - spot) / spot
pos_size = INITIAL * 0.10
if move <= prem:
raw = (prem - move) * pos_size
else:
raw = max(-(move - prem) * pos_size, -pos_size * 0.05)
net = raw - FEE_ROUNDTRIP * pos_size
year = ts.iloc[i].year
if year not in yearly_detail:
yearly_detail[year] = []
yearly_detail[year].append(net)
print(f"\n Dettaglio per anno ({best_name}):")
for y in sorted(yearly_detail):
pnls = yearly_detail[y]
w = sum(1 for p in pnls if p > 0)
a = w / len(pnls) * 100
tag = " ← CRASH" if y in [2020, 2021, 2022] else ""
print(f" {y}: trades={len(pnls):4d} acc={a:.1f}% avg=€{np.mean(pnls):+.2f} tot=€{np.sum(pnls):+.0f}{tag}")
for asset in ["ETH", "BTC"]:
run_filtered(asset, dte=48)
run_filtered(asset, dte=24)