Create ranker.py
Browse files- ml_engine/ranker.py +110 -0
ml_engine/ranker.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ml_engine/ranker.py (V9.1 - Layer 1 Smart Ranker)
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
import os
|
| 5 |
+
from typing import List, Dict, Any
|
| 6 |
+
|
| 7 |
+
try:
|
| 8 |
+
import lightgbm as lgb
|
| 9 |
+
LGBM_AVAILABLE = True
|
| 10 |
+
except ImportError:
|
| 11 |
+
print("❌❌ [Ranker V9.1] مكتبة 'lightgbm' غير موجودة. الرانكر سيفشل.")
|
| 12 |
+
print(" -> قم بتثبيتها: pip install lightgbm")
|
| 13 |
+
LGBM_AVAILABLE = False
|
| 14 |
+
|
| 15 |
+
class Layer1Ranker:
|
| 16 |
+
def __init__(self, model_path: str = "ml_models/layer1_ranker.lgbm", r2_service=None):
|
| 17 |
+
"""
|
| 18 |
+
تهيئة "العقل الذكي" (نموذج V9.1) للكاشف المصغر.
|
| 19 |
+
|
| 20 |
+
Args:
|
| 21 |
+
model_path (str): المسار المحلي إلى ملف نموذج LightGBM المدرب.
|
| 22 |
+
r2_service: (اختياري) خدمة R2 لتحميل النموذج من السحابة.
|
| 23 |
+
"""
|
| 24 |
+
if not LGBM_AVAILABLE:
|
| 25 |
+
raise ImportError("مكتبة 'lightgbm' مطلوبة لتشغيل الرانكر.")
|
| 26 |
+
|
| 27 |
+
self.model_path = model_path
|
| 28 |
+
self.model_name = os.path.basename(model_path)
|
| 29 |
+
self.r2_service = r2_service # (للاستخدام المستقبلي لتحميل النموذج من R2)
|
| 30 |
+
self.model = None
|
| 31 |
+
self.features_in_ = None # (قائمة الميزات التي تدرب عليها النموذج)
|
| 32 |
+
|
| 33 |
+
async def initialize(self):
|
| 34 |
+
"""
|
| 35 |
+
تحميل النموذج المدرب وميزاته.
|
| 36 |
+
(ملاحظة: حالياً يحمل من ملف محلي. يمكن تعديله ليحمل من R2)
|
| 37 |
+
"""
|
| 38 |
+
print(f"🔄 [Ranker V9.1] بدء تحميل نموذج الكاشف المصغر: {self.model_name}...")
|
| 39 |
+
try:
|
| 40 |
+
# (الخطة أ: التحميل من R2 إذا كان متاحاً - غير مفعل حالياً)
|
| 41 |
+
# if self.r2_service:
|
| 42 |
+
# print(f" -> (محاولة التحميل من R2...)")
|
| 43 |
+
# model_bytes = await self.r2_service.load_model_from_r2(self.model_name)
|
| 44 |
+
# if model_bytes:
|
| 45 |
+
# self.model = lgb.Booster(model_str=model_bytes.decode('utf-8'))
|
| 46 |
+
# print(" -> (تم التحميل بنجاح من R2)")
|
| 47 |
+
|
| 48 |
+
# (الخطة ب: التحميل من ملف محلي إذا فشلت الخطة أ أو لم تكن مفعلة)
|
| 49 |
+
if self.model is None:
|
| 50 |
+
if not os.path.exists(self.model_path):
|
| 51 |
+
print(f"❌❌ [Ranker V9.1] خطأ فادح: ملف النموذج {self.model_path} غير موجود.")
|
| 52 |
+
print(" -> هل قمت بتنزيل النموذج 'layer1_ranker.lgbm' من Colab؟")
|
| 53 |
+
print(" -> سيتم استخدام 'نموذج وهمي' (Placeholder) مؤقت. هذا سيمنع أي تداول.")
|
| 54 |
+
self.model = None
|
| 55 |
+
# (هذه هي الميزات التي نتوقعها من 'indicators.py' V9.1)
|
| 56 |
+
self.features_in_ = [
|
| 57 |
+
'price_to_ema_50', 'price_to_ema_200', 'price_to_min_100',
|
| 58 |
+
'price_to_max_100', 'slope_14_50', 'volume_zscore_50',
|
| 59 |
+
'vwap_gap', 'rsi_14', 'rsi_mean_10', 'rsi_std_10',
|
| 60 |
+
'mfi_14', 'mfi_mean_10', 'adx_14', 'atr_percent',
|
| 61 |
+
'atr_normalized_return'
|
| 62 |
+
]
|
| 63 |
+
else:
|
| 64 |
+
# (التحميل الناجح من الملف المحلي)
|
| 65 |
+
self.model = lgb.Booster(model_file=self.model_path)
|
| 66 |
+
self.features_in_ = self.model.feature_name()
|
| 67 |
+
print(f"✅ [Ranker V9.1] تم تحميل النموذج {self.model_name} بنجاح من ملف محلي.")
|
| 68 |
+
print(f" -> عدد الميزات المطلوبة: {len(self.features_in_)}")
|
| 69 |
+
|
| 70 |
+
except Exception as e:
|
| 71 |
+
print(f"❌ [Ranker V9.1] فشل فادح في تحميل النموذج {self.model_name}: {e}")
|
| 72 |
+
self.model = None
|
| 73 |
+
|
| 74 |
+
def predict_proba(self, features_df: pd.DataFrame) -> np.ndarray:
|
| 75 |
+
"""
|
| 76 |
+
التنبؤ باحتمالية "الانطلاقة" (الدرجة 0.0 إلى 1.0) لدفعة من العملات.
|
| 77 |
+
"""
|
| 78 |
+
if self.model is None:
|
| 79 |
+
# (الوضع الآمن: إذا لم يتم تحميل النموذج، إرجاع أصفار)
|
| 80 |
+
print("⚠️ [Ranker V9.1] التنبؤ باستخدام 'الوضع الوهمي' (Placeholder). إرجاع 0.0")
|
| 81 |
+
return np.zeros(len(features_df))
|
| 82 |
+
|
| 83 |
+
try:
|
| 84 |
+
# (التأكد من أن DataFrame يحتوي على الميزات المطلوبة بنفس الترتيب)
|
| 85 |
+
# (هذا يحمي من أخطاء ترتيب الأعمدة)
|
| 86 |
+
if not all(feature in features_df.columns for feature in self.features_in_):
|
| 87 |
+
print("❌ [Ranker V9.1] خطأ: الميزات المدخلة لا تتطابق مع ميزات النموذج.")
|
| 88 |
+
missing_features = set(self.features_in_) - set(features_df.columns)
|
| 89 |
+
print(f" -> ميزات مفقودة: {missing_features}")
|
| 90 |
+
return np.zeros(len(features_df))
|
| 91 |
+
|
| 92 |
+
aligned_df = features_df[self.features_in_]
|
| 93 |
+
|
| 94 |
+
# (التنبؤ بالاحتمالية - [:, 1] هي احتمالية الفئة "1" (انطلاقة))
|
| 95 |
+
probabilities = self.model.predict(aligned_df)
|
| 96 |
+
|
| 97 |
+
# (التحقق مما إذا كان النموذج يعيد فئة واحدة أو اثنتين)
|
| 98 |
+
if probabilities.ndim == 2:
|
| 99 |
+
# (الحالة القياسية: [prob_0, prob_1])
|
| 100 |
+
return probabilities[:, 1]
|
| 101 |
+
else:
|
| 102 |
+
# (الحالة الأخرى: يعيد prob_1 فقط)
|
| 103 |
+
return probabilities
|
| 104 |
+
|
| 105 |
+
except Exception as e:
|
| 106 |
+
print(f"❌ [Ranker V9.1] خطأ أثناء التنبؤ: {e}")
|
| 107 |
+
# (إرجاع أصفار في حالة الفشل)
|
| 108 |
+
return np.zeros(len(features_df))
|
| 109 |
+
|
| 110 |
+
print("✅ ML Module: Layer 1 Ranker (V9.1) loaded.")
|