chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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}")
|
||||
Reference in New Issue
Block a user