Riy777 commited on
Commit
a96dda4
·
verified ·
1 Parent(s): 58d7d2b

Update ml_engine/data_manager.py

Browse files
Files changed (1) hide show
  1. ml_engine/data_manager.py +76 -199
ml_engine/data_manager.py CHANGED
@@ -1,11 +1,11 @@
1
- # ============================================================
2
  # 📂 ml_engine/data_manager.py
3
- # (V70.4 - GEM-Architect: Anti-Stagnation & Deep Scanning)
4
- # ============================================================
5
- # - Removed "Volatility Trap" to stop repeating bad coins.
6
- # - Expanded scanning range to 900 coins.
7
- # - Stricter pattern filtering at Layer 1.
8
- # ============================================================
9
 
10
  import asyncio
11
  import httpx
@@ -44,13 +44,13 @@ class DataManager:
44
  self.http_client = None
45
  self.market_cache = {}
46
 
47
- # القائمة السوداء للعملات غير المرغوبة (Leveraged/Stable/Fiat)
48
  self.BLACKLIST_TOKENS = [
49
  'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
50
  'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', '5S', '5L'
51
  ]
52
 
53
- print(f"📦 [DataManager V70.4] Initialized (Smart Filter Active).")
54
 
55
  async def initialize(self):
56
  """تهيئة الاتصال والأسواق"""
@@ -82,238 +82,130 @@ class DataManager:
82
  return self.contracts_db
83
 
84
  # ==================================================================
85
- # 🌍 Global Market Validator V2 (Smart Breadth Scanner)
86
  # ==================================================================
87
  async def check_global_market_health(self) -> Dict[str, Any]:
88
  """
89
- يفحص صحة السوق العامة باستخدام منطق مزدوج.
90
  """
91
  try:
92
- # 1. جلب بيانات البيتكوين الأساسية
93
- btc_ohlcv = await self.exchange.fetch_ohlcv('BTC/USDT', '1d', limit=30)
94
- if not btc_ohlcv: return {'is_safe': True, 'reason': 'No BTC Data - Bypassed'}
95
 
96
  df = pd.DataFrame(btc_ohlcv, columns=['ts', 'o', 'h', 'l', 'c', 'v'])
97
- current_close = df['c'].iloc[-1]
98
- prev_close = df['c'].iloc[-2]
99
 
100
- # --- [ CRITICAL CHECK ] ---
101
- daily_change = (current_close - prev_close) / prev_close
102
- if daily_change < -0.05: # -5% Crash check
103
  return {'is_safe': False, 'reason': f'🚨 BTC CRASHING ({daily_change*100:.2f}%)'}
104
 
105
- # فحص المتوسطات (Trend Check)
106
- sma20 = df['c'].rolling(20).mean().iloc[-1]
107
- if current_close < sma20 * 0.90: # Bear Market Deep
108
- return {'is_safe': False, 'reason': '📉 Deep Bear Market (Risk Off)'}
109
-
110
- return {'is_safe': True, 'reason': '✅ Market Healthy'}
111
 
112
  except Exception as e:
113
- print(f"⚠️ [Market Validator] Error: {e}")
114
  return {'is_safe': True, 'reason': 'Error Bypass'}
115
 
116
  # ==================================================================
117
- # 🧠 Layer 1: Classification (Strict Funnel)
118
  # ==================================================================
119
- async def layer1_rapid_screening(self, limit=300, adaptive_hub_ref=None) -> List[Dict[str, Any]]:
 
 
 
 
120
  self.adaptive_hub_ref = adaptive_hub_ref
121
- print(f"🔍 [Layer 1] Screening Market (Deep Scan)...")
122
 
123
- # 0. فحص صحة السوق
124
  market_health = await self.check_global_market_health()
125
-
126
  if not market_health['is_safe']:
127
  print(f"⛔ [Market Validator] Trading Halted: {market_health['reason']}")
128
  return []
129
- else:
130
- print(f" 🌍 [Market Validator] Status: {market_health['reason']}")
131
 
132
- # 1. فلتر السيولة الأساسي
133
  initial_candidates = await self._stage0_universe_filter()
134
  if not initial_candidates:
135
- print("⚠️ [Layer 1] Stage 0 returned 0 candidates.")
136
  return []
137
 
138
- # 2. جلب البيانات الفنية (Batch Fetching)
139
- # زيادة نطاق البحث إلى 900 عملة لزيادة التنوع
140
- top_candidates = initial_candidates[:900]
 
 
 
141
  enriched_data = await self._fetch_technical_data_batch(top_candidates)
142
 
143
- semi_final_list = []
144
 
145
- # 3. التصنيف الفني
146
  for item in enriched_data:
147
- classification = self._classify_opportunity_type(item)
 
148
 
149
- if classification['type'] != 'NONE':
150
- # تشخيص الحالة
151
- regime_info = self._diagnose_asset_regime(item)
152
- item['asset_regime'] = regime_info['regime']
153
- item['asset_regime_conf'] = regime_info['conf']
154
-
155
- item['strategy_type'] = classification['type']
156
- item['l1_sort_score'] = classification['score']
157
- item['strategy_tag'] = classification['type']
158
-
159
- # تخطي العملات "الميتة" إلا إذا كانت في حالة ضغط
160
- if regime_info['regime'] == 'DEAD' and classification['type'] == 'MOMENTUM_LAUNCH':
161
- if not classification.get('is_squeeze', False):
162
- continue
163
-
164
- semi_final_list.append(item)
165
-
166
- # 4. فحص العمق وحقن الإعدادات
167
- final_list = []
168
- # ترتيب حسب السكور الفني وليس الفوليوم
169
- semi_final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
170
- candidates_for_depth = semi_final_list[:limit]
171
-
172
- if candidates_for_depth:
173
- print(f" 🛡️ [Layer 1.5] Checking Depth for {len(candidates_for_depth)} candidates...")
174
-
175
- for item in candidates_for_depth:
176
- # أ. فحص العمق (Depth Check)
177
- if item['strategy_type'] in ['ACCUMULATION_SQUEEZE', 'SAFE_BOTTOM']:
178
- try:
179
- atr_val = item.get('atr_value', 0.0)
180
- curr_price = item.get('current_price', 0.0)
181
-
182
- if atr_val > 0 and curr_price > 0:
183
- range_2h = atr_val * 2.0
184
- ob_score = await self._check_ob_pressure(item['symbol'], curr_price, range_2h)
185
-
186
- if ob_score > 0.6:
187
- item['l1_sort_score'] += 0.15
188
- item['note'] = f"Strong Depth Support ({ob_score:.2f})"
189
- elif ob_score < 0.4:
190
- item['l1_sort_score'] -= 0.10
191
- except Exception: pass
192
 
193
- # ب. حقن الإعدادات
 
 
 
 
 
 
 
 
194
  if self.adaptive_hub_ref:
195
- coin_type = item.get('strategy_type', 'SAFE_BOTTOM')
196
- dynamic_config = self.adaptive_hub_ref.get_coin_type_config(coin_type)
197
  item['dynamic_limits'] = dynamic_config
198
 
199
  final_list.append(item)
200
 
201
- # الترتيب النهائي
202
- final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
203
- selection = final_list[:limit]
204
 
205
- print(f"✅ [Layer 1] Passed {len(selection)} active candidates.")
206
- return selection
207
 
208
  # ==================================================================
209
- # 🧱 Order Book Depth Scanner
210
  # ==================================================================
211
- async def _check_ob_pressure(self, symbol: str, current_price: float, price_range: float) -> float:
212
- """فحص ضغط الشراء مقابل البيع في عمق السوق"""
213
- try:
214
- ob = await self.exchange.fetch_order_book(symbol, limit=50)
215
- bids = ob['bids']
216
- asks = ob['asks']
217
-
218
- min_price = current_price - price_range
219
- max_price = current_price + price_range
220
-
221
- support_vol = 0.0
222
- resistance_vol = 0.0
223
-
224
- for p, v in bids:
225
- if p >= min_price: support_vol += v
226
- else: break
227
-
228
- for p, v in asks:
229
- if p <= max_price: resistance_vol += v
230
- else: break
231
-
232
- if (support_vol + resistance_vol) == 0: return 0.5
233
- return support_vol / (support_vol + resistance_vol)
234
- except Exception:
235
- return 0.5
236
-
237
- # ==================================================================
238
- # ⚖️ The Dual-Classifier Logic (STRICT MODE)
239
- # ==================================================================
240
- def _classify_opportunity_type(self, data: Dict[str, Any]) -> Dict[str, Any]:
241
- """تصنيف العملة - نسخة منقحة بدون تمرير عشوائي"""
242
- try:
243
- df_1h = self._calc_indicators(data['ohlcv_1h_raw'])
244
- curr = df_1h.iloc[-1]
245
- data['atr_value'] = curr['atr']
246
- except: return {'type': 'NONE', 'score': 0}
247
-
248
- rsi = curr['rsi']
249
- close = curr['close']
250
- ema20 = curr['ema20']
251
- ema50 = curr['ema50']
252
- atr = curr['atr']
253
-
254
- lower_bb = curr['lower_bb'] if 'lower_bb' in curr else (curr['ema20'] - (2*curr['atr']))
255
- upper_bb = curr['upper_bb'] if 'upper_bb' in curr else (curr['ema20'] + (2*curr['atr']))
256
- bb_width = (upper_bb - lower_bb) / curr['ema20'] if curr['ema20'] > 0 else 1.0
257
-
258
- # 🔥 1. Dead Coin Filter
259
- volatility_pct = (atr / close) * 100 if close > 0 else 0
260
- if volatility_pct < 2 : return {'type': 'NONE', 'score': 0}
261
-
262
- # 🛡️ TYPE 1: SAFE_BOTTOM (القيعان الآمنة)
263
- # تشبع بيعي حقيقي فقط
264
- if rsi < 50: # Raised slightly from 55
265
- # يجب أن يكون السعر قريباً جداً من القاع أو تحته
266
- if close <= lower_bb * 1.05:
267
- score = (60 - rsi) / 20.0
268
- return {'type': 'SAFE_BOTTOM', 'score': min(score, 1.0)}
269
-
270
- # 🔋 TYPE 2: ACCUMULATION_SQUEEZE (التجميع والضغط)
271
- elif 40 <= rsi <= 65:
272
- if bb_width < 0.15: # Stricter squeeze
273
- score = 1.0 - (bb_width * 3.0)
274
- return {'type': 'ACCUMULATION_SQUEEZE', 'score': max(score, 0.5), 'is_squeeze': True}
275
-
276
- # 🚀 TYPE 3: MOMENTUM_LAUNCH (انطلاق الزخم)
277
- elif 50 < rsi < 80:
278
- if close > ema50:
279
- dist_to_upper = (upper_bb - close) / close
280
- if dist_to_upper < 0.10: # قريب جداً من الاختراق
281
- score = rsi / 100.0
282
- return {'type': 'MOMENTUM_LAUNCH', 'score': score}
283
-
284
- # ❌ REMOVED: "Special Case: High Volatility Catch"
285
- # This was causing the "Same Coin" loop.
286
- # If it doesn't fit the patterns above, it's rejected.
287
-
288
- return {'type': 'NONE', 'score': 0}
289
 
290
  # ==================================================================
291
- # 🔍 Stage 0: Universe Filter
292
  # ==================================================================
293
  async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
294
- """جلب كل العملات وتصفيتها حسب الحجم"""
295
  try:
296
- MIN_VOLUME_THRESHOLD = 2000000.0 # Reduced to $750k to find hidden gems
 
297
 
298
  print(f" 🛡️ [Stage 0] Fetching Tickers (Min Vol: ${MIN_VOLUME_THRESHOLD:,.0f})...")
299
  tickers = await self.exchange.fetch_tickers()
300
  candidates = []
301
 
302
  SOVEREIGN_COINS = ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT']
303
- reject_stats = {"volume": 0, "change": 0, "blacklist": 0}
304
 
305
  for symbol, ticker in tickers.items():
306
  if not symbol.endswith('/USDT'): continue
307
 
 
308
  base_curr = symbol.split('/')[0]
309
  if any(bad in base_curr for bad in self.BLACKLIST_TOKENS):
310
  reject_stats["blacklist"] += 1
311
  continue
312
 
 
313
  base_vol = float(ticker.get('baseVolume') or 0.0)
314
  last_price = float(ticker.get('last') or 0.0)
315
  calc_quote_vol = base_vol * last_price
316
 
 
317
  is_sovereign = symbol in SOVEREIGN_COINS
318
 
319
  if not is_sovereign:
@@ -324,10 +216,6 @@ class DataManager:
324
  change_pct = ticker.get('percentage')
325
  if change_pct is None: change_pct = 0.0
326
 
327
- if abs(change_pct) > 40.0: # Relaxed slightly
328
- reject_stats["change"] += 1
329
- continue
330
-
331
  candidates.append({
332
  'symbol': symbol,
333
  'quote_volume': calc_quote_vol,
@@ -335,8 +223,9 @@ class DataManager:
335
  'change_24h': change_pct
336
  })
337
 
 
338
  candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
339
- print(f" ℹ️ [Stage 0] Ignored {reject_stats['volume']} low-vol coins.")
340
  return candidates
341
 
342
  except Exception as e:
@@ -345,10 +234,10 @@ class DataManager:
345
  return []
346
 
347
  # ------------------------------------------------------------------
348
- # 🧭 The Diagnoser
349
  # ------------------------------------------------------------------
350
  def _diagnose_asset_regime(self, item: Dict[str, Any]) -> Dict[str, Any]:
351
- """تشخيص حالة العملة الفردية"""
352
  try:
353
  if 'df_1h' not in item:
354
  if 'ohlcv_1h_raw' in item:
@@ -364,18 +253,14 @@ class DataManager:
364
  ema20 = curr['ema20']
365
  ema50 = curr['ema50']
366
  rsi = curr['rsi']
367
- atr = curr['atr']
368
- atr_pct = (atr / price) * 100 if price > 0 else 0
369
 
370
  regime = "RANGE"
371
  conf = 0.5
372
 
373
- if atr_pct < 0.4: return {'regime': 'DEAD', 'conf': 0.9}
374
-
375
- if price > ema20 and ema20 > ema50 and rsi > 50:
376
  regime = "BULL"
377
  conf = 0.8 if rsi > 55 else 0.6
378
- elif price < ema20 and ema20 < ema50 and rsi < 50:
379
  regime = "BEAR"
380
  conf = 0.8 if rsi < 45 else 0.6
381
 
@@ -383,7 +268,7 @@ class DataManager:
383
  except Exception: return {'regime': 'RANGE', 'conf': 0.0}
384
 
385
  # ------------------------------------------------------------------
386
- # Helpers
387
  # ------------------------------------------------------------------
388
  async def _fetch_technical_data_batch(self, candidates):
389
  """جلب البيانات الفنية (1h, 15m) على دفعات"""
@@ -398,6 +283,7 @@ class DataManager:
398
 
399
  async def _fetch_single(self, c):
400
  try:
 
401
  h1 = await self.exchange.fetch_ohlcv(c['symbol'], '1h', limit=100)
402
  m15 = await self.exchange.fetch_ohlcv(c['symbol'], '15m', limit=50)
403
  if not h1 or not m15: return None
@@ -427,11 +313,6 @@ class DataManager:
427
  tr = np.maximum(df['h']-df['l'], np.maximum(abs(df['h']-df['c'].shift()), abs(df['l']-df['c'].shift())))
428
  df['atr'] = tr.rolling(14).mean()
429
 
430
- # Bollinger Bands
431
- std = df['c'].rolling(20).std()
432
- df['upper_bb'] = df['ema20'] + (2 * std)
433
- df['lower_bb'] = df['ema20'] - (2 * std)
434
-
435
  df.rename(columns={'o':'open', 'h':'high', 'l':'low', 'c':'close', 'v':'volume'}, inplace=True)
436
  return df.fillna(0)
437
 
@@ -441,8 +322,4 @@ class DataManager:
441
 
442
  async def get_latest_ohlcv(self, symbol, timeframe='5m', limit=100):
443
  try: return await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
444
- except: return []
445
-
446
- async def get_order_book_snapshot(self, symbol, limit=20):
447
- try: return await self.exchange.fetch_order_book(symbol, limit)
448
- except: return {}
 
1
+ # ==============================================================================
2
  # 📂 ml_engine/data_manager.py
3
+ # (V75.0 - GEM-Architect: Open Floodgates Mode)
4
+ # ==============================================================================
5
+ # - Removed Technical Hard Filters (Safe Bottom, Momentum, etc.).
6
+ # - Primary Filter: Volume > $1M ONLY.
7
+ # - Decision Authority: Delegated 100% to Layer 2 (Neural Models).
8
+ # ==============================================================================
9
 
10
  import asyncio
11
  import httpx
 
44
  self.http_client = None
45
  self.market_cache = {}
46
 
47
+ # القائمة السوداء لعملات المستقرة والرافعة)
48
  self.BLACKLIST_TOKENS = [
49
  'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
50
  'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', '5S', '5L'
51
  ]
52
 
53
+ print(f"📦 [DataManager V75.0] Initialized (Filter: Vol > $1M Only).")
54
 
55
  async def initialize(self):
56
  """تهيئة الاتصال والأسواق"""
 
82
  return self.contracts_db
83
 
84
  # ==================================================================
85
+ # 🌍 Global Market Validator (Bypassable)
86
  # ==================================================================
87
  async def check_global_market_health(self) -> Dict[str, Any]:
88
  """
89
+ فحص سريع لصحة السوق. لن يوقف التداول ولكنه يعطي تحذيراً.
90
  """
91
  try:
92
+ btc_ohlcv = await self.exchange.fetch_ohlcv('BTC/USDT', '1d', limit=7)
93
+ if not btc_ohlcv: return {'is_safe': True, 'reason': 'No BTC Data'}
 
94
 
95
  df = pd.DataFrame(btc_ohlcv, columns=['ts', 'o', 'h', 'l', 'c', 'v'])
96
+ daily_change = (df['c'].iloc[-1] - df['c'].iloc[-2]) / df['c'].iloc[-2]
 
97
 
98
+ # فقط الانهيار الكبير جداً يوقف النظام
99
+ if daily_change < -0.10:
 
100
  return {'is_safe': False, 'reason': f'🚨 BTC CRASHING ({daily_change*100:.2f}%)'}
101
 
102
+ return {'is_safe': True, 'reason': '✅ Market Open'}
 
 
 
 
 
103
 
104
  except Exception as e:
 
105
  return {'is_safe': True, 'reason': 'Error Bypass'}
106
 
107
  # ==================================================================
108
+ # 🧠 Layer 1: The Open Gate (Volume Only)
109
  # ==================================================================
110
+ async def layer1_rapid_screening(self, limit=200, adaptive_hub_ref=None) -> List[Dict[str, Any]]:
111
+ """
112
+ يقوم بفلترة العملات بناءً على الفوليوم فقط (1 مليون دولار).
113
+ يمرر أفضل العملات (حسب الفوليوم) إلى الطبقة الثانية للتحليل العصبي.
114
+ """
115
  self.adaptive_hub_ref = adaptive_hub_ref
116
+ print(f"🔍 [Layer 1] Screening Market (Volume Only > $1M)...")
117
 
118
+ # 0. فحص السوق
119
  market_health = await self.check_global_market_health()
 
120
  if not market_health['is_safe']:
121
  print(f"⛔ [Market Validator] Trading Halted: {market_health['reason']}")
122
  return []
 
 
123
 
124
+ # 1. فلتر السيولة الصارم (Stage 0)
125
  initial_candidates = await self._stage0_universe_filter()
126
  if not initial_candidates:
127
+ print("⚠️ [Layer 1] No coins met the $1M volume criteria.")
128
  return []
129
 
130
+ # نأخذ أعلى العملات سيولة لنقوم بجلب بياناتها
131
+ # نرفع العدد هنا لأننا لن نفلتر بالفنيات، بل سنرسل للنماذج
132
+ # لكن يجب الحذر من الـ Rate Limits، لذا سنأخذ أفضل 150 عملة
133
+ top_candidates = initial_candidates[:limit]
134
+ print(f" 📥 Fetching data for top {len(top_candidates)} liquid assets...")
135
+
136
  enriched_data = await self._fetch_technical_data_batch(top_candidates)
137
 
138
+ final_list = []
139
 
140
+ # 2. التجهيز للطبقة الثانية (بدون فلترة فنية)
141
  for item in enriched_data:
142
+ # تشخيص الحالة فقط للعلم (Info) وليس للرفض
143
+ regime_info = self._diagnose_asset_regime(item)
144
 
145
+ item['asset_regime'] = regime_info['regime']
146
+ item['asset_regime_conf'] = regime_info['conf']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
 
148
+ # تعيين نوع موحد ليتم التعامل معه في الطبقات التالية
149
+ item['strategy_type'] = 'NEURAL_SCAN'
150
+ item['strategy_tag'] = 'NEURAL'
151
+
152
+ # السكور الأولي يعتمد على السيولة النسبية (لترتيب الأولويات فقط)
153
+ # يمكن تعديله لاحقاً بواسطة Neural Models
154
+ item['l1_sort_score'] = 0.5 # درجة حيادية للسماح للكل بالمرور
155
+
156
+ # حقن الإعدادات الديناميكية (إن وجدت)
157
  if self.adaptive_hub_ref:
158
+ # نستخدم إعدادات SAFE_BOTTOM كإعداد��ت افتراضية محافظة
159
+ dynamic_config = self.adaptive_hub_ref.get_coin_type_config('SAFE_BOTTOM')
160
  item['dynamic_limits'] = dynamic_config
161
 
162
  final_list.append(item)
163
 
164
+ # الترتيب حسب الفوليوم (الأكثر سيولة أولاً)
165
+ # item['quote_volume'] موجودة من Stage 0
166
+ final_list.sort(key=lambda x: x.get('quote_volume', 0), reverse=True)
167
 
168
+ print(f"✅ [Layer 1] Passed {len(final_list)} candidates directly to Neural Layer.")
169
+ return final_list
170
 
171
  # ==================================================================
172
+ # 🧱 Order Book Depth Scanner (Optional Helper)
173
  # ==================================================================
174
+ async def get_order_book_snapshot(self, symbol: str, limit=20):
175
+ try: return await self.exchange.fetch_order_book(symbol, limit)
176
+ except: return {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
  # ==================================================================
179
+ # 🔍 Stage 0: Universe Filter (1 Million Dollar Rule)
180
  # ==================================================================
181
  async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
182
+ """جلب كل العملات وتصفيتها حسب شرط المليون دولار"""
183
  try:
184
+ # الشرط الوحيد: 1 مليون دولار
185
+ MIN_VOLUME_THRESHOLD = 1000000.0
186
 
187
  print(f" 🛡️ [Stage 0] Fetching Tickers (Min Vol: ${MIN_VOLUME_THRESHOLD:,.0f})...")
188
  tickers = await self.exchange.fetch_tickers()
189
  candidates = []
190
 
191
  SOVEREIGN_COINS = ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT']
192
+ reject_stats = {"volume": 0, "blacklist": 0}
193
 
194
  for symbol, ticker in tickers.items():
195
  if not symbol.endswith('/USDT'): continue
196
 
197
+ # إقصاء العملات المحظورة
198
  base_curr = symbol.split('/')[0]
199
  if any(bad in base_curr for bad in self.BLACKLIST_TOKENS):
200
  reject_stats["blacklist"] += 1
201
  continue
202
 
203
+ # حساب الفوليوم بالدولار
204
  base_vol = float(ticker.get('baseVolume') or 0.0)
205
  last_price = float(ticker.get('last') or 0.0)
206
  calc_quote_vol = base_vol * last_price
207
 
208
+ # استثناء العملات السيادية من شرط الفوليوم (دائماً مقبولة)
209
  is_sovereign = symbol in SOVEREIGN_COINS
210
 
211
  if not is_sovereign:
 
216
  change_pct = ticker.get('percentage')
217
  if change_pct is None: change_pct = 0.0
218
 
 
 
 
 
219
  candidates.append({
220
  'symbol': symbol,
221
  'quote_volume': calc_quote_vol,
 
223
  'change_24h': change_pct
224
  })
225
 
226
+ # ترتيب القائمة حسب الفوليوم لضمان معالجة الأهم أولاً
227
  candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
228
+ print(f" ℹ️ [Stage 0] Ignored {reject_stats['volume']} low-vol coins. Found {len(candidates)} candidates.")
229
  return candidates
230
 
231
  except Exception as e:
 
234
  return []
235
 
236
  # ------------------------------------------------------------------
237
+ # 🧭 The Diagnoser (For Context Only)
238
  # ------------------------------------------------------------------
239
  def _diagnose_asset_regime(self, item: Dict[str, Any]) -> Dict[str, Any]:
240
+ """تشخيص حالة العملة الفردية (للعلم فقط)"""
241
  try:
242
  if 'df_1h' not in item:
243
  if 'ohlcv_1h_raw' in item:
 
253
  ema20 = curr['ema20']
254
  ema50 = curr['ema50']
255
  rsi = curr['rsi']
 
 
256
 
257
  regime = "RANGE"
258
  conf = 0.5
259
 
260
+ if price > ema20 and ema20 > ema50:
 
 
261
  regime = "BULL"
262
  conf = 0.8 if rsi > 55 else 0.6
263
+ elif price < ema20 and ema20 < ema50:
264
  regime = "BEAR"
265
  conf = 0.8 if rsi < 45 else 0.6
266
 
 
268
  except Exception: return {'regime': 'RANGE', 'conf': 0.0}
269
 
270
  # ------------------------------------------------------------------
271
+ # Helpers & Data Fetching
272
  # ------------------------------------------------------------------
273
  async def _fetch_technical_data_batch(self, candidates):
274
  """جلب البيانات الفنية (1h, 15m) على دفعات"""
 
283
 
284
  async def _fetch_single(self, c):
285
  try:
286
+ # نحتاج هذه البيانات للمراحل القادمة (Processor)
287
  h1 = await self.exchange.fetch_ohlcv(c['symbol'], '1h', limit=100)
288
  m15 = await self.exchange.fetch_ohlcv(c['symbol'], '15m', limit=50)
289
  if not h1 or not m15: return None
 
313
  tr = np.maximum(df['h']-df['l'], np.maximum(abs(df['h']-df['c'].shift()), abs(df['l']-df['c'].shift())))
314
  df['atr'] = tr.rolling(14).mean()
315
 
 
 
 
 
 
316
  df.rename(columns={'o':'open', 'h':'high', 'l':'low', 'c':'close', 'v':'volume'}, inplace=True)
317
  return df.fillna(0)
318
 
 
322
 
323
  async def get_latest_ohlcv(self, symbol, timeframe='5m', limit=100):
324
  try: return await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
325
+ except: return []