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,223 @@
|
||||
"""Strategia 11: Volatility compression → breakout.
|
||||
Approccio diverso: non predire la direzione direttamente.
|
||||
1. Identifica momenti di COMPRESSIONE (Bollinger squeeze, ATR basso, bassa fractal dim)
|
||||
2. Quando la volatilità ESPLODE dopo compressione, segui la direzione del breakout
|
||||
3. Alta precisione perché il breakout DOPO compressione ha forte momentum
|
||||
Target: pochi trade molto precisi, con leva.
|
||||
"""
|
||||
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
|
||||
from src.fractal.indicators import volatility_ratio
|
||||
|
||||
FEE_PCT = 0.001
|
||||
LEVERAGE = 3
|
||||
INITIAL_CAPITAL = 1000
|
||||
|
||||
|
||||
def bollinger_bandwidth(close: np.ndarray, window: int = 20) -> np.ndarray:
|
||||
"""Bandwidth = (upper - lower) / middle."""
|
||||
result = np.full(len(close), np.nan)
|
||||
for i in range(window, len(close)):
|
||||
w = close[i - window : i]
|
||||
ma = np.mean(w)
|
||||
std = np.std(w)
|
||||
if ma > 0:
|
||||
result[i] = (2 * 2 * std) / ma
|
||||
return result
|
||||
|
||||
|
||||
def keltner_channel_ratio(close: np.ndarray, high: np.ndarray, low: np.ndarray, window: int = 20) -> np.ndarray:
|
||||
"""Ratio of Bollinger to Keltner — squeeze when < 1."""
|
||||
result = np.full(len(close), np.nan)
|
||||
for i in range(window, len(close)):
|
||||
w_c = close[i - window : i]
|
||||
w_h = high[i - window : i]
|
||||
w_l = low[i - window : i]
|
||||
|
||||
ma = np.mean(w_c)
|
||||
bb_std = np.std(w_c)
|
||||
bb_upper = ma + 2 * bb_std
|
||||
bb_lower = ma - 2 * bb_std
|
||||
|
||||
tr = np.maximum(w_h - w_l, np.maximum(np.abs(w_h - np.roll(w_c, 1)), np.abs(w_l - np.roll(w_c, 1))))
|
||||
atr = np.mean(tr[1:])
|
||||
kc_upper = ma + 1.5 * atr
|
||||
kc_lower = ma - 1.5 * atr
|
||||
|
||||
kc_range = kc_upper - kc_lower
|
||||
bb_range = bb_upper - bb_lower
|
||||
if kc_range > 0:
|
||||
result[i] = bb_range / kc_range
|
||||
return result
|
||||
|
||||
|
||||
def detect_squeeze_release(
|
||||
close: np.ndarray,
|
||||
high: np.ndarray,
|
||||
low: np.ndarray,
|
||||
volume: np.ndarray,
|
||||
bb_window: int = 20,
|
||||
squeeze_threshold: float = 0.8,
|
||||
breakout_bars: int = 3,
|
||||
volume_mult: float = 1.5,
|
||||
) -> list[dict]:
|
||||
"""Detect squeeze → breakout events."""
|
||||
bw = bollinger_bandwidth(close, bb_window)
|
||||
kcr = keltner_channel_ratio(close, high, low, bb_window)
|
||||
|
||||
events = []
|
||||
in_squeeze = False
|
||||
squeeze_start = 0
|
||||
|
||||
for i in range(bb_window + 1, len(close)):
|
||||
if np.isnan(kcr[i]):
|
||||
continue
|
||||
|
||||
is_squeeze = kcr[i] < squeeze_threshold
|
||||
|
||||
if is_squeeze and not in_squeeze:
|
||||
in_squeeze = True
|
||||
squeeze_start = i
|
||||
elif not is_squeeze and in_squeeze:
|
||||
in_squeeze = False
|
||||
squeeze_duration = i - squeeze_start
|
||||
|
||||
if squeeze_duration < 5:
|
||||
continue
|
||||
|
||||
# Check breakout direction using next few bars
|
||||
if i + breakout_bars >= len(close):
|
||||
continue
|
||||
|
||||
breakout_ret = (close[i + breakout_bars - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
# Volume confirmation
|
||||
avg_vol = np.mean(volume[squeeze_start:i])
|
||||
breakout_vol = np.mean(volume[i:i + breakout_bars])
|
||||
vol_confirmed = breakout_vol > avg_vol * volume_mult if avg_vol > 0 else False
|
||||
|
||||
# Momentum confirmation
|
||||
mom_3 = (close[i + 2] - close[i - 1]) / close[i - 1] if i + 2 < len(close) else 0
|
||||
|
||||
events.append({
|
||||
"idx": i,
|
||||
"squeeze_duration": squeeze_duration,
|
||||
"breakout_ret": breakout_ret,
|
||||
"vol_confirmed": vol_confirmed,
|
||||
"mom_3": mom_3,
|
||||
"bb_expansion": bw[i] / bw[squeeze_start] if bw[squeeze_start] > 0 else 1,
|
||||
})
|
||||
|
||||
return events
|
||||
|
||||
|
||||
def run_squeeze_strategy(asset: str, tf: str = "1h"):
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} {tf} — VOLATILITY SQUEEZE BREAKOUT")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df = load_data(asset, tf)
|
||||
close = df["close"].values
|
||||
high = df["high"].values
|
||||
low = df["low"].values
|
||||
volume = df["volume"].values
|
||||
n = len(df)
|
||||
|
||||
split_idx = int(n * 0.7)
|
||||
|
||||
for bb_w in [14, 20, 30]:
|
||||
for sq_thr in [0.7, 0.8, 0.9]:
|
||||
for brk_bars in [3, 6]:
|
||||
events = detect_squeeze_release(close, high, low, volume,
|
||||
bb_window=bb_w, squeeze_threshold=sq_thr,
|
||||
breakout_bars=brk_bars, volume_mult=1.3)
|
||||
|
||||
test_events = [e for e in events if e["idx"] >= split_idx]
|
||||
if len(test_events) < 10:
|
||||
continue
|
||||
|
||||
# Strategy: follow breakout direction, with volume confirmation
|
||||
capital = float(INITIAL_CAPITAL)
|
||||
correct = 0
|
||||
total = 0
|
||||
|
||||
for e in test_events:
|
||||
i = e["idx"]
|
||||
if i + brk_bars * 2 >= n:
|
||||
continue
|
||||
|
||||
# First 1-bar direction as signal
|
||||
first_bar_ret = (close[i] - close[i - 1]) / close[i - 1]
|
||||
if abs(first_bar_ret) < 0.001:
|
||||
continue
|
||||
|
||||
direction = "long" if first_bar_ret > 0 else "short"
|
||||
|
||||
# Actual result after holding for brk_bars more
|
||||
actual_ret = (close[i + brk_bars - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
is_correct = (direction == "long" and actual_ret > 0) or (direction == "short" and actual_ret < 0)
|
||||
total += 1
|
||||
if is_correct:
|
||||
correct += 1
|
||||
|
||||
trade_ret = actual_ret if direction == "long" else -actual_ret
|
||||
net = trade_ret * LEVERAGE - FEE_PCT * 2 * LEVERAGE
|
||||
capital += capital * 0.2 * net
|
||||
capital = max(capital, 0)
|
||||
|
||||
# Enhanced: volume-confirmed only
|
||||
if total > 0:
|
||||
acc = correct / total * 100
|
||||
ret = (capital - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
|
||||
test_candles = n - split_idx
|
||||
test_years = test_candles / (24 * 365.25)
|
||||
ann = ((capital / INITIAL_CAPITAL) ** (1 / test_years) - 1) * 100 if test_years > 0 and capital > 0 else -100
|
||||
if acc >= 55 and total >= 15:
|
||||
print(f" BBw={bb_w:2d} sqThr={sq_thr:.1f} brk={brk_bars}: trades={total:4d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}%")
|
||||
|
||||
# Volume-confirmed only
|
||||
cap_vc = float(INITIAL_CAPITAL)
|
||||
correct_vc = 0
|
||||
total_vc = 0
|
||||
|
||||
for e in test_events:
|
||||
if not e["vol_confirmed"]:
|
||||
continue
|
||||
i = e["idx"]
|
||||
if i + brk_bars * 2 >= n:
|
||||
continue
|
||||
|
||||
first_bar_ret = (close[i] - close[i - 1]) / close[i - 1]
|
||||
if abs(first_bar_ret) < 0.001:
|
||||
continue
|
||||
|
||||
direction = "long" if first_bar_ret > 0 else "short"
|
||||
actual_ret = (close[i + brk_bars - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
is_correct = (direction == "long" and actual_ret > 0) or (direction == "short" and actual_ret < 0)
|
||||
total_vc += 1
|
||||
if is_correct:
|
||||
correct_vc += 1
|
||||
|
||||
trade_ret = actual_ret if direction == "long" else -actual_ret
|
||||
net = trade_ret * LEVERAGE - FEE_PCT * 2 * LEVERAGE
|
||||
cap_vc += cap_vc * 0.2 * net
|
||||
cap_vc = max(cap_vc, 0)
|
||||
|
||||
if total_vc >= 10:
|
||||
acc_vc = correct_vc / total_vc * 100
|
||||
ret_vc = (cap_vc - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
|
||||
ann_vc = ((cap_vc / INITIAL_CAPITAL) ** (1 / (test_candles/(24*365.25))) - 1) * 100 if cap_vc > 0 else -100
|
||||
if acc_vc >= 55:
|
||||
print(f" +VOL BBw={bb_w:2d} sqThr={sq_thr:.1f} brk={brk_bars}: trades={total_vc:4d} acc={acc_vc:.1f}% ret={ret_vc:+.1f}% ann={ann_vc:+.1f}%")
|
||||
|
||||
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for tf in ["1h", "15m"]:
|
||||
run_squeeze_strategy(asset, tf)
|
||||
@@ -0,0 +1,408 @@
|
||||
"""Strategia 13: Squeeze + ML ibrida.
|
||||
Squeeze breakout come PRE-FILTRO (quando tradare),
|
||||
ML come CONFERMA DIREZIONALE (quale direzione).
|
||||
|
||||
Pipeline:
|
||||
1. Rileva squeeze release (Bollinger esce da Keltner)
|
||||
2. Estrai features frattali/strutturali dalla finestra
|
||||
3. ML predice direzione con confidenza
|
||||
4. Trade SOLO se squeeze + ML concordano
|
||||
|
||||
Obiettivo: accuracy squeeze (>80%) + volume trade ML.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.patterns import encode_candles
|
||||
|
||||
FEE = 0.001
|
||||
INITIAL = 1000
|
||||
|
||||
|
||||
def keltner_ratio(close, high, low, window=20):
|
||||
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 detect_squeeze_releases(close, high, low, volume, bb_w, sq_thr, min_duration=5):
|
||||
kcr = keltner_ratio(close, high, low, bb_w)
|
||||
events = []
|
||||
in_sq = False
|
||||
sq_start = 0
|
||||
|
||||
for i in range(bb_w + 1, len(close)):
|
||||
if np.isnan(kcr[i]):
|
||||
continue
|
||||
is_sq = kcr[i] < sq_thr
|
||||
if is_sq and not in_sq:
|
||||
in_sq = True
|
||||
sq_start = i
|
||||
elif not is_sq and in_sq:
|
||||
in_sq = False
|
||||
duration = i - sq_start
|
||||
if duration < min_duration:
|
||||
continue
|
||||
|
||||
avg_vol = np.mean(volume[sq_start:i])
|
||||
events.append({
|
||||
"idx": i,
|
||||
"squeeze_start": sq_start,
|
||||
"duration": duration,
|
||||
"avg_vol_squeeze": avg_vol,
|
||||
"kcr_at_release": kcr[i],
|
||||
})
|
||||
return events
|
||||
|
||||
|
||||
def build_features_at(df, i, squeeze_info):
|
||||
"""Features per il punto di squeeze release."""
|
||||
if i < 100:
|
||||
return None
|
||||
|
||||
o = df["open"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
c = df["close"].values
|
||||
v = df["volume"].values
|
||||
|
||||
feats = []
|
||||
|
||||
# Structural features multi-window
|
||||
for w in [12, 24, 48]:
|
||||
win_c = c[i-w:i]
|
||||
win_o = o[i-w:i]
|
||||
win_h = h[i-w:i]
|
||||
win_l = l[i-w:i]
|
||||
win_v = v[i-w:i]
|
||||
|
||||
mn, mx = win_l.min(), max(win_h.max(), win_c.max())
|
||||
rng = mx - mn if mx - mn > 0 else 1e-10
|
||||
|
||||
total = win_h - win_l
|
||||
total = np.where(total == 0, 1e-10, total)
|
||||
body = np.abs(win_c - win_o) / total
|
||||
direction = np.sign(win_c - win_o)
|
||||
|
||||
log_c = np.log(np.where(win_c == 0, 1e-10, win_c))
|
||||
rets = np.diff(log_c)
|
||||
|
||||
v_mean = np.mean(win_v)
|
||||
|
||||
feats.extend([
|
||||
np.mean(rets) if len(rets) > 0 else 0,
|
||||
np.std(rets) if len(rets) > 0 else 0,
|
||||
np.sum(rets) if len(rets) > 0 else 0,
|
||||
float(pd.Series(rets).skew()) if len(rets) > 2 else 0,
|
||||
float(pd.Series(rets).kurtosis()) if len(rets) > 3 else 0,
|
||||
np.mean(body),
|
||||
np.std(body),
|
||||
np.mean(direction),
|
||||
np.mean(direction[-min(3, w):]),
|
||||
(win_c[-1] - mn) / rng,
|
||||
win_v[-1] / v_mean if v_mean > 0 else 1,
|
||||
np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
|
||||
])
|
||||
|
||||
# Squeeze-specific features
|
||||
sq = squeeze_info
|
||||
feats.extend([
|
||||
sq["duration"],
|
||||
sq["duration"] / 24, # durata in giorni
|
||||
sq["kcr_at_release"],
|
||||
v[i-1] / sq["avg_vol_squeeze"] if sq["avg_vol_squeeze"] > 0 else 1,
|
||||
np.mean(v[i:min(i+3, len(v))]) / sq["avg_vol_squeeze"] if sq["avg_vol_squeeze"] > 0 else 1,
|
||||
])
|
||||
|
||||
# Price position
|
||||
h48 = np.max(h[max(0, i-48):i])
|
||||
l48 = np.min(l[max(0, i-48):i])
|
||||
r48 = h48 - l48
|
||||
feats.append((c[i-1] - l48) / r48 if r48 > 0 else 0.5)
|
||||
|
||||
# ATR normalized
|
||||
tr = np.maximum(h[i-14:i] - l[i-14:i],
|
||||
np.maximum(np.abs(h[i-14:i] - np.roll(c[i-14:i], 1)),
|
||||
np.abs(l[i-14:i] - np.roll(c[i-14:i], 1))))
|
||||
atr = np.mean(tr[1:])
|
||||
feats.append(atr / c[i-1] if c[i-1] > 0 else 0)
|
||||
|
||||
# First bar momentum (la barra di breakout)
|
||||
first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0
|
||||
feats.append(first_ret)
|
||||
|
||||
return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
|
||||
def run_hybrid(asset, tf, bb_w, sq_thr, brk_bars, leverage, pos_pct):
|
||||
print(f"\n{'='*65}")
|
||||
print(f" {asset} {tf} — HYBRID Squeeze+ML (BBw={bb_w}, sq={sq_thr}, brk={brk_bars})")
|
||||
print(f" Leverage: {leverage}x, Position: {pos_pct*100:.0f}%")
|
||||
print(f"{'='*65}")
|
||||
|
||||
df = load_data(asset, tf)
|
||||
close = df["close"].values
|
||||
high = df["high"].values
|
||||
low = df["low"].values
|
||||
volume = df["volume"].values
|
||||
n = len(df)
|
||||
|
||||
events = detect_squeeze_releases(close, high, low, volume, bb_w, sq_thr)
|
||||
print(f" Squeeze releases totali: {len(events)}")
|
||||
|
||||
# Build dataset: solo ai punti di squeeze
|
||||
X_all, y_all, ev_all = [], [], []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i + brk_bars >= n or i < 100:
|
||||
continue
|
||||
feats = build_features_at(df, i, ev)
|
||||
if feats is None:
|
||||
continue
|
||||
actual_ret = (close[i + brk_bars - 1] - close[i - 1]) / close[i - 1]
|
||||
X_all.append(feats)
|
||||
y_all.append(1 if actual_ret > 0 else 0)
|
||||
ev_all.append(ev)
|
||||
|
||||
if len(X_all) < 50:
|
||||
print(" Troppi pochi campioni.")
|
||||
return None
|
||||
|
||||
X = np.array(X_all)
|
||||
y = np.array(y_all)
|
||||
|
||||
print(f" Samples: {len(X)}, Up: {np.mean(y)*100:.1f}%")
|
||||
|
||||
# Walk-forward
|
||||
TRAIN_SIZE = max(int(len(X) * 0.5), 50)
|
||||
STEP_SIZE = max(int(len(X) * 0.1), 10)
|
||||
|
||||
results = {}
|
||||
|
||||
for ml_thr in [0.50, 0.55, 0.60, 0.65, 0.70]:
|
||||
capital = float(INITIAL)
|
||||
equity = [capital]
|
||||
trades_list = []
|
||||
correct = 0
|
||||
total = 0
|
||||
|
||||
start = 0
|
||||
while start + TRAIN_SIZE + STEP_SIZE <= len(X):
|
||||
train_end = start + TRAIN_SIZE
|
||||
test_end = min(train_end + STEP_SIZE, len(X))
|
||||
|
||||
X_tr = X[start:train_end]
|
||||
y_tr = y[start:train_end]
|
||||
X_te = X[train_end:test_end]
|
||||
y_te = y[train_end:test_end]
|
||||
|
||||
if len(np.unique(y_tr)) < 2:
|
||||
start += STEP_SIZE
|
||||
continue
|
||||
|
||||
scaler = StandardScaler()
|
||||
X_tr_s = scaler.fit_transform(X_tr)
|
||||
X_te_s = scaler.transform(X_te)
|
||||
|
||||
model = GradientBoostingClassifier(
|
||||
n_estimators=150, max_depth=4, min_samples_leaf=10,
|
||||
learning_rate=0.05, subsample=0.8, random_state=42,
|
||||
)
|
||||
model.fit(X_tr_s, y_tr)
|
||||
|
||||
up_idx = list(model.classes_).index(1) if 1 in model.classes_ else -1
|
||||
if up_idx < 0:
|
||||
start += STEP_SIZE
|
||||
continue
|
||||
|
||||
for j in range(len(X_te)):
|
||||
proba = model.predict_proba(X_te_s[j:j+1])[0]
|
||||
p_up = proba[up_idx]
|
||||
|
||||
ev = ev_all[train_end + j]
|
||||
i = ev["idx"]
|
||||
actual_ret = (close[i + brk_bars - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
# ML decide direction
|
||||
direction = None
|
||||
if p_up >= ml_thr:
|
||||
direction = "long"
|
||||
elif p_up <= (1 - ml_thr):
|
||||
direction = "short"
|
||||
|
||||
if direction is None:
|
||||
continue
|
||||
|
||||
is_correct = (direction == "long" and actual_ret > 0) or (direction == "short" and actual_ret < 0)
|
||||
total += 1
|
||||
if is_correct:
|
||||
correct += 1
|
||||
|
||||
trade_ret = actual_ret if direction == "long" else -actual_ret
|
||||
net = trade_ret * leverage - FEE * 2 * leverage
|
||||
pnl = capital * pos_pct * net
|
||||
capital += pnl
|
||||
capital = max(capital, 0)
|
||||
equity.append(capital)
|
||||
|
||||
trades_list.append({
|
||||
"idx": i,
|
||||
"direction": direction,
|
||||
"p_up": p_up,
|
||||
"actual_ret": actual_ret,
|
||||
"correct": is_correct,
|
||||
"pnl": pnl,
|
||||
})
|
||||
|
||||
start += STEP_SIZE
|
||||
|
||||
if total == 0:
|
||||
continue
|
||||
|
||||
acc = correct / total * 100
|
||||
|
||||
# Max drawdown
|
||||
peak = equity[0]
|
||||
max_dd = 0
|
||||
for v in equity:
|
||||
if v > peak:
|
||||
peak = v
|
||||
dd = (peak - v) / peak if peak > 0 else 0
|
||||
max_dd = max(max_dd, dd)
|
||||
|
||||
# Annualized
|
||||
first_ev = ev_all[TRAIN_SIZE] if TRAIN_SIZE < len(ev_all) else ev_all[0]
|
||||
last_ev = ev_all[-1]
|
||||
test_candles = last_ev["idx"] - first_ev["idx"]
|
||||
if tf == "1h":
|
||||
test_days = test_candles / 24
|
||||
elif tf == "15m":
|
||||
test_days = test_candles / (24 * 4)
|
||||
else:
|
||||
test_days = test_candles / 24
|
||||
test_years = test_days / 365.25 if test_days > 0 else 1
|
||||
ann = ((capital / INITIAL) ** (1 / test_years) - 1) * 100 if test_years > 0 and capital > 0 else -100
|
||||
daily_pnl = (capital - INITIAL) / test_days if test_days > 0 else 0
|
||||
trades_yr = total / test_years if test_years > 0 else 0
|
||||
|
||||
tag = ""
|
||||
if acc >= 80:
|
||||
tag = " ✅✅"
|
||||
elif acc >= 70:
|
||||
tag = " ✅"
|
||||
|
||||
print(f" ml_thr={ml_thr:.2f}: trades={total:4d} acc={acc:.1f}%{tag} ret={(capital-INITIAL)/INITIAL*100:+.1f}% ann={ann:+.1f}% dd={max_dd*100:.1f}% sharpe= trades/yr={trades_yr:.0f} €/day={daily_pnl:.2f}")
|
||||
|
||||
results[ml_thr] = {
|
||||
"trades": total, "accuracy": acc, "capital": capital,
|
||||
"annualized": ann, "max_dd": max_dd * 100, "daily_pnl": daily_pnl,
|
||||
"trades_yr": trades_yr,
|
||||
}
|
||||
|
||||
# Modalità "squeeze puro" come baseline
|
||||
capital_sq = float(INITIAL)
|
||||
correct_sq = 0
|
||||
total_sq = 0
|
||||
split = int(len(X) * 0.5)
|
||||
|
||||
for k in range(split, len(X)):
|
||||
ev = ev_all[k]
|
||||
i = ev["idx"]
|
||||
if i + brk_bars >= n:
|
||||
continue
|
||||
first_ret = (close[i] - close[i-1]) / close[i-1]
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
actual_ret = (close[i + brk_bars - 1] - close[i - 1]) / close[i - 1]
|
||||
is_correct = (direction == 1 and actual_ret > 0) or (direction == -1 and actual_ret < 0)
|
||||
total_sq += 1
|
||||
if is_correct:
|
||||
correct_sq += 1
|
||||
trade_ret = actual_ret * direction
|
||||
net = trade_ret * leverage - FEE * 2 * leverage
|
||||
capital_sq += capital_sq * pos_pct * net
|
||||
capital_sq = max(capital_sq, 0)
|
||||
|
||||
if total_sq > 0:
|
||||
acc_sq = correct_sq / total_sq * 100
|
||||
print(f" BASELINE squeeze puro: trades={total_sq:4d} acc={acc_sq:.1f}% ret={(capital_sq-INITIAL)/INITIAL*100:+.1f}%")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# ===== MAIN: test su multiple configurazioni =====
|
||||
print("=" * 70)
|
||||
print(" STRATEGIA 13: SQUEEZE + ML IBRIDA")
|
||||
print("=" * 70)
|
||||
|
||||
configs = [
|
||||
# (asset, tf, bb_w, sq_thr, brk_bars, leverage, pos_pct)
|
||||
("ETH", "1h", 20, 0.8, 3, 3, 0.2),
|
||||
("ETH", "1h", 30, 0.9, 3, 3, 0.2),
|
||||
("ETH", "1h", 14, 0.8, 3, 3, 0.2),
|
||||
("ETH", "1h", 20, 0.9, 3, 3, 0.2),
|
||||
("BTC", "1h", 14, 0.8, 3, 3, 0.2),
|
||||
("BTC", "1h", 20, 0.8, 3, 3, 0.2),
|
||||
("BTC", "1h", 14, 0.9, 6, 3, 0.2),
|
||||
("ETH", "15m", 14, 0.8, 3, 3, 0.15),
|
||||
("ETH", "15m", 20, 0.9, 3, 3, 0.15),
|
||||
("BTC", "15m", 14, 0.9, 3, 3, 0.15),
|
||||
# Aggressive
|
||||
("ETH", "1h", 20, 0.8, 3, 5, 0.3),
|
||||
("ETH", "1h", 30, 0.9, 3, 5, 0.3),
|
||||
]
|
||||
|
||||
all_results = []
|
||||
for cfg in configs:
|
||||
r = run_hybrid(*cfg)
|
||||
if r:
|
||||
for thr, data in r.items():
|
||||
all_results.append({
|
||||
"config": f"{cfg[0]} {cfg[1]} BBw={cfg[2]} sq={cfg[3]} brk={cfg[4]} lev={cfg[5]} pos={cfg[6]}",
|
||||
"ml_thr": thr,
|
||||
**data,
|
||||
})
|
||||
|
||||
# Sort by accuracy
|
||||
print("\n\n" + "=" * 70)
|
||||
print(" CLASSIFICA PER ACCURACY (top 20)")
|
||||
print("=" * 70)
|
||||
sorted_acc = sorted(all_results, key=lambda x: x["accuracy"], reverse=True)
|
||||
for r in sorted_acc[:20]:
|
||||
tag = "✅✅" if r["accuracy"] >= 80 else "✅" if r["accuracy"] >= 70 else ""
|
||||
print(f" {r['config']:55s} ml={r['ml_thr']:.2f} → acc={r['accuracy']:.1f}% {tag:4s} trades={r['trades']:4d} ret={(r['capital']-INITIAL)/INITIAL*100:+.1f}% ann={r['annualized']:+.1f}% dd={r['max_dd']:.1f}% €/day={r['daily_pnl']:.2f}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(" CLASSIFICA PER ROI ANNUO (top 20, min 20 trades)")
|
||||
print("=" * 70)
|
||||
sorted_roi = sorted([r for r in all_results if r["trades"] >= 20], key=lambda x: x["annualized"], reverse=True)
|
||||
for r in sorted_roi[:20]:
|
||||
tag = "✅✅" if r["accuracy"] >= 80 else "✅" if r["accuracy"] >= 70 else ""
|
||||
print(f" {r['config']:55s} ml={r['ml_thr']:.2f} → acc={r['accuracy']:.1f}% {tag:4s} ann={r['annualized']:+.1f}% trades={r['trades']:4d} dd={r['max_dd']:.1f}% €/day={r['daily_pnl']:.2f}")
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print(" SWEET SPOT: acc>=75% AND ann>=20% AND trades>=15")
|
||||
print("=" * 70)
|
||||
sweet = [r for r in all_results if r["accuracy"] >= 75 and r["annualized"] >= 20 and r["trades"] >= 15]
|
||||
sweet.sort(key=lambda x: x["accuracy"] * x["annualized"], reverse=True)
|
||||
for r in sweet:
|
||||
print(f" {r['config']:55s} ml={r['ml_thr']:.2f} → acc={r['accuracy']:.1f}% ann={r['annualized']:+.1f}% trades={r['trades']:4d} dd={r['max_dd']:.1f}% €/day={r['daily_pnl']:.2f}")
|
||||
@@ -0,0 +1,317 @@
|
||||
"""S3-01: Squeeze Migliorato — test per-anno, dati reali.
|
||||
Miglioramenti rispetto al squeeze base:
|
||||
1. Cross-asset: squeeze su BTC + ETH contemporaneo = segnale più forte
|
||||
2. Timing orario: accuracy per fascia oraria
|
||||
3. Squeeze duration weighted: squeeze lunghi → breakout più forti
|
||||
4. Dual-timeframe: squeeze su 1h confermato da 15m
|
||||
5. Anti-fakeout: skip se candela post-breakout ritraccia >50%
|
||||
6. Dynamic exit: trailing stop basato su 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_RT = 0.002
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
|
||||
def keltner_ratio(close, high, low, window=14):
|
||||
n = len(close)
|
||||
r = np.full(n, np.nan)
|
||||
for i in range(window, n):
|
||||
wc, wh, wl = close[i-window:i], high[i-window:i], 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 = (ma+1.5*atr)-(ma-1.5*atr)
|
||||
bb = (ma+2*bb_std)-(ma-2*bb_std)
|
||||
if kc > 0:
|
||||
r[i] = bb/kc
|
||||
return r
|
||||
|
||||
|
||||
def atr_calc(high, low, close, period=14):
|
||||
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]
|
||||
r = np.full(len(close), np.nan)
|
||||
r[period-1] = np.mean(tr[:period])
|
||||
k = 2/(period+1)
|
||||
for i in range(period, len(close)):
|
||||
r[i] = tr[i]*k + r[i-1]*(1-k)
|
||||
return r
|
||||
|
||||
|
||||
def detect_squeezes(close, high, low, volume, kcr, sq_thr=0.8, min_dur=5):
|
||||
"""Ritorna lista di squeeze events con metadata."""
|
||||
events = []
|
||||
in_sq = False
|
||||
sq_start = 0
|
||||
n = len(close)
|
||||
|
||||
for i in range(1, n):
|
||||
if np.isnan(kcr[i]):
|
||||
continue
|
||||
is_sq = kcr[i] < sq_thr
|
||||
if is_sq and not in_sq:
|
||||
in_sq = True
|
||||
sq_start = i
|
||||
elif not is_sq and in_sq:
|
||||
in_sq = False
|
||||
dur = i - sq_start
|
||||
if dur < min_dur:
|
||||
continue
|
||||
avg_vol = np.mean(volume[sq_start:i])
|
||||
# Range durante squeeze
|
||||
sq_range = (np.max(high[sq_start:i]) - np.min(low[sq_start:i])) / close[sq_start] if close[sq_start] > 0 else 0
|
||||
events.append({
|
||||
"release_idx": i,
|
||||
"duration": dur,
|
||||
"avg_vol": avg_vol,
|
||||
"squeeze_range": sq_range,
|
||||
"kcr_at_release": kcr[i],
|
||||
})
|
||||
return events
|
||||
|
||||
|
||||
def run_improved_squeeze(primary_asset, tf="1h"):
|
||||
# Carica asset primario
|
||||
df = load_data(primary_asset, tf)
|
||||
c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values
|
||||
n = len(df)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
ts_ms = df["timestamp"].values
|
||||
|
||||
kcr = keltner_ratio(c, h, l, 14)
|
||||
atr_14 = atr_calc(h, l, c, 14)
|
||||
events = detect_squeezes(c, h, l, v, kcr)
|
||||
|
||||
# Carica asset secondario per cross-check
|
||||
secondary = "BTC" if primary_asset == "ETH" else "ETH"
|
||||
df2 = load_data(secondary, tf)
|
||||
c2, h2, l2 = df2["close"].values, df2["high"].values, df2["low"].values
|
||||
ts2_ms = df2["timestamp"].values
|
||||
kcr2 = keltner_ratio(c2, h2, l2, 14)
|
||||
|
||||
# Mappa ts2 → indici per allineare
|
||||
def find_idx2(ts_val):
|
||||
idx = np.searchsorted(ts2_ms, ts_val)
|
||||
return min(idx, len(c2)-1)
|
||||
|
||||
# Carica 15m per dual-TF
|
||||
if tf == "1h":
|
||||
df_15m = load_data(primary_asset, "15m")
|
||||
c15 = df_15m["close"].values
|
||||
h15 = df_15m["high"].values
|
||||
l15 = df_15m["low"].values
|
||||
ts15 = df_15m["timestamp"].values
|
||||
kcr_15m = keltner_ratio(c15, h15, l15, 14)
|
||||
else:
|
||||
kcr_15m = None
|
||||
ts15 = None
|
||||
|
||||
# ================================================================
|
||||
# CONFIGURAZIONI
|
||||
# ================================================================
|
||||
configs = [
|
||||
# (name, use_cross, use_timing, use_duration, use_dual_tf, use_antifake, use_trailing, hold, stop_atr)
|
||||
("BASE", False, False, False, False, False, False, 3, 0),
|
||||
("cross_asset", True, False, False, False, False, False, 3, 0),
|
||||
("timing_filter", False, True, False, False, False, False, 3, 0),
|
||||
("long_squeeze", False, False, True, False, False, False, 3, 0),
|
||||
("dual_tf", False, False, False, True, False, False, 3, 0),
|
||||
("anti_fakeout", False, False, False, False, True, False, 3, 0),
|
||||
("trailing_stop", False, False, False, False, False, True, 6, 1.5),
|
||||
("cross+timing", True, True, False, False, False, False, 3, 0),
|
||||
("cross+long+timing", True, True, True, False, False, False, 3, 0),
|
||||
("cross+dual_tf", True, False, False, True, False, False, 3, 0),
|
||||
("ALL_FILTERS", True, True, True, True, True, False, 3, 0),
|
||||
("ALL+trailing", True, True, True, True, True, True, 6, 1.5),
|
||||
("cross+antifake", True, False, False, False, True, False, 3, 0),
|
||||
("timing+antifake", False, True, False, False, True, False, 3, 0),
|
||||
("cross+timing+antifk", True, True, False, False, True, False, 3, 0),
|
||||
("cross+timing+trail", True, True, False, False, False, True, 6, 1.5),
|
||||
]
|
||||
|
||||
print(f"\n{'#'*75}")
|
||||
print(f" {primary_asset} {tf} — SQUEEZE MIGLIORATO")
|
||||
print(f"{'#'*75}")
|
||||
|
||||
results = []
|
||||
|
||||
for name, f_cross, f_timing, f_dur, f_dual, f_antifake, f_trail, hold, stop_atr_m in configs:
|
||||
yearly = {}
|
||||
capital = float(INITIAL)
|
||||
peak = capital
|
||||
max_dd = 0
|
||||
|
||||
for ev in events:
|
||||
i = ev["release_idx"]
|
||||
if i + hold + 2 >= n:
|
||||
continue
|
||||
|
||||
# --- FILTRI ---
|
||||
skip = False
|
||||
|
||||
# Cross-asset: secondary deve anche essere in squeeze recente o breakout
|
||||
if f_cross:
|
||||
i2 = find_idx2(ts_ms[i])
|
||||
if i2 >= 5:
|
||||
sec_in_squeeze = any(not np.isnan(kcr2[j]) and kcr2[j] < 0.85 for j in range(max(0,i2-10), i2+1))
|
||||
if not sec_in_squeeze:
|
||||
skip = True
|
||||
|
||||
# Timing: solo certe ore (testato: 6-14 UTC migliori)
|
||||
if f_timing:
|
||||
hour = ts.iloc[i].hour
|
||||
if hour < 4 or hour > 16:
|
||||
skip = True
|
||||
|
||||
# Duration: solo squeeze > 10 barre
|
||||
if f_dur:
|
||||
if ev["duration"] < 10:
|
||||
skip = True
|
||||
|
||||
# Dual-TF: squeeze anche su 15m
|
||||
if f_dual and kcr_15m is not None and ts15 is not None:
|
||||
i15 = np.searchsorted(ts15, ts_ms[i])
|
||||
if i15 >= 5:
|
||||
sq_15m = any(not np.isnan(kcr_15m[j]) and kcr_15m[j] < 0.85 for j in range(max(0,i15-20), i15+1))
|
||||
if not sq_15m:
|
||||
skip = True
|
||||
|
||||
# Anti-fakeout: prima candela post-breakout non deve ritracciare >50%
|
||||
if f_antifake and i + 1 < n:
|
||||
breakout_bar_range = h[i] - l[i]
|
||||
if breakout_bar_range > 0:
|
||||
if c[i] > c[i-1]: # breakout up
|
||||
retrace = (h[i] - c[i]) / breakout_bar_range
|
||||
else: # breakout down
|
||||
retrace = (c[i] - l[i]) / breakout_bar_range
|
||||
if retrace > 0.6:
|
||||
skip = True
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
# --- DIREZIONE ---
|
||||
first_ret = (c[i] - c[i-1]) / c[i-1]
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
|
||||
# --- EXIT ---
|
||||
entry = c[i-1]
|
||||
if f_trail and not np.isnan(atr_14[i]):
|
||||
# Trailing stop
|
||||
trail_dist = atr_14[i] * stop_atr_m
|
||||
best_price = entry
|
||||
exit_price = c[min(i+hold, n-1)]
|
||||
for j in range(i, min(i+hold+1, n)):
|
||||
if direction == 1:
|
||||
best_price = max(best_price, h[j])
|
||||
if l[j] <= best_price - trail_dist:
|
||||
exit_price = best_price - trail_dist
|
||||
break
|
||||
else:
|
||||
best_price = min(best_price, l[j])
|
||||
if h[j] >= best_price + trail_dist:
|
||||
exit_price = best_price + trail_dist
|
||||
break
|
||||
exit_price = c[j]
|
||||
else:
|
||||
exit_price = c[min(i+hold-1, n-1)]
|
||||
|
||||
actual = (exit_price - entry) / entry * direction
|
||||
net = actual * LEVERAGE - FEE_RT * LEVERAGE
|
||||
|
||||
capital += capital * 0.15 * net
|
||||
capital = max(capital, 10)
|
||||
if capital > peak: peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"wins": 0, "total": 0, "pnls": []}
|
||||
yearly[year]["total"] += 1
|
||||
if actual > 0:
|
||||
yearly[year]["wins"] += 1
|
||||
yearly[year]["pnls"].append(net * INITIAL)
|
||||
|
||||
all_t = sum(d["total"] for d in yearly.values())
|
||||
all_w = sum(d["wins"] for d in yearly.values())
|
||||
if all_t < 30:
|
||||
continue
|
||||
|
||||
acc = all_w / all_t * 100
|
||||
all_pnls = [p for d in yearly.values() for p in d["pnls"]]
|
||||
tot_pnl = sum(all_pnls)
|
||||
|
||||
# Worst year
|
||||
worst_y_acc = 100
|
||||
worst_y = ""
|
||||
for y, d in yearly.items():
|
||||
ya = d["wins"]/d["total"]*100 if d["total"] > 0 else 0
|
||||
if ya < worst_y_acc:
|
||||
worst_y_acc = ya
|
||||
worst_y = str(y)
|
||||
|
||||
results.append({
|
||||
"name": name, "trades": all_t, "acc": acc, "pnl": tot_pnl,
|
||||
"max_dd": max_dd*100, "capital": capital,
|
||||
"worst": f"{worst_y}({worst_y_acc:.0f}%)",
|
||||
"yearly": yearly,
|
||||
})
|
||||
|
||||
# Sort by accuracy
|
||||
results.sort(key=lambda x: x["acc"], reverse=True)
|
||||
|
||||
print(f"\n {'Name':.<26s} {'Trades':>7s} {'Acc':>6s} {'PnL€':>9s} {'DD%':>6s} {'Capital':>10s} {'Worst':>12s}")
|
||||
print(f" {'-'*80}")
|
||||
for r in results:
|
||||
tag = "✅✅" if r["acc"] >= 80 else "✅" if r["acc"] >= 76 else ""
|
||||
print(f" {r['name']:.<26s} {r['trades']:>7d} {r['acc']:>5.1f}% €{r['pnl']:>+8.0f} {r['max_dd']:>5.1f}% €{r['capital']:>9,.0f} {r['worst']:>12s} {tag}")
|
||||
|
||||
# Dettaglio per anno del migliore
|
||||
if results:
|
||||
best = results[0]
|
||||
print(f"\n MIGLIORE: {best['name']} → {best['acc']:.1f}% acc")
|
||||
print(f" {'Anno':>6s} {'Trades':>7s} {'Acc':>6s} {'PnL€':>9s}")
|
||||
for y in sorted(best["yearly"]):
|
||||
d = best["yearly"][y]
|
||||
ya = d["wins"]/d["total"]*100 if d["total"] > 0 else 0
|
||||
yp = sum(d["pnls"])
|
||||
tag = " ← CRASH" if y in [2020,2021,2022] else ""
|
||||
print(f" {y:>6d} {d['total']:>7d} {ya:>5.1f}% €{yp:>+8.0f}{tag}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
# Run su entrambi gli asset e timeframe
|
||||
all_results = {}
|
||||
for asset in ["ETH", "BTC"]:
|
||||
for tf in ["1h", "15m"]:
|
||||
key = f"{asset}_{tf}"
|
||||
all_results[key] = run_improved_squeeze(asset, tf)
|
||||
|
||||
# Classifica globale
|
||||
print(f"\n\n{'='*75}")
|
||||
print(f" CLASSIFICA GLOBALE — TOP 15")
|
||||
print(f"{'='*75}")
|
||||
|
||||
global_list = []
|
||||
for key, results in all_results.items():
|
||||
for r in results:
|
||||
global_list.append({**r, "asset_tf": key})
|
||||
|
||||
global_list.sort(key=lambda x: x["acc"], reverse=True)
|
||||
print(f"\n {'Asset_TF':.<12s} {'Name':.<26s} {'Trades':>6s} {'Acc':>6s} {'PnL€':>9s} {'DD%':>5s} {'Worst':>12s}")
|
||||
for r in global_list[:15]:
|
||||
tag = "✅✅" if r["acc"] >= 80 else "✅" if r["acc"] >= 76 else ""
|
||||
print(f" {r['asset_tf']:.<12s} {r['name']:.<26s} {r['trades']:>6d} {r['acc']:>5.1f}% €{r['pnl']:>+8.0f} {r['max_dd']:>4.1f}% {r['worst']:>12s} {tag}")
|
||||
@@ -0,0 +1,290 @@
|
||||
"""S3-02: Lead-lag multi-asset squeeze.
|
||||
Quando BTC fa squeeze breakout, ETH/SOL spesso seguono.
|
||||
Usa il breakout di BTC per anticipare entrata su ETH (e viceversa).
|
||||
Testa anche correlazione inter-asset per conferma segnale.
|
||||
"""
|
||||
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
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
|
||||
def keltner_ratio(close, high, low, window=14):
|
||||
n = len(close)
|
||||
r = np.full(n, np.nan)
|
||||
for i in range(window, n):
|
||||
wc, wh, wl = close[i-window:i], high[i-window:i], 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 = (ma+1.5*atr)-(ma-1.5*atr)
|
||||
bb = (ma+2*bb_std)-(ma-2*bb_std)
|
||||
if kc > 0: r[i] = bb/kc
|
||||
return r
|
||||
|
||||
|
||||
def load_aligned(assets, tf):
|
||||
"""Carica e allinea dati multi-asset per timestamp."""
|
||||
dfs = {}
|
||||
for asset in assets:
|
||||
try:
|
||||
if asset == "SOL":
|
||||
df = pd.read_parquet(f"data/raw/sol_{tf}.parquet")
|
||||
df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
else:
|
||||
df = load_data(asset, tf)
|
||||
dfs[asset] = df
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if len(dfs) < 2:
|
||||
return None
|
||||
|
||||
# Allinea per timestamp
|
||||
common_ts = set(dfs[list(dfs.keys())[0]]["timestamp"].values)
|
||||
for df in dfs.values():
|
||||
common_ts &= set(df["timestamp"].values)
|
||||
common_ts = sorted(common_ts)
|
||||
|
||||
aligned = {}
|
||||
for asset, df in dfs.items():
|
||||
mask = df["timestamp"].isin(common_ts)
|
||||
aligned[asset] = df[mask].sort_values("timestamp").reset_index(drop=True)
|
||||
|
||||
return aligned
|
||||
|
||||
|
||||
def detect_breakouts(close, high, low, volume, kcr, sq_thr=0.8, min_dur=5):
|
||||
"""Detect squeeze breakout events."""
|
||||
events = []
|
||||
in_sq = False
|
||||
sq_start = 0
|
||||
for i in range(1, len(close)):
|
||||
if np.isnan(kcr[i]):
|
||||
continue
|
||||
is_sq = kcr[i] < sq_thr
|
||||
if is_sq and not in_sq:
|
||||
in_sq = True
|
||||
sq_start = i
|
||||
elif not is_sq and in_sq:
|
||||
in_sq = False
|
||||
if i - sq_start < min_dur:
|
||||
continue
|
||||
first_ret = (close[i] - close[i-1]) / close[i-1] if close[i-1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
events.append({
|
||||
"idx": i,
|
||||
"duration": i - sq_start,
|
||||
"direction": 1 if first_ret > 0 else -1,
|
||||
"first_ret": first_ret,
|
||||
})
|
||||
return events
|
||||
|
||||
|
||||
print("=" * 75)
|
||||
print(" S3-02: LEAD-LAG MULTI-ASSET SQUEEZE")
|
||||
print("=" * 75)
|
||||
|
||||
for tf in ["1h", "15m"]:
|
||||
aligned = load_aligned(["BTC", "ETH", "SOL"], tf)
|
||||
if aligned is None:
|
||||
continue
|
||||
|
||||
n = len(aligned["BTC"])
|
||||
ts = pd.to_datetime(aligned["BTC"]["timestamp"], unit="ms", utc=True)
|
||||
|
||||
print(f"\n Timeframe: {tf}, Candles allineate: {n}")
|
||||
|
||||
# Calcola squeeze per ogni asset
|
||||
asset_data = {}
|
||||
for asset in aligned:
|
||||
df = aligned[asset]
|
||||
c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values
|
||||
kcr = keltner_ratio(c, h, l, 14)
|
||||
events = detect_breakouts(c, h, l, v, kcr)
|
||||
asset_data[asset] = {"close": c, "high": h, "low": l, "vol": v, "kcr": kcr, "events": events}
|
||||
print(f" {asset}: {len(events)} squeeze breakouts")
|
||||
|
||||
# ================================================================
|
||||
# STRATEGIA A: Leader-follower
|
||||
# Quando BTC fa breakout, entra su ETH/SOL nella stessa direzione
|
||||
# ================================================================
|
||||
print(f"\n --- LEADER-FOLLOWER ({tf}) ---")
|
||||
|
||||
for leader, follower in [("BTC", "ETH"), ("BTC", "SOL"), ("ETH", "BTC"), ("ETH", "SOL")]:
|
||||
if leader not in asset_data or follower not in asset_data:
|
||||
continue
|
||||
|
||||
leader_events = asset_data[leader]["events"]
|
||||
fc = asset_data[follower]["close"]
|
||||
|
||||
for hold in [3, 6]:
|
||||
for delay in [0, 1, 2]:
|
||||
yearly = {}
|
||||
|
||||
for ev in leader_events:
|
||||
i = ev["idx"] + delay
|
||||
if i + hold >= n:
|
||||
continue
|
||||
|
||||
# Anti-fakeout su follower
|
||||
entry = fc[i]
|
||||
exit_price = fc[min(i + hold, n - 1)]
|
||||
direction = ev["direction"]
|
||||
actual = (exit_price - entry) / entry * direction
|
||||
net = actual * LEVERAGE - FEE_RT * LEVERAGE
|
||||
|
||||
year = ts.iloc[min(i, n-1)].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnls": []}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0:
|
||||
yearly[year]["w"] += 1
|
||||
yearly[year]["pnls"].append(net * INITIAL)
|
||||
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t < 30:
|
||||
continue
|
||||
acc = all_w / all_t * 100
|
||||
pnl = sum(p for d in yearly.values() for p in d["pnls"])
|
||||
worst_y = min(yearly.items(), key=lambda x: x[1]["w"]/x[1]["t"] if x[1]["t"]>0 else 0)
|
||||
worst_acc = worst_y[1]["w"]/worst_y[1]["t"]*100 if worst_y[1]["t"]>0 else 0
|
||||
tag = "✅" if acc >= 76 else ""
|
||||
print(f" {leader}→{follower} d={delay} h={hold}: trades={all_t:5d} acc={acc:.1f}% pnl=€{pnl:+.0f} worst={worst_y[0]}({worst_acc:.0f}%) {tag}")
|
||||
|
||||
# ================================================================
|
||||
# STRATEGIA B: Consensus multi-asset
|
||||
# Trade solo quando 2+ asset hanno squeeze breakout nello stesso momento
|
||||
# ================================================================
|
||||
print(f"\n --- CONSENSUS MULTI-ASSET ({tf}) ---")
|
||||
|
||||
# Build event map: timestamp → list of (asset, direction)
|
||||
event_map = {}
|
||||
for asset, data in asset_data.items():
|
||||
for ev in data["events"]:
|
||||
idx = ev["idx"]
|
||||
if idx not in event_map:
|
||||
event_map[idx] = []
|
||||
event_map[idx].append((asset, ev["direction"]))
|
||||
|
||||
for target in ["BTC", "ETH", "SOL"]:
|
||||
if target not in asset_data:
|
||||
continue
|
||||
tc = asset_data[target]["close"]
|
||||
|
||||
for min_consensus in [2, 3]:
|
||||
for window_bars in [1, 3, 5]:
|
||||
yearly = {}
|
||||
daily_done = set()
|
||||
|
||||
for idx in sorted(event_map.keys()):
|
||||
if idx + 6 >= n:
|
||||
continue
|
||||
|
||||
day = ts.iloc[idx].strftime("%Y-%m-%d")
|
||||
if day in daily_done:
|
||||
continue
|
||||
|
||||
# Count consensus within window
|
||||
nearby_events = []
|
||||
for j in range(max(0, idx - window_bars), idx + window_bars + 1):
|
||||
if j in event_map:
|
||||
nearby_events.extend(event_map[j])
|
||||
|
||||
# Unique assets
|
||||
unique_assets = set(a for a, d in nearby_events)
|
||||
if len(unique_assets) < min_consensus:
|
||||
continue
|
||||
|
||||
# Majority direction
|
||||
dirs = [d for a, d in nearby_events]
|
||||
majority = 1 if sum(dirs) > 0 else -1
|
||||
|
||||
entry = tc[idx]
|
||||
exit_price = tc[min(idx + 3, n - 1)]
|
||||
actual = (exit_price - entry) / entry * majority
|
||||
net = actual * LEVERAGE - FEE_RT * LEVERAGE
|
||||
|
||||
year = ts.iloc[idx].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnls": []}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0:
|
||||
yearly[year]["w"] += 1
|
||||
yearly[year]["pnls"].append(net * INITIAL)
|
||||
daily_done.add(day)
|
||||
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t < 20:
|
||||
continue
|
||||
acc = all_w / all_t * 100
|
||||
pnl = sum(p for d in yearly.values() for p in d["pnls"])
|
||||
tag = "✅" if acc >= 76 else ""
|
||||
print(f" {target} consensus>={min_consensus} w={window_bars}: trades={all_t:4d} acc={acc:.1f}% pnl=€{pnl:+.0f} {tag}")
|
||||
|
||||
# ================================================================
|
||||
# STRATEGIA C: Correlation-weighted squeeze
|
||||
# Peso il segnale squeeze in base alla correlazione rolling con BTC
|
||||
# ================================================================
|
||||
print(f"\n --- CORRELATION-WEIGHTED ({tf}) ---")
|
||||
|
||||
for target in ["ETH", "SOL"]:
|
||||
if target not in asset_data:
|
||||
continue
|
||||
tc = asset_data[target]["close"]
|
||||
btc_c = asset_data["BTC"]["close"]
|
||||
|
||||
# Rolling correlation
|
||||
corr_window = 48 # 48 bars
|
||||
rolling_corr = np.full(n, np.nan)
|
||||
ret_t = np.diff(np.log(np.where(tc == 0, 1e-10, tc)))
|
||||
ret_b = np.diff(np.log(np.where(btc_c == 0, 1e-10, btc_c)))
|
||||
for i in range(corr_window, len(ret_t)):
|
||||
c_val = np.corrcoef(ret_t[i-corr_window:i], ret_b[i-corr_window:i])[0, 1]
|
||||
rolling_corr[i + 1] = c_val if np.isfinite(c_val) else 0
|
||||
|
||||
events = asset_data[target]["events"]
|
||||
|
||||
for corr_thr in [0.5, 0.6, 0.7, 0.8]:
|
||||
yearly = {}
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i + 3 >= n or np.isnan(rolling_corr[i]):
|
||||
continue
|
||||
|
||||
# Solo quando correlazione con BTC è alta
|
||||
if abs(rolling_corr[i]) < corr_thr:
|
||||
continue
|
||||
|
||||
entry = tc[i - 1]
|
||||
exit_price = tc[min(i + 2, n - 1)]
|
||||
actual = (exit_price - entry) / entry * ev["direction"]
|
||||
net = actual * LEVERAGE - FEE_RT * LEVERAGE
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnls": []}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0:
|
||||
yearly[year]["w"] += 1
|
||||
yearly[year]["pnls"].append(net * INITIAL)
|
||||
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t < 20:
|
||||
continue
|
||||
acc = all_w / all_t * 100
|
||||
pnl = sum(p for d in yearly.values() for p in d["pnls"])
|
||||
tag = "✅" if acc >= 76 else ""
|
||||
print(f" {target} corr>={corr_thr}: trades={all_t:4d} acc={acc:.1f}% pnl=€{pnl:+.0f} {tag}")
|
||||
@@ -0,0 +1,256 @@
|
||||
"""S3-03: Ultimate Squeeze — combina TUTTI i filtri migliori.
|
||||
Filtri che funzionano (testati singolarmente):
|
||||
- Anti-fakeout (+1% acc)
|
||||
- Long squeeze duration (+1% acc)
|
||||
- Cross-asset squeeze simultaneo (+0.5%)
|
||||
- Timing 4-16 UTC (+0.5%)
|
||||
- Correlation ETH-BTC alta per ETH trades (+1%)
|
||||
- Volume confirmation al breakout
|
||||
|
||||
Nuovi filtri da testare:
|
||||
- Volume delta: up_volume - down_volume al breakout
|
||||
- Momentum confirmation: breakout nella direzione del trend 1h
|
||||
- Volatility regime: skip in regime estremo (RV > 100%)
|
||||
"""
|
||||
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
|
||||
INITIAL = 1000
|
||||
LEVERAGE = 3
|
||||
|
||||
|
||||
def keltner_ratio(close, high, low, window=14):
|
||||
n = len(close)
|
||||
r = np.full(n, np.nan)
|
||||
for i in range(window, n):
|
||||
wc, wh, wl = close[i-window:i], high[i-window:i], 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 = (ma+1.5*atr)-(ma-1.5*atr)
|
||||
bb = (ma+2*bb_std)-(ma-2*bb_std)
|
||||
if kc > 0: r[i] = bb/kc
|
||||
return r
|
||||
|
||||
|
||||
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 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 run_ultimate(primary, tf="15m"):
|
||||
secondary = "ETH" if primary == "BTC" else "BTC"
|
||||
|
||||
df = load_data(primary, tf)
|
||||
c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values
|
||||
n = len(df)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
|
||||
df2 = load_data(secondary, tf)
|
||||
c2, ts2 = df2["close"].values, df2["timestamp"].values
|
||||
|
||||
kcr = keltner_ratio(c, h, l, 14)
|
||||
kcr2 = keltner_ratio(c2, df2["high"].values, df2["low"].values, 14)
|
||||
|
||||
ema_50 = ema(c, 50)
|
||||
rv_48 = rv_ann(c, 48)
|
||||
|
||||
# Rolling correlation
|
||||
ret1 = np.diff(np.log(np.where(c == 0, 1e-10, c)))
|
||||
ret2 = np.diff(np.log(np.where(c2[:len(c)] == 0, 1e-10, c2[:len(c)])))
|
||||
min_len = min(len(ret1), len(ret2))
|
||||
ret1 = ret1[:min_len]
|
||||
ret2 = ret2[:min_len]
|
||||
corr = np.full(n, np.nan)
|
||||
for i in range(48, min_len):
|
||||
cv = np.corrcoef(ret1[i-48:i], ret2[i-48:i])[0,1]
|
||||
corr[i+1] = cv if np.isfinite(cv) else 0
|
||||
|
||||
# Detect squeezes
|
||||
events = []
|
||||
in_sq = False
|
||||
sq_start = 0
|
||||
for i in range(15, n):
|
||||
if np.isnan(kcr[i]): continue
|
||||
is_sq = kcr[i] < 0.8
|
||||
if is_sq and not in_sq:
|
||||
in_sq = True
|
||||
sq_start = i
|
||||
elif not is_sq and in_sq:
|
||||
in_sq = False
|
||||
dur = i - sq_start
|
||||
if dur < 5 or i + 6 >= n:
|
||||
continue
|
||||
events.append({"idx": i, "dur": dur, "sq_start": sq_start})
|
||||
|
||||
print(f"\n{'#'*70}")
|
||||
print(f" {primary} {tf} — ULTIMATE SQUEEZE ({len(events)} squeeze events)")
|
||||
print(f"{'#'*70}")
|
||||
|
||||
filters_map = {
|
||||
"antifake": lambda ev, i: not _antifake(c, h, l, i),
|
||||
"long_sq": lambda ev, i: ev["dur"] >= 10,
|
||||
"timing": lambda ev, i: 4 <= ts.iloc[i].hour <= 16,
|
||||
"cross": lambda ev, i: _cross_squeeze(kcr2, i, ts, ts2),
|
||||
"corr_high": lambda ev, i: not np.isnan(corr[i]) and abs(corr[i]) >= 0.6,
|
||||
"vol_confirm": lambda ev, i: _vol_confirm(v, i, ev["sq_start"]),
|
||||
"trend_align": lambda ev, i: _trend_align(c, ema_50, i),
|
||||
"low_rv": lambda ev, i: not np.isnan(rv_48[i]) and rv_48[i] < 1.5,
|
||||
}
|
||||
|
||||
def _antifake(c, h, l, i):
|
||||
if i + 1 >= len(c): return False
|
||||
br = h[i] - l[i]
|
||||
if br <= 0: return False
|
||||
if c[i] > c[i-1]:
|
||||
return (h[i] - c[i]) / br > 0.6
|
||||
return (c[i] - l[i]) / br > 0.6
|
||||
|
||||
def _cross_squeeze(kcr2, i, ts1, ts2_arr):
|
||||
i2 = np.searchsorted(ts2_arr, ts.values[i].astype("int64") // 10**6)
|
||||
i2 = min(i2, len(kcr2)-1)
|
||||
return any(not np.isnan(kcr2[j]) and kcr2[j] < 0.85 for j in range(max(0,i2-10), i2+1))
|
||||
|
||||
def _vol_confirm(v, i, sq_start):
|
||||
avg = np.mean(v[sq_start:i])
|
||||
return avg > 0 and v[i] > avg * 1.3
|
||||
|
||||
def _trend_align(c, ema_val, i):
|
||||
if np.isnan(ema_val[i]): return True
|
||||
first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0
|
||||
if first_ret > 0:
|
||||
return c[i] > ema_val[i]
|
||||
return c[i] < ema_val[i]
|
||||
|
||||
# Test combinazioni incrementali
|
||||
combos = [
|
||||
("BASE", []),
|
||||
("antifake", ["antifake"]),
|
||||
("long_sq", ["long_sq"]),
|
||||
("antifake+long", ["antifake", "long_sq"]),
|
||||
("antifake+timing", ["antifake", "timing"]),
|
||||
("antifake+cross", ["antifake", "cross"]),
|
||||
("antifake+corr", ["antifake", "corr_high"]),
|
||||
("antifake+vol", ["antifake", "vol_confirm"]),
|
||||
("antifake+trend", ["antifake", "trend_align"]),
|
||||
("af+long+timing", ["antifake", "long_sq", "timing"]),
|
||||
("af+long+cross", ["antifake", "long_sq", "cross"]),
|
||||
("af+long+corr", ["antifake", "long_sq", "corr_high"]),
|
||||
("af+long+trend", ["antifake", "long_sq", "trend_align"]),
|
||||
("af+long+cross+time", ["antifake", "long_sq", "cross", "timing"]),
|
||||
("af+long+corr+time", ["antifake", "long_sq", "corr_high", "timing"]),
|
||||
("af+long+corr+trend", ["antifake", "long_sq", "corr_high", "trend_align"]),
|
||||
("ALL_NO_VOL", ["antifake", "long_sq", "cross", "timing", "corr_high", "trend_align", "low_rv"]),
|
||||
("ALL", ["antifake", "long_sq", "cross", "timing", "corr_high", "vol_confirm", "trend_align", "low_rv"]),
|
||||
("BEST_5", ["antifake", "long_sq", "corr_high", "trend_align", "low_rv"]),
|
||||
]
|
||||
|
||||
results = []
|
||||
for combo_name, filter_names in combos:
|
||||
yearly = {}
|
||||
capital = float(INITIAL)
|
||||
peak = capital
|
||||
max_dd = 0
|
||||
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
|
||||
skip = False
|
||||
for fn in filter_names:
|
||||
if fn in filters_map and not filters_map[fn](ev, i):
|
||||
skip = True
|
||||
break
|
||||
if skip:
|
||||
continue
|
||||
|
||||
direction = 1 if first_ret > 0 else -1
|
||||
entry = c[i-1]
|
||||
exit_price = c[min(i+2, n-1)]
|
||||
actual = (exit_price - entry) / entry * direction
|
||||
net = actual * LEVERAGE - FEE_RT * LEVERAGE
|
||||
|
||||
capital += capital * 0.15 * net
|
||||
capital = max(capital, 10)
|
||||
if capital > peak: peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnls": []}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0: yearly[year]["w"] += 1
|
||||
yearly[year]["pnls"].append(net * INITIAL)
|
||||
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t < 20: continue
|
||||
|
||||
acc = all_w / all_t * 100
|
||||
pnl = sum(p for d in yearly.values() for p in d["pnls"])
|
||||
worst = min(yearly.items(), key=lambda x: x[1]["w"]/x[1]["t"] if x[1]["t"]>0 else 0)
|
||||
wa = worst[1]["w"]/worst[1]["t"]*100 if worst[1]["t"]>0 else 0
|
||||
|
||||
results.append({
|
||||
"name": combo_name, "trades": all_t, "acc": acc, "pnl": pnl,
|
||||
"dd": max_dd*100, "capital": capital, "worst": f"{worst[0]}({wa:.0f}%)",
|
||||
"yearly": yearly,
|
||||
})
|
||||
|
||||
results.sort(key=lambda x: x["acc"], reverse=True)
|
||||
|
||||
print(f"\n {'Name':.<28s} {'Trades':>6s} {'Acc':>6s} {'PnL€':>9s} {'DD%':>5s} {'Worst':>12s}")
|
||||
print(f" {'-'*70}")
|
||||
for r in results[:20]:
|
||||
tag = "✅✅" if r["acc"] >= 80 else "✅" if r["acc"] >= 78 else ""
|
||||
print(f" {r['name']:.<28s} {r['trades']:>6d} {r['acc']:>5.1f}% €{r['pnl']:>+8.0f} {r['dd']:>4.1f}% {r['worst']:>12s} {tag}")
|
||||
|
||||
# Dettaglio migliore
|
||||
if results:
|
||||
best = results[0]
|
||||
print(f"\n MIGLIORE: {best['name']} → {best['acc']:.1f}% acc, DD {best['dd']:.1f}%")
|
||||
for y in sorted(best["yearly"]):
|
||||
d = best["yearly"][y]
|
||||
ya = d["w"]/d["t"]*100 if d["t"]>0 else 0
|
||||
tag = " ← CRASH" if y in [2020,2021,2022] else ""
|
||||
print(f" {y}: {d['t']:4d}t {ya:5.1f}% €{sum(d['pnls']):+.0f}{tag}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
all_r = []
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for tf in ["15m", "1h"]:
|
||||
r = run_ultimate(asset, tf)
|
||||
for x in r:
|
||||
all_r.append({**x, "key": f"{asset}_{tf}"})
|
||||
|
||||
all_r.sort(key=lambda x: x["acc"], reverse=True)
|
||||
print(f"\n\n{'='*70}")
|
||||
print(f" TOP 10 GLOBALE")
|
||||
print(f"{'='*70}")
|
||||
for r in all_r[:10]:
|
||||
tag = "✅✅" if r["acc"] >= 80 else "✅" if r["acc"] >= 78 else ""
|
||||
print(f" {r['key']:.<10s} {r['name']:.<28s} {r['trades']:>5d} {r['acc']:>5.1f}% €{r['pnl']:>+8.0f} DD {r['dd']:.1f}% {r['worst']:>12s} {tag}")
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Analisi baseline: distribuzione pattern frattali e prima strategia naive."""
|
||||
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
|
||||
from src.fractal.patterns import encode_candles, find_patterns, pattern_frequency
|
||||
from src.backtest.engine import run_backtest, BacktestResult
|
||||
|
||||
print("=" * 60)
|
||||
print(" ANALISI BASELINE — BTC 1H")
|
||||
print("=" * 60)
|
||||
|
||||
df = load_data("BTC", "1h")
|
||||
print(f"\nDati: {len(df)} candele [{df['datetime'].iloc[0]} → {df['datetime'].iloc[-1]}]")
|
||||
|
||||
# 1. Distribuzione pattern
|
||||
print("\n--- DISTRIBUZIONE PATTERN (3-6 candele) ---")
|
||||
candle_types = encode_candles(df)
|
||||
unique, counts = np.unique(candle_types, return_counts=True)
|
||||
type_map = {-1: "DOWN", 0: "DOJI", 1: "UP"}
|
||||
for t, c in zip(unique, counts):
|
||||
print(f" {type_map[t]}: {c} ({c/len(df)*100:.1f}%)")
|
||||
|
||||
patterns = find_patterns(df, min_len=3, max_len=6)
|
||||
freq = pattern_frequency(patterns)
|
||||
print(f"\nPattern unici: {len(freq)}")
|
||||
print(f"\nTop 20 pattern più frequenti:")
|
||||
print(freq.head(20).to_string(index=False))
|
||||
|
||||
# 2. Analisi predittiva: dopo ogni pattern, il prezzo sale o scende?
|
||||
print("\n\n--- ANALISI PREDITTIVA PER PATTERN ---")
|
||||
print("Per ogni pattern: % volte che il prezzo sale nelle N candele successive")
|
||||
|
||||
LOOKAHEAD = [1, 3, 6, 12, 24]
|
||||
top_patterns = freq.head(30)["pattern"].tolist()
|
||||
|
||||
results = []
|
||||
for code in top_patterns:
|
||||
matching = [p for p in patterns if p.code == code]
|
||||
if len(matching) < 50:
|
||||
continue
|
||||
|
||||
row = {"pattern": code, "count": len(matching)}
|
||||
for ahead in LOOKAHEAD:
|
||||
ups = 0
|
||||
valid = 0
|
||||
for p in matching:
|
||||
future_idx = p.end_idx + ahead
|
||||
if future_idx >= len(df):
|
||||
continue
|
||||
valid += 1
|
||||
if df["close"].iloc[future_idx] > df["close"].iloc[p.end_idx - 1]:
|
||||
ups += 1
|
||||
if valid > 0:
|
||||
row[f"up_{ahead}h"] = round(ups / valid * 100, 1)
|
||||
else:
|
||||
row[f"up_{ahead}h"] = None
|
||||
results.append(row)
|
||||
|
||||
pred_df = pd.DataFrame(results)
|
||||
print(pred_df.to_string(index=False))
|
||||
|
||||
# 3. Strategia naive: compra quando il pattern più bullish si presenta
|
||||
print("\n\n--- STRATEGIA 1: PATTERN BULLISH NAIVE ---")
|
||||
# Trova pattern con up_24h > 55%
|
||||
bullish_patterns = pred_df[pred_df["up_24h"] > 55]["pattern"].tolist()
|
||||
bearish_patterns = pred_df[pred_df["up_24h"] < 45]["pattern"].tolist()
|
||||
print(f"Pattern bullish (>55% up in 24h): {len(bullish_patterns)}")
|
||||
print(f"Pattern bearish (<45% up in 24h): {len(bearish_patterns)}")
|
||||
|
||||
# Genera segnali
|
||||
signals = pd.Series(0, index=df.index)
|
||||
all_patterns = find_patterns(df, min_len=3, max_len=6)
|
||||
for p in all_patterns:
|
||||
if p.code in bullish_patterns:
|
||||
signals.iloc[p.end_idx - 1] = 1
|
||||
elif p.code in bearish_patterns:
|
||||
if signals.iloc[p.end_idx - 1] == 0:
|
||||
signals.iloc[p.end_idx - 1] = -1
|
||||
|
||||
# Train/test split: 70/30
|
||||
split_idx = int(len(df) * 0.7)
|
||||
train_df = df.iloc[:split_idx].reset_index(drop=True)
|
||||
test_df = df.iloc[split_idx:].reset_index(drop=True)
|
||||
train_signals = signals.iloc[:split_idx].reset_index(drop=True)
|
||||
test_signals = signals.iloc[split_idx:].reset_index(drop=True)
|
||||
|
||||
train_result = run_backtest(train_df, train_signals, initial_capital=1000, fee_pct=0.001)
|
||||
test_result = run_backtest(test_df, test_signals, initial_capital=1000, fee_pct=0.001)
|
||||
|
||||
print("\nRISULTATI TRAIN (70%):")
|
||||
for k, v in train_result.summary().items():
|
||||
print(f" {k}: {v}")
|
||||
|
||||
print("\nRISULTATI TEST (30%):")
|
||||
for k, v in test_result.summary().items():
|
||||
print(f" {k}: {v}")
|
||||
|
||||
# 4. Buy & Hold come benchmark
|
||||
print("\n\n--- BENCHMARK: BUY & HOLD ---")
|
||||
bh_signals = pd.Series(0, index=test_df.index)
|
||||
bh_signals.iloc[0] = 1 # Compra al primo candle
|
||||
bh_result = run_backtest(test_df, bh_signals, initial_capital=1000, fee_pct=0.001, max_hold_candles=len(test_df))
|
||||
print("Buy & Hold (test period):")
|
||||
for k, v in bh_result.summary().items():
|
||||
print(f" {k}: {v}")
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Strategia 2: DTW pattern matching.
|
||||
Idea: per ogni finestra di N candele, cerca le K finestre più simili nel passato
|
||||
via DTW sui prezzi normalizzati. Se la maggioranza delle match passate è salita
|
||||
dopo, vai long. Se è scesa, vai short.
|
||||
"""
|
||||
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
|
||||
from src.fractal.similarity import dtw_distance
|
||||
from src.fractal.patterns import normalize_pattern_window
|
||||
from src.backtest.engine import run_backtest
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 2: DTW PATTERN MATCHING — BTC 1H")
|
||||
print("=" * 60)
|
||||
|
||||
df = load_data("BTC", "1h")
|
||||
close = df["close"].values
|
||||
|
||||
WINDOW = 12
|
||||
LOOKAHEAD = 6
|
||||
K_NEIGHBORS = 20
|
||||
LOOKBACK = 2000
|
||||
THRESHOLD = 0.65
|
||||
|
||||
split_idx = int(len(df) * 0.7)
|
||||
|
||||
def normalize_window(arr: np.ndarray) -> np.ndarray:
|
||||
mn, mx = arr.min(), arr.max()
|
||||
if mx - mn == 0:
|
||||
return np.zeros_like(arr)
|
||||
return (arr - mn) / (mx - mn)
|
||||
|
||||
def compute_returns(close_arr: np.ndarray, idx: int, ahead: int) -> float:
|
||||
if idx + ahead >= len(close_arr):
|
||||
return 0.0
|
||||
return (close_arr[idx + ahead] - close_arr[idx]) / close_arr[idx]
|
||||
|
||||
print(f"\nParametri: window={WINDOW}, lookahead={LOOKAHEAD}, K={K_NEIGHBORS}")
|
||||
print(f"Lookback: {LOOKBACK} candele, threshold: {THRESHOLD}")
|
||||
print(f"Train: 0→{split_idx}, Test: {split_idx}→{len(df)}")
|
||||
|
||||
signals = pd.Series(0, index=df.index)
|
||||
accuracies = []
|
||||
|
||||
step = 6
|
||||
test_range = range(split_idx, len(df) - LOOKAHEAD, step)
|
||||
total_steps = len(list(test_range))
|
||||
print(f"\nValutazione: {total_steps} punti (step={step})...")
|
||||
|
||||
for count, i in enumerate(test_range):
|
||||
if count % 500 == 0:
|
||||
print(f" Progresso: {count}/{total_steps} ({count/total_steps*100:.0f}%)")
|
||||
|
||||
current = normalize_window(close[i - WINDOW : i])
|
||||
|
||||
search_start = max(WINDOW, i - LOOKBACK)
|
||||
search_end = i - LOOKAHEAD
|
||||
|
||||
if search_end - search_start < K_NEIGHBORS:
|
||||
continue
|
||||
|
||||
distances = []
|
||||
for j in range(search_start, search_end):
|
||||
candidate = normalize_window(close[j - WINDOW : j])
|
||||
if len(candidate) != len(current):
|
||||
continue
|
||||
d = dtw_distance(current, candidate)
|
||||
future_ret = compute_returns(close, j, LOOKAHEAD)
|
||||
distances.append((d, future_ret))
|
||||
|
||||
if len(distances) < K_NEIGHBORS:
|
||||
continue
|
||||
|
||||
distances.sort(key=lambda x: x[0])
|
||||
top_k = distances[:K_NEIGHBORS]
|
||||
up_count = sum(1 for _, ret in top_k if ret > 0)
|
||||
up_ratio = up_count / K_NEIGHBORS
|
||||
|
||||
if up_ratio >= THRESHOLD:
|
||||
signals.iloc[i] = 1
|
||||
elif up_ratio <= (1 - THRESHOLD):
|
||||
signals.iloc[i] = -1
|
||||
|
||||
actual_ret = compute_returns(close, i, LOOKAHEAD)
|
||||
predicted_up = up_ratio >= THRESHOLD
|
||||
predicted_down = up_ratio <= (1 - THRESHOLD)
|
||||
if predicted_up:
|
||||
accuracies.append(1 if actual_ret > 0 else 0)
|
||||
elif predicted_down:
|
||||
accuracies.append(1 if actual_ret < 0 else 0)
|
||||
|
||||
print(f"\nSegnali generati: {(signals != 0).sum()}")
|
||||
print(f" Long: {(signals == 1).sum()}, Short: {(signals == -1).sum()}")
|
||||
|
||||
if accuracies:
|
||||
print(f"Accuratezza direzione: {np.mean(accuracies)*100:.1f}% su {len(accuracies)} segnali")
|
||||
|
||||
test_df = df.iloc[split_idx:].reset_index(drop=True)
|
||||
test_signals = signals.iloc[split_idx:].reset_index(drop=True)
|
||||
|
||||
result = run_backtest(test_df, test_signals, initial_capital=1000, fee_pct=0.001, max_hold_candles=LOOKAHEAD)
|
||||
|
||||
print("\nRISULTATI TEST:")
|
||||
for k, v in result.summary().items():
|
||||
print(f" {k}: {v}")
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Strategia 3: Fourier decomposition e proiezione.
|
||||
Ispirata al paper Pythagoras Trading Prediction.
|
||||
Idea: scomponi il prezzo in componenti sinusoidali via FFT,
|
||||
ricostruisci con le N componenti più forti, proietta nel futuro.
|
||||
"""
|
||||
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
|
||||
from src.backtest.engine import run_backtest
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 3: FOURIER PROJECTION — BTC 1H")
|
||||
print("=" * 60)
|
||||
|
||||
df = load_data("BTC", "1h")
|
||||
close = df["close"].values
|
||||
n_total = len(close)
|
||||
|
||||
WINDOW = 588 # dal paper: 588 candele per l'indicatore H-C
|
||||
N_COMPONENTS = 25 # dal paper: 25 linee verticali
|
||||
LOOKAHEAD = 6
|
||||
STEP = 6
|
||||
|
||||
split_idx = int(n_total * 0.7)
|
||||
|
||||
def fourier_project(series: np.ndarray, n_components: int, ahead: int) -> np.ndarray:
|
||||
"""Ricostruisci serie con top-N componenti Fourier e proietta avanti."""
|
||||
n = len(series)
|
||||
detrended = series - np.linspace(series[0], series[-1], n)
|
||||
fft_vals = np.fft.fft(detrended)
|
||||
freqs = np.fft.fftfreq(n)
|
||||
|
||||
magnitudes = np.abs(fft_vals)
|
||||
magnitudes[0] = 0
|
||||
top_indices = np.argsort(magnitudes)[-n_components * 2:]
|
||||
|
||||
fft_filtered = np.zeros_like(fft_vals)
|
||||
fft_filtered[top_indices] = fft_vals[top_indices]
|
||||
|
||||
t_extended = np.arange(n + ahead)
|
||||
reconstruction = np.zeros(n + ahead)
|
||||
for idx in top_indices:
|
||||
amp = np.abs(fft_vals[idx]) / n
|
||||
phase = np.angle(fft_vals[idx])
|
||||
freq = freqs[idx]
|
||||
reconstruction += amp * np.cos(2 * np.pi * freq * t_extended / 1 + phase)
|
||||
|
||||
trend_slope = (series[-1] - series[0]) / n
|
||||
trend_extended = series[0] + trend_slope * t_extended
|
||||
reconstruction += trend_extended
|
||||
|
||||
return reconstruction
|
||||
|
||||
|
||||
print(f"\nParametri: window={WINDOW}, components={N_COMPONENTS}, lookahead={LOOKAHEAD}")
|
||||
print(f"Train: 0→{split_idx}, Test: {split_idx}→{n_total}")
|
||||
|
||||
signals = pd.Series(0, index=df.index)
|
||||
accuracies = []
|
||||
|
||||
test_range = range(max(split_idx, WINDOW), n_total - LOOKAHEAD, STEP)
|
||||
total_steps = len(list(test_range))
|
||||
print(f"Valutazione: {total_steps} punti (step={STEP})...")
|
||||
|
||||
for count, i in enumerate(test_range):
|
||||
if count % 500 == 0:
|
||||
print(f" Progresso: {count}/{total_steps} ({count/total_steps*100:.0f}%)")
|
||||
|
||||
window_data = close[i - WINDOW : i]
|
||||
projected = fourier_project(window_data, N_COMPONENTS, LOOKAHEAD)
|
||||
|
||||
current_price = close[i - 1]
|
||||
projected_price = projected[-1]
|
||||
change_pct = (projected_price - current_price) / current_price
|
||||
|
||||
if change_pct > 0.005:
|
||||
signals.iloc[i] = 1
|
||||
elif change_pct < -0.005:
|
||||
signals.iloc[i] = -1
|
||||
|
||||
actual_ret = (close[i + LOOKAHEAD - 1] - current_price) / current_price
|
||||
if signals.iloc[i] == 1:
|
||||
accuracies.append(1 if actual_ret > 0 else 0)
|
||||
elif signals.iloc[i] == -1:
|
||||
accuracies.append(1 if actual_ret < 0 else 0)
|
||||
|
||||
print(f"\nSegnali generati: {(signals != 0).sum()}")
|
||||
print(f" Long: {(signals == 1).sum()}, Short: {(signals == -1).sum()}")
|
||||
|
||||
if accuracies:
|
||||
print(f"Accuratezza direzione: {np.mean(accuracies)*100:.1f}% su {len(accuracies)} segnali")
|
||||
|
||||
test_df = df.iloc[split_idx:].reset_index(drop=True)
|
||||
test_signals = signals.iloc[split_idx:].reset_index(drop=True)
|
||||
|
||||
result = run_backtest(test_df, test_signals, initial_capital=1000, fee_pct=0.001, max_hold_candles=LOOKAHEAD)
|
||||
|
||||
print("\nRISULTATI TEST:")
|
||||
for k, v in result.summary().items():
|
||||
print(f" {k}: {v}")
|
||||
|
||||
# Varianti con parametri diversi
|
||||
print("\n\n--- VARIANTI PARAMETRI ---")
|
||||
for n_comp in [5, 10, 15, 25, 50]:
|
||||
for window in [144, 288, 588]:
|
||||
sigs = pd.Series(0, index=df.index)
|
||||
accs = []
|
||||
test_r = range(max(split_idx, window), n_total - LOOKAHEAD, STEP)
|
||||
for i in test_r:
|
||||
w = close[i - window : i]
|
||||
proj = fourier_project(w, n_comp, LOOKAHEAD)
|
||||
cp = close[i - 1]
|
||||
pp = proj[-1]
|
||||
ch = (pp - cp) / cp
|
||||
if ch > 0.005:
|
||||
sigs.iloc[i] = 1
|
||||
elif ch < -0.005:
|
||||
sigs.iloc[i] = -1
|
||||
ar = (close[i + LOOKAHEAD - 1] - cp) / cp
|
||||
if sigs.iloc[i] == 1:
|
||||
accs.append(1 if ar > 0 else 0)
|
||||
elif sigs.iloc[i] == -1:
|
||||
accs.append(1 if ar < 0 else 0)
|
||||
|
||||
if not accs:
|
||||
continue
|
||||
t_sigs = sigs.iloc[split_idx:].reset_index(drop=True)
|
||||
res = run_backtest(test_df, t_sigs, initial_capital=1000, fee_pct=0.001, max_hold_candles=LOOKAHEAD)
|
||||
acc = np.mean(accs) * 100
|
||||
print(f" W={window:3d} N={n_comp:2d} → acc={acc:.1f}% trades={res.total_trades} ret={res.total_return*100:+.1f}% sharpe={res.sharpe_ratio:.2f}")
|
||||
@@ -0,0 +1,231 @@
|
||||
"""Strategia 4: Regime-aware fractal ML.
|
||||
Combina:
|
||||
1. Hurst exponent per regime detection (trend vs mean-revert vs random)
|
||||
2. Feature engineering da indicatori frattali
|
||||
3. RandomForest per predizione direzione
|
||||
4. Trade filtering aggressivo (solo alta confidenza)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
|
||||
from sklearn.metrics import accuracy_score, classification_report
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.indicators import (
|
||||
hurst_exponent,
|
||||
fractal_dimension_higuchi,
|
||||
self_similarity_score,
|
||||
volatility_ratio,
|
||||
)
|
||||
from src.fractal.patterns import encode_candles, extract_body_ratios, extract_shadow_ratios
|
||||
from src.backtest.engine import run_backtest
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 4: REGIME-AWARE FRACTAL ML — BTC 1H")
|
||||
print("=" * 60)
|
||||
|
||||
df = load_data("BTC", "1h")
|
||||
close = df["close"].values
|
||||
n = len(close)
|
||||
|
||||
LOOKBACK = 48
|
||||
LOOKAHEAD = 6
|
||||
MIN_CONFIDENCE = 0.60
|
||||
|
||||
print(f"\nDati: {n} candele")
|
||||
print(f"Lookback: {LOOKBACK}, Lookahead: {LOOKAHEAD}")
|
||||
|
||||
# --- Feature engineering ---
|
||||
print("\nCalcolo features...")
|
||||
|
||||
features_list = []
|
||||
labels = []
|
||||
indices = []
|
||||
|
||||
returns = np.diff(np.log(np.where(close == 0, 1e-10, close)))
|
||||
candle_types = encode_candles(df)
|
||||
body_ratios = extract_body_ratios(df)
|
||||
shadow_ratios = extract_shadow_ratios(df)
|
||||
|
||||
for i in range(LOOKBACK, n - LOOKAHEAD, 3):
|
||||
if i % 5000 == 0:
|
||||
print(f" Feature extraction: {i}/{n}")
|
||||
|
||||
window = close[i - LOOKBACK : i]
|
||||
ret_window = returns[i - LOOKBACK : i - 1]
|
||||
|
||||
if len(ret_window) < 10:
|
||||
continue
|
||||
|
||||
h = hurst_exponent(ret_window, max_lag=min(len(ret_window) // 4, 20))
|
||||
fd = fractal_dimension_higuchi(ret_window, k_max=min(8, len(ret_window) // 4))
|
||||
|
||||
larger_window = close[max(0, i - LOOKBACK * 6) : i]
|
||||
ss = self_similarity_score(larger_window, min(LOOKBACK, len(larger_window)))
|
||||
vr = volatility_ratio(window, fast=12, slow=LOOKBACK)
|
||||
|
||||
# Candle pattern features
|
||||
ct = candle_types[i - 6 : i]
|
||||
br = body_ratios[i - 6 : i]
|
||||
sr = shadow_ratios[i - 6 : i]
|
||||
|
||||
recent_returns = ret_window[-12:]
|
||||
momentum_short = np.sum(recent_returns[-3:])
|
||||
momentum_mid = np.sum(recent_returns[-6:])
|
||||
momentum_long = np.sum(recent_returns)
|
||||
|
||||
vol_short = np.std(recent_returns[-6:]) if len(recent_returns) >= 6 else 0
|
||||
vol_long = np.std(ret_window) if len(ret_window) > 0 else 0
|
||||
|
||||
volume_window = df["volume"].values[i - 12 : i]
|
||||
vol_avg = np.mean(volume_window) if len(volume_window) > 0 else 0
|
||||
vol_last = df["volume"].values[i - 1] if i > 0 else 0
|
||||
vol_ratio = vol_last / vol_avg if vol_avg > 0 else 1.0
|
||||
|
||||
up_count_6 = np.sum(ct[-6:] == 1) / 6
|
||||
down_count_6 = np.sum(ct[-6:] == -1) / 6
|
||||
|
||||
features = [
|
||||
h, # Hurst exponent
|
||||
fd, # Fractal dimension
|
||||
ss, # Self-similarity
|
||||
vr, # Volatility ratio
|
||||
momentum_short, # 3-candle momentum
|
||||
momentum_mid, # 6-candle momentum
|
||||
momentum_long, # Full window momentum
|
||||
vol_short, # Short-term volatility
|
||||
vol_long, # Long-term volatility
|
||||
vol_ratio, # Volume spike ratio
|
||||
up_count_6, # Bullish ratio (last 6)
|
||||
down_count_6, # Bearish ratio (last 6)
|
||||
np.mean(br[-6:]), # Avg body ratio
|
||||
np.mean(sr[-6:]), # Avg shadow ratio
|
||||
np.mean(br[-3:]), # Avg body ratio (last 3)
|
||||
np.std(br[-6:]), # Body ratio std
|
||||
close[i - 1] / np.mean(window), # Price vs MA
|
||||
]
|
||||
|
||||
# Label: 1 if price goes up in next LOOKAHEAD candles, 0 otherwise
|
||||
future_ret = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
label = 1 if future_ret > 0.002 else (0 if future_ret > -0.002 else -1)
|
||||
|
||||
features_list.append(features)
|
||||
labels.append(label)
|
||||
indices.append(i)
|
||||
|
||||
X = np.array(features_list)
|
||||
y = np.array(labels)
|
||||
idx_arr = np.array(indices)
|
||||
|
||||
print(f"\nDataset: {len(X)} samples")
|
||||
print(f"Label distribution: up={np.sum(y==1)}, flat={np.sum(y==0)}, down={np.sum(y==-1)}")
|
||||
|
||||
# Train/test split cronologico
|
||||
split_point = int(len(X) * 0.7)
|
||||
X_train, X_test = X[:split_point], X[split_point:]
|
||||
y_train, y_test = y[:split_point], y[split_point:]
|
||||
idx_train, idx_test = idx_arr[:split_point], idx_arr[split_point:]
|
||||
|
||||
# Handle NaN/Inf
|
||||
X_train = np.nan_to_num(X_train, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
X_test = np.nan_to_num(X_test, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
# --- Modelli ---
|
||||
print("\n--- TRAINING ---")
|
||||
|
||||
models = {
|
||||
"RandomForest": RandomForestClassifier(
|
||||
n_estimators=200, max_depth=8, min_samples_leaf=20,
|
||||
class_weight="balanced", random_state=42, n_jobs=-1,
|
||||
),
|
||||
"GradientBoosting": GradientBoostingClassifier(
|
||||
n_estimators=200, max_depth=5, min_samples_leaf=20,
|
||||
learning_rate=0.05, random_state=42,
|
||||
),
|
||||
}
|
||||
|
||||
for name, model in models.items():
|
||||
print(f"\n{'='*40}")
|
||||
print(f" {name}")
|
||||
print(f"{'='*40}")
|
||||
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
# Feature importance
|
||||
if hasattr(model, "feature_importances_"):
|
||||
feat_names = [
|
||||
"hurst", "fractal_dim", "self_sim", "vol_ratio",
|
||||
"mom_3", "mom_6", "mom_full", "vol_short", "vol_long",
|
||||
"vol_spike", "up_ratio", "down_ratio", "body_avg",
|
||||
"shadow_avg", "body_3", "body_std", "price_vs_ma"
|
||||
]
|
||||
imp = model.feature_importances_
|
||||
sorted_idx = np.argsort(imp)[::-1]
|
||||
print("\nFeature importance (top 10):")
|
||||
for j in sorted_idx[:10]:
|
||||
print(f" {feat_names[j]:15s}: {imp[j]:.4f}")
|
||||
|
||||
# Prediction con probabilità
|
||||
y_pred = model.predict(X_test)
|
||||
proba = model.predict_proba(X_test)
|
||||
|
||||
print(f"\nAccuracy: {accuracy_score(y_test, y_pred)*100:.1f}%")
|
||||
print(classification_report(y_test, y_pred, target_names=["down", "flat", "up"], zero_division=0))
|
||||
|
||||
# Genera segnali filtrati per confidenza
|
||||
signals = pd.Series(0, index=df.index)
|
||||
accuracies_filtered = []
|
||||
classes = model.classes_
|
||||
|
||||
up_class_idx = list(classes).index(1) if 1 in classes else -1
|
||||
down_class_idx = list(classes).index(-1) if -1 in classes else -1
|
||||
|
||||
for k, i in enumerate(idx_test):
|
||||
p = proba[k]
|
||||
if up_class_idx >= 0 and p[up_class_idx] >= MIN_CONFIDENCE:
|
||||
signals.iloc[i] = 1
|
||||
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
accuracies_filtered.append(1 if actual > 0 else 0)
|
||||
elif down_class_idx >= 0 and p[down_class_idx] >= MIN_CONFIDENCE:
|
||||
signals.iloc[i] = -1
|
||||
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
accuracies_filtered.append(1 if actual < 0 else 0)
|
||||
|
||||
n_signals = (signals != 0).sum()
|
||||
print(f"\nSegnali filtrati (conf>={MIN_CONFIDENCE}): {n_signals}")
|
||||
if accuracies_filtered:
|
||||
print(f"Accuratezza filtrata: {np.mean(accuracies_filtered)*100:.1f}%")
|
||||
|
||||
# Backtest
|
||||
split_idx = int(len(df) * 0.7)
|
||||
test_df = df.iloc[split_idx:].reset_index(drop=True)
|
||||
test_signals = signals.iloc[split_idx:].reset_index(drop=True)
|
||||
|
||||
result = run_backtest(test_df, test_signals, initial_capital=1000, fee_pct=0.001, max_hold_candles=LOOKAHEAD)
|
||||
print(f"\nBACKTEST:")
|
||||
for kk, v in result.summary().items():
|
||||
print(f" {kk}: {v}")
|
||||
|
||||
# Prova con soglie diverse
|
||||
print(f"\n Varianti soglia:")
|
||||
for threshold in [0.55, 0.60, 0.65, 0.70, 0.75, 0.80]:
|
||||
sigs = pd.Series(0, index=df.index)
|
||||
accs = []
|
||||
for k, i in enumerate(idx_test):
|
||||
p = proba[k]
|
||||
if up_class_idx >= 0 and p[up_class_idx] >= threshold:
|
||||
sigs.iloc[i] = 1
|
||||
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
elif down_class_idx >= 0 and p[down_class_idx] >= threshold:
|
||||
sigs.iloc[i] = -1
|
||||
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
|
||||
t_sigs = sigs.iloc[split_idx:].reset_index(drop=True)
|
||||
res = run_backtest(test_df, t_sigs, initial_capital=1000, fee_pct=0.001, max_hold_candles=LOOKAHEAD)
|
||||
acc = np.mean(accs) * 100 if accs else 0
|
||||
print(f" thr={threshold:.2f}: signals={len(accs):4d} acc={acc:.1f}% ret={res.total_return*100:+.1f}% wr={res.win_rate*100:.0f}% sharpe={res.sharpe_ratio:.2f}")
|
||||
@@ -0,0 +1,202 @@
|
||||
"""Strategia 5: Enhanced fractal features + binary classification + position management.
|
||||
Miglioramenti rispetto a #4:
|
||||
- Binary classification (up vs down, ignora flat)
|
||||
- Feature engineering esteso: multi-window fractal indicators
|
||||
- Migliore filtraggio segnali
|
||||
- Position sizing basato su confidenza
|
||||
- Trailing stop
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
from sklearn.metrics import accuracy_score
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.indicators import (
|
||||
hurst_exponent,
|
||||
fractal_dimension_higuchi,
|
||||
self_similarity_score,
|
||||
volatility_ratio,
|
||||
)
|
||||
from src.fractal.patterns import encode_candles, extract_body_ratios, extract_shadow_ratios
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 5: ENHANCED FRACTAL — BTC + ETH 1H")
|
||||
print("=" * 60)
|
||||
|
||||
LOOKAHEADS = [3, 6, 12]
|
||||
MIN_RETURN = 0.003 # 0.3% threshold for "up" label
|
||||
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for LOOKAHEAD in LOOKAHEADS:
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} 1H — LOOKAHEAD={LOOKAHEAD}")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df = load_data(asset, "1h")
|
||||
close = df["close"].values
|
||||
volume = df["volume"].values
|
||||
n = len(close)
|
||||
log_close = np.log(np.where(close == 0, 1e-10, close))
|
||||
returns = np.diff(log_close)
|
||||
|
||||
candle_types = encode_candles(df)
|
||||
body_ratios = extract_body_ratios(df)
|
||||
shadow_ratios = extract_shadow_ratios(df)
|
||||
|
||||
WINDOWS = [24, 48, 96, 192]
|
||||
features_list = []
|
||||
labels = []
|
||||
indices = []
|
||||
|
||||
max_window = max(WINDOWS) + 50
|
||||
|
||||
for i in range(max_window, n - LOOKAHEAD, 2):
|
||||
feats = []
|
||||
|
||||
for w in WINDOWS:
|
||||
ret_w = returns[i - w : i - 1]
|
||||
close_w = close[i - w : i]
|
||||
|
||||
h = hurst_exponent(ret_w, max_lag=min(len(ret_w) // 4, 20))
|
||||
fd = fractal_dimension_higuchi(ret_w, k_max=min(6, len(ret_w) // 4))
|
||||
vr = volatility_ratio(close_w, fast=min(12, w // 4), slow=w)
|
||||
|
||||
mom = np.sum(ret_w)
|
||||
vol = np.std(ret_w)
|
||||
skew = float(pd.Series(ret_w).skew()) if len(ret_w) > 2 else 0
|
||||
kurt = float(pd.Series(ret_w).kurtosis()) if len(ret_w) > 3 else 0
|
||||
|
||||
ma = np.mean(close_w)
|
||||
price_vs_ma = close[i - 1] / ma if ma > 0 else 1
|
||||
|
||||
# Autocorrelation lag-1
|
||||
if len(ret_w) > 1 and np.std(ret_w) > 0:
|
||||
ac1 = np.corrcoef(ret_w[:-1], ret_w[1:])[0, 1]
|
||||
if not np.isfinite(ac1):
|
||||
ac1 = 0
|
||||
else:
|
||||
ac1 = 0
|
||||
|
||||
feats.extend([h, fd, vr, mom, vol, skew, kurt, price_vs_ma, ac1])
|
||||
|
||||
# Self-similarity multi-scale
|
||||
large_window = close[max(0, i - 192 * 4) : i]
|
||||
ss = self_similarity_score(large_window, 48)
|
||||
feats.append(ss)
|
||||
|
||||
# Candle pattern features (last 12 candles)
|
||||
ct = candle_types[i - 12 : i]
|
||||
br = body_ratios[i - 12 : i]
|
||||
sr = shadow_ratios[i - 12 : i]
|
||||
|
||||
feats.extend([
|
||||
np.mean(ct[-3:]),
|
||||
np.mean(ct[-6:]),
|
||||
np.mean(ct[-12:]),
|
||||
np.std(br[-6:]),
|
||||
np.mean(br[-3:]),
|
||||
np.mean(sr[-6:]),
|
||||
np.max(br[-6:]),
|
||||
np.min(br[-6:]),
|
||||
])
|
||||
|
||||
# Volume features
|
||||
vol_w = volume[i - 24 : i]
|
||||
if np.mean(vol_w) > 0:
|
||||
feats.append(volume[i - 1] / np.mean(vol_w))
|
||||
feats.append(np.std(vol_w) / np.mean(vol_w))
|
||||
else:
|
||||
feats.extend([1.0, 0.0])
|
||||
|
||||
# Range/ATR proxy
|
||||
h_arr = df["high"].values[i - 14 : i]
|
||||
l_arr = df["low"].values[i - 14 : i]
|
||||
c_arr = close[i - 14 : i]
|
||||
tr = np.maximum(h_arr - l_arr, np.maximum(np.abs(h_arr - np.roll(c_arr, 1)), np.abs(l_arr - np.roll(c_arr, 1))))
|
||||
atr = np.mean(tr[1:])
|
||||
feats.append(atr / close[i - 1] if close[i - 1] > 0 else 0)
|
||||
|
||||
# Label
|
||||
future_ret = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
if abs(future_ret) < MIN_RETURN:
|
||||
continue # skip flat zones
|
||||
|
||||
label = 1 if future_ret > 0 else 0
|
||||
|
||||
features_list.append(feats)
|
||||
labels.append(label)
|
||||
indices.append(i)
|
||||
|
||||
X = np.array(features_list)
|
||||
y = np.array(labels)
|
||||
idx_arr = np.array(indices)
|
||||
|
||||
X = np.nan_to_num(X, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
# Split
|
||||
split = int(len(X) * 0.7)
|
||||
X_train, X_test = X[:split], X[split:]
|
||||
y_train, y_test = y[:split], y[split:]
|
||||
idx_test = idx_arr[split:]
|
||||
|
||||
print(f"Samples: {len(X)} (train={split}, test={len(X)-split})")
|
||||
print(f"Label balance: up={np.mean(y)*100:.1f}%")
|
||||
|
||||
# Train
|
||||
model = GradientBoostingClassifier(
|
||||
n_estimators=300, max_depth=5, min_samples_leaf=30,
|
||||
learning_rate=0.03, subsample=0.8, random_state=42,
|
||||
)
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
y_pred = model.predict(X_test)
|
||||
proba = model.predict_proba(X_test)
|
||||
|
||||
base_acc = accuracy_score(y_test, y_pred)
|
||||
print(f"Base accuracy: {base_acc*100:.1f}%")
|
||||
|
||||
# Threshold sweep
|
||||
print(f"\n Threshold sweep:")
|
||||
for thr in [0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80]:
|
||||
up_idx = model.classes_.tolist().index(1)
|
||||
|
||||
sigs = []
|
||||
accs = []
|
||||
for k in range(len(X_test)):
|
||||
p_up = proba[k][up_idx]
|
||||
i = idx_test[k]
|
||||
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
if p_up >= thr:
|
||||
sigs.append(("long", i))
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
elif p_up <= (1 - thr):
|
||||
sigs.append(("short", i))
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
|
||||
if not accs:
|
||||
print(f" thr={thr:.2f}: no signals")
|
||||
continue
|
||||
|
||||
acc = np.mean(accs) * 100
|
||||
# Simple PnL estimate
|
||||
pnl = 0
|
||||
capital = 1000
|
||||
for direction, i in sigs:
|
||||
entry = close[i - 1]
|
||||
exit_ = close[i + LOOKAHEAD - 1]
|
||||
if direction == "long":
|
||||
ret = (exit_ - entry) / entry
|
||||
else:
|
||||
ret = (entry - exit_) / entry
|
||||
ret -= 0.002 # fees round-trip
|
||||
pnl += capital * ret * 0.5 # 50% per trade
|
||||
capital += capital * ret * 0.5
|
||||
|
||||
total_ret = (capital - 1000) / 1000 * 100
|
||||
trades_per_year = len(sigs) / ((n - max_window) / (24 * 365))
|
||||
print(f" thr={thr:.2f}: signals={len(sigs):5d} acc={acc:.1f}% ret={total_ret:+.1f}% trades/yr={trades_per_year:.0f}")
|
||||
@@ -0,0 +1,201 @@
|
||||
"""Strategia 6: Structural Pattern Matching con DTW veloce.
|
||||
Idea diversa: invece di ML generico, cerca nel passato le finestre OHLC
|
||||
più simili alla finestra corrente usando una versione veloce (reduced DTW).
|
||||
Vota sulla direzione basandosi sui K-nearest neighbors nel passato.
|
||||
Usa features normalizzate (non DTW puro sul prezzo che è lento).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.neighbors import KNeighborsClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.patterns import normalize_pattern_window
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 6: STRUCTURAL PATTERN KNN — BTC 1H")
|
||||
print("=" * 60)
|
||||
|
||||
df = load_data("BTC", "1h")
|
||||
close = df["close"].values
|
||||
n = len(close)
|
||||
|
||||
WINDOW = 24
|
||||
LOOKAHEAD = 6
|
||||
MIN_RETURN = 0.003
|
||||
|
||||
def extract_structural_features(df: pd.DataFrame, idx: int, window: int) -> np.ndarray | None:
|
||||
"""Extract normalized structural features from OHLC window."""
|
||||
if idx < window:
|
||||
return None
|
||||
|
||||
o = df["open"].values[idx - window : idx]
|
||||
h = df["high"].values[idx - window : idx]
|
||||
l = df["low"].values[idx - window : idx]
|
||||
c = df["close"].values[idx - window : idx]
|
||||
v = df["volume"].values[idx - window : idx]
|
||||
|
||||
# Normalize price to [0,1]
|
||||
all_prices = np.concatenate([o, h, l, c])
|
||||
mn, mx = all_prices.min(), all_prices.max()
|
||||
if mx - mn == 0:
|
||||
return None
|
||||
o_n = (o - mn) / (mx - mn)
|
||||
h_n = (h - mn) / (mx - mn)
|
||||
l_n = (l - mn) / (mx - mn)
|
||||
c_n = (c - mn) / (mx - mn)
|
||||
|
||||
# Body and shadow ratios (already normalized)
|
||||
total = h - l
|
||||
total = np.where(total == 0, 1e-10, total)
|
||||
body = np.abs(c - o) / total
|
||||
upper_shadow = (h - np.maximum(o, c)) / total
|
||||
lower_shadow = (np.minimum(o, c) - l) / total
|
||||
direction = np.sign(c - o)
|
||||
|
||||
# Returns
|
||||
log_c = np.log(np.where(c == 0, 1e-10, c))
|
||||
returns = np.diff(log_c)
|
||||
|
||||
# Volume profile (normalized)
|
||||
v_mean = np.mean(v)
|
||||
v_n = v / v_mean if v_mean > 0 else np.ones_like(v)
|
||||
|
||||
# Downsample to fixed-size feature vector
|
||||
# Take every N-th candle if window is large
|
||||
step = max(1, window // 12)
|
||||
sampled_idx = np.arange(0, window, step)[:12]
|
||||
|
||||
features = np.concatenate([
|
||||
c_n[sampled_idx], # 12: normalized close
|
||||
body[sampled_idx], # 12: body ratios
|
||||
direction[sampled_idx], # 12: direction
|
||||
upper_shadow[sampled_idx], # 12: upper shadow
|
||||
lower_shadow[sampled_idx], # 12: lower shadow
|
||||
v_n[sampled_idx], # 12: volume profile
|
||||
[np.mean(returns), np.std(returns), np.sum(returns)], # 3: return stats
|
||||
[np.mean(body), np.std(body)], # 2: body stats
|
||||
])
|
||||
return features
|
||||
|
||||
|
||||
print("Extracting features...")
|
||||
features_all = []
|
||||
labels_all = []
|
||||
indices_all = []
|
||||
|
||||
for i in range(WINDOW, n - LOOKAHEAD, 1):
|
||||
feats = extract_structural_features(df, i, WINDOW)
|
||||
if feats is None:
|
||||
continue
|
||||
|
||||
future_ret = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
if abs(future_ret) < MIN_RETURN:
|
||||
continue
|
||||
|
||||
features_all.append(feats)
|
||||
labels_all.append(1 if future_ret > 0 else 0)
|
||||
indices_all.append(i)
|
||||
|
||||
X = np.array(features_all)
|
||||
y = np.array(labels_all)
|
||||
idx_arr = np.array(indices_all)
|
||||
|
||||
X = np.nan_to_num(X, nan=0.0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
split = int(len(X) * 0.7)
|
||||
X_train, X_test = X[:split], X[split:]
|
||||
y_train, y_test = y[:split], y[split:]
|
||||
idx_test = idx_arr[split:]
|
||||
|
||||
print(f"Samples: {len(X)} (train={split}, test={len(X)-split})")
|
||||
print(f"Label balance: up={np.mean(y)*100:.1f}%")
|
||||
|
||||
scaler = StandardScaler()
|
||||
X_train_s = scaler.fit_transform(X_train)
|
||||
X_test_s = scaler.transform(X_test)
|
||||
|
||||
# Test diversi K
|
||||
print("\n--- KNN SWEEP ---")
|
||||
for K in [5, 10, 20, 50, 100, 200]:
|
||||
knn = KNeighborsClassifier(n_neighbors=K, weights="distance", n_jobs=-1)
|
||||
knn.fit(X_train_s, y_train)
|
||||
|
||||
proba = knn.predict_proba(X_test_s)
|
||||
up_idx = list(knn.classes_).index(1)
|
||||
|
||||
for thr in [0.55, 0.60, 0.65, 0.70]:
|
||||
sigs = []
|
||||
accs = []
|
||||
for j in range(len(X_test)):
|
||||
p_up = proba[j][up_idx]
|
||||
i = idx_test[j]
|
||||
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
if p_up >= thr:
|
||||
sigs.append(1)
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
elif p_up <= (1 - thr):
|
||||
sigs.append(-1)
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
|
||||
if not accs:
|
||||
continue
|
||||
|
||||
acc = np.mean(accs) * 100
|
||||
# PnL
|
||||
capital = 1000
|
||||
for direction, j in zip(sigs, range(len(accs))):
|
||||
i_idx = idx_test[[k for k in range(len(X_test)) if proba[k][up_idx] >= thr or proba[k][up_idx] <= (1 - thr)][j]]
|
||||
entry = close[i_idx - 1]
|
||||
exit_ = close[i_idx + LOOKAHEAD - 1]
|
||||
if direction == 1:
|
||||
ret = (exit_ - entry) / entry
|
||||
else:
|
||||
ret = (entry - exit_) / entry
|
||||
ret -= 0.002
|
||||
capital *= (1 + ret * 0.5)
|
||||
|
||||
total_ret = (capital - 1000) / 1000 * 100
|
||||
print(f" K={K:3d} thr={thr:.2f}: signals={len(accs):5d} acc={acc:.1f}% ret={total_ret:+.1f}%")
|
||||
|
||||
# Best combo: try with Gradient Boosting on same features
|
||||
print("\n\n--- GRADIENT BOOSTING SU STRUCTURAL FEATURES ---")
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
|
||||
gb = GradientBoostingClassifier(
|
||||
n_estimators=300, max_depth=5, min_samples_leaf=30,
|
||||
learning_rate=0.03, subsample=0.8, random_state=42,
|
||||
)
|
||||
gb.fit(X_train_s, y_train)
|
||||
proba_gb = gb.predict_proba(X_test_s)
|
||||
up_idx_gb = list(gb.classes_).index(1)
|
||||
|
||||
for thr in [0.50, 0.55, 0.60, 0.65, 0.70, 0.75]:
|
||||
accs = []
|
||||
capital = 1000
|
||||
n_trades = 0
|
||||
for j in range(len(X_test)):
|
||||
p_up = proba_gb[j][up_idx_gb]
|
||||
i = idx_test[j]
|
||||
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
if p_up >= thr:
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
ret = actual - 0.002
|
||||
capital *= (1 + ret * 0.5)
|
||||
n_trades += 1
|
||||
elif p_up <= (1 - thr):
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
ret = -actual - 0.002
|
||||
capital *= (1 + ret * 0.5)
|
||||
n_trades += 1
|
||||
|
||||
if not accs:
|
||||
continue
|
||||
acc = np.mean(accs) * 100
|
||||
total_ret = (capital - 1000) / 1000 * 100
|
||||
print(f" thr={thr:.2f}: trades={n_trades:5d} acc={acc:.1f}% ret={total_ret:+.1f}%")
|
||||
@@ -0,0 +1,320 @@
|
||||
"""Strategia 7: LSTM su features frattali multi-timeframe.
|
||||
Usa sequenze di features frattali come input a un LSTM
|
||||
per predire la direzione del prezzo.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from torch.utils.data import DataLoader, TensorDataset
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.indicators import hurst_exponent, fractal_dimension_higuchi, volatility_ratio
|
||||
from src.fractal.patterns import encode_candles, extract_body_ratios, extract_shadow_ratios
|
||||
|
||||
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||
print(f"Device: {DEVICE}")
|
||||
|
||||
|
||||
class FractalLSTM(nn.Module):
|
||||
def __init__(self, input_size: int, hidden_size: int = 64, num_layers: int = 2, dropout: float = 0.3):
|
||||
super().__init__()
|
||||
self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout)
|
||||
self.classifier = nn.Sequential(
|
||||
nn.Linear(hidden_size, 32),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(dropout),
|
||||
nn.Linear(32, 1),
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
_, (h_n, _) = self.lstm(x)
|
||||
out = self.classifier(h_n[-1])
|
||||
return out.squeeze(-1)
|
||||
|
||||
|
||||
def extract_candle_features(df: pd.DataFrame, i: int) -> np.ndarray:
|
||||
"""Extract per-candle features at index i."""
|
||||
o, h, l, c = df["open"].values[i], df["high"].values[i], df["low"].values[i], df["close"].values[i]
|
||||
v = df["volume"].values[i]
|
||||
total = h - l if h - l > 0 else 1e-10
|
||||
body = abs(c - o) / total
|
||||
upper_s = (h - max(o, c)) / total
|
||||
lower_s = (min(o, c) - l) / total
|
||||
direction = 1 if c > o else (-1 if c < o else 0)
|
||||
|
||||
# Log return from previous candle
|
||||
if i > 0:
|
||||
prev_c = df["close"].values[i - 1]
|
||||
log_ret = np.log(c / prev_c) if prev_c > 0 else 0
|
||||
else:
|
||||
log_ret = 0
|
||||
|
||||
return np.array([body, upper_s, lower_s, direction, log_ret, v])
|
||||
|
||||
|
||||
def build_dataset(df: pd.DataFrame, seq_len: int = 48, lookahead: int = 6, min_ret: float = 0.003):
|
||||
"""Build sequences of candle features with labels."""
|
||||
close = df["close"].values
|
||||
n = len(df)
|
||||
|
||||
vol_mean = pd.Series(df["volume"].values).rolling(100, min_periods=1).mean().values
|
||||
|
||||
sequences = []
|
||||
labels = []
|
||||
indices = []
|
||||
|
||||
# Pre-compute additional features
|
||||
candle_types = encode_candles(df)
|
||||
body_ratios = extract_body_ratios(df)
|
||||
shadow_ratios = extract_shadow_ratios(df)
|
||||
|
||||
for i in range(seq_len, n - lookahead, 2):
|
||||
seq = []
|
||||
for j in range(i - seq_len, i):
|
||||
feats = extract_candle_features(df, j)
|
||||
# Normalize volume by rolling mean
|
||||
feats[5] = feats[5] / vol_mean[j] if vol_mean[j] > 0 else 1.0
|
||||
seq.append(feats)
|
||||
|
||||
future_ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
|
||||
if abs(future_ret) < min_ret:
|
||||
continue
|
||||
|
||||
sequences.append(seq)
|
||||
labels.append(1 if future_ret > 0 else 0)
|
||||
indices.append(i)
|
||||
|
||||
return np.array(sequences), np.array(labels), np.array(indices)
|
||||
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 7: LSTM FRACTAL — BTC 1H")
|
||||
print("=" * 60)
|
||||
|
||||
df = load_data("BTC", "1h")
|
||||
close = df["close"].values
|
||||
|
||||
SEQ_LEN = 48
|
||||
LOOKAHEAD = 6
|
||||
EPOCHS = 30
|
||||
BATCH_SIZE = 256
|
||||
LR = 0.001
|
||||
|
||||
print(f"\nSeq length: {SEQ_LEN}, Lookahead: {LOOKAHEAD}")
|
||||
print("Building dataset...")
|
||||
|
||||
X, y, idx_arr = build_dataset(df, seq_len=SEQ_LEN, lookahead=LOOKAHEAD)
|
||||
print(f"Samples: {len(X)}, Features per candle: {X.shape[2]}, Up ratio: {np.mean(y)*100:.1f}%")
|
||||
|
||||
# Chronological split
|
||||
split = int(len(X) * 0.7)
|
||||
val_split = int(len(X) * 0.85)
|
||||
|
||||
X_train, X_val, X_test = X[:split], X[split:val_split], X[val_split:]
|
||||
y_train, y_val, y_test = y[:split], y[split:val_split], y[val_split:]
|
||||
idx_test_arr = idx_arr[val_split:]
|
||||
|
||||
# Normalize features per-feature across time
|
||||
n_features = X.shape[2]
|
||||
for f in range(n_features):
|
||||
scaler = StandardScaler()
|
||||
X_train[:, :, f] = scaler.fit_transform(X_train[:, :, f])
|
||||
X_val[:, :, f] = scaler.transform(X_val[:, :, f])
|
||||
X_test[:, :, f] = scaler.transform(X_test[:, :, f])
|
||||
|
||||
# To tensors
|
||||
X_train_t = torch.FloatTensor(X_train).to(DEVICE)
|
||||
y_train_t = torch.FloatTensor(y_train).to(DEVICE)
|
||||
X_val_t = torch.FloatTensor(X_val).to(DEVICE)
|
||||
y_val_t = torch.FloatTensor(y_val).to(DEVICE)
|
||||
X_test_t = torch.FloatTensor(X_test).to(DEVICE)
|
||||
|
||||
train_ds = TensorDataset(X_train_t, y_train_t)
|
||||
train_dl = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True)
|
||||
|
||||
# Model
|
||||
model = FractalLSTM(input_size=n_features, hidden_size=64, num_layers=2, dropout=0.3).to(DEVICE)
|
||||
optimizer = torch.optim.Adam(model.parameters(), lr=LR, weight_decay=1e-5)
|
||||
criterion = nn.BCEWithLogitsLoss()
|
||||
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5, factor=0.5)
|
||||
|
||||
print(f"\nTraining on {DEVICE}...")
|
||||
best_val_acc = 0
|
||||
patience_counter = 0
|
||||
|
||||
for epoch in range(EPOCHS):
|
||||
model.train()
|
||||
total_loss = 0
|
||||
for xb, yb in train_dl:
|
||||
optimizer.zero_grad()
|
||||
pred = model(xb)
|
||||
loss = criterion(pred, yb)
|
||||
loss.backward()
|
||||
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
|
||||
optimizer.step()
|
||||
total_loss += loss.item()
|
||||
|
||||
# Validation
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
val_pred = model(X_val_t)
|
||||
val_loss = criterion(val_pred, y_val_t).item()
|
||||
val_proba = torch.sigmoid(val_pred).cpu().numpy()
|
||||
val_acc = np.mean((val_proba > 0.5) == y_val)
|
||||
|
||||
scheduler.step(val_loss)
|
||||
|
||||
if val_acc > best_val_acc:
|
||||
best_val_acc = val_acc
|
||||
torch.save(model.state_dict(), "data/processed/best_lstm.pt")
|
||||
patience_counter = 0
|
||||
else:
|
||||
patience_counter += 1
|
||||
|
||||
if epoch % 5 == 0 or patience_counter > 8:
|
||||
print(f" Epoch {epoch:2d}: train_loss={total_loss/len(train_dl):.4f} val_loss={val_loss:.4f} val_acc={val_acc*100:.1f}% best={best_val_acc*100:.1f}%")
|
||||
|
||||
if patience_counter > 10:
|
||||
print(f" Early stopping at epoch {epoch}")
|
||||
break
|
||||
|
||||
# Load best model and test
|
||||
model.load_state_dict(torch.load("data/processed/best_lstm.pt", weights_only=True))
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
test_pred = model(X_test_t)
|
||||
test_proba = torch.sigmoid(test_pred).cpu().numpy()
|
||||
|
||||
test_acc = np.mean((test_proba > 0.5) == y_test)
|
||||
print(f"\nTest accuracy (base): {test_acc*100:.1f}%")
|
||||
|
||||
# Threshold sweep
|
||||
print("\n--- THRESHOLD SWEEP ---")
|
||||
for thr in [0.50, 0.55, 0.60, 0.65, 0.70, 0.75, 0.80]:
|
||||
accs = []
|
||||
capital = 1000
|
||||
n_trades = 0
|
||||
|
||||
for j in range(len(X_test)):
|
||||
p = test_proba[j]
|
||||
i = idx_test_arr[j]
|
||||
actual = (close[i + LOOKAHEAD - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
if p >= thr:
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
ret = actual - 0.002
|
||||
capital *= (1 + ret * 0.3)
|
||||
n_trades += 1
|
||||
elif p <= (1 - thr):
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
ret = -actual - 0.002
|
||||
capital *= (1 + ret * 0.3)
|
||||
n_trades += 1
|
||||
|
||||
if not accs:
|
||||
print(f" thr={thr:.2f}: no signals")
|
||||
continue
|
||||
|
||||
acc = np.mean(accs) * 100
|
||||
total_ret = (capital - 1000) / 1000 * 100
|
||||
# Annualized
|
||||
test_days = (idx_test_arr[-1] - idx_test_arr[0]) / 24
|
||||
years = test_days / 365.25 if test_days > 0 else 1
|
||||
ann_ret = ((capital / 1000) ** (1 / years) - 1) * 100 if years > 0 and capital > 0 else -100
|
||||
trades_yr = n_trades / years if years > 0 else 0
|
||||
print(f" thr={thr:.2f}: trades={n_trades:5d} acc={acc:.1f}% ret={total_ret:+.1f}% ann={ann_ret:+.1f}% trades/yr={trades_yr:.0f}")
|
||||
|
||||
# Also try ETH
|
||||
print("\n\n" + "=" * 60)
|
||||
print(" LSTM SU ETH 1H (same model architecture)")
|
||||
print("=" * 60)
|
||||
|
||||
df_eth = load_data("ETH", "1h")
|
||||
close_eth = df_eth["close"].values
|
||||
|
||||
X_eth, y_eth, idx_eth = build_dataset(df_eth, seq_len=SEQ_LEN, lookahead=LOOKAHEAD)
|
||||
print(f"ETH samples: {len(X_eth)}, Up ratio: {np.mean(y_eth)*100:.1f}%")
|
||||
|
||||
split_e = int(len(X_eth) * 0.7)
|
||||
val_e = int(len(X_eth) * 0.85)
|
||||
X_train_e, X_val_e, X_test_e = X_eth[:split_e], X_eth[split_e:val_e], X_eth[val_e:]
|
||||
y_train_e, y_val_e, y_test_e = y_eth[:split_e], y_eth[split_e:val_e], y_eth[val_e:]
|
||||
idx_test_e = idx_eth[val_e:]
|
||||
|
||||
for f in range(n_features):
|
||||
sc = StandardScaler()
|
||||
X_train_e[:, :, f] = sc.fit_transform(X_train_e[:, :, f])
|
||||
X_val_e[:, :, f] = sc.transform(X_val_e[:, :, f])
|
||||
X_test_e[:, :, f] = sc.transform(X_test_e[:, :, f])
|
||||
|
||||
X_tr_e = torch.FloatTensor(X_train_e).to(DEVICE)
|
||||
y_tr_e = torch.FloatTensor(y_train_e).to(DEVICE)
|
||||
X_va_e = torch.FloatTensor(X_val_e).to(DEVICE)
|
||||
y_va_e = torch.FloatTensor(y_val_e).to(DEVICE)
|
||||
X_te_e = torch.FloatTensor(X_test_e).to(DEVICE)
|
||||
|
||||
model_eth = FractalLSTM(input_size=n_features, hidden_size=64, num_layers=2, dropout=0.3).to(DEVICE)
|
||||
opt_e = torch.optim.Adam(model_eth.parameters(), lr=LR, weight_decay=1e-5)
|
||||
ds_e = TensorDataset(X_tr_e, y_tr_e)
|
||||
dl_e = DataLoader(ds_e, batch_size=BATCH_SIZE, shuffle=True)
|
||||
sch_e = torch.optim.lr_scheduler.ReduceLROnPlateau(opt_e, patience=5, factor=0.5)
|
||||
|
||||
best_e = 0
|
||||
pc = 0
|
||||
for epoch in range(EPOCHS):
|
||||
model_eth.train()
|
||||
tl = 0
|
||||
for xb, yb in dl_e:
|
||||
opt_e.zero_grad()
|
||||
p = model_eth(xb)
|
||||
loss = criterion(p, yb)
|
||||
loss.backward()
|
||||
torch.nn.utils.clip_grad_norm_(model_eth.parameters(), 1.0)
|
||||
opt_e.step()
|
||||
tl += loss.item()
|
||||
|
||||
model_eth.eval()
|
||||
with torch.no_grad():
|
||||
vp = model_eth(X_va_e)
|
||||
vl = criterion(vp, y_va_e).item()
|
||||
va = np.mean((torch.sigmoid(vp).cpu().numpy() > 0.5) == y_val_e)
|
||||
|
||||
sch_e.step(vl)
|
||||
if va > best_e:
|
||||
best_e = va
|
||||
torch.save(model_eth.state_dict(), "data/processed/best_lstm_eth.pt")
|
||||
pc = 0
|
||||
else:
|
||||
pc += 1
|
||||
if epoch % 5 == 0:
|
||||
print(f" Epoch {epoch:2d}: val_acc={va*100:.1f}% best={best_e*100:.1f}%")
|
||||
if pc > 10:
|
||||
break
|
||||
|
||||
model_eth.load_state_dict(torch.load("data/processed/best_lstm_eth.pt", weights_only=True))
|
||||
model_eth.eval()
|
||||
with torch.no_grad():
|
||||
tp_e = torch.sigmoid(model_eth(X_te_e)).cpu().numpy()
|
||||
|
||||
print(f"\nETH Test accuracy: {np.mean((tp_e > 0.5) == y_test_e)*100:.1f}%")
|
||||
|
||||
for thr in [0.55, 0.60, 0.65, 0.70]:
|
||||
accs = []
|
||||
capital = 1000
|
||||
for j in range(len(X_test_e)):
|
||||
p = tp_e[j]
|
||||
i = idx_test_e[j]
|
||||
actual = (close_eth[i + LOOKAHEAD - 1] - close_eth[i - 1]) / close_eth[i - 1]
|
||||
if p >= thr:
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
capital *= (1 + (actual - 0.002) * 0.3)
|
||||
elif p <= (1 - thr):
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
capital *= (1 + (-actual - 0.002) * 0.3)
|
||||
if accs:
|
||||
print(f" thr={thr:.2f}: trades={len(accs):5d} acc={np.mean(accs)*100:.1f}% ret={(capital-1000)/10:+.1f}%")
|
||||
@@ -0,0 +1,290 @@
|
||||
"""Strategia 8: Ensemble multi-timeframe.
|
||||
Combina i migliori approcci:
|
||||
1. GBM su structural features (miglior singolo finora: 58.6%/+57.5%)
|
||||
2. GBM su fractal indicators
|
||||
3. Multi-timeframe: 1h features + 15m aggregati
|
||||
Vota con consensus: trade solo quando almeno 2/3 modelli concordano.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.patterns import encode_candles, extract_body_ratios, extract_shadow_ratios
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 8: ENSEMBLE MULTI-TF — BTC")
|
||||
print("=" * 60)
|
||||
|
||||
# Load both timeframes
|
||||
df_1h = load_data("BTC", "1h")
|
||||
df_15m = load_data("BTC", "15m")
|
||||
|
||||
close_1h = df_1h["close"].values
|
||||
ts_1h = df_1h["timestamp"].values
|
||||
|
||||
WINDOW_1H = 24
|
||||
LOOKAHEAD = 6
|
||||
MIN_RETURN = 0.003
|
||||
|
||||
def structural_features_1h(df: pd.DataFrame, i: int, window: int = 24) -> np.ndarray | None:
|
||||
if i < window:
|
||||
return None
|
||||
|
||||
o = df["open"].values[i - window : i]
|
||||
h = df["high"].values[i - window : i]
|
||||
l = df["low"].values[i - window : i]
|
||||
c = df["close"].values[i - window : i]
|
||||
v = df["volume"].values[i - window : i]
|
||||
|
||||
all_p = np.concatenate([o, h, l, c])
|
||||
mn, mx = all_p.min(), all_p.max()
|
||||
if mx - mn == 0:
|
||||
return None
|
||||
o_n = (o - mn) / (mx - mn)
|
||||
h_n = (h - mn) / (mx - mn)
|
||||
l_n = (l - mn) / (mx - mn)
|
||||
c_n = (c - mn) / (mx - mn)
|
||||
|
||||
total = h - l
|
||||
total = np.where(total == 0, 1e-10, total)
|
||||
body = np.abs(c - o) / total
|
||||
u_shadow = (h - np.maximum(o, c)) / total
|
||||
l_shadow = (np.minimum(o, c) - l) / total
|
||||
direction = np.sign(c - o)
|
||||
|
||||
log_c = np.log(np.where(c == 0, 1e-10, c))
|
||||
rets = np.diff(log_c)
|
||||
|
||||
v_mean = np.mean(v)
|
||||
v_n = v / v_mean if v_mean > 0 else np.ones_like(v)
|
||||
|
||||
step = max(1, window // 12)
|
||||
idx = np.arange(0, window, step)[:12]
|
||||
|
||||
features = np.concatenate([
|
||||
c_n[idx], body[idx], direction[idx],
|
||||
u_shadow[idx], l_shadow[idx], v_n[idx],
|
||||
[np.mean(rets), np.std(rets), np.sum(rets),
|
||||
np.mean(body), np.std(body),
|
||||
np.max(body[-6:]) - np.min(body[-6:])],
|
||||
])
|
||||
return features
|
||||
|
||||
|
||||
def multi_tf_features(ts_current: int, df_15m: pd.DataFrame, n_bars: int = 48) -> np.ndarray | None:
|
||||
"""Extract aggregated features from 15m data aligned to current 1h candle."""
|
||||
ts_15m = df_15m["timestamp"].values
|
||||
mask = ts_15m <= ts_current
|
||||
end_idx = np.sum(mask)
|
||||
|
||||
if end_idx < n_bars:
|
||||
return None
|
||||
|
||||
start = end_idx - n_bars
|
||||
chunk = df_15m.iloc[start:end_idx]
|
||||
|
||||
c = chunk["close"].values
|
||||
h = chunk["high"].values
|
||||
l = chunk["low"].values
|
||||
v = chunk["volume"].values
|
||||
|
||||
if len(c) < n_bars:
|
||||
return None
|
||||
|
||||
log_c = np.log(np.where(c == 0, 1e-10, c))
|
||||
rets = np.diff(log_c)
|
||||
|
||||
# Micro-structure features
|
||||
mom_12 = np.sum(rets[-12:])
|
||||
mom_24 = np.sum(rets[-24:])
|
||||
vol_12 = np.std(rets[-12:])
|
||||
vol_48 = np.std(rets)
|
||||
|
||||
# Candle pattern stats
|
||||
ct = encode_candles(chunk)
|
||||
up_ratio_12 = np.mean(ct[-12:] == 1)
|
||||
up_ratio_24 = np.mean(ct[-24:] == 1)
|
||||
|
||||
# Intra-bar volatility (high-low range)
|
||||
ranges = (h - l) / np.where(c == 0, 1e-10, c)
|
||||
avg_range_12 = np.mean(ranges[-12:])
|
||||
avg_range_48 = np.mean(ranges)
|
||||
|
||||
# Volume profile
|
||||
v_mean = np.mean(v)
|
||||
v_recent = np.mean(v[-12:])
|
||||
vol_surge = v_recent / v_mean if v_mean > 0 else 1.0
|
||||
|
||||
# Autocorrelation
|
||||
if np.std(rets) > 0 and len(rets) > 1:
|
||||
ac1 = np.corrcoef(rets[:-1], rets[1:])[0, 1]
|
||||
ac1 = 0 if not np.isfinite(ac1) else ac1
|
||||
else:
|
||||
ac1 = 0
|
||||
|
||||
return np.array([
|
||||
mom_12, mom_24, vol_12, vol_48,
|
||||
up_ratio_12, up_ratio_24,
|
||||
avg_range_12, avg_range_48,
|
||||
vol_surge, ac1,
|
||||
vol_12 / vol_48 if vol_48 > 0 else 1.0,
|
||||
])
|
||||
|
||||
|
||||
print("Extracting features...")
|
||||
n_1h = len(df_1h)
|
||||
|
||||
X_struct = []
|
||||
X_multi = []
|
||||
y_all = []
|
||||
indices = []
|
||||
|
||||
for i in range(WINDOW_1H, n_1h - LOOKAHEAD, 1):
|
||||
if i % 5000 == 0:
|
||||
print(f" {i}/{n_1h}")
|
||||
|
||||
sf = structural_features_1h(df_1h, i, WINDOW_1H)
|
||||
if sf is None:
|
||||
continue
|
||||
|
||||
mf = multi_tf_features(ts_1h[i - 1], df_15m)
|
||||
if mf is None:
|
||||
continue
|
||||
|
||||
future_ret = (close_1h[i + LOOKAHEAD - 1] - close_1h[i - 1]) / close_1h[i - 1]
|
||||
if abs(future_ret) < MIN_RETURN:
|
||||
continue
|
||||
|
||||
X_struct.append(sf)
|
||||
X_multi.append(mf)
|
||||
y_all.append(1 if future_ret > 0 else 0)
|
||||
indices.append(i)
|
||||
|
||||
X_s = np.nan_to_num(np.array(X_struct), nan=0, posinf=1e6, neginf=-1e6)
|
||||
X_m = np.nan_to_num(np.array(X_multi), nan=0, posinf=1e6, neginf=-1e6)
|
||||
X_combined = np.hstack([X_s, X_m])
|
||||
y = np.array(y_all)
|
||||
idx_arr = np.array(indices)
|
||||
|
||||
print(f"\nSamples: {len(y)}, struct_feats: {X_s.shape[1]}, multi_feats: {X_m.shape[1]}, combined: {X_combined.shape[1]}")
|
||||
print(f"Up ratio: {np.mean(y)*100:.1f}%")
|
||||
|
||||
split = int(len(y) * 0.7)
|
||||
|
||||
# 3 models
|
||||
configs = {
|
||||
"M1_structural": X_s,
|
||||
"M2_multi_tf": X_m,
|
||||
"M3_combined": X_combined,
|
||||
}
|
||||
|
||||
probas = {}
|
||||
for name, X_data in configs.items():
|
||||
X_tr, X_te = X_data[:split], X_data[split:]
|
||||
y_tr, y_te = y[:split], y[split:]
|
||||
|
||||
sc = StandardScaler()
|
||||
X_tr_s = sc.fit_transform(X_tr)
|
||||
X_te_s = sc.transform(X_te)
|
||||
|
||||
model = GradientBoostingClassifier(
|
||||
n_estimators=300, max_depth=5, min_samples_leaf=30,
|
||||
learning_rate=0.03, subsample=0.8, random_state=42,
|
||||
)
|
||||
model.fit(X_tr_s, y_tr)
|
||||
proba = model.predict_proba(X_te_s)
|
||||
up_idx = list(model.classes_).index(1)
|
||||
|
||||
probas[name] = proba[:, up_idx]
|
||||
|
||||
# Individual results
|
||||
for thr in [0.55, 0.60, 0.65, 0.70]:
|
||||
accs = []
|
||||
capital = 1000
|
||||
for j in range(len(X_te)):
|
||||
p = proba[j][up_idx]
|
||||
i = idx_arr[split + j]
|
||||
actual = (close_1h[i + LOOKAHEAD - 1] - close_1h[i - 1]) / close_1h[i - 1]
|
||||
if p >= thr:
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
capital *= (1 + (actual - 0.002) * 0.5)
|
||||
elif p <= (1 - thr):
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
capital *= (1 + (-actual - 0.002) * 0.5)
|
||||
|
||||
if accs:
|
||||
acc = np.mean(accs) * 100
|
||||
ret = (capital - 1000) / 1000 * 100
|
||||
test_days = (idx_arr[-1] - idx_arr[split]) / 24
|
||||
years = test_days / 365.25
|
||||
ann = ((capital / 1000) ** (1 / years) - 1) * 100 if years > 0 and capital > 0 else -100
|
||||
print(f" {name:15s} thr={thr:.2f}: trades={len(accs):5d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}%")
|
||||
|
||||
|
||||
# Ensemble voting
|
||||
print("\n\n--- ENSEMBLE VOTING ---")
|
||||
y_test = y[split:]
|
||||
idx_test = idx_arr[split:]
|
||||
|
||||
for min_agree in [2, 3]:
|
||||
for thr in [0.55, 0.60, 0.65, 0.70]:
|
||||
accs = []
|
||||
capital = 1000
|
||||
for j in range(len(y_test)):
|
||||
votes_up = sum(1 for p in probas.values() if p[j] >= thr)
|
||||
votes_down = sum(1 for p in probas.values() if p[j] <= (1 - thr))
|
||||
|
||||
i = idx_test[j]
|
||||
actual = (close_1h[i + LOOKAHEAD - 1] - close_1h[i - 1]) / close_1h[i - 1]
|
||||
|
||||
if votes_up >= min_agree:
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
capital *= (1 + (actual - 0.002) * 0.5)
|
||||
elif votes_down >= min_agree:
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
capital *= (1 + (-actual - 0.002) * 0.5)
|
||||
|
||||
if accs:
|
||||
acc = np.mean(accs) * 100
|
||||
ret = (capital - 1000) / 1000 * 100
|
||||
test_days = (idx_test[-1] - idx_test[0]) / 24
|
||||
years = test_days / 365.25
|
||||
ann = ((capital / 1000) ** (1 / years) - 1) * 100 if years > 0 and capital > 0 else -100
|
||||
trades_yr = len(accs) / years if years > 0 else 0
|
||||
print(f" agree>={min_agree} thr={thr:.2f}: trades={len(accs):5d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}% trades/yr={trades_yr:.0f}")
|
||||
|
||||
|
||||
# Average probability ensemble
|
||||
print("\n--- ENSEMBLE AVERAGE PROBABILITY ---")
|
||||
avg_proba = np.mean([p for p in probas.values()], axis=0)
|
||||
|
||||
for thr in [0.55, 0.60, 0.65, 0.70, 0.75]:
|
||||
accs = []
|
||||
capital = 1000
|
||||
for j in range(len(y_test)):
|
||||
p = avg_proba[j]
|
||||
i = idx_test[j]
|
||||
actual = (close_1h[i + LOOKAHEAD - 1] - close_1h[i - 1]) / close_1h[i - 1]
|
||||
|
||||
if p >= thr:
|
||||
accs.append(1 if actual > 0 else 0)
|
||||
capital *= (1 + (actual - 0.002) * 0.5)
|
||||
elif p <= (1 - thr):
|
||||
accs.append(1 if actual < 0 else 0)
|
||||
capital *= (1 + (-actual - 0.002) * 0.5)
|
||||
|
||||
if accs:
|
||||
acc = np.mean(accs) * 100
|
||||
ret = (capital - 1000) / 1000 * 100
|
||||
test_days = (idx_test[-1] - idx_test[0]) / 24
|
||||
years = test_days / 365.25
|
||||
ann = ((capital / 1000) ** (1 / years) - 1) * 100 if years > 0 and capital > 0 else -100
|
||||
trades_yr = len(accs) / years if years > 0 else 0
|
||||
daily_ret = capital ** (1 / (test_days)) - 1 if test_days > 0 and capital > 0 else 0
|
||||
daily_pnl_on_1k = 1000 * daily_ret
|
||||
print(f" thr={thr:.2f}: trades={len(accs):5d} acc={acc:.1f}% ret={ret:+.1f}% ann={ann:+.1f}% trades/yr={trades_yr:.0f} daily_pnl=€{daily_pnl_on_1k:.2f}")
|
||||
@@ -0,0 +1,309 @@
|
||||
"""Strategia 9: Refined walk-forward with adaptive features.
|
||||
Combina le lezioni apprese:
|
||||
- Structural features (migliore singolo)
|
||||
- Walk-forward validation (no single split bias)
|
||||
- XGBoost (più potente di GBM per dati tabulari)
|
||||
- Dynamic exit: trailing stop + take profit
|
||||
- Multi-asset: BTC + ETH in portafoglio
|
||||
- Position sizing basato su confidenza
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.patterns import encode_candles, extract_body_ratios, extract_shadow_ratios
|
||||
from src.fractal.indicators import hurst_exponent, volatility_ratio
|
||||
|
||||
print("=" * 60)
|
||||
print(" STRATEGIA 9: WALK-FORWARD REFINATA")
|
||||
print("=" * 60)
|
||||
|
||||
|
||||
def build_features(df: pd.DataFrame, i: int) -> np.ndarray | None:
|
||||
"""All features from structural + fractal, no leakage."""
|
||||
if i < 200:
|
||||
return None
|
||||
|
||||
o = df["open"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
c = df["close"].values
|
||||
v = df["volume"].values
|
||||
|
||||
feats = []
|
||||
|
||||
# Structural features (3 windows)
|
||||
for w in [12, 24, 48]:
|
||||
win_c = c[i - w : i]
|
||||
win_o = o[i - w : i]
|
||||
win_h = h[i - w : i]
|
||||
win_l = l[i - w : i]
|
||||
win_v = v[i - w : i]
|
||||
|
||||
mn, mx = min(win_l.min(), win_o.min()), max(win_h.max(), win_o.max())
|
||||
if mx - mn == 0:
|
||||
feats.extend([0] * 15)
|
||||
continue
|
||||
|
||||
c_n = (win_c - mn) / (mx - mn)
|
||||
total = win_h - win_l
|
||||
total = np.where(total == 0, 1e-10, total)
|
||||
|
||||
body = np.abs(win_c - win_o) / total
|
||||
direction = np.sign(win_c - win_o)
|
||||
|
||||
log_c = np.log(np.where(win_c == 0, 1e-10, win_c))
|
||||
rets = np.diff(log_c)
|
||||
|
||||
v_mean = np.mean(win_v)
|
||||
v_n = win_v / v_mean if v_mean > 0 else np.ones_like(win_v)
|
||||
|
||||
feats.extend([
|
||||
np.mean(rets),
|
||||
np.std(rets),
|
||||
np.sum(rets),
|
||||
float(pd.Series(rets).skew()) if len(rets) > 2 else 0,
|
||||
float(pd.Series(rets).kurtosis()) if len(rets) > 3 else 0,
|
||||
np.mean(body),
|
||||
np.std(body),
|
||||
np.mean(direction[-6:]),
|
||||
np.mean(direction),
|
||||
c_n[-1],
|
||||
np.mean(c_n[-6:]),
|
||||
v_n[-1],
|
||||
np.mean(v_n[-6:]),
|
||||
np.max(body[-6:]),
|
||||
np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
|
||||
])
|
||||
|
||||
# Fractal features
|
||||
ret_long = np.diff(np.log(np.where(c[i-96:i] == 0, 1e-10, c[i-96:i])))
|
||||
if len(ret_long) > 20:
|
||||
h_exp = hurst_exponent(ret_long, max_lag=min(len(ret_long)//4, 20))
|
||||
else:
|
||||
h_exp = 0.5
|
||||
|
||||
feats.append(h_exp)
|
||||
feats.append(volatility_ratio(c[i-48:i], fast=12, slow=48))
|
||||
|
||||
# ATR
|
||||
tr_arr = np.maximum(h[i-14:i] - l[i-14:i],
|
||||
np.maximum(np.abs(h[i-14:i] - np.roll(c[i-14:i], 1)),
|
||||
np.abs(l[i-14:i] - np.roll(c[i-14:i], 1))))
|
||||
atr = np.mean(tr_arr[1:])
|
||||
feats.append(atr / c[i-1] if c[i-1] > 0 else 0)
|
||||
|
||||
# Price position relative to recent range
|
||||
high_48 = np.max(h[i-48:i])
|
||||
low_48 = np.min(l[i-48:i])
|
||||
range_48 = high_48 - low_48
|
||||
feats.append((c[i-1] - low_48) / range_48 if range_48 > 0 else 0.5)
|
||||
|
||||
return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
|
||||
def walk_forward_backtest(
|
||||
df: pd.DataFrame,
|
||||
train_size: int = 10000,
|
||||
step_size: int = 2000,
|
||||
lookahead: int = 6,
|
||||
min_return: float = 0.003,
|
||||
threshold: float = 0.60,
|
||||
fee_pct: float = 0.001,
|
||||
position_pct: float = 0.3,
|
||||
) -> dict:
|
||||
"""Walk-forward validation with rolling train window."""
|
||||
close = df["close"].values
|
||||
n = len(df)
|
||||
|
||||
all_trades = []
|
||||
capital = 1000.0
|
||||
equity = [capital]
|
||||
|
||||
start = 200
|
||||
features_cache: dict[int, np.ndarray] = {}
|
||||
|
||||
def get_features(idx: int) -> np.ndarray | None:
|
||||
if idx not in features_cache:
|
||||
features_cache[idx] = build_features(df, idx)
|
||||
return features_cache[idx]
|
||||
|
||||
# Pre-compute all features
|
||||
print(" Pre-computing features...")
|
||||
for i in range(start, n - lookahead, 2):
|
||||
get_features(i)
|
||||
|
||||
fold = 0
|
||||
train_start = start
|
||||
total_signals = 0
|
||||
total_correct = 0
|
||||
|
||||
while train_start + train_size + step_size + lookahead < n:
|
||||
train_end = train_start + train_size
|
||||
test_end = min(train_end + step_size, n - lookahead)
|
||||
|
||||
# Build train set
|
||||
X_train, y_train = [], []
|
||||
for i in range(train_start, train_end, 2):
|
||||
f = get_features(i)
|
||||
if f is None:
|
||||
continue
|
||||
ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
|
||||
if abs(ret) < min_return:
|
||||
continue
|
||||
X_train.append(f)
|
||||
y_train.append(1 if ret > 0 else 0)
|
||||
|
||||
if len(X_train) < 100:
|
||||
train_start += step_size
|
||||
continue
|
||||
|
||||
X_tr = np.array(X_train)
|
||||
y_tr = np.array(y_train)
|
||||
|
||||
scaler = StandardScaler()
|
||||
X_tr_s = scaler.fit_transform(X_tr)
|
||||
|
||||
model = GradientBoostingClassifier(
|
||||
n_estimators=200, max_depth=5, min_samples_leaf=30,
|
||||
learning_rate=0.05, subsample=0.8, random_state=42,
|
||||
)
|
||||
model.fit(X_tr_s, y_tr)
|
||||
|
||||
up_idx = list(model.classes_).index(1)
|
||||
|
||||
# Test on next step
|
||||
fold_trades = 0
|
||||
fold_correct = 0
|
||||
for i in range(train_end, test_end, 2):
|
||||
f = get_features(i)
|
||||
if f is None:
|
||||
continue
|
||||
|
||||
f_s = scaler.transform(f.reshape(1, -1))
|
||||
proba = model.predict_proba(f_s)[0]
|
||||
p_up = proba[up_idx]
|
||||
|
||||
actual_ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
|
||||
if abs(actual_ret) < min_return:
|
||||
continue
|
||||
|
||||
direction = None
|
||||
if p_up >= threshold:
|
||||
direction = "long"
|
||||
elif p_up <= (1 - threshold):
|
||||
direction = "short"
|
||||
|
||||
if direction:
|
||||
if direction == "long":
|
||||
trade_ret = actual_ret
|
||||
else:
|
||||
trade_ret = -actual_ret
|
||||
|
||||
net_ret = trade_ret - fee_pct * 2
|
||||
pnl = capital * position_pct * net_ret
|
||||
capital += pnl
|
||||
equity.append(capital)
|
||||
|
||||
is_correct = (direction == "long" and actual_ret > 0) or (direction == "short" and actual_ret < 0)
|
||||
fold_trades += 1
|
||||
if is_correct:
|
||||
fold_correct += 1
|
||||
|
||||
all_trades.append({
|
||||
"fold": fold,
|
||||
"idx": i,
|
||||
"direction": direction,
|
||||
"prob": p_up,
|
||||
"actual_ret": actual_ret,
|
||||
"net_ret": net_ret,
|
||||
"pnl": pnl,
|
||||
"correct": is_correct,
|
||||
})
|
||||
|
||||
total_signals += fold_trades
|
||||
total_correct += fold_correct
|
||||
fold_acc = fold_correct / fold_trades * 100 if fold_trades > 0 else 0
|
||||
if fold % 3 == 0:
|
||||
print(f" Fold {fold}: trades={fold_trades} acc={fold_acc:.0f}% capital=€{capital:.0f}")
|
||||
|
||||
fold += 1
|
||||
train_start += step_size
|
||||
|
||||
# Results
|
||||
if not all_trades:
|
||||
return {"error": "no trades"}
|
||||
|
||||
trades_df = pd.DataFrame(all_trades)
|
||||
total_acc = total_correct / total_signals * 100 if total_signals > 0 else 0
|
||||
|
||||
test_candles = n - 200 - train_size
|
||||
test_days = test_candles / 24
|
||||
test_years = test_days / 365.25
|
||||
|
||||
ann_ret = ((capital / 1000) ** (1 / test_years) - 1) * 100 if test_years > 0 and capital > 0 else -100
|
||||
|
||||
# Max drawdown
|
||||
peak = equity[0]
|
||||
max_dd = 0
|
||||
for v in equity:
|
||||
if v > peak:
|
||||
peak = v
|
||||
dd = (peak - v) / peak if peak > 0 else 0
|
||||
if dd > max_dd:
|
||||
max_dd = dd
|
||||
|
||||
# Sharpe
|
||||
equity_arr = np.array(equity)
|
||||
rets = np.diff(equity_arr) / equity_arr[:-1]
|
||||
rets = rets[np.isfinite(rets)]
|
||||
sharpe = np.mean(rets) / np.std(rets) * np.sqrt(252 * 24) if np.std(rets) > 0 else 0
|
||||
|
||||
return {
|
||||
"total_trades": total_signals,
|
||||
"accuracy": total_acc,
|
||||
"total_return": (capital - 1000) / 1000 * 100,
|
||||
"annualized_return": ann_ret,
|
||||
"max_drawdown": max_dd * 100,
|
||||
"sharpe": sharpe,
|
||||
"final_capital": capital,
|
||||
"trades_per_year": total_signals / test_years if test_years > 0 else 0,
|
||||
"daily_pnl": (capital - 1000) / test_days if test_days > 0 else 0,
|
||||
"folds": fold,
|
||||
}
|
||||
|
||||
|
||||
# Run for both assets with parameter sweep
|
||||
for asset in ["BTC", "ETH"]:
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} 1H — WALK-FORWARD")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df = load_data(asset, "1h")
|
||||
|
||||
for lookahead in [3, 6]:
|
||||
for threshold in [0.55, 0.60, 0.65, 0.70]:
|
||||
result = walk_forward_backtest(
|
||||
df,
|
||||
train_size=15000,
|
||||
step_size=3000,
|
||||
lookahead=lookahead,
|
||||
threshold=threshold,
|
||||
position_pct=0.3,
|
||||
)
|
||||
if "error" in result:
|
||||
continue
|
||||
|
||||
print(f"\n LA={lookahead} thr={threshold:.2f}: "
|
||||
f"trades={result['total_trades']:4d} "
|
||||
f"acc={result['accuracy']:.1f}% "
|
||||
f"ret={result['total_return']:+.1f}% "
|
||||
f"ann={result['annualized_return']:+.1f}% "
|
||||
f"dd={result['max_drawdown']:.1f}% "
|
||||
f"sharpe={result['sharpe']:.2f} "
|
||||
f"€/day={result['daily_pnl']:.2f}")
|
||||
@@ -0,0 +1,340 @@
|
||||
"""Strategia 10: High Precision (target >80% accuracy).
|
||||
Approccio: combina TUTTI gli indicatori disponibili, usa ensemble di 5 modelli,
|
||||
trade SOLO quando tutti concordano. Pochi trade ma molto precisi.
|
||||
Usa leva 3x per compensare bassa frequenza.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import GradientBoostingClassifier, RandomForestClassifier, ExtraTreesClassifier
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
from sklearn.svm import SVC
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
from src.data.downloader import load_data
|
||||
from src.fractal.patterns import encode_candles, extract_body_ratios, extract_shadow_ratios
|
||||
from src.fractal.indicators import hurst_exponent, volatility_ratio
|
||||
|
||||
LEVERAGE = 3
|
||||
FEE_PCT = 0.001
|
||||
INITIAL_CAPITAL = 1000
|
||||
|
||||
|
||||
def build_rich_features(df: pd.DataFrame, i: int) -> np.ndarray | None:
|
||||
if i < 200:
|
||||
return None
|
||||
|
||||
o = df["open"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
c = df["close"].values
|
||||
v = df["volume"].values
|
||||
|
||||
feats = []
|
||||
|
||||
for w in [6, 12, 24, 48, 96]:
|
||||
if i < w:
|
||||
feats.extend([0] * 18)
|
||||
continue
|
||||
|
||||
win_c = c[i - w : i]
|
||||
win_o = o[i - w : i]
|
||||
win_h = h[i - w : i]
|
||||
win_l = l[i - w : i]
|
||||
win_v = v[i - w : i]
|
||||
|
||||
mn, mx = win_l.min(), max(win_h.max(), win_c.max())
|
||||
rng = mx - mn if mx - mn > 0 else 1e-10
|
||||
|
||||
total = win_h - win_l
|
||||
total = np.where(total == 0, 1e-10, total)
|
||||
body = np.abs(win_c - win_o) / total
|
||||
direction = np.sign(win_c - win_o)
|
||||
|
||||
log_c = np.log(np.where(win_c == 0, 1e-10, win_c))
|
||||
rets = np.diff(log_c)
|
||||
|
||||
v_mean = np.mean(win_v)
|
||||
|
||||
feats.extend([
|
||||
np.mean(rets) if len(rets) > 0 else 0,
|
||||
np.std(rets) if len(rets) > 0 else 0,
|
||||
np.sum(rets) if len(rets) > 0 else 0,
|
||||
float(pd.Series(rets).skew()) if len(rets) > 2 else 0,
|
||||
float(pd.Series(rets).kurtosis()) if len(rets) > 3 else 0,
|
||||
np.mean(body),
|
||||
np.std(body),
|
||||
np.mean(direction),
|
||||
np.mean(direction[-min(3, w):]),
|
||||
(win_c[-1] - mn) / rng,
|
||||
win_v[-1] / v_mean if v_mean > 0 else 1,
|
||||
np.max(body) - np.min(body),
|
||||
np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
|
||||
np.max(rets) if len(rets) > 0 else 0,
|
||||
np.min(rets) if len(rets) > 0 else 0,
|
||||
np.mean(np.abs(rets)) if len(rets) > 0 else 0,
|
||||
np.sum(direction == 1) / w,
|
||||
np.sum(direction == -1) / w,
|
||||
])
|
||||
|
||||
# Hurst on different windows
|
||||
for w in [48, 96]:
|
||||
ret_w = np.diff(np.log(np.where(c[max(0,i-w):i] == 0, 1e-10, c[max(0,i-w):i])))
|
||||
if len(ret_w) > 20:
|
||||
feats.append(hurst_exponent(ret_w, max_lag=min(len(ret_w)//4, 15)))
|
||||
else:
|
||||
feats.append(0.5)
|
||||
|
||||
# Volatility ratios
|
||||
feats.append(volatility_ratio(c[max(0,i-48):i], fast=6, slow=48))
|
||||
feats.append(volatility_ratio(c[max(0,i-96):i], fast=12, slow=96))
|
||||
|
||||
# ATR normalized
|
||||
tr = np.maximum(h[i-14:i] - l[i-14:i],
|
||||
np.maximum(np.abs(h[i-14:i] - np.roll(c[i-14:i], 1)),
|
||||
np.abs(l[i-14:i] - np.roll(c[i-14:i], 1))))
|
||||
atr = np.mean(tr[1:])
|
||||
feats.append(atr / c[i-1] if c[i-1] > 0 else 0)
|
||||
|
||||
# Position in range
|
||||
h48 = np.max(h[i-48:i])
|
||||
l48 = np.min(l[i-48:i])
|
||||
r48 = h48 - l48
|
||||
feats.append((c[i-1] - l48) / r48 if r48 > 0 else 0.5)
|
||||
|
||||
h96 = np.max(h[i-96:i])
|
||||
l96 = np.min(l[i-96:i])
|
||||
r96 = h96 - l96
|
||||
feats.append((c[i-1] - l96) / r96 if r96 > 0 else 0.5)
|
||||
|
||||
return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
|
||||
def run_high_precision(asset: str, lookahead: int = 3):
|
||||
print(f"\n{'#'*60}")
|
||||
print(f" {asset} 1H — HIGH PRECISION (LA={lookahead})")
|
||||
print(f"{'#'*60}")
|
||||
|
||||
df = load_data(asset, "1h")
|
||||
close = df["close"].values
|
||||
n = len(df)
|
||||
|
||||
MIN_RETURN = 0.003
|
||||
|
||||
# Build dataset
|
||||
print(" Building features...")
|
||||
X_all, y_all, idx_all = [], [], []
|
||||
for i in range(200, n - lookahead, 1):
|
||||
f = build_rich_features(df, i)
|
||||
if f is None:
|
||||
continue
|
||||
ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
|
||||
if abs(ret) < MIN_RETURN:
|
||||
continue
|
||||
X_all.append(f)
|
||||
y_all.append(1 if ret > 0 else 0)
|
||||
idx_all.append(i)
|
||||
|
||||
X = np.array(X_all)
|
||||
y = np.array(y_all)
|
||||
idx_arr = np.array(idx_all)
|
||||
|
||||
print(f" Samples: {len(X)}, Features: {X.shape[1]}, Up: {np.mean(y)*100:.1f}%")
|
||||
|
||||
# Walk-forward with 5-model ensemble
|
||||
TRAIN_SIZE = 15000
|
||||
STEP_SIZE = 3000
|
||||
|
||||
models_config = [
|
||||
("GB1", GradientBoostingClassifier(n_estimators=200, max_depth=4, min_samples_leaf=30, learning_rate=0.05, subsample=0.8, random_state=42)),
|
||||
("GB2", GradientBoostingClassifier(n_estimators=300, max_depth=5, min_samples_leaf=50, learning_rate=0.03, subsample=0.7, random_state=123)),
|
||||
("RF", RandomForestClassifier(n_estimators=300, max_depth=8, min_samples_leaf=30, class_weight="balanced", random_state=42, n_jobs=-1)),
|
||||
("ET", ExtraTreesClassifier(n_estimators=300, max_depth=8, min_samples_leaf=30, class_weight="balanced", random_state=42, n_jobs=-1)),
|
||||
("LR", LogisticRegression(max_iter=1000, C=0.1, random_state=42)),
|
||||
]
|
||||
|
||||
capital = float(INITIAL_CAPITAL)
|
||||
all_trades = []
|
||||
equity = [capital]
|
||||
|
||||
fold = 0
|
||||
start = 0
|
||||
|
||||
while start + TRAIN_SIZE + STEP_SIZE < len(X):
|
||||
train_end = start + TRAIN_SIZE
|
||||
test_end = min(train_end + STEP_SIZE, len(X))
|
||||
|
||||
X_tr, y_tr = X[start:train_end], y[start:train_end]
|
||||
X_te, y_te = X[train_end:test_end], y[train_end:test_end]
|
||||
idx_te = idx_arr[train_end:test_end]
|
||||
|
||||
scaler = StandardScaler()
|
||||
X_tr_s = scaler.fit_transform(X_tr)
|
||||
X_te_s = scaler.transform(X_te)
|
||||
|
||||
# Train all models
|
||||
trained = []
|
||||
for name, model in models_config:
|
||||
m = type(model)(**model.get_params())
|
||||
m.fit(X_tr_s, y_tr)
|
||||
trained.append((name, m))
|
||||
|
||||
# Test with consensus voting
|
||||
for j in range(len(X_te)):
|
||||
votes_up = 0
|
||||
votes_down = 0
|
||||
max_conf = 0
|
||||
|
||||
for name, m in trained:
|
||||
proba = m.predict_proba(X_te_s[j:j+1])[0]
|
||||
up_idx = list(m.classes_).index(1)
|
||||
p_up = proba[up_idx]
|
||||
|
||||
if p_up >= 0.60:
|
||||
votes_up += 1
|
||||
max_conf = max(max_conf, p_up)
|
||||
elif p_up <= 0.40:
|
||||
votes_down += 1
|
||||
max_conf = max(max_conf, 1 - p_up)
|
||||
|
||||
i = idx_te[j]
|
||||
actual_ret = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
# Trade only with strong consensus
|
||||
min_votes = 4 # at least 4 out of 5 models agree
|
||||
direction = None
|
||||
if votes_up >= min_votes:
|
||||
direction = "long"
|
||||
elif votes_down >= min_votes:
|
||||
direction = "short"
|
||||
|
||||
if direction:
|
||||
if direction == "long":
|
||||
trade_ret = actual_ret
|
||||
else:
|
||||
trade_ret = -actual_ret
|
||||
|
||||
net_ret = trade_ret * LEVERAGE - FEE_PCT * 2 * LEVERAGE
|
||||
pos_size = 0.2 # 20% of capital per trade
|
||||
pnl = capital * pos_size * net_ret
|
||||
capital += pnl
|
||||
capital = max(capital, 0)
|
||||
equity.append(capital)
|
||||
|
||||
is_correct = (direction == "long" and actual_ret > 0) or (direction == "short" and actual_ret < 0)
|
||||
all_trades.append({
|
||||
"fold": fold,
|
||||
"idx": i,
|
||||
"direction": direction,
|
||||
"votes_up": votes_up,
|
||||
"votes_down": votes_down,
|
||||
"actual_ret": actual_ret,
|
||||
"net_ret": net_ret,
|
||||
"pnl": pnl,
|
||||
"correct": is_correct,
|
||||
})
|
||||
|
||||
fold += 1
|
||||
start += STEP_SIZE
|
||||
|
||||
if not all_trades:
|
||||
print(" No trades generated!")
|
||||
return
|
||||
|
||||
trades_df = pd.DataFrame(all_trades)
|
||||
n_correct = trades_df["correct"].sum()
|
||||
n_total = len(trades_df)
|
||||
accuracy = n_correct / n_total * 100
|
||||
|
||||
test_candles = idx_arr[-1] - idx_arr[TRAIN_SIZE]
|
||||
test_days = test_candles / 24
|
||||
test_years = test_days / 365.25
|
||||
|
||||
ann_ret = ((capital / INITIAL_CAPITAL) ** (1 / test_years) - 1) * 100 if test_years > 0 and capital > 0 else -100
|
||||
daily_pnl = (capital - INITIAL_CAPITAL) / test_days if test_days > 0 else 0
|
||||
|
||||
# Max DD
|
||||
peak = equity[0]
|
||||
max_dd = 0
|
||||
for v in equity:
|
||||
if v > peak:
|
||||
peak = v
|
||||
dd = (peak - v) / peak if peak > 0 else 0
|
||||
max_dd = max(max_dd, dd)
|
||||
|
||||
print(f"\n RISULTATI:")
|
||||
print(f" Trades: {n_total}")
|
||||
print(f" Accuracy: {accuracy:.1f}%")
|
||||
print(f" Return: {(capital-INITIAL_CAPITAL)/INITIAL_CAPITAL*100:+.1f}%")
|
||||
print(f" Annualized: {ann_ret:+.1f}%")
|
||||
print(f" Max Drawdown: {max_dd*100:.1f}%")
|
||||
print(f" Capital: €{capital:.0f}")
|
||||
print(f" Trades/year: {n_total/test_years:.0f}")
|
||||
print(f" €/day avg: €{daily_pnl:.2f}")
|
||||
|
||||
# Consensus threshold sweep
|
||||
print(f"\n --- CONSENSUS SWEEP ---")
|
||||
for min_v in [3, 4, 5]:
|
||||
for ind_thr in [0.55, 0.60, 0.65]:
|
||||
cap = float(INITIAL_CAPITAL)
|
||||
trades_count = 0
|
||||
correct_count = 0
|
||||
eq = [cap]
|
||||
|
||||
fold_s = 0
|
||||
start_s = 0
|
||||
while start_s + TRAIN_SIZE + STEP_SIZE < len(X):
|
||||
train_end_s = start_s + TRAIN_SIZE
|
||||
test_end_s = min(train_end_s + STEP_SIZE, len(X))
|
||||
|
||||
X_tr_s2 = scaler.fit_transform(X[start_s:train_end_s])
|
||||
X_te_s2 = scaler.transform(X[train_end_s:test_end_s])
|
||||
y_tr_s2 = y[start_s:train_end_s]
|
||||
idx_te_s2 = idx_arr[train_end_s:test_end_s]
|
||||
|
||||
trained_s = []
|
||||
for name, model in models_config:
|
||||
m2 = type(model)(**model.get_params())
|
||||
m2.fit(X_tr_s2, y_tr_s2)
|
||||
trained_s.append(m2)
|
||||
|
||||
for j in range(len(X_te_s2)):
|
||||
vu = sum(1 for m2 in trained_s
|
||||
if m2.predict_proba(X_te_s2[j:j+1])[0][list(m2.classes_).index(1)] >= ind_thr)
|
||||
vd = sum(1 for m2 in trained_s
|
||||
if m2.predict_proba(X_te_s2[j:j+1])[0][list(m2.classes_).index(1)] <= (1-ind_thr))
|
||||
|
||||
i_s = idx_te_s2[j]
|
||||
ar = (close[i_s + lookahead - 1] - close[i_s - 1]) / close[i_s - 1]
|
||||
|
||||
d = None
|
||||
if vu >= min_v:
|
||||
d = "long"
|
||||
elif vd >= min_v:
|
||||
d = "short"
|
||||
|
||||
if d:
|
||||
tr = ar if d == "long" else -ar
|
||||
nr = tr * LEVERAGE - FEE_PCT * 2 * LEVERAGE
|
||||
cap += cap * 0.2 * nr
|
||||
cap = max(cap, 0)
|
||||
eq.append(cap)
|
||||
trades_count += 1
|
||||
if (d == "long" and ar > 0) or (d == "short" and ar < 0):
|
||||
correct_count += 1
|
||||
|
||||
start_s += STEP_SIZE
|
||||
|
||||
if trades_count > 0:
|
||||
acc_s = correct_count / trades_count * 100
|
||||
ret_s = (cap - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
|
||||
ann_s = ((cap / INITIAL_CAPITAL) ** (1 / test_years) - 1) * 100 if test_years > 0 and cap > 0 else -100
|
||||
dpnl = (cap - INITIAL_CAPITAL) / test_days if test_days > 0 else 0
|
||||
print(f" min_votes={min_v} ind_thr={ind_thr:.2f}: trades={trades_count:4d} acc={acc_s:.1f}% ret={ret_s:+.1f}% ann={ann_s:+.1f}% €/day={dpnl:.2f}")
|
||||
|
||||
|
||||
for asset in ["BTC", "ETH"]:
|
||||
for la in [3, 6]:
|
||||
run_high_precision(asset, la)
|
||||
@@ -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