Spaces:
Paused
Paused
Create guard_engine.py
Browse files- ml_engine/guard_engine.py +132 -0
ml_engine/guard_engine.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ml_engine/guard_engine.py
|
| 2 |
+
# (V1.0 - Guard Exit Engine V2 - High Precision Protector)
|
| 3 |
+
|
| 4 |
+
import os
|
| 5 |
+
import joblib
|
| 6 |
+
import numpy as np
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import pandas_ta as ta
|
| 9 |
+
import xgboost as xgb
|
| 10 |
+
import logging
|
| 11 |
+
import traceback
|
| 12 |
+
|
| 13 |
+
# إعداد تسجيل بسيط للأخطاء
|
| 14 |
+
logger = logging.getLogger("GuardEngine")
|
| 15 |
+
|
| 16 |
+
class GuardEngine:
|
| 17 |
+
def __init__(self, models_dir="ml_models/guard_v2"):
|
| 18 |
+
self.models_dir = models_dir
|
| 19 |
+
self.exit_model = None
|
| 20 |
+
self.exit_features = None
|
| 21 |
+
self.initialized = False
|
| 22 |
+
|
| 23 |
+
# إعدادات الحارس (V2 Settings)
|
| 24 |
+
self.EXIT_THRESHOLD = 0.80 # العتبة الذهبية التي اكتشفناها في الاختبار
|
| 25 |
+
|
| 26 |
+
async def initialize(self):
|
| 27 |
+
"""تحميل نموذج الحماية وقائمة ميزاته"""
|
| 28 |
+
if self.initialized: return
|
| 29 |
+
|
| 30 |
+
try:
|
| 31 |
+
print(f"🛡️ [GuardEngine] Loading V2 models from {self.models_dir}...")
|
| 32 |
+
|
| 33 |
+
# مسارات الملفات (تأكد من نقلها من درايف إلى هذا المجلد في الخادم)
|
| 34 |
+
model_path = os.path.join(self.models_dir, "Guard_Exit_V2.json")
|
| 35 |
+
feat_path = os.path.join(self.models_dir, "Guard_Exit_V2_features.pkl")
|
| 36 |
+
|
| 37 |
+
if os.path.exists(model_path) and os.path.exists(feat_path):
|
| 38 |
+
self.exit_model = xgb.Booster()
|
| 39 |
+
self.exit_model.load_model(model_path)
|
| 40 |
+
self.exit_features = joblib.load(feat_path)
|
| 41 |
+
self.initialized = True
|
| 42 |
+
print(f"✅ [GuardEngine] Exit Guard V2 Loaded! (Threshold: {self.EXIT_THRESHOLD})")
|
| 43 |
+
else:
|
| 44 |
+
print(f"❌ [GuardEngine] CRITICAL: Model files missing in {self.models_dir}")
|
| 45 |
+
|
| 46 |
+
except Exception as e:
|
| 47 |
+
print(f"❌ [GuardEngine] Initialization failed: {e}")
|
| 48 |
+
traceback.print_exc()
|
| 49 |
+
|
| 50 |
+
def _engineer_features(self, df_raw):
|
| 51 |
+
"""
|
| 52 |
+
مصنع الميزات الحي (يجب أن يطابق كولاب 100%)
|
| 53 |
+
"""
|
| 54 |
+
df = df_raw.copy()
|
| 55 |
+
|
| 56 |
+
# 1. ميزات تيتان (Titan Features)
|
| 57 |
+
df['RSI_14'] = ta.rsi(df['close'], length=14)
|
| 58 |
+
df['MFI_14'] = ta.mfi(df['high'], df['low'], df['close'], df['volume'], length=14)
|
| 59 |
+
macd = ta.macd(df['close'])
|
| 60 |
+
if macd is not None:
|
| 61 |
+
df['MACD'] = macd.iloc[:, 0]
|
| 62 |
+
df['MACD_Hist'] = macd.iloc[:, 1]
|
| 63 |
+
|
| 64 |
+
df['ADX_14'] = ta.adx(df['high'], df['low'], df['close'], length=14).iloc[:, 0]
|
| 65 |
+
|
| 66 |
+
for p in [9, 21, 50, 200]:
|
| 67 |
+
ema = ta.ema(df['close'], length=p)
|
| 68 |
+
df[f'EMA_{p}'] = ema
|
| 69 |
+
df[f'Dist_EMA_{p}'] = (df['close'] / ema) - 1
|
| 70 |
+
|
| 71 |
+
df['ATR_14'] = ta.atr(df['high'], df['low'], df['close'], length=14)
|
| 72 |
+
df['ATR_Pct'] = df['ATR_14'] / df['close']
|
| 73 |
+
|
| 74 |
+
bb = ta.bbands(df['close'], length=20, std=2)
|
| 75 |
+
if bb is not None:
|
| 76 |
+
df['BB_Width'] = (bb.iloc[:, 2] - bb.iloc[:, 0]) / bb.iloc[:, 1]
|
| 77 |
+
df['BB_Pos'] = (df['close'] - bb.iloc[:, 0]) / (bb.iloc[:, 2] - bb.iloc[:, 0])
|
| 78 |
+
|
| 79 |
+
# 2. ميزات الأنماط (Pattern Features) - Lookback=12
|
| 80 |
+
for i in range(1, 13):
|
| 81 |
+
df[f'Close_R_{i}'] = df['close'].shift(i) / df['close']
|
| 82 |
+
df[f'Vol_R_{i}'] = df['volume'].shift(i) / (df['volume'] + 1e-9)
|
| 83 |
+
|
| 84 |
+
df['Body_Size'] = abs(df['close'] - df['open']) / df['open']
|
| 85 |
+
df['Upper_Wick'] = (df['high'] - np.maximum(df['close'], df['open'])) / df['open']
|
| 86 |
+
df['Lower_Wick'] = (np.minimum(df['close'], df['open']) - df['low']) / df['open']
|
| 87 |
+
|
| 88 |
+
return df
|
| 89 |
+
|
| 90 |
+
def check_exit_signal(self, symbol, ohlcv_5m):
|
| 91 |
+
"""
|
| 92 |
+
الفحص الحي للخروج: يستقبل شموع 5m ويعيد قرار الحماية
|
| 93 |
+
"""
|
| 94 |
+
if not self.initialized or not self.exit_model:
|
| 95 |
+
return {'action': 'HOLD', 'confidence': 0.0, 'reason': 'Guard not ready'}
|
| 96 |
+
|
| 97 |
+
try:
|
| 98 |
+
if len(ohlcv_5m) < 250: # نحتاج بيانات كافية للمؤشرات (200 EMA + هامش)
|
| 99 |
+
return {'action': 'HOLD', 'confidence': 0.0, 'reason': 'Not enough 5m data'}
|
| 100 |
+
|
| 101 |
+
# 1. تجهيز البيانات
|
| 102 |
+
df = pd.DataFrame(ohlcv_5m, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
| 103 |
+
df = self._engineer_features(df)
|
| 104 |
+
|
| 105 |
+
# نأخذ آخر صف فقط (الوضع الحالي)
|
| 106 |
+
latest_row = df.iloc[-1:].copy()
|
| 107 |
+
|
| 108 |
+
# 2. ترتيب الميزات بالضبط كما في التدريب
|
| 109 |
+
# (أي ميزة ناقصة سنملأها بـ NaN، وسيتعامل معها XGBoost)
|
| 110 |
+
dmatrix = xgb.DMatrix(latest_row[self.exit_features])
|
| 111 |
+
|
| 112 |
+
# 3. التنبؤ
|
| 113 |
+
prob = self.exit_model.predict(dmatrix)[0]
|
| 114 |
+
|
| 115 |
+
# 4. اتخاذ القرار بناءً على العتبة الذهبي�� (0.80)
|
| 116 |
+
if prob >= self.EXIT_THRESHOLD:
|
| 117 |
+
return {
|
| 118 |
+
'action': 'EXIT_NOW',
|
| 119 |
+
'confidence': float(prob),
|
| 120 |
+
'reason': f'Guard V2 Exit Signal (Conf: {prob:.2f} >= {self.EXIT_THRESHOLD})'
|
| 121 |
+
}
|
| 122 |
+
else:
|
| 123 |
+
return {
|
| 124 |
+
'action': 'HOLD',
|
| 125 |
+
'confidence': float(prob),
|
| 126 |
+
'reason': f'Guard V2 Secure (Conf: {prob:.2f})'
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
except Exception as e:
|
| 130 |
+
# logger.error(f"Guard check failed for {symbol}: {e}")
|
| 131 |
+
# traceback.print_exc()
|
| 132 |
+
return {'action': 'ERROR', 'confidence': 0.0, 'reason': str(e)}
|