Riy777 commited on
Commit
165d676
·
1 Parent(s): bd24c07

Create sniper_engine.py

Browse files
Files changed (1) hide show
  1. ml_engine/sniper_engine.py +238 -0
ml_engine/sniper_engine.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ============================================================
2
+ # 🎯 ml_engine/sniper_engine.py (V1.0 - L2 Entry Sniper)
3
+ # هذا هو "الحارس V3" الذي بنيناه (زناد الدخول)
4
+ # ============================================================
5
+
6
+ import os
7
+ import sys
8
+ import numpy as np
9
+ import pandas as pd
10
+ import pandas_ta as ta
11
+ import lightgbm as lgb
12
+ import joblib
13
+ import asyncio
14
+ import traceback
15
+ from typing import List, Dict, Any
16
+
17
+ # --- [ 💡 💡 💡 ] ---
18
+ # [ 🚀 🚀 🚀 ] العتبة الافتراضية (بناءً على طلبك بعد الاختبار المنفصل)
19
+ DEFAULT_SNIPER_THRESHOLD = 0.60
20
+ # [ 🚀 🚀 🚀 ]
21
+ # --- [ 💡 💡 💡 ] ---
22
+
23
+ N_SPLITS = 5
24
+ LOOKBACK_WINDOW = 500 # (الحد الأدنى للشموع 1m لحساب Z-Score (w=500))
25
+
26
+ # ============================================================
27
+ # 🔧 1. دوال هندسة الميزات (مطابقة 100% للمرحلة 2.ب)
28
+ # ============================================================
29
+
30
+ def _z_score_rolling(x, w=500):
31
+ """حساب Z-Score المتدحرج (آمن من القسمة على صفر)"""
32
+ r = x.rolling(w).mean()
33
+ s = x.rolling(w).std().replace(0, np.nan)
34
+ z = (x - r) / s
35
+ return z.fillna(0)
36
+
37
+ def _add_liquidity_proxies(df):
38
+ """
39
+ إضافة بدائل السيولة وتدفق الطلب المتقدمة.
40
+ """
41
+ df_proxy = df.copy()
42
+ if 'datetime' not in df_proxy.index:
43
+ if 'timestamp' in df_proxy.columns:
44
+ df_proxy['datetime'] = pd.to_datetime(df_proxy['timestamp'], unit='ms')
45
+ df_proxy = df_proxy.set_index('datetime')
46
+ else:
47
+ print("❌ [SniperEngine] خطأ في بدائل السيولة: المؤشر الزمني مفقود.")
48
+ return df_proxy
49
+
50
+ df_proxy['ret'] = df_proxy['close'].pct_change().fillna(0)
51
+ df_proxy['dollar_vol'] = df_proxy['close'] * df_proxy['volume']
52
+
53
+ df_proxy['amihud'] = (df_proxy['ret'].abs() / df_proxy['dollar_vol'].replace(0, np.nan)).fillna(np.inf)
54
+
55
+ dp = df_proxy['close'].diff()
56
+ roll_cov = dp.rolling(64).cov(dp.shift(1))
57
+ df_proxy['roll_spread'] = (2 * np.sqrt(np.maximum(0, -roll_cov))).fillna(method='bfill')
58
+
59
+ sign = np.sign(df_proxy['close'].diff()).fillna(0)
60
+ df_proxy['signed_vol'] = sign * df_proxy['volume']
61
+ df_proxy['ofi'] = df_proxy['signed_vol'].rolling(30).sum().fillna(0)
62
+
63
+ buy_vol = (sign > 0) * df_proxy['volume']
64
+ sell_vol = (sign < 0) * df_proxy['volume']
65
+ imb = (buy_vol.rolling(60).sum() - sell_vol.rolling(60).sum()).abs()
66
+ tot = df_proxy['volume'].rolling(60).sum()
67
+ df_proxy['vpin'] = (imb / tot.replace(0, np.nan)).fillna(0)
68
+
69
+ df_proxy['rv_gk'] = (np.log(df_proxy['high'] / df_proxy['low'])**2) / 2 - \
70
+ (2 * np.log(2) - 1) * (np.log(df_proxy['close'] / df_proxy['open'])**2)
71
+
72
+ vwap_window = 20
73
+ df_proxy['vwap'] = (df_proxy['close'] * df_proxy['volume']).rolling(vwap_window).sum() / \
74
+ df_proxy['volume'].rolling(vwap_window).sum()
75
+ df_proxy['vwap_dev'] = (df_proxy['close'] - df_proxy['vwap']).fillna(0)
76
+
77
+ df_proxy['L_score'] = (
78
+ _z_score_rolling(df_proxy['volume']) +
79
+ _z_score_rolling(1 / df_proxy['amihud'].replace(np.inf, np.nan)) +
80
+ _z_score_rolling(-df_proxy['roll_spread']) +
81
+ _z_score_rolling(-df_proxy['rv_gk'].abs()) +
82
+ _z_score_rolling(-df_proxy['vwap_dev'].abs()) +
83
+ _z_score_rolling(df_proxy['ofi'])
84
+ )
85
+
86
+ return df_proxy
87
+
88
+ def _add_standard_features(df):
89
+ """إضافة الميزات القياسية (عوائد، زخم، حجم)"""
90
+ df_feat = df.copy()
91
+
92
+ df_feat['return_1m'] = df_feat['close'].pct_change(1)
93
+ df_feat['return_3m'] = df_feat['close'].pct_change(3)
94
+ df_feat['return_5m'] = df_feat['close'].pct_change(5)
95
+ df_feat['return_15m'] = df_feat['close'].pct_change(15)
96
+
97
+ df_feat['rsi_14'] = ta.rsi(df_feat['close'], length=14)
98
+ ema_9 = ta.ema(df_feat['close'], length=9)
99
+ ema_21 = ta.ema(df_feat['close'], length=21)
100
+ df_feat['ema_9_slope'] = (ema_9 - ema_9.shift(1)) / ema_9.shift(1)
101
+ df_feat['ema_21_dist'] = (df_feat['close'] - ema_21) / ema_21
102
+
103
+ df_feat['atr'] = ta.atr(df_feat['high'], df_feat['low'], df_feat['close'], length=100)
104
+ df_feat['vol_zscore_50'] = _z_score_rolling(df_feat['volume'], w=50)
105
+
106
+ df_feat['candle_range'] = df_feat['high'] - df_feat['low']
107
+ df_feat['close_pos_in_range'] = (df_feat['close'] - df_feat['low']) / (df_feat['candle_range'].replace(0, np.nan))
108
+
109
+ return df_feat
110
+
111
+ # ============================================================
112
+ # 🎯 2. كلاس المحرك الرئيسي (SniperEngine V1)
113
+ # ============================================================
114
+
115
+ class SniperEngine:
116
+ def __init__(self, base_project_dir: str):
117
+ """
118
+ تهيئة محرك قناص الدخول V1 (L2 Sniper).
119
+ Args:
120
+ base_project_dir: المسار الرئيسي للمشروع (Guard_Project)
121
+ """
122
+ # (نحمل النماذج من 'Models_V3' الذي أنشأناه)
123
+ self.models_dir = os.path.join(base_project_dir, "Models_V3")
124
+ self.models: List[lgb.Booster] = []
125
+ self.feature_names: List[str] = []
126
+
127
+ self.threshold = DEFAULT_SNIPER_THRESHOLD
128
+ self.initialized = False
129
+
130
+ # (جعل LOOKBACK_WINDOW متاحاً للكود الخارجي)
131
+ self.LOOKBACK_WINDOW = LOOKBACK_WINDOW
132
+
133
+ print("🎯 [SniperEngine V1] تم الإنشاء. جاهز للتهيئة.")
134
+
135
+ async def initialize(self):
136
+ """
137
+ تحميل النماذج الخمسة (Ensemble) وقائمة الميزات.
138
+ """
139
+ print(f"🎯 [SniperEngine V1] جاري التهيئة من {self.models_dir}...")
140
+ try:
141
+ model_files = [f for f in os.listdir(self.models_dir) if f.startswith('lgbm_guard_v3_fold_')]
142
+ if len(model_files) < N_SPLITS:
143
+ print(f"❌ [SniperEngine V1] خطأ فادح: تم العثور على {len(model_files)} نماذج فقط، مطلوب {N_SPLITS}.")
144
+ return
145
+
146
+ for f in sorted(model_files):
147
+ model_path = os.path.join(self.models_dir, f)
148
+ self.models.append(lgb.Booster(model_file=model_path))
149
+
150
+ self.feature_names = self.models[0].feature_name()
151
+ self.initialized = True
152
+ print(f"✅ [SniperEngine V1] تم تحميل {len(self.models)} نماذج قنص بنجاح.")
153
+ print(f" -> تم تحديد {len(self.feature_names)} ميزة مطلوبة.")
154
+ print(f" -> تم ضبط عتبة الدخول الافتراضية على: {self.threshold * 100:.1f}%")
155
+
156
+ except Exception as e:
157
+ print(f"❌ [SniperEngine V1] فشل التهيئة: {e}")
158
+ traceback.print_exc()
159
+ self.initialized = False
160
+
161
+ def set_entry_threshold(self, new_threshold: float):
162
+ """
163
+ السماح بتغيير العتبة أثناء التشغيل.
164
+ """
165
+ if 0.30 <= new_threshold <= 0.70:
166
+ print(f"🎯 [SniperEngine V1] تم تحديث العتبة من {self.threshold} إلى {new_threshold}")
167
+ self.threshold = new_threshold
168
+ else:
169
+ print(f"⚠️ [SniperEngine V1] تم تجاهل العتبة (خارج النطاق): {new_threshold}")
170
+
171
+ def _calculate_features_live(self, df_1m: pd.DataFrame) -> pd.DataFrame:
172
+ """
173
+ الدالة الخاصة لتطبيق خط أنابيب الميزات الكامل.
174
+ """
175
+ try:
176
+ df_with_std_feats = _add_standard_features(df_1m)
177
+ df_with_all_feats = _add_liquidity_proxies(df_with_std_feats)
178
+ df_final = df_with_all_feats.replace([np.inf, -np.inf], np.nan)
179
+ return df_final
180
+
181
+ except Exception as e:
182
+ print(f"❌ [SniperEngine V1] فشل حساب الميزات: {e}")
183
+ return pd.DataFrame()
184
+
185
+ async def check_entry_signal_async(self, ohlcv_1m_data: List[List]) -> Dict[str, Any]:
186
+ """
187
+ الدالة الرئيسية: التحقق من إشارة الدخول لأحدث شمعة.
188
+ Args:
189
+ ohlcv_1m_data: قائمة بالشموع (آخر 500+ شمعة 1m)
190
+ """
191
+ if not self.initialized:
192
+ return {'signal': 'WAIT', 'reason': 'Sniper Engine not initialized'}
193
+
194
+ if len(ohlcv_1m_data) < self.LOOKBACK_WINDOW:
195
+ return {'signal': 'WAIT', 'reason': f'Insufficient 1m data ({len(ohlcv_1m_data)} < {self.LOOKBACK_WINDOW})'}
196
+
197
+ try:
198
+ df = pd.DataFrame(ohlcv_1m_data, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
199
+ df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float)
200
+
201
+ df_features = self._calculate_features_live(df)
202
+
203
+ if df_features.empty:
204
+ return {'signal': 'WAIT', 'reason': 'Feature calculation failed'}
205
+
206
+ latest_features_row = df_features.iloc[-1:]
207
+
208
+ X_live = latest_features_row[self.feature_names].fillna(0)
209
+
210
+ all_probs = []
211
+ for model in self.models:
212
+ all_probs.append(model.predict(X_live))
213
+
214
+ stacked_probs = np.stack(all_probs)
215
+ mean_probs = np.mean(stacked_probs, axis=0)
216
+
217
+ avg_prob_1 = mean_probs[0][1]
218
+
219
+ if avg_prob_1 >= self.threshold:
220
+ # (طباعة مخففة، لأنها قد تتكرر كثيراً في وضع L2)
221
+ # print(f"🔥 [Sniper V1] إشارة شراء! (الثقة: {avg_prob_1*100:.2f}% > {self.threshold*100:.2f}%)")
222
+ return {
223
+ 'signal': 'BUY',
224
+ 'confidence_prob': float(avg_prob_1),
225
+ 'threshold': self.threshold
226
+ }
227
+ else:
228
+ return {
229
+ 'signal': 'WAIT',
230
+ 'reason': 'Sniper confidence below threshold',
231
+ 'confidence_prob': float(avg_prob_1),
232
+ 'threshold': self.threshold
233
+ }
234
+
235
+ except Exception as e:
236
+ print(f"❌ [SniperEngine V1] خطأ فادح في التحقق من الإشارة: {e}")
237
+ traceback.print_exc()
238
+ return {'signal': 'WAIT', 'reason': f'Exception: {e}'}