Compare commits
3 Commits
0ab3b5698a
...
fa2d74be77
| Author | SHA1 | Date | |
|---|---|---|---|
| fa2d74be77 | |||
| 041db2191c | |||
| 185ac0d49b |
@@ -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}")
|
||||||
Reference in New Issue
Block a user