0e47956f7a
- src/strategies/base.py: Strategy ABC con Signal, BacktestResult, YearlyStats - src/strategies/indicators.py: keltner_ratio, detect_squeezes, ema, atr, rv, corr - scripts/strategies/: SQ01-SQ04 (squeeze puro/filtri), ML01 (squeeze+GBM) - scripts/waste/: W01-W22 script scartati + REF originali - scripts/analysis/: compare, best_yearly, final_report, paper_status - CLAUDE.md aggiornato con nuova struttura e tabella strategie Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
182 lines
6.0 KiB
Python
182 lines
6.0 KiB
Python
"""S2-09: VRP test per-anno — verità nuda.
|
||
Test su OGNI anno separatamente per vedere performance durante crash.
|
||
Niente compounding — PnL medio per trade in punti percentuali.
|
||
Costi realistici Deribit options.
|
||
"""
|
||
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 # 0.52% roundtrip (4 legs × 0.13%)
|
||
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 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_per_year(asset, dte=24):
|
||
print(f"\n{'='*70}")
|
||
print(f" {asset} — VRP PER ANNO (DTE={dte}h, NO compounding)")
|
||
print(f" Fee roundtrip: {FEE_ROUNDTRIP*100:.2f}%")
|
||
print(f"{'='*70}")
|
||
|
||
df = load_data(asset, "1h")
|
||
close = df["close"].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)
|
||
|
||
# IV/RV premium: conservative estimate per regime
|
||
# Storicamene crypto VRP ≈ 15-30% (IV/RV ≈ 1.15-1.30)
|
||
# Ma durante crash VRP va NEGATIVO (RV > IV)
|
||
|
||
years = sorted(set(ts.dt.year))
|
||
|
||
print(f"\n {'Year':>6s} {'Trades':>7s} {'Wins':>5s} {'Acc%':>6s} {'AvgPnL%':>9s} {'TotPnL€':>9s} {'Worst%':>8s} {'MaxMove%':>9s}")
|
||
print(f" {'-'*70}")
|
||
|
||
all_pnls = []
|
||
yearly_stats = []
|
||
|
||
for year in years:
|
||
year_mask = ts.dt.year == year
|
||
year_indices = np.where(year_mask.values)[0]
|
||
|
||
if len(year_indices) < 200:
|
||
continue
|
||
|
||
trades_pnl = []
|
||
trades_detail = []
|
||
|
||
for i in year_indices:
|
||
if i < 170 or i + dte >= n:
|
||
continue
|
||
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
|
||
|
||
# IV estimate: regime-dependent
|
||
regime = rv_s / rv_l if rv_l > 0 else 1.0
|
||
|
||
if regime > 2.0:
|
||
# CRASH: RV esplosa, IV probabilmente = RV o meno
|
||
iv_premium_factor = 0.9
|
||
elif regime > 1.5:
|
||
iv_premium_factor = 1.0
|
||
elif regime > 1.0:
|
||
iv_premium_factor = 1.1
|
||
else:
|
||
# Calm: VRP positivo
|
||
iv_premium_factor = 1.2
|
||
|
||
iv = rv_l * iv_premium_factor
|
||
prem = straddle_prem(iv, dte)
|
||
|
||
spot = close[i]
|
||
exit_idx = min(i + dte, n - 1)
|
||
actual_move = abs(close[exit_idx] - spot) / spot
|
||
|
||
# P&L (senza compounding — flat € su €1000)
|
||
pos_size = INITIAL * 0.10 # 10% fisso, no leverage
|
||
if actual_move <= prem:
|
||
raw_pnl = (prem - actual_move) * pos_size
|
||
else:
|
||
raw_pnl = -(actual_move - prem) * pos_size
|
||
raw_pnl = max(raw_pnl, -pos_size * 0.05) # cap loss
|
||
|
||
cost = FEE_ROUNDTRIP * pos_size
|
||
net_pnl = raw_pnl - cost
|
||
|
||
trades_pnl.append(net_pnl)
|
||
trades_detail.append({
|
||
"prem": prem,
|
||
"move": actual_move,
|
||
"regime": regime,
|
||
"rv_s": rv_s,
|
||
"iv": iv,
|
||
})
|
||
all_pnls.append(net_pnl)
|
||
|
||
if not trades_pnl:
|
||
continue
|
||
|
||
wins = sum(1 for p in trades_pnl if p > 0)
|
||
acc = wins / len(trades_pnl) * 100
|
||
avg_pnl = np.mean(trades_pnl)
|
||
tot_pnl = np.sum(trades_pnl)
|
||
worst = np.min(trades_pnl)
|
||
max_move = max(t["move"] for t in trades_detail) * 100
|
||
|
||
tag = ""
|
||
if year in [2020, 2021, 2022]:
|
||
tag = " ← CRASH YEAR"
|
||
if acc >= 70 and avg_pnl > 0:
|
||
tag += " ✅"
|
||
|
||
print(f" {year:>6d} {len(trades_pnl):>7d} {wins:>5d} {acc:>5.1f}% {avg_pnl:>+8.2f}€ {tot_pnl:>+8.0f}€ {worst:>+7.2f}€ {max_move:>8.1f}% {tag}")
|
||
|
||
yearly_stats.append({
|
||
"year": year, "trades": len(trades_pnl), "acc": acc,
|
||
"avg_pnl": avg_pnl, "tot_pnl": tot_pnl, "worst": worst,
|
||
})
|
||
|
||
# Summary
|
||
if all_pnls:
|
||
total_trades = len(all_pnls)
|
||
total_wins = sum(1 for p in all_pnls if p > 0)
|
||
print(f"\n {'TOTALE':>6s} {total_trades:>7d} {total_wins:>5d} {total_wins/total_trades*100:>5.1f}% {np.mean(all_pnls):>+8.2f}€ {np.sum(all_pnls):>+8.0f}€ {np.min(all_pnls):>+7.2f}€")
|
||
|
||
# Con compounding realistico
|
||
capital = float(INITIAL)
|
||
peak = capital
|
||
max_dd = 0
|
||
for pnl in all_pnls:
|
||
capital += pnl * (capital / INITIAL) # scala con capitale
|
||
capital = max(capital, 10)
|
||
if capital > peak:
|
||
peak = capital
|
||
dd = (peak - capital) / peak
|
||
max_dd = max(max_dd, dd)
|
||
|
||
years_total = (yearly_stats[-1]["year"] - yearly_stats[0]["year"] + 1)
|
||
ann = ((capital / INITIAL) ** (1 / years_total) - 1) * 100 if capital > 0 else -100
|
||
daily_avg = (capital - INITIAL) / (total_trades) # approx 1 trade/day
|
||
|
||
print(f"\n CON COMPOUNDING:")
|
||
print(f" Capitale finale: €{capital:,.0f}")
|
||
print(f" ROI annualizzato: {ann:+.1f}%")
|
||
print(f" Max Drawdown: {max_dd*100:.1f}%")
|
||
print(f" €/trade medio: €{daily_avg:.2f}")
|
||
|
||
# Worst year
|
||
worst_year = min(yearly_stats, key=lambda x: x["tot_pnl"])
|
||
best_year = max(yearly_stats, key=lambda x: x["tot_pnl"])
|
||
print(f"\n Anno peggiore: {worst_year['year']} → {worst_year['tot_pnl']:+.0f}€ ({worst_year['acc']:.0f}% acc)")
|
||
print(f" Anno migliore: {best_year['year']} → {best_year['tot_pnl']:+.0f}€ ({best_year['acc']:.0f}% acc)")
|
||
|
||
|
||
for asset in ["ETH", "BTC"]:
|
||
for dte in [24, 48]:
|
||
run_per_year(asset, dte)
|