chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
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>
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user