refactor: riorganizzazione script — Strategy ABC, folder strategies/waste/analysis
- 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>
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
"""S2-03: Volatility Selling — Straddle/Strangle corto simulato.
|
||||
La IV crypto è cronicamente sopra la realized vol → vendere premium è profittevole.
|
||||
Simulazione: vendi straddle ATM → profitto = max(0, premium - |move|).
|
||||
Premium stimato da IV storica. Ingresso giornaliero.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy.stats import norm
|
||||
from src.data.downloader import load_data
|
||||
|
||||
FEE = 0.001
|
||||
INITIAL = 1000
|
||||
|
||||
|
||||
def realized_vol(close: np.ndarray, window: int = 24) -> np.ndarray:
|
||||
"""Annualized realized volatility rolling."""
|
||||
log_ret = np.diff(np.log(np.where(close == 0, 1e-10, close)))
|
||||
result = np.full(len(close), 0.5)
|
||||
for i in range(window, len(log_ret)):
|
||||
rv = np.std(log_ret[i - window : i]) * np.sqrt(24 * 365)
|
||||
result[i + 1] = rv
|
||||
return result
|
||||
|
||||
|
||||
def implied_vol_proxy(close: np.ndarray, window: int = 48) -> np.ndarray:
|
||||
"""IV proxy: realized vol * premium factor.
|
||||
Storicamente IV crypto ≈ 1.2-1.5x realized vol (variance risk premium).
|
||||
"""
|
||||
rv = realized_vol(close, window)
|
||||
# Premium factor varia: alto in panic, basso in calma
|
||||
result = np.full(len(close), 0.5)
|
||||
for i in range(window, len(close)):
|
||||
short_rv = realized_vol(close[max(0, i-12):i+1], min(12, i))[-1] if i >= 12 else rv[i]
|
||||
if rv[i] > 0:
|
||||
regime = short_rv / rv[i]
|
||||
premium = 1.15 + 0.3 * max(0, regime - 1) # più alto in regime volatile
|
||||
else:
|
||||
premium = 1.2
|
||||
result[i] = rv[i] * premium
|
||||
return result
|
||||
|
||||
|
||||
def bs_straddle_price(spot: float, iv: float, dte_hours: float) -> float:
|
||||
"""Black-Scholes straddle price (call + put ATM)."""
|
||||
if dte_hours <= 0 or iv <= 0:
|
||||
return 0
|
||||
t = dte_hours / (24 * 365)
|
||||
d1 = (0.5 * iv * iv * t) / (iv * np.sqrt(t))
|
||||
call = spot * (2 * norm.cdf(d1) - 1)
|
||||
return call * 2 # straddle = 2 * ATM call (approx for ATM)
|
||||
|
||||
|
||||
def run_vol_selling(asset):
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} — VOLATILITY SELLING (SHORT STRADDLE)")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df = load_data(asset, "1h")
|
||||
close = df["close"].values
|
||||
n = len(close)
|
||||
split = int(n * 0.7)
|
||||
timestamps = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
|
||||
rv = realized_vol(close, 24)
|
||||
iv_proxy = implied_vol_proxy(close)
|
||||
|
||||
configs = [
|
||||
# (dte_hours, iv_floor, iv_rv_ratio_min, position_pct, name)
|
||||
(24, 0.3, 1.15, 0.1, "daily_24h"),
|
||||
(12, 0.3, 1.15, 0.08, "half_day_12h"),
|
||||
(48, 0.3, 1.10, 0.12, "2day_48h"),
|
||||
(24, 0.4, 1.20, 0.1, "daily_highIV"),
|
||||
(8, 0.25, 1.10, 0.06, "ultra_short_8h"),
|
||||
(24, 0.3, 1.30, 0.15, "daily_bigPremium"),
|
||||
]
|
||||
|
||||
for dte, iv_floor, ratio_min, pos_pct, name in configs:
|
||||
capital = float(INITIAL)
|
||||
correct = 0
|
||||
total = 0
|
||||
daily_trades = {}
|
||||
|
||||
for i in range(max(split, 50), n - dte):
|
||||
day = timestamps[i].strftime("%Y-%m-%d")
|
||||
if daily_trades.get(day, 0) >= 1:
|
||||
continue
|
||||
|
||||
hour = timestamps[i].dt.hour if hasattr(timestamps[i], 'dt') else timestamps.iloc[i].hour
|
||||
if hour != 8: # entrata alle 08 UTC ogni giorno
|
||||
continue
|
||||
|
||||
current_iv = iv_proxy[i]
|
||||
current_rv = rv[i]
|
||||
|
||||
if current_iv < iv_floor:
|
||||
continue
|
||||
if current_rv > 0 and current_iv / current_rv < ratio_min:
|
||||
continue
|
||||
|
||||
spot = close[i]
|
||||
premium = bs_straddle_price(spot, current_iv, dte)
|
||||
premium_pct = premium / spot
|
||||
|
||||
# Actual move during holding period
|
||||
exit_idx = min(i + dte, n - 1)
|
||||
actual_move = abs(close[exit_idx] - spot)
|
||||
actual_move_pct = actual_move / spot
|
||||
|
||||
# P&L: premium received - actual move (capped at max loss)
|
||||
max_loss = spot * 0.05 # cap loss at 5% of spot
|
||||
pnl = premium - min(actual_move, max_loss + premium)
|
||||
|
||||
pnl_on_capital = pnl / spot * pos_pct
|
||||
fee_cost = FEE * 4 * pos_pct # 4 legs: sell call, sell put, buy back
|
||||
net_pnl = pnl_on_capital - fee_cost
|
||||
|
||||
capital += capital * net_pnl
|
||||
capital = max(capital, 0)
|
||||
|
||||
total += 1
|
||||
if pnl > 0:
|
||||
correct += 1
|
||||
daily_trades[day] = daily_trades.get(day, 0) + 1
|
||||
|
||||
if total < 20:
|
||||
continue
|
||||
|
||||
acc = correct / total * 100
|
||||
ret = (capital - INITIAL) / INITIAL * 100
|
||||
test_days = (n - split) / 24
|
||||
test_years = test_days / 365.25
|
||||
ann = ((capital / INITIAL) ** (1 / test_years) - 1) * 100 if test_years > 0 and capital > 0 else -100
|
||||
dpnl = (capital - INITIAL) / test_days if test_days > 0 else 0
|
||||
days_active = len(daily_trades)
|
||||
|
||||
tag = "✅" if acc >= 60 and ann >= 30 else ""
|
||||
print(f" {name:20s}: trades={total:4d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}% €/day={dpnl:.2f} active={days_active} {tag}")
|
||||
|
||||
|
||||
for asset in ["ETH", "BTC"]:
|
||||
run_vol_selling(asset)
|
||||
Reference in New Issue
Block a user