Spaces:
Paused
Paused
| # ml_engine/deep_steward.py | |
| # (V1.0 - Deep Steward Production Inference Engine) | |
| import os | |
| import numpy as np | |
| import pandas as pd | |
| import xgboost as xgb | |
| import logging | |
| import traceback | |
| import pandas_ta as ta | |
| class DeepSteward: | |
| def __init__(self, model_path="ml_models/DeepSteward_Production_V1.json"): | |
| self.model_path = model_path | |
| self.model = None | |
| self.initialized = False | |
| # ⚙️ إعدادات الحساسية (قابل للتعديل من الخارج) | |
| # أي إشارة خروج بثقة أقل من هذه سيتم تجاهلها | |
| self.EXIT_THRESHOLD = 0.50 | |
| # أسماء الميزات بالترتيب (مهم جداً للمطابقة مع التدريب) | |
| self.FEATURE_NAMES = [ | |
| 'log_ret', 'rel_vol', 'rsi_norm', 'macd_hist', 'roc', | |
| 'bb_width', 'bb_pct', 'atr_pct', 'dist_ema50', 'dist_ema200', | |
| 'pnl_channel' # القناة 11 | |
| ] | |
| def initialize(self): | |
| """تحميل النموذج للذاكرة""" | |
| try: | |
| if not os.path.exists(self.model_path): | |
| print(f"❌ [DeepSteward] Model file not found: {self.model_path}") | |
| return False | |
| self.model = xgb.Booster() | |
| self.model.load_model(self.model_path) | |
| self.initialized = True | |
| print(f"✅ [DeepSteward] V1 Loaded successfully. (Threshold: {self.EXIT_THRESHOLD})") | |
| return True | |
| except Exception as e: | |
| print(f"❌ [DeepSteward] Init Failed: {e}") | |
| return False | |
| def _engineer_features(self, df: pd.DataFrame, current_price: float, entry_price: float) -> np.ndarray: | |
| """ | |
| تحويل الشموع الخام إلى ميزات Titan (مطابقة 100% لكود التدريب) | |
| """ | |
| try: | |
| # نسخة لتجنب التعديل على الأصل | |
| df = df.copy() | |
| # التأكد من وجود بيانات كافية | |
| if len(df) < 200: return None | |
| # 1. Log Returns | |
| df['log_ret'] = np.log(df['close'] / df['close'].shift(1)) | |
| # 2. Relative Volume | |
| vol_sma = df['volume'].rolling(window=50).mean() | |
| df['rel_vol'] = (df['volume'] - vol_sma) / (vol_sma + 1e-9) | |
| # 3. RSI (14) - تطبيع | |
| df['rsi'] = ta.rsi(df['close'], length=14) | |
| df['rsi_norm'] = df['rsi'] / 100.0 | |
| # 4. MACD (12, 26, 9) | |
| macd = ta.macd(df['close'], fast=12, slow=26, signal=9) | |
| # pandas_ta returns columns like MACD_12_26_9, MACDh_12_26_9, MACDs_12_26_9 | |
| # We need the Histogram (MACDh) | |
| hist_col = [c for c in macd.columns if 'MACDh' in c][0] | |
| df['macd_hist'] = macd[hist_col] | |
| # 5. ROC (9) | |
| df['roc'] = ta.roc(df['close'], length=9) / 100.0 # pandas_ta returns percentage | |
| # 6. Bollinger Bands (20, 2) | |
| bb = ta.bbands(df['close'], length=20, std=2) | |
| # Columns: BBL, BBM, BBU, BBB (Bandwidth), BBP (Percent) | |
| # We need Width and Percent | |
| w_col = [c for c in bb.columns if 'BBB' in c][0] | |
| p_col = [c for c in bb.columns if 'BBP' in c][0] | |
| # تطبيع العرض (pandas_ta يعطيه كنسبة مئوية جاهزة أحياناً، لكن لنتأكد) | |
| df['bb_width'] = bb[w_col] / 100.0 # إذا كان كبيراً، نقسمه | |
| df['bb_pct'] = bb[p_col] | |
| # 7. ATR (14) | |
| df['atr'] = ta.atr(df['high'], df['low'], df['close'], length=14) | |
| df['atr_pct'] = df['atr'] / df['close'] | |
| # 8. EMA Distances | |
| df['ema50'] = ta.ema(df['close'], length=50) | |
| df['ema200'] = ta.ema(df['close'], length=200) | |
| df['dist_ema50'] = (df['close'] - df['ema50']) / (df['ema50'] + 1e-9) | |
| df['dist_ema200'] = (df['close'] - df['ema200']) / (df['ema200'] + 1e-9) | |
| # 9. PnL Channel (الحالة الحالية) | |
| # نحسب الربح/الخسارة اللحظية | |
| current_pnl = (current_price - entry_price) / entry_price | |
| df['pnl_channel'] = current_pnl | |
| # تنظيف القيم المفقودة | |
| df = df.fillna(0) | |
| # استخراج آخر صف (الوضع الحالي) | |
| latest_row = df.iloc[-1][self.FEATURE_NAMES] | |
| # تحويل لمصفوفة ثنائية الأبعاد (1, 11) | |
| # بما أننا دربنا XGBoost على بيانات مسطحة (Flattened) من 64 شمعة | |
| # هنا نقطة مهمة: هل النموذج الحالي هو XGBoost الذي تدرب على 64 شمعة مسطحة؟ | |
| # أم هو نسخة مبسطة؟ | |
| # ⚠️ تصحيح هندسي هام: | |
| # نموذج XGBoost تدرب على مصفوفة مسطحة لـ 64 شمعة (Flattened Sequence) | |
| # أي (64 * 11 = 704 ميزة). | |
| # لذلك يجب أن نأخذ آخر 64 شمعة ونسطحها، وليس شمعة واحدة فقط! | |
| window = df.iloc[-64:][self.FEATURE_NAMES].values # (64, 11) | |
| if window.shape[0] < 64: | |
| return None # بيانات غير كافية | |
| # التسطيح (1, 704) | |
| feature_vector = window.reshape(1, -1) | |
| return feature_vector | |
| except Exception as e: | |
| # print(f"⚠️ [DeepSteward] Feature Eng Error: {e}") | |
| return None | |
| def analyze_position(self, ohlcv_5m: list, entry_price: float) -> dict: | |
| """ | |
| تحليل وضع الصفقة الحالي واتخاذ قرار | |
| """ | |
| if not self.initialized or not self.model: | |
| return {'action': 'HOLD', 'confidence': 0.0, 'reason': 'Not Initialized'} | |
| try: | |
| # تحويل القائمة لـ DataFrame | |
| df = pd.DataFrame(ohlcv_5m, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) | |
| current_price = float(df['close'].iloc[-1]) | |
| # هندسة الميزات | |
| features = self._engineer_features(df, current_price, entry_price) | |
| if features is None: | |
| return {'action': 'HOLD', 'confidence': 0.0, 'reason': 'Insufficient Data'} | |
| # التنبؤ | |
| dmatrix = xgb.DMatrix(features) | |
| prob = self.model.predict(dmatrix)[0] # يعيد قيمة بين 0 و 1 | |
| # منطق القرار | |
| # النموذج تدرب على: 1 = EXIT (خطر/هروب)، 0 = HOLD (أمان/صعود) | |
| if prob > self.EXIT_THRESHOLD: | |
| return { | |
| 'action': 'EXIT', | |
| 'confidence': float(prob), | |
| 'reason': f'DeepSteward Danger Signal ({prob:.1%})' | |
| } | |
| else: | |
| return { | |
| 'action': 'HOLD', | |
| 'confidence': float(1 - prob), # ثقة البقاء | |
| 'reason': f'DeepSteward Safe ({prob:.1%})' | |
| } | |
| except Exception as e: | |
| print(f"❌ [DeepSteward] Inference Error: {e}") | |
| return {'action': 'HOLD', 'confidence': 0.0, 'reason': f'Error: {e}'} |