Compare commits
3 Commits
56bad4741e
...
bdcef09057
| Author | SHA1 | Date | |
|---|---|---|---|
| bdcef09057 | |||
| d39c75b103 | |||
| f42fec9fac |
@@ -16,3 +16,4 @@ data/processed/
|
|||||||
*.pt
|
*.pt
|
||||||
*.pth
|
*.pth
|
||||||
notebooks/.ipynb_checkpoints/
|
notebooks/.ipynb_checkpoints/
|
||||||
|
data/paper_trades/
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
"""Report accuracy per ANNO × MERCATO delle strategie migliori.
|
||||||
|
|
||||||
|
Esegue ogni strategia vincente su BTC e ETH e produce tabella
|
||||||
|
accuracy/trades per ogni anno. Permette di vedere robustezza temporale
|
||||||
|
e differenze tra mercati.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, ".")
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
STRATEGIES_DIR = Path("scripts/strategies")
|
||||||
|
|
||||||
|
|
||||||
|
def load_class(module_file, class_name):
|
||||||
|
path = STRATEGIES_DIR / f"{module_file}.py"
|
||||||
|
spec = importlib.util.spec_from_file_location(module_file, path)
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return getattr(mod, class_name)
|
||||||
|
|
||||||
|
|
||||||
|
# (label, module, class, params, hold)
|
||||||
|
STRATEGIES = [
|
||||||
|
("SQ02 antifake+vol", "SQ02_squeeze_antifake_vol", "SqueezeAntifakeVol", {}, 3),
|
||||||
|
("MT01 ema20+vol", "MT01_squeeze_mtf_momentum", "SqueezeMTFMomentum",
|
||||||
|
{"ema_period": 20, "min_slope": 0.001, "vol_filter": True}, 3),
|
||||||
|
("PD01 vtb3 vm1.3", "PD01_price_volume_divergence", "PriceVolumeDivergence",
|
||||||
|
{}, 3),
|
||||||
|
("CM01 cb6+vol", "CM01_cross_market_momentum", "CrossMarketMomentum",
|
||||||
|
{"cross_bars": 6, "mom_min": 0.001, "use_vol": True}, 3),
|
||||||
|
("AD01 lt.65 ht.95", "AD01_adaptive_squeeze", "AdaptiveSqueeze",
|
||||||
|
{"low_thr": 0.65, "high_thr": 0.95, "use_vol": True}, 3),
|
||||||
|
]
|
||||||
|
|
||||||
|
ASSETS = ["BTC", "ETH"]
|
||||||
|
TF = "15m"
|
||||||
|
ALL_YEARS = list(range(2018, 2027))
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
results = {} # (label, asset) -> BacktestResult
|
||||||
|
|
||||||
|
for label, module, cls_name, params, hold in STRATEGIES:
|
||||||
|
try:
|
||||||
|
cls = load_class(module, cls_name)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"SKIP {label}: {e}")
|
||||||
|
continue
|
||||||
|
strat = cls()
|
||||||
|
for asset in ASSETS:
|
||||||
|
try:
|
||||||
|
r = strat.backtest(asset, TF, hold=hold, **params)
|
||||||
|
if r:
|
||||||
|
results[(label, asset)] = r
|
||||||
|
except Exception as e:
|
||||||
|
print(f" errore {label} {asset}: {e}")
|
||||||
|
|
||||||
|
# ── Tabella ACCURACY per anno × mercato ──────────────────────────
|
||||||
|
print(f"\n{'=' * 140}")
|
||||||
|
print(f" ACCURACY PER ANNO × MERCATO — {TF} (fee 0.2% RT, leva 3x, pos 15%)")
|
||||||
|
print(f"{'=' * 140}")
|
||||||
|
|
||||||
|
header = f" {'Strategia':<22s} {'Mkt':>3s}"
|
||||||
|
for y in ALL_YEARS:
|
||||||
|
header += f" {y:>7d}"
|
||||||
|
header += f" │ {'TOT':>6s} {'DD%':>5s} {'Worst':>10s}"
|
||||||
|
print(header)
|
||||||
|
print(f" {'─' * 136}")
|
||||||
|
|
||||||
|
for label, module, cls_name, params, hold in STRATEGIES:
|
||||||
|
for asset in ASSETS:
|
||||||
|
r = results.get((label, asset))
|
||||||
|
if not r:
|
||||||
|
continue
|
||||||
|
yd = {ys.year: ys for ys in r.yearly}
|
||||||
|
line = f" {label:<22s} {asset:>3s}"
|
||||||
|
for y in ALL_YEARS:
|
||||||
|
if y in yd:
|
||||||
|
line += f" {yd[y].accuracy:>5.0f}%↑" if yd[y].accuracy >= 80 else f" {yd[y].accuracy:>5.0f}% "
|
||||||
|
else:
|
||||||
|
line += f" {'—':>7s}"
|
||||||
|
worst = r.worst_year
|
||||||
|
worst_str = f"{worst.year}({worst.accuracy:.0f}%)" if worst else "N/A"
|
||||||
|
line += f" │ {r.accuracy:>5.1f}% {r.max_dd:>4.1f}% {worst_str:>10s}"
|
||||||
|
print(line)
|
||||||
|
print(f" {'·' * 136}")
|
||||||
|
|
||||||
|
# ── Tabella TRADES per anno × mercato ────────────────────────────
|
||||||
|
print(f"\n{'=' * 140}")
|
||||||
|
print(f" NUMERO TRADES PER ANNO × MERCATO")
|
||||||
|
print(f"{'=' * 140}")
|
||||||
|
|
||||||
|
header = f" {'Strategia':<22s} {'Mkt':>3s}"
|
||||||
|
for y in ALL_YEARS:
|
||||||
|
header += f" {y:>7d}"
|
||||||
|
header += f" │ {'TOT':>6s} {'€/day':>6s}"
|
||||||
|
print(header)
|
||||||
|
print(f" {'─' * 130}")
|
||||||
|
|
||||||
|
for label, module, cls_name, params, hold in STRATEGIES:
|
||||||
|
for asset in ASSETS:
|
||||||
|
r = results.get((label, asset))
|
||||||
|
if not r:
|
||||||
|
continue
|
||||||
|
yd = {ys.year: ys for ys in r.yearly}
|
||||||
|
line = f" {label:<22s} {asset:>3s}"
|
||||||
|
for y in ALL_YEARS:
|
||||||
|
if y in yd:
|
||||||
|
line += f" {yd[y].trades:>7d}"
|
||||||
|
else:
|
||||||
|
line += f" {'—':>7s}"
|
||||||
|
line += f" │ {r.trades:>6d} {r.daily_pnl:>+6.2f}"
|
||||||
|
print(line)
|
||||||
|
print(f" {'·' * 130}")
|
||||||
|
|
||||||
|
# ── Tabella PnL per anno × mercato ──────────────────────────────
|
||||||
|
print(f"\n{'=' * 140}")
|
||||||
|
print(f" PnL € PER ANNO × MERCATO (su €1000, no compounding tra anni)")
|
||||||
|
print(f"{'=' * 140}")
|
||||||
|
|
||||||
|
header = f" {'Strategia':<22s} {'Mkt':>3s}"
|
||||||
|
for y in ALL_YEARS:
|
||||||
|
header += f" {y:>7d}"
|
||||||
|
header += f" │ {'TOT€':>8s}"
|
||||||
|
print(header)
|
||||||
|
print(f" {'─' * 132}")
|
||||||
|
|
||||||
|
for label, module, cls_name, params, hold in STRATEGIES:
|
||||||
|
for asset in ASSETS:
|
||||||
|
r = results.get((label, asset))
|
||||||
|
if not r:
|
||||||
|
continue
|
||||||
|
yd = {ys.year: ys for ys in r.yearly}
|
||||||
|
line = f" {label:<22s} {asset:>3s}"
|
||||||
|
for y in ALL_YEARS:
|
||||||
|
if y in yd:
|
||||||
|
line += f" {yd[y].pnl:>+7.0f}"
|
||||||
|
else:
|
||||||
|
line += f" {'—':>7s}"
|
||||||
|
line += f" │ {r.pnl:>+8.0f}"
|
||||||
|
print(line)
|
||||||
|
print(f" {'·' * 132}")
|
||||||
|
|
||||||
|
# ── Sintesi: media per anno (tutte le strategie) ────────────────
|
||||||
|
print(f"\n{'=' * 140}")
|
||||||
|
print(f" SINTESI — Accuracy media per anno (tutte le strategie, BTC+ETH)")
|
||||||
|
print(f"{'=' * 140}")
|
||||||
|
year_acc = {y: [] for y in ALL_YEARS}
|
||||||
|
for (label, asset), r in results.items():
|
||||||
|
for ys in r.yearly:
|
||||||
|
if ys.trades >= 10:
|
||||||
|
year_acc[ys.year].append(ys.accuracy)
|
||||||
|
|
||||||
|
line_y = f" {'Anno':<22s} "
|
||||||
|
line_a = f" {'Acc media':<22s} "
|
||||||
|
for y in ALL_YEARS:
|
||||||
|
accs = year_acc[y]
|
||||||
|
avg = sum(accs) / len(accs) if accs else 0
|
||||||
|
line_y += f" {y:>7d}"
|
||||||
|
line_a += f" {avg:>6.1f}%"
|
||||||
|
print(line_y)
|
||||||
|
print(line_a)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
run()
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
"""AD01 — Adaptive Squeeze Threshold.
|
||||||
|
|
||||||
|
Problema SQ02: sq_threshold fisso (0.8) non si adatta al regime di volatilità.
|
||||||
|
Soluzione: threshold adattivo basato su volatilità recente.
|
||||||
|
|
||||||
|
Logica:
|
||||||
|
- Calcola volatilità rolling (std dei rendimenti su finestra 100 barre)
|
||||||
|
- Confronta con percentile storico (rolling 500 barre)
|
||||||
|
- Alta vol (>70° percentile) → soglia BASSA (0.65) — squeeze più "lenti"
|
||||||
|
- Bassa vol (<30° percentile) → soglia ALTA (0.90) — squeeze "stretti"
|
||||||
|
- Vol media → soglia standard (0.80)
|
||||||
|
|
||||||
|
Razionale: in mercati calmi, il BB si stringe molto → sq_threshold alto cattura
|
||||||
|
segnali migliori. In mercati volatili, bastano squeeze minori per essere significativi.
|
||||||
|
|
||||||
|
Anti-overfitting: solo 3 parametri (low_thr, mid_thr, high_thr), logica deterministica.
|
||||||
|
Eredita antifakeout + volume da SQ02.
|
||||||
|
"""
|
||||||
|
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, ema
|
||||||
|
from src.data.downloader import load_data
|
||||||
|
|
||||||
|
|
||||||
|
def _adaptive_sq_threshold(close: np.ndarray,
|
||||||
|
vol_window: int = 100,
|
||||||
|
regime_window: int = 500,
|
||||||
|
low_thr: float = 0.65,
|
||||||
|
mid_thr: float = 0.80,
|
||||||
|
high_thr: float = 0.90) -> np.ndarray:
|
||||||
|
"""Calcola sq_threshold adattivo per ogni barra."""
|
||||||
|
n = len(close)
|
||||||
|
lr = np.diff(np.log(np.where(close <= 0, 1e-10, close)))
|
||||||
|
vol = np.full(n, np.nan)
|
||||||
|
for i in range(vol_window, n):
|
||||||
|
vol[i] = np.std(lr[i - vol_window:i])
|
||||||
|
|
||||||
|
# Percentile rolling della volatilità
|
||||||
|
thresh = np.full(n, mid_thr)
|
||||||
|
for i in range(regime_window, n):
|
||||||
|
if np.isnan(vol[i]):
|
||||||
|
continue
|
||||||
|
hist = vol[i - regime_window:i]
|
||||||
|
hist = hist[~np.isnan(hist)]
|
||||||
|
if len(hist) < 10:
|
||||||
|
continue
|
||||||
|
p30 = np.percentile(hist, 30)
|
||||||
|
p70 = np.percentile(hist, 70)
|
||||||
|
if vol[i] < p30:
|
||||||
|
thresh[i] = high_thr # vol bassa → soglia alta
|
||||||
|
elif vol[i] > p70:
|
||||||
|
thresh[i] = low_thr # vol alta → soglia bassa
|
||||||
|
else:
|
||||||
|
thresh[i] = mid_thr
|
||||||
|
return thresh
|
||||||
|
|
||||||
|
|
||||||
|
def _detect_adaptive_squeezes(close, high, low, kcr, adaptive_thr,
|
||||||
|
min_dur: int = 5) -> list[dict]:
|
||||||
|
"""Squeeze con threshold adattivo per ogni barra."""
|
||||||
|
events = []
|
||||||
|
in_sq = False
|
||||||
|
sq_start = 0
|
||||||
|
for i in range(1, len(close)):
|
||||||
|
if np.isnan(kcr[i]) or np.isnan(adaptive_thr[i]):
|
||||||
|
continue
|
||||||
|
thr = adaptive_thr[i]
|
||||||
|
is_sq = kcr[i] < 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],
|
||||||
|
"thr_used": adaptive_thr[i],
|
||||||
|
})
|
||||||
|
return events
|
||||||
|
|
||||||
|
|
||||||
|
class AdaptiveSqueeze(Strategy):
|
||||||
|
name = "AD01_adaptive_squeeze"
|
||||||
|
description = "Squeeze con threshold adattivo a regime volatilità"
|
||||||
|
default_assets = ["BTC", "ETH"]
|
||||||
|
default_timeframes = ["15m", "1h"]
|
||||||
|
fee_rt = 0.002
|
||||||
|
leverage = 3.0
|
||||||
|
position_size = 0.15
|
||||||
|
initial_capital = 1000.0
|
||||||
|
|
||||||
|
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)
|
||||||
|
low_thr = params.get("low_thr", 0.65)
|
||||||
|
mid_thr = params.get("mid_thr", 0.80)
|
||||||
|
high_thr = params.get("high_thr", 0.90)
|
||||||
|
retrace_limit = params.get("retrace_limit", 0.6)
|
||||||
|
vol_mult = params.get("vol_multiplier", 1.3)
|
||||||
|
use_vol = params.get("use_vol", True)
|
||||||
|
vol_window = params.get("vol_window", 100)
|
||||||
|
regime_window = params.get("regime_window", 500)
|
||||||
|
|
||||||
|
kcr = keltner_ratio(c, h, l, bb_w)
|
||||||
|
adaptive_thr = _adaptive_sq_threshold(
|
||||||
|
c, vol_window, regime_window, low_thr, mid_thr, high_thr
|
||||||
|
)
|
||||||
|
events = _detect_adaptive_squeezes(c, h, l, kcr, adaptive_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
|
||||||
|
direction = 1 if first_ret > 0 else -1
|
||||||
|
|
||||||
|
# Anti-fakeout
|
||||||
|
br = h[i] - l[i]
|
||||||
|
if br > 0:
|
||||||
|
if direction == 1 and (h[i] - c[i]) / br > retrace_limit:
|
||||||
|
continue
|
||||||
|
elif direction == -1 and (c[i] - l[i]) / br > retrace_limit:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Volume confirm
|
||||||
|
if use_vol:
|
||||||
|
sq_start = ev["sq_start"]
|
||||||
|
avg_sq_v = np.mean(v[sq_start:i])
|
||||||
|
if avg_sq_v > 0 and v[i] <= avg_sq_v * vol_mult:
|
||||||
|
continue
|
||||||
|
|
||||||
|
signals.append(Signal(
|
||||||
|
idx=i,
|
||||||
|
direction=direction,
|
||||||
|
entry_price=c[i - 1],
|
||||||
|
metadata={
|
||||||
|
"dur": ev["dur"],
|
||||||
|
"thr_used": ev.get("thr_used", mid_thr),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
return signals
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
strategy = AdaptiveSqueeze()
|
||||||
|
|
||||||
|
configs = [
|
||||||
|
# low_thr, mid_thr, high_thr, use_vol
|
||||||
|
{"low_thr": 0.65, "mid_thr": 0.80, "high_thr": 0.90, "use_vol": True},
|
||||||
|
{"low_thr": 0.65, "mid_thr": 0.80, "high_thr": 0.90, "use_vol": False},
|
||||||
|
{"low_thr": 0.60, "mid_thr": 0.78, "high_thr": 0.92, "use_vol": True},
|
||||||
|
{"low_thr": 0.70, "mid_thr": 0.82, "high_thr": 0.90, "use_vol": True},
|
||||||
|
{"low_thr": 0.65, "mid_thr": 0.80, "high_thr": 0.95, "use_vol": True},
|
||||||
|
{"low_thr": 0.65, "mid_thr": 0.80, "high_thr": 0.90,
|
||||||
|
"use_vol": True, "vol_multiplier": 1.2},
|
||||||
|
]
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
for cfg in configs:
|
||||||
|
for asset in ["BTC", "ETH"]:
|
||||||
|
for tf in ["15m", "1h"]:
|
||||||
|
for hold in [3, 6]:
|
||||||
|
r = strategy.backtest(asset, tf, hold=hold, **cfg)
|
||||||
|
if r and r.trades >= 20:
|
||||||
|
lbl = (f"AD01 lt={cfg['low_thr']} ht={cfg['high_thr']} "
|
||||||
|
f"v={cfg['use_vol']} h={hold}")
|
||||||
|
r.strategy_name = lbl
|
||||||
|
all_results.append(r)
|
||||||
|
|
||||||
|
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 130}")
|
||||||
|
print(" AD01 ADAPTIVE SQUEEZE THRESHOLD — TOP 20")
|
||||||
|
print(f"{'=' * 130}")
|
||||||
|
print(f" {'Nome':<50s} {'A/T':>7s} {'Trades':>6s} {'Acc':>6s} "
|
||||||
|
f"{'PnL€':>10s} {'DD%':>6s} {'€/day':>7s} "
|
||||||
|
f"{'Mkt%':>5s} {'Dur':>5s} {'Anni':>4s}")
|
||||||
|
print(f" {'─' * 120}")
|
||||||
|
for r in all_results[:20]:
|
||||||
|
r.print_summary()
|
||||||
|
|
||||||
|
if all_results:
|
||||||
|
all_results[0].print_yearly()
|
||||||
|
|
||||||
|
print(f"\n BENCHMARK SQ02: 79.7% acc, 1250t, DD 6.5%, €5.23/day, 9 anni")
|
||||||
|
print(f" BENCHMARK MT01: 82.7% acc, 503t, DD 5.9%")
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
"""CM01 — Cross-Market Momentum Filter.
|
||||||
|
|
||||||
|
Squeeze su asset primario, entra SOLO se l'altro asset (BTC↔ETH)
|
||||||
|
mostra momentum short-term nella STESSA direzione.
|
||||||
|
|
||||||
|
Differenza da MT01: MT01 usa EMA slope su 1h (trend lento).
|
||||||
|
CM01 usa rendimento grezzo degli ultimi 3-6 bar sull'asset cross
|
||||||
|
(momentum veloce, stesso timeframe).
|
||||||
|
|
||||||
|
Razionale: BTC e ETH sono altamente correlati ma non perfettamente.
|
||||||
|
Se BTC fa squeeze breakout UP e anche ETH sta salendo (momentum 3-6 bar),
|
||||||
|
la probabilità di continuazione è maggiore perché c'è consenso di mercato.
|
||||||
|
|
||||||
|
Anti-overfitting: 1 parametro chiave (cross_bars 3-6), logica deterministica.
|
||||||
|
Eredita antifakeout + volume da SQ02.
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
from src.strategies.indicators import keltner_ratio, detect_squeezes
|
||||||
|
from src.data.downloader import load_data
|
||||||
|
|
||||||
|
|
||||||
|
class CrossMarketMomentum(Strategy):
|
||||||
|
name = "CM01_cross_momentum"
|
||||||
|
description = "Squeeze + cross-asset short-term momentum filter"
|
||||||
|
default_assets = ["BTC", "ETH"]
|
||||||
|
default_timeframes = ["15m", "1h"]
|
||||||
|
fee_rt = 0.002
|
||||||
|
leverage = 3.0
|
||||||
|
position_size = 0.15
|
||||||
|
initial_capital = 1000.0
|
||||||
|
|
||||||
|
# Map asset → cross asset
|
||||||
|
_CROSS = {"BTC": "ETH", "ETH": "BTC"}
|
||||||
|
|
||||||
|
def generate_signals(self, df: pd.DataFrame, ts: pd.DatetimeIndex,
|
||||||
|
**params) -> list[Signal]:
|
||||||
|
"""Genera segnali con cross-market momentum."""
|
||||||
|
c = df["close"].values
|
||||||
|
h = df["high"].values
|
||||||
|
l = df["low"].values
|
||||||
|
v = df["volume"].values
|
||||||
|
n = len(c)
|
||||||
|
ts_ms = df["timestamp"].values
|
||||||
|
|
||||||
|
asset = params.get("asset", "BTC")
|
||||||
|
tf = params.get("tf", "15m")
|
||||||
|
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)
|
||||||
|
use_vol = params.get("use_vol", True)
|
||||||
|
cross_bars = params.get("cross_bars", 4) # barre momentum cross
|
||||||
|
mom_min = params.get("mom_min", 0.0) # momentum minimo (0 = solo direzione)
|
||||||
|
|
||||||
|
# Carica cross asset
|
||||||
|
cross_asset = self._CROSS.get(asset)
|
||||||
|
if cross_asset is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
df_cross = load_data(cross_asset, tf)
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
c_cross = df_cross["close"].values
|
||||||
|
ts_cross_ms = df_cross["timestamp"].values
|
||||||
|
n_cross = len(c_cross)
|
||||||
|
|
||||||
|
# Momentum cross: rendimento log su cross_bars barre
|
||||||
|
cross_mom = np.full(n_cross, np.nan)
|
||||||
|
for i in range(cross_bars, n_cross):
|
||||||
|
if c_cross[i - cross_bars] > 0:
|
||||||
|
cross_mom[i] = np.log(c_cross[i] / c_cross[i - cross_bars])
|
||||||
|
|
||||||
|
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
|
||||||
|
direction = 1 if first_ret > 0 else -1
|
||||||
|
|
||||||
|
# Anti-fakeout
|
||||||
|
br = h[i] - l[i]
|
||||||
|
if br > 0:
|
||||||
|
if direction == 1 and (h[i] - c[i]) / br > retrace_limit:
|
||||||
|
continue
|
||||||
|
elif direction == -1 and (c[i] - l[i]) / br > retrace_limit:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Volume confirm
|
||||||
|
if use_vol:
|
||||||
|
sq_start = ev["sq_start"]
|
||||||
|
avg_sq_v = np.mean(v[sq_start:i])
|
||||||
|
if avg_sq_v > 0 and v[i] <= avg_sq_v * vol_mult:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Cross-market momentum: trova indice cross corrispondente
|
||||||
|
i_cross = np.searchsorted(ts_cross_ms, ts_ms[i]) - 1
|
||||||
|
if i_cross < cross_bars or i_cross >= n_cross:
|
||||||
|
continue
|
||||||
|
mom = cross_mom[i_cross]
|
||||||
|
if np.isnan(mom):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Filtra per direzione concordante
|
||||||
|
if direction == 1 and mom <= mom_min:
|
||||||
|
continue
|
||||||
|
if direction == -1 and mom >= -mom_min:
|
||||||
|
continue
|
||||||
|
|
||||||
|
signals.append(Signal(
|
||||||
|
idx=i,
|
||||||
|
direction=direction,
|
||||||
|
entry_price=c[i - 1],
|
||||||
|
metadata={
|
||||||
|
"dur": ev["dur"],
|
||||||
|
"cross_mom": float(mom),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
return signals
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
strategy = CrossMarketMomentum()
|
||||||
|
|
||||||
|
configs = [
|
||||||
|
# cross_bars, mom_min, use_vol
|
||||||
|
{"cross_bars": 3, "mom_min": 0.0, "use_vol": True},
|
||||||
|
{"cross_bars": 4, "mom_min": 0.0, "use_vol": True},
|
||||||
|
{"cross_bars": 6, "mom_min": 0.0, "use_vol": True},
|
||||||
|
{"cross_bars": 4, "mom_min": 0.001, "use_vol": True},
|
||||||
|
{"cross_bars": 4, "mom_min": 0.002, "use_vol": True},
|
||||||
|
{"cross_bars": 4, "mom_min": 0.0, "use_vol": False},
|
||||||
|
{"cross_bars": 3, "mom_min": 0.001, "use_vol": False},
|
||||||
|
{"cross_bars": 6, "mom_min": 0.001, "use_vol": True},
|
||||||
|
]
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
for cfg in configs:
|
||||||
|
for asset in ["BTC", "ETH"]:
|
||||||
|
for tf in ["15m", "1h"]:
|
||||||
|
for hold in [3, 6]:
|
||||||
|
r = strategy.backtest(asset, tf, hold=hold,
|
||||||
|
cross_bars=cfg["cross_bars"],
|
||||||
|
mom_min=cfg["mom_min"],
|
||||||
|
use_vol=cfg["use_vol"])
|
||||||
|
if r and r.trades >= 20:
|
||||||
|
lbl = (f"CM01 cb={cfg['cross_bars']} "
|
||||||
|
f"mm={cfg['mom_min']} v={cfg['use_vol']} h={hold}")
|
||||||
|
r.strategy_name = lbl
|
||||||
|
all_results.append(r)
|
||||||
|
|
||||||
|
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 130}")
|
||||||
|
print(" CM01 CROSS-MARKET MOMENTUM — TOP 20")
|
||||||
|
print(f"{'=' * 130}")
|
||||||
|
print(f" {'Nome':<50s} {'A/T':>7s} {'Trades':>6s} {'Acc':>6s} "
|
||||||
|
f"{'PnL€':>10s} {'DD%':>6s} {'€/day':>7s} "
|
||||||
|
f"{'Mkt%':>5s} {'Dur':>5s} {'Anni':>4s}")
|
||||||
|
print(f" {'─' * 120}")
|
||||||
|
for r in all_results[:20]:
|
||||||
|
r.print_summary()
|
||||||
|
|
||||||
|
if all_results:
|
||||||
|
all_results[0].print_yearly()
|
||||||
|
|
||||||
|
print(f"\n BENCHMARK SQ02: 79.7% acc, 1250t, DD 6.5%, €5.23/day, 9 anni")
|
||||||
|
print(f" BENCHMARK MT01: 82.7% acc, 503t, DD 5.9%")
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
"""MT01 — Squeeze + Multi-Timeframe Momentum.
|
||||||
|
|
||||||
|
Problema SQ02: entra al breakout 15m ma non sa se il trend 1h è allineato.
|
||||||
|
Soluzione: squeeze su 15m + conferma momentum su 1h.
|
||||||
|
|
||||||
|
Anti-overfitting: usa solo 2 indicatori (squeeze + EMA slope),
|
||||||
|
nessun parametro complesso.
|
||||||
|
|
||||||
|
IN:
|
||||||
|
- OHLCV 15m + 1h per lo stesso asset
|
||||||
|
- Parametri: sq_threshold, ema_period_1h, min_slope
|
||||||
|
|
||||||
|
OUT:
|
||||||
|
- Signal al breakout 15m confermato da trend 1h
|
||||||
|
- BacktestResult
|
||||||
|
|
||||||
|
Logica:
|
||||||
|
1. Squeeze release su 15m (come SQ01)
|
||||||
|
2. Antifakeout filter (come SQ02)
|
||||||
|
3. Check 1h: EMA slope positiva per long, negativa per short
|
||||||
|
4. Check 1h: prezzo sopra/sotto EMA per conferma trend
|
||||||
|
5. Entra solo se 15m e 1h concordano
|
||||||
|
"""
|
||||||
|
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, ema
|
||||||
|
from src.data.downloader import load_data
|
||||||
|
|
||||||
|
|
||||||
|
class SqueezeMTFMomentum(Strategy):
|
||||||
|
name = "MT01_squeeze_mtf"
|
||||||
|
description = "Squeeze 15m + momentum trend 1h — multi-timeframe"
|
||||||
|
default_assets = ["BTC", "ETH"]
|
||||||
|
default_timeframes = ["15m"]
|
||||||
|
fee_rt = 0.002
|
||||||
|
|
||||||
|
def generate_signals(self, df, ts, **params):
|
||||||
|
"""Genera segnali squeeze 15m confermati da trend 1h."""
|
||||||
|
c = df["close"].values
|
||||||
|
h = df["high"].values
|
||||||
|
l = df["low"].values
|
||||||
|
v = df["volume"].values
|
||||||
|
n = len(c)
|
||||||
|
|
||||||
|
asset = params.get("asset", "BTC")
|
||||||
|
sq_thr = params.get("sq_threshold", 0.8)
|
||||||
|
ema_period = params.get("ema_period", 50)
|
||||||
|
min_slope_val = params.get("min_slope", 0.001)
|
||||||
|
use_antifake = params.get("antifake", True)
|
||||||
|
use_vol = params.get("vol_filter", False)
|
||||||
|
|
||||||
|
kcr = keltner_ratio(c, h, l, 14)
|
||||||
|
events = detect_squeezes(c, h, l, kcr, sq_thr)
|
||||||
|
|
||||||
|
df_1h = load_data(asset, "1h")
|
||||||
|
c1h = df_1h["close"].values
|
||||||
|
ts1h_ms = df_1h["timestamp"].values
|
||||||
|
n1h = len(c1h)
|
||||||
|
ema_1h = ema(c1h, ema_period)
|
||||||
|
ema_slope_arr = np.full(n1h, np.nan)
|
||||||
|
for i in range(5, n1h):
|
||||||
|
if not np.isnan(ema_1h[i]) and not np.isnan(ema_1h[i-5]) and ema_1h[i-5] > 0:
|
||||||
|
ema_slope_arr[i] = (ema_1h[i] - ema_1h[i-5]) / ema_1h[i-5]
|
||||||
|
|
||||||
|
ts_ms = df["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
|
||||||
|
if use_antifake:
|
||||||
|
br = h[i] - l[i]
|
||||||
|
if br > 0:
|
||||||
|
if c[i] > c[i-1] and (h[i] - c[i]) / br > 0.6:
|
||||||
|
continue
|
||||||
|
elif c[i] <= c[i-1] and (c[i] - l[i]) / br > 0.6:
|
||||||
|
continue
|
||||||
|
if use_vol:
|
||||||
|
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
|
||||||
|
i1h = np.searchsorted(ts1h_ms, ts_ms[i]) - 1
|
||||||
|
if i1h < ema_period or i1h >= n1h:
|
||||||
|
continue
|
||||||
|
if np.isnan(ema_1h[i1h]) or np.isnan(ema_slope_arr[i1h]):
|
||||||
|
continue
|
||||||
|
if direction == 1:
|
||||||
|
if c1h[i1h] < ema_1h[i1h] or ema_slope_arr[i1h] < min_slope_val:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if c1h[i1h] > ema_1h[i1h] or ema_slope_arr[i1h] > -min_slope_val:
|
||||||
|
continue
|
||||||
|
|
||||||
|
signals.append(Signal(idx=i, direction=direction, entry_price=c[i-1]))
|
||||||
|
|
||||||
|
return signals
|
||||||
|
|
||||||
|
def backtest(self, asset, tf="15m", hold=3, **params):
|
||||||
|
sq_thr = params.get("sq_threshold", 0.8)
|
||||||
|
ema_period = params.get("ema_period", 50)
|
||||||
|
min_slope = params.get("min_slope", 0.001)
|
||||||
|
use_antifake = params.get("antifake", True)
|
||||||
|
use_vol = params.get("vol_filter", False)
|
||||||
|
|
||||||
|
# Carica 15m e 1h
|
||||||
|
df_15m = load_data(asset, "15m")
|
||||||
|
df_1h = load_data(asset, "1h")
|
||||||
|
|
||||||
|
c15 = df_15m["close"].values
|
||||||
|
h15 = df_15m["high"].values
|
||||||
|
l15 = df_15m["low"].values
|
||||||
|
v15 = df_15m["volume"].values
|
||||||
|
n15 = len(c15)
|
||||||
|
ts15 = pd.to_datetime(df_15m["timestamp"], unit="ms", utc=True)
|
||||||
|
ts15_ms = df_15m["timestamp"].values
|
||||||
|
|
||||||
|
c1h = df_1h["close"].values
|
||||||
|
ts1h_ms = df_1h["timestamp"].values
|
||||||
|
n1h = len(c1h)
|
||||||
|
|
||||||
|
kcr = keltner_ratio(c15, h15, l15, 14)
|
||||||
|
events = detect_squeezes(c15, h15, l15, kcr, sq_thr)
|
||||||
|
|
||||||
|
# EMA su 1h
|
||||||
|
ema_1h = ema(c1h, ema_period)
|
||||||
|
|
||||||
|
# EMA slope (variazione percentuale su 5 barre)
|
||||||
|
ema_slope = np.full(n1h, np.nan)
|
||||||
|
for i in range(5, n1h):
|
||||||
|
if not np.isnan(ema_1h[i]) and not np.isnan(ema_1h[i - 5]) and ema_1h[i - 5] > 0:
|
||||||
|
ema_slope[i] = (ema_1h[i] - ema_1h[i - 5]) / ema_1h[i - 5]
|
||||||
|
|
||||||
|
yearly = {}
|
||||||
|
capital = float(self.initial_capital)
|
||||||
|
peak = capital
|
||||||
|
max_dd = 0.0
|
||||||
|
total_bars = 0
|
||||||
|
|
||||||
|
for ev in events:
|
||||||
|
i = ev["idx"]
|
||||||
|
if i + hold + 1 >= n15 or i < 1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
first_ret = (c15[i] - c15[i - 1]) / c15[i - 1] if c15[i - 1] > 0 else 0
|
||||||
|
if abs(first_ret) < 0.001:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Antifake
|
||||||
|
if use_antifake:
|
||||||
|
br = h15[i] - l15[i]
|
||||||
|
if br > 0:
|
||||||
|
if c15[i] > c15[i - 1] and (h15[i] - c15[i]) / br > 0.6:
|
||||||
|
continue
|
||||||
|
elif c15[i] <= c15[i - 1] and (c15[i] - l15[i]) / br > 0.6:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Volume filter
|
||||||
|
if use_vol:
|
||||||
|
avg_v = np.mean(v15[ev["sq_start"]:i])
|
||||||
|
if avg_v > 0 and v15[i] <= avg_v * 1.3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
direction = 1 if first_ret > 0 else -1
|
||||||
|
|
||||||
|
# Trova indice 1h corrispondente
|
||||||
|
i1h = np.searchsorted(ts1h_ms, ts15_ms[i]) - 1
|
||||||
|
if i1h < ema_period or i1h >= n1h or np.isnan(ema_1h[i1h]) or np.isnan(ema_slope[i1h]):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Conferma trend 1h
|
||||||
|
if direction == 1:
|
||||||
|
if c1h[i1h] < ema_1h[i1h]:
|
||||||
|
continue
|
||||||
|
if ema_slope[i1h] < min_slope:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if c1h[i1h] > ema_1h[i1h]:
|
||||||
|
continue
|
||||||
|
if ema_slope[i1h] > -min_slope:
|
||||||
|
continue
|
||||||
|
|
||||||
|
entry = c15[i - 1]
|
||||||
|
exit_price = c15[min(i + hold - 1, n15 - 1)]
|
||||||
|
actual = (exit_price - entry) / entry * 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 = ts15.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="15m", 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 / n15 * 100,
|
||||||
|
avg_trade_duration_h=hold * 15 / 60, years_active=len(yearly), yearly=yearly_stats,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
strategy = SqueezeMTFMomentum()
|
||||||
|
|
||||||
|
configs = [
|
||||||
|
("ema50 sl0.1%", {"ema_period": 50, "min_slope": 0.001}),
|
||||||
|
("ema50 sl0.05%", {"ema_period": 50, "min_slope": 0.0005}),
|
||||||
|
("ema50 sl0.2%", {"ema_period": 50, "min_slope": 0.002}),
|
||||||
|
("ema20 sl0.1%", {"ema_period": 20, "min_slope": 0.001}),
|
||||||
|
("ema50 sl0.1%+vol", {"ema_period": 50, "min_slope": 0.001, "vol_filter": True}),
|
||||||
|
("ema20 sl0.1%+vol", {"ema_period": 20, "min_slope": 0.001, "vol_filter": True}),
|
||||||
|
("ema50 noAF", {"ema_period": 50, "min_slope": 0.001, "antifake": False}),
|
||||||
|
("ema100 sl0.05%", {"ema_period": 100, "min_slope": 0.0005}),
|
||||||
|
]
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
for label, params in configs:
|
||||||
|
for asset in ["BTC", "ETH"]:
|
||||||
|
for hold in [3, 6]:
|
||||||
|
r = strategy.backtest(asset, "15m", hold=hold, **params)
|
||||||
|
if r and r.trades >= 30:
|
||||||
|
r.strategy_name = f"MT01 {label} h={hold}"
|
||||||
|
all_results.append(r)
|
||||||
|
|
||||||
|
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||||
|
print(f"\n{'=' * 130}")
|
||||||
|
print(f" MT01 SQUEEZE + MTF MOMENTUM — TOP 20")
|
||||||
|
print(f"{'=' * 130}")
|
||||||
|
for r in all_results[:20]:
|
||||||
|
r.print_summary()
|
||||||
|
if all_results:
|
||||||
|
all_results[0].print_yearly()
|
||||||
|
|
||||||
|
print(f"\n BENCHMARK SQ02: 79.7% acc, 1250t, DD 6.5%, 9 anni, €5.23/day")
|
||||||
@@ -0,0 +1,158 @@
|
|||||||
|
"""PD01 — Price-Volume Divergence Squeeze.
|
||||||
|
|
||||||
|
Estende SQ02 con volume TREND come filtro:
|
||||||
|
- Breakout UP con volume CRESCENTE (ultimi 3 bar vs media squeeze) → ENTRA
|
||||||
|
- Breakout UP con volume CALANTE → SALTA (divergenza bearish)
|
||||||
|
- Viceversa per short
|
||||||
|
|
||||||
|
Logica anti-fakeout:
|
||||||
|
1. Squeeze rilascio (come SQ02)
|
||||||
|
2. Anti-fakeout candela (come SQ02)
|
||||||
|
3. Volume al breakout > media squeeze (come SQ02)
|
||||||
|
4. NUOVO: volume trending UP nelle ultime 3 barre prima del breakout
|
||||||
|
|
||||||
|
Parametri semplici, nessun overfitting.
|
||||||
|
"""
|
||||||
|
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 PriceVolumeDivergence(Strategy):
|
||||||
|
name = "PD01_price_vol_div"
|
||||||
|
description = "Squeeze + antifakeout + volume trend confirmation"
|
||||||
|
default_assets = ["BTC", "ETH"]
|
||||||
|
default_timeframes = ["15m", "1h"]
|
||||||
|
fee_rt = 0.002
|
||||||
|
leverage = 3.0
|
||||||
|
position_size = 0.15
|
||||||
|
initial_capital = 1000.0
|
||||||
|
|
||||||
|
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)
|
||||||
|
vol_trend_bars = params.get("vol_trend_bars", 3) # barre per trend volume
|
||||||
|
|
||||||
|
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 < vol_trend_bars + 1 or i >= n:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Direzione breakout
|
||||||
|
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
|
||||||
|
|
||||||
|
# Anti-fakeout
|
||||||
|
br = h[i] - l[i]
|
||||||
|
if br > 0:
|
||||||
|
if direction == 1 and (h[i] - c[i]) / br > retrace_limit:
|
||||||
|
continue
|
||||||
|
elif direction == -1 and (c[i] - l[i]) / br > retrace_limit:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Volume al breakout > media squeeze
|
||||||
|
sq_start = ev["sq_start"]
|
||||||
|
avg_sq_v = np.mean(v[sq_start:i])
|
||||||
|
if avg_sq_v <= 0 or v[i] <= avg_sq_v * vol_mult:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Volume TREND: slope delle ultime vol_trend_bars barre
|
||||||
|
# Usa regressione lineare semplice (rank correlation del volume)
|
||||||
|
recent_v = v[i - vol_trend_bars:i + 1] # include breakout bar
|
||||||
|
if len(recent_v) < vol_trend_bars:
|
||||||
|
continue
|
||||||
|
# slope: media seconda metà vs prima metà
|
||||||
|
mid = len(recent_v) // 2
|
||||||
|
v_early = np.mean(recent_v[:mid])
|
||||||
|
v_late = np.mean(recent_v[mid:])
|
||||||
|
vol_trending_up = v_late > v_early
|
||||||
|
vol_trending_down = v_early > v_late
|
||||||
|
|
||||||
|
# Concordanza: long richiede volume trending up, short trending down
|
||||||
|
if direction == 1 and not vol_trending_up:
|
||||||
|
continue
|
||||||
|
if direction == -1 and not vol_trending_down:
|
||||||
|
continue
|
||||||
|
|
||||||
|
signals.append(Signal(
|
||||||
|
idx=i,
|
||||||
|
direction=direction,
|
||||||
|
entry_price=c[i - 1],
|
||||||
|
metadata={
|
||||||
|
"dur": ev["dur"],
|
||||||
|
"vol_ratio": v[i] / avg_sq_v if avg_sq_v > 0 else 0,
|
||||||
|
"vol_trend": v_late / v_early if v_early > 0 else 1,
|
||||||
|
},
|
||||||
|
))
|
||||||
|
return signals
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
strategy = PriceVolumeDivergence()
|
||||||
|
|
||||||
|
configs = [
|
||||||
|
{"bb_window": 14, "sq_threshold": 0.8, "retrace_limit": 0.6,
|
||||||
|
"vol_multiplier": 1.3, "vol_trend_bars": 3},
|
||||||
|
{"bb_window": 14, "sq_threshold": 0.8, "retrace_limit": 0.6,
|
||||||
|
"vol_multiplier": 1.2, "vol_trend_bars": 3},
|
||||||
|
{"bb_window": 14, "sq_threshold": 0.8, "retrace_limit": 0.6,
|
||||||
|
"vol_multiplier": 1.3, "vol_trend_bars": 5},
|
||||||
|
{"bb_window": 14, "sq_threshold": 0.8, "retrace_limit": 0.5,
|
||||||
|
"vol_multiplier": 1.3, "vol_trend_bars": 3},
|
||||||
|
{"bb_window": 14, "sq_threshold": 0.75, "retrace_limit": 0.6,
|
||||||
|
"vol_multiplier": 1.3, "vol_trend_bars": 3},
|
||||||
|
{"bb_window": 20, "sq_threshold": 0.8, "retrace_limit": 0.6,
|
||||||
|
"vol_multiplier": 1.3, "vol_trend_bars": 3},
|
||||||
|
]
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
for cfg in configs:
|
||||||
|
for asset in ["BTC", "ETH"]:
|
||||||
|
for tf in ["15m", "1h"]:
|
||||||
|
for hold in [3, 6]:
|
||||||
|
r = strategy.backtest(asset, tf, hold=hold, **cfg)
|
||||||
|
if r and r.trades >= 20:
|
||||||
|
lbl = (f"PD01 vtb={cfg['vol_trend_bars']} "
|
||||||
|
f"vm={cfg['vol_multiplier']} "
|
||||||
|
f"sq={cfg['sq_threshold']} h={hold}")
|
||||||
|
r.strategy_name = lbl
|
||||||
|
all_results.append(r)
|
||||||
|
|
||||||
|
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||||
|
|
||||||
|
print(f"\n{'=' * 130}")
|
||||||
|
print(" PD01 PRICE-VOLUME DIVERGENCE — TOP 20")
|
||||||
|
print(f"{'=' * 130}")
|
||||||
|
print(f" {'Nome':<50s} {'A/T':>7s} {'Trades':>6s} {'Acc':>6s} "
|
||||||
|
f"{'PnL€':>10s} {'DD%':>6s} {'€/day':>7s} "
|
||||||
|
f"{'Mkt%':>5s} {'Dur':>5s} {'Anni':>4s}")
|
||||||
|
print(f" {'─' * 120}")
|
||||||
|
for r in all_results[:20]:
|
||||||
|
r.print_summary()
|
||||||
|
|
||||||
|
if all_results:
|
||||||
|
all_results[0].print_yearly()
|
||||||
|
|
||||||
|
print(f"\n BENCHMARK SQ02: 79.7% acc, 1250t, DD 6.5%, €5.23/day, 9 anni")
|
||||||
|
print(f" BENCHMARK MT01: 82.7% acc, 503t, DD 5.9%")
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
"""IB01 — Inside Bar Breakout.
|
||||||
|
|
||||||
|
Pattern di compressione a singola candela: quando una barra ha high < prev high
|
||||||
|
E low > prev low, il prezzo si sta comprimendo. Al breakout del range della
|
||||||
|
inside bar, segui la direzione.
|
||||||
|
|
||||||
|
17% delle candele 15m sono inside bars → frequenza altissima.
|
||||||
|
|
||||||
|
IN:
|
||||||
|
- OHLCV DataFrame
|
||||||
|
- Parametri: min_consecutive (N inside bars consecutivi),
|
||||||
|
volume_filter, breakout_confirm
|
||||||
|
|
||||||
|
OUT:
|
||||||
|
- Signal al breakout del range dell'inside bar
|
||||||
|
- BacktestResult
|
||||||
|
|
||||||
|
Logica:
|
||||||
|
1. Identifica N inside bars consecutivi (compressione)
|
||||||
|
2. Quando il prezzo rompe il range → entra nella direzione del breakout
|
||||||
|
3. Filtro: volume al breakout > media
|
||||||
|
4. Hold fisso
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class InsideBarBreakout(Strategy):
|
||||||
|
name = "IB01_inside_bar"
|
||||||
|
description = "Inside bar breakout — compressione a singola candela"
|
||||||
|
default_assets = ["BTC", "ETH"]
|
||||||
|
default_timeframes = ["15m", "1h"]
|
||||||
|
fee_rt = 0.002
|
||||||
|
|
||||||
|
def generate_signals(self, df, ts, **params):
|
||||||
|
c = df["close"].values
|
||||||
|
h = df["high"].values
|
||||||
|
l = df["low"].values
|
||||||
|
v = df["volume"].values
|
||||||
|
n = len(c)
|
||||||
|
|
||||||
|
min_consec = params.get("min_consecutive", 2)
|
||||||
|
use_vol = params.get("vol_filter", False)
|
||||||
|
min_range_pct = params.get("min_range_pct", 0.002)
|
||||||
|
|
||||||
|
# Volume media
|
||||||
|
vol_ma = np.full(n, np.nan)
|
||||||
|
for i in range(20, n):
|
||||||
|
vol_ma[i] = np.mean(v[i - 20:i])
|
||||||
|
|
||||||
|
signals = []
|
||||||
|
consec = 0
|
||||||
|
mother_high = 0.0
|
||||||
|
mother_low = 0.0
|
||||||
|
|
||||||
|
for i in range(1, n - 1):
|
||||||
|
is_inside = h[i] <= h[i - 1] and l[i] >= l[i - 1]
|
||||||
|
|
||||||
|
if is_inside:
|
||||||
|
if consec == 0:
|
||||||
|
mother_high = h[i - 1]
|
||||||
|
mother_low = l[i - 1]
|
||||||
|
consec += 1
|
||||||
|
else:
|
||||||
|
if consec >= min_consec:
|
||||||
|
range_pct = (mother_high - mother_low) / mother_low if mother_low > 0 else 0
|
||||||
|
if range_pct < min_range_pct:
|
||||||
|
consec = 0
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Breakout detection sulla barra corrente
|
||||||
|
if c[i] > mother_high:
|
||||||
|
direction = 1
|
||||||
|
elif c[i] < mother_low:
|
||||||
|
direction = -1
|
||||||
|
else:
|
||||||
|
consec = 0
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Volume filter
|
||||||
|
if use_vol and not np.isnan(vol_ma[i]):
|
||||||
|
if v[i] < vol_ma[i] * 1.2:
|
||||||
|
consec = 0
|
||||||
|
continue
|
||||||
|
|
||||||
|
signals.append(Signal(
|
||||||
|
idx=i, direction=direction, entry_price=c[i],
|
||||||
|
metadata={"consec": consec, "range_pct": round(range_pct * 100, 3)},
|
||||||
|
))
|
||||||
|
|
||||||
|
consec = 0
|
||||||
|
|
||||||
|
return signals
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
strategy = InsideBarBreakout()
|
||||||
|
|
||||||
|
configs = [
|
||||||
|
("2ib", {"min_consecutive": 2}),
|
||||||
|
("3ib", {"min_consecutive": 3}),
|
||||||
|
("4ib", {"min_consecutive": 4}),
|
||||||
|
("2ib+vol", {"min_consecutive": 2, "vol_filter": True}),
|
||||||
|
("3ib+vol", {"min_consecutive": 3, "vol_filter": True}),
|
||||||
|
("2ib r>0.3%", {"min_consecutive": 2, "min_range_pct": 0.003}),
|
||||||
|
("3ib r>0.3%", {"min_consecutive": 3, "min_range_pct": 0.003}),
|
||||||
|
]
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
for label, params in configs:
|
||||||
|
for asset in ["BTC", "ETH"]:
|
||||||
|
for tf in ["15m", "1h"]:
|
||||||
|
for hold in [3, 6]:
|
||||||
|
r = strategy.backtest(asset, tf, hold=hold, **params)
|
||||||
|
if r and r.trades >= 30:
|
||||||
|
r.strategy_name = f"IB01 {label} h={hold}"
|
||||||
|
all_results.append(r)
|
||||||
|
|
||||||
|
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||||
|
print(f"\n{'=' * 120}")
|
||||||
|
print(f" IB01 INSIDE BAR BREAKOUT — TOP 20")
|
||||||
|
print(f"{'=' * 120}")
|
||||||
|
for r in all_results[:20]:
|
||||||
|
r.print_summary()
|
||||||
|
if all_results:
|
||||||
|
all_results[0].print_yearly()
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"""DC01 — Donchian Channel Breakout con filtri.
|
||||||
|
|
||||||
|
Trend-following classico: quando il prezzo rompe il massimo/minimo degli
|
||||||
|
ultimi N periodi, entra nella direzione del breakout.
|
||||||
|
|
||||||
|
Completamente diverso dallo squeeze (che usa Bollinger/Keltner).
|
||||||
|
Donchian cattura breakout di RANGE, non di VOLATILITÀ.
|
||||||
|
|
||||||
|
IN:
|
||||||
|
- OHLCV DataFrame
|
||||||
|
- Parametri: channel_period, volume_filter, atr_stop, trend_filter
|
||||||
|
|
||||||
|
OUT:
|
||||||
|
- Signal al breakout del canale Donchian
|
||||||
|
- BacktestResult
|
||||||
|
|
||||||
|
Logica:
|
||||||
|
1. Donchian upper = max(high, N periodi), lower = min(low, N periodi)
|
||||||
|
2. Close > upper → LONG (breakout rialzista)
|
||||||
|
3. Close < lower → SHORT (breakout ribassista)
|
||||||
|
4. Filtri: volume, trend EMA, ATR minimo
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class DonchianBreakout(Strategy):
|
||||||
|
name = "DC01_donchian"
|
||||||
|
description = "Donchian Channel breakout — trend-following su range"
|
||||||
|
default_assets = ["BTC", "ETH"]
|
||||||
|
default_timeframes = ["15m", "1h"]
|
||||||
|
fee_rt = 0.002
|
||||||
|
|
||||||
|
def generate_signals(self, df, ts, **params):
|
||||||
|
c = df["close"].values
|
||||||
|
h = df["high"].values
|
||||||
|
l = df["low"].values
|
||||||
|
v = df["volume"].values
|
||||||
|
n = len(c)
|
||||||
|
|
||||||
|
period = params.get("channel_period", 48)
|
||||||
|
use_vol = params.get("vol_filter", False)
|
||||||
|
use_trend = params.get("trend_filter", False)
|
||||||
|
cooldown = params.get("cooldown", 6)
|
||||||
|
|
||||||
|
# EMA per trend filter
|
||||||
|
ema_50 = np.full(n, np.nan)
|
||||||
|
k = 2 / 51
|
||||||
|
ema_50[49] = np.mean(c[:50])
|
||||||
|
for i in range(50, n):
|
||||||
|
ema_50[i] = c[i] * k + ema_50[i - 1] * (1 - k)
|
||||||
|
|
||||||
|
# Volume media
|
||||||
|
vol_ma = np.full(n, np.nan)
|
||||||
|
for i in range(20, n):
|
||||||
|
vol_ma[i] = np.mean(v[i - 20:i])
|
||||||
|
|
||||||
|
signals = []
|
||||||
|
last_signal_idx = -cooldown
|
||||||
|
|
||||||
|
for i in range(period + 1, n):
|
||||||
|
if i - last_signal_idx < cooldown:
|
||||||
|
continue
|
||||||
|
|
||||||
|
upper = np.max(h[i - period:i])
|
||||||
|
lower = np.min(l[i - period:i])
|
||||||
|
|
||||||
|
direction = 0
|
||||||
|
if c[i] > upper:
|
||||||
|
direction = 1
|
||||||
|
elif c[i] < lower:
|
||||||
|
direction = -1
|
||||||
|
|
||||||
|
if direction == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Trend filter: breakout must align with EMA trend
|
||||||
|
if use_trend and not np.isnan(ema_50[i]):
|
||||||
|
if direction == 1 and c[i] < ema_50[i]:
|
||||||
|
continue
|
||||||
|
if direction == -1 and c[i] > ema_50[i]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Volume filter
|
||||||
|
if use_vol and not np.isnan(vol_ma[i]):
|
||||||
|
if v[i] < vol_ma[i] * 1.3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
signals.append(Signal(
|
||||||
|
idx=i, direction=direction, entry_price=c[i],
|
||||||
|
metadata={"upper": float(upper), "lower": float(lower)},
|
||||||
|
))
|
||||||
|
last_signal_idx = i
|
||||||
|
|
||||||
|
return signals
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
strategy = DonchianBreakout()
|
||||||
|
|
||||||
|
configs = [
|
||||||
|
("p=24", {"channel_period": 24}),
|
||||||
|
("p=48", {"channel_period": 48}),
|
||||||
|
("p=96", {"channel_period": 96}),
|
||||||
|
("p=48+trend", {"channel_period": 48, "trend_filter": True}),
|
||||||
|
("p=48+vol", {"channel_period": 48, "vol_filter": True}),
|
||||||
|
("p=48+t+v", {"channel_period": 48, "trend_filter": True, "vol_filter": True}),
|
||||||
|
("p=96+t+v", {"channel_period": 96, "trend_filter": True, "vol_filter": True}),
|
||||||
|
]
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
for label, params in configs:
|
||||||
|
for asset in ["BTC", "ETH"]:
|
||||||
|
for tf in ["15m", "1h"]:
|
||||||
|
for hold in [3, 6, 12]:
|
||||||
|
r = strategy.backtest(asset, tf, hold=hold, **params)
|
||||||
|
if r and r.trades >= 30:
|
||||||
|
r.strategy_name = f"DC01 {label} h={hold}"
|
||||||
|
all_results.append(r)
|
||||||
|
|
||||||
|
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||||
|
print(f"\n{'=' * 120}")
|
||||||
|
print(f" DC01 DONCHIAN BREAKOUT — TOP 20")
|
||||||
|
print(f"{'=' * 120}")
|
||||||
|
for r in all_results[:20]:
|
||||||
|
r.print_summary()
|
||||||
|
if all_results:
|
||||||
|
all_results[0].print_yearly()
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
"""SB01 — Squeeze Breakout con Retest.
|
||||||
|
|
||||||
|
Il problema di SQ01/SQ02: entri al breakout, ma molti breakout sono fakeout.
|
||||||
|
Soluzione: aspetta il RETEST. Dopo il breakout, il prezzo spesso torna a
|
||||||
|
testare il livello di breakout prima di continuare.
|
||||||
|
|
||||||
|
Più selettivo di SQ02 → meno trade ma più accurati.
|
||||||
|
Anti-overfitting: meccanismo strutturale (retest è fenomeno di mercato reale).
|
||||||
|
|
||||||
|
IN:
|
||||||
|
- OHLCV DataFrame
|
||||||
|
- Parametri: bb_window, sq_threshold, retest_window (quante barre aspettare
|
||||||
|
il retest), retest_tolerance (quanto può tornare indietro)
|
||||||
|
|
||||||
|
OUT:
|
||||||
|
- Signal al retest confermato (non al breakout iniziale)
|
||||||
|
- BacktestResult
|
||||||
|
|
||||||
|
Logica:
|
||||||
|
1. Rileva squeeze release (come SQ01)
|
||||||
|
2. NON entrare subito — segna direzione e livello di breakout
|
||||||
|
3. Nelle N barre successive, aspetta che il prezzo torni verso il livello
|
||||||
|
4. Se il prezzo torna nel range di tolleranza e poi rimbalza → ENTRA
|
||||||
|
5. Se il prezzo non torna → skip (momentum troppo forte, entry persa)
|
||||||
|
6. Se il prezzo sfonda il livello → fakeout confermato, skip
|
||||||
|
"""
|
||||||
|
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 SqueezeBreakoutRetest(Strategy):
|
||||||
|
name = "SB01_squeeze_retest"
|
||||||
|
description = "Squeeze breakout con retest — entra solo dopo pullback confermato"
|
||||||
|
default_assets = ["BTC", "ETH"]
|
||||||
|
default_timeframes = ["15m", "1h"]
|
||||||
|
fee_rt = 0.002
|
||||||
|
|
||||||
|
def generate_signals(self, df, ts, **params):
|
||||||
|
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)
|
||||||
|
retest_window = params.get("retest_window", 8)
|
||||||
|
retest_tol = params.get("retest_tolerance", 0.5)
|
||||||
|
use_vol = params.get("vol_filter", False)
|
||||||
|
|
||||||
|
kcr = keltner_ratio(c, h, l, bb_w)
|
||||||
|
events = detect_squeezes(c, h, l, kcr, sq_thr)
|
||||||
|
|
||||||
|
vol_ma = np.full(n, np.nan)
|
||||||
|
for i in range(20, n):
|
||||||
|
vol_ma[i] = np.mean(v[i - 20:i])
|
||||||
|
|
||||||
|
signals = []
|
||||||
|
|
||||||
|
for ev in events:
|
||||||
|
brk_idx = ev["idx"]
|
||||||
|
if brk_idx + retest_window + 3 >= n or brk_idx < 1:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Direzione breakout
|
||||||
|
first_ret = (c[brk_idx] - c[brk_idx - 1]) / c[brk_idx - 1]
|
||||||
|
if abs(first_ret) < 0.001:
|
||||||
|
continue
|
||||||
|
|
||||||
|
direction = 1 if first_ret > 0 else -1
|
||||||
|
breakout_level = c[brk_idx - 1]
|
||||||
|
breakout_move = abs(first_ret)
|
||||||
|
|
||||||
|
# Aspetta retest nelle prossime N barre
|
||||||
|
retest_found = False
|
||||||
|
retest_idx = -1
|
||||||
|
|
||||||
|
for j in range(brk_idx + 1, min(brk_idx + retest_window + 1, n)):
|
||||||
|
if direction == 1:
|
||||||
|
# Long: il prezzo deve tornare GIÙ verso breakout_level
|
||||||
|
pullback = (h[brk_idx] - l[j]) / (h[brk_idx] - breakout_level) if h[brk_idx] > breakout_level else 0
|
||||||
|
if pullback >= retest_tol:
|
||||||
|
# Tornato abbastanza — ora deve rimbalzare
|
||||||
|
if c[j] > breakout_level:
|
||||||
|
retest_found = True
|
||||||
|
retest_idx = j
|
||||||
|
break
|
||||||
|
elif c[j] < breakout_level * 0.998:
|
||||||
|
# Sfondato sotto → fakeout
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Short: il prezzo deve tornare SU verso breakout_level
|
||||||
|
pullback = (h[j] - l[brk_idx]) / (breakout_level - l[brk_idx]) if breakout_level > l[brk_idx] else 0
|
||||||
|
if pullback >= retest_tol:
|
||||||
|
if c[j] < breakout_level:
|
||||||
|
retest_found = True
|
||||||
|
retest_idx = j
|
||||||
|
break
|
||||||
|
elif c[j] > breakout_level * 1.002:
|
||||||
|
break
|
||||||
|
|
||||||
|
if not retest_found or retest_idx < 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Volume filter al retest
|
||||||
|
if use_vol and not np.isnan(vol_ma[retest_idx]):
|
||||||
|
if v[retest_idx] < vol_ma[retest_idx] * 0.8:
|
||||||
|
continue
|
||||||
|
|
||||||
|
signals.append(Signal(
|
||||||
|
idx=retest_idx, direction=direction,
|
||||||
|
entry_price=c[retest_idx],
|
||||||
|
metadata={
|
||||||
|
"breakout_idx": brk_idx,
|
||||||
|
"retest_bars": retest_idx - brk_idx,
|
||||||
|
"breakout_move": round(breakout_move * 100, 3),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
|
||||||
|
return signals
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
strategy = SqueezeBreakoutRetest()
|
||||||
|
|
||||||
|
configs = [
|
||||||
|
("rt8 tol50%", {"retest_window": 8, "retest_tolerance": 0.5}),
|
||||||
|
("rt6 tol50%", {"retest_window": 6, "retest_tolerance": 0.5}),
|
||||||
|
("rt10 tol50%", {"retest_window": 10, "retest_tolerance": 0.5}),
|
||||||
|
("rt8 tol30%", {"retest_window": 8, "retest_tolerance": 0.3}),
|
||||||
|
("rt8 tol70%", {"retest_window": 8, "retest_tolerance": 0.7}),
|
||||||
|
("rt8 tol50%+vol", {"retest_window": 8, "retest_tolerance": 0.5, "vol_filter": True}),
|
||||||
|
("rt6 tol30%", {"retest_window": 6, "retest_tolerance": 0.3}),
|
||||||
|
("rt12 tol50%", {"retest_window": 12, "retest_tolerance": 0.5}),
|
||||||
|
]
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
for label, params in configs:
|
||||||
|
for asset in ["BTC", "ETH"]:
|
||||||
|
for tf in ["15m", "1h"]:
|
||||||
|
for hold in [3, 6]:
|
||||||
|
r = strategy.backtest(asset, tf, hold=hold, **params)
|
||||||
|
if r and r.trades >= 30:
|
||||||
|
r.strategy_name = f"SB01 {label} h={hold}"
|
||||||
|
all_results.append(r)
|
||||||
|
|
||||||
|
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||||
|
print(f"\n{'=' * 130}")
|
||||||
|
print(f" SB01 SQUEEZE BREAKOUT RETEST — TOP 25")
|
||||||
|
print(f"{'=' * 130}")
|
||||||
|
for r in all_results[:25]:
|
||||||
|
r.print_summary()
|
||||||
|
if all_results:
|
||||||
|
all_results[0].print_yearly()
|
||||||
|
|
||||||
|
# Confronto con benchmark
|
||||||
|
print(f"\n BENCHMARK SQ02: 79.7% acc, 1250 trades, DD 6.5%, 9/9 anni")
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
"""MR01 — Mean Reversion da estremi RSI.
|
||||||
|
|
||||||
|
Approccio opposto allo squeeze: quando il prezzo va troppo lontano troppo veloce,
|
||||||
|
scommetti che torni indietro. Autocorrelazione lag-1 negativa (-0.21 BTC, -0.35 ETH)
|
||||||
|
conferma che il mercato a 15m è mean-reverting.
|
||||||
|
|
||||||
|
IN:
|
||||||
|
- OHLCV DataFrame
|
||||||
|
- Parametri: rsi_period, rsi_oversold, rsi_overbought, hold_bars,
|
||||||
|
volume_filter (volume > N× media), atr_filter (move > N×ATR)
|
||||||
|
|
||||||
|
OUT:
|
||||||
|
- Signal: long quando RSI < oversold, short quando RSI > overbought
|
||||||
|
- BacktestResult con metriche
|
||||||
|
|
||||||
|
Logica:
|
||||||
|
1. RSI scende sotto soglia oversold → LONG (prezzo tornerà su)
|
||||||
|
2. RSI sale sopra soglia overbought → SHORT (prezzo tornerà giù)
|
||||||
|
3. Filtro opzionale: volume spike conferma l'eccesso
|
||||||
|
4. Filtro opzionale: move recente > N×ATR (eccesso di prezzo)
|
||||||
|
5. Hold fisso, poi chiudi
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def rsi(close, period=14):
|
||||||
|
delta = np.diff(close)
|
||||||
|
gain = np.where(delta > 0, delta, 0)
|
||||||
|
loss = np.where(delta < 0, -delta, 0)
|
||||||
|
result = np.full(len(close), 50.0)
|
||||||
|
if len(gain) < period:
|
||||||
|
return result
|
||||||
|
ag = np.mean(gain[:period])
|
||||||
|
al = np.mean(loss[:period])
|
||||||
|
for i in range(period, len(delta)):
|
||||||
|
ag = (ag * (period - 1) + gain[i]) / period
|
||||||
|
al = (al * (period - 1) + loss[i]) / period
|
||||||
|
result[i + 1] = 100 if al == 0 else 100 - 100 / (1 + ag / al)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class MeanReversionRSI(Strategy):
|
||||||
|
name = "MR01_mean_reversion_rsi"
|
||||||
|
description = "Mean reversion da estremi RSI — fade eccessi direzionali"
|
||||||
|
default_assets = ["BTC", "ETH"]
|
||||||
|
default_timeframes = ["15m", "1h"]
|
||||||
|
fee_rt = 0.002
|
||||||
|
|
||||||
|
def generate_signals(self, df, ts, **params):
|
||||||
|
c = df["close"].values
|
||||||
|
h = df["high"].values
|
||||||
|
l = df["low"].values
|
||||||
|
v = df["volume"].values
|
||||||
|
n = len(c)
|
||||||
|
|
||||||
|
rsi_period = params.get("rsi_period", 14)
|
||||||
|
oversold = params.get("rsi_oversold", 25)
|
||||||
|
overbought = params.get("rsi_overbought", 75)
|
||||||
|
use_vol_filter = params.get("vol_filter", False)
|
||||||
|
use_atr_filter = params.get("atr_filter", False)
|
||||||
|
cooldown = params.get("cooldown", 4)
|
||||||
|
|
||||||
|
rsi_vals = rsi(c, rsi_period)
|
||||||
|
|
||||||
|
# Volume media rolling
|
||||||
|
vol_ma = np.full(n, np.nan)
|
||||||
|
for i in range(20, n):
|
||||||
|
vol_ma[i] = np.mean(v[i - 20:i])
|
||||||
|
|
||||||
|
# ATR
|
||||||
|
tr = np.maximum(h[1:] - l[1:],
|
||||||
|
np.maximum(np.abs(h[1:] - c[:-1]), np.abs(l[1:] - c[:-1])))
|
||||||
|
atr_vals = np.full(n, np.nan)
|
||||||
|
for i in range(15, len(tr)):
|
||||||
|
atr_vals[i + 1] = np.mean(tr[i - 14:i])
|
||||||
|
|
||||||
|
signals = []
|
||||||
|
last_signal_idx = -cooldown
|
||||||
|
|
||||||
|
for i in range(20, n):
|
||||||
|
if i - last_signal_idx < cooldown:
|
||||||
|
continue
|
||||||
|
|
||||||
|
direction = 0
|
||||||
|
if rsi_vals[i] < oversold:
|
||||||
|
direction = 1 # oversold → long
|
||||||
|
elif rsi_vals[i] > overbought:
|
||||||
|
direction = -1 # overbought → short
|
||||||
|
|
||||||
|
if direction == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Volume filter
|
||||||
|
if use_vol_filter and not np.isnan(vol_ma[i]):
|
||||||
|
if v[i] < vol_ma[i] * 1.5:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ATR filter: il move recente deve essere > 1.5× ATR
|
||||||
|
if use_atr_filter and not np.isnan(atr_vals[i]):
|
||||||
|
recent_move = abs(c[i] - c[max(0, i - 3)]) / c[max(0, i - 3)]
|
||||||
|
if recent_move < atr_vals[i] / c[i] * 1.5:
|
||||||
|
continue
|
||||||
|
|
||||||
|
signals.append(Signal(
|
||||||
|
idx=i, direction=direction, entry_price=c[i],
|
||||||
|
metadata={"rsi": float(rsi_vals[i])},
|
||||||
|
))
|
||||||
|
last_signal_idx = i
|
||||||
|
|
||||||
|
return signals
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
strategy = MeanReversionRSI()
|
||||||
|
|
||||||
|
configs = [
|
||||||
|
("RSI25/75", {}),
|
||||||
|
("RSI20/80", {"rsi_oversold": 20, "rsi_overbought": 80}),
|
||||||
|
("RSI25/75+vol", {"vol_filter": True}),
|
||||||
|
("RSI20/80+vol", {"rsi_oversold": 20, "rsi_overbought": 80, "vol_filter": True}),
|
||||||
|
("RSI25/75+atr", {"atr_filter": True}),
|
||||||
|
("RSI20/80+vol+atr", {"rsi_oversold": 20, "rsi_overbought": 80, "vol_filter": True, "atr_filter": True}),
|
||||||
|
]
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
for label, params in configs:
|
||||||
|
for asset in ["BTC", "ETH"]:
|
||||||
|
for tf in ["15m", "1h"]:
|
||||||
|
for hold in [3, 6]:
|
||||||
|
r = strategy.backtest(asset, tf, hold=hold, **params)
|
||||||
|
if r and r.trades >= 30:
|
||||||
|
r.strategy_name = f"MR01 {label} h={hold}"
|
||||||
|
all_results.append(r)
|
||||||
|
|
||||||
|
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||||
|
print(f"\n{'=' * 120}")
|
||||||
|
print(f" MR01 MEAN REVERSION RSI — TOP 20")
|
||||||
|
print(f"{'=' * 120}")
|
||||||
|
for r in all_results[:20]:
|
||||||
|
r.print_summary()
|
||||||
|
if all_results:
|
||||||
|
all_results[0].print_yearly()
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"""VO01 — Volume Spike Reversal.
|
||||||
|
|
||||||
|
Quando il volume esplode (>3× media) con un forte move direzionale,
|
||||||
|
il mercato è in eccesso → fade il move (mean reversion).
|
||||||
|
|
||||||
|
Diverso dallo squeeze: non cerca compressione, cerca ECCESSO.
|
||||||
|
Il volume spike indica panico/euforia → reversal probabile.
|
||||||
|
|
||||||
|
IN:
|
||||||
|
- OHLCV DataFrame
|
||||||
|
- Parametri: vol_mult (3), move_threshold (0.005), hold
|
||||||
|
|
||||||
|
OUT:
|
||||||
|
- Signal: fade la direzione del volume spike
|
||||||
|
- BacktestResult
|
||||||
|
|
||||||
|
Logica:
|
||||||
|
1. Volume > vol_mult × media 20 periodi
|
||||||
|
2. Move nella candela > move_threshold (0.5%)
|
||||||
|
3. Direzione: opposta al move (mean reversion)
|
||||||
|
4. Filtro: non entrare se già in trend forte (EMA slope)
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class VolumeSpikeReversal(Strategy):
|
||||||
|
name = "VO01_vol_spike_reversal"
|
||||||
|
description = "Volume spike reversal — fade eccessi di volume/prezzo"
|
||||||
|
default_assets = ["BTC", "ETH"]
|
||||||
|
default_timeframes = ["15m", "1h"]
|
||||||
|
fee_rt = 0.002
|
||||||
|
|
||||||
|
def generate_signals(self, df, ts, **params):
|
||||||
|
c = df["close"].values
|
||||||
|
o = df["open"].values
|
||||||
|
h = df["high"].values
|
||||||
|
l = df["low"].values
|
||||||
|
v = df["volume"].values
|
||||||
|
n = len(c)
|
||||||
|
|
||||||
|
vol_mult = params.get("vol_mult", 3.0)
|
||||||
|
move_thr = params.get("move_threshold", 0.005)
|
||||||
|
use_trend_filter = params.get("trend_filter", False)
|
||||||
|
cooldown = params.get("cooldown", 4)
|
||||||
|
|
||||||
|
# Volume media rolling
|
||||||
|
vol_ma = np.full(n, np.nan)
|
||||||
|
for i in range(20, n):
|
||||||
|
vol_ma[i] = np.mean(v[i - 20:i])
|
||||||
|
|
||||||
|
# EMA per trend filter
|
||||||
|
ema_20 = np.full(n, np.nan)
|
||||||
|
k = 2 / 21
|
||||||
|
ema_20[19] = np.mean(c[:20])
|
||||||
|
for i in range(20, n):
|
||||||
|
ema_20[i] = c[i] * k + ema_20[i - 1] * (1 - k)
|
||||||
|
|
||||||
|
signals = []
|
||||||
|
last_idx = -cooldown
|
||||||
|
|
||||||
|
for i in range(21, n):
|
||||||
|
if i - last_idx < cooldown:
|
||||||
|
continue
|
||||||
|
if np.isnan(vol_ma[i]):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Volume spike
|
||||||
|
if v[i] < vol_ma[i] * vol_mult:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Price move
|
||||||
|
move = (c[i] - o[i]) / o[i] if o[i] > 0 else 0
|
||||||
|
if abs(move) < move_thr:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Fade: opposto al move
|
||||||
|
direction = -1 if move > 0 else 1
|
||||||
|
|
||||||
|
# Trend filter: non fare mean reversion contro trend forte
|
||||||
|
if use_trend_filter and not np.isnan(ema_20[i]):
|
||||||
|
ema_slope = (ema_20[i] - ema_20[max(0, i - 5)]) / ema_20[max(0, i - 5)]
|
||||||
|
if direction == -1 and ema_slope > 0.005:
|
||||||
|
continue
|
||||||
|
if direction == 1 and ema_slope < -0.005:
|
||||||
|
continue
|
||||||
|
|
||||||
|
signals.append(Signal(
|
||||||
|
idx=i, direction=direction, entry_price=c[i],
|
||||||
|
metadata={"vol_ratio": float(v[i] / vol_ma[i]), "move_pct": round(move * 100, 3)},
|
||||||
|
))
|
||||||
|
last_idx = i
|
||||||
|
|
||||||
|
return signals
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
strategy = VolumeSpikeReversal()
|
||||||
|
|
||||||
|
configs = [
|
||||||
|
("v3x m0.5%", {"vol_mult": 3.0, "move_threshold": 0.005}),
|
||||||
|
("v3x m1%", {"vol_mult": 3.0, "move_threshold": 0.01}),
|
||||||
|
("v4x m0.5%", {"vol_mult": 4.0, "move_threshold": 0.005}),
|
||||||
|
("v4x m1%", {"vol_mult": 4.0, "move_threshold": 0.01}),
|
||||||
|
("v3x m0.5%+tf", {"vol_mult": 3.0, "move_threshold": 0.005, "trend_filter": True}),
|
||||||
|
("v3x m1%+tf", {"vol_mult": 3.0, "move_threshold": 0.01, "trend_filter": True}),
|
||||||
|
("v5x m1%", {"vol_mult": 5.0, "move_threshold": 0.01}),
|
||||||
|
("v5x m1%+tf", {"vol_mult": 5.0, "move_threshold": 0.01, "trend_filter": True}),
|
||||||
|
]
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
for label, params in configs:
|
||||||
|
for asset in ["BTC", "ETH"]:
|
||||||
|
for tf in ["15m", "1h"]:
|
||||||
|
for hold in [3, 6]:
|
||||||
|
r = strategy.backtest(asset, tf, hold=hold, **params)
|
||||||
|
if r and r.trades >= 30:
|
||||||
|
r.strategy_name = f"VO01 {label} h={hold}"
|
||||||
|
all_results.append(r)
|
||||||
|
|
||||||
|
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||||
|
print(f"\n{'=' * 120}")
|
||||||
|
print(f" VO01 VOLUME SPIKE REVERSAL — TOP 20")
|
||||||
|
print(f"{'=' * 120}")
|
||||||
|
for r in all_results[:20]:
|
||||||
|
r.print_summary()
|
||||||
|
if all_results:
|
||||||
|
all_results[0].print_yearly()
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
"""HY01 — Squeeze + Mean Reversion Ibrida.
|
||||||
|
|
||||||
|
Insight: durante lo squeeze (bassa volatilità), il prezzo mean-reverte
|
||||||
|
DENTRO il range compresso. Autocorrelazione negativa a 15m conferma.
|
||||||
|
Invece di aspettare il BREAKOUT, tradi la MEAN REVERSION dentro lo squeeze.
|
||||||
|
|
||||||
|
Completamente diverso da SQ01-SQ04 che aspettano il RILASCIO.
|
||||||
|
|
||||||
|
IN:
|
||||||
|
- OHLCV DataFrame
|
||||||
|
- Parametri: bb_window, sq_threshold, rsi_period, rsi_levels,
|
||||||
|
vol_filter, bb_touch (prezzo tocca banda Bollinger)
|
||||||
|
|
||||||
|
OUT:
|
||||||
|
- Signal: long quando RSI oversold DURANTE squeeze, short quando overbought
|
||||||
|
- BacktestResult
|
||||||
|
|
||||||
|
Logica:
|
||||||
|
1. Verifica che siamo IN squeeze (BB dentro KC)
|
||||||
|
2. Prezzo tocca banda inferiore BB → LONG (tornerà alla media)
|
||||||
|
3. Prezzo tocca banda superiore BB → SHORT (tornerà alla media)
|
||||||
|
4. Conferma RSI: deve essere estremo nella direzione
|
||||||
|
5. Hold corto (2-3 barre) — target: ritorno alla media
|
||||||
|
6. Stop: se prezzo rompe lo squeeze → chiudi subito
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def rsi(close, period=14):
|
||||||
|
delta = np.diff(close)
|
||||||
|
gain = np.where(delta > 0, delta, 0)
|
||||||
|
loss = np.where(delta < 0, -delta, 0)
|
||||||
|
result = np.full(len(close), 50.0)
|
||||||
|
if len(gain) < period:
|
||||||
|
return result
|
||||||
|
ag = np.mean(gain[:period])
|
||||||
|
al = np.mean(loss[:period])
|
||||||
|
for i in range(period, len(delta)):
|
||||||
|
ag = (ag * (period - 1) + gain[i]) / period
|
||||||
|
al = (al * (period - 1) + loss[i]) / period
|
||||||
|
result[i + 1] = 100 if al == 0 else 100 - 100 / (1 + ag / al)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def bollinger(close, window=14):
|
||||||
|
n = len(close)
|
||||||
|
upper = np.full(n, np.nan)
|
||||||
|
lower = np.full(n, np.nan)
|
||||||
|
mid = np.full(n, np.nan)
|
||||||
|
for i in range(window, n):
|
||||||
|
wc = close[i - window:i]
|
||||||
|
m = np.mean(wc)
|
||||||
|
s = np.std(wc)
|
||||||
|
mid[i] = m
|
||||||
|
upper[i] = m + 2 * s
|
||||||
|
lower[i] = m - 2 * s
|
||||||
|
return upper, mid, lower
|
||||||
|
|
||||||
|
|
||||||
|
class SqueezeMeanReversion(Strategy):
|
||||||
|
name = "HY01_squeeze_mr"
|
||||||
|
description = "Mean reversion DENTRO lo squeeze — fade estremi in range compresso"
|
||||||
|
default_assets = ["BTC", "ETH"]
|
||||||
|
default_timeframes = ["15m", "1h"]
|
||||||
|
fee_rt = 0.002
|
||||||
|
|
||||||
|
def generate_signals(self, df, ts, **params):
|
||||||
|
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)
|
||||||
|
rsi_period = params.get("rsi_period", 14)
|
||||||
|
rsi_low = params.get("rsi_oversold", 30)
|
||||||
|
rsi_high = params.get("rsi_overbought", 70)
|
||||||
|
use_bb_touch = params.get("bb_touch", True)
|
||||||
|
cooldown = params.get("cooldown", 3)
|
||||||
|
|
||||||
|
kcr = keltner_ratio(c, h, l, bb_w)
|
||||||
|
rsi_vals = rsi(c, rsi_period)
|
||||||
|
bb_upper, bb_mid, bb_lower = bollinger(c, bb_w)
|
||||||
|
|
||||||
|
signals = []
|
||||||
|
last_idx = -cooldown
|
||||||
|
|
||||||
|
for i in range(bb_w + 1, n):
|
||||||
|
if i - last_idx < cooldown:
|
||||||
|
continue
|
||||||
|
if np.isnan(kcr[i]) or np.isnan(bb_lower[i]):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Must be IN squeeze
|
||||||
|
if kcr[i] >= sq_thr:
|
||||||
|
continue
|
||||||
|
|
||||||
|
direction = 0
|
||||||
|
|
||||||
|
if use_bb_touch:
|
||||||
|
# Prezzo tocca/rompe BB lower → long (mean reversion up)
|
||||||
|
if c[i] <= bb_lower[i] and rsi_vals[i] < rsi_low:
|
||||||
|
direction = 1
|
||||||
|
# Prezzo tocca/rompe BB upper → short (mean reversion down)
|
||||||
|
elif c[i] >= bb_upper[i] and rsi_vals[i] > rsi_high:
|
||||||
|
direction = -1
|
||||||
|
else:
|
||||||
|
# Solo RSI
|
||||||
|
if rsi_vals[i] < rsi_low:
|
||||||
|
direction = 1
|
||||||
|
elif rsi_vals[i] > rsi_high:
|
||||||
|
direction = -1
|
||||||
|
|
||||||
|
if direction == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
signals.append(Signal(
|
||||||
|
idx=i, direction=direction, entry_price=c[i],
|
||||||
|
metadata={
|
||||||
|
"rsi": float(rsi_vals[i]),
|
||||||
|
"kcr": float(kcr[i]),
|
||||||
|
"bb_pos": "lower" if direction == 1 else "upper",
|
||||||
|
},
|
||||||
|
))
|
||||||
|
last_idx = i
|
||||||
|
|
||||||
|
return signals
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
strategy = SqueezeMeanReversion()
|
||||||
|
|
||||||
|
configs = [
|
||||||
|
("bb+rsi30/70", {"bb_touch": True, "rsi_oversold": 30, "rsi_overbought": 70}),
|
||||||
|
("bb+rsi25/75", {"bb_touch": True, "rsi_oversold": 25, "rsi_overbought": 75}),
|
||||||
|
("bb+rsi35/65", {"bb_touch": True, "rsi_oversold": 35, "rsi_overbought": 65}),
|
||||||
|
("rsi30/70 only", {"bb_touch": False, "rsi_oversold": 30, "rsi_overbought": 70}),
|
||||||
|
("rsi25/75 only", {"bb_touch": False, "rsi_oversold": 25, "rsi_overbought": 75}),
|
||||||
|
("sq<0.7 bb+rsi30", {"bb_touch": True, "sq_threshold": 0.7, "rsi_oversold": 30, "rsi_overbought": 70}),
|
||||||
|
("sq<0.9 bb+rsi30", {"bb_touch": True, "sq_threshold": 0.9, "rsi_oversold": 30, "rsi_overbought": 70}),
|
||||||
|
("sq<0.9 rsi35/65", {"bb_touch": False, "sq_threshold": 0.9, "rsi_oversold": 35, "rsi_overbought": 65}),
|
||||||
|
]
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
for label, params in configs:
|
||||||
|
for asset in ["BTC", "ETH"]:
|
||||||
|
for tf in ["15m", "1h"]:
|
||||||
|
for hold in [2, 3, 4]:
|
||||||
|
r = strategy.backtest(asset, tf, hold=hold, **params)
|
||||||
|
if r and r.trades >= 30:
|
||||||
|
r.strategy_name = f"HY01 {label} h={hold}"
|
||||||
|
all_results.append(r)
|
||||||
|
|
||||||
|
all_results.sort(key=lambda r: r.accuracy, reverse=True)
|
||||||
|
print(f"\n{'=' * 130}")
|
||||||
|
print(f" HY01 SQUEEZE MEAN REVERSION — TOP 25")
|
||||||
|
print(f"{'=' * 130}")
|
||||||
|
for r in all_results[:25]:
|
||||||
|
r.print_summary()
|
||||||
|
if all_results:
|
||||||
|
all_results[0].print_yearly()
|
||||||
@@ -18,6 +18,7 @@ MODULE_MAP = {
|
|||||||
"SQ03_filtered": ("SQ03_squeeze_all_filters", "SqueezeFiltered"),
|
"SQ03_filtered": ("SQ03_squeeze_all_filters", "SqueezeFiltered"),
|
||||||
"SQ04_ultimate": ("SQ04_squeeze_ultimate", "SqueezeUltimate"),
|
"SQ04_ultimate": ("SQ04_squeeze_ultimate", "SqueezeUltimate"),
|
||||||
"ML01_squeeze_gbm": ("ML01_squeeze_gbm", "SqueezeGBM"),
|
"ML01_squeeze_gbm": ("ML01_squeeze_gbm", "SqueezeGBM"),
|
||||||
|
"MT01_squeeze_mtf": ("MT01_squeeze_mtf_momentum", "SqueezeMTFMomentum"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,3 +31,21 @@ strategies:
|
|||||||
ml_threshold: 0.70
|
ml_threshold: 0.70
|
||||||
bb_window: 14
|
bb_window: 14
|
||||||
sq_threshold: 0.8
|
sq_threshold: 0.8
|
||||||
|
|
||||||
|
- name: MT01_squeeze_mtf
|
||||||
|
asset: BTC
|
||||||
|
tf: 15m
|
||||||
|
enabled: true
|
||||||
|
params:
|
||||||
|
ema_period: 20
|
||||||
|
min_slope: 0.001
|
||||||
|
vol_filter: true
|
||||||
|
|
||||||
|
- name: MT01_squeeze_mtf
|
||||||
|
asset: ETH
|
||||||
|
tf: 15m
|
||||||
|
enabled: true
|
||||||
|
params:
|
||||||
|
ema_period: 20
|
||||||
|
min_slope: 0.001
|
||||||
|
vol_filter: true
|
||||||
|
|||||||
Reference in New Issue
Block a user