From f6e111f72d32961d03fbb60541afd5ea5b44b776 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Wed, 27 May 2026 10:51:42 +0200 Subject: [PATCH] =?UTF-8?q?test(strategy2):=20VRP=20+=20filtri=20honest=20?= =?UTF-8?q?=E2=80=94=2069%=20acc=20max,=20squeeze=20filter=20non=20aiuta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- scripts/s2_10_vrp_filtered.py | 297 ++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 scripts/s2_10_vrp_filtered.py diff --git a/scripts/s2_10_vrp_filtered.py b/scripts/s2_10_vrp_filtered.py new file mode 100644 index 0000000..ef9a828 --- /dev/null +++ b/scripts/s2_10_vrp_filtered.py @@ -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)