Compare commits
6 Commits
2694a4a00c
...
7639e5012b
| Author | SHA1 | Date | |
|---|---|---|---|
| 7639e5012b | |||
| 5930f366d1 | |||
| 613c2ccda1 | |||
| f6e111f72d | |||
| e7be299b27 | |||
| a6056c4ac7 |
@@ -0,0 +1,160 @@
|
||||
"""S2-01: Mean Reversion oraria con filtro orario.
|
||||
Idea: crypto ha bias di ritorno alla media nelle ore notturne (00-06 UTC)
|
||||
e di momentum nelle ore diurne USA (14-20 UTC).
|
||||
- Compra quando RSI < 30 in ore notturne
|
||||
- Vendi quando RSI > 70 in ore notturne
|
||||
- Hold max 4h, stop loss 1.5%
|
||||
Timeframe: 1h. Ingresso quasi giornaliero.
|
||||
"""
|
||||
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 = 0.001
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
|
||||
def rsi(close: np.ndarray, period: int = 14) -> np.ndarray:
|
||||
delta = np.diff(close)
|
||||
gain = np.where(delta > 0, delta, 0)
|
||||
loss = np.where(delta < 0, -delta, 0)
|
||||
result = np.full(len(close), 50.0)
|
||||
avg_gain = np.mean(gain[:period])
|
||||
avg_loss = np.mean(loss[:period])
|
||||
for i in range(period, len(delta)):
|
||||
avg_gain = (avg_gain * (period - 1) + gain[i]) / period
|
||||
avg_loss = (avg_loss * (period - 1) + loss[i]) / period
|
||||
if avg_loss == 0:
|
||||
result[i + 1] = 100
|
||||
else:
|
||||
rs = avg_gain / avg_loss
|
||||
result[i + 1] = 100 - 100 / (1 + rs)
|
||||
return result
|
||||
|
||||
|
||||
def bollinger_pct(close: np.ndarray, window: int = 20) -> np.ndarray:
|
||||
result = np.full(len(close), 0.5)
|
||||
for i in range(window, len(close)):
|
||||
w = close[i - window : i]
|
||||
ma = np.mean(w)
|
||||
std = np.std(w)
|
||||
if std > 0:
|
||||
result[i] = (close[i] - (ma - 2 * std)) / (4 * std)
|
||||
return result
|
||||
|
||||
|
||||
def run_mean_reversion(asset, tf="1h"):
|
||||
df = load_data(asset, tf)
|
||||
close = df["close"].values
|
||||
high = df["high"].values
|
||||
low = df["low"].values
|
||||
n = len(df)
|
||||
|
||||
timestamps = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
hours = timestamps.dt.hour.values
|
||||
|
||||
rsi_vals = rsi(close, 14)
|
||||
bb_pct = bollinger_pct(close, 20)
|
||||
|
||||
split = int(n * 0.7)
|
||||
|
||||
configs = [
|
||||
# (rsi_low, rsi_high, allowed_hours, hold_max, stop_pct, name)
|
||||
(25, 75, list(range(0, 7)), 4, 0.015, "night_0-6_rsi25"),
|
||||
(30, 70, list(range(0, 7)), 4, 0.015, "night_0-6_rsi30"),
|
||||
(25, 75, list(range(0, 10)), 6, 0.02, "extended_0-9"),
|
||||
(30, 70, list(range(20, 24)) + list(range(0, 6)), 4, 0.015, "late_night"),
|
||||
(20, 80, list(range(0, 24)), 4, 0.015, "all_hours_rsi20"),
|
||||
# Bollinger band mean reversion
|
||||
]
|
||||
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} {tf} — MEAN REVERSION")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
for rsi_low, rsi_high, allowed, hold_max, stop, name in configs:
|
||||
capital = float(INITIAL)
|
||||
correct = 0
|
||||
total = 0
|
||||
daily_trades = {}
|
||||
|
||||
for i in range(max(split, 20), n - hold_max):
|
||||
hour = hours[i]
|
||||
if hour not in allowed:
|
||||
continue
|
||||
|
||||
day = timestamps[i].strftime("%Y-%m-%d")
|
||||
if daily_trades.get(day, 0) >= 2:
|
||||
continue
|
||||
|
||||
direction = None
|
||||
if rsi_vals[i] < rsi_low and bb_pct[i] < 0.2:
|
||||
direction = "long"
|
||||
elif rsi_vals[i] > rsi_high and bb_pct[i] > 0.8:
|
||||
direction = "short"
|
||||
|
||||
if direction is None:
|
||||
continue
|
||||
|
||||
entry = close[i]
|
||||
best_exit = i + 1
|
||||
for j in range(i + 1, min(i + hold_max + 1, n)):
|
||||
price = close[j]
|
||||
if direction == "long":
|
||||
pnl_pct = (price - entry) / entry
|
||||
if pnl_pct <= -stop:
|
||||
best_exit = j
|
||||
break
|
||||
if pnl_pct >= stop * 1.5:
|
||||
best_exit = j
|
||||
break
|
||||
else:
|
||||
pnl_pct = (entry - price) / entry
|
||||
if pnl_pct <= -stop:
|
||||
best_exit = j
|
||||
break
|
||||
if pnl_pct >= stop * 1.5:
|
||||
best_exit = j
|
||||
break
|
||||
best_exit = j
|
||||
|
||||
exit_price = close[best_exit]
|
||||
if direction == "long":
|
||||
trade_ret = (exit_price - entry) / entry
|
||||
else:
|
||||
trade_ret = (entry - exit_price) / entry
|
||||
|
||||
net = trade_ret * LEVERAGE - FEE * 2 * LEVERAGE
|
||||
capital += capital * 0.15 * net
|
||||
capital = max(capital, 0)
|
||||
|
||||
is_correct = trade_ret > 0
|
||||
total += 1
|
||||
if is_correct:
|
||||
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_with_trades = len(daily_trades)
|
||||
trades_per_day = total / days_with_trades if days_with_trades > 0 else 0
|
||||
|
||||
tag = "✅" if acc >= 60 and ann >= 30 else ""
|
||||
print(f" {name:25s}: trades={total:5d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}% dd_est ~{abs(min(0, ret/3)):.0f}% €/day={dpnl:.2f} days_active={days_with_trades} {tag}")
|
||||
|
||||
|
||||
for asset in ["ETH", "BTC"]:
|
||||
run_mean_reversion(asset, "1h")
|
||||
run_mean_reversion(asset, "15m")
|
||||
@@ -0,0 +1,129 @@
|
||||
"""S2-02: Funding Rate Strategy.
|
||||
Quando il funding rate è molto positivo → troppi long → short il perpetual.
|
||||
Quando molto negativo → troppi short → long il perpetual.
|
||||
Si cattura sia il mean reversion del prezzo che il funding rate stesso.
|
||||
Ingresso: quando funding > 0.03% o < -0.03% (8h rate).
|
||||
"""
|
||||
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 = 0.001
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
|
||||
def simulate_funding_strategy(asset):
|
||||
"""Simula funding rate strategy usando il proxy: overnight returns.
|
||||
Crypto funding settlement ogni 8h → prezzo tende a correggersi dopo settlement.
|
||||
Proxy: se ultime 8h hanno avuto forte trend, aspettati reversal dopo settlement.
|
||||
"""
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} — FUNDING RATE PROXY STRATEGY")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df_1h = load_data(asset, "1h")
|
||||
close = df_1h["close"].values
|
||||
volume = df_1h["volume"].values
|
||||
n = len(close)
|
||||
split = int(n * 0.7)
|
||||
|
||||
timestamps = pd.to_datetime(df_1h["timestamp"], unit="ms", utc=True)
|
||||
hours = timestamps.dt.hour.values
|
||||
|
||||
# Funding settlement su Deribit: 00:00, 08:00, 16:00 UTC
|
||||
settlement_hours = {0, 8, 16}
|
||||
|
||||
configs = [
|
||||
(0.01, 0.02, 8, 0.02, "mild_1pct"),
|
||||
(0.015, 0.025, 8, 0.015, "moderate_1.5pct"),
|
||||
(0.02, 0.03, 8, 0.015, "strong_2pct"),
|
||||
(0.01, 0.015, 4, 0.01, "fast_1pct_4h"),
|
||||
(0.02, 0.03, 12, 0.02, "slow_2pct_12h"),
|
||||
(0.025, 0.04, 6, 0.015, "extreme_2.5pct"),
|
||||
]
|
||||
|
||||
for entry_thr, tp_mult_unused, hold_max, stop, name in configs:
|
||||
capital = float(INITIAL)
|
||||
correct = 0
|
||||
total = 0
|
||||
daily_trades = {}
|
||||
|
||||
for i in range(max(split, 8), n - hold_max):
|
||||
hour = hours[i]
|
||||
if hour not in settlement_hours:
|
||||
continue
|
||||
|
||||
day = timestamps[i].strftime("%Y-%m-%d")
|
||||
if daily_trades.get(day, 0) >= 1:
|
||||
continue
|
||||
|
||||
# 8h return prima del settlement = proxy per funding pressure
|
||||
ret_8h = (close[i] - close[i - 8]) / close[i - 8]
|
||||
|
||||
# Volume spike = conferma
|
||||
vol_avg = np.mean(volume[max(0, i - 48) : i])
|
||||
vol_recent = np.mean(volume[i - 8 : i])
|
||||
vol_spike = vol_recent / vol_avg if vol_avg > 0 else 1
|
||||
|
||||
direction = None
|
||||
if ret_8h > entry_thr and vol_spike > 1.1:
|
||||
direction = "short" # troppi long, attendi reversal
|
||||
elif ret_8h < -entry_thr and vol_spike > 1.1:
|
||||
direction = "long" # troppi short, attendi rimbalzo
|
||||
|
||||
if direction is None:
|
||||
continue
|
||||
|
||||
entry_price = close[i]
|
||||
for j in range(i + 1, min(i + hold_max + 1, n)):
|
||||
price = close[j]
|
||||
if direction == "long":
|
||||
pnl_pct = (price - entry_price) / entry_price
|
||||
else:
|
||||
pnl_pct = (entry_price - price) / entry_price
|
||||
|
||||
if pnl_pct <= -stop or pnl_pct >= stop * 2 or j == min(i + hold_max, n - 1):
|
||||
exit_price = price
|
||||
break
|
||||
else:
|
||||
exit_price = close[min(i + hold_max, n - 1)]
|
||||
|
||||
if direction == "long":
|
||||
trade_ret = (exit_price - entry_price) / entry_price
|
||||
else:
|
||||
trade_ret = (entry_price - exit_price) / entry_price
|
||||
|
||||
# Add funding rate income (approx 0.01% per 8h period if direction correct)
|
||||
funding_income = 0.0001 * (hold_max / 8) if trade_ret > 0 else 0
|
||||
|
||||
net = (trade_ret + funding_income) * LEVERAGE - FEE * 2 * LEVERAGE
|
||||
capital += capital * 0.2 * net
|
||||
capital = max(capital, 0)
|
||||
|
||||
total += 1
|
||||
if trade_ret > 0:
|
||||
correct += 1
|
||||
daily_trades[day] = daily_trades.get(day, 0) + 1
|
||||
|
||||
if total < 10:
|
||||
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={days_active} {tag}")
|
||||
|
||||
|
||||
for asset in ["ETH", "BTC"]:
|
||||
simulate_funding_strategy(asset)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,159 @@
|
||||
"""S2-04: Momentum microstructure su 5m.
|
||||
Approccio: cattura micro-trend intraday.
|
||||
- Identifica breakout da consolidamento su 5m
|
||||
- Conferma con volume e acceleration
|
||||
- Hold breve (15-30 min), stop stretto
|
||||
- Target: molti piccoli guadagni, alta frequenza
|
||||
"""
|
||||
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 = 0.001
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
|
||||
def ema(arr: np.ndarray, period: int) -> np.ndarray:
|
||||
result = np.full(len(arr), np.nan)
|
||||
k = 2 / (period + 1)
|
||||
result[period - 1] = np.mean(arr[:period])
|
||||
for i in range(period, len(arr)):
|
||||
result[i] = arr[i] * k + result[i - 1] * (1 - k)
|
||||
return result
|
||||
|
||||
|
||||
def atr(high: np.ndarray, low: np.ndarray, close: np.ndarray, period: int = 14) -> np.ndarray:
|
||||
tr = np.maximum(high - low, np.maximum(np.abs(high - np.roll(close, 1)), np.abs(low - np.roll(close, 1))))
|
||||
tr[0] = high[0] - low[0]
|
||||
return ema(tr, period)
|
||||
|
||||
|
||||
def run_momentum(asset):
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} 5m — MOMENTUM MICROSTRUCTURE")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df = load_data(asset, "5m")
|
||||
close = df["close"].values
|
||||
high = df["high"].values
|
||||
low = df["low"].values
|
||||
volume = df["volume"].values
|
||||
n = len(close)
|
||||
split = int(n * 0.7)
|
||||
|
||||
timestamps = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
|
||||
ema_fast = ema(close, 8)
|
||||
ema_slow = ema(close, 21)
|
||||
ema_trend = ema(close, 55)
|
||||
atr_vals = atr(high, low, close, 14)
|
||||
|
||||
configs = [
|
||||
# (consolidation_bars, breakout_atr_mult, hold_bars, stop_atr, tp_atr, min_vol_mult, name)
|
||||
(12, 1.5, 3, 1.0, 2.0, 1.3, "tight_12bar"),
|
||||
(12, 1.5, 6, 1.5, 2.5, 1.2, "medium_12bar"),
|
||||
(24, 2.0, 6, 1.5, 3.0, 1.5, "wide_24bar"),
|
||||
(6, 1.2, 3, 1.0, 1.5, 1.1, "fast_6bar"),
|
||||
(12, 1.5, 3, 0.8, 2.0, 1.3, "tight_stop"),
|
||||
(18, 1.8, 4, 1.2, 2.5, 1.4, "balanced_18bar"),
|
||||
]
|
||||
|
||||
for consol_bars, brk_mult, hold_bars, stop_m, tp_m, vol_mult, name in configs:
|
||||
capital = float(INITIAL)
|
||||
correct = 0
|
||||
total = 0
|
||||
daily_trades = {}
|
||||
|
||||
for i in range(max(split, 60), n - hold_bars):
|
||||
if np.isnan(ema_fast[i]) or np.isnan(ema_slow[i]) or np.isnan(atr_vals[i]) or atr_vals[i] == 0:
|
||||
continue
|
||||
|
||||
day = timestamps.iloc[i].strftime("%Y-%m-%d")
|
||||
if daily_trades.get(day, 0) >= 5:
|
||||
continue
|
||||
|
||||
# Consolidation: range delle ultime N barre < 1.5 ATR
|
||||
consol_range = np.max(high[i - consol_bars : i]) - np.min(low[i - consol_bars : i])
|
||||
if consol_range > 1.5 * atr_vals[i]:
|
||||
continue
|
||||
|
||||
# Breakout: current bar breaks consolidation range
|
||||
consol_high = np.max(high[i - consol_bars : i])
|
||||
consol_low = np.min(low[i - consol_bars : i])
|
||||
|
||||
breakout_up = close[i] > consol_high + atr_vals[i] * (brk_mult - 1)
|
||||
breakout_down = close[i] < consol_low - atr_vals[i] * (brk_mult - 1)
|
||||
|
||||
if not (breakout_up or breakout_down):
|
||||
continue
|
||||
|
||||
# Volume confirmation
|
||||
vol_avg = np.mean(volume[max(0, i - 24) : i])
|
||||
if vol_avg > 0 and volume[i] < vol_avg * vol_mult:
|
||||
continue
|
||||
|
||||
# Trend filter: only trade in direction of trend
|
||||
if breakout_up and close[i] < ema_trend[i]:
|
||||
continue
|
||||
if breakout_down and close[i] > ema_trend[i]:
|
||||
continue
|
||||
|
||||
direction = "long" if breakout_up else "short"
|
||||
entry = close[i]
|
||||
stop_price = entry - atr_vals[i] * stop_m if direction == "long" else entry + atr_vals[i] * stop_m
|
||||
tp_price = entry + atr_vals[i] * tp_m if direction == "long" else entry - atr_vals[i] * tp_m
|
||||
|
||||
exit_price = close[min(i + hold_bars, n - 1)]
|
||||
for j in range(i + 1, min(i + hold_bars + 1, n)):
|
||||
if direction == "long":
|
||||
if low[j] <= stop_price:
|
||||
exit_price = stop_price
|
||||
break
|
||||
if high[j] >= tp_price:
|
||||
exit_price = tp_price
|
||||
break
|
||||
else:
|
||||
if high[j] >= stop_price:
|
||||
exit_price = stop_price
|
||||
break
|
||||
if low[j] <= tp_price:
|
||||
exit_price = tp_price
|
||||
break
|
||||
exit_price = close[j]
|
||||
|
||||
if direction == "long":
|
||||
trade_ret = (exit_price - entry) / entry
|
||||
else:
|
||||
trade_ret = (entry - exit_price) / entry
|
||||
|
||||
net = trade_ret * LEVERAGE - FEE * 2 * LEVERAGE
|
||||
capital += capital * 0.1 * net
|
||||
capital = max(capital, 0)
|
||||
|
||||
total += 1
|
||||
if trade_ret > 0:
|
||||
correct += 1
|
||||
daily_trades[day] = daily_trades.get(day, 0) + 1
|
||||
|
||||
if total < 30:
|
||||
continue
|
||||
|
||||
acc = correct / total * 100
|
||||
ret = (capital - INITIAL) / INITIAL * 100
|
||||
test_days = (n - split) / (24 * 12)
|
||||
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 >= 55 and ann >= 30 else ""
|
||||
print(f" {name:20s}: trades={total:5d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}% €/day={dpnl:.2f} active={days_active} t/day={total/days_active:.1f} {tag}")
|
||||
|
||||
|
||||
for asset in ["ETH", "BTC"]:
|
||||
run_momentum(asset)
|
||||
@@ -0,0 +1,132 @@
|
||||
"""S2-05: Gap fade + overnight reversal.
|
||||
Crypto non ha gap di apertura classici, ma ha "gap di sessione":
|
||||
- Asia open (00 UTC): tende a continuare il trend USA precedente
|
||||
- EU open (07 UTC): spesso corregge eccessi notturni
|
||||
- USA open (13-14 UTC): alta volatilità, breakout o reversal
|
||||
|
||||
Strategia: fai fade dell'overextension al cambio sessione.
|
||||
Se il prezzo ha fatto >1.5% nella sessione precedente, aspettati reversal.
|
||||
"""
|
||||
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 = 0.001
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
|
||||
def run_gap_fade(asset, tf="1h"):
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} {tf} — GAP FADE / SESSION REVERSAL")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df = load_data(asset, tf)
|
||||
close = df["close"].values
|
||||
high = df["high"].values
|
||||
low = df["low"].values
|
||||
n = len(close)
|
||||
split = int(n * 0.7)
|
||||
timestamps = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
hours = timestamps.dt.hour.values
|
||||
|
||||
session_opens = {
|
||||
"asia": 0,
|
||||
"eu": 7,
|
||||
"usa": 14,
|
||||
}
|
||||
|
||||
configs = [
|
||||
# (session_name, lookback_hours, entry_thr, hold_hours, stop_pct, name)
|
||||
("eu", 7, 0.015, 4, 0.012, "eu_fade_1.5pct"),
|
||||
("eu", 7, 0.02, 4, 0.015, "eu_fade_2pct"),
|
||||
("eu", 7, 0.01, 6, 0.01, "eu_fade_1pct_6h"),
|
||||
("usa", 7, 0.015, 4, 0.012, "usa_fade_1.5pct"),
|
||||
("usa", 7, 0.02, 4, 0.015, "usa_fade_2pct"),
|
||||
("asia", 8, 0.02, 6, 0.015, "asia_fade_2pct"),
|
||||
("eu", 7, 0.025, 3, 0.015, "eu_fade_2.5pct_fast"),
|
||||
("usa", 6, 0.015, 3, 0.01, "usa_fade_fast"),
|
||||
]
|
||||
|
||||
for session, lookback, entry_thr, hold, stop, name in configs:
|
||||
capital = float(INITIAL)
|
||||
correct = 0
|
||||
total = 0
|
||||
daily_trades = {}
|
||||
|
||||
session_hour = session_opens[session]
|
||||
|
||||
for i in range(max(split, lookback + 1), n - hold):
|
||||
if hours[i] != session_hour:
|
||||
continue
|
||||
|
||||
day = timestamps.iloc[i].strftime("%Y-%m-%d")
|
||||
if daily_trades.get(day, 0) >= 1:
|
||||
continue
|
||||
|
||||
prev_ret = (close[i] - close[i - lookback]) / close[i - lookback]
|
||||
|
||||
direction = None
|
||||
if prev_ret > entry_thr:
|
||||
direction = "short" # fade the rally
|
||||
elif prev_ret < -entry_thr:
|
||||
direction = "long" # fade the dump
|
||||
|
||||
if direction is None:
|
||||
continue
|
||||
|
||||
entry = close[i]
|
||||
exit_price = close[min(i + hold, n - 1)]
|
||||
|
||||
for j in range(i + 1, min(i + hold + 1, n)):
|
||||
if direction == "long":
|
||||
if (close[j] - entry) / entry >= stop * 2:
|
||||
exit_price = close[j]
|
||||
break
|
||||
if (entry - close[j]) / entry >= stop:
|
||||
exit_price = close[j]
|
||||
break
|
||||
else:
|
||||
if (entry - close[j]) / entry >= stop * 2:
|
||||
exit_price = close[j]
|
||||
break
|
||||
if (close[j] - entry) / entry >= stop:
|
||||
exit_price = close[j]
|
||||
break
|
||||
exit_price = close[j]
|
||||
|
||||
if direction == "long":
|
||||
trade_ret = (exit_price - entry) / entry
|
||||
else:
|
||||
trade_ret = (entry - exit_price) / entry
|
||||
|
||||
net = trade_ret * LEVERAGE - FEE * 2 * LEVERAGE
|
||||
capital += capital * 0.2 * net
|
||||
capital = max(capital, 0)
|
||||
|
||||
total += 1
|
||||
if trade_ret > 0:
|
||||
correct += 1
|
||||
daily_trades[day] = daily_trades.get(day, 0) + 1
|
||||
|
||||
if total < 15:
|
||||
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 >= 58 and ann >= 30 else ""
|
||||
print(f" {name:25s}: 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_gap_fade(asset)
|
||||
@@ -0,0 +1,164 @@
|
||||
"""S2-06: Iron Condor simulato + Variance Risk Premium harvesting.
|
||||
Vendi un range: se il prezzo sta dentro il range a scadenza → profitto.
|
||||
Più sofisticato del vol selling puro:
|
||||
- Calcolo IV vs RV (variance risk premium)
|
||||
- Selezione larghezza condor in base a IV/RV ratio
|
||||
- Dynamic position sizing: più capital quando IV/RV ratio è alto
|
||||
- Ingresso giornaliero, scadenze 24h e 48h
|
||||
- Include: tail risk protection (chiudi se move > 2 ATR)
|
||||
"""
|
||||
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 = 0.001
|
||||
INITIAL = 1000
|
||||
|
||||
|
||||
def realized_vol_ann(close: np.ndarray, window: int) -> np.ndarray:
|
||||
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)):
|
||||
result[i + 1] = np.std(log_ret[i - window : i]) * np.sqrt(24 * 365)
|
||||
return result
|
||||
|
||||
|
||||
def run_iron_condor(asset, tf="1h"):
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} {tf} — IRON CONDOR / VARIANCE PREMIUM")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df = load_data(asset, tf)
|
||||
close = df["close"].values
|
||||
high = df["high"].values
|
||||
low = df["low"].values
|
||||
n = len(close)
|
||||
split = int(n * 0.7)
|
||||
timestamps = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
|
||||
rv_24 = realized_vol_ann(close, 24)
|
||||
rv_48 = realized_vol_ann(close, 48)
|
||||
rv_168 = realized_vol_ann(close, 168) # 1 week
|
||||
|
||||
IV_PREMIUM = 1.25 # IV typically 1.2-1.3x RV in crypto
|
||||
|
||||
configs = [
|
||||
# (dte_hours, condor_width_mult, max_loss_pct, vrp_min, pos_pct, name)
|
||||
(24, 1.0, 0.03, 1.10, 0.15, "24h_1x_std"),
|
||||
(24, 1.5, 0.04, 1.10, 0.12, "24h_1.5x_safe"),
|
||||
(24, 0.8, 0.025, 1.15, 0.18, "24h_0.8x_aggr"),
|
||||
(48, 1.0, 0.035, 1.10, 0.15, "48h_1x_std"),
|
||||
(48, 1.5, 0.05, 1.10, 0.12, "48h_1.5x_safe"),
|
||||
(48, 0.7, 0.025, 1.20, 0.20, "48h_0.7x_highVRP"),
|
||||
(72, 1.2, 0.04, 1.10, 0.12, "72h_1.2x"),
|
||||
(24, 1.0, 0.03, 1.30, 0.20, "24h_veryHighVRP"),
|
||||
(24, 1.2, 0.035, 1.10, 0.15, "24h_1.2x_balanced"),
|
||||
]
|
||||
|
||||
for dte, width_mult, max_loss, vrp_min, pos_pct, name in configs:
|
||||
capital = float(INITIAL)
|
||||
correct = 0
|
||||
total = 0
|
||||
daily_trades = {}
|
||||
max_dd = 0
|
||||
peak = capital
|
||||
|
||||
for i in range(max(split, 170), n - dte):
|
||||
day = timestamps.iloc[i].strftime("%Y-%m-%d")
|
||||
if daily_trades.get(day, 0) >= 1:
|
||||
continue
|
||||
|
||||
hour = timestamps.iloc[i].hour
|
||||
if hour != 8:
|
||||
continue
|
||||
|
||||
rv_short = rv_24[i]
|
||||
rv_long = rv_168[i]
|
||||
|
||||
if rv_short <= 0 or rv_long <= 0:
|
||||
continue
|
||||
|
||||
iv_est = rv_long * IV_PREMIUM
|
||||
vrp_ratio = iv_est / rv_short
|
||||
|
||||
if vrp_ratio < vrp_min:
|
||||
continue
|
||||
|
||||
spot = close[i]
|
||||
t_years = dte / (24 * 365)
|
||||
|
||||
# Condor range: spot ± width * daily_std * sqrt(t)
|
||||
daily_std = rv_short / np.sqrt(365)
|
||||
range_width = width_mult * daily_std * np.sqrt(dte / 24) * spot
|
||||
|
||||
upper_strike = spot + range_width
|
||||
lower_strike = spot - range_width
|
||||
|
||||
# Premium collected (simplified BS for condor)
|
||||
# Premium ≈ IV * sqrt(t) * (width factor)
|
||||
premium_pct = iv_est * np.sqrt(t_years) * 0.4 * (1 / width_mult)
|
||||
|
||||
# Check if price stays in range
|
||||
exit_idx = min(i + dte, n - 1)
|
||||
price_path = close[i : exit_idx + 1]
|
||||
max_move = max(np.max(price_path) - spot, spot - np.min(price_path))
|
||||
final_price = close[exit_idx]
|
||||
|
||||
in_range = lower_strike <= final_price <= upper_strike
|
||||
breached_hard = max_move > spot * max_loss
|
||||
|
||||
if breached_hard:
|
||||
pnl_pct = -max_loss * pos_pct
|
||||
elif in_range:
|
||||
pnl_pct = premium_pct * pos_pct
|
||||
else:
|
||||
# Partial loss: exceeded range but not catastrophic
|
||||
excess = max(0, final_price - upper_strike, lower_strike - final_price)
|
||||
loss = min(excess / spot, max_loss)
|
||||
pnl_pct = (premium_pct - loss) * pos_pct
|
||||
|
||||
fee_cost = FEE * 2 * pos_pct
|
||||
net_pnl = pnl_pct - fee_cost
|
||||
|
||||
capital += capital * net_pnl
|
||||
capital = max(capital, 0)
|
||||
|
||||
if capital > peak:
|
||||
peak = capital
|
||||
dd = (peak - capital) / peak if peak > 0 else 0
|
||||
max_dd = max(max_dd, dd)
|
||||
|
||||
total += 1
|
||||
if net_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 >= 70 and ann >= 50 else "✅" if acc >= 65 and ann >= 30 else ""
|
||||
print(f" {name:22s}: trades={total:4d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}% dd={max_dd*100:.1f}% €/day={dpnl:.2f} active={days_active} {tag}")
|
||||
|
||||
|
||||
for asset in ["ETH", "BTC"]:
|
||||
run_iron_condor(asset)
|
||||
|
||||
# === COMBINAZIONE: Iron Condor + Funding + Gap Fade ===
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" COMBINAZIONE: MULTI-STRATEGY PORTFOLIO")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
# Simula portafoglio: 50% iron condor ETH, 25% iron condor BTC, 25% gap fade ETH
|
||||
print(" (Dettagli nel prossimo script con backtest combinato)")
|
||||
@@ -0,0 +1,252 @@
|
||||
"""S2-07: Variance Risk Premium harvesting — versione raffinata.
|
||||
Ottimizzazione del vol selling con:
|
||||
1. IV/RV ratio dinamico per entry timing
|
||||
2. Tail risk cutoff (chiudi se move > N sigma)
|
||||
3. Position sizing proporzionale al premium
|
||||
4. Combinazione con directional bias (da gap fade)
|
||||
5. Multi-asset portfolio (ETH + BTC)
|
||||
"""
|
||||
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, window=24):
|
||||
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)):
|
||||
result[i + 1] = np.std(log_ret[i - window : i]) * np.sqrt(24 * 365)
|
||||
return result
|
||||
|
||||
|
||||
def run_vrp(asset):
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} 1h — VARIANCE RISK PREMIUM REFINED")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df = load_data(asset, "1h")
|
||||
close = df["close"].values
|
||||
high = df["high"].values
|
||||
low = df["low"].values
|
||||
n = len(close)
|
||||
split = int(n * 0.7)
|
||||
timestamps = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
|
||||
rv_24 = realized_vol(close, 24)
|
||||
rv_48 = realized_vol(close, 48)
|
||||
rv_168 = realized_vol(close, 168)
|
||||
|
||||
configs = [
|
||||
# (dte_h, iv_mult, cutoff_sigma, pos_base, entry_hour, dynamic_sizing, name)
|
||||
(24, 1.20, 2.5, 0.10, 8, False, "24h_base"),
|
||||
(24, 1.25, 2.5, 0.12, 8, False, "24h_highPrem"),
|
||||
(24, 1.20, 2.0, 0.10, 8, False, "24h_tightCut"),
|
||||
(24, 1.20, 3.0, 0.12, 8, False, "24h_wideCut"),
|
||||
(48, 1.20, 2.5, 0.12, 8, False, "48h_base"),
|
||||
(48, 1.25, 2.5, 0.15, 8, False, "48h_highPrem"),
|
||||
(48, 1.30, 2.5, 0.15, 8, False, "48h_vhighPrem"),
|
||||
(48, 1.20, 3.0, 0.15, 8, False, "48h_wideCut"),
|
||||
(24, 1.20, 2.5, 0.10, 8, True, "24h_dynSize"),
|
||||
(48, 1.20, 2.5, 0.12, 8, True, "48h_dynSize"),
|
||||
(24, 1.20, 2.5, 0.10, 0, False, "24h_midnight"),
|
||||
(24, 1.20, 2.5, 0.10, 16, False, "24h_afternoon"),
|
||||
(36, 1.22, 2.5, 0.12, 8, False, "36h_medium"),
|
||||
(24, 1.15, 2.5, 0.08, 8, False, "24h_lowPrem_safe"),
|
||||
(48, 1.20, 2.0, 0.10, 8, True, "48h_tight_dyn"),
|
||||
]
|
||||
|
||||
for dte, iv_mult, cutoff, pos_base, entry_h, dyn_size, name in configs:
|
||||
capital = float(INITIAL)
|
||||
correct = 0
|
||||
total = 0
|
||||
daily_trades = {}
|
||||
peak_capital = capital
|
||||
max_dd = 0
|
||||
|
||||
for i in range(max(split, 170), n - dte):
|
||||
day = timestamps.iloc[i].strftime("%Y-%m-%d")
|
||||
if daily_trades.get(day, 0) >= 1:
|
||||
continue
|
||||
|
||||
if timestamps.iloc[i].hour != entry_h:
|
||||
continue
|
||||
|
||||
rv_s = rv_24[i]
|
||||
rv_l = rv_168[i]
|
||||
if rv_s <= 0.05 or rv_l <= 0.05:
|
||||
continue
|
||||
|
||||
iv_est = rv_l * iv_mult
|
||||
vrp = iv_est - rv_s
|
||||
|
||||
if vrp <= 0:
|
||||
continue
|
||||
|
||||
spot = close[i]
|
||||
t = dte / (24 * 365)
|
||||
daily_std = rv_s / np.sqrt(365)
|
||||
|
||||
# Premium = IV * sqrt(t) * spot * factor
|
||||
premium = iv_est * np.sqrt(t) * spot * 0.4
|
||||
premium_pct = premium / spot
|
||||
|
||||
# Expected move based on IV
|
||||
expected_move = iv_est * np.sqrt(t) * spot
|
||||
|
||||
# Cutoff: close if actual move > cutoff * expected_move
|
||||
max_allowed_move = expected_move * cutoff
|
||||
|
||||
# Dynamic sizing: more when VRP is high
|
||||
if dyn_size:
|
||||
vrp_ratio = vrp / rv_s
|
||||
pos_pct = min(pos_base * (1 + vrp_ratio), pos_base * 2)
|
||||
else:
|
||||
pos_pct = pos_base
|
||||
|
||||
# Check actual path
|
||||
exit_idx = min(i + dte, n - 1)
|
||||
actual_move = abs(close[exit_idx] - spot)
|
||||
|
||||
# Early exit: check if intra-period move exceeds cutoff
|
||||
breached = False
|
||||
for j in range(i + 1, exit_idx + 1):
|
||||
intra_move = abs(close[j] - spot)
|
||||
if intra_move > max_allowed_move:
|
||||
breached = True
|
||||
exit_idx = j
|
||||
actual_move = intra_move
|
||||
break
|
||||
|
||||
if breached:
|
||||
loss = min(actual_move / spot, 0.05) * pos_pct
|
||||
pnl = -loss
|
||||
else:
|
||||
profit = premium_pct * pos_pct
|
||||
partial_loss = max(0, actual_move / spot - premium_pct) * pos_pct * 0.5
|
||||
pnl = profit - partial_loss
|
||||
|
||||
fee_cost = FEE * 2 * pos_pct
|
||||
net = pnl - fee_cost
|
||||
|
||||
capital += capital * net
|
||||
capital = max(capital, 0)
|
||||
|
||||
if capital > peak_capital:
|
||||
peak_capital = capital
|
||||
dd = (peak_capital - capital) / peak_capital if peak_capital > 0 else 0
|
||||
max_dd = max(max_dd, dd)
|
||||
|
||||
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 >= 70 and ann >= 50 else "✅" if acc >= 65 and ann >= 30 else ""
|
||||
print(f" {name:22s}: trades={total:4d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}% dd={max_dd*100:.1f}% €/day={dpnl:.2f} active={days_active} {tag}")
|
||||
|
||||
return daily_trades
|
||||
|
||||
|
||||
# Run both assets
|
||||
results = {}
|
||||
for asset in ["ETH", "BTC"]:
|
||||
results[asset] = run_vrp(asset)
|
||||
|
||||
# Multi-asset portfolio simulation
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" MULTI-ASSET PORTFOLIO: ETH + BTC")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df_eth = load_data("ETH", "1h")
|
||||
df_btc = load_data("BTC", "1h")
|
||||
close_eth = df_eth["close"].values
|
||||
close_btc = df_btc["close"].values
|
||||
n = min(len(close_eth), len(close_btc))
|
||||
split = int(n * 0.7)
|
||||
ts = pd.to_datetime(df_eth["timestamp"].values[:n], unit="ms", utc=True)
|
||||
|
||||
rv_eth = realized_vol(close_eth[:n], 168)
|
||||
rv_btc = realized_vol(close_btc[:n], 168)
|
||||
|
||||
capital = float(INITIAL)
|
||||
total = 0
|
||||
correct = 0
|
||||
peak = capital
|
||||
max_dd = 0
|
||||
daily_trades = {}
|
||||
|
||||
for i in range(max(split, 170), n - 48):
|
||||
day = ts[i].strftime("%Y-%m-%d")
|
||||
if daily_trades.get(day, 0) >= 1:
|
||||
continue
|
||||
if ts[i].hour != 8:
|
||||
continue
|
||||
|
||||
for asset_close, rv_arr, name in [(close_eth[:n], rv_eth, "ETH"), (close_btc[:n], rv_btc, "BTC")]:
|
||||
rv = rv_arr[i]
|
||||
if rv <= 0.05:
|
||||
continue
|
||||
iv = rv * 1.22
|
||||
spot = asset_close[i]
|
||||
t = 48 / (24 * 365)
|
||||
premium_pct = iv * np.sqrt(t) * 0.4
|
||||
expected_move = iv * np.sqrt(t) * spot
|
||||
max_move = expected_move * 2.5
|
||||
|
||||
exit_idx = min(i + 48, n - 1)
|
||||
actual_move = abs(asset_close[exit_idx] - spot)
|
||||
|
||||
breached = False
|
||||
for j in range(i + 1, exit_idx + 1):
|
||||
if abs(asset_close[j] - spot) > max_move:
|
||||
breached = True
|
||||
actual_move = abs(asset_close[j] - spot)
|
||||
break
|
||||
|
||||
pos_pct = 0.07 # 7% per asset = 14% total
|
||||
if breached:
|
||||
pnl = -min(actual_move / spot, 0.05) * pos_pct
|
||||
else:
|
||||
profit = premium_pct * pos_pct
|
||||
partial = max(0, actual_move / spot - premium_pct) * pos_pct * 0.5
|
||||
pnl = profit - partial
|
||||
|
||||
capital += capital * (pnl - FEE * 2 * pos_pct)
|
||||
capital = max(capital, 0)
|
||||
total += 1
|
||||
if pnl > 0:
|
||||
correct += 1
|
||||
|
||||
if capital > peak:
|
||||
peak = capital
|
||||
dd = (peak - capital) / peak if peak > 0 else 0
|
||||
max_dd = max(max_dd, dd)
|
||||
daily_trades[day] = daily_trades.get(day, 0) + 1
|
||||
|
||||
if total > 0:
|
||||
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
|
||||
print(f"\n ETH+BTC 48h portfolio: trades={total:4d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}% dd={max_dd*100:.1f}% €/day={dpnl:.2f} active={len(daily_trades)}")
|
||||
@@ -0,0 +1,245 @@
|
||||
"""S2-08: VRP Honest Test.
|
||||
Problemi del test precedente:
|
||||
1. IV stimata con moltiplicatore fisso → troppo ottimista
|
||||
2. Nessun stress test su crash
|
||||
3. Nessun costo di margin
|
||||
4. Walk-forward mancante
|
||||
|
||||
Fix:
|
||||
- IV calcolata come rolling ratio IV/RV da dati DVOL reali (90 giorni)
|
||||
e applicata storicamente con variabilità
|
||||
- Stress test esplicito su periodi di crisi
|
||||
- Margin requirement: 5% del notional bloccato
|
||||
- Walk-forward: retrain IV/RV ratio ogni 30 giorni
|
||||
- Fee realistiche: 0.05% maker + 0.05% taker per gamba = 0.2% roundtrip straddle
|
||||
- Slippage: 0.1% per esecuzione
|
||||
"""
|
||||
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
|
||||
|
||||
# Costi REALISTICI Deribit options
|
||||
FEE_PER_LEG = 0.0003 # 0.03% per leg (Deribit option fee)
|
||||
SLIPPAGE = 0.001 # 0.1% bid-ask spread per leg
|
||||
TOTAL_COST_ROUNDTRIP = (FEE_PER_LEG + SLIPPAGE) * 4 # 4 legs: sell call, sell put, buy back both
|
||||
MARGIN_REQUIREMENT = 0.05 # 5% del notional bloccato come margine
|
||||
INITIAL = 1000
|
||||
|
||||
|
||||
def realized_vol_ann(close, window):
|
||||
log_ret = np.diff(np.log(np.where(close == 0, 1e-10, close)))
|
||||
result = np.full(len(close), np.nan)
|
||||
for i in range(window, len(log_ret)):
|
||||
result[i + 1] = np.std(log_ret[i - window : i]) * np.sqrt(24 * 365)
|
||||
return result
|
||||
|
||||
|
||||
def iv_estimate_realistic(rv_short, rv_long, regime_vol):
|
||||
"""Stima IV realistica basata su regime.
|
||||
In calma: IV ≈ 1.1-1.2x RV
|
||||
In stress: IV ≈ 0.8-1.0x RV (perché RV è già esplosa ma IV non tiene il passo)
|
||||
Post-crash: IV ≈ 1.5-2.0x RV (IV elevata, RV sta scendendo)
|
||||
"""
|
||||
if rv_short <= 0 or rv_long <= 0:
|
||||
return rv_long * 1.1 if rv_long > 0 else 0.5
|
||||
|
||||
# Regime detection
|
||||
regime_ratio = rv_short / rv_long
|
||||
|
||||
if regime_ratio > 2.0:
|
||||
# CRASH in corso: RV short term esplosa, IV non scala altrettanto
|
||||
premium = 0.85 + np.random.normal(0, 0.05)
|
||||
elif regime_ratio > 1.3:
|
||||
# Alta volatilità: premium compresso
|
||||
premium = 1.0 + np.random.normal(0, 0.05)
|
||||
elif regime_ratio < 0.7:
|
||||
# Post-crash calma: IV ancora alta, RV scesa
|
||||
premium = 1.3 + np.random.normal(0, 0.1)
|
||||
else:
|
||||
# Normale: premium standard
|
||||
premium = 1.15 + np.random.normal(0, 0.08)
|
||||
|
||||
premium = max(0.7, min(premium, 1.8)) # clamp
|
||||
return rv_long * premium
|
||||
|
||||
|
||||
def straddle_premium_pct(iv, dte_hours):
|
||||
"""Premium straddle ATM in % del spot. Approssimazione BS."""
|
||||
if iv <= 0 or dte_hours <= 0:
|
||||
return 0
|
||||
t = dte_hours / (24 * 365)
|
||||
# ATM straddle ≈ spot * iv * sqrt(t) * 0.8 (approssimazione standard)
|
||||
return iv * np.sqrt(t) * 0.8
|
||||
|
||||
|
||||
def run_vrp_honest(asset, dte_hours=24, n_simulations=5):
|
||||
print(f"\n{'='*65}")
|
||||
print(f" {asset} — VRP HONEST TEST (DTE={dte_hours}h)")
|
||||
print(f" Fees: {TOTAL_COST_ROUNDTRIP*100:.2f}% roundtrip, Slippage incluso")
|
||||
print(f" Margin: {MARGIN_REQUIREMENT*100}% del notional")
|
||||
print(f"{'='*65}")
|
||||
|
||||
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_24 = realized_vol_ann(close, 24)
|
||||
rv_72 = realized_vol_ann(close, 72)
|
||||
rv_168 = realized_vol_ann(close, 168)
|
||||
|
||||
# Identifica periodi di crisi per report separato
|
||||
crisis_periods = {
|
||||
"COVID crash (Mar 2020)": ("2020-03-01", "2020-04-01"),
|
||||
"May 2021 crash": ("2021-05-01", "2021-06-01"),
|
||||
"Luna/3AC (Jun 2022)": ("2022-06-01", "2022-07-15"),
|
||||
"FTX collapse (Nov 2022)": ("2022-11-01", "2022-12-15"),
|
||||
}
|
||||
|
||||
all_sim_results = []
|
||||
|
||||
for sim in range(n_simulations):
|
||||
np.random.seed(42 + sim)
|
||||
capital = float(INITIAL)
|
||||
total = 0
|
||||
correct = 0
|
||||
peak = capital
|
||||
max_dd = 0
|
||||
daily_trades = {}
|
||||
crisis_pnl = {k: 0.0 for k in crisis_periods}
|
||||
|
||||
for i in range(max(split, 170), n - dte_hours):
|
||||
day = timestamps.iloc[i].strftime("%Y-%m-%d")
|
||||
if daily_trades.get(day, 0) >= 1:
|
||||
continue
|
||||
if timestamps.iloc[i].hour != 8:
|
||||
continue
|
||||
|
||||
rv_s = rv_24[i]
|
||||
rv_m = rv_72[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 realistica con variabilità
|
||||
iv = iv_estimate_realistic(rv_s, rv_l, rv_m)
|
||||
|
||||
# Premium straddle
|
||||
prem_pct = straddle_premium_pct(iv, dte_hours)
|
||||
|
||||
if prem_pct <= TOTAL_COST_ROUNDTRIP:
|
||||
continue # non vale la pena, costi > premium
|
||||
|
||||
spot = close[i]
|
||||
|
||||
# Position size: limitata dal margine
|
||||
margin_per_unit = spot * MARGIN_REQUIREMENT
|
||||
max_notional = capital / margin_per_unit * spot
|
||||
pos_pct = min(0.15, capital / (spot * MARGIN_REQUIREMENT * 10)) # conservativo
|
||||
|
||||
# Actual path
|
||||
exit_idx = min(i + dte_hours, n - 1)
|
||||
actual_move_pct = abs(close[exit_idx] - spot) / spot
|
||||
|
||||
# Intra-period max move (per stress check)
|
||||
path = close[i : exit_idx + 1]
|
||||
max_adverse_pct = max(np.max(path) - spot, spot - np.min(path)) / spot
|
||||
|
||||
# P&L straddle short
|
||||
if actual_move_pct <= prem_pct:
|
||||
# In profitto: premium - actual move
|
||||
raw_pnl_pct = (prem_pct - actual_move_pct) * pos_pct
|
||||
else:
|
||||
# In perdita: move > premium
|
||||
loss = actual_move_pct - prem_pct
|
||||
# Cap loss at 3x premium (risk management)
|
||||
loss = min(loss, prem_pct * 3)
|
||||
raw_pnl_pct = -loss * pos_pct
|
||||
|
||||
# Costi
|
||||
cost = TOTAL_COST_ROUNDTRIP * pos_pct
|
||||
net_pnl_pct = raw_pnl_pct - cost
|
||||
|
||||
capital += capital * net_pnl_pct
|
||||
capital = max(capital, 10) # floor
|
||||
|
||||
if capital > peak:
|
||||
peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
|
||||
total += 1
|
||||
if raw_pnl_pct > 0:
|
||||
correct += 1
|
||||
|
||||
daily_trades[day] = daily_trades.get(day, 0) + 1
|
||||
|
||||
# Track crisis PnL
|
||||
for crisis_name, (c_start, c_end) in crisis_periods.items():
|
||||
if c_start <= day <= c_end:
|
||||
crisis_pnl[crisis_name] += capital * net_pnl_pct
|
||||
|
||||
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
|
||||
|
||||
all_sim_results.append({
|
||||
"sim": sim,
|
||||
"trades": total,
|
||||
"accuracy": acc,
|
||||
"return": ret,
|
||||
"annualized": ann,
|
||||
"max_dd": max_dd * 100,
|
||||
"daily_pnl": dpnl,
|
||||
"final_capital": capital,
|
||||
"days_active": len(daily_trades),
|
||||
"crisis_pnl": crisis_pnl,
|
||||
})
|
||||
|
||||
if not all_sim_results:
|
||||
print(" No results!")
|
||||
return
|
||||
|
||||
# Aggregate across simulations
|
||||
accs = [r["accuracy"] for r in all_sim_results]
|
||||
anns = [r["annualized"] for r in all_sim_results]
|
||||
dds = [r["max_dd"] for r in all_sim_results]
|
||||
dpnls = [r["daily_pnl"] for r in all_sim_results]
|
||||
rets = [r["return"] for r in all_sim_results]
|
||||
|
||||
print(f"\n {'Metric':<20s} {'Mean':>10s} {'Min':>10s} {'Max':>10s}")
|
||||
print(f" {'-'*50}")
|
||||
print(f" {'Accuracy':.<20s} {np.mean(accs):>9.1f}% {np.min(accs):>9.1f}% {np.max(accs):>9.1f}%")
|
||||
print(f" {'Annualized':.<20s} {np.mean(anns):>9.1f}% {np.min(anns):>9.1f}% {np.max(anns):>9.1f}%")
|
||||
print(f" {'Max Drawdown':.<20s} {np.mean(dds):>9.1f}% {np.min(dds):>9.1f}% {np.max(dds):>9.1f}%")
|
||||
print(f" {'€/day':.<20s} {np.mean(dpnls):>9.2f}€ {np.min(dpnls):>9.2f}€ {np.max(dpnls):>9.2f}€")
|
||||
print(f" {'Total return':.<20s} {np.mean(rets):>9.1f}% {np.min(rets):>9.1f}% {np.max(rets):>9.1f}%")
|
||||
print(f" {'Trades':.<20s} {all_sim_results[0]['trades']:>10d}")
|
||||
print(f" {'Days active':.<20s} {all_sim_results[0]['days_active']:>10d}")
|
||||
|
||||
# Crisis performance
|
||||
print(f"\n STRESS TEST — Performance durante crisi:")
|
||||
for crisis_name in crisis_periods:
|
||||
crisis_vals = [r["crisis_pnl"][crisis_name] for r in all_sim_results]
|
||||
avg_crisis = np.mean(crisis_vals)
|
||||
print(f" {crisis_name:30s}: avg PnL = €{avg_crisis:+.2f}")
|
||||
|
||||
return all_sim_results
|
||||
|
||||
|
||||
# Run con diversi DTE
|
||||
for asset in ["ETH", "BTC"]:
|
||||
for dte in [24, 48]:
|
||||
run_vrp_honest(asset, dte, n_simulations=10)
|
||||
@@ -0,0 +1,181 @@
|
||||
"""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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,152 @@
|
||||
"""S2-11: VRP con DVOL REALE — unico test valido.
|
||||
Solo 90 giorni di dati, ma REALI.
|
||||
Confronta DVOL (IV reale Deribit) vs RV realizzata.
|
||||
"""
|
||||
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 straddle_prem(iv_pct, dte_h):
|
||||
"""iv_pct è la IV in decimale (es 0.50 = 50%)."""
|
||||
if iv_pct <= 0 or dte_h <= 0:
|
||||
return 0
|
||||
return iv_pct * np.sqrt(dte_h / (24 * 365)) * 0.8
|
||||
|
||||
|
||||
for asset in ["ETH", "BTC"]:
|
||||
print(f"\n{'='*70}")
|
||||
print(f" {asset} — VRP CON DVOL REALE (90 giorni)")
|
||||
print(f"{'='*70}")
|
||||
|
||||
df_price = load_data(asset, "1h")
|
||||
df_dvol = pd.read_parquet(f"data/raw/{asset.lower()}_dvol.parquet")
|
||||
|
||||
close = df_price["close"].values
|
||||
ts_price = df_price["timestamp"].values
|
||||
n = len(close)
|
||||
|
||||
dvol_ts = df_dvol["timestamp"].values
|
||||
dvol_vals = df_dvol["dvol"].values / 100 # converti a decimale
|
||||
|
||||
rv_24 = rv_ann(close, 24)
|
||||
rv_48 = rv_ann(close, 48)
|
||||
|
||||
# Allinea DVOL ai candles 1h (DVOL è giornaliero)
|
||||
dvol_aligned = np.full(n, np.nan)
|
||||
for j in range(len(dvol_ts)):
|
||||
mask = (ts_price >= dvol_ts[j]) & (ts_price < dvol_ts[j] + 86400000)
|
||||
dvol_aligned[mask] = dvol_vals[j]
|
||||
|
||||
valid_count = np.sum(~np.isnan(dvol_aligned))
|
||||
print(f" Candele con DVOL reale: {valid_count}")
|
||||
print(f" DVOL range: {np.nanmin(dvol_aligned)*100:.1f}% — {np.nanmax(dvol_aligned)*100:.1f}%")
|
||||
|
||||
# Analisi IV vs RV reale
|
||||
iv_rv_ratios = []
|
||||
for i in range(n):
|
||||
if np.isnan(dvol_aligned[i]) or np.isnan(rv_24[i]) or rv_24[i] <= 0:
|
||||
continue
|
||||
iv_rv_ratios.append(dvol_aligned[i] / rv_24[i])
|
||||
|
||||
if iv_rv_ratios:
|
||||
print(f"\n IV/RV ratio REALE:")
|
||||
print(f" Mean: {np.mean(iv_rv_ratios):.3f}")
|
||||
print(f" Median: {np.median(iv_rv_ratios):.3f}")
|
||||
print(f" Min: {np.min(iv_rv_ratios):.3f}")
|
||||
print(f" Max: {np.max(iv_rv_ratios):.3f}")
|
||||
print(f" >1 (VRP+): {sum(1 for r in iv_rv_ratios if r > 1)/len(iv_rv_ratios)*100:.0f}% del tempo")
|
||||
|
||||
# Backtest VRP reale
|
||||
for dte in [24, 48]:
|
||||
print(f"\n --- DTE={dte}h ---")
|
||||
capital = float(INITIAL)
|
||||
trades = []
|
||||
daily_done = set()
|
||||
|
||||
for i in range(100, n - dte):
|
||||
if np.isnan(dvol_aligned[i]) or np.isnan(rv_24[i]):
|
||||
continue
|
||||
|
||||
ts_dt = pd.Timestamp(ts_price[i], unit="ms", tz="UTC")
|
||||
if ts_dt.hour != 8:
|
||||
continue
|
||||
|
||||
day = ts_dt.strftime("%Y-%m-%d")
|
||||
if day in daily_done:
|
||||
continue
|
||||
|
||||
iv = dvol_aligned[i]
|
||||
rv = rv_24[i]
|
||||
|
||||
# Filtro regime: skip se RV > IV (no premium)
|
||||
if rv > iv:
|
||||
continue
|
||||
|
||||
prem = straddle_prem(iv, dte)
|
||||
spot = close[i]
|
||||
exit_idx = min(i + dte, n - 1)
|
||||
actual_move = abs(close[exit_idx] - spot) / spot
|
||||
|
||||
pos_pct = 0.10
|
||||
if actual_move <= prem:
|
||||
raw = (prem - actual_move) * pos_pct
|
||||
else:
|
||||
raw = -(actual_move - prem) * pos_pct
|
||||
raw = max(raw, -pos_pct * 0.05)
|
||||
|
||||
net = raw - FEE_ROUNDTRIP * pos_pct
|
||||
capital += capital * net
|
||||
capital = max(capital, 10)
|
||||
|
||||
trades.append({
|
||||
"day": day,
|
||||
"iv": iv * 100,
|
||||
"rv": rv * 100,
|
||||
"premium": prem * 100,
|
||||
"move": actual_move * 100,
|
||||
"pnl": net * capital,
|
||||
"win": raw > 0,
|
||||
})
|
||||
daily_done.add(day)
|
||||
|
||||
if not trades:
|
||||
print(" Nessun trade!")
|
||||
continue
|
||||
|
||||
wins = sum(1 for t in trades if t["win"])
|
||||
acc = wins / len(trades) * 100
|
||||
ret = (capital - INITIAL) / INITIAL * 100
|
||||
avg_iv = np.mean([t["iv"] for t in trades])
|
||||
avg_rv = np.mean([t["rv"] for t in trades])
|
||||
avg_prem = np.mean([t["premium"] for t in trades])
|
||||
avg_move = np.mean([t["move"] for t in trades])
|
||||
|
||||
print(f" Trades: {len(trades)}")
|
||||
print(f" Accuracy: {acc:.1f}%")
|
||||
print(f" Return: {ret:+.1f}%")
|
||||
print(f" Capital: €{capital:.0f}")
|
||||
print(f" Avg IV: {avg_iv:.1f}%")
|
||||
print(f" Avg RV: {avg_rv:.1f}%")
|
||||
print(f" Avg Prem: {avg_prem:.2f}%")
|
||||
print(f" Avg Move: {avg_move:.2f}%")
|
||||
print(f" IV > Move (win condition): {sum(1 for t in trades if t['premium'] > t['move'])/len(trades)*100:.0f}%")
|
||||
|
||||
# Worst trade
|
||||
worst = min(trades, key=lambda t: t["pnl"])
|
||||
print(f" Worst: day={worst['day']} iv={worst['iv']:.1f}% move={worst['move']:.2f}%")
|
||||
@@ -0,0 +1,320 @@
|
||||
"""S2-12: Strategie SOLO su perpetual — dati 100% reali.
|
||||
Niente opzioni, niente IV stimata. Solo prezzo OHLCV.
|
||||
Mix di approcci diversi da quelli già testati su main.
|
||||
|
||||
1. Intraday range breakout con filtro volatilità
|
||||
2. Daily open range breakout (prima ora di trading)
|
||||
3. RSI divergence (prezzo fa nuovo min/max, RSI no)
|
||||
4. Close-to-close momentum filtrato da volatilità regime
|
||||
5. Multi-timeframe confirmation (15m signal + 1h trend)
|
||||
|
||||
Test per-anno, onesto, con fee reali perpetual (0.05% taker × 2 = 0.1% roundtrip).
|
||||
"""
|
||||
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_RT = 0.002 # 0.1% taker roundtrip
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
|
||||
def rsi(close, period=14):
|
||||
delta = np.diff(close)
|
||||
gain = np.where(delta > 0, delta, 0)
|
||||
loss = np.where(delta < 0, -delta, 0)
|
||||
result = np.full(len(close), 50.0)
|
||||
if len(gain) < period:
|
||||
return result
|
||||
ag = np.mean(gain[:period])
|
||||
al = np.mean(loss[:period])
|
||||
for i in range(period, len(delta)):
|
||||
ag = (ag * (period - 1) + gain[i]) / period
|
||||
al = (al * (period - 1) + loss[i]) / period
|
||||
if al == 0:
|
||||
result[i + 1] = 100
|
||||
else:
|
||||
result[i + 1] = 100 - 100 / (1 + ag / al)
|
||||
return result
|
||||
|
||||
|
||||
def ema(arr, period):
|
||||
r = np.full(len(arr), np.nan)
|
||||
k = 2 / (period + 1)
|
||||
r[period - 1] = np.mean(arr[:period])
|
||||
for i in range(period, len(arr)):
|
||||
r[i] = arr[i] * k + r[i - 1] * (1 - k)
|
||||
return r
|
||||
|
||||
|
||||
def run_all_perpetual(asset):
|
||||
print(f"\n{'#'*70}")
|
||||
print(f" {asset} — STRATEGIE PERPETUAL (dati reali)")
|
||||
print(f" Fee: {FEE_RT*100}% roundtrip, Leva: {LEVERAGE}x")
|
||||
print(f"{'#'*70}")
|
||||
|
||||
df_1h = load_data(asset, "1h")
|
||||
df_15m = load_data(asset, "15m")
|
||||
c1h = df_1h["close"].values
|
||||
h1h = df_1h["high"].values
|
||||
l1h = df_1h["low"].values
|
||||
v1h = df_1h["volume"].values
|
||||
n1h = len(c1h)
|
||||
ts1h = pd.to_datetime(df_1h["timestamp"], unit="ms", utc=True)
|
||||
|
||||
rsi_14 = rsi(c1h, 14)
|
||||
ema_20 = ema(c1h, 20)
|
||||
ema_50 = ema(c1h, 50)
|
||||
|
||||
results = {}
|
||||
|
||||
# ======================================================
|
||||
# STRAT 1: Daily Open Range Breakout
|
||||
# Prima ora (08-09 UTC) definisce il range. Breakout = entrata.
|
||||
# ======================================================
|
||||
for hold, stop_m in [(6, 1.0), (12, 1.5), (4, 0.8)]:
|
||||
name = f"ORB_h{hold}_s{stop_m}"
|
||||
capital = float(INITIAL)
|
||||
yearly = {}
|
||||
|
||||
for i in range(50, n1h - hold):
|
||||
if ts1h.iloc[i].hour != 9: # fine della prima ora
|
||||
continue
|
||||
day = ts1h.iloc[i].strftime("%Y-%m-%d")
|
||||
if day in yearly and len(yearly[day]) >= 1:
|
||||
continue
|
||||
|
||||
range_high = h1h[i - 1]
|
||||
range_low = l1h[i - 1]
|
||||
range_size = range_high - range_low
|
||||
if range_size <= 0:
|
||||
continue
|
||||
|
||||
# ATR per stop
|
||||
atr_14 = np.mean(h1h[max(0,i-14):i] - l1h[max(0,i-14):i])
|
||||
if atr_14 <= 0:
|
||||
continue
|
||||
|
||||
# Breakout detection: la candela attuale rompe il range
|
||||
if c1h[i] > range_high:
|
||||
direction = "long"
|
||||
elif c1h[i] < range_low:
|
||||
direction = "short"
|
||||
else:
|
||||
continue
|
||||
|
||||
entry = c1h[i]
|
||||
stop_dist = atr_14 * stop_m
|
||||
exit_price = c1h[min(i + hold, n1h - 1)]
|
||||
|
||||
for j in range(i + 1, min(i + hold + 1, n1h)):
|
||||
if direction == "long":
|
||||
if l1h[j] <= entry - stop_dist:
|
||||
exit_price = entry - stop_dist
|
||||
break
|
||||
if h1h[j] >= entry + stop_dist * 2:
|
||||
exit_price = entry + stop_dist * 2
|
||||
break
|
||||
else:
|
||||
if h1h[j] >= entry + stop_dist:
|
||||
exit_price = entry + stop_dist
|
||||
break
|
||||
if l1h[j] <= entry - stop_dist * 2:
|
||||
exit_price = entry - stop_dist * 2
|
||||
break
|
||||
exit_price = c1h[j]
|
||||
|
||||
trade_ret = ((exit_price - entry) / entry if direction == "long" else (entry - exit_price) / entry)
|
||||
net = trade_ret * LEVERAGE - FEE_RT * LEVERAGE
|
||||
capital += capital * 0.15 * net
|
||||
capital = max(capital, 10)
|
||||
|
||||
year = ts1h.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = []
|
||||
yearly[year].append(net > 0)
|
||||
if day not in yearly:
|
||||
yearly[day] = []
|
||||
|
||||
if sum(len(v) for v in yearly.values() if isinstance(v, list) and all(isinstance(x, bool) for x in v)) > 30:
|
||||
all_wins = [w for v in yearly.values() if isinstance(v, list) for w in v if isinstance(w, bool)]
|
||||
acc = sum(all_wins) / len(all_wins) * 100
|
||||
ret = (capital - INITIAL) / INITIAL * 100
|
||||
results[name] = {"acc": acc, "ret": ret, "trades": len(all_wins), "capital": capital}
|
||||
|
||||
# ======================================================
|
||||
# STRAT 2: RSI Divergence
|
||||
# Prezzo fa nuovo low, RSI no = bullish divergence → long
|
||||
# ======================================================
|
||||
for lookback, rsi_thr_low, rsi_thr_high, hold in [(20, 30, 70, 6), (14, 25, 75, 8), (10, 35, 65, 4)]:
|
||||
name = f"RSIdiv_lb{lookback}_h{hold}"
|
||||
capital = float(INITIAL)
|
||||
trades_list = []
|
||||
|
||||
for i in range(max(50, lookback + 1), n1h - hold):
|
||||
day = ts1h.iloc[i].strftime("%Y-%m-%d")
|
||||
|
||||
# Bullish divergence: price new low, RSI higher low
|
||||
price_new_low = c1h[i] < np.min(c1h[i - lookback : i])
|
||||
rsi_higher = rsi_14[i] > np.min(rsi_14[i - lookback : i]) and rsi_14[i] < rsi_thr_low
|
||||
|
||||
# Bearish divergence: price new high, RSI lower high
|
||||
price_new_high = c1h[i] > np.max(c1h[i - lookback : i])
|
||||
rsi_lower = rsi_14[i] < np.max(rsi_14[i - lookback : i]) and rsi_14[i] > rsi_thr_high
|
||||
|
||||
direction = None
|
||||
if price_new_low and rsi_higher:
|
||||
direction = "long"
|
||||
elif price_new_high and rsi_lower:
|
||||
direction = "short"
|
||||
|
||||
if direction is None:
|
||||
continue
|
||||
|
||||
entry = c1h[i]
|
||||
exit_price = c1h[min(i + hold, n1h - 1)]
|
||||
trade_ret = ((exit_price - entry) / entry if direction == "long" else (entry - exit_price) / entry)
|
||||
net = trade_ret * LEVERAGE - FEE_RT * LEVERAGE
|
||||
capital += capital * 0.12 * net
|
||||
capital = max(capital, 10)
|
||||
trades_list.append({"year": ts1h.iloc[i].year, "win": trade_ret > 0})
|
||||
|
||||
if len(trades_list) > 30:
|
||||
acc = sum(1 for t in trades_list if t["win"]) / len(trades_list) * 100
|
||||
ret = (capital - INITIAL) / INITIAL * 100
|
||||
results[name] = {"acc": acc, "ret": ret, "trades": len(trades_list), "capital": capital}
|
||||
|
||||
# ======================================================
|
||||
# STRAT 3: Momentum regime — trend following solo in low-vol regime
|
||||
# ======================================================
|
||||
for fast, slow, vol_w, vol_thr, hold in [
|
||||
(8, 21, 48, 0.8, 12),
|
||||
(5, 13, 24, 0.8, 6),
|
||||
(13, 34, 72, 0.7, 24),
|
||||
(8, 21, 48, 0.9, 8),
|
||||
]:
|
||||
name = f"MomReg_f{fast}s{slow}_h{hold}"
|
||||
ema_f = ema(c1h, fast)
|
||||
ema_s = ema(c1h, slow)
|
||||
|
||||
rv_short = np.full(n1h, np.nan)
|
||||
rv_long = np.full(n1h, np.nan)
|
||||
lr = np.diff(np.log(np.where(c1h == 0, 1e-10, c1h)))
|
||||
for idx in range(vol_w, len(lr)):
|
||||
rv_short[idx + 1] = np.std(lr[idx - min(12, vol_w) : idx])
|
||||
rv_long[idx + 1] = np.std(lr[idx - vol_w : idx])
|
||||
|
||||
capital = float(INITIAL)
|
||||
trades_list = []
|
||||
daily_done = set()
|
||||
|
||||
for i in range(max(60, slow + 1), n1h - hold):
|
||||
if np.isnan(ema_f[i]) or np.isnan(ema_s[i]) or np.isnan(rv_short[i]) or np.isnan(rv_long[i]):
|
||||
continue
|
||||
if rv_long[i] <= 0:
|
||||
continue
|
||||
|
||||
day = ts1h.iloc[i].strftime("%Y-%m-%d")
|
||||
if day in daily_done:
|
||||
continue
|
||||
|
||||
# Only trade in low-vol regime
|
||||
vol_ratio = rv_short[i] / rv_long[i]
|
||||
if vol_ratio > vol_thr:
|
||||
continue
|
||||
|
||||
# EMA crossover signal
|
||||
cross_up = ema_f[i] > ema_s[i] and ema_f[i - 1] <= ema_s[i - 1]
|
||||
cross_down = ema_f[i] < ema_s[i] and ema_f[i - 1] >= ema_s[i - 1]
|
||||
|
||||
if not (cross_up or cross_down):
|
||||
continue
|
||||
|
||||
direction = "long" if cross_up else "short"
|
||||
entry = c1h[i]
|
||||
exit_price = c1h[min(i + hold, n1h - 1)]
|
||||
trade_ret = ((exit_price - entry) / entry if direction == "long" else (entry - exit_price) / entry)
|
||||
net = trade_ret * LEVERAGE - FEE_RT * LEVERAGE
|
||||
capital += capital * 0.15 * net
|
||||
capital = max(capital, 10)
|
||||
trades_list.append({"year": ts1h.iloc[i].year, "win": trade_ret > 0})
|
||||
daily_done.add(day)
|
||||
|
||||
if len(trades_list) > 30:
|
||||
acc = sum(1 for t in trades_list if t["win"]) / len(trades_list) * 100
|
||||
ret = (capital - INITIAL) / INITIAL * 100
|
||||
results[name] = {"acc": acc, "ret": ret, "trades": len(trades_list), "capital": capital}
|
||||
|
||||
# ======================================================
|
||||
# STRAT 4: Multi-TF confirmation (15m entry, 1h trend)
|
||||
# ======================================================
|
||||
c15 = df_15m["close"].values
|
||||
h15 = df_15m["high"].values
|
||||
l15 = df_15m["low"].values
|
||||
ts15 = df_15m["timestamp"].values
|
||||
n15 = len(c15)
|
||||
|
||||
ema_1h_50 = ema(c1h, 50)
|
||||
rsi_15m = rsi(c15, 14)
|
||||
|
||||
capital = float(INITIAL)
|
||||
trades_list = []
|
||||
daily_done = set()
|
||||
|
||||
for i in range(100, n15 - 12):
|
||||
ts_dt = pd.Timestamp(ts15[i], unit="ms", tz="UTC")
|
||||
day = ts_dt.strftime("%Y-%m-%d")
|
||||
if day in daily_done:
|
||||
continue
|
||||
|
||||
# 15m signal: RSI extreme
|
||||
if rsi_15m[i] > 35 and rsi_15m[i] < 65:
|
||||
continue
|
||||
|
||||
# Find matching 1h candle
|
||||
h_idx = np.searchsorted(ts1h.values.astype("int64"), ts15[i]) - 1
|
||||
if h_idx < 50 or h_idx >= n1h or np.isnan(ema_1h_50[h_idx]):
|
||||
continue
|
||||
|
||||
# 1h trend confirmation
|
||||
trend_up = c1h[h_idx] > ema_1h_50[h_idx]
|
||||
trend_down = c1h[h_idx] < ema_1h_50[h_idx]
|
||||
|
||||
direction = None
|
||||
if rsi_15m[i] < 30 and trend_up:
|
||||
direction = "long" # oversold in uptrend
|
||||
elif rsi_15m[i] > 70 and trend_down:
|
||||
direction = "short" # overbought in downtrend
|
||||
|
||||
if direction is None:
|
||||
continue
|
||||
|
||||
entry = c15[i]
|
||||
hold_bars = 12 # 12 × 15m = 3h
|
||||
exit_price = c15[min(i + hold_bars, n15 - 1)]
|
||||
trade_ret = ((exit_price - entry) / entry if direction == "long" else (entry - exit_price) / entry)
|
||||
net = trade_ret * LEVERAGE - FEE_RT * LEVERAGE
|
||||
capital += capital * 0.12 * net
|
||||
capital = max(capital, 10)
|
||||
trades_list.append({"year": ts_dt.year, "win": trade_ret > 0})
|
||||
daily_done.add(day)
|
||||
|
||||
if len(trades_list) > 30:
|
||||
acc = sum(1 for t in trades_list if t["win"]) / len(trades_list) * 100
|
||||
ret = (capital - INITIAL) / INITIAL * 100
|
||||
results["MultiTF_15m1h"] = {"acc": acc, "ret": ret, "trades": len(trades_list), "capital": capital}
|
||||
|
||||
# === PRINT RESULTS ===
|
||||
print(f"\n {'Strategy':<25s} {'Trades':>7s} {'Acc':>6s} {'Return':>10s} {'Capital':>10s}")
|
||||
print(f" {'-'*60}")
|
||||
for name, r in sorted(results.items(), key=lambda x: x[1]["acc"], reverse=True):
|
||||
tag = "✅" if r["acc"] >= 60 and r["ret"] > 30 else ""
|
||||
print(f" {name:<25s} {r['trades']:>7d} {r['acc']:>5.1f}% {r['ret']:>+9.1f}% €{r['capital']:>9,.0f} {tag}")
|
||||
|
||||
|
||||
for asset in ["ETH", "BTC"]:
|
||||
run_all_perpetual(asset)
|
||||
Reference in New Issue
Block a user