refactor: riorganizzazione script — Strategy ABC, folder strategies/waste/analysis
- src/strategies/base.py: Strategy ABC con Signal, BacktestResult, YearlyStats - src/strategies/indicators.py: keltner_ratio, detect_squeezes, ema, atr, rv, corr - scripts/strategies/: SQ01-SQ04 (squeeze puro/filtri), ML01 (squeeze+GBM) - scripts/waste/: W01-W22 script scartati + REF originali - scripts/analysis/: compare, best_yearly, final_report, paper_status - CLAUDE.md aggiornato con nuova struttura e tabella strategie Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
"""ML01 — Squeeze + GBM (Gradient Boosting Machine) Walk-Forward.
|
||||
|
||||
Strategia ibrida: squeeze breakout come pre-filtro (QUANDO tradare),
|
||||
GradientBoosting su features strutturali come conferma (QUALE direzione).
|
||||
|
||||
Pipeline:
|
||||
1. Rileva squeeze release (Bollinger esce da Keltner)
|
||||
2. Estrai 44 features dalla finestra (structural multi-window + squeeze
|
||||
metadata + price position + ATR + momentum breakout)
|
||||
3. GBM walk-forward: train su 50% rolling, step 10%, predice direzione
|
||||
4. Trade solo se ML ha confidenza ≥ ml_threshold
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame
|
||||
- Parametri: bb_window (14), sq_threshold (0.8), brk_bars (3),
|
||||
ml_threshold (0.70), leverage (3), position_pct (0.15)
|
||||
|
||||
OUT:
|
||||
- BacktestResult con metriche walk-forward (no data leakage)
|
||||
- Solo periodo di test (seconda metà dati)
|
||||
|
||||
Risultati tipici:
|
||||
ETH 15m bb14 ml=0.70: 76.9% acc, 1213 trades, DD 4.2%, €13.78/day
|
||||
BTC 15m bb14 ml=0.70: 78.8% acc, 1964 trades, DD 7.0%, €5.51/day
|
||||
BTC 1h bb14 ml=0.70: 77.3% acc, 617 trades, DD 6.7%, €3.85/day
|
||||
|
||||
Note:
|
||||
- GBM = GradientBoostingClassifier di scikit-learn
|
||||
- Walk-forward: nessun look-ahead, train sempre prima di test
|
||||
- Il baseline squeeze puro ha accuracy più alta (~79.5%) ma DD peggiore
|
||||
- Il valore del ML è filtrare breakout deboli → DD ridotto
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
|
||||
from src.strategies.base import Strategy, Signal, BacktestResult, YearlyStats, TF_MINUTES
|
||||
from src.strategies.indicators import keltner_ratio, detect_squeezes
|
||||
from src.data.downloader import load_data
|
||||
|
||||
|
||||
def _build_features(df: pd.DataFrame, i: int, squeeze_info: dict) -> np.ndarray | None:
|
||||
"""44 features per il punto di squeeze release."""
|
||||
if i < 100:
|
||||
return None
|
||||
o, h, l, c, v = (df["open"].values, df["high"].values, df["low"].values,
|
||||
df["close"].values, df["volume"].values)
|
||||
feats = []
|
||||
for w in [12, 24, 48]:
|
||||
wc, wo = c[i-w:i], o[i-w:i]
|
||||
wh, wl, wv = h[i-w:i], l[i-w:i], v[i-w:i]
|
||||
mn, mx = wl.min(), max(wh.max(), wc.max())
|
||||
rng = mx - mn if mx - mn > 0 else 1e-10
|
||||
total = np.where(wh - wl == 0, 1e-10, wh - wl)
|
||||
body = np.abs(wc - wo) / total
|
||||
direction = np.sign(wc - wo)
|
||||
log_c = np.log(np.where(wc == 0, 1e-10, wc))
|
||||
rets = np.diff(log_c)
|
||||
v_mean = np.mean(wv)
|
||||
feats.extend([
|
||||
np.mean(rets) if len(rets) > 0 else 0,
|
||||
np.std(rets) if len(rets) > 0 else 0,
|
||||
np.sum(rets) if len(rets) > 0 else 0,
|
||||
float(pd.Series(rets).skew()) if len(rets) > 2 else 0,
|
||||
float(pd.Series(rets).kurtosis()) if len(rets) > 3 else 0,
|
||||
np.mean(body), np.std(body),
|
||||
np.mean(direction), np.mean(direction[-min(3, w):]),
|
||||
(wc[-1] - mn) / rng,
|
||||
wv[-1] / v_mean if v_mean > 0 else 1,
|
||||
np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
|
||||
])
|
||||
sq = squeeze_info
|
||||
feats.extend([
|
||||
sq["dur"], sq["dur"] / 24, sq["kcr_at_release"],
|
||||
v[i-1] / sq.get("avg_vol", 1) if sq.get("avg_vol", 0) > 0 else 1,
|
||||
np.mean(v[i:min(i+3, len(v))]) / sq.get("avg_vol", 1) if sq.get("avg_vol", 0) > 0 else 1,
|
||||
])
|
||||
h48, l48 = np.max(h[max(0, i-48):i]), np.min(l[max(0, i-48):i])
|
||||
r48 = h48 - l48
|
||||
feats.append((c[i-1] - l48) / r48 if r48 > 0 else 0.5)
|
||||
tr = np.maximum(h[i-14:i] - l[i-14:i],
|
||||
np.maximum(np.abs(h[i-14:i] - np.roll(c[i-14:i], 1)),
|
||||
np.abs(l[i-14:i] - np.roll(c[i-14:i], 1))))
|
||||
feats.append(np.mean(tr[1:]) / c[i-1] if c[i-1] > 0 else 0)
|
||||
feats.append((c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0)
|
||||
return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
|
||||
class SqueezeGBM(Strategy):
|
||||
name = "ML01_squeeze_gbm"
|
||||
description = "Squeeze + GBM walk-forward — ML filtra breakout deboli"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
fee_ml = 0.001
|
||||
|
||||
def generate_signals(self, df, ts, **params):
|
||||
raise NotImplementedError("ML01 usa backtest custom con walk-forward")
|
||||
|
||||
def backtest(self, asset: str, tf: str, hold: int = 3, **params) -> BacktestResult | None:
|
||||
bb_w = params.get("bb_window", 14)
|
||||
sq_thr = params.get("sq_threshold", 0.8 if tf == "1h" else 0.9)
|
||||
brk = params.get("brk_bars", hold)
|
||||
ml_thr = params.get("ml_threshold", 0.70)
|
||||
lev = params.get("leverage", self.leverage)
|
||||
pos = params.get("position_pct", self.position_size)
|
||||
|
||||
df = load_data(asset, tf)
|
||||
close = df["close"].values
|
||||
high = df["high"].values
|
||||
low = df["low"].values
|
||||
volume = df["volume"].values
|
||||
n = len(df)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
|
||||
kcr = keltner_ratio(close, high, low, bb_w)
|
||||
raw_events = detect_squeezes(close, high, low, kcr, sq_thr)
|
||||
|
||||
# Aggiungi avg_vol a ogni evento
|
||||
events = []
|
||||
for ev in raw_events:
|
||||
ev["avg_vol"] = float(np.mean(volume[ev["sq_start"]:ev["idx"]]))
|
||||
events.append(ev)
|
||||
|
||||
X_all, y_all, ev_all = [], [], []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i + brk >= n or i < 100:
|
||||
continue
|
||||
feats = _build_features(df, i, ev)
|
||||
if feats is None:
|
||||
continue
|
||||
actual_ret = (close[i + brk - 1] - close[i - 1]) / close[i - 1]
|
||||
X_all.append(feats)
|
||||
y_all.append(1 if actual_ret > 0 else 0)
|
||||
ev_all.append(ev)
|
||||
|
||||
if len(X_all) < 50:
|
||||
return None
|
||||
|
||||
X, y = np.array(X_all), np.array(y_all)
|
||||
TRAIN_SIZE = max(int(len(X) * 0.5), 50)
|
||||
STEP_SIZE = max(int(len(X) * 0.1), 10)
|
||||
|
||||
yearly: dict[int, dict] = {}
|
||||
capital = float(self.initial_capital)
|
||||
peak = capital
|
||||
max_dd = 0.0
|
||||
total_bars = 0
|
||||
all_t = all_w = 0
|
||||
|
||||
start = 0
|
||||
while start + TRAIN_SIZE + STEP_SIZE <= len(X):
|
||||
train_end = start + TRAIN_SIZE
|
||||
test_end = min(train_end + STEP_SIZE, len(X))
|
||||
X_tr, y_tr = X[start:train_end], y[start:train_end]
|
||||
X_te = X[train_end:test_end]
|
||||
|
||||
if len(np.unique(y_tr)) < 2:
|
||||
start += STEP_SIZE
|
||||
continue
|
||||
|
||||
scaler = StandardScaler()
|
||||
X_tr_s = scaler.fit_transform(X_tr)
|
||||
X_te_s = scaler.transform(X_te)
|
||||
|
||||
model = GradientBoostingClassifier(
|
||||
n_estimators=150, max_depth=4, min_samples_leaf=10,
|
||||
learning_rate=0.05, subsample=0.8, random_state=42,
|
||||
)
|
||||
model.fit(X_tr_s, y_tr)
|
||||
up_idx = list(model.classes_).index(1) if 1 in model.classes_ else -1
|
||||
if up_idx < 0:
|
||||
start += STEP_SIZE
|
||||
continue
|
||||
|
||||
for j in range(len(X_te)):
|
||||
proba = model.predict_proba(X_te_s[j:j+1])[0]
|
||||
p_up = proba[up_idx]
|
||||
ev = ev_all[train_end + j]
|
||||
i = ev["idx"]
|
||||
actual_ret = (close[i + brk - 1] - close[i - 1]) / close[i - 1]
|
||||
|
||||
if p_up >= ml_thr:
|
||||
direction = 1
|
||||
elif p_up <= (1 - ml_thr):
|
||||
direction = -1
|
||||
else:
|
||||
continue
|
||||
|
||||
is_correct = (direction == 1 and actual_ret > 0) or (direction == -1 and actual_ret < 0)
|
||||
trade_ret = actual_ret * direction
|
||||
net = trade_ret * lev - self.fee_ml * 2 * lev
|
||||
capital += capital * pos * net
|
||||
capital = max(capital, 10)
|
||||
if capital > peak:
|
||||
peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
total_bars += brk
|
||||
|
||||
all_t += 1
|
||||
if is_correct:
|
||||
all_w += 1
|
||||
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnl": 0.0}
|
||||
yearly[year]["t"] += 1
|
||||
if is_correct:
|
||||
yearly[year]["w"] += 1
|
||||
yearly[year]["pnl"] += net * self.initial_capital
|
||||
|
||||
start += STEP_SIZE
|
||||
|
||||
if all_t == 0:
|
||||
return None
|
||||
|
||||
yearly_stats = [
|
||||
YearlyStats(year=y, trades=d["t"], wins=d["w"], pnl=d["pnl"])
|
||||
for y, d in sorted(yearly.items())
|
||||
]
|
||||
|
||||
return BacktestResult(
|
||||
strategy_name=self.name,
|
||||
asset=asset,
|
||||
timeframe=tf,
|
||||
params={"bb_w": bb_w, "sq_thr": sq_thr, "ml_thr": ml_thr,
|
||||
"brk": brk, "lev": lev, "pos": pos},
|
||||
trades=all_t,
|
||||
wins=all_w,
|
||||
pnl=sum(d["pnl"] for d in yearly.values()),
|
||||
capital=capital,
|
||||
initial_capital=self.initial_capital,
|
||||
max_dd=max_dd * 100,
|
||||
time_in_market_pct=total_bars / n * 100,
|
||||
avg_trade_duration_h=brk * TF_MINUTES.get(tf, 60) / 60,
|
||||
years_active=len(yearly),
|
||||
yearly=yearly_stats,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = SqueezeGBM()
|
||||
print("Training ML models...\n")
|
||||
results = []
|
||||
for asset in ["ETH", "BTC"]:
|
||||
for tf in ["15m", "1h"]:
|
||||
for ml_thr in [0.65, 0.70]:
|
||||
r = strategy.backtest(asset, tf, ml_threshold=ml_thr)
|
||||
if r and r.trades >= 20:
|
||||
r.strategy_name = f"ML01 {asset} {tf} ml={ml_thr}"
|
||||
results.append(r)
|
||||
results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
|
||||
print(f"{'=' * 120}")
|
||||
print(f" ML01 SQUEEZE+GBM — RISULTATI")
|
||||
print(f"{'=' * 120}")
|
||||
for r in results:
|
||||
r.print_summary()
|
||||
if results:
|
||||
results[0].print_yearly()
|
||||
@@ -0,0 +1,68 @@
|
||||
"""SQ01 — Squeeze Breakout Base.
|
||||
|
||||
Strategia strutturale: rileva compressione di volatilità (Bollinger dentro
|
||||
Keltner Channel) e segue la direzione del breakout al rilascio.
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame (da load_data)
|
||||
- Parametri: bb_window (14), sq_threshold (0.8), min_squeeze_dur (5)
|
||||
|
||||
OUT:
|
||||
- Lista di Signal con direzione breakout (+1/-1)
|
||||
- BacktestResult con equity, yearly breakdown, metriche
|
||||
|
||||
Risultati tipici:
|
||||
BTC 15m: 76.7% acc, 4062 trades, DD 6.7%, €9.32/day
|
||||
ETH 15m: 76.4% acc, 2948 trades, DD 6.2%, €10.31/day
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal
|
||||
from src.strategies.indicators import keltner_ratio, detect_squeezes
|
||||
|
||||
|
||||
class SqueezeBase(Strategy):
|
||||
name = "SQ01_squeeze_base"
|
||||
description = "Squeeze breakout puro — segui direzione al rilascio"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
n = len(c)
|
||||
|
||||
bb_w = params.get("bb_window", 14)
|
||||
sq_thr = params.get("sq_threshold", 0.8)
|
||||
min_dur = params.get("min_dur", 5)
|
||||
|
||||
kcr = keltner_ratio(c, h, l, bb_w)
|
||||
events = detect_squeezes(c, h, l, kcr, sq_thr, min_dur)
|
||||
|
||||
signals = []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i < 1 or i >= n:
|
||||
continue
|
||||
first_ret = (c[i] - c[i - 1]) / c[i - 1] if c[i - 1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
signals.append(Signal(
|
||||
idx=i,
|
||||
direction=1 if first_ret > 0 else -1,
|
||||
entry_price=c[i - 1],
|
||||
metadata={"dur": ev["dur"], "kcr": ev["kcr_at_release"]},
|
||||
))
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = SqueezeBase()
|
||||
strategy.report()
|
||||
@@ -0,0 +1,87 @@
|
||||
"""SQ02 — Squeeze Breakout + Anti-Fakeout + Volume Confirmation.
|
||||
|
||||
Migliora SQ01 con due filtri:
|
||||
1. Anti-fakeout: scarta breakout dove la candela ritraccia >60% del range
|
||||
2. Volume confirm: volume al breakout deve essere >1.3× la media durante squeeze
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame
|
||||
- Parametri: bb_window (14), sq_threshold (0.8), retrace_limit (0.6),
|
||||
vol_multiplier (1.3)
|
||||
|
||||
OUT:
|
||||
- Lista di Signal filtrati
|
||||
- BacktestResult
|
||||
|
||||
Risultati tipici:
|
||||
BTC 15m: 79.7% acc, 1250 trades, DD 6.5%, €5.23/day — SOLIDO 9/9 anni
|
||||
ETH 15m: 78.6% acc, 942 trades, DD 3.4%, €4.33/day
|
||||
BTC 1h: 78.0% acc, 473 trades, DD 3.5%, Sharpe 6.57
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal
|
||||
from src.strategies.indicators import keltner_ratio, detect_squeezes
|
||||
|
||||
|
||||
class SqueezeAntifakeVol(Strategy):
|
||||
name = "SQ02_antifake_vol"
|
||||
description = "Squeeze + antifakeout + volume confirmation"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
bb_w = params.get("bb_window", 14)
|
||||
sq_thr = params.get("sq_threshold", 0.8)
|
||||
retrace_limit = params.get("retrace_limit", 0.6)
|
||||
vol_mult = params.get("vol_multiplier", 1.3)
|
||||
|
||||
kcr = keltner_ratio(c, h, l, bb_w)
|
||||
events = detect_squeezes(c, h, l, kcr, sq_thr)
|
||||
|
||||
signals = []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i < 1 or i >= n:
|
||||
continue
|
||||
first_ret = (c[i] - c[i - 1]) / c[i - 1] if c[i - 1] > 0 else 0
|
||||
if abs(first_ret) < 0.001:
|
||||
continue
|
||||
|
||||
br = h[i] - l[i]
|
||||
if br > 0:
|
||||
if c[i] > c[i - 1]:
|
||||
if (h[i] - c[i]) / br > retrace_limit:
|
||||
continue
|
||||
else:
|
||||
if (c[i] - l[i]) / br > retrace_limit:
|
||||
continue
|
||||
|
||||
avg_v = np.mean(v[ev["sq_start"]:i])
|
||||
if avg_v > 0 and v[i] <= avg_v * vol_mult:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i,
|
||||
direction=1 if first_ret > 0 else -1,
|
||||
entry_price=c[i - 1],
|
||||
metadata={"dur": ev["dur"], "vol_ratio": v[i] / avg_v if avg_v > 0 else 0},
|
||||
))
|
||||
return signals
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = SqueezeAntifakeVol()
|
||||
strategy.report()
|
||||
@@ -0,0 +1,175 @@
|
||||
"""SQ03 — Squeeze con filtri selezionabili.
|
||||
|
||||
Ogni filtro è opzionale e attivabile via parametro. Di default attiva solo
|
||||
antifake + long_squeeze (i due filtri con miglior rapporto accuracy/trade).
|
||||
Esegue tutte le combinazioni utili e classifica.
|
||||
|
||||
Filtri disponibili:
|
||||
- antifake: scarta breakout con retrace >60% (guadagna ~+1% acc)
|
||||
- long_sq: solo squeeze durata ≥10 barre (+1% acc, dimezza trade)
|
||||
- timing: solo ore 4-16 UTC (+0.5% acc)
|
||||
- cross: asset secondario in squeeze nelle ultime 10 barre (+0.5%)
|
||||
- vol: volume al breakout >1.3× media squeeze (+1% acc)
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame (primario + secondario per cross-check)
|
||||
- Parametri: filters (lista), bb_window, sq_threshold
|
||||
|
||||
OUT:
|
||||
- BacktestResult per ogni preset di filtri
|
||||
|
||||
Risultati tipici (BTC 15m):
|
||||
antifake+long: 77.3% acc, 2179 trades
|
||||
antifake+vol: 79.7% acc, 1250 trades — SOLIDO
|
||||
ALL_FILTERS: 79.2% acc, 696 trades (restrittivo)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal, BacktestResult, YearlyStats, TF_MINUTES
|
||||
from src.strategies.indicators import keltner_ratio, detect_squeezes
|
||||
from src.data.downloader import load_data
|
||||
|
||||
|
||||
PRESETS = {
|
||||
"antifake": ["antifake"],
|
||||
"long_sq": ["long_sq"],
|
||||
"antifake+long": ["antifake", "long_sq"],
|
||||
"antifake+vol": ["antifake", "vol"],
|
||||
"antifake+timing": ["antifake", "timing"],
|
||||
"long+timing": ["long_sq", "timing"],
|
||||
"antifake+long+time": ["antifake", "long_sq", "timing"],
|
||||
"antifake+cross": ["antifake", "cross"],
|
||||
"ALL_FILTERS": ["antifake", "long_sq", "timing", "cross"],
|
||||
}
|
||||
|
||||
|
||||
class SqueezeFiltered(Strategy):
|
||||
name = "SQ03_filtered"
|
||||
description = "Squeeze + filtri selezionabili (antifake, long, timing, cross, vol)"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
bb_w = params.get("bb_window", 14)
|
||||
sq_thr = params.get("sq_threshold", 0.8)
|
||||
filters = params.get("filters", ["antifake", "long_sq"])
|
||||
asset = params.get("asset", "BTC")
|
||||
tf = params.get("tf", "15m")
|
||||
|
||||
kcr = keltner_ratio(c, h, l, bb_w)
|
||||
events = detect_squeezes(c, h, l, kcr, sq_thr)
|
||||
|
||||
kcr2 = None
|
||||
ts2 = None
|
||||
if "cross" in filters:
|
||||
secondary = "ETH" if asset == "BTC" else "BTC"
|
||||
df2 = load_data(secondary, tf)
|
||||
kcr2 = keltner_ratio(df2["close"].values, df2["high"].values,
|
||||
df2["low"].values, bb_w)
|
||||
ts2 = df2["timestamp"].values
|
||||
|
||||
signals = []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i < 1 or i >= n:
|
||||
continue
|
||||
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
|
||||
|
||||
if "antifake" in filters:
|
||||
br = h[i] - l[i]
|
||||
if br > 0:
|
||||
if c[i] > c[i - 1] and (h[i] - c[i]) / br > 0.6:
|
||||
skip = True
|
||||
elif c[i] <= c[i - 1] and (c[i] - l[i]) / br > 0.6:
|
||||
skip = True
|
||||
|
||||
if not skip and "long_sq" in filters:
|
||||
if ev["dur"] < 10:
|
||||
skip = True
|
||||
|
||||
if not skip and "timing" in filters:
|
||||
hour = ts.iloc[i].hour
|
||||
if hour < 4 or hour > 16:
|
||||
skip = True
|
||||
|
||||
if not skip and "vol" in filters:
|
||||
avg_v = np.mean(v[ev["sq_start"]:i])
|
||||
if avg_v > 0 and v[i] <= avg_v * 1.3:
|
||||
skip = True
|
||||
|
||||
if not skip and "cross" in filters and kcr2 is not None and ts2 is not None:
|
||||
i2 = np.searchsorted(ts2, ts.values[i].astype("int64") // 10**6)
|
||||
i2 = min(i2, len(kcr2) - 1)
|
||||
cross_ok = any(
|
||||
not np.isnan(kcr2[j]) and kcr2[j] < 0.85
|
||||
for j in range(max(0, i2 - 10), i2 + 1)
|
||||
)
|
||||
if not cross_ok:
|
||||
skip = True
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i,
|
||||
direction=1 if first_ret > 0 else -1,
|
||||
entry_price=c[i - 1],
|
||||
metadata={"dur": ev["dur"], "filters": filters},
|
||||
))
|
||||
return signals
|
||||
|
||||
def report_all_presets(self, assets=None, timeframes=None, hold=3):
|
||||
"""Esegue tutti i preset di filtri × asset × tf."""
|
||||
assets = assets or self.default_assets
|
||||
timeframes = timeframes or self.default_timeframes
|
||||
all_results = []
|
||||
|
||||
for preset_name, filter_list in PRESETS.items():
|
||||
for asset in assets:
|
||||
for tf in timeframes:
|
||||
r = self.backtest(asset, tf, hold, filters=filter_list)
|
||||
if r and r.trades >= 20:
|
||||
r.strategy_name = f"SQ03 {preset_name}"
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
|
||||
print(f"\n{'=' * 120}")
|
||||
print(f" SQ03 SQUEEZE FILTRATO — TUTTI I PRESET ({len(all_results)} config)")
|
||||
print(f" Fee: {self.fee_rt*100:.1f}% RT | Leva: {self.leverage:.0f}x | Pos: {self.position_size*100:.0f}%")
|
||||
print(f"{'=' * 120}")
|
||||
print(f" {'Nome':<30s} {'A/T':>7s} {'Trades':>6s} {'Acc':>6s} "
|
||||
f"{'PnL€':>10s} {'DD%':>6s} {'€/day':>7s} "
|
||||
f"{'Mkt%':>5s} {'Dur':>5s} {'Worst':>12s} {'Anni':>4s}")
|
||||
print(f" {'─' * 110}")
|
||||
|
||||
for r in all_results:
|
||||
r.print_summary()
|
||||
|
||||
if all_results:
|
||||
print(f"\n MIGLIORE: ", end="")
|
||||
best = all_results[0]
|
||||
best.print_yearly()
|
||||
|
||||
return all_results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = SqueezeFiltered()
|
||||
strategy.report_all_presets()
|
||||
@@ -0,0 +1,204 @@
|
||||
"""SQ04 — Ultimate Squeeze — combinazione incrementale di tutti i filtri.
|
||||
|
||||
Testa combinazioni di filtri (antifake, long_sq, timing, cross-asset,
|
||||
correlation, volume, trend alignment, volatility regime) e classifica
|
||||
per accuracy.
|
||||
|
||||
IN:
|
||||
- OHLCV DataFrame (primario + secondario)
|
||||
- Parametri: bb_window, sq_threshold, lista filtri da attivare
|
||||
|
||||
OUT:
|
||||
- BacktestResult per ogni combinazione di filtri
|
||||
- Classifica globale
|
||||
|
||||
Risultati tipici:
|
||||
BTC 15m antifake+corr: 81.6% acc (ma concentrato 2018)
|
||||
BTC 15m antifake+vol: 79.7% acc, 1250 trades — robusto
|
||||
ETH 1h antifake+corr: 80.7% acc (solo 2018)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
sys.path.insert(0, ".")
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.strategies.base import Strategy, Signal
|
||||
from src.strategies.indicators import (
|
||||
keltner_ratio, detect_squeezes, ema, rv_annualized, rolling_correlation,
|
||||
)
|
||||
from src.data.downloader import load_data
|
||||
|
||||
|
||||
class SqueezeUltimate(Strategy):
|
||||
name = "SQ04_ultimate"
|
||||
description = "Ultimate squeeze — tutti i filtri combinabili"
|
||||
default_assets = ["BTC", "ETH"]
|
||||
default_timeframes = ["15m", "1h"]
|
||||
|
||||
FILTER_PRESETS = {
|
||||
"antifake+vol": ["antifake", "vol_confirm"],
|
||||
"antifake+corr": ["antifake", "corr_high"],
|
||||
"af+long+corr+trend": ["antifake", "long_sq", "corr_high", "trend_align"],
|
||||
"ALL": ["antifake", "long_sq", "cross", "timing", "corr_high",
|
||||
"vol_confirm", "trend_align", "low_rv"],
|
||||
}
|
||||
|
||||
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||
**params) -> list[Signal]:
|
||||
c = df["close"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
v = df["volume"].values
|
||||
n = len(c)
|
||||
|
||||
asset = params.get("asset", "BTC")
|
||||
tf = params.get("tf", "15m")
|
||||
filters = params.get("filters", ["antifake", "vol_confirm"])
|
||||
|
||||
kcr = keltner_ratio(c, h, l, 14)
|
||||
events = detect_squeezes(c, h, l, kcr)
|
||||
|
||||
secondary = "ETH" if asset == "BTC" else "BTC"
|
||||
df2 = load_data(secondary, tf)
|
||||
c2 = df2["close"].values
|
||||
kcr2 = keltner_ratio(c2, df2["high"].values, df2["low"].values, 14)
|
||||
ts2 = df2["timestamp"].values
|
||||
|
||||
ema_50 = ema(c, 50)
|
||||
rv_48 = rv_annualized(c, 48)
|
||||
corr = rolling_correlation(c, c2)
|
||||
|
||||
signals = []
|
||||
for ev in events:
|
||||
i = ev["idx"]
|
||||
if i < 1 or i >= n:
|
||||
continue
|
||||
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 f in filters:
|
||||
if f == "antifake":
|
||||
br = h[i] - l[i]
|
||||
if br > 0:
|
||||
if c[i] > c[i-1] and (h[i] - c[i]) / br > 0.6:
|
||||
skip = True
|
||||
elif c[i] <= c[i-1] and (c[i] - l[i]) / br > 0.6:
|
||||
skip = True
|
||||
elif f == "long_sq":
|
||||
if ev["dur"] < 10:
|
||||
skip = True
|
||||
elif f == "timing":
|
||||
if ts.iloc[i].hour < 4 or ts.iloc[i].hour > 16:
|
||||
skip = True
|
||||
elif f == "cross":
|
||||
i2 = np.searchsorted(ts2, ts.values[i].astype("int64") // 10**6)
|
||||
i2 = min(i2, len(kcr2) - 1)
|
||||
if not any(not np.isnan(kcr2[j]) and kcr2[j] < 0.85
|
||||
for j in range(max(0, i2 - 10), i2 + 1)):
|
||||
skip = True
|
||||
elif f == "corr_high":
|
||||
if np.isnan(corr[i]) or abs(corr[i]) < 0.6:
|
||||
skip = True
|
||||
elif f == "vol_confirm":
|
||||
avg_v = np.mean(v[ev["sq_start"]:i])
|
||||
if avg_v > 0 and v[i] <= avg_v * 1.3:
|
||||
skip = True
|
||||
elif f == "trend_align":
|
||||
if not np.isnan(ema_50[i]):
|
||||
if first_ret > 0 and c[i] < ema_50[i]:
|
||||
skip = True
|
||||
elif first_ret < 0 and c[i] > ema_50[i]:
|
||||
skip = True
|
||||
elif f == "low_rv":
|
||||
if not np.isnan(rv_48[i]) and rv_48[i] >= 1.5:
|
||||
skip = True
|
||||
if skip:
|
||||
break
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
signals.append(Signal(
|
||||
idx=i,
|
||||
direction=1 if first_ret > 0 else -1,
|
||||
entry_price=c[i - 1],
|
||||
metadata={"dur": ev["dur"], "filters": filters},
|
||||
))
|
||||
return signals
|
||||
|
||||
def backtest(self, asset: str, tf: str, hold: int = 3, **params):
|
||||
params.setdefault("asset", asset)
|
||||
params.setdefault("tf", tf)
|
||||
df = load_data(asset, tf)
|
||||
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||
signals = self.generate_signals(df, ts, **params)
|
||||
# Usa il backtest della base ma passando i segnali già generati
|
||||
from src.strategies.base import BacktestResult, YearlyStats, TF_MINUTES
|
||||
c = df["close"].values
|
||||
n = len(c)
|
||||
yearly: dict[int, dict] = {}
|
||||
capital = float(self.initial_capital)
|
||||
peak = capital
|
||||
max_dd = 0.0
|
||||
total_bars = 0
|
||||
for sig in signals:
|
||||
i = sig.idx
|
||||
if i + hold >= n or i < 1:
|
||||
continue
|
||||
entry = sig.entry_price
|
||||
exit_price = c[min(i + hold - 1, n - 1)]
|
||||
actual = (exit_price - entry) / entry * sig.direction
|
||||
net = actual * self.leverage - self.fee_rt * self.leverage
|
||||
capital += capital * self.position_size * net
|
||||
capital = max(capital, 10)
|
||||
if capital > peak: peak = capital
|
||||
dd = (peak - capital) / peak
|
||||
max_dd = max(max_dd, dd)
|
||||
total_bars += hold
|
||||
year = ts.iloc[i].year
|
||||
if year not in yearly:
|
||||
yearly[year] = {"w": 0, "t": 0, "pnl": 0.0}
|
||||
yearly[year]["t"] += 1
|
||||
if actual > 0: yearly[year]["w"] += 1
|
||||
yearly[year]["pnl"] += net * self.initial_capital
|
||||
all_t = sum(d["t"] for d in yearly.values())
|
||||
all_w = sum(d["w"] for d in yearly.values())
|
||||
if all_t == 0: return None
|
||||
yearly_stats = [YearlyStats(y, d["t"], d["w"], d["pnl"]) for y, d in sorted(yearly.items())]
|
||||
return BacktestResult(
|
||||
strategy_name=self.name, asset=asset, timeframe=tf, params=params,
|
||||
trades=all_t, wins=all_w, pnl=sum(d["pnl"] for d in yearly.values()),
|
||||
capital=capital, initial_capital=self.initial_capital,
|
||||
max_dd=max_dd * 100, time_in_market_pct=total_bars / n * 100,
|
||||
avg_trade_duration_h=hold * TF_MINUTES.get(tf, 60) / 60,
|
||||
years_active=len(yearly), yearly=yearly_stats,
|
||||
)
|
||||
|
||||
def report_all_presets(self):
|
||||
"""Esegue tutte le combinazioni preset × asset × tf."""
|
||||
all_results = []
|
||||
for preset_name, filter_list in self.FILTER_PRESETS.items():
|
||||
for asset in self.default_assets:
|
||||
for tf in self.default_timeframes:
|
||||
r = self.backtest(asset, tf, filters=filter_list)
|
||||
if r and r.trades >= 20:
|
||||
r.strategy_name = f"SQ04 {preset_name}"
|
||||
all_results.append(r)
|
||||
|
||||
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||
|
||||
print(f"\n{'=' * 120}")
|
||||
print(f" SQ04 ULTIMATE — TUTTI I PRESET")
|
||||
print(f"{'=' * 120}")
|
||||
for r in all_results:
|
||||
r.print_summary()
|
||||
return all_results
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
strategy = SqueezeUltimate()
|
||||
strategy.report_all_presets()
|
||||
Reference in New Issue
Block a user