Riy777 commited on
Commit
6fd65f8
·
1 Parent(s): 181ac85

Create oracle_engine.py

Browse files
Files changed (1) hide show
  1. ml_engine/oracle_engine.py +283 -0
ml_engine/oracle_engine.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import numpy as np
3
+ import pandas as pd
4
+ import pandas_ta as ta
5
+ import lightgbm as lgb
6
+ from scipy.signal import find_peaks
7
+ import warnings
8
+ from typing import Dict, Any, List, Optional
9
+
10
+ # --- [ 0. إعدادات ] ---
11
+ warnings.filterwarnings('ignore', category=FutureWarning)
12
+
13
+ PIPELINE_SETTINGS = {
14
+ 'SWING_PROMINENCE_PCT': 0.02, # 2%
15
+ }
16
+
17
+ # (بناءً على تقريرك [cite: 5-8]، الدقة فوق 75% ممتازة)
18
+ DECISION_CONFIDENCE_THRESHOLD = 0.75
19
+ N_STRATEGY_MODELS = 11
20
+
21
+ STRATEGY_MAP = {
22
+ 0: 'WAIT',
23
+ 1: 'SWING_LONG',
24
+ 2: 'SCALP_LONG',
25
+ 3: 'SWING_SHORT',
26
+ 4: 'SCALP_SHORT'
27
+ }
28
+
29
+ # (الأطر الزمنية التي تم تدريب النماذج عليها)
30
+ TIMEBYTES_TO_PROCESS = ['15m', '1h', '4h']
31
+
32
+
33
+ class OracleEngine:
34
+ def __init__(self, model_dir: str = "ml_models/Unified_Models_V1"):
35
+ self.model_dir = model_dir
36
+ self.strategy_boosters: List[lgb.Booster] = []
37
+ self.quantile_boosters: Dict[str, lgb.Booster] = {}
38
+ self.feature_names: List[str] = []
39
+ self.initialized = False
40
+ print("🧠 [OracleEngine V2] تم الإنشاء (Multi-Timeframe). جاهز للتهيئة.")
41
+
42
+ async def initialize(self):
43
+ """
44
+ تحميل جميع النماذج الـ 15 (11 استراتيجية + 4 أهداف) إلى الذاكرة.
45
+ """
46
+ if self.initialized:
47
+ return True
48
+
49
+ print(f"🧠 [OracleEngine V2] جاري تحميل 15 نموذجاً من {self.model_dir}...")
50
+ try:
51
+ # 1. تحميل نماذج "لجنة القرار" (Strategy Ensemble)
52
+ for i in range(N_STRATEGY_MODELS):
53
+ model_file = os.path.join(self.model_dir, f"lgbm_strategy_fold_{i}.txt")
54
+ if not os.path.exists(model_file):
55
+ print(f"❌ [Oracle Error] ملف نموذج مفقود: {model_file}")
56
+ return False
57
+ booster = lgb.Booster(model_file=model_file)
58
+ self.strategy_boosters.append(booster)
59
+
60
+ print(f" ✅ تم تحميل {len(self.strategy_boosters)} نماذج استراتيجية.")
61
+
62
+ # 2. تحميل نماذج "لجنة الأهداف" (Quantile Models)
63
+ quantile_names = ['tp_p20', 'tp_p50', 'tp_p80', 'sl_p80']
64
+ for name in quantile_names:
65
+ model_file = os.path.join(self.model_dir, f"lgbm_{name}.txt")
66
+ if not os.path.exists(model_file):
67
+ print(f"❌ [Oracle Error] ملف نموذج مفقود: {model_file}")
68
+ return False
69
+ booster = lgb.Booster(model_file=model_file)
70
+ self.quantile_boosters[name] = booster
71
+
72
+ print(f" ✅ تم تحميل {len(self.quantile_boosters)} نماذج أهداف.")
73
+
74
+ # 3. حفظ قائمة الميزات
75
+ self.feature_names = self.strategy_boosters[0].feature_name()
76
+ self.initialized = True
77
+
78
+ print(f"✅ [OracleEngine V2] جاهز. (Threshold: {DECISION_CONFIDENCE_THRESHOLD*100}%)")
79
+ print(f" -> سيعمل على الأطر: {TIMEBYTES_TO_PROCESS}")
80
+ return True
81
+
82
+ except Exception as e:
83
+ print(f"❌ [OracleEngine V2] فشل فادح أثناء التهيئة: {e}")
84
+ self.initialized = False
85
+ return False
86
+
87
+ # --- [ دوال هندسة الميزات (مطابقة 100% للتدريب) ] ---
88
+
89
+ def _calculate_base_ta(self, df: pd.DataFrame) -> pd.DataFrame:
90
+ df.ta.rsi(length=14, append=True)
91
+ df.ta.adx(length=14, append=True)
92
+ df.ta.macd(fast=12, slow=26, signal=9, append=True)
93
+ df.ta.bbands(length=20, std=2, append=True)
94
+ df.ta.atr(length=14, append=True)
95
+ for length in [9, 21, 50, 100, 200]:
96
+ df[f'EMA_{length}'] = ta.ema(df['close'], length=length)
97
+ return df
98
+
99
+ def _calculate_market_structure(self, df: pd.DataFrame, prominence_pct: float) -> pd.DataFrame:
100
+ prominence_value = df['close'].mean() * prominence_pct
101
+ high_peaks_idx, _ = find_peaks(df['high'], prominence=prominence_value)
102
+ low_peaks_idx, _ = find_peaks(-df['low'], prominence=prominence_value)
103
+ df['last_SH_price'] = df.iloc[high_peaks_idx]['high'].reindex(df.index).ffill().bfill()
104
+ df['last_SL_price'] = df.iloc[low_peaks_idx]['low'].reindex(df.index).ffill().bfill()
105
+ df['BOS_Long'] = np.where(df['close'] > df['last_SH_price'].shift(1), 1, 0)
106
+ df['BOS_Short'] = np.where(df['low'] < df['last_SL_price'].shift(1), 1, 0)
107
+ return df
108
+
109
+ def _calculate_fibonacci_matrix(self, df: pd.DataFrame) -> pd.DataFrame:
110
+ wave_range = df['last_SH_price'] - df['last_SL_price']
111
+ df['fibo_0.382'] = df['last_SH_price'] - (wave_range * 0.382)
112
+ df['fibo_0.500'] = df['last_SH_price'] - (wave_range * 0.500)
113
+ df['fibo_0.618'] = df['last_SL_price'] + (wave_range * 0.618)
114
+ df['fibo_ext_1.618'] = df['last_SH_price'] + (wave_range * 1.618)
115
+ df['dist_to_0.618_pct'] = (df['close'] - df['fibo_0.618']) / (df['close'] + 1e-9)
116
+ df['dist_to_1.618_pct'] = (df['fibo_ext_1.618'] - df['close']) / (df['close'] + 1e-9)
117
+ df['is_in_golden_pocket'] = np.where(
118
+ (df['close'] < df['fibo_0.500']) & (df['close'] > df['fibo_0.618']), 1, 0
119
+ )
120
+ df.replace([np.inf, -np.inf], np.nan, inplace=True)
121
+ return df
122
+
123
+ def _calculate_alpha_strategies(self, df: pd.DataFrame) -> pd.DataFrame:
124
+ df['volume_zscore'] = (df['volume'] - df['volume'].rolling(50).mean()) / (df['volume'].rolling(50).std() + 1e-9)
125
+ df['dist_from_EMA200_pct'] = (df['close'] - df['EMA_200']) / (df['EMA_200'] + 1e-9)
126
+
127
+ # (إصلاح Bollinger Bands كما في كود التجميع)
128
+ bbu_col = next((col for col in df.columns if 'BBU_20_2.0' in str(col)), None)
129
+ bbl_col = next((col for col in df.columns if 'BBL_20_2.0' in str(col)), None)
130
+ bbm_col = next((col for col in df.columns if 'BBM_20_2.0' in str(col)), None)
131
+
132
+ if all([bbu_col, bbl_col, bbm_col]):
133
+ df['BBW_pct'] = (df[bbu_col] - df[bbl_col]) / (df[bbm_col] + 1e-9)
134
+ df['is_squeeze'] = np.where(df['BBW_pct'] < df['BBW_pct'].rolling(100).min(), 1, 0)
135
+ else:
136
+ df['BBW_pct'] = np.nan
137
+ df['is_squeeze'] = 0
138
+
139
+ df['is_trending'] = np.where(df['ADX_14'] > 25, 1, 0)
140
+ df['ATR_pct'] = (df['ATRr_14'] / df['close']) * 100
141
+ return df
142
+
143
+ def _create_feature_vector(self, ohlcv_tf_data: List) -> Optional[pd.DataFrame]:
144
+ """
145
+ تشغيل خط أنابيب الميزات الكامل على بيانات إطار زمني واحد.
146
+ """
147
+ if ohlcv_tf_data is None or len(ohlcv_tf_data) < 200:
148
+ return None
149
+
150
+ df = pd.DataFrame(ohlcv_tf_data, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
151
+ df = df.astype(float)
152
+ df['datetime'] = pd.to_datetime(df['timestamp'], unit='ms')
153
+ df = df.set_index('datetime')
154
+
155
+ # تشغيل خط الأنابيب
156
+ df = self._calculate_base_ta(df)
157
+ df = self._calculate_market_structure(df, PIPELINE_SETTINGS['SWING_PROMINENCE_PCT'])
158
+ df = self._calculate_fibonacci_matrix(df)
159
+ df = self._calculate_alpha_strategies(df)
160
+
161
+ # ملء أي قيم NaN متبقية (مهم جداً للتنبؤ)
162
+ df = df.ffill().bfill()
163
+
164
+ # أخذ آخر صف فقط
165
+ latest_features = df.iloc[-1:]
166
+
167
+ # التأكد من وجود جميع الميزات بالترتيب الصحيح
168
+ try:
169
+ feature_vector = latest_features[self.feature_names]
170
+ # التأكد من عدم وجود NaN نهائياً
171
+ if feature_vector.isnull().values.any():
172
+ print("⚠️ [Oracle Warning] Feature vector contains NaN after fill.")
173
+ return None
174
+ return feature_vector
175
+ except Exception as e:
176
+ print(f"❌ [Oracle Error] عدم تطابق الميزات: {e}")
177
+ return None
178
+
179
+
180
+ async def predict(self, symbol_data: Dict[str, Any]) -> Dict[str, Any]:
181
+ """
182
+ الدالة الرئيسية: تحليل إشارة مرشحة وإرجاع قرار كامل.
183
+ (هذه الدالة تحل محل llm_service.get_trading_decision)
184
+ """
185
+ if not self.initialized:
186
+ return {'action': 'WAIT', 'reason': 'Oracle Engine not initialized'}
187
+
188
+ ohlcv_data = symbol_data.get('ohlcv')
189
+ current_price = symbol_data.get('current_price')
190
+ if not ohlcv_data or not current_price:
191
+ return {'action': 'WAIT', 'reason': 'Missing OHLCV or price data'}
192
+
193
+ try:
194
+ all_tf_decisions = []
195
+
196
+ # --- [ الخطوة 1: تحليل كل إطار زمني على حدة ] ---
197
+ for tf in TIMEBYTES_TO_PROCESS:
198
+ feature_vector = self._create_feature_vector(ohlcv_data.get(tf))
199
+
200
+ if feature_vector is None:
201
+ print(f" -> {symbol_data['symbol']} @ {tf}: Skipped (Insufficient data)")
202
+ continue
203
+
204
+ # 1. تشغيل "لجنة القرار" (Strategy Ensemble)
205
+ all_probs = [
206
+ booster.predict(feature_vector, num_iteration=booster.best_iteration)
207
+ for booster in self.strategy_boosters
208
+ ]
209
+ ensemble_probs = np.mean(all_probs, axis=0)[0] # (نأخذ التنبؤ الأول)
210
+
211
+ # 2. تحليل القرار والثقة
212
+ predicted_strategy_idx = np.argmax(ensemble_probs)
213
+ confidence = ensemble_probs[predicted_strategy_idx]
214
+ strategy_name = STRATEGY_MAP.get(predicted_strategy_idx, 'WAIT')
215
+
216
+ all_tf_decisions.append({
217
+ 'timeframe': tf,
218
+ 'strategy': strategy_name,
219
+ 'confidence': float(confidence),
220
+ 'feature_vector': feature_vector # (نحتفظ به لاستخدامه لاحقاً)
221
+ })
222
+
223
+ if not all_tf_decisions:
224
+ return {'action': 'IGNORE', 'reason': 'Feature calculation failed for all TFs'}
225
+
226
+ # --- [ الخطوة 2: اختيار القرار الأفضل (أعلى ثقة) ] ---
227
+ best_decision = max(all_tf_decisions, key=lambda x: x['confidence'])
228
+
229
+ strategy_name = best_decision['strategy']
230
+ confidence = best_decision['confidence']
231
+ best_tf = best_decision['timeframe']
232
+
233
+ # --- [ الخطوة 3: تطبيق فلتر الثقة (الأهم) ] ---
234
+ if confidence < DECISION_CONFIDENCE_THRESHOLD or strategy_name == 'WAIT':
235
+ return {
236
+ 'action': 'IGNORE',
237
+ 'reason': f"Best signal ({strategy_name} @ {best_tf}) confidence ({confidence:.2f}) is below threshold ({DECISION_CONFIDENCE_THRESHOLD})",
238
+ 'confidence': confidence,
239
+ 'strategy': strategy_name
240
+ }
241
+
242
+ # --- [ الخطوة 4: (نجحت الثقة) - تشغيل "لجنة الأهداف" ] ---
243
+ winning_feature_vector = best_decision['feature_vector']
244
+ preds_quantile = {}
245
+ for name, booster in self.quantile_boosters.items():
246
+ preds_quantile[name] = booster.predict(winning_feature_vector, num_iteration=booster.best_iteration)[0]
247
+
248
+ # --- [ الخطوة 5: تحديد الأهداف النهائية ] ---
249
+ tp_pct = preds_quantile['tp_p50'] # (الهدف الواقعي) [cite: 79]
250
+ sl_pct = preds_quantile['sl_p80'] # (وقف الخسارة الآمن) [cite: 80]
251
+
252
+ if tp_pct <= 0 or sl_pct <= 0:
253
+ return {'action': 'IGNORE', 'reason': f'Quantile model predicted negative TP/SL ({tp_pct=}, {sl_pct=})'}
254
+
255
+ if "LONG" in strategy_name:
256
+ tp_price = current_price * (1 + tp_pct)
257
+ sl_price = current_price * (1 - sl_pct)
258
+ action_type = "BUY"
259
+ elif "SHORT" in strategy_name:
260
+ tp_price = current_price * (1 - tp_pct)
261
+ sl_price = current_price * (1 + sl_pct)
262
+ action_type = "SELL" # (إذا كان النظام يدعم البيع)
263
+ else:
264
+ return {'action': 'IGNORE', 'reason': 'Strategy not actionable'}
265
+
266
+ # --- [ الخطوة 6: إرجاع القرار الكامل ] ---
267
+ return {
268
+ 'action': 'WATCH', # (للتوافق مع `app.py` القديم)
269
+ 'confidence': confidence,
270
+ 'analysis_summary': f"Oracle Consensus @ {best_tf}: {strategy_name} (Conf: {confidence:.2%})",
271
+ 'strategy': strategy_name,
272
+ 'action_type': action_type, # (إضافة: BUY أو SELL)
273
+ 'tp_price': float(tp_price),
274
+ 'sl_price': float(sl_price),
275
+ 'quantile_tp_pct': float(tp_pct),
276
+ 'quantile_sl_pct': float(sl_pct)
277
+ }
278
+
279
+ except Exception as e:
280
+ print(f"❌ [OracleEngine V2] فشل فادح أثناء التنبؤ: {e}")
281
+ import traceback
282
+ traceback.print_exc()
283
+ return {'action': 'WAIT', 'reason': f'Exception: {e}'}