"""Analisi finale — S1 (squeeze puro) vs Script 13 (squeeze+ML GBM). Metriche: PnL, num trades, DD max, tempo medio a mercato, descrizione. """ 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_PERP = 0.002 FEE_ML = 0.001 INITIAL = 1000 LEVERAGE = 3 TF_MINUTES = {"1m": 1, "5m": 5, "15m": 15, "1h": 60, "4h": 240, "1d": 1440} # ── helpers ────────────────────────────────────────────────────────── 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 detect_squeezes(close, high, low, kcr, sq_thr=0.8, min_dur=5): 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 dur = i - sq_start if dur < min_dur: continue events.append({"idx": i, "dur": dur, "sq_start": sq_start, "avg_vol_squeeze": np.mean(close[sq_start:i]), "kcr_at_release": kcr[i]}) return events def _build_result(yearly, capital, max_dd, all_t, all_w, time_pct, avg_dur_h): acc = all_w / all_t * 100 tot_pnl = sum(p for d in yearly.values() for p in d["pnls"]) years_active = len(yearly) all_pnls = [p for d in yearly.values() for p in d["pnls"]] sharpe = np.mean(all_pnls) / np.std(all_pnls) * np.sqrt(252) if len(all_pnls) > 1 and np.std(all_pnls) > 0 else 0 year_details = {} for y in sorted(yearly): d = yearly[y] ya = d["w"] / d["t"] * 100 if d["t"] > 0 else 0 yp = sum(d["pnls"]) year_details[y] = {"trades": d["t"], "acc": ya, "pnl": yp} valid_years = {y: d for y, d in year_details.items() if d["trades"] >= 10} if valid_years: worst_y = min(valid_years, key=lambda y: valid_years[y]["acc"]) worst_acc = valid_years[worst_y]["acc"] elif year_details: worst_y = min(year_details, key=lambda y: year_details[y]["acc"]) worst_acc = year_details[worst_y]["acc"] else: worst_y = "N/A" worst_acc = 0 daily_pnl = tot_pnl / (years_active * 365) if years_active > 0 else 0 return { "trades": all_t, "acc": acc, "pnl": tot_pnl, "capital": capital, "max_dd": max_dd * 100, "sharpe": sharpe, "daily_pnl": daily_pnl, "time_in_market_pct": time_pct, "avg_dur_h": avg_dur_h, "years_active": years_active, "worst_year": str(worst_y), "worst_acc": worst_acc, "year_details": year_details, } # ── S1: Squeeze breakout puro ──────────────────────────────────────── def run_s1_squeeze(asset, tf, hold=3): df = load_data(asset, tf) c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values n = len(c) ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) kcr = keltner_ratio(c, h, l, 14) events = detect_squeezes(c, h, l, kcr) yearly = {} capital = float(INITIAL) peak = capital max_dd = 0 total_bars = 0 for ev in events: i = ev["idx"] if i + hold + 1 >= n: continue first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0 if abs(first_ret) < 0.001: continue direction = 1 if first_ret > 0 else -1 entry = c[i-1] exit_price = c[min(i + hold - 1, n - 1)] actual = (exit_price - entry) / entry * direction net = actual * LEVERAGE - FEE_PERP * 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) total_bars += hold 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 == 0: return None return _build_result(yearly, capital, max_dd, all_t, all_w, total_bars / n * 100, hold * TF_MINUTES.get(tf, 60) / 60) def run_s1_antifake_vol(asset, tf, hold=3): df = load_data(asset, tf) c, h, l, v = df["close"].values, df["high"].values, df["low"].values, df["volume"].values n = len(c) ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) kcr = keltner_ratio(c, h, l, 14) events = detect_squeezes(c, h, l, kcr) yearly = {} capital = float(INITIAL) peak = capital max_dd = 0 total_bars = 0 for ev in events: i = ev["idx"] if i + hold + 1 >= n: continue first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0 if abs(first_ret) < 0.001: continue br = h[i] - l[i] if br > 0: if c[i] > c[i-1]: if (h[i] - c[i]) / br > 0.6: continue else: if (c[i] - l[i]) / br > 0.6: continue avg_v = np.mean(v[ev["sq_start"]:i]) if avg_v > 0 and v[i] <= avg_v * 1.3: continue direction = 1 if first_ret > 0 else -1 entry = c[i-1] exit_price = c[min(i + hold - 1, n - 1)] actual = (exit_price - entry) / entry * direction net = actual * LEVERAGE - FEE_PERP * 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) total_bars += hold 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 == 0: return None return _build_result(yearly, capital, max_dd, all_t, all_w, total_bars / n * 100, hold * TF_MINUTES.get(tf, 60) / 60) # ── Script 13: Squeeze + ML ibrida (GBM walk-forward) ──────────────── def build_features_at(df, i, squeeze_info): 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 = [] 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, ]) sq = squeeze_info feats.extend([ sq["dur"], sq["dur"] / 24, 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, ]) 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) 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_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_s13_hybrid(asset, tf, bb_w, sq_thr, brk_bars, leverage, pos_pct, ml_thr): df = load_data(asset, tf) close = df["close"].values high = df["high"].values low = df["low"].values volume = df["volume"].values n = len(df) ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True) kcr = keltner_ratio(close, high, low, bb_w) events = detect_squeezes(close, high, low, kcr, sq_thr) 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: return None X = np.array(X_all) y = np.array(y_all) TRAIN_SIZE = max(int(len(X) * 0.5), 50) STEP_SIZE = max(int(len(X) * 0.1), 10) yearly = {} capital = float(INITIAL) peak = capital max_dd = 0 total_bars = 0 all_t = 0 all_w = 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 = X[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] direction = None if p_up >= ml_thr: direction = 1 elif p_up <= (1 - ml_thr): direction = -1 if direction is None: continue is_correct = (direction == 1 and actual_ret > 0) or (direction == -1 and actual_ret < 0) trade_ret = actual_ret * direction net = trade_ret * leverage - FEE_ML * 2 * leverage capital += capital * pos_pct * net capital = max(capital, 10) if capital > peak: peak = capital dd = (peak - capital) / peak max_dd = max(max_dd, dd) total_bars += brk_bars all_t += 1 if is_correct: all_w += 1 year = ts.iloc[i].year if year not in yearly: yearly[year] = {"w": 0, "t": 0, "pnls": []} yearly[year]["t"] += 1 if is_correct: yearly[year]["w"] += 1 yearly[year]["pnls"].append(net * INITIAL) start += STEP_SIZE if all_t == 0: return None return _build_result(yearly, capital, max_dd, all_t, all_w, total_bars / n * 100, brk_bars * TF_MINUTES.get(tf, 60) / 60) # ═══════════════════════════════════════════════════════════════════ # ESECUZIONE # ═══════════════════════════════════════════════════════════════════ print("Calcolo in corso...\n") strategies = [] def add(name, desc, cat, result): if result and result["trades"] >= 20: strategies.append({"name": name, "desc": desc, "cat": cat, **result}) # ── S1: Squeeze puro ──────────────────────────────────────────── add("S1 Squeeze BTC 15m", "Squeeze breakout puro, BBw=14, hold 3×15m, leva 3x", "S1", run_s1_squeeze("BTC", "15m")) add("S1 Squeeze ETH 15m", "Squeeze breakout puro, BBw=14, hold 3×15m, leva 3x", "S1", run_s1_squeeze("ETH", "15m")) add("S1 Squeeze BTC 1h", "Squeeze breakout puro, BBw=14, hold 3×1h, leva 3x", "S1", run_s1_squeeze("BTC", "1h")) add("S1 Squeeze ETH 1h", "Squeeze breakout puro, BBw=14, hold 3×1h, leva 3x", "S1", run_s1_squeeze("ETH", "1h")) add("S1 AF+Vol BTC 15m", "Squeeze + antifakeout + volume confirm >1.3× media", "S1", run_s1_antifake_vol("BTC", "15m")) add("S1 AF+Vol ETH 15m", "Squeeze + antifakeout + volume confirm >1.3× media", "S1", run_s1_antifake_vol("ETH", "15m")) add("S1 AF+Vol BTC 1h", "Squeeze + antifakeout + volume confirm >1.3× media", "S1", run_s1_antifake_vol("BTC", "1h")) add("S1 AF+Vol ETH 1h", "Squeeze + antifakeout + volume confirm >1.3× media", "S1", run_s1_antifake_vol("ETH", "1h")) # ── Script 13: Squeeze + ML (GBM walk-forward) ───────────────── print(" Training ML models...") add("S13 ETH 15m bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.70, 3x leva 15% pos", "S13", run_s13_hybrid("ETH", "15m", 14, 0.8, 3, 3, 0.15, 0.70)) add("S13 ETH 15m bb14 ml65", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.65, 3x leva 15% pos", "S13", run_s13_hybrid("ETH", "15m", 14, 0.8, 3, 3, 0.15, 0.65)) add("S13 ETH 15m bb20 ml70", "Squeeze+GBM walk-forward, BBw=20 sq=0.9 ml≥0.70, 3x leva 15% pos", "S13", run_s13_hybrid("ETH", "15m", 20, 0.9, 3, 3, 0.15, 0.70)) add("S13 BTC 15m bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.9 ml≥0.70, 3x leva 15% pos", "S13", run_s13_hybrid("BTC", "15m", 14, 0.9, 3, 3, 0.15, 0.70)) add("S13 BTC 15m bb14 ml65", "Squeeze+GBM walk-forward, BBw=14 sq=0.9 ml≥0.65, 3x leva 15% pos", "S13", run_s13_hybrid("BTC", "15m", 14, 0.9, 3, 3, 0.15, 0.65)) add("S13 BTC 1h bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.70, 3x leva 20% pos", "S13", run_s13_hybrid("BTC", "1h", 14, 0.8, 3, 3, 0.20, 0.70)) add("S13 BTC 1h bb14 ml65", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.65, 3x leva 20% pos", "S13", run_s13_hybrid("BTC", "1h", 14, 0.8, 3, 3, 0.20, 0.65)) add("S13 ETH 1h bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.70, 3x leva 20% pos", "S13", run_s13_hybrid("ETH", "1h", 14, 0.8, 3, 3, 0.20, 0.70)) strategies.sort(key=lambda x: x["acc"], reverse=True) # ═══════════════════════════════════════════════════════════════════ # TABELLA 1: Classifica # ═══════════════════════════════════════════════════════════════════ W = 150 print("=" * W) print(" S1 (SQUEEZE PURO) vs S13 (SQUEEZE + GBM) — CLASSIFICA FINALE") print(f" Fee: 0.2% RT. Dati OHLCV reali 2018-2026. Position 15%. Leva 3x.") print("=" * W) hdr = (f" {'#':>2s} {'Cat':>3s} {'Nome':<26s} {'Trades':>6s} {'Acc':>6s} " f"{'PnL€':>9s} {'DD%':>6s} {'€/day':>7s} {'Sharpe':>7s} " f"{'Mkt%':>5s} {'Dur':>6s} {'Worst':>12s} {'Anni':>4s}") print(hdr) print(f" {'─'*(W-4)}") for idx, s in enumerate(strategies, 1): worst = f"{s['worst_year']}({s['worst_acc']:.0f}%)" dur_str = f"{s['avg_dur_h']:.0f}h" if s['avg_dur_h'] >= 1 else f"{s['avg_dur_h']*60:.0f}m" tag = " ★★" if s["acc"] >= 78 else " ★" if s["acc"] >= 76 else "" print(f" {idx:>2d} {s['cat']:>3s} {s['name']:<26s} {s['trades']:>6d} {s['acc']:>5.1f}% " f"€{s['pnl']:>+8.0f} {s['max_dd']:>5.1f}% {s['daily_pnl']:>+6.2f} {s['sharpe']:>7.2f} " f"{s['time_in_market_pct']:>4.1f}% {dur_str:>6s} {worst:>12s} {s['years_active']:>4d}{tag}") # ═══════════════════════════════════════════════════════════════════ # TABELLA 2: Descrizione # ═══════════════════════════════════════════════════════════════════ print(f"\n\n{'=' * W}") print(" DESCRIZIONE") print(f"{'=' * W}") print(f" {'#':>2s} {'Nome':<26s} {'Descrizione'}") print(f" {'─'*(W-4)}") for idx, s in enumerate(strategies, 1): print(f" {idx:>2d} {s['name']:<26s} {s['desc']}") # ═══════════════════════════════════════════════════════════════════ # TABELLA 3: Breakdown per anno # ═══════════════════════════════════════════════════════════════════ top_n = min(12, len(strategies)) top = strategies[:top_n] all_years = sorted(set(y for s in top for y in s["year_details"])) print(f"\n\n{'=' * W}") print(f" BREAKDOWN PER ANNO — TOP {top_n} (accuracy% / trades)") print(f"{'=' * W}") header = f" {'Nome':<26s}" for y in all_years: header += f" {y:>10d}" print(header) print(f" {'─'*(W-4)}") for s in top: line = f" {s['name']:<26s}" for y in all_years: if y in s["year_details"]: d = s["year_details"][y] line += f" {d['acc']:>4.0f}%/{d['trades']:<4d}" else: line += f" {'—':>10s}" print(line) # ═══════════════════════════════════════════════════════════════════ # TABELLA 4: Robustezza # ═══════════════════════════════════════════════════════════════════ print(f"\n\n{'=' * W}") print(f" ANALISI ROBUSTEZZA") print(f"{'=' * W}") print(f" {'#':>2s} {'Nome':<26s} {'MinAcc':>7s} {'MaxAcc':>7s} {'Spread':>7s} " f"{'AnniOK':>7s} {'€/trade':>8s} {'Verdict':<12s}") print(f" {'─'*90}") for idx, s in enumerate(strategies, 1): yd = s["year_details"] valid = {y: d for y, d in yd.items() if d["trades"] >= 10} accs = [d["acc"] for d in (valid if valid else yd).values()] if not accs: continue min_a, max_a = min(accs), max(accs) spread = max_a - min_a years_ok = sum(1 for a in accs if a >= 70) avg_pnl = s["pnl"] / s["trades"] if s["trades"] > 0 else 0 n_valid = len(valid if valid else yd) if n_valid < 4: verdict = "⚠ CORTO" elif min_a < 60: verdict = "⚠ FRAGILE" elif min_a >= 72 and s["acc"] >= 77: verdict = "✅ SOLIDO" elif min_a >= 65 and s["acc"] >= 74: verdict = "~ BUONO" else: verdict = "~ OK" print(f" {idx:>2d} {s['name']:<26s} {min_a:>6.1f}% {max_a:>6.1f}% {spread:>6.1f}% " f"{years_ok:>3d}/{n_valid:<3d} €{avg_pnl:>+7.1f} {verdict:<12s}") # ═══════════════════════════════════════════════════════════════════ # VERDETTO # ═══════════════════════════════════════════════════════════════════ print(f"\n\n{'=' * W}") print(f" VERDETTO FINALE") print(f"{'=' * W}") solidi = [s for s in strategies if s["trades"] >= 200 and s["years_active"] >= 5 and s["worst_acc"] >= 65] solidi_s1 = [s for s in solidi if s["cat"] == "S1"] solidi_ml = [s for s in solidi if s["cat"] == "S13"] solidi_s1.sort(key=lambda x: x["acc"], reverse=True) solidi_ml.sort(key=lambda x: x["daily_pnl"], reverse=True) if solidi_s1: b = solidi_s1[0] print(f"\n MIGLIORE S1 (regole pure, facile da deployare):") print(f" {b['name']} — {b['acc']:.1f}% acc, {b['trades']} trades, DD {b['max_dd']:.1f}%, €{b['daily_pnl']:+.2f}/day, Sharpe {b['sharpe']:.2f}") if solidi_ml: m = solidi_ml[0] print(f"\n MIGLIORE S13 (squeeze+GBM, più complesso):") print(f" {m['name']} — {m['acc']:.1f}% acc, {m['trades']} trades, DD {m['max_dd']:.1f}%, €{m['daily_pnl']:+.2f}/day, Sharpe {m['sharpe']:.2f}") max_pnl = max(strategies, key=lambda x: x["pnl"]) print(f"\n MAX PnL: {max_pnl['name']} — €{max_pnl['pnl']:+,.0f}")