Spaces:
Paused
Paused
Update ml_engine/data_manager.py
Browse files- ml_engine/data_manager.py +93 -71
ml_engine/data_manager.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# ============================================================
|
| 2 |
# 📂 ml_engine/data_manager.py
|
| 3 |
-
# (
|
| 4 |
# ============================================================
|
| 5 |
|
| 6 |
import asyncio
|
|
@@ -44,7 +44,7 @@ class DataManager:
|
|
| 44 |
'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L'
|
| 45 |
]
|
| 46 |
|
| 47 |
-
print(f"📦 [DataManager
|
| 48 |
|
| 49 |
async def initialize(self):
|
| 50 |
print(" > [DataManager] Starting initialization...")
|
|
@@ -73,13 +73,13 @@ class DataManager:
|
|
| 73 |
return self.contracts_db
|
| 74 |
|
| 75 |
# ==================================================================
|
| 76 |
-
# 🧠 Layer 1:
|
| 77 |
# ==================================================================
|
| 78 |
async def layer1_rapid_screening(self, adaptive_hub_ref=None) -> List[Dict[str, Any]]:
|
| 79 |
self.adaptive_hub_ref = adaptive_hub_ref
|
| 80 |
-
print(f"🔍 [Layer 1] Screening
|
| 81 |
|
| 82 |
-
# 1. فلتر السيولة
|
| 83 |
initial_candidates = await self._stage0_universe_filter()
|
| 84 |
if not initial_candidates:
|
| 85 |
print("⚠️ [Layer 1] Stage 0 returned 0 candidates.")
|
|
@@ -92,41 +92,99 @@ class DataManager:
|
|
| 92 |
final_list = []
|
| 93 |
|
| 94 |
for item in enriched_data:
|
| 95 |
-
# 3. التصنيف ال
|
| 96 |
-
classification = self.
|
| 97 |
|
| 98 |
if classification['type'] != 'NONE':
|
| 99 |
-
#
|
| 100 |
regime_info = self._diagnose_asset_regime(item)
|
| 101 |
current_regime = regime_info['regime']
|
| 102 |
|
| 103 |
-
# 🔥
|
| 104 |
-
# إذا السوق عرضي (RANGE) أو ميت (DEAD)، نمنع
|
| 105 |
-
# ل
|
| 106 |
-
if current_regime in ['RANGE', 'DEAD'] and classification['type'] == '
|
| 107 |
-
|
| 108 |
-
continue
|
| 109 |
|
| 110 |
-
# إ
|
| 111 |
item['asset_regime'] = current_regime
|
| 112 |
item['asset_regime_conf'] = regime_info['conf']
|
| 113 |
|
| 114 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
if self.adaptive_hub_ref:
|
| 116 |
dynamic_config = self.adaptive_hub_ref.get_regime_config(current_regime)
|
| 117 |
item['dynamic_limits'] = dynamic_config
|
| 118 |
|
| 119 |
-
|
| 120 |
item['strategy_tag'] = classification['type']
|
|
|
|
| 121 |
final_list.append(item)
|
| 122 |
|
| 123 |
-
#
|
| 124 |
final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
|
| 125 |
|
| 126 |
selection = final_list[:50]
|
| 127 |
-
print(f"✅ [Layer 1] Passed {len(selection)} candidates (
|
| 128 |
return selection
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
# ==================================================================
|
| 131 |
# 🔍 Stage 0: Universe Filter (Robust USD Calc)
|
| 132 |
# ==================================================================
|
|
@@ -140,7 +198,6 @@ class DataManager:
|
|
| 140 |
SOVEREIGN_COINS = ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT']
|
| 141 |
|
| 142 |
reject_stats = {"volume": 0, "change": 0, "blacklist": 0}
|
| 143 |
-
debug_printed = False
|
| 144 |
|
| 145 |
for symbol, ticker in tickers.items():
|
| 146 |
if not symbol.endswith('/USDT'): continue
|
|
@@ -157,22 +214,18 @@ class DataManager:
|
|
| 157 |
|
| 158 |
is_sovereign = symbol in SOVEREIGN_COINS
|
| 159 |
|
| 160 |
-
# طباعة فحص BTC مرة واحدة
|
| 161 |
-
if "BTC/USDT" in symbol and not debug_printed:
|
| 162 |
-
print(f" 🐛 [DEBUG] BTC Vol: ${calc_quote_vol:,.0f}")
|
| 163 |
-
debug_printed = True
|
| 164 |
-
|
| 165 |
# فلتر السيولة (500k) - نتجاوزه للعملات السيادية
|
| 166 |
if not is_sovereign:
|
| 167 |
if calc_quote_vol < 500000:
|
| 168 |
reject_stats["volume"] += 1
|
| 169 |
continue
|
| 170 |
|
| 171 |
-
# فلتر التغير (20%)
|
|
|
|
| 172 |
change_pct = ticker.get('percentage')
|
| 173 |
if change_pct is None: change_pct = 0.0
|
| 174 |
|
| 175 |
-
if abs(change_pct) >
|
| 176 |
reject_stats["change"] += 1
|
| 177 |
continue
|
| 178 |
|
|
@@ -183,9 +236,6 @@ class DataManager:
|
|
| 183 |
'change_24h': change_pct
|
| 184 |
})
|
| 185 |
|
| 186 |
-
print(f" 📊 [Filter Stats] Total: {len(tickers)} | Passed: {len(candidates)}")
|
| 187 |
-
print(f" ❌ Rejected: Vol < 500k ({reject_stats['volume']}) | Change > 20% ({reject_stats['change']})")
|
| 188 |
-
|
| 189 |
candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
|
| 190 |
return candidates
|
| 191 |
|
|
@@ -225,46 +275,7 @@ class DataManager:
|
|
| 225 |
except Exception: return {'regime': 'RANGE', 'conf': 0.0}
|
| 226 |
|
| 227 |
# ------------------------------------------------------------------
|
| 228 |
-
#
|
| 229 |
-
# ------------------------------------------------------------------
|
| 230 |
-
def _apply_strict_logic_tree(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
| 231 |
-
try:
|
| 232 |
-
df_1h = self._calc_indicators(data['ohlcv_1h_raw'])
|
| 233 |
-
df_15m = self._calc_indicators(data['ohlcv_15m_raw'])
|
| 234 |
-
data['df_1h'] = df_1h
|
| 235 |
-
except: return {'type': 'NONE', 'score': 0}
|
| 236 |
-
|
| 237 |
-
curr_1h = df_1h.iloc[-1]
|
| 238 |
-
curr_15m = df_15m.iloc[-1]
|
| 239 |
-
|
| 240 |
-
try:
|
| 241 |
-
close_4h_ago = df_1h.iloc[-5]['close']
|
| 242 |
-
change_4h = ((curr_1h['close'] - close_4h_ago) / close_4h_ago) * 100
|
| 243 |
-
except: change_4h = 0.0
|
| 244 |
-
|
| 245 |
-
# Gates
|
| 246 |
-
if change_4h > 12.0: return {'type': 'NONE', 'score': 0}
|
| 247 |
-
if curr_1h['rsi'] > 75: return {'type': 'NONE', 'score': 0}
|
| 248 |
-
dev = (curr_1h['close'] - curr_1h['ema20']) / curr_1h['atr'] if curr_1h['atr'] > 0 else 0
|
| 249 |
-
if dev > 2.2: return {'type': 'NONE', 'score': 0}
|
| 250 |
-
|
| 251 |
-
# A. Breakout
|
| 252 |
-
is_bullish = (curr_1h['ema20'] > curr_1h['ema50']) or (curr_1h['close'] > curr_1h['ema20'])
|
| 253 |
-
if is_bullish and (45 <= curr_1h['rsi'] <= 75):
|
| 254 |
-
vol_ma = df_15m['volume'].rolling(20).mean().iloc[-1]
|
| 255 |
-
if curr_15m['volume'] >= 1.2 * vol_ma:
|
| 256 |
-
score = curr_15m['volume'] / vol_ma if vol_ma > 0 else 1.0
|
| 257 |
-
return {'type': 'BREAKOUT', 'score': score}
|
| 258 |
-
|
| 259 |
-
# B. Reversal
|
| 260 |
-
if 20 <= curr_1h['rsi'] <= 40 and change_4h <= -2.0:
|
| 261 |
-
score = (100 - curr_1h['rsi'])
|
| 262 |
-
return {'type': 'REVERSAL', 'score': score}
|
| 263 |
-
|
| 264 |
-
return {'type': 'NONE', 'score': 0}
|
| 265 |
-
|
| 266 |
-
# ------------------------------------------------------------------
|
| 267 |
-
# Helpers
|
| 268 |
# ------------------------------------------------------------------
|
| 269 |
async def _fetch_technical_data_batch(self, candidates):
|
| 270 |
chunk_size = 10; results = []
|
|
@@ -278,7 +289,7 @@ class DataManager:
|
|
| 278 |
|
| 279 |
async def _fetch_single(self, c):
|
| 280 |
try:
|
| 281 |
-
h1 = await self.exchange.fetch_ohlcv(c['symbol'], '1h', limit=
|
| 282 |
m15 = await self.exchange.fetch_ohlcv(c['symbol'], '15m', limit=60)
|
| 283 |
if not h1 or not m15: return None
|
| 284 |
c['ohlcv'] = {'1h': h1, '15m': m15}
|
|
@@ -294,10 +305,21 @@ class DataManager:
|
|
| 294 |
loss = (-delta.where(delta<0, 0)).rolling(14).mean()
|
| 295 |
rs = gain/loss
|
| 296 |
df['rsi'] = 100 - (100/(1+rs))
|
|
|
|
|
|
|
| 297 |
df['ema20'] = df['c'].ewm(span=20).mean()
|
| 298 |
df['ema50'] = df['c'].ewm(span=50).mean()
|
|
|
|
|
|
|
|
|
|
| 299 |
tr = np.maximum(df['h']-df['l'], np.maximum(abs(df['h']-df['c'].shift()), abs(df['l']-df['c'].shift())))
|
| 300 |
df['atr'] = tr.rolling(14).mean()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
df.rename(columns={'o':'open', 'h':'high', 'l':'low', 'c':'close', 'v':'volume'}, inplace=True)
|
| 302 |
return df.fillna(0)
|
| 303 |
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
# 📂 ml_engine/data_manager.py
|
| 3 |
+
# (V61.0 - GEM-Architect: Dual-Opportunity Classification)
|
| 4 |
# ============================================================
|
| 5 |
|
| 6 |
import asyncio
|
|
|
|
| 44 |
'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L'
|
| 45 |
]
|
| 46 |
|
| 47 |
+
print(f"📦 [DataManager V61.0] Dual Classification (Safe Bottoms & Momentum) Active.")
|
| 48 |
|
| 49 |
async def initialize(self):
|
| 50 |
print(" > [DataManager] Starting initialization...")
|
|
|
|
| 73 |
return self.contracts_db
|
| 74 |
|
| 75 |
# ==================================================================
|
| 76 |
+
# 🧠 Layer 1: Classification (Bottom vs Momentum)
|
| 77 |
# ==================================================================
|
| 78 |
async def layer1_rapid_screening(self, adaptive_hub_ref=None) -> List[Dict[str, Any]]:
|
| 79 |
self.adaptive_hub_ref = adaptive_hub_ref
|
| 80 |
+
print(f"🔍 [Layer 1] Screening for SAFE BOTTOMS & MOMENTUM LAUNCHES...")
|
| 81 |
|
| 82 |
+
# 1. فلتر السيولة الأساسي
|
| 83 |
initial_candidates = await self._stage0_universe_filter()
|
| 84 |
if not initial_candidates:
|
| 85 |
print("⚠️ [Layer 1] Stage 0 returned 0 candidates.")
|
|
|
|
| 92 |
final_list = []
|
| 93 |
|
| 94 |
for item in enriched_data:
|
| 95 |
+
# 3. التصنيف الجديد: قاع آمن أم انفجار وشيك؟
|
| 96 |
+
classification = self._classify_opportunity_type(item)
|
| 97 |
|
| 98 |
if classification['type'] != 'NONE':
|
| 99 |
+
# تشخيص حالة السوق العامة (Regime) يبقى كما هو للأمان العام
|
| 100 |
regime_info = self._diagnose_asset_regime(item)
|
| 101 |
current_regime = regime_info['regime']
|
| 102 |
|
| 103 |
+
# 🔥 Regime Gating: الحماية من المصائد
|
| 104 |
+
# إذا السوق عرضي (RANGE) أو ميت (DEAD)، نمنع صفقات الزخم (MOMENTUM) لأنها غالباً مصائد
|
| 105 |
+
# لكن نسمح بصفقات القيعان (SAFE_BOTTOM) لأنها آمنة في التذبذب
|
| 106 |
+
if current_regime in ['RANGE', 'DEAD'] and classification['type'] == 'MOMENTUM_LAUNCH':
|
| 107 |
+
continue
|
|
|
|
| 108 |
|
| 109 |
+
# إضافة البيانات
|
| 110 |
item['asset_regime'] = current_regime
|
| 111 |
item['asset_regime_conf'] = regime_info['conf']
|
| 112 |
|
| 113 |
+
# 🏷️ الوسم الجديد للاستراتيجية
|
| 114 |
+
item['strategy_type'] = classification['type']
|
| 115 |
+
item['l1_sort_score'] = classification['score']
|
| 116 |
+
|
| 117 |
+
# حقن العتبات الديناميكية
|
| 118 |
if self.adaptive_hub_ref:
|
| 119 |
dynamic_config = self.adaptive_hub_ref.get_regime_config(current_regime)
|
| 120 |
item['dynamic_limits'] = dynamic_config
|
| 121 |
|
| 122 |
+
# حفظ التاج القديم للتوافق
|
| 123 |
item['strategy_tag'] = classification['type']
|
| 124 |
+
|
| 125 |
final_list.append(item)
|
| 126 |
|
| 127 |
+
# 4. الترتيب النهائي
|
| 128 |
final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
|
| 129 |
|
| 130 |
selection = final_list[:50]
|
| 131 |
+
print(f"✅ [Layer 1] Passed {len(selection)} candidates (Types: Bottom/Launch).")
|
| 132 |
return selection
|
| 133 |
|
| 134 |
+
# ==================================================================
|
| 135 |
+
# ⚖️ The Dual-Classifier Logic
|
| 136 |
+
# ==================================================================
|
| 137 |
+
def _classify_opportunity_type(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
| 138 |
+
"""
|
| 139 |
+
تقسيم العملات إلى نوعين:
|
| 140 |
+
1. SAFE_BOTTOM: عملات في القاع (آمنة من الهبوط الحاد).
|
| 141 |
+
2. MOMENTUM_LAUNCH: عملات تجهز للانفجار (نسبة 90%).
|
| 142 |
+
"""
|
| 143 |
+
try:
|
| 144 |
+
df_1h = self._calc_indicators(data['ohlcv_1h_raw'])
|
| 145 |
+
# يمكن استخدام فريم 15 دقيقة للتأكيد الإضافي مستقبلاً
|
| 146 |
+
# df_15m = self._calc_indicators(data['ohlcv_15m_raw'])
|
| 147 |
+
curr = df_1h.iloc[-1]
|
| 148 |
+
except: return {'type': 'NONE', 'score': 0}
|
| 149 |
+
|
| 150 |
+
# --- المؤشرات الأساسية ---
|
| 151 |
+
rsi = curr['rsi']
|
| 152 |
+
close = curr['close']
|
| 153 |
+
ema50 = curr['ema50']
|
| 154 |
+
ema200 = curr['ema200'] if 'ema200' in curr else ema50
|
| 155 |
+
|
| 156 |
+
# Bollinger Bands Check
|
| 157 |
+
lower_bb = curr['lower_bb'] if 'lower_bb' in curr else (curr['ema20'] - (2*curr['atr']))
|
| 158 |
+
upper_bb = curr['upper_bb'] if 'upper_bb' in curr else (curr['ema20'] + (2*curr['atr']))
|
| 159 |
+
|
| 160 |
+
# 🛡️ TYPE 1: SAFE_BOTTOM (The Safety Net)
|
| 161 |
+
# الشروط: RSI منخفض جداً، السعر عند أو تحت الحد السفلي، بعيد عن المتوسطات
|
| 162 |
+
# هذا النوع آمن لأن الهبوط الإضافي صعب إحصائياً (Mean Reversion)
|
| 163 |
+
if rsi < 35:
|
| 164 |
+
# التأكد من الابتعاد عن المتوسط (Oversold Extension)
|
| 165 |
+
dist_from_ema = (ema50 - close) / ema50
|
| 166 |
+
|
| 167 |
+
# السعر عند الدعم السفلي أو تحته بقليل
|
| 168 |
+
if close <= lower_bb * 1.015 and dist_from_ema > 0.03:
|
| 169 |
+
# Score: كلما كان RSI أقل، كلما كان الارتداد أقوى
|
| 170 |
+
score = (50 - rsi) / 20.0
|
| 171 |
+
return {'type': 'SAFE_BOTTOM', 'score': min(score, 1.0)}
|
| 172 |
+
|
| 173 |
+
# 🚀 TYPE 2: MOMENTUM_LAUNCH (The Rocket)
|
| 174 |
+
# الشروط: RSI صحي وقوي (فوق 55 وتحت 75)، السعر فوق المتوسطات، ضغط سعري (Squeeze)
|
| 175 |
+
# هذا النوع يجهز للانفجار للأعلى
|
| 176 |
+
elif 55 < rsi < 75:
|
| 177 |
+
if close > ema50 and close > ema200:
|
| 178 |
+
# السعر يضغط قرب الحد العلوي (Squeeze before breakout)
|
| 179 |
+
dist_to_upper = (upper_bb - close) / close
|
| 180 |
+
|
| 181 |
+
# قريب جداً من الانفجار (أقل من 3% للحد العلوي)
|
| 182 |
+
if dist_to_upper < 0.03:
|
| 183 |
+
score = rsi / 100.0
|
| 184 |
+
return {'type': 'MOMENTUM_LAUNCH', 'score': score}
|
| 185 |
+
|
| 186 |
+
return {'type': 'NONE', 'score': 0}
|
| 187 |
+
|
| 188 |
# ==================================================================
|
| 189 |
# 🔍 Stage 0: Universe Filter (Robust USD Calc)
|
| 190 |
# ==================================================================
|
|
|
|
| 198 |
SOVEREIGN_COINS = ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT']
|
| 199 |
|
| 200 |
reject_stats = {"volume": 0, "change": 0, "blacklist": 0}
|
|
|
|
| 201 |
|
| 202 |
for symbol, ticker in tickers.items():
|
| 203 |
if not symbol.endswith('/USDT'): continue
|
|
|
|
| 214 |
|
| 215 |
is_sovereign = symbol in SOVEREIGN_COINS
|
| 216 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
# فلتر السيولة (500k) - نتجاوزه للعملات السيادية
|
| 218 |
if not is_sovereign:
|
| 219 |
if calc_quote_vol < 500000:
|
| 220 |
reject_stats["volume"] += 1
|
| 221 |
continue
|
| 222 |
|
| 223 |
+
# فلتر التغير (20%) - نتجاوزه إذا كنا نبحث عن قيعان (قد تكون هابطة) لكن بحذر
|
| 224 |
+
# هنا نبقي الفلتر العام للحماية من الانهيارات التامة
|
| 225 |
change_pct = ticker.get('percentage')
|
| 226 |
if change_pct is None: change_pct = 0.0
|
| 227 |
|
| 228 |
+
if abs(change_pct) > 25.0:
|
| 229 |
reject_stats["change"] += 1
|
| 230 |
continue
|
| 231 |
|
|
|
|
| 236 |
'change_24h': change_pct
|
| 237 |
})
|
| 238 |
|
|
|
|
|
|
|
|
|
|
| 239 |
candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
|
| 240 |
return candidates
|
| 241 |
|
|
|
|
| 275 |
except Exception: return {'regime': 'RANGE', 'conf': 0.0}
|
| 276 |
|
| 277 |
# ------------------------------------------------------------------
|
| 278 |
+
# Helpers & Indicators (Updated for BB & EMA200)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
# ------------------------------------------------------------------
|
| 280 |
async def _fetch_technical_data_batch(self, candidates):
|
| 281 |
chunk_size = 10; results = []
|
|
|
|
| 289 |
|
| 290 |
async def _fetch_single(self, c):
|
| 291 |
try:
|
| 292 |
+
h1 = await self.exchange.fetch_ohlcv(c['symbol'], '1h', limit=210) # زاد العدد لحساب EMA200 بدقة
|
| 293 |
m15 = await self.exchange.fetch_ohlcv(c['symbol'], '15m', limit=60)
|
| 294 |
if not h1 or not m15: return None
|
| 295 |
c['ohlcv'] = {'1h': h1, '15m': m15}
|
|
|
|
| 305 |
loss = (-delta.where(delta<0, 0)).rolling(14).mean()
|
| 306 |
rs = gain/loss
|
| 307 |
df['rsi'] = 100 - (100/(1+rs))
|
| 308 |
+
|
| 309 |
+
# EMAs
|
| 310 |
df['ema20'] = df['c'].ewm(span=20).mean()
|
| 311 |
df['ema50'] = df['c'].ewm(span=50).mean()
|
| 312 |
+
df['ema200'] = df['c'].ewm(span=200).mean() # مهم لنوع الزخم
|
| 313 |
+
|
| 314 |
+
# ATR
|
| 315 |
tr = np.maximum(df['h']-df['l'], np.maximum(abs(df['h']-df['c'].shift()), abs(df['l']-df['c'].shift())))
|
| 316 |
df['atr'] = tr.rolling(14).mean()
|
| 317 |
+
|
| 318 |
+
# Bollinger Bands (Standard: 20, 2)
|
| 319 |
+
std = df['c'].rolling(20).std()
|
| 320 |
+
df['upper_bb'] = df['ema20'] + (2 * std)
|
| 321 |
+
df['lower_bb'] = df['ema20'] - (2 * std)
|
| 322 |
+
|
| 323 |
df.rename(columns={'o':'open', 'h':'high', 'l':'low', 'c':'close', 'v':'volume'}, inplace=True)
|
| 324 |
return df.fillna(0)
|
| 325 |
|