Spaces:
Paused
Paused
| # ============================================================ | |
| # 🎯 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 | |
| } |