Riy777 commited on
Commit
bc5527d
·
verified ·
1 Parent(s): 8b1c1fd

Update ml_engine/sniper_engine.py

Browse files
Files changed (1) hide show
  1. ml_engine/sniper_engine.py +171 -97
ml_engine/sniper_engine.py CHANGED
@@ -1,23 +1,22 @@
1
  # ============================================================
2
- # 🎯 ml_engine/sniper_engine.py (V1.8 - Fully Configurable)
 
3
  # ============================================================
4
 
5
  import os
6
- import sys
7
  import numpy as np
8
  import pandas as pd
9
  import pandas_ta as ta
10
  import lightgbm as lgb
11
- import joblib
12
- import asyncio
13
  import traceback
14
- from typing import List, Dict, Any
15
 
16
  N_SPLITS = 5
17
  LOOKBACK_WINDOW = 500
18
 
19
  # ============================================================
20
- # 🔧 1. دوال هندسة الميزات (Feature Engineering)
21
  # ============================================================
22
 
23
  def _z_score_rolling(x, w=500):
@@ -27,6 +26,7 @@ def _z_score_rolling(x, w=500):
27
  return z.fillna(0)
28
 
29
  def _add_liquidity_proxies(df):
 
30
  df_proxy = df.copy()
31
  if 'datetime' not in df_proxy.index:
32
  if 'timestamp' in df_proxy.columns:
@@ -36,30 +36,37 @@ def _add_liquidity_proxies(df):
36
  df_proxy['ret'] = df_proxy['close'].pct_change().fillna(0)
37
  df_proxy['dollar_vol'] = df_proxy['close'] * df_proxy['volume']
38
 
 
39
  df_proxy['amihud'] = (df_proxy['ret'].abs() / df_proxy['dollar_vol'].replace(0, np.nan)).fillna(np.inf)
40
 
 
41
  dp = df_proxy['close'].diff()
42
  roll_cov = dp.rolling(64).cov(dp.shift(1))
43
  df_proxy['roll_spread'] = (2 * np.sqrt(np.maximum(0, -roll_cov))).bfill()
44
 
 
45
  sign = np.sign(df_proxy['close'].diff()).fillna(0)
46
  df_proxy['signed_vol'] = sign * df_proxy['volume']
47
  df_proxy['ofi'] = df_proxy['signed_vol'].rolling(30).sum().fillna(0)
48
 
 
49
  buy_vol = (sign > 0) * df_proxy['volume']
50
  sell_vol = (sign < 0) * df_proxy['volume']
51
  imb = (buy_vol.rolling(60).sum() - sell_vol.rolling(60).sum()).abs()
52
  tot = df_proxy['volume'].rolling(60).sum()
53
  df_proxy['vpin'] = (imb / tot.replace(0, np.nan)).fillna(0)
54
 
 
55
  df_proxy['rv_gk'] = (np.log(df_proxy['high'] / df_proxy['low'])**2) / 2 - \
56
  (2 * np.log(2) - 1) * (np.log(df_proxy['close'] / df_proxy['open'])**2)
57
 
 
58
  vwap_window = 20
59
  df_proxy['vwap'] = (df_proxy['close'] * df_proxy['volume']).rolling(vwap_window).sum() / \
60
  df_proxy['volume'].rolling(vwap_window).sum()
61
  df_proxy['vwap_dev'] = (df_proxy['close'] - df_proxy['vwap']).fillna(0)
62
 
 
63
  df_proxy['L_score'] = (
64
  _z_score_rolling(df_proxy['volume']) +
65
  _z_score_rolling(1 / df_proxy['amihud'].replace(np.inf, np.nan)) +
@@ -71,6 +78,7 @@ def _add_liquidity_proxies(df):
71
  return df_proxy
72
 
73
  def _add_standard_features(df):
 
74
  df_feat = df.copy()
75
 
76
  df_feat['return_1m'] = df_feat['close'].pct_change(1)
@@ -102,7 +110,7 @@ def _add_standard_features(df):
102
  return df_feat
103
 
104
  # ============================================================
105
- # 🎯 2. كلاس المحرك الرئيسي (SniperEngine V1.8)
106
  # ============================================================
107
 
108
  class SniperEngine:
@@ -112,49 +120,62 @@ class SniperEngine:
112
  self.models: List[lgb.Booster] = []
113
  self.feature_names: List[str] = []
114
 
115
- # القيم الافتراضية (سيتم تحديثها من Processor)
116
  self.entry_threshold = 0.40
117
- self.wall_ratio_limit = 0.40 # نسبة الفيتو لجدار البيع
118
-
119
- # ✅ إضافة متغيرات للأوزان (قابلة للتكوين)
120
  self.weight_ml = 0.60
121
  self.weight_ob = 0.40
122
 
 
 
 
 
 
 
123
  self.initialized = False
124
  self.LOOKBACK_WINDOW = LOOKBACK_WINDOW
125
  self.ORDER_BOOK_DEPTH = 20
126
 
127
- print("🎯 [SniperEngine V1.8] Created.")
128
-
129
- # دالة تكوين شاملة (محدثة لاستقبال الأوزان)
130
- def configure_settings(self, threshold: float, wall_ratio: float, w_ml: float = 0.60, w_ob: float = 0.40):
 
 
 
 
 
 
 
 
 
 
131
  self.entry_threshold = threshold
132
  self.wall_ratio_limit = wall_ratio
133
  self.weight_ml = w_ml
134
  self.weight_ob = w_ob
135
- # print(f"🔧 [Sniper] Configured: Threshold={self.entry_threshold}, WallVeto={self.wall_ratio_limit}, W_ML={self.weight_ml}")
136
-
137
- # (إبقاء هذه للدعم القديم إذا لزم الأمر، لكن configure_settings أفضل)
138
- def set_entry_threshold(self, new_threshold: float):
139
- self.entry_threshold = new_threshold
140
 
141
  async def initialize(self):
142
- """تحميل النماذج"""
143
  print(f"🎯 [SniperEngine] Loading models from {self.models_dir}...")
144
  try:
145
  model_files = [f for f in os.listdir(self.models_dir) if f.startswith('lgbm_guard_v3_fold_')]
146
 
147
  if len(model_files) < N_SPLITS:
148
  print(f"❌ [SniperEngine] Error: Found {len(model_files)} models, need {N_SPLITS}.")
149
- return
150
-
151
  for f in sorted(model_files):
152
  model_path = os.path.join(self.models_dir, f)
153
  self.models.append(lgb.Booster(model_file=model_path))
154
 
155
- self.feature_names = self.models[0].feature_name()
 
 
156
  self.initialized = True
157
- print(f"✅ [SniperEngine] Ready. Threshold: {self.entry_threshold}, WallVeto: {self.wall_ratio_limit}")
158
 
159
  except Exception as e:
160
  print(f"❌ [SniperEngine] Init failed: {e}")
@@ -172,108 +193,161 @@ class SniperEngine:
172
  return pd.DataFrame()
173
 
174
  # ==============================================================================
175
- # 📊 3. منطق تحليل دفتر الطلبات
176
  # ==============================================================================
177
- def _score_order_book(self, order_book: Dict[str, Any]) -> Dict[str, Any]:
178
  try:
179
  bids = order_book.get('bids', [])
180
  asks = order_book.get('asks', [])
181
 
182
  if not bids or not asks:
183
- return {'score': 0.0, 'imbalance': 0.0, 'wall_ratio': 0.0, 'reason': 'Empty'}
184
-
185
- depth = self.ORDER_BOOK_DEPTH
186
- top_bids = bids[:depth]
187
- top_asks = asks[:depth]
188
-
189
- total_bid_vol = sum([float(x[1]) for x in top_bids])
190
- total_ask_vol = sum([float(x[1]) for x in top_asks])
191
- total_vol = total_bid_vol + total_ask_vol
192
-
193
- if total_vol == 0:
194
- return {'score': 0.0, 'imbalance': 0.0, 'wall_ratio': 0.0, 'reason': 'Zero Vol'}
195
 
196
- bid_imbalance = total_bid_vol / total_vol
 
 
 
197
 
198
- max_ask_wall = max([float(x[1]) for x in top_asks]) if top_asks else 0
199
- ask_wall_ratio = max_ask_wall / total_ask_vol if total_ask_vol > 0 else 0
200
-
201
- # ✅ استخدام المتغير المحقون wall_ratio_limit
202
- if ask_wall_ratio >= self.wall_ratio_limit:
203
  return {
204
  'score': 0.0,
205
- 'imbalance': float(bid_imbalance),
206
- 'wall_ratio': float(ask_wall_ratio),
207
- 'veto': True,
208
- 'reason': f"⛔ SELL WALL ({ask_wall_ratio:.2f} >= {self.wall_ratio_limit})"
209
  }
210
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  return {
212
- 'score': float(bid_imbalance),
213
- 'imbalance': float(bid_imbalance),
214
- 'wall_ratio': float(ask_wall_ratio),
215
- 'veto': False,
216
- 'reason': "OK"
 
217
  }
218
 
219
  except Exception as e:
220
- return {'score': 0.0, 'reason': f"Error: {e}", 'veto': True}
221
 
222
  # ==============================================================================
223
- # 🎯 4. دالة الفحص الرئيسية
224
  # ==============================================================================
225
- async def check_entry_signal_async(self, ohlcv_1m_data: List[List], order_book_data: Dict[str, Any] = None) -> Dict[str, Any]:
 
 
 
 
226
  if not self.initialized:
227
  return {'signal': 'WAIT', 'reason': 'Not initialized'}
228
 
229
- if len(ohlcv_1m_data) < self.LOOKBACK_WINDOW:
230
- return {'signal': 'WAIT', 'reason': 'Insuff Data'}
231
-
232
- try:
233
- df = pd.DataFrame(ohlcv_1m_data, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
234
- df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float)
235
-
236
- df_features = self._calculate_features_live(df)
237
- if df_features.empty:
238
- return {'signal': 'WAIT', 'reason': 'Feat Fail'}
239
-
240
- X_live = df_features.iloc[-1:][self.feature_names].fillna(0)
241
- preds = [m.predict(X_live)[0][1] for m in self.models]
242
- ml_score = float(np.mean(preds))
243
-
244
- except Exception as e:
245
- print(f"❌ [Sniper] ML Error: {e}")
246
- return {'signal': 'WAIT', 'reason': f'ML Exception: {e}'}
247
-
248
- ob_data = {'score': 0.5, 'imbalance': 0.5, 'wall_ratio': 0.0, 'veto': False}
249
- if order_book_data:
250
- ob_data = self._score_order_book(order_book_data)
251
-
252
- # ✅ استخدام الأوزان المحقونة (Dynamic Weights)
253
- final_score = (ml_score * self.weight_ml) + (ob_data['score'] * self.weight_ob)
254
 
255
- signal = 'WAIT'
256
- reason_str = f"Final:{final_score:.2f} (ML:{ml_score:.2f} + OB:{ob_data['score']:.2f})"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
257
 
258
- if ob_data.get('veto', False):
 
 
 
259
  signal = 'WAIT'
260
- reason_str = f"⛔ BLOCKED by OB Veto: {ob_data.get('reason')}"
261
-
262
- # ✅ استخدام العتبة المحقونة entry_threshold
263
- elif final_score >= self.entry_threshold:
264
- signal = 'BUY'
265
- reason_str = f"✅ APPROVED: {final_score:.2f} >= {self.entry_threshold} | ML:{ml_score:.2f}"
266
-
267
  else:
268
- signal = 'WAIT'
269
- reason_str = f"❌ LOW SCORE: {final_score:.2f} < {self.entry_threshold} | ML:{ml_score:.2f}"
 
 
 
 
 
 
270
 
271
  return {
272
  'signal': signal,
273
  'confidence_prob': final_score,
274
  'ml_score': ml_score,
275
- 'ob_score': ob_data['score'],
276
- 'ob_data': ob_data,
277
- 'threshold': self.entry_threshold,
278
  'reason': reason_str
279
  }
 
1
  # ============================================================
2
+ # 🎯 ml_engine/sniper_engine.py
3
+ # (V2.0 - GEM-Architect: Weighted Depth & Smart Microstructure)
4
  # ============================================================
5
 
6
  import os
7
+ import time
8
  import numpy as np
9
  import pandas as pd
10
  import pandas_ta as ta
11
  import lightgbm as lgb
 
 
12
  import traceback
13
+ from typing import List, Dict, Any, Optional
14
 
15
  N_SPLITS = 5
16
  LOOKBACK_WINDOW = 500
17
 
18
  # ============================================================
19
+ # 🔧 1. Feature Engineering (Standard + Liquidity Proxies)
20
  # ============================================================
21
 
22
  def _z_score_rolling(x, w=500):
 
26
  return z.fillna(0)
27
 
28
  def _add_liquidity_proxies(df):
29
+ """حساب مؤشرات السيولة المتقدمة (Amihud, VPIN, OFI, etc.)"""
30
  df_proxy = df.copy()
31
  if 'datetime' not in df_proxy.index:
32
  if 'timestamp' in df_proxy.columns:
 
36
  df_proxy['ret'] = df_proxy['close'].pct_change().fillna(0)
37
  df_proxy['dollar_vol'] = df_proxy['close'] * df_proxy['volume']
38
 
39
+ # Amihud Illiquidity Ratio
40
  df_proxy['amihud'] = (df_proxy['ret'].abs() / df_proxy['dollar_vol'].replace(0, np.nan)).fillna(np.inf)
41
 
42
+ # Roll Spread Proxy
43
  dp = df_proxy['close'].diff()
44
  roll_cov = dp.rolling(64).cov(dp.shift(1))
45
  df_proxy['roll_spread'] = (2 * np.sqrt(np.maximum(0, -roll_cov))).bfill()
46
 
47
+ # Order Flow Imbalance (Volume-based proxy)
48
  sign = np.sign(df_proxy['close'].diff()).fillna(0)
49
  df_proxy['signed_vol'] = sign * df_proxy['volume']
50
  df_proxy['ofi'] = df_proxy['signed_vol'].rolling(30).sum().fillna(0)
51
 
52
+ # VPIN-like Imbalance
53
  buy_vol = (sign > 0) * df_proxy['volume']
54
  sell_vol = (sign < 0) * df_proxy['volume']
55
  imb = (buy_vol.rolling(60).sum() - sell_vol.rolling(60).sum()).abs()
56
  tot = df_proxy['volume'].rolling(60).sum()
57
  df_proxy['vpin'] = (imb / tot.replace(0, np.nan)).fillna(0)
58
 
59
+ # Volatility Estimator (Garman-Klass)
60
  df_proxy['rv_gk'] = (np.log(df_proxy['high'] / df_proxy['low'])**2) / 2 - \
61
  (2 * np.log(2) - 1) * (np.log(df_proxy['close'] / df_proxy['open'])**2)
62
 
63
+ # VWAP Deviation
64
  vwap_window = 20
65
  df_proxy['vwap'] = (df_proxy['close'] * df_proxy['volume']).rolling(vwap_window).sum() / \
66
  df_proxy['volume'].rolling(vwap_window).sum()
67
  df_proxy['vwap_dev'] = (df_proxy['close'] - df_proxy['vwap']).fillna(0)
68
 
69
+ # Composite Liquidity Score
70
  df_proxy['L_score'] = (
71
  _z_score_rolling(df_proxy['volume']) +
72
  _z_score_rolling(1 / df_proxy['amihud'].replace(np.inf, np.nan)) +
 
78
  return df_proxy
79
 
80
  def _add_standard_features(df):
81
+ """المؤشرات الفنية القياسية"""
82
  df_feat = df.copy()
83
 
84
  df_feat['return_1m'] = df_feat['close'].pct_change(1)
 
110
  return df_feat
111
 
112
  # ============================================================
113
+ # 🎯 2. SniperEngine Class (Refactored)
114
  # ============================================================
115
 
116
  class SniperEngine:
 
120
  self.models: List[lgb.Booster] = []
121
  self.feature_names: List[str] = []
122
 
123
+ # --- Configurable Thresholds (Defaults) ---
124
  self.entry_threshold = 0.40
125
+ self.wall_ratio_limit = 0.40 # Veto threshold for sell wall
 
 
126
  self.weight_ml = 0.60
127
  self.weight_ob = 0.40
128
 
129
+ # --- Advanced OB Settings (New in V2.0) ---
130
+ self.ob_depth_decay = 0.15 # Decay factor for weighted depth
131
+ self.max_wall_dist = 0.005 # 0.5% max distance to consider a wall
132
+ self.max_spread_pct = 0.002 # 0.2% max spread allowed
133
+ self.spoof_patience = 0 # How many previous checks to ignore a new wall (0 = Instant Veto)
134
+
135
  self.initialized = False
136
  self.LOOKBACK_WINDOW = LOOKBACK_WINDOW
137
  self.ORDER_BOOK_DEPTH = 20
138
 
139
+ # --- Persistence Cache for Anti-Spoofing ---
140
+ # Format: {symbol: {'last_check': timestamp, 'wall_counter': int}}
141
+ self._wall_cache = {}
142
+
143
+ print("🎯 [SniperEngine V2.0] Weighted Depth & Smart Microstructure Ready.")
144
+
145
+ def configure_settings(self,
146
+ threshold: float,
147
+ wall_ratio: float,
148
+ w_ml: float = 0.60,
149
+ w_ob: float = 0.40,
150
+ max_wall_dist: float = 0.005,
151
+ max_spread: float = 0.002):
152
+ """Dynamic configuration injection"""
153
  self.entry_threshold = threshold
154
  self.wall_ratio_limit = wall_ratio
155
  self.weight_ml = w_ml
156
  self.weight_ob = w_ob
157
+ self.max_wall_dist = max_wall_dist
158
+ self.max_spread_pct = max_spread
 
 
 
159
 
160
  async def initialize(self):
161
+ """Load LightGBM Models"""
162
  print(f"🎯 [SniperEngine] Loading models from {self.models_dir}...")
163
  try:
164
  model_files = [f for f in os.listdir(self.models_dir) if f.startswith('lgbm_guard_v3_fold_')]
165
 
166
  if len(model_files) < N_SPLITS:
167
  print(f"❌ [SniperEngine] Error: Found {len(model_files)} models, need {N_SPLITS}.")
168
+ # Don't return, allow initialization without models (fallback mode)
169
+
170
  for f in sorted(model_files):
171
  model_path = os.path.join(self.models_dir, f)
172
  self.models.append(lgb.Booster(model_file=model_path))
173
 
174
+ if self.models:
175
+ self.feature_names = self.models[0].feature_name()
176
+
177
  self.initialized = True
178
+ print(f"✅ [SniperEngine] Active. WallLimit: {self.wall_ratio_limit}, MaxDist: {self.max_wall_dist*100}%")
179
 
180
  except Exception as e:
181
  print(f"❌ [SniperEngine] Init failed: {e}")
 
193
  return pd.DataFrame()
194
 
195
  # ==============================================================================
196
+ # 📊 3. Smart Order Book Logic (The Architect's Upgrade)
197
  # ==============================================================================
198
+ def _score_order_book(self, order_book: Dict[str, Any], symbol: str = None) -> Dict[str, Any]:
199
  try:
200
  bids = order_book.get('bids', [])
201
  asks = order_book.get('asks', [])
202
 
203
  if not bids or not asks:
204
+ return {'score': 0.0, 'imbalance': 0.0, 'veto': True, 'reason': 'Empty OB'}
 
 
 
 
 
 
 
 
 
 
 
205
 
206
+ # --- 1. Spread Check ---
207
+ best_bid = float(bids[0][0])
208
+ best_ask = float(asks[0][0])
209
+ spread_pct = (best_ask - best_bid) / best_bid
210
 
211
+ if spread_pct > self.max_spread_pct:
 
 
 
 
212
  return {
213
  'score': 0.0,
214
+ 'veto': True,
215
+ 'reason': f"Wide Spread ({spread_pct:.2%})"
 
 
216
  }
217
 
218
+ # --- 2. Weighted Depth Imbalance ---
219
+ # Calculates imbalance giving higher weight to prices closer to spread
220
+ w_bid_vol = 0.0
221
+ w_ask_vol = 0.0
222
+ total_raw_ask_vol = 0.0 # for wall calculation
223
+
224
+ # Limit depth processing to configured depth
225
+ depth = min(len(bids), len(asks), self.ORDER_BOOK_DEPTH)
226
+
227
+ for i in range(depth):
228
+ # Decay Function: 1 / (1 + k * rank)
229
+ weight = 1.0 / (1.0 + (self.ob_depth_decay * i))
230
+
231
+ bid_vol = float(bids[i][1])
232
+ ask_vol = float(asks[i][1])
233
+
234
+ w_bid_vol += bid_vol * weight
235
+ w_ask_vol += ask_vol * weight
236
+ total_raw_ask_vol += ask_vol
237
+
238
+ total_w_vol = w_bid_vol + w_ask_vol
239
+ weighted_imbalance = w_bid_vol / total_w_vol if total_w_vol > 0 else 0.5
240
+
241
+ # --- 3. Distance-Aware Wall Detection ---
242
+ max_valid_wall = 0.0
243
+ limit_price = best_ask * (1 + self.max_wall_dist)
244
+
245
+ for price, vol in asks[:depth]:
246
+ p = float(price)
247
+ v = float(vol)
248
+ if p <= limit_price:
249
+ if v > max_valid_wall: max_valid_wall = v
250
+
251
+ wall_ratio = max_valid_wall / total_raw_ask_vol if total_raw_ask_vol > 0 else 0
252
+
253
+ # --- 4. Anti-Spoofing / Persistence Logic ---
254
+ veto_wall = False
255
+ veto_reason = "OK"
256
+
257
+ if wall_ratio >= self.wall_ratio_limit:
258
+ # Wall Detected
259
+ veto_wall = True
260
+ veto_reason = f"Sell Wall ({wall_ratio:.2f})"
261
+
262
+ if symbol:
263
+ curr_time = time.time()
264
+ cache = self._wall_cache.get(symbol, {'last_check': 0, 'count': 0})
265
+
266
+ # If this is a NEW wall (seen less than 1 second ago)
267
+ if curr_time - cache['last_check'] > 5.0:
268
+ # Reset counter if too much time passed
269
+ cache['count'] = 1
270
+ else:
271
+ cache['count'] += 1
272
+
273
+ cache['last_check'] = curr_time
274
+ self._wall_cache[symbol] = cache
275
+
276
+ # Optional: Logic to IGNORE flashing walls could go here
277
+ # For now, we block on first sight (Safety First)
278
+ else:
279
+ # No wall, clear cache slightly
280
+ if symbol and symbol in self._wall_cache:
281
+ self._wall_cache[symbol]['count'] = 0
282
+
283
  return {
284
+ 'score': float(weighted_imbalance),
285
+ 'imbalance': float(weighted_imbalance), # Now Weighted
286
+ 'wall_ratio': float(wall_ratio),
287
+ 'veto': veto_wall,
288
+ 'spread_ok': True,
289
+ 'reason': veto_reason
290
  }
291
 
292
  except Exception as e:
293
+ return {'score': 0.0, 'veto': True, 'reason': f"OB Error: {e}"}
294
 
295
  # ==============================================================================
296
+ # 🎯 4. Main Signal Check (Async)
297
  # ==============================================================================
298
+ async def check_entry_signal_async(self,
299
+ ohlcv_1m_data: List[List],
300
+ order_book_data: Dict[str, Any] = None,
301
+ symbol: str = None) -> Dict[str, Any]:
302
+
303
  if not self.initialized:
304
  return {'signal': 'WAIT', 'reason': 'Not initialized'}
305
 
306
+ # --- ML Prediction ---
307
+ ml_score = 0.5
308
+ ml_reason = "No Data"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
 
310
+ if len(ohlcv_1m_data) >= self.LOOKBACK_WINDOW and self.models:
311
+ try:
312
+ df = pd.DataFrame(ohlcv_1m_data, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
313
+ df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float)
314
+
315
+ df_features = self._calculate_features_live(df)
316
+ if not df_features.empty:
317
+ X_live = df_features.iloc[-1:][self.feature_names].fillna(0)
318
+ preds = [m.predict(X_live)[0][1] for m in self.models]
319
+ ml_score = float(np.mean(preds))
320
+ ml_reason = f"ML:{ml_score:.2f}"
321
+ except Exception as e:
322
+ print(f"❌ [Sniper] ML Error: {e}")
323
+ ml_reason = "ML Err"
324
+
325
+ # --- Smart Order Book Analysis ---
326
+ ob_res = {'score': 0.5, 'imbalance': 0.5, 'veto': False, 'reason': 'No OB'}
327
+ if order_book_data:
328
+ ob_res = self._score_order_book(order_book_data, symbol=symbol)
329
 
330
+ # --- Final Hybrid Score ---
331
+ # If OB vetos (Spread too high OR Sell Wall), we force score down or WAIT
332
+ if ob_res.get('veto', False):
333
+ final_score = 0.0
334
  signal = 'WAIT'
335
+ reason_str = f"⛔ {ob_res['reason']} | {ml_reason}"
 
 
 
 
 
 
336
  else:
337
+ final_score = (ml_score * self.weight_ml) + (ob_res['score'] * self.weight_ob)
338
+
339
+ if final_score >= self.entry_threshold:
340
+ signal = 'BUY'
341
+ reason_str = f"✅ GO: {final_score:.2f} | {ml_reason} | OB:{ob_res['score']:.2f}"
342
+ else:
343
+ signal = 'WAIT'
344
+ reason_str = f"📉 Low Score: {final_score:.2f} | {ml_reason}"
345
 
346
  return {
347
  'signal': signal,
348
  'confidence_prob': final_score,
349
  'ml_score': ml_score,
350
+ 'ob_score': ob_res['score'],
351
+ 'entry_price': float(order_book_data['asks'][0][0]) if order_book_data and order_book_data.get('asks') else 0.0,
 
352
  'reason': reason_str
353
  }