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