feat: paper trading live su Deribit testnet — squeeze+ML ibrida
Sistema completo: client Cerbero MCP, signal engine (squeeze + GBM), paper trader con gestione posizioni, stop loss, log JSONL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
"""Motore segnali: squeeze detection + ML confirmation su dati live."""
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.ensemble import GradientBoostingClassifier
|
||||
from sklearn.preprocessing import StandardScaler
|
||||
|
||||
|
||||
def keltner_ratio(close: np.ndarray, high: np.ndarray, low: np.ndarray, window: int = 14) -> np.ndarray:
|
||||
n = len(close)
|
||||
result = np.full(n, np.nan)
|
||||
for i in range(window, n):
|
||||
wc = close[i - window : i]
|
||||
wh = high[i - window : i]
|
||||
wl = low[i - window : i]
|
||||
ma = np.mean(wc)
|
||||
bb_std = np.std(wc)
|
||||
tr = np.maximum(wh - wl, np.maximum(np.abs(wh - np.roll(wc, 1)), np.abs(wl - np.roll(wc, 1))))
|
||||
atr = np.mean(tr[1:])
|
||||
kc_r = (ma + 1.5 * atr) - (ma - 1.5 * atr)
|
||||
bb_r = (ma + 2 * bb_std) - (ma - 2 * bb_std)
|
||||
if kc_r > 0:
|
||||
result[i] = bb_r / kc_r
|
||||
return result
|
||||
|
||||
|
||||
def build_features(df: pd.DataFrame, i: int, squeeze_duration: int, squeeze_avg_vol: float, kcr_val: float) -> np.ndarray | None:
|
||||
if i < 100 or i >= len(df):
|
||||
return None
|
||||
|
||||
o = df["open"].values
|
||||
h = df["high"].values
|
||||
l = df["low"].values
|
||||
c = df["close"].values
|
||||
v = df["volume"].values
|
||||
|
||||
feats = []
|
||||
for w in [12, 24, 48]:
|
||||
if i < w:
|
||||
feats.extend([0] * 12)
|
||||
continue
|
||||
|
||||
win_c = c[i - w : i]
|
||||
win_o = o[i - w : i]
|
||||
win_h = h[i - w : i]
|
||||
win_l = l[i - w : i]
|
||||
win_v = v[i - w : i]
|
||||
|
||||
mn, mx = win_l.min(), max(win_h.max(), win_c.max())
|
||||
rng = mx - mn if mx - mn > 0 else 1e-10
|
||||
total = win_h - win_l
|
||||
total = np.where(total == 0, 1e-10, total)
|
||||
body = np.abs(win_c - win_o) / total
|
||||
direction = np.sign(win_c - win_o)
|
||||
log_c = np.log(np.where(win_c == 0, 1e-10, win_c))
|
||||
rets = np.diff(log_c)
|
||||
v_mean = np.mean(win_v)
|
||||
|
||||
feats.extend([
|
||||
np.mean(rets) if len(rets) > 0 else 0,
|
||||
np.std(rets) if len(rets) > 0 else 0,
|
||||
np.sum(rets) if len(rets) > 0 else 0,
|
||||
float(pd.Series(rets).skew()) if len(rets) > 2 else 0,
|
||||
float(pd.Series(rets).kurtosis()) if len(rets) > 3 else 0,
|
||||
np.mean(body),
|
||||
np.std(body),
|
||||
np.mean(direction),
|
||||
np.mean(direction[-min(3, w):]),
|
||||
(win_c[-1] - mn) / rng,
|
||||
win_v[-1] / v_mean if v_mean > 0 else 1,
|
||||
np.corrcoef(rets[:-1], rets[1:])[0, 1] if len(rets) > 1 and np.std(rets) > 0 else 0,
|
||||
])
|
||||
|
||||
feats.extend([
|
||||
squeeze_duration,
|
||||
squeeze_duration / (24 * 4),
|
||||
kcr_val,
|
||||
v[i - 1] / squeeze_avg_vol if squeeze_avg_vol > 0 else 1,
|
||||
np.mean(v[max(0, i - 3) : i]) / squeeze_avg_vol if squeeze_avg_vol > 0 else 1,
|
||||
])
|
||||
|
||||
h48 = np.max(h[max(0, i - 48) : i])
|
||||
l48 = np.min(l[max(0, i - 48) : i])
|
||||
r48 = h48 - l48
|
||||
feats.append((c[i - 1] - l48) / r48 if r48 > 0 else 0.5)
|
||||
|
||||
tr = np.maximum(h[i - 14 : i] - l[i - 14 : i],
|
||||
np.maximum(np.abs(h[i - 14 : i] - np.roll(c[i - 14 : i], 1)),
|
||||
np.abs(l[i - 14 : i] - np.roll(c[i - 14 : i], 1))))
|
||||
atr = np.mean(tr[1:])
|
||||
feats.append(atr / c[i - 1] if c[i - 1] > 0 else 0)
|
||||
|
||||
first_ret = (c[i - 1] - c[i - 2]) / c[i - 2] if i >= 2 and c[i - 2] > 0 else 0
|
||||
feats.append(first_ret)
|
||||
|
||||
return np.nan_to_num(np.array(feats), nan=0, posinf=1e6, neginf=-1e6)
|
||||
|
||||
|
||||
class SignalEngine:
|
||||
"""Rileva squeeze e genera segnali ML in real-time."""
|
||||
|
||||
def __init__(self, bb_w: int = 14, sq_thr: float = 0.8, ml_thr: float = 0.70, min_squeeze_bars: int = 5):
|
||||
self.bb_w = bb_w
|
||||
self.sq_thr = sq_thr
|
||||
self.ml_thr = ml_thr
|
||||
self.min_squeeze_bars = min_squeeze_bars
|
||||
|
||||
self.model: GradientBoostingClassifier | None = None
|
||||
self.scaler: StandardScaler | None = None
|
||||
self.in_squeeze = False
|
||||
self.squeeze_start_idx = 0
|
||||
self.trained = False
|
||||
|
||||
def train(self, df: pd.DataFrame, lookahead: int = 3) -> dict:
|
||||
"""Addestra il modello su dati storici."""
|
||||
close = df["close"].values
|
||||
high = df["high"].values
|
||||
low = df["low"].values
|
||||
volume = df["volume"].values
|
||||
n = len(df)
|
||||
|
||||
kcr = keltner_ratio(close, high, low, self.bb_w)
|
||||
|
||||
X_all, y_all = [], []
|
||||
in_sq = False
|
||||
sq_start = 0
|
||||
|
||||
for i in range(self.bb_w + 1, n - lookahead):
|
||||
if np.isnan(kcr[i]):
|
||||
continue
|
||||
is_sq = kcr[i] < self.sq_thr
|
||||
if is_sq and not in_sq:
|
||||
in_sq = True
|
||||
sq_start = i
|
||||
elif not is_sq and in_sq:
|
||||
in_sq = False
|
||||
duration = i - sq_start
|
||||
if duration < self.min_squeeze_bars:
|
||||
continue
|
||||
|
||||
avg_vol = np.mean(volume[sq_start:i])
|
||||
feats = build_features(df, i, duration, avg_vol, kcr[i])
|
||||
if feats is None:
|
||||
continue
|
||||
|
||||
actual = (close[i + lookahead - 1] - close[i - 1]) / close[i - 1]
|
||||
X_all.append(feats)
|
||||
y_all.append(1 if actual > 0 else 0)
|
||||
|
||||
if len(X_all) < 30:
|
||||
return {"error": "not enough training samples", "samples": len(X_all)}
|
||||
|
||||
X = np.array(X_all)
|
||||
y = np.array(y_all)
|
||||
|
||||
self.scaler = StandardScaler()
|
||||
X_s = self.scaler.fit_transform(X)
|
||||
|
||||
self.model = GradientBoostingClassifier(
|
||||
n_estimators=150, max_depth=4, min_samples_leaf=10,
|
||||
learning_rate=0.05, subsample=0.8, random_state=42,
|
||||
)
|
||||
self.model.fit(X_s, y)
|
||||
self.trained = True
|
||||
|
||||
preds = self.model.predict(X_s)
|
||||
train_acc = np.mean(preds == y) * 100
|
||||
|
||||
return {"samples": len(X), "up_ratio": np.mean(y) * 100, "train_accuracy": train_acc}
|
||||
|
||||
def check_signal(self, df: pd.DataFrame) -> dict | None:
|
||||
"""Controlla se c'è un segnale sulle ultime candele.
|
||||
Ritorna dict con direzione e probabilità, oppure None.
|
||||
"""
|
||||
if not self.trained:
|
||||
return None
|
||||
|
||||
close = df["close"].values
|
||||
high = df["high"].values
|
||||
low = df["low"].values
|
||||
volume = df["volume"].values
|
||||
n = len(df)
|
||||
|
||||
kcr = keltner_ratio(close, high, low, self.bb_w)
|
||||
|
||||
if n < self.bb_w + 10:
|
||||
return None
|
||||
|
||||
last_kcr = kcr[-1]
|
||||
prev_kcr = kcr[-2] if n > 1 else np.nan
|
||||
|
||||
if np.isnan(last_kcr) or np.isnan(prev_kcr):
|
||||
return None
|
||||
|
||||
was_squeeze = prev_kcr < self.sq_thr
|
||||
is_released = last_kcr >= self.sq_thr
|
||||
|
||||
if not (was_squeeze and is_released):
|
||||
self.in_squeeze = prev_kcr < self.sq_thr
|
||||
if self.in_squeeze and not hasattr(self, '_sq_start_tracking'):
|
||||
self._sq_start_tracking = n - 1
|
||||
if not self.in_squeeze:
|
||||
self._sq_start_tracking = None
|
||||
return None
|
||||
|
||||
sq_start = getattr(self, '_sq_start_tracking', n - 10)
|
||||
if sq_start is None:
|
||||
sq_start = n - 10
|
||||
duration = (n - 1) - sq_start
|
||||
if duration < self.min_squeeze_bars:
|
||||
self._sq_start_tracking = None
|
||||
return None
|
||||
|
||||
avg_vol = np.mean(volume[max(0, sq_start) : n - 1])
|
||||
feats = build_features(df, n - 1, duration, avg_vol, last_kcr)
|
||||
self._sq_start_tracking = None
|
||||
|
||||
if feats is None:
|
||||
return None
|
||||
|
||||
feats_s = self.scaler.transform(feats.reshape(1, -1))
|
||||
proba = self.model.predict_proba(feats_s)[0]
|
||||
up_idx = list(self.model.classes_).index(1)
|
||||
p_up = proba[up_idx]
|
||||
|
||||
if p_up >= self.ml_thr:
|
||||
return {"direction": "buy", "probability": p_up, "squeeze_duration": duration}
|
||||
elif p_up <= (1 - self.ml_thr):
|
||||
return {"direction": "sell", "probability": 1 - p_up, "squeeze_duration": duration}
|
||||
|
||||
return None
|
||||
Reference in New Issue
Block a user