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>
170 lines
5.8 KiB
Python
170 lines
5.8 KiB
Python
"""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()
|