Riy777 commited on
Commit
a1e2912
·
verified ·
1 Parent(s): 849fc8a

Update ml_engine/data_manager.py

Browse files
Files changed (1) hide show
  1. ml_engine/data_manager.py +168 -285
ml_engine/data_manager.py CHANGED
@@ -1,6 +1,6 @@
1
  # ============================================================
2
  # 📂 ml_engine/data_manager.py
3
- # (V36.0 - GEM-Architect: Regime-Adaptive Vision Full)
4
  # ============================================================
5
 
6
  import asyncio
@@ -14,355 +14,238 @@ from typing import List, Dict, Any
14
 
15
  import ccxt.async_support as ccxt
16
 
17
- # ✅ استيراد الدستور الديناميكي (لقراءة الحالة الحالية)
18
  try:
19
  from ml_engine.processor import SystemLimits
20
  except ImportError:
21
- # Fallback إذا لم يتم التحميل بعد
22
  class SystemLimits:
23
  L1_MIN_AFFINITY_SCORE = 15.0
24
  CURRENT_REGIME = "RANGE"
 
 
 
 
 
 
 
25
 
26
- # إعدادات التسجيل لإسكات الإزعاج
27
  logging.getLogger("httpx").setLevel(logging.WARNING)
28
- logging.getLogger("httpcore").setLevel(logging.WARNING)
29
  logging.getLogger("ccxt").setLevel(logging.WARNING)
30
 
31
  class DataManager:
32
  """
33
- DataManager V36.0 (The Chameleon Eye)
34
- - تتغير خوارزمية الفلترة (L1) تماماً بناءً على حالة السوق (Regime).
35
- - تعتمد على SystemLimits.CURRENT_REGIME لتحديد التكتيك.
36
  """
37
 
38
  def __init__(self, contracts_db, whale_monitor, r2_service=None):
39
- # ==================================================================
40
- # ⚙️ إعدادات التحكم والتهيئة
41
- # ==================================================================
42
  self.contracts_db = contracts_db or {}
43
  self.whale_monitor = whale_monitor
44
  self.r2_service = r2_service
45
-
46
- # إعداد عميل المنصة (KuCoin) مع تفعيل حدود السرعة
47
  self.exchange = ccxt.kucoin({
48
  'enableRateLimit': True,
49
  'timeout': 30000,
50
  'options': {'defaultType': 'spot'}
51
  })
52
-
53
  self.http_client = None
54
- self.market_cache = {}
55
-
56
- # 🚫 قوائم الاستبعاد (العملات المستقرة، الرافعة، والعملات المحظورة)
57
- self.BLACKLIST_TOKENS = [
58
- 'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
59
- 'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', 'USDD', 'USDP'
60
- ]
61
-
62
- print(f"📦 [DataManager V36.0] Adaptive Vision Online.")
63
 
64
  async def initialize(self):
65
- """تهيئة مدير البيانات والاتصالات"""
66
- print(" > [DataManager] Starting initialization...")
67
- try:
68
- self.http_client = httpx.AsyncClient(timeout=60.0)
69
- await self._load_markets()
70
- print(f"✅ [DataManager] Ready (Mode: {getattr(SystemLimits, 'CURRENT_REGIME', 'UNKNOWN')}).")
71
- except Exception as e:
72
- print(f"❌ [DataManager] Init Error: {e}")
73
- traceback.print_exc()
74
 
75
  async def _load_markets(self):
76
- """تحميل أزواج التداول وحفظها في الذاكرة"""
77
- try:
78
- if self.exchange:
79
- # التأكد من عدم تحميل الأسواق مرتين إذا كانت محملة
80
- if not self.exchange.markets:
81
- await self.exchange.load_markets()
82
- self.market_cache = self.exchange.markets
83
- except Exception as e:
84
- print(f"❌ [DataManager] Market load failed: {e}")
85
- traceback.print_exc()
86
 
87
  async def close(self):
88
- """إغلاق الاتصالات بأمان"""
89
- if self.http_client:
90
- await self.http_client.aclose()
91
- if self.exchange:
92
- await self.exchange.close()
93
- print("🛑 [DataManager] Connections Closed.")
94
 
95
  # ==================================================================
96
- # 🚀 R2 Compatibility
97
- # ==================================================================
98
- async def load_contracts_from_r2(self):
99
- if not self.r2_service: return
100
- try:
101
- self.contracts_db = await self.r2_service.load_contracts_db_async()
102
- print(f" -> [DataManager] Contracts DB updated from R2: {len(self.contracts_db)} records.")
103
- except Exception as e:
104
- print(f"⚠️ [DataManager] R2 Load Warning: {e}")
105
- self.contracts_db = {}
106
-
107
- def get_contracts_db(self) -> Dict[str, Any]:
108
- return self.contracts_db
109
-
110
- # ==================================================================
111
- # 🛡️ Layer 1: Regime-Adaptive Screening (The Core Update)
112
  # ==================================================================
113
  async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
114
  """
115
- تقوم باختيار استراتيجية المسح بناءً على حالة السوق الحالية.
 
 
 
 
116
  """
117
- # 1. تحديد الحالة من الدستور
118
- # ملاحظة: AdaptiveHub هو المسؤول عن تحديث هذه القيمة في SystemLimits
119
  current_regime = getattr(SystemLimits, "CURRENT_REGIME", "RANGE")
 
120
  min_score = getattr(SystemLimits, "L1_MIN_AFFINITY_SCORE", 15.0)
121
-
122
- print(f"🔍 [Layer 1] Screening Mode: {current_regime} | Min Score: {min_score}")
123
-
124
- # 2. جلب الكون الأولي (Universe) - استخدام الفلتر الأساسي الكامل
125
- all_tickers = await self._stage0_universe_filter()
126
- if not all_tickers:
127
- print("⚠️ [Layer 1] No tickers passed Stage 0.")
128
- return []
129
 
130
- # 3. توجيه المسح حسب الاستراتيجية
131
- candidates = []
 
 
 
132
 
133
- if current_regime == "BULL":
134
- candidates = self._pre_filter_bull(all_tickers)
135
- elif current_regime == "BEAR":
136
- candidates = self._pre_filter_bear(all_tickers)
137
- elif current_regime == "DEAD":
138
- candidates = self._pre_filter_dead(all_tickers)
139
- else: # RANGE or Default
140
- candidates = self._pre_filter_range(all_tickers)
141
-
142
- print(f" -> Pre-filter selected {len(candidates)} candidates for Deep Scan.")
143
 
144
- # 4. الجلب العميق للبيانات (Deep Data Fetch)
145
- # نأخذ أفضل 60 عملة فقط لتوفير الموارد
146
- top_candidates = candidates[:60]
147
- enriched_data = await self._fetch_technical_and_depth_batch(top_candidates)
148
 
149
- if not enriched_data:
150
- print("❌ [Layer 1] Failed to fetch deep data.")
151
- return []
152
-
153
- # 5. حساب النقاط (Synergy Score) بناءً على الحالة
154
- final_selection = []
155
  for item in enriched_data:
156
- synergy = self._calculate_adaptive_score(item, current_regime)
157
- item['l1_score'] = synergy['score']
158
- item['tags'] = synergy['tags']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
- if item['l1_score'] >= min_score:
161
- final_selection.append(item)
 
 
 
162
 
163
- # ترتيب نهائي وتنظيف
164
- final_selection.sort(key=lambda x: x['l1_score'], reverse=True)
165
-
166
- # طباعة عي��ة للمراقبة
167
- if final_selection:
168
- print(f" -> Top pick: {final_selection[0]['symbol']} (Score: {final_selection[0]['l1_score']:.1f})")
169
- else:
170
- print("⚠️ [Layer 1] No candidates passed the adaptive score threshold.")
171
 
172
- # نمرر أفضل 40 للمعالج
 
 
 
173
  return [
174
  {
175
  'symbol': c['symbol'],
176
- 'quote_volume': c.get('quote_volume', 0),
177
- 'current_price': c.get('current_price', 0),
178
- 'type': ','.join(c.get('tags', [])),
179
- 'l1_score': c.get('l1_score', 0)
180
  }
181
- for c in final_selection[:40]
182
  ]
183
 
184
- # ==================================================================
185
- # 🌍 Stage 0 & Pre-Filters
186
- # ==================================================================
187
- async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
188
- """
189
- فلتر أولي سريع لاستبعاد العملات الميتة أو المحظورة.
190
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  try:
192
  tickers = await self.exchange.fetch_tickers()
193
  candidates = []
194
-
195
  for symbol, ticker in tickers.items():
196
  if not symbol.endswith('/USDT'): continue
197
-
198
- base_curr = symbol.split('/')[0]
199
- if any(bad in base_curr for bad in self.BLACKLIST_TOKENS): continue
200
-
201
- quote_vol = ticker.get('quoteVolume')
202
- if not quote_vol or quote_vol < 300_000: continue # Min 300k USDT
203
-
204
- last_price = ticker.get('last')
205
- if not last_price or last_price < 0.0001: continue
206
-
207
  candidates.append({
208
  'symbol': symbol,
209
- 'quote_volume': quote_vol,
210
- 'current_price': last_price,
211
- 'change_24h': float(ticker.get('percentage', 0.0))
212
  })
213
-
214
  return candidates
215
- except Exception as e:
216
- print(f"❌ [L1 Error] Universe filter failed: {e}")
217
- return []
218
-
219
- def _pre_filter_bull(self, tickers):
220
- """🐂 Bull Mode: السيولة العالية والارتفاع في السعر"""
221
- filtered = [t for t in tickers if t['change_24h'] > -2.0 and t['quote_volume'] > 500_000]
222
- filtered.sort(key=lambda x: (x['change_24h'], x['quote_volume']), reverse=True)
223
- return filtered
224
-
225
- def _pre_filter_bear(self, tickers):
226
- """🐻 Bear Mode: الارتدادات من القاع (Panic Selling)"""
227
- filtered = [t for t in tickers if t['change_24h'] < -5.0 and t['quote_volume'] > 1_000_000]
228
- filtered.sort(key=lambda x: x['change_24h'], reverse=False) # تصاعدي (الأكثر سلبية)
229
- return filtered
230
-
231
- def _pre_filter_range(self, tickers):
232
- """↔️ Range Mode: الاستقرار والسيولة المتوسطة"""
233
- filtered = [t for t in tickers if -5.0 < t['change_24h'] < 5.0 and t['quote_volume'] > 300_000]
234
- filtered.sort(key=lambda x: x['quote_volume'], reverse=True)
235
- return filtered
236
-
237
- def _pre_filter_dead(self, tickers):
238
- """💤 Dead/Accumulation Mode: سيولة منخفضة وتحرك مفاجئ"""
239
- filtered = [t for t in tickers if t['quote_volume'] > 100_000]
240
- import random
241
- random.shuffle(filtered) # عشوائية لاستكشاف الجواهر
242
- return filtered
243
-
244
- # ==================================================================
245
- # 🧠 Adaptive Scoring Matrix
246
- # ==================================================================
247
- def _calculate_adaptive_score(self, item: Dict[str, Any], regime: str) -> Dict[str, Any]:
248
- """
249
- حساب النقاط بناءً على السياق (Regime-Specific Scoring).
250
- """
251
- try:
252
- df = pd.DataFrame(item['ohlcv_1h'], columns=['ts', 'o', 'h', 'l', 'c', 'v'])
253
- df['c'] = df['c'].astype(float)
254
-
255
- # المؤشرات الأساسية
256
- curr_close = df['c'].iloc[-1]
257
- rsi = ta.rsi(df['c'], 14).iloc[-1]
258
- ema50 = ta.ema(df['c'], 50).iloc[-1]
259
-
260
- score = 0.0
261
- tags = []
262
-
263
- # 🐂 BULL LOGIC
264
- if regime == "BULL":
265
- # نحب الـ RSI العالي (زخم) لكن ليس المفرط جداً
266
- if 55 < rsi < 80: score += 20
267
- if curr_close > ema50: score += 20
268
- tags.append("TrendFollow")
269
-
270
- # 🐻 BEAR LOGIC
271
- elif regime == "BEAR":
272
- # نحب الـ RSI المنخفض (تشبع بيعي)
273
- if rsi < 30:
274
- score += 30
275
- tags.append("Oversold")
276
- elif rsi > 60:
277
- score -= 20 # خطر
278
-
279
- # في السوق الهابط، السعر غالباً تحت EMA، نبحث عن الابتعاد الشديد عنه
280
- dist = (curr_close - ema50) / ema50
281
- if dist < -0.15:
282
- score += 15
283
- tags.append("DeepValue")
284
-
285
- # ↔️ RANGE LOGIC
286
- elif regime == "RANGE":
287
- # نحب الـ RSI في المنتصف للارتداد
288
- if rsi < 35:
289
- score += 25; tags.append("RangeBot")
290
- elif rsi > 65:
291
- score -= 10; tags.append("RangeTop")
292
- else:
293
- score += 10 # منطقة آمنة
294
-
295
- # دفتر الطلبات (مشترك لكن بأوزان مختلفة ضمنياً)
296
- ob = item.get('order_book_snapshot', {})
297
- if ob:
298
- bids = sum([float(x[1]) for x in ob.get('bids', [])[:10]])
299
- asks = sum([float(x[1]) for x in ob.get('asks', [])[:10]])
300
- if asks > 0:
301
- ratio = bids / asks
302
- if ratio > 1.5: score += 15; tags.append("BidWall")
303
- if regime == "BEAR" and ratio > 2.0: score += 20 # جدار الشراء في الهبوط إشارة قوية
304
-
305
- return {'score': score, 'tags': tags}
306
-
307
- except Exception:
308
- return {'score': 0, 'tags': ['Error']}
309
 
310
- # ==================================================================
311
- # ⚡ Batch Fetching Utilities (Parallel Execution)
312
- # ==================================================================
313
- async def _fetch_technical_and_depth_batch(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
314
- """جلب البيانات الفنية + دفتر الطلبات بالتوازي"""
315
- chunk_size = 10
316
  results = []
317
-
318
  for i in range(0, len(candidates), chunk_size):
319
- chunk = candidates[i:i + chunk_size]
320
- chunk_tasks = [self._fetch_single_full_data(c) for c in chunk]
321
- chunk_results = await asyncio.gather(*chunk_tasks)
322
- results.extend([r for r in chunk_results if r is not None])
323
- await asyncio.sleep(0.1)
324
-
325
  return results
326
 
327
- async def _fetch_single_full_data(self, candidate: Dict[str, Any]) -> Any:
328
- """جلب بيانات عملة واحدة (شارت + عمق)"""
329
- symbol = candidate['symbol']
330
  try:
331
- ohlcv_task = self.exchange.fetch_ohlcv(symbol, '1h', limit=100)
332
- ob_task = self.exchange.fetch_order_book(symbol, limit=20)
333
-
334
- ohlcv_1h, order_book = await asyncio.gather(ohlcv_task, ob_task)
335
-
336
- if not ohlcv_1h or len(ohlcv_1h) < 50:
337
- return None
338
-
339
- candidate['ohlcv_1h'] = ohlcv_1h
340
- candidate['order_book_snapshot'] = order_book
341
  return candidate
342
-
343
- except Exception:
344
- return None
345
-
346
- # ==================================================================
347
- # 🎯 Public Helpers
348
- # ==================================================================
349
- async def get_latest_price_async(self, symbol: str) -> float:
350
- try:
351
- ticker = await self.exchange.fetch_ticker(symbol)
352
- return float(ticker['last'])
353
- except Exception: return 0.0
354
-
355
- async def get_latest_ohlcv(self, symbol: str, timeframe: str = '5m', limit: int = 100) -> List[List[float]]:
356
- try:
357
- candles = await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
358
- return candles or []
359
- except Exception: return []
360
-
361
- async def get_order_book_snapshot(self, symbol: str, limit: int = 20) -> Dict[str, Any]:
362
- try:
363
- ob = await self.exchange.fetch_order_book(symbol, limit)
364
- return ob
365
- except Exception: return {}
366
-
367
- def get_supported_timeframes(self):
368
- return list(self.exchange.timeframes.keys()) if self.exchange else []
 
1
  # ============================================================
2
  # 📂 ml_engine/data_manager.py
3
+ # (V40.0 - GEM-Architect: The Scanner Matrix)
4
  # ============================================================
5
 
6
  import asyncio
 
14
 
15
  import ccxt.async_support as ccxt
16
 
 
17
  try:
18
  from ml_engine.processor import SystemLimits
19
  except ImportError:
 
20
  class SystemLimits:
21
  L1_MIN_AFFINITY_SCORE = 15.0
22
  CURRENT_REGIME = "RANGE"
23
+ # أوزان الكاشفات الجديدة (تتغير بالباكتست)
24
+ SCANNER_WEIGHTS = {
25
+ "RSI_MOMENTUM": 0.3,
26
+ "BB_BREAKOUT": 0.3,
27
+ "MACD_CROSS": 0.2,
28
+ "VOLUME_FLOW": 0.2
29
+ }
30
 
 
31
  logging.getLogger("httpx").setLevel(logging.WARNING)
 
32
  logging.getLogger("ccxt").setLevel(logging.WARNING)
33
 
34
  class DataManager:
35
  """
36
+ DataManager V40.0 (The Scanner Matrix)
37
+ - L1 Screening uses a multi-strategy ensemble approach.
38
+ - Optimized for speed using batch processing.
39
  """
40
 
41
  def __init__(self, contracts_db, whale_monitor, r2_service=None):
 
 
 
42
  self.contracts_db = contracts_db or {}
43
  self.whale_monitor = whale_monitor
44
  self.r2_service = r2_service
 
 
45
  self.exchange = ccxt.kucoin({
46
  'enableRateLimit': True,
47
  'timeout': 30000,
48
  'options': {'defaultType': 'spot'}
49
  })
 
50
  self.http_client = None
51
+ self.BLACKLIST_TOKENS = ['USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L']
52
+ print(f"📦 [DataManager V40.0] Scanner Matrix Online.")
 
 
 
 
 
 
 
53
 
54
  async def initialize(self):
55
+ self.http_client = httpx.AsyncClient(timeout=60.0)
56
+ await self._load_markets()
 
 
 
 
 
 
 
57
 
58
  async def _load_markets(self):
59
+ if self.exchange and not self.exchange.markets:
60
+ await self.exchange.load_markets()
 
 
 
 
 
 
 
 
61
 
62
  async def close(self):
63
+ if self.http_client: await self.http_client.aclose()
64
+ if self.exchange: await self.exchange.close()
 
 
 
 
65
 
66
  # ==================================================================
67
+ # 🛡️ Layer 1: The Scanner Matrix (New Logic)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  # ==================================================================
69
  async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
70
  """
71
+ تنفيذ الفحص المتعدد (Matrix Scan).
72
+ 1. جلب أفضل 80 عملة سيولة.
73
+ 2. جلب شموع 15m لهذه العملات.
74
+ 3. تطبيق 4 استراتيجيات كشف مختلفة.
75
+ 4. حساب النتيجة الموزونة.
76
  """
 
 
77
  current_regime = getattr(SystemLimits, "CURRENT_REGIME", "RANGE")
78
+ scanner_weights = getattr(SystemLimits, "SCANNER_WEIGHTS", {"RSI_MOMENTUM": 1.0})
79
  min_score = getattr(SystemLimits, "L1_MIN_AFFINITY_SCORE", 15.0)
 
 
 
 
 
 
 
 
80
 
81
+ print(f"🔍 [L1 Matrix] Regime: {current_regime} | Weights: {scanner_weights}")
82
+
83
+ # 1. تصفية الكون الأولي (High Volume Universe)
84
+ tickers = await self._fetch_universe_tickers()
85
+ if not tickers: return []
86
 
87
+ # نأخذ أفضل 80 عملة فقط لتجنب قتل الـ API Rate Limits
88
+ top_candidates = tickers[:80]
 
 
 
 
 
 
 
 
89
 
90
+ # 2. جلب البيانات الفنية دفعة واحدة (Batch Fetch 15m)
91
+ enriched_data = await self._batch_fetch_ta_data(top_candidates, timeframe='15m', limit=100)
 
 
92
 
93
+ scored_candidates = []
 
 
 
 
 
94
  for item in enriched_data:
95
+ df = item.get('df')
96
+ if df is None or len(df) < 50: continue
97
+
98
+ # 3. تطبيق الكاشفات (Scanners)
99
+ scores = self._apply_scanner_strategies(df)
100
+
101
+ # 4. حساب النتيجة النهائية الموزونة
102
+ final_score = 0.0
103
+ tags = []
104
+
105
+ for strategy, val in scores.items():
106
+ w = scanner_weights.get(strategy, 0.0)
107
+ final_score += (val['score'] * w)
108
+ if val['active']: tags.append(strategy)
109
+
110
+ # إضافة نقاط إضافية بناءً على النظام القديم (Volume/Price Action)
111
+ # للحفاظ على التوافقية
112
+ if item['change_24h'] > 5.0 and current_regime == "BULL": final_score += 10
113
 
114
+ item['l1_score'] = final_score
115
+ item['tags'] = tags
116
+
117
+ if final_score >= min_score:
118
+ scored_candidates.append(item)
119
 
120
+ scored_candidates.sort(key=lambda x: x['l1_score'], reverse=True)
 
 
 
 
 
 
 
121
 
122
+ print(f" -> Matrix selected {len(scored_candidates)} candidates.")
123
+ if scored_candidates:
124
+ print(f" -> Top Pick: {scored_candidates[0]['symbol']} (Score: {scored_candidates[0]['l1_score']:.1f})")
125
+
126
  return [
127
  {
128
  'symbol': c['symbol'],
129
+ 'quote_volume': c['quote_volume'],
130
+ 'current_price': c['current_price'],
131
+ 'type': ','.join(c['tags']),
132
+ 'l1_score': c['l1_score']
133
  }
134
+ for c in scored_candidates[:40] # تمرير أفضل 40
135
  ]
136
 
137
+ # ------------------------------------------------------------------
138
+ # 🧩 Scanner Strategies Logic
139
+ # ------------------------------------------------------------------
140
+ def _apply_scanner_strategies(self, df: pd.DataFrame) -> Dict[str, Any]:
141
+ """تطبيق مؤشرات فنية متعددة على البيانات"""
142
+ results = {}
143
+ close = df['close']
144
+
145
+ # Strategy A: RSI Momentum (زخم)
146
+ rsi = ta.rsi(close, length=14)
147
+ curr_rsi = rsi.iloc[-1]
148
+ score_rsi = 0
149
+ active_rsi = False
150
+ if 50 < curr_rsi < 75:
151
+ score_rsi = 100
152
+ active_rsi = True
153
+ elif curr_rsi <= 30: # Oversold Bounce
154
+ score_rsi = 80
155
+ active_rsi = True
156
+ results["RSI_MOMENTUM"] = {'score': score_rsi, 'active': active_rsi}
157
+
158
+ # Strategy B: Bollinger Band Breakout (انفجار سعري)
159
+ bb = ta.bbands(close, length=20, std=2)
160
+ upper = bb['BBU_20_2.0'].iloc[-1]
161
+ width = bb['BBB_20_2.0'].iloc[-1]
162
+ curr_price = close.iloc[-1]
163
+ score_bb = 0
164
+ active_bb = False
165
+ if curr_price > upper and width > 0.1: # اختراق حقيقي
166
+ score_bb = 100
167
+ active_bb = True
168
+ results["BB_BREAKOUT"] = {'score': score_bb, 'active': active_bb}
169
+
170
+ # Strategy C: MACD Cross (تغير اتجاه)
171
+ macd = ta.macd(close)
172
+ macd_line = macd['MACD_12_26_9'].iloc[-1]
173
+ signal_line = macd['MACDS_12_26_9'].iloc[-1]
174
+ hist = macd['MACDh_12_26_9'].iloc[-1]
175
+ score_macd = 0
176
+ active_macd = False
177
+ if macd_line > signal_line and hist > 0:
178
+ score_macd = 100
179
+ active_macd = True
180
+ results["MACD_CROSS"] = {'score': score_macd, 'active': active_macd}
181
+
182
+ # Strategy D: Volume Flow (تدفق سيولة)
183
+ vol = df['volume']
184
+ vol_ma = ta.sma(vol, length=20).iloc[-1]
185
+ curr_vol = vol.iloc[-1]
186
+ score_vol = 0
187
+ active_vol = False
188
+ if curr_vol > (vol_ma * 1.5): # حجم تداول ضخم مفاجئ
189
+ score_vol = 100
190
+ active_vol = True
191
+ results["VOLUME_FLOW"] = {'score': score_vol, 'active': active_vol}
192
+
193
+ return results
194
+
195
+ # ------------------------------------------------------------------
196
+ # ⚡ Batch & Async Helpers
197
+ # ------------------------------------------------------------------
198
+ async def _fetch_universe_tickers(self):
199
  try:
200
  tickers = await self.exchange.fetch_tickers()
201
  candidates = []
 
202
  for symbol, ticker in tickers.items():
203
  if not symbol.endswith('/USDT'): continue
204
+ if any(bad in symbol for bad in self.BLACKLIST_TOKENS): continue
205
+ if not ticker.get('quoteVolume') or ticker['quoteVolume'] < 500_000: continue # Min 500k Vol
 
 
 
 
 
 
 
 
206
  candidates.append({
207
  'symbol': symbol,
208
+ 'quote_volume': ticker['quoteVolume'],
209
+ 'current_price': ticker['last'],
210
+ 'change_24h': float(ticker.get('percentage', 0.0))
211
  })
212
+ candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
213
  return candidates
214
+ except Exception: return []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
 
216
+ async def _batch_fetch_ta_data(self, candidates, timeframe='15m', limit=100):
 
 
 
 
 
217
  results = []
218
+ chunk_size = 15 # لعدم تجاوز الحدود
219
  for i in range(0, len(candidates), chunk_size):
220
+ chunk = candidates[i:i+chunk_size]
221
+ tasks = [self._fetch_ohlcv_safe(c, timeframe, limit) for c in chunk]
222
+ chunk_res = await asyncio.gather(*tasks)
223
+ results.extend([r for r in chunk_res if r is not None])
224
+ await asyncio.sleep(0.1)
 
225
  return results
226
 
227
+ async def _fetch_ohlcv_safe(self, candidate, tf, limit):
 
 
228
  try:
229
+ ohlcv = await self.exchange.fetch_ohlcv(candidate['symbol'], tf, limit=limit)
230
+ if not ohlcv: return None
231
+ df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
232
+ df['close'] = df['close'].astype(float)
233
+ df['volume'] = df['volume'].astype(float)
234
+ candidate['df'] = df
 
 
 
 
235
  return candidate
236
+ except: return None
237
+
238
+ # Helpers needed for Processor/TradeManager
239
+ async def get_latest_price_async(self, symbol):
240
+ t = await self.exchange.fetch_ticker(symbol)
241
+ return float(t['last'])
242
+
243
+ async def get_latest_ohlcv(self, symbol, tf, limit=100):
244
+ return await self.exchange.fetch_ohlcv(symbol, tf, limit=limit)
245
+
246
+ async def get_order_book_snapshot(self, symbol, limit=20):
247
+ return await self.exchange.fetch_order_book(symbol, limit)
248
+
249
+ # R2 Placeholder
250
+ async def load_contracts_from_r2(self): pass
251
+ def get_contracts_db(self): return self.contracts_db