"""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}")