Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fa2d74be77 | |||
| 041db2191c | |||
| 185ac0d49b | |||
| 0ab3b5698a | |||
| 7639e5012b | |||
| 2694a4a00c | |||
| a7b3c3c203 |
@@ -5,6 +5,8 @@ services:
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- PYTHONUNBUFFERED=1
|
||||
healthcheck:
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
"""Confronto migliori strategie S1 e S2 — andamento per anno."""
|
||||
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
|
||||
|
||||
FEE_PERP = 0.002 # 0.1% taker roundtrip perpetual
|
||||
FEE_OPT = 0.0052 # options roundtrip
|
||||
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 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 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
|
||||
result[i+1] = 100 if al == 0 else 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
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# S1 BEST: Squeeze Breakout ETH 1h (BBw=14, sq=0.8, brk=3)
|
||||
# =====================================================================
|
||||
def run_s1_squeeze(asset, tf):
|
||||
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)
|
||||
|
||||
yearly = {}
|
||||
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
|
||||
if i - sq_start < 5 or i + 3 >= n:
|
||||
continue
|
||||
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
|
||||
actual = (c[i+2] - c[i-1]) / c[i-1]
|
||||
trade_ret = actual * direction
|
||||
net = trade_ret * LEVERAGE - FEE_PERP * LEVERAGE
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"pnls": [], "wins": 0, "total": 0}
|
||||
yearly[year]["pnls"].append(net)
|
||||
yearly[year]["total"] += 1
|
||||
if trade_ret > 0:
|
||||
yearly[year]["wins"] += 1
|
||||
|
||||
return yearly
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# S1 BEST ALT: Squeeze+ML hybrid ETH 15m
|
||||
# =====================================================================
|
||||
# Troppo complesso da ricalcolare (serve ML training). Uso i dati S1 squeeze puro.
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# S2 BEST: VRP ETH 48h (con IV stimata, unico disponibile su 8 anni)
|
||||
# =====================================================================
|
||||
def run_s2_vrp(asset, dte=48):
|
||||
df = load_data(asset, "1h")
|
||||
c = df["close"].values
|
||||
n = len(c)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
rv_24 = rv_ann(c, 24)
|
||||
rv_168 = rv_ann(c, 168)
|
||||
|
||||
yearly = {}
|
||||
for i in range(170, n - dte):
|
||||
if ts.iloc[i].hour != 8:
|
||||
continue
|
||||
rv_s, rv_l = rv_24[i], rv_168[i]
|
||||
if np.isnan(rv_s) or np.isnan(rv_l) or rv_s < 0.05 or rv_l < 0.05:
|
||||
continue
|
||||
regime = rv_s / rv_l
|
||||
iv_pf = 0.9 if regime > 2 else (1.0 if regime > 1.5 else (1.1 if regime > 1 else 1.2))
|
||||
iv = rv_l * iv_pf
|
||||
prem = iv * np.sqrt(dte/(24*365)) * 0.8
|
||||
spot = c[i]
|
||||
move = abs(c[min(i+dte, n-1)] - spot) / spot
|
||||
pos = 0.10
|
||||
raw = (prem - move) * pos if move <= prem else max(-(move-prem)*pos, -pos*0.05)
|
||||
net = raw - FEE_OPT * pos
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"pnls": [], "wins": 0, "total": 0}
|
||||
yearly[year]["pnls"].append(net)
|
||||
yearly[year]["total"] += 1
|
||||
if raw > 0:
|
||||
yearly[year]["wins"] += 1
|
||||
return yearly
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# S2 BEST PERPETUAL: Multi-TF 15m+1h BTC
|
||||
# =====================================================================
|
||||
def run_s2_multitf(asset):
|
||||
df_1h = load_data(asset, "1h")
|
||||
df_15m = load_data(asset, "15m")
|
||||
c1h = df_1h["close"].values
|
||||
ts1h = pd.to_datetime(df_1h["timestamp"], unit="ms", utc=True)
|
||||
c15 = df_15m["close"].values
|
||||
ts15 = df_15m["timestamp"].values
|
||||
n15 = len(c15)
|
||||
|
||||
ema_50 = ema(c1h, 50)
|
||||
rsi_15m = rsi(c15, 14)
|
||||
|
||||
yearly = {}
|
||||
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
|
||||
if rsi_15m[i] > 35 and rsi_15m[i] < 65:
|
||||
continue
|
||||
h_idx = np.searchsorted(ts1h.values.astype("int64"), ts15[i]) - 1
|
||||
if h_idx < 50 or h_idx >= len(c1h) or np.isnan(ema_50[h_idx]):
|
||||
continue
|
||||
|
||||
direction = None
|
||||
if rsi_15m[i] < 30 and c1h[h_idx] > ema_50[h_idx]:
|
||||
direction = "long"
|
||||
elif rsi_15m[i] > 70 and c1h[h_idx] < ema_50[h_idx]:
|
||||
direction = "short"
|
||||
if direction is None:
|
||||
continue
|
||||
|
||||
entry = c15[i]
|
||||
exit_price = c15[min(i+12, n15-1)]
|
||||
trade_ret = (exit_price-entry)/entry if direction == "long" else (entry-exit_price)/entry
|
||||
net = trade_ret * LEVERAGE - FEE_PERP * LEVERAGE
|
||||
|
||||
year = ts_dt.year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"pnls": [], "wins": 0, "total": 0}
|
||||
yearly[year]["pnls"].append(net)
|
||||
yearly[year]["total"] += 1
|
||||
if trade_ret > 0:
|
||||
yearly[year]["wins"] += 1
|
||||
daily_done.add(day)
|
||||
return yearly
|
||||
|
||||
|
||||
# =====================================================================
|
||||
# REPORT
|
||||
# =====================================================================
|
||||
strategies = {
|
||||
"S1: Squeeze BTC 1h": run_s1_squeeze("BTC", "1h"),
|
||||
"S1: Squeeze ETH 1h": run_s1_squeeze("ETH", "1h"),
|
||||
"S1: Squeeze ETH 15m": run_s1_squeeze("ETH", "15m"),
|
||||
"S2: VRP ETH 48h (IV est)": run_s2_vrp("ETH", 48),
|
||||
"S2: VRP BTC 48h (IV est)": run_s2_vrp("BTC", 48),
|
||||
"S2: MultiTF BTC 15m+1h": run_s2_multitf("BTC"),
|
||||
"S2: MultiTF ETH 15m+1h": run_s2_multitf("ETH"),
|
||||
}
|
||||
|
||||
all_years = sorted(set(y for v in strategies.values() for y in v))
|
||||
|
||||
print("=" * 120)
|
||||
print(" MIGLIORI STRATEGIE — ANDAMENTO PER ANNO")
|
||||
print(" Fee reali. PnL su €1000 flat (no compounding). Dati OHLCV reali 2018-2026.")
|
||||
print(" ⚠ VRP usa IV STIMATA (non reale) — fidarsi solo dei dati perpetual per backtest lungo")
|
||||
print("=" * 120)
|
||||
|
||||
# Header
|
||||
hdr = f" {'Anno':>6s}"
|
||||
for name in strategies:
|
||||
short = name.split(": ")[1][:18]
|
||||
hdr += f" | {short:>18s}"
|
||||
print(hdr)
|
||||
print(f" {'-' * (len(hdr) - 2)}")
|
||||
|
||||
# Per anno: accuracy / PnL totale
|
||||
for year in all_years:
|
||||
row_acc = f" {year:>6d}"
|
||||
row_pnl = f" {'':>6s}"
|
||||
for name, yearly in strategies.items():
|
||||
if year in yearly:
|
||||
d = yearly[year]
|
||||
acc = d["wins"]/d["total"]*100 if d["total"] > 0 else 0
|
||||
pnl = sum(d["pnls"]) * INITIAL
|
||||
tag = "▓" if acc >= 75 else "▒" if acc >= 65 else "░" if acc >= 55 else " "
|
||||
row_acc += f" | {acc:>5.1f}% {tag} {d['total']:>3d}t"
|
||||
row_pnl += f" | €{pnl:>+8.0f} "
|
||||
else:
|
||||
row_acc += f" | {'—':>18s}"
|
||||
row_pnl += f" | {'':>18s}"
|
||||
print(row_acc)
|
||||
print(row_pnl)
|
||||
|
||||
# Totali
|
||||
print(f" {'-' * (len(hdr) - 2)}")
|
||||
row_tot = f" {'TOT':>6s}"
|
||||
for name, yearly in strategies.items():
|
||||
all_pnls = [p for d in yearly.values() for p in d["pnls"]]
|
||||
all_wins = sum(d["wins"] for d in yearly.values())
|
||||
all_total = sum(d["total"] for d in yearly.values())
|
||||
acc = all_wins/all_total*100 if all_total > 0 else 0
|
||||
pnl = sum(all_pnls) * INITIAL
|
||||
row_tot += f" | {acc:>5.1f}% {all_total:>4d}t"
|
||||
print(row_tot)
|
||||
|
||||
row_pnl_tot = f" {'€TOT':>6s}"
|
||||
for name, yearly in strategies.items():
|
||||
all_pnls = [p for d in yearly.values() for p in d["pnls"]]
|
||||
pnl = sum(all_pnls) * INITIAL
|
||||
row_pnl_tot += f" | €{pnl:>+8.0f} "
|
||||
print(row_pnl_tot)
|
||||
|
||||
# Compounding
|
||||
print(f"\n {'':>6s}", end="")
|
||||
for name in strategies:
|
||||
short = name.split(": ")[1][:18]
|
||||
print(f" | {short:>18s}", end="")
|
||||
print()
|
||||
|
||||
row_comp = f" {'COMP':>6s}"
|
||||
for name, yearly in strategies.items():
|
||||
cap = float(INITIAL)
|
||||
for year in sorted(yearly):
|
||||
for pnl in yearly[year]["pnls"]:
|
||||
cap += cap * pnl
|
||||
cap = max(cap, 10)
|
||||
row_comp += f" | €{cap:>12,.0f} "
|
||||
print(row_comp)
|
||||
|
||||
# Drawdown
|
||||
row_dd = f" {'MAXDD':>6s}"
|
||||
for name, yearly in strategies.items():
|
||||
cap = float(INITIAL)
|
||||
peak = cap
|
||||
mdd = 0
|
||||
for year in sorted(yearly):
|
||||
for pnl in yearly[year]["pnls"]:
|
||||
cap += cap * pnl
|
||||
cap = max(cap, 10)
|
||||
if cap > peak: peak = cap
|
||||
dd = (peak - cap) / peak
|
||||
mdd = max(mdd, dd)
|
||||
row_dd += f" | {mdd*100:>12.1f}% "
|
||||
print(row_dd)
|
||||
|
||||
# Legenda
|
||||
print(f"\n Legenda: ▓ ≥75% acc ▒ ≥65% acc ░ ≥55% acc")
|
||||
print(f" ⚠ S2 VRP: IV stimata (rv_long × 1.0-1.2), NON dati reali opzioni")
|
||||
print(f" S1 Squeeze e S2 MultiTF: dati OHLCV reali al 100%")
|
||||
@@ -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}")
|
||||
@@ -10,6 +10,7 @@ import pandas as pd
|
||||
|
||||
from src.live.cerbero_client import CerberoClient
|
||||
from src.live.signal_engine import SignalEngine
|
||||
from src.live.telegram_notifier import notify_event
|
||||
|
||||
LOG_DIR = Path(__file__).resolve().parents[2] / "data" / "paper_trades"
|
||||
INSTRUMENT = "ETH_USDC-PERPETUAL"
|
||||
@@ -52,6 +53,7 @@ class PaperTrader:
|
||||
with open(self.log_path, "a") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
print(f" [{entry['timestamp'][:19]}] {event}: {json.dumps(data or {})}")
|
||||
notify_event(event, data)
|
||||
|
||||
def save_status(self):
|
||||
status = {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"""Notifiche Telegram per il paper trader."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
import json
|
||||
|
||||
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "")
|
||||
CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "")
|
||||
|
||||
NOTIFY_EVENTS = {
|
||||
"SIGNAL", "OPENED", "CLOSED", "OPEN_FAILED", "CLOSE_FAILED",
|
||||
"ERROR", "STARTUP", "SHUTDOWN", "TRAINING_FAILED",
|
||||
}
|
||||
|
||||
|
||||
def send_telegram(text: str) -> bool:
|
||||
if not BOT_TOKEN or not CHAT_ID:
|
||||
return False
|
||||
try:
|
||||
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
|
||||
data = urllib.parse.urlencode({"chat_id": CHAT_ID, "text": text, "parse_mode": "HTML"}).encode()
|
||||
urllib.request.urlopen(url, data, timeout=10)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def notify_event(event: str, data: dict | None = None):
|
||||
if event not in NOTIFY_EVENTS:
|
||||
return
|
||||
lines = [f"📊 <b>{event}</b>"]
|
||||
if data:
|
||||
for k, v in data.items():
|
||||
if k in ("signal",):
|
||||
continue
|
||||
lines.append(f" {k}: {v}")
|
||||
send_telegram("\n".join(lines))
|
||||
@@ -542,30 +542,30 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cuda-bindings"
|
||||
version = "13.2.0"
|
||||
version = "13.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/a9/3a8241c6e19483ac1f1dcf5c10238205dcb8a6e9d0d4d4709240dff28ff4/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:721104c603f059780d287969be3d194a18d0cc3b713ed9049065a1107706759d", size = 5730273, upload-time = "2026-03-11T00:12:37.18Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/94/2748597f47bb1600cd466b20cab4159f1530a3a33fe7f70fee199b3abb9e/cuda_bindings-13.2.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1eba9504ac70667dd48313395fe05157518fd6371b532790e96fbb31bbb5a5e1", size = 6313924, upload-time = "2026-03-11T00:12:39.462Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/52/50673d25e46d199556f827514bf646a49471d50538c5e577201245b348a9/cuda_bindings-13.3.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:244a167d81a9d07d3209d02c8e02e848c2b7c38f4d01e8e4d1f9620b173ae006", size = 6051409, upload-time = "2026-05-27T03:59:01.648Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/ee/e8f4bdfb808c3689539b7c035d63b6dac9f236b2d6f807f18c7f5f3ef879/cuda_bindings-13.3.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:820ab45be3f39a088f39c4a04eb8b852d0b339bff8c518da5c258882b8a4e21b", size = 6671833, upload-time = "2026-05-27T03:59:03.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/e0/4b3fdba08ff177e9451f376a4ba2df18d76f9158e6a16cdc062bd83db9fa/cuda_bindings-13.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f72f67f5790e7fa51e3f893e007e49567573ccdf4ed1ca988fbbbd36cd77847c", size = 6020531, upload-time = "2026-05-27T03:59:07.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/40/a2ea4d8f032bfd6c220d50b6f92cd61f33d48f31959da39ed1b178cfee54/cuda_bindings-13.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a99c3b8d584f266c616bd0f30c7cd83e33553e3ef2abad41ff5a74fbc033a69a", size = 6653764, upload-time = "2026-05-27T03:59:09.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/a0/156efe7816699c2de1ea2395031db7d010b7af23c243563a3ee6f0ecc1de/cuda_bindings-13.3.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7698fcc4577aa96372866f4d0c9a6cf686cd5c90eab94581c29d37fe6600542", size = 5914803, upload-time = "2026-05-27T03:59:14.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/91/510aae64d53227b5b36db6bfaea41514b66d92cd65ddc43aa49566f18313/cuda_bindings-13.3.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:abd908f651160d12c45c5714a38ee102a1173a55433c0d1509ec0e8293beb4a6", size = 6472506, upload-time = "2026-05-27T03:59:16.551Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/53/2ef49e5b3734a5531b2ba5d726cba724d9cbb262404e586ed61070604826/cuda_bindings-13.3.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a801fa30e75d25b74252123aefc746b6c4275624d2b8640632dd1dfeeaa1f88", size = 6008814, upload-time = "2026-05-27T03:59:20.921Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/cb/3a9fcf0651e0a49b4d0f1955837ce079245b27086c22fb2f253039bdf324/cuda_bindings-13.3.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94d40ef7b4bdd9dce0244a1baa132e0e538f1eb2c0d162fb3648a15e48515365", size = 6531477, upload-time = "2026-05-27T03:59:23.391Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/6987c5ee98f117317a85650ddc79480a3fa59a573ae1c923d0722b56ae71/cuda_bindings-13.3.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5911bea15810b749a8077f8c45423ed785d51618b8e8664dea1fc8f5a2a76c8", size = 5807073, upload-time = "2026-05-27T03:59:28.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/ab/46ceee07dc19f18a5d1c28d592750ed9dbdc803077eb083576a442c9938c/cuda_bindings-13.3.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e2870fed7707a37f8af0c02364b05f355ebe8921604e8c68eb56cf66867e0798", size = 6354325, upload-time = "2026-05-27T03:59:30.715Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cuda-pathfinder"
|
||||
version = "1.5.4"
|
||||
version = "1.5.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/c8/26f2e4aae92f11522a96043892ba39a90eac610d5242523aa863212bc1c7/cuda_pathfinder-1.5.5-py3-none-any.whl", hash = "sha256:0228c023f95d1480f143ef5c8922d27a2ab052087a942e81dc289c9eb8f91689", size = 51671, upload-time = "2026-05-27T01:21:25.413Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user