14522262e6
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
409 lines
14 KiB
Python
409 lines
14 KiB
Python
"""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}")
|