7 Commits

Author SHA1 Message Date
Adriano fa2d74be77 feat(strategy3): ultimate squeeze — BTC 15m antifake+vol 79.7%, antifake+corr 81.6%
Top results con dati reali:
- BTC 15m antifake+vol: 79.7% acc, 1250 trades, DD 6.5%
- ETH 15m antifake+vol: 78.5% acc, 941 trades, DD 3.4%
- BTC 15m antifake+corr: 81.6% acc, 376 trades (pochi anni)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:25:22 +02:00
Adriano 041db2191c test(strategy3): lead-lag multi-asset — leader-follower fallito, corr-weighted 76.8%
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:22:44 +02:00
Adriano 185ac0d49b feat(strategy3): squeeze migliorato — BTC 15m ALL_FILTERS 79.2% acc
Cross-asset + timing + long_squeeze + dual_tf + anti_fakeout.
Worst year: 2021 76.8%. Tutti gli anni profittevoli.
ETH 15m long_squeeze: 77.9% acc. BTC 1h anti_fakeout: 76.3%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:20:44 +02:00
Adriano 0ab3b5698a docs: confronto migliori strategie S1/S2 per anno, dati reali
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 11:12:47 +02:00
Adriano 7639e5012b Merge branch 'main' of ssh://git.tielogic.xyz:222/Adriano/PythagorasGoal
# Conflicts:
#	uv.lock
2026-05-27 11:09:52 +02:00
Adriano Dal Pastro 2694a4a00c feat: notifiche Telegram dal paper trader via bot
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:52:11 +00:00
Adriano Dal Pastro a7b3c3c203 infra: add uv.lock per build Docker riproducibili
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 08:19:15 +00:00
8 changed files with 1228 additions and 13 deletions
+2
View File
@@ -5,6 +5,8 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- ./data:/app/data - ./data:/app/data
env_file:
- .env
environment: environment:
- PYTHONUNBUFFERED=1 - PYTHONUNBUFFERED=1
healthcheck: healthcheck:
+309
View File
@@ -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%")
+317
View File
@@ -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}")
+290
View File
@@ -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}")
+256
View File
@@ -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}")
+2
View File
@@ -10,6 +10,7 @@ import pandas as pd
from src.live.cerbero_client import CerberoClient from src.live.cerbero_client import CerberoClient
from src.live.signal_engine import SignalEngine 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" LOG_DIR = Path(__file__).resolve().parents[2] / "data" / "paper_trades"
INSTRUMENT = "ETH_USDC-PERPETUAL" INSTRUMENT = "ETH_USDC-PERPETUAL"
@@ -52,6 +53,7 @@ class PaperTrader:
with open(self.log_path, "a") as f: with open(self.log_path, "a") as f:
f.write(json.dumps(entry) + "\n") f.write(json.dumps(entry) + "\n")
print(f" [{entry['timestamp'][:19]}] {event}: {json.dumps(data or {})}") print(f" [{entry['timestamp'][:19]}] {event}: {json.dumps(data or {})}")
notify_event(event, data)
def save_status(self): def save_status(self):
status = { status = {
+39
View File
@@ -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))
Generated
+13 -13
View File
@@ -542,30 +542,30 @@ wheels = [
[[package]] [[package]]
name = "cuda-bindings" name = "cuda-bindings"
version = "13.2.0" version = "13.3.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
] ]
wheels = [ 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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]] [[package]]
name = "cuda-pathfinder" name = "cuda-pathfinder"
version = "1.5.4" version = "1.5.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ 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]] [[package]]