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:
@@ -19,7 +19,12 @@ Progetto di ricerca: riconoscimento pattern frattali per trading algoritmico su
|
|||||||
src/data/ → download e caricamento dati (downloader.py)
|
src/data/ → download e caricamento dati (downloader.py)
|
||||||
src/fractal/ → indicatori frattali (patterns.py, indicators.py, similarity.py)
|
src/fractal/ → indicatori frattali (patterns.py, indicators.py, similarity.py)
|
||||||
src/backtest/ → engine di backtesting (engine.py)
|
src/backtest/ → engine di backtesting (engine.py)
|
||||||
scripts/ → analisi e strategie numerate 01–13
|
src/strategies/ → classe base Strategy ABC + indicatori condivisi
|
||||||
|
base.py → Strategy, Signal, BacktestResult, YearlyStats
|
||||||
|
indicators.py → keltner_ratio, detect_squeezes, ema, atr, rv, correlation
|
||||||
|
scripts/strategies/ → strategie attive (SQ01-SQ04, ML01)
|
||||||
|
scripts/waste/ → strategie scartate (W01-W22 + REF originali)
|
||||||
|
scripts/analysis/ → script di confronto e report
|
||||||
docs/diary/ → diario di ricerca giornaliero
|
docs/diary/ → diario di ricerca giornaliero
|
||||||
data/raw/ → file .parquet OHLCV (gitignored)
|
data/raw/ → file .parquet OHLCV (gitignored)
|
||||||
data/processed/ → modelli salvati (gitignored)
|
data/processed/ → modelli salvati (gitignored)
|
||||||
@@ -30,7 +35,8 @@ data/processed/ → modelli salvati (gitignored)
|
|||||||
```bash
|
```bash
|
||||||
uv sync # installa dipendenze
|
uv sync # installa dipendenze
|
||||||
uv run python -m src.data.downloader # scarica dati storici
|
uv run python -m src.data.downloader # scarica dati storici
|
||||||
uv run python scripts/13_squeeze_ml_hybrid.py # strategia vincente
|
uv run python scripts/strategies/SQ02_squeeze_antifake_vol.py # miglior strategia robusta
|
||||||
|
uv run python scripts/strategies/ML01_squeeze_gbm.py # squeeze + ML (GBM)
|
||||||
uv run pytest # test
|
uv run pytest # test
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -60,9 +66,23 @@ Configurazione migliore: ETH 15m, BBw=14, squeeze threshold=0.8, breakout=3 barr
|
|||||||
|
|
||||||
Risultato backtestato: 76.9% accuracy, 118% annuo, 4.2% drawdown, €13.78/giorno da €1.000.
|
Risultato backtestato: 76.9% accuracy, 118% annuo, 4.2% drawdown, €13.78/giorno da €1.000.
|
||||||
|
|
||||||
|
## Strategie attive
|
||||||
|
|
||||||
|
| Codice | Nome | Tipo | Accuracy | Note |
|
||||||
|
|--------|------|------|----------|------|
|
||||||
|
| SQ01 | Squeeze Base | Regole | 76.7% | Squeeze breakout puro, baseline |
|
||||||
|
| SQ02 | Antifake+Vol | Regole | 79.7% | **Miglior robusto** — 9 anni, Sharpe 5.01 |
|
||||||
|
| SQ03 | All Filters | Regole | 79.2% | Cross-asset + timing + long squeeze |
|
||||||
|
| SQ04 | Ultimate | Regole | 81.6% | Max accuracy ma concentrato 2018 |
|
||||||
|
| ML01 | Squeeze+GBM | ML | 78.8% | Walk-forward, €12/day, DD basso |
|
||||||
|
|
||||||
|
Tutte le strategie estendono `src.strategies.base.Strategy` con interfaccia comune:
|
||||||
|
`generate_signals() → backtest() → report()`.
|
||||||
|
|
||||||
## Convenzioni
|
## Convenzioni
|
||||||
|
|
||||||
- Script numerati progressivamente (`01_`, `02_`, …). Ogni script è autocontenuto.
|
- Strategie in `scripts/strategies/` con codice univoco (SQ01, ML01, ...).
|
||||||
|
- Script scartati in `scripts/waste/` con prefisso W01-W22.
|
||||||
- Diario in `docs/diary/YYYY-MM-DD.md`. Aggiornare dopo ogni esperimento significativo.
|
- Diario in `docs/diary/YYYY-MM-DD.md`. Aggiornare dopo ogni esperimento significativo.
|
||||||
- Nessun dato sensibile nei commit (token, chiavi API). Usare `.gitignore`.
|
- Nessun dato sensibile nei commit (token, chiavi API). Usare `.gitignore`.
|
||||||
- Verificare sempre assenza di data leakage prima di fidarsi dei risultati. In particolare: `returns[i-w : i]` include `close[i]` che è un candle nel futuro — usare `returns[i-w : i-1]`.
|
- Verificare sempre assenza di data leakage prima di fidarsi dei risultati. In particolare: `returns[i-w : i]` include `close[i]` che è un candle nel futuro — usare `returns[i-w : i-1]`.
|
||||||
|
|||||||
@@ -0,0 +1,559 @@
|
|||||||
|
"""Analisi finale — S1 (squeeze puro) vs Script 13 (squeeze+ML GBM).
|
||||||
|
Metriche: PnL, num trades, DD max, tempo medio a mercato, descrizione.
|
||||||
|
"""
|
||||||
|
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.data.downloader import load_data
|
||||||
|
from src.fractal.patterns import encode_candles
|
||||||
|
|
||||||
|
FEE_PERP = 0.002
|
||||||
|
FEE_ML = 0.001
|
||||||
|
INITIAL = 1000
|
||||||
|
LEVERAGE = 3
|
||||||
|
|
||||||
|
TF_MINUTES = {"1m": 1, "5m": 5, "15m": 15, "1h": 60, "4h": 240, "1d": 1440}
|
||||||
|
|
||||||
|
|
||||||
|
# ── helpers ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
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 detect_squeezes(close, high, low, kcr, sq_thr=0.8, min_dur=5):
|
||||||
|
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
|
||||||
|
dur = i - sq_start
|
||||||
|
if dur < min_dur:
|
||||||
|
continue
|
||||||
|
events.append({"idx": i, "dur": dur, "sq_start": sq_start,
|
||||||
|
"avg_vol_squeeze": np.mean(close[sq_start:i]),
|
||||||
|
"kcr_at_release": kcr[i]})
|
||||||
|
return events
|
||||||
|
|
||||||
|
|
||||||
|
def _build_result(yearly, capital, max_dd, all_t, all_w, time_pct, avg_dur_h):
|
||||||
|
acc = all_w / all_t * 100
|
||||||
|
tot_pnl = sum(p for d in yearly.values() for p in d["pnls"])
|
||||||
|
years_active = len(yearly)
|
||||||
|
all_pnls = [p for d in yearly.values() for p in d["pnls"]]
|
||||||
|
sharpe = np.mean(all_pnls) / np.std(all_pnls) * np.sqrt(252) if len(all_pnls) > 1 and np.std(all_pnls) > 0 else 0
|
||||||
|
|
||||||
|
year_details = {}
|
||||||
|
for y in sorted(yearly):
|
||||||
|
d = yearly[y]
|
||||||
|
ya = d["w"] / d["t"] * 100 if d["t"] > 0 else 0
|
||||||
|
yp = sum(d["pnls"])
|
||||||
|
year_details[y] = {"trades": d["t"], "acc": ya, "pnl": yp}
|
||||||
|
|
||||||
|
valid_years = {y: d for y, d in year_details.items() if d["trades"] >= 10}
|
||||||
|
if valid_years:
|
||||||
|
worst_y = min(valid_years, key=lambda y: valid_years[y]["acc"])
|
||||||
|
worst_acc = valid_years[worst_y]["acc"]
|
||||||
|
elif year_details:
|
||||||
|
worst_y = min(year_details, key=lambda y: year_details[y]["acc"])
|
||||||
|
worst_acc = year_details[worst_y]["acc"]
|
||||||
|
else:
|
||||||
|
worst_y = "N/A"
|
||||||
|
worst_acc = 0
|
||||||
|
|
||||||
|
daily_pnl = tot_pnl / (years_active * 365) if years_active > 0 else 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
"trades": all_t, "acc": acc, "pnl": tot_pnl, "capital": capital,
|
||||||
|
"max_dd": max_dd * 100, "sharpe": sharpe, "daily_pnl": daily_pnl,
|
||||||
|
"time_in_market_pct": time_pct, "avg_dur_h": avg_dur_h,
|
||||||
|
"years_active": years_active, "worst_year": str(worst_y),
|
||||||
|
"worst_acc": worst_acc, "year_details": year_details,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── S1: Squeeze breakout puro ────────────────────────────────────────
|
||||||
|
|
||||||
|
def run_s1_squeeze(asset, tf, hold=3):
|
||||||
|
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)
|
||||||
|
events = detect_squeezes(c, h, l, kcr)
|
||||||
|
|
||||||
|
yearly = {}
|
||||||
|
capital = float(INITIAL)
|
||||||
|
peak = capital
|
||||||
|
max_dd = 0
|
||||||
|
total_bars = 0
|
||||||
|
|
||||||
|
for ev in events:
|
||||||
|
i = ev["idx"]
|
||||||
|
if i + hold + 1 >= 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
|
||||||
|
direction = 1 if first_ret > 0 else -1
|
||||||
|
entry = c[i-1]
|
||||||
|
exit_price = c[min(i + hold - 1, n - 1)]
|
||||||
|
actual = (exit_price - entry) / entry * direction
|
||||||
|
net = actual * LEVERAGE - FEE_PERP * 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)
|
||||||
|
total_bars += hold
|
||||||
|
|
||||||
|
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 == 0: return None
|
||||||
|
return _build_result(yearly, capital, max_dd, all_t, all_w,
|
||||||
|
total_bars / n * 100, hold * TF_MINUTES.get(tf, 60) / 60)
|
||||||
|
|
||||||
|
|
||||||
|
def run_s1_antifake_vol(asset, tf, hold=3):
|
||||||
|
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)
|
||||||
|
events = detect_squeezes(c, h, l, kcr)
|
||||||
|
|
||||||
|
yearly = {}
|
||||||
|
capital = float(INITIAL)
|
||||||
|
peak = capital
|
||||||
|
max_dd = 0
|
||||||
|
total_bars = 0
|
||||||
|
|
||||||
|
for ev in events:
|
||||||
|
i = ev["idx"]
|
||||||
|
if i + hold + 1 >= 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 > 0.6:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if (c[i] - l[i]) / br > 0.6:
|
||||||
|
continue
|
||||||
|
avg_v = np.mean(v[ev["sq_start"]:i])
|
||||||
|
if avg_v > 0 and v[i] <= avg_v * 1.3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
direction = 1 if first_ret > 0 else -1
|
||||||
|
entry = c[i-1]
|
||||||
|
exit_price = c[min(i + hold - 1, n - 1)]
|
||||||
|
actual = (exit_price - entry) / entry * direction
|
||||||
|
net = actual * LEVERAGE - FEE_PERP * 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)
|
||||||
|
total_bars += hold
|
||||||
|
|
||||||
|
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 == 0: return None
|
||||||
|
return _build_result(yearly, capital, max_dd, all_t, all_w,
|
||||||
|
total_bars / n * 100, hold * TF_MINUTES.get(tf, 60) / 60)
|
||||||
|
|
||||||
|
|
||||||
|
# ── Script 13: Squeeze + ML ibrida (GBM walk-forward) ────────────────
|
||||||
|
|
||||||
|
def build_features_at(df, i, squeeze_info):
|
||||||
|
if i < 100:
|
||||||
|
return None
|
||||||
|
o = df["open"].values
|
||||||
|
h = df["high"].values
|
||||||
|
l = df["low"].values
|
||||||
|
c = df["close"].values
|
||||||
|
v = df["volume"].values
|
||||||
|
feats = []
|
||||||
|
for w in [12, 24, 48]:
|
||||||
|
win_c = c[i-w:i]
|
||||||
|
win_o = o[i-w:i]
|
||||||
|
win_h = h[i-w:i]
|
||||||
|
win_l = l[i-w:i]
|
||||||
|
win_v = v[i-w:i]
|
||||||
|
mn, mx = win_l.min(), max(win_h.max(), win_c.max())
|
||||||
|
rng = mx - mn if mx - mn > 0 else 1e-10
|
||||||
|
total = win_h - win_l
|
||||||
|
total = np.where(total == 0, 1e-10, total)
|
||||||
|
body = np.abs(win_c - win_o) / total
|
||||||
|
direction = np.sign(win_c - win_o)
|
||||||
|
log_c = np.log(np.where(win_c == 0, 1e-10, win_c))
|
||||||
|
rets = np.diff(log_c)
|
||||||
|
v_mean = np.mean(win_v)
|
||||||
|
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):]),
|
||||||
|
(win_c[-1] - mn) / rng,
|
||||||
|
win_v[-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["avg_vol_squeeze"] if sq["avg_vol_squeeze"] > 0 else 1,
|
||||||
|
np.mean(v[i:min(i+3, len(v))]) / sq["avg_vol_squeeze"] if sq["avg_vol_squeeze"] > 0 else 1,
|
||||||
|
])
|
||||||
|
h48 = np.max(h[max(0, i-48):i])
|
||||||
|
l48 = 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))))
|
||||||
|
atr = np.mean(tr[1:])
|
||||||
|
feats.append(atr / c[i-1] if c[i-1] > 0 else 0)
|
||||||
|
first_ret = (c[i] - c[i-1]) / c[i-1] if c[i-1] > 0 else 0
|
||||||
|
feats.append(first_ret)
|
||||||
|
return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)
|
||||||
|
|
||||||
|
|
||||||
|
def run_s13_hybrid(asset, tf, bb_w, sq_thr, brk_bars, leverage, pos_pct, ml_thr):
|
||||||
|
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)
|
||||||
|
events = detect_squeezes(close, high, low, kcr, sq_thr)
|
||||||
|
|
||||||
|
X_all, y_all, ev_all = [], [], []
|
||||||
|
for ev in events:
|
||||||
|
i = ev["idx"]
|
||||||
|
if i + brk_bars >= n or i < 100:
|
||||||
|
continue
|
||||||
|
feats = build_features_at(df, i, ev)
|
||||||
|
if feats is None:
|
||||||
|
continue
|
||||||
|
actual_ret = (close[i + brk_bars - 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 = np.array(X_all)
|
||||||
|
y = np.array(y_all)
|
||||||
|
|
||||||
|
TRAIN_SIZE = max(int(len(X) * 0.5), 50)
|
||||||
|
STEP_SIZE = max(int(len(X) * 0.1), 10)
|
||||||
|
|
||||||
|
yearly = {}
|
||||||
|
capital = float(INITIAL)
|
||||||
|
peak = capital
|
||||||
|
max_dd = 0
|
||||||
|
total_bars = 0
|
||||||
|
all_t = 0
|
||||||
|
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_bars - 1] - close[i - 1]) / close[i - 1]
|
||||||
|
|
||||||
|
direction = None
|
||||||
|
if p_up >= ml_thr:
|
||||||
|
direction = 1
|
||||||
|
elif p_up <= (1 - ml_thr):
|
||||||
|
direction = -1
|
||||||
|
if direction is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
is_correct = (direction == 1 and actual_ret > 0) or (direction == -1 and actual_ret < 0)
|
||||||
|
trade_ret = actual_ret * direction
|
||||||
|
net = trade_ret * leverage - FEE_ML * 2 * leverage
|
||||||
|
capital += capital * pos_pct * net
|
||||||
|
capital = max(capital, 10)
|
||||||
|
if capital > peak: peak = capital
|
||||||
|
dd = (peak - capital) / peak
|
||||||
|
max_dd = max(max_dd, dd)
|
||||||
|
total_bars += brk_bars
|
||||||
|
|
||||||
|
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, "pnls": []}
|
||||||
|
yearly[year]["t"] += 1
|
||||||
|
if is_correct: yearly[year]["w"] += 1
|
||||||
|
yearly[year]["pnls"].append(net * INITIAL)
|
||||||
|
|
||||||
|
start += STEP_SIZE
|
||||||
|
|
||||||
|
if all_t == 0:
|
||||||
|
return None
|
||||||
|
return _build_result(yearly, capital, max_dd, all_t, all_w,
|
||||||
|
total_bars / n * 100, brk_bars * TF_MINUTES.get(tf, 60) / 60)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# ESECUZIONE
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
print("Calcolo in corso...\n")
|
||||||
|
|
||||||
|
strategies = []
|
||||||
|
|
||||||
|
def add(name, desc, cat, result):
|
||||||
|
if result and result["trades"] >= 20:
|
||||||
|
strategies.append({"name": name, "desc": desc, "cat": cat, **result})
|
||||||
|
|
||||||
|
# ── S1: Squeeze puro ────────────────────────────────────────────
|
||||||
|
add("S1 Squeeze BTC 15m", "Squeeze breakout puro, BBw=14, hold 3×15m, leva 3x",
|
||||||
|
"S1", run_s1_squeeze("BTC", "15m"))
|
||||||
|
add("S1 Squeeze ETH 15m", "Squeeze breakout puro, BBw=14, hold 3×15m, leva 3x",
|
||||||
|
"S1", run_s1_squeeze("ETH", "15m"))
|
||||||
|
add("S1 Squeeze BTC 1h", "Squeeze breakout puro, BBw=14, hold 3×1h, leva 3x",
|
||||||
|
"S1", run_s1_squeeze("BTC", "1h"))
|
||||||
|
add("S1 Squeeze ETH 1h", "Squeeze breakout puro, BBw=14, hold 3×1h, leva 3x",
|
||||||
|
"S1", run_s1_squeeze("ETH", "1h"))
|
||||||
|
add("S1 AF+Vol BTC 15m", "Squeeze + antifakeout + volume confirm >1.3× media",
|
||||||
|
"S1", run_s1_antifake_vol("BTC", "15m"))
|
||||||
|
add("S1 AF+Vol ETH 15m", "Squeeze + antifakeout + volume confirm >1.3× media",
|
||||||
|
"S1", run_s1_antifake_vol("ETH", "15m"))
|
||||||
|
add("S1 AF+Vol BTC 1h", "Squeeze + antifakeout + volume confirm >1.3× media",
|
||||||
|
"S1", run_s1_antifake_vol("BTC", "1h"))
|
||||||
|
add("S1 AF+Vol ETH 1h", "Squeeze + antifakeout + volume confirm >1.3× media",
|
||||||
|
"S1", run_s1_antifake_vol("ETH", "1h"))
|
||||||
|
|
||||||
|
# ── Script 13: Squeeze + ML (GBM walk-forward) ─────────────────
|
||||||
|
print(" Training ML models...")
|
||||||
|
add("S13 ETH 15m bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.70, 3x leva 15% pos",
|
||||||
|
"S13", run_s13_hybrid("ETH", "15m", 14, 0.8, 3, 3, 0.15, 0.70))
|
||||||
|
add("S13 ETH 15m bb14 ml65", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.65, 3x leva 15% pos",
|
||||||
|
"S13", run_s13_hybrid("ETH", "15m", 14, 0.8, 3, 3, 0.15, 0.65))
|
||||||
|
add("S13 ETH 15m bb20 ml70", "Squeeze+GBM walk-forward, BBw=20 sq=0.9 ml≥0.70, 3x leva 15% pos",
|
||||||
|
"S13", run_s13_hybrid("ETH", "15m", 20, 0.9, 3, 3, 0.15, 0.70))
|
||||||
|
add("S13 BTC 15m bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.9 ml≥0.70, 3x leva 15% pos",
|
||||||
|
"S13", run_s13_hybrid("BTC", "15m", 14, 0.9, 3, 3, 0.15, 0.70))
|
||||||
|
add("S13 BTC 15m bb14 ml65", "Squeeze+GBM walk-forward, BBw=14 sq=0.9 ml≥0.65, 3x leva 15% pos",
|
||||||
|
"S13", run_s13_hybrid("BTC", "15m", 14, 0.9, 3, 3, 0.15, 0.65))
|
||||||
|
add("S13 BTC 1h bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.70, 3x leva 20% pos",
|
||||||
|
"S13", run_s13_hybrid("BTC", "1h", 14, 0.8, 3, 3, 0.20, 0.70))
|
||||||
|
add("S13 BTC 1h bb14 ml65", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.65, 3x leva 20% pos",
|
||||||
|
"S13", run_s13_hybrid("BTC", "1h", 14, 0.8, 3, 3, 0.20, 0.65))
|
||||||
|
add("S13 ETH 1h bb14 ml70", "Squeeze+GBM walk-forward, BBw=14 sq=0.8 ml≥0.70, 3x leva 20% pos",
|
||||||
|
"S13", run_s13_hybrid("ETH", "1h", 14, 0.8, 3, 3, 0.20, 0.70))
|
||||||
|
|
||||||
|
strategies.sort(key=lambda x: x["acc"], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# TABELLA 1: Classifica
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
W = 150
|
||||||
|
print("=" * W)
|
||||||
|
print(" S1 (SQUEEZE PURO) vs S13 (SQUEEZE + GBM) — CLASSIFICA FINALE")
|
||||||
|
print(f" Fee: 0.2% RT. Dati OHLCV reali 2018-2026. Position 15%. Leva 3x.")
|
||||||
|
print("=" * W)
|
||||||
|
hdr = (f" {'#':>2s} {'Cat':>3s} {'Nome':<26s} {'Trades':>6s} {'Acc':>6s} "
|
||||||
|
f"{'PnL€':>9s} {'DD%':>6s} {'€/day':>7s} {'Sharpe':>7s} "
|
||||||
|
f"{'Mkt%':>5s} {'Dur':>6s} {'Worst':>12s} {'Anni':>4s}")
|
||||||
|
print(hdr)
|
||||||
|
print(f" {'─'*(W-4)}")
|
||||||
|
|
||||||
|
for idx, s in enumerate(strategies, 1):
|
||||||
|
worst = f"{s['worst_year']}({s['worst_acc']:.0f}%)"
|
||||||
|
dur_str = f"{s['avg_dur_h']:.0f}h" if s['avg_dur_h'] >= 1 else f"{s['avg_dur_h']*60:.0f}m"
|
||||||
|
tag = " ★★" if s["acc"] >= 78 else " ★" if s["acc"] >= 76 else ""
|
||||||
|
print(f" {idx:>2d} {s['cat']:>3s} {s['name']:<26s} {s['trades']:>6d} {s['acc']:>5.1f}% "
|
||||||
|
f"€{s['pnl']:>+8.0f} {s['max_dd']:>5.1f}% {s['daily_pnl']:>+6.2f} {s['sharpe']:>7.2f} "
|
||||||
|
f"{s['time_in_market_pct']:>4.1f}% {dur_str:>6s} {worst:>12s} {s['years_active']:>4d}{tag}")
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# TABELLA 2: Descrizione
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
print(f"\n\n{'=' * W}")
|
||||||
|
print(" DESCRIZIONE")
|
||||||
|
print(f"{'=' * W}")
|
||||||
|
print(f" {'#':>2s} {'Nome':<26s} {'Descrizione'}")
|
||||||
|
print(f" {'─'*(W-4)}")
|
||||||
|
for idx, s in enumerate(strategies, 1):
|
||||||
|
print(f" {idx:>2d} {s['name']:<26s} {s['desc']}")
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# TABELLA 3: Breakdown per anno
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
top_n = min(12, len(strategies))
|
||||||
|
top = strategies[:top_n]
|
||||||
|
all_years = sorted(set(y for s in top for y in s["year_details"]))
|
||||||
|
|
||||||
|
print(f"\n\n{'=' * W}")
|
||||||
|
print(f" BREAKDOWN PER ANNO — TOP {top_n} (accuracy% / trades)")
|
||||||
|
print(f"{'=' * W}")
|
||||||
|
|
||||||
|
header = f" {'Nome':<26s}"
|
||||||
|
for y in all_years:
|
||||||
|
header += f" {y:>10d}"
|
||||||
|
print(header)
|
||||||
|
print(f" {'─'*(W-4)}")
|
||||||
|
|
||||||
|
for s in top:
|
||||||
|
line = f" {s['name']:<26s}"
|
||||||
|
for y in all_years:
|
||||||
|
if y in s["year_details"]:
|
||||||
|
d = s["year_details"][y]
|
||||||
|
line += f" {d['acc']:>4.0f}%/{d['trades']:<4d}"
|
||||||
|
else:
|
||||||
|
line += f" {'—':>10s}"
|
||||||
|
print(line)
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# TABELLA 4: Robustezza
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
print(f"\n\n{'=' * W}")
|
||||||
|
print(f" ANALISI ROBUSTEZZA")
|
||||||
|
print(f"{'=' * W}")
|
||||||
|
print(f" {'#':>2s} {'Nome':<26s} {'MinAcc':>7s} {'MaxAcc':>7s} {'Spread':>7s} "
|
||||||
|
f"{'AnniOK':>7s} {'€/trade':>8s} {'Verdict':<12s}")
|
||||||
|
print(f" {'─'*90}")
|
||||||
|
|
||||||
|
for idx, s in enumerate(strategies, 1):
|
||||||
|
yd = s["year_details"]
|
||||||
|
valid = {y: d for y, d in yd.items() if d["trades"] >= 10}
|
||||||
|
accs = [d["acc"] for d in (valid if valid else yd).values()]
|
||||||
|
if not accs:
|
||||||
|
continue
|
||||||
|
min_a, max_a = min(accs), max(accs)
|
||||||
|
spread = max_a - min_a
|
||||||
|
years_ok = sum(1 for a in accs if a >= 70)
|
||||||
|
avg_pnl = s["pnl"] / s["trades"] if s["trades"] > 0 else 0
|
||||||
|
n_valid = len(valid if valid else yd)
|
||||||
|
|
||||||
|
if n_valid < 4:
|
||||||
|
verdict = "⚠ CORTO"
|
||||||
|
elif min_a < 60:
|
||||||
|
verdict = "⚠ FRAGILE"
|
||||||
|
elif min_a >= 72 and s["acc"] >= 77:
|
||||||
|
verdict = "✅ SOLIDO"
|
||||||
|
elif min_a >= 65 and s["acc"] >= 74:
|
||||||
|
verdict = "~ BUONO"
|
||||||
|
else:
|
||||||
|
verdict = "~ OK"
|
||||||
|
|
||||||
|
print(f" {idx:>2d} {s['name']:<26s} {min_a:>6.1f}% {max_a:>6.1f}% {spread:>6.1f}% "
|
||||||
|
f"{years_ok:>3d}/{n_valid:<3d} €{avg_pnl:>+7.1f} {verdict:<12s}")
|
||||||
|
|
||||||
|
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
# VERDETTO
|
||||||
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
print(f"\n\n{'=' * W}")
|
||||||
|
print(f" VERDETTO FINALE")
|
||||||
|
print(f"{'=' * W}")
|
||||||
|
|
||||||
|
solidi = [s for s in strategies if s["trades"] >= 200 and s["years_active"] >= 5 and s["worst_acc"] >= 65]
|
||||||
|
solidi_s1 = [s for s in solidi if s["cat"] == "S1"]
|
||||||
|
solidi_ml = [s for s in solidi if s["cat"] == "S13"]
|
||||||
|
solidi_s1.sort(key=lambda x: x["acc"], reverse=True)
|
||||||
|
solidi_ml.sort(key=lambda x: x["daily_pnl"], reverse=True)
|
||||||
|
|
||||||
|
if solidi_s1:
|
||||||
|
b = solidi_s1[0]
|
||||||
|
print(f"\n MIGLIORE S1 (regole pure, facile da deployare):")
|
||||||
|
print(f" {b['name']} — {b['acc']:.1f}% acc, {b['trades']} trades, DD {b['max_dd']:.1f}%, €{b['daily_pnl']:+.2f}/day, Sharpe {b['sharpe']:.2f}")
|
||||||
|
|
||||||
|
if solidi_ml:
|
||||||
|
m = solidi_ml[0]
|
||||||
|
print(f"\n MIGLIORE S13 (squeeze+GBM, più complesso):")
|
||||||
|
print(f" {m['name']} — {m['acc']:.1f}% acc, {m['trades']} trades, DD {m['max_dd']:.1f}%, €{m['daily_pnl']:+.2f}/day, Sharpe {m['sharpe']:.2f}")
|
||||||
|
|
||||||
|
max_pnl = max(strategies, key=lambda x: x["pnl"])
|
||||||
|
print(f"\n MAX PnL: {max_pnl['name']} — €{max_pnl['pnl']:+,.0f}")
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
"""Strategie di trading — classe base e indicatori condivisi."""
|
||||||
|
from src.strategies.base import Strategy, Signal, BacktestResult, YearlyStats
|
||||||
|
from src.strategies.indicators import (
|
||||||
|
keltner_ratio, detect_squeezes, ema, atr, rv_annualized, rolling_correlation,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"Strategy", "Signal", "BacktestResult", "YearlyStats",
|
||||||
|
"keltner_ratio", "detect_squeezes", "ema", "atr",
|
||||||
|
"rv_annualized", "rolling_correlation",
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
"""Classe base astratta per tutte le strategie di trading."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from src.data.downloader import load_data
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Signal:
|
||||||
|
"""Segnale di trading generato da una strategia."""
|
||||||
|
idx: int
|
||||||
|
direction: int # +1 long, -1 short
|
||||||
|
entry_price: float
|
||||||
|
metadata: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class YearlyStats:
|
||||||
|
year: int
|
||||||
|
trades: int
|
||||||
|
wins: int
|
||||||
|
pnl: float
|
||||||
|
|
||||||
|
@property
|
||||||
|
def accuracy(self) -> float:
|
||||||
|
return self.wins / self.trades * 100 if self.trades > 0 else 0.0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BacktestResult:
|
||||||
|
"""Risultato completo di un backtest."""
|
||||||
|
strategy_name: str
|
||||||
|
asset: str
|
||||||
|
timeframe: str
|
||||||
|
params: dict
|
||||||
|
|
||||||
|
trades: int
|
||||||
|
wins: int
|
||||||
|
pnl: float
|
||||||
|
capital: float
|
||||||
|
initial_capital: float
|
||||||
|
max_dd: float
|
||||||
|
time_in_market_pct: float
|
||||||
|
avg_trade_duration_h: float
|
||||||
|
years_active: int
|
||||||
|
yearly: list[YearlyStats]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def accuracy(self) -> float:
|
||||||
|
return self.wins / self.trades * 100 if self.trades > 0 else 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sharpe(self) -> float:
|
||||||
|
pnls = []
|
||||||
|
for ys in self.yearly:
|
||||||
|
pnls.append(ys.pnl)
|
||||||
|
if len(pnls) < 2 or np.std(pnls) == 0:
|
||||||
|
return 0.0
|
||||||
|
return float(np.mean(pnls) / np.std(pnls) * np.sqrt(len(pnls)))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def daily_pnl(self) -> float:
|
||||||
|
return self.pnl / (self.years_active * 365) if self.years_active > 0 else 0.0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def worst_year(self) -> YearlyStats | None:
|
||||||
|
valid = [y for y in self.yearly if y.trades >= 10]
|
||||||
|
if not valid:
|
||||||
|
valid = self.yearly
|
||||||
|
return min(valid, key=lambda y: y.accuracy) if valid else None
|
||||||
|
|
||||||
|
def print_summary(self):
|
||||||
|
worst = self.worst_year
|
||||||
|
worst_str = f"{worst.year}({worst.accuracy:.0f}%)" if worst else "N/A"
|
||||||
|
dur = f"{self.avg_trade_duration_h:.0f}h" if self.avg_trade_duration_h >= 1 else f"{self.avg_trade_duration_h * 60:.0f}m"
|
||||||
|
print(f" {self.strategy_name:<30s} {self.asset:>3s} {self.timeframe:>3s} "
|
||||||
|
f"{self.trades:>5d}t {self.accuracy:>5.1f}% "
|
||||||
|
f"€{self.pnl:>+9.0f} DD {self.max_dd:>4.1f}% "
|
||||||
|
f"€/d {self.daily_pnl:>+6.2f} "
|
||||||
|
f"Mkt {self.time_in_market_pct:>4.1f}% {dur:>5s} "
|
||||||
|
f"worst={worst_str} {self.years_active}y")
|
||||||
|
|
||||||
|
def print_yearly(self):
|
||||||
|
print(f"\n {self.strategy_name} [{self.asset} {self.timeframe}] — per anno:")
|
||||||
|
print(f" {'Anno':>6s} {'Trades':>7s} {'Acc':>6s} {'PnL€':>9s}")
|
||||||
|
for ys in sorted(self.yearly, key=lambda y: y.year):
|
||||||
|
print(f" {ys.year:>6d} {ys.trades:>7d} {ys.accuracy:>5.1f}% €{ys.pnl:>+8.0f}")
|
||||||
|
|
||||||
|
|
||||||
|
TF_MINUTES = {"1m": 1, "5m": 5, "15m": 15, "1h": 60, "4h": 240, "1d": 1440}
|
||||||
|
|
||||||
|
|
||||||
|
class Strategy(ABC):
|
||||||
|
"""Classe base per tutte le strategie.
|
||||||
|
|
||||||
|
Sottoclassi devono implementare:
|
||||||
|
- name, description, default_assets, default_timeframes
|
||||||
|
- generate_signals(df, timestamps, **params) -> list[Signal]
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str = "unnamed"
|
||||||
|
description: str = ""
|
||||||
|
default_assets: list[str] = ["BTC", "ETH"]
|
||||||
|
default_timeframes: list[str] = ["15m", "1h"]
|
||||||
|
|
||||||
|
# Parametri di backtest
|
||||||
|
fee_rt: float = 0.002
|
||||||
|
leverage: float = 3.0
|
||||||
|
position_size: float = 0.15
|
||||||
|
initial_capital: float = 1000.0
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||||
|
**params) -> list[Signal]:
|
||||||
|
"""Genera segnali di trading dal dataframe OHLCV.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
df: DataFrame con colonne open, high, low, close, volume, timestamp
|
||||||
|
ts: DatetimeIndex UTC dei timestamp
|
||||||
|
**params: parametri specifici della strategia
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Lista di Signal con idx, direction, entry_price
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
def backtest(self, asset: str, tf: str, hold: int = 3,
|
||||||
|
**params) -> BacktestResult | None:
|
||||||
|
"""Esegue backtest su un asset/timeframe."""
|
||||||
|
df = load_data(asset, tf)
|
||||||
|
c = df["close"].values
|
||||||
|
n = len(c)
|
||||||
|
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||||
|
|
||||||
|
sig_params = {**params, "asset": asset, "tf": tf}
|
||||||
|
signals = self.generate_signals(df, ts, **sig_params)
|
||||||
|
if not signals:
|
||||||
|
return None
|
||||||
|
|
||||||
|
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(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=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 run_all(self, assets: list[str] | None = None,
|
||||||
|
timeframes: list[str] | None = None,
|
||||||
|
hold: int = 3, **params) -> list[BacktestResult]:
|
||||||
|
"""Esegue backtest su tutte le combinazioni asset/timeframe."""
|
||||||
|
assets = assets or self.default_assets
|
||||||
|
timeframes = timeframes or self.default_timeframes
|
||||||
|
results = []
|
||||||
|
for asset in assets:
|
||||||
|
for tf in timeframes:
|
||||||
|
r = self.backtest(asset, tf, hold=hold, **params)
|
||||||
|
if r and r.trades >= 20:
|
||||||
|
results.append(r)
|
||||||
|
results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def report(self, results: list[BacktestResult] | None = None,
|
||||||
|
assets: list[str] | None = None,
|
||||||
|
timeframes: list[str] | None = None,
|
||||||
|
hold: int = 3, **params):
|
||||||
|
"""Esegue e stampa report completo."""
|
||||||
|
if results is None:
|
||||||
|
results = self.run_all(assets, timeframes, hold, **params)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 120}")
|
||||||
|
print(f" {self.name} — {self.description}")
|
||||||
|
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 results:
|
||||||
|
r.print_summary()
|
||||||
|
|
||||||
|
if results:
|
||||||
|
best = results[0]
|
||||||
|
best.print_yearly()
|
||||||
|
|
||||||
|
return results
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
"""Indicatori tecnici condivisi tra tutte le strategie."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
def keltner_ratio(close: np.ndarray, high: np.ndarray, low: np.ndarray,
|
||||||
|
window: int = 14) -> np.ndarray:
|
||||||
|
"""Rapporto Bollinger / Keltner. Sotto 1 = squeeze (BB dentro KC)."""
|
||||||
|
n = len(close)
|
||||||
|
r = np.full(n, np.nan)
|
||||||
|
for i in range(window, n):
|
||||||
|
wc = close[i - window:i]
|
||||||
|
wh = high[i - window:i]
|
||||||
|
wl = 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 detect_squeezes(close: np.ndarray, high: np.ndarray, low: np.ndarray,
|
||||||
|
kcr: np.ndarray, sq_thr: float = 0.8,
|
||||||
|
min_dur: int = 5) -> list[dict]:
|
||||||
|
"""Rileva squeeze events: periodi dove BB sta dentro KC."""
|
||||||
|
events: list[dict] = []
|
||||||
|
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
|
||||||
|
dur = i - sq_start
|
||||||
|
if dur < min_dur:
|
||||||
|
continue
|
||||||
|
events.append({
|
||||||
|
"idx": i, "dur": dur, "sq_start": sq_start,
|
||||||
|
"kcr_at_release": kcr[i],
|
||||||
|
})
|
||||||
|
return events
|
||||||
|
|
||||||
|
|
||||||
|
def ema(arr: np.ndarray, period: int) -> np.ndarray:
|
||||||
|
"""Exponential Moving Average."""
|
||||||
|
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 atr(high: np.ndarray, low: np.ndarray, close: np.ndarray,
|
||||||
|
period: int = 14) -> np.ndarray:
|
||||||
|
"""Average True Range (EMA-smoothed)."""
|
||||||
|
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 rv_annualized(close: np.ndarray, window: int) -> np.ndarray:
|
||||||
|
"""Realized volatility annualizzata (hourly data assumed)."""
|
||||||
|
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 rolling_correlation(close_a: np.ndarray, close_b: np.ndarray,
|
||||||
|
window: int = 48) -> np.ndarray:
|
||||||
|
"""Correlazione rolling tra rendimenti logaritmici di due asset."""
|
||||||
|
n = max(len(close_a), len(close_b))
|
||||||
|
ret_a = np.diff(np.log(np.where(close_a == 0, 1e-10, close_a)))
|
||||||
|
ret_b = np.diff(np.log(np.where(close_b[:len(close_a)] == 0, 1e-10, close_b[:len(close_a)])))
|
||||||
|
min_len = min(len(ret_a), len(ret_b))
|
||||||
|
corr = np.full(n, np.nan)
|
||||||
|
for i in range(window, min_len):
|
||||||
|
cv = np.corrcoef(ret_a[i - window:i], ret_b[i - window:i])[0, 1]
|
||||||
|
corr[i + 1] = cv if np.isfinite(cv) else 0
|
||||||
|
return corr
|
||||||
Reference in New Issue
Block a user