Tradtesting / ml_engine /sniper_engine.py
Riy777's picture
Update ml_engine/sniper_engine.py
031543e verified
raw
history blame
11.7 kB
# ============================================================
# 🎯 ml_engine/sniper_engine.py (V1.8 - Fully Configurable)
# ============================================================
import os
import sys
import numpy as np
import pandas as pd
import pandas_ta as ta
import lightgbm as lgb
import joblib
import asyncio
import traceback
from typing import List, Dict, Any
N_SPLITS = 5
LOOKBACK_WINDOW = 500
# ============================================================
# 🔧 1. دوال هندسة الميزات (Feature Engineering)
# ============================================================
def _z_score_rolling(x, w=500):
r = x.rolling(w).mean()
s = x.rolling(w).std().replace(0, np.nan)
z = (x - r) / s
return z.fillna(0)
def _add_liquidity_proxies(df):
df_proxy = df.copy()
if 'datetime' not in df_proxy.index:
if 'timestamp' in df_proxy.columns:
df_proxy['datetime'] = pd.to_datetime(df_proxy['timestamp'], unit='ms')
df_proxy = df_proxy.set_index('datetime')
df_proxy['ret'] = df_proxy['close'].pct_change().fillna(0)
df_proxy['dollar_vol'] = df_proxy['close'] * df_proxy['volume']
df_proxy['amihud'] = (df_proxy['ret'].abs() / df_proxy['dollar_vol'].replace(0, np.nan)).fillna(np.inf)
dp = df_proxy['close'].diff()
roll_cov = dp.rolling(64).cov(dp.shift(1))
df_proxy['roll_spread'] = (2 * np.sqrt(np.maximum(0, -roll_cov))).bfill()
sign = np.sign(df_proxy['close'].diff()).fillna(0)
df_proxy['signed_vol'] = sign * df_proxy['volume']
df_proxy['ofi'] = df_proxy['signed_vol'].rolling(30).sum().fillna(0)
buy_vol = (sign > 0) * df_proxy['volume']
sell_vol = (sign < 0) * df_proxy['volume']
imb = (buy_vol.rolling(60).sum() - sell_vol.rolling(60).sum()).abs()
tot = df_proxy['volume'].rolling(60).sum()
df_proxy['vpin'] = (imb / tot.replace(0, np.nan)).fillna(0)
df_proxy['rv_gk'] = (np.log(df_proxy['high'] / df_proxy['low'])**2) / 2 - \
(2 * np.log(2) - 1) * (np.log(df_proxy['close'] / df_proxy['open'])**2)
vwap_window = 20
df_proxy['vwap'] = (df_proxy['close'] * df_proxy['volume']).rolling(vwap_window).sum() / \
df_proxy['volume'].rolling(vwap_window).sum()
df_proxy['vwap_dev'] = (df_proxy['close'] - df_proxy['vwap']).fillna(0)
df_proxy['L_score'] = (
_z_score_rolling(df_proxy['volume']) +
_z_score_rolling(1 / df_proxy['amihud'].replace(np.inf, np.nan)) +
_z_score_rolling(-df_proxy['roll_spread']) +
_z_score_rolling(-df_proxy['rv_gk'].abs()) +
_z_score_rolling(-df_proxy['vwap_dev'].abs()) +
_z_score_rolling(df_proxy['ofi'])
)
return df_proxy
def _add_standard_features(df):
df_feat = df.copy()
df_feat['return_1m'] = df_feat['close'].pct_change(1)
df_feat['return_3m'] = df_feat['close'].pct_change(3)
df_feat['return_5m'] = df_feat['close'].pct_change(5)
df_feat['return_15m'] = df_feat['close'].pct_change(15)
df_feat['rsi_14'] = ta.rsi(df_feat['close'], length=14)
ema_9 = ta.ema(df_feat['close'], length=9)
ema_21 = ta.ema(df_feat['close'], length=21)
if ema_9 is not None:
df_feat['ema_9_slope'] = (ema_9 - ema_9.shift(1)) / ema_9.shift(1)
else:
df_feat['ema_9_slope'] = 0
if ema_21 is not None:
df_feat['ema_21_dist'] = (df_feat['close'] - ema_21) / ema_21
else:
df_feat['ema_21_dist'] = 0
df_feat['atr'] = ta.atr(df_feat['high'], df_feat['low'], df_feat['close'], length=100)
df_feat['vol_zscore_50'] = _z_score_rolling(df_feat['volume'], w=50)
df_feat['candle_range'] = df_feat['high'] - df_feat['low']
df_feat['close_pos_in_range'] = (df_feat['close'] - df_feat['low']) / (df_feat['candle_range'].replace(0, np.nan))
return df_feat
# ============================================================
# 🎯 2. كلاس المحرك الرئيسي (SniperEngine V1.8)
# ============================================================
class SniperEngine:
def __init__(self, models_dir: str):
self.models_dir = models_dir
self.models: List[lgb.Booster] = []
self.feature_names: List[str] = []
# ✅ القيم الافتراضية (سيتم تحديثها من Processor)
self.entry_threshold = 0.40
self.wall_ratio_limit = 0.40 # نسبة الفيتو لجدار البيع
# ✅ إضافة متغيرات للأوزان (قابلة للتكوين)
self.weight_ml = 0.60
self.weight_ob = 0.40
self.initialized = False
self.LOOKBACK_WINDOW = LOOKBACK_WINDOW
self.ORDER_BOOK_DEPTH = 20
print("🎯 [SniperEngine V1.8] Created.")
# ✅ دالة تكوين شاملة (محدثة لاستقبال الأوزان)
def configure_settings(self, threshold: float, wall_ratio: float, w_ml: float = 0.60, w_ob: float = 0.40):
self.entry_threshold = threshold
self.wall_ratio_limit = wall_ratio
self.weight_ml = w_ml
self.weight_ob = w_ob
# print(f"🔧 [Sniper] Configured: Threshold={self.entry_threshold}, WallVeto={self.wall_ratio_limit}, W_ML={self.weight_ml}")
# (إبقاء هذه للدعم القديم إذا لزم الأمر، لكن configure_settings أفضل)
def set_entry_threshold(self, new_threshold: float):
self.entry_threshold = new_threshold
async def initialize(self):
"""تحميل النماذج"""
print(f"🎯 [SniperEngine] Loading models from {self.models_dir}...")
try:
model_files = [f for f in os.listdir(self.models_dir) if f.startswith('lgbm_guard_v3_fold_')]
if len(model_files) < N_SPLITS:
print(f"❌ [SniperEngine] Error: Found {len(model_files)} models, need {N_SPLITS}.")
return
for f in sorted(model_files):
model_path = os.path.join(self.models_dir, f)
self.models.append(lgb.Booster(model_file=model_path))
self.feature_names = self.models[0].feature_name()
self.initialized = True
print(f"✅ [SniperEngine] Ready. Threshold: {self.entry_threshold}, WallVeto: {self.wall_ratio_limit}")
except Exception as e:
print(f"❌ [SniperEngine] Init failed: {e}")
traceback.print_exc()
self.initialized = False
def _calculate_features_live(self, df_1m: pd.DataFrame) -> pd.DataFrame:
try:
df_with_std_feats = _add_standard_features(df_1m)
df_with_all_feats = _add_liquidity_proxies(df_with_std_feats)
df_final = df_with_all_feats.replace([np.inf, -np.inf], np.nan)
return df_final
except Exception as e:
print(f"❌ [SniperEngine] Feature calc error: {e}")
return pd.DataFrame()
# ==============================================================================
# 📊 3. منطق تحليل دفتر الطلبات
# ==============================================================================
def _score_order_book(self, order_book: Dict[str, Any]) -> Dict[str, Any]:
try:
bids = order_book.get('bids', [])
asks = order_book.get('asks', [])
if not bids or not asks:
return {'score': 0.0, 'imbalance': 0.0, 'wall_ratio': 0.0, 'reason': 'Empty'}
depth = self.ORDER_BOOK_DEPTH
top_bids = bids[:depth]
top_asks = asks[:depth]
total_bid_vol = sum([float(x[1]) for x in top_bids])
total_ask_vol = sum([float(x[1]) for x in top_asks])
total_vol = total_bid_vol + total_ask_vol
if total_vol == 0:
return {'score': 0.0, 'imbalance': 0.0, 'wall_ratio': 0.0, 'reason': 'Zero Vol'}
bid_imbalance = total_bid_vol / total_vol
max_ask_wall = max([float(x[1]) for x in top_asks]) if top_asks else 0
ask_wall_ratio = max_ask_wall / total_ask_vol if total_ask_vol > 0 else 0
# ✅ استخدام المتغير المحقون wall_ratio_limit
if ask_wall_ratio >= self.wall_ratio_limit:
return {
'score': 0.0,
'imbalance': float(bid_imbalance),
'wall_ratio': float(ask_wall_ratio),
'veto': True,
'reason': f"⛔ SELL WALL ({ask_wall_ratio:.2f} >= {self.wall_ratio_limit})"
}
return {
'score': float(bid_imbalance),
'imbalance': float(bid_imbalance),
'wall_ratio': float(ask_wall_ratio),
'veto': False,
'reason': "OK"
}
except Exception as e:
return {'score': 0.0, 'reason': f"Error: {e}", 'veto': True}
# ==============================================================================
# 🎯 4. دالة الفحص الرئيسية
# ==============================================================================
async def check_entry_signal_async(self, ohlcv_1m_data: List[List], order_book_data: Dict[str, Any] = None) -> Dict[str, Any]:
if not self.initialized:
return {'signal': 'WAIT', 'reason': 'Not initialized'}
if len(ohlcv_1m_data) < self.LOOKBACK_WINDOW:
return {'signal': 'WAIT', 'reason': 'Insuff Data'}
try:
df = pd.DataFrame(ohlcv_1m_data, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float)
df_features = self._calculate_features_live(df)
if df_features.empty:
return {'signal': 'WAIT', 'reason': 'Feat Fail'}
X_live = df_features.iloc[-1:][self.feature_names].fillna(0)
preds = [m.predict(X_live)[0][1] for m in self.models]
ml_score = float(np.mean(preds))
except Exception as e:
print(f"❌ [Sniper] ML Error: {e}")
return {'signal': 'WAIT', 'reason': f'ML Exception: {e}'}
ob_data = {'score': 0.5, 'imbalance': 0.5, 'wall_ratio': 0.0, 'veto': False}
if order_book_data:
ob_data = self._score_order_book(order_book_data)
# ✅ استخدام الأوزان المحقونة (Dynamic Weights)
final_score = (ml_score * self.weight_ml) + (ob_data['score'] * self.weight_ob)
signal = 'WAIT'
reason_str = f"Final:{final_score:.2f} (ML:{ml_score:.2f} + OB:{ob_data['score']:.2f})"
if ob_data.get('veto', False):
signal = 'WAIT'
reason_str = f"⛔ BLOCKED by OB Veto: {ob_data.get('reason')}"
# ✅ استخدام العتبة المحقونة entry_threshold
elif final_score >= self.entry_threshold:
signal = 'BUY'
reason_str = f"✅ APPROVED: {final_score:.2f} >= {self.entry_threshold} | ML:{ml_score:.2f}"
else:
signal = 'WAIT'
reason_str = f"❌ LOW SCORE: {final_score:.2f} < {self.entry_threshold} | ML:{ml_score:.2f}"
return {
'signal': signal,
'confidence_prob': final_score,
'ml_score': ml_score,
'ob_score': ob_data['score'],
'ob_data': ob_data,
'threshold': self.entry_threshold,
'reason': reason_str
}