Riy777 commited on
Commit
164b380
·
1 Parent(s): f3a103c

Update data_manager.py

Browse files
Files changed (1) hide show
  1. data_manager.py +191 -382
data_manager.py CHANGED
@@ -1,4 +1,4 @@
1
- # data_manager.py (Updated to V6.2 - Smart Stablecoin Filter)
2
  import os
3
  import asyncio
4
  import httpx
@@ -9,6 +9,16 @@ import ccxt
9
  import numpy as np
10
  import logging
11
  from typing import List, Dict, Any
 
 
 
 
 
 
 
 
 
 
12
 
13
  logging.getLogger("httpx").setLevel(logging.WARNING)
14
  logging.getLogger("httpcore").setLevel(logging.WARNING)
@@ -37,7 +47,7 @@ class DataManager:
37
  async def initialize(self):
38
  self.http_client = httpx.AsyncClient(timeout=30.0)
39
  await self._load_markets()
40
- print("✅ DataManager initialized - Efficient Volume-Based Screening")
41
 
42
  async def _load_markets(self):
43
  try:
@@ -54,8 +64,7 @@ class DataManager:
54
  print(f"❌ فشل تحميل بيانات الأسواق: {e}")
55
 
56
  async def close(self):
57
- # 🔴 --- START OF CHANGE --- 🔴
58
- # (إصلاح تسريب الموارد)
59
  if self.http_client and not self.http_client.is_closed:
60
  await self.http_client.aclose()
61
  print(" ✅ DataManager: http_client closed.")
@@ -66,8 +75,8 @@ class DataManager:
66
  print(" ✅ DataManager: ccxt.kucoin exchange closed.")
67
  except Exception as e:
68
  print(f" ⚠️ DataManager: Error closing ccxt.kucoin: {e}")
69
- # 🔴 --- END OF CHANGE --- 🔴
70
-
71
  async def get_market_context_async(self):
72
  """جلب سياق السوق الأساسي فقط"""
73
  try:
@@ -225,48 +234,170 @@ class DataManager:
225
  'data_quality': 'LOW'
226
  }
227
 
 
 
228
  async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
229
  """
230
- الطبقة 1: فحص سريع - جلب أفضل 200 عملة حسب الحجم مباشرة
 
 
 
 
231
  """
232
- print("📊 الطبقة 1: جلب أفضل 200 عملة حسب حجم التداول...")
233
-
234
- # المحاولة 1: الطريقة المثلى - استخدام fetch_tickers
235
- volume_data = await self._get_volume_data_optimal()
236
 
 
 
237
  if not volume_data:
238
- # المحاولة 2: الطريقة البديلة - استخدام API المباشر
239
  volume_data = await self._get_volume_data_direct_api()
240
 
241
  if not volume_data:
242
- # المحاولة 3: الطريقة التقليدية (الاحتياطية)
243
- volume_data = await self._get_volume_data_traditional()
244
-
245
- if not volume_data:
246
- print("❌ فشل جميع محاولات جلب بيانات الأحجام")
247
  return []
248
 
249
- # أخذ أفضل 200 عملة حسب الحجم فقط
250
  volume_data.sort(key=lambda x: x['dollar_volume'], reverse=True)
251
- top_200_by_volume = volume_data[:200]
252
 
253
- print(f"✅ تم اختيار أفضل {len(top_200_by_volume)} عملة حسب الحجم")
254
 
255
- # المرحلة 2: تطبيق المؤشرات الأخرى على الـ200 فقط
256
- final_candidates = await self._apply_advanced_indicators(top_200_by_volume)
257
 
258
- print(f"🎯 تم تحليل {len(final_candidates)} عملة للطبقة 2")
259
-
260
- # عرض أفضل 15 عملة
261
- print("🏆 أفضل 15 عملة من الطبقة 1:")
262
- for i, candidate in enumerate(final_candidates[:15]):
263
- score = candidate.get('layer1_score', 0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  volume = candidate.get('dollar_volume', 0)
265
- change = candidate.get('price_change_24h', 0)
266
- print(f" {i+1:2d}. {candidate['symbol']}: {score:.3f} | ${volume:>10,.0f} | {change:>+6.1f}%")
267
 
268
  return final_candidates
269
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
270
  async def _get_volume_data_optimal(self) -> List[Dict[str, Any]]:
271
  """الطريقة المثلى: استخدام fetch_tickers لجميع البيانات مرة واحدة"""
272
  try:
@@ -279,35 +410,26 @@ class DataManager:
279
  processed = 0
280
 
281
  for symbol, ticker in tickers.items():
282
- # تصفية أزواج USDT النشطة فقط
283
- if not symbol.endswith('/USDT'):
284
- continue
285
-
286
- if not ticker.get('active', True):
287
  continue
288
 
289
- # استخدام quoteVolume (الحجم بالدولار) إذا متوفر
290
  current_price = ticker.get('last', 0)
291
  quote_volume = ticker.get('quoteVolume', 0)
292
 
293
- # ✅ الإصلاح: التحقق من القيم قبل العمليات الحسابية
294
  if current_price is None or current_price <= 0:
295
  continue
296
 
297
  if quote_volume is not None and quote_volume > 0:
298
  dollar_volume = quote_volume
299
  else:
300
- # fallback: baseVolume * السعر
301
  base_volume = ticker.get('baseVolume', 0)
302
  if base_volume is None:
303
  continue
304
  dollar_volume = base_volume * current_price
305
 
306
- # الإصلاح: التحقق من أن dollar_volume قيمة صالحة
307
- if dollar_volume is None or dollar_volume < 50000: # أقل من 50K دولار
308
  continue
309
 
310
- # ✅ الإصلاح: التحقق من أن price_change_24h قيمة صالحة
311
  price_change_24h = (ticker.get('percentage', 0) or 0) * 100
312
  if price_change_24h is None:
313
  price_change_24h = 0
@@ -322,7 +444,7 @@ class DataManager:
322
 
323
  processed += 1
324
 
325
- print(f"✅ تم معالجة {processed} عملة في الطريقة المثلى")
326
  return volume_data
327
 
328
  except Exception as e:
@@ -331,7 +453,7 @@ class DataManager:
331
  return []
332
 
333
  async def _get_volume_data_direct_api(self) -> List[Dict[str, Any]]:
334
- """الطريقة الثانية: استخدام KuCoin API مباشرة"""
335
  try:
336
  url = "https://api.kucoin.com/api/v1/market/allTickers"
337
 
@@ -349,15 +471,12 @@ class DataManager:
349
  for ticker in tickers:
350
  symbol = ticker['symbol']
351
 
352
- # تصفية أزواج USDT فقط
353
  if not symbol.endswith('USDT'):
354
  continue
355
 
356
- # تحويل الرمز للتنسيق القياسي (BTC-USDT → BTC/USDT)
357
  formatted_symbol = symbol.replace('-', '/')
358
 
359
  try:
360
- # ✅ الإصلاح: التحقق من القيم قبل التحويل
361
  vol_value = ticker.get('volValue')
362
  last_price = ticker.get('last')
363
  change_rate = ticker.get('changeRate')
@@ -366,7 +485,6 @@ class DataManager:
366
  if vol_value is None or last_price is None or change_rate is None or vol is None:
367
  continue
368
 
369
- # ✅ الإصلاح: استخدام القيم الافتراضية للتحويل
370
  dollar_volume = float(vol_value) if vol_value else 0
371
  current_price = float(last_price) if last_price else 0
372
  price_change = (float(change_rate) * 100) if change_rate else 0
@@ -383,7 +501,7 @@ class DataManager:
383
  except (ValueError, TypeError, KeyError) as e:
384
  continue
385
 
386
- print(f"✅ تم معالجة {len(volume_data)} عملة في الطريقة المباشرة")
387
  return volume_data
388
 
389
  except Exception as e:
@@ -391,277 +509,26 @@ class DataManager:
391
  traceback.print_exc()
392
  return []
393
 
394
- async def _get_volume_data_traditional(self) -> List[Dict[str, Any]]:
395
- """الطريقة التقليدية: جلب كل رمز على حدة (الاحتياطي)"""
396
- try:
397
- if not self.exchange or not self.market_cache:
398
- return []
399
-
400
- usdt_symbols = [
401
- symbol for symbol in self.market_cache.keys()
402
- if symbol.endswith('/USDT') and self.market_cache[symbol].get('active', False)
403
- ]
404
-
405
- volume_data = []
406
- processed = 0
407
-
408
- # معالجة دفعات لتجنب rate limits
409
- batch_size = 20 # تقليل حجم الدفعة لتحسين الاستقرار
410
- for i in range(0, len(usdt_symbols), batch_size):
411
- batch = usdt_symbols[i:i + batch_size]
412
- batch_tasks = [self._process_single_symbol(sym) for sym in batch]
413
- batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True)
414
-
415
- for result in batch_results:
416
- if isinstance(result, dict):
417
- volume_data.append(result)
418
-
419
- processed += len(batch)
420
-
421
- # انتظار قصير بين الدفعات
422
- if i + batch_size < len(usdt_symbols):
423
- await asyncio.sleep(1)
424
-
425
- print(f"✅ تم معالجة {len(volume_data)} عملة في الطريقة التقليدية")
426
- return volume_data
427
-
428
- except Exception as e:
429
- print(f"❌ خطأ في جلب بيانات الحجم التقليدية: {e}")
430
- traceback.print_exc()
431
- return []
432
-
433
- async def _process_single_symbol(self, symbol: str) -> Dict[str, Any]:
434
- """معالجة رمز واحد لجلب بيانات الحجم"""
435
- try:
436
- ticker = self.exchange.fetch_ticker(symbol)
437
- if not ticker:
438
- return None
439
-
440
- current_price = ticker.get('last', 0)
441
- quote_volume = ticker.get('quoteVolume', 0)
442
-
443
- # ✅ الإصلاح: التحقق من القيم قبل العمليات الحسابية
444
- if current_price is None or current_price <= 0:
445
- return None
446
-
447
- if quote_volume is not None and quote_volume > 0:
448
- dollar_volume = quote_volume
449
- else:
450
- base_volume = ticker.get('baseVolume', 0)
451
- if base_volume is None:
452
- return None
453
- dollar_volume = base_volume * current_price
454
-
455
- if dollar_volume is None or dollar_volume < 50000:
456
- return None
457
-
458
- # ✅ الإصلاح: التحقق من أن price_change_24h قيمة صالحة
459
- price_change_24h = (ticker.get('percentage', 0) or 0) * 100
460
- if price_change_24h is None:
461
- price_change_24h = 0
462
-
463
- return {
464
- 'symbol': symbol,
465
- 'dollar_volume': dollar_volume,
466
- 'current_price': current_price,
467
- 'volume_24h': ticker.get('baseVolume', 0) or 0,
468
- 'price_change_24h': price_change_24h
469
- }
470
- except Exception:
471
- return None
472
-
473
- async def _apply_advanced_indicators(self, volume_data: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
474
- """تطبيق المؤشرات المتقدمة على أفضل العملات حسب الحجم"""
475
- candidates = []
476
-
477
- for i, symbol_data in enumerate(volume_data):
478
- try:
479
- symbol = symbol_data['symbol']
480
-
481
- # جلب بيانات إضافية للرمز
482
- detailed_data = await self._get_detailed_symbol_data(symbol)
483
- if not detailed_data:
484
- continue
485
-
486
- # دمج البيانات
487
- symbol_data.update(detailed_data)
488
-
489
- # حساب الدرجة النهائية
490
- score = self._calculate_advanced_score(symbol_data)
491
- symbol_data['layer1_score'] = score
492
-
493
- candidates.append(symbol_data)
494
-
495
- except Exception as e:
496
- continue
497
-
498
- # ترتيب المرشحين حسب الدرجة النهائية
499
- candidates.sort(key=lambda x: x.get('layer1_score', 0), reverse=True)
500
- return candidates
501
-
502
- async def _get_detailed_symbol_data(self, symbol: str) -> Dict[str, Any]:
503
- """جلب بيانات تفصيلية للرمز"""
504
- try:
505
- ticker = self.exchange.fetch_ticker(symbol)
506
- if not ticker:
507
- return None
508
-
509
- current_price = ticker.get('last', 0)
510
- high_24h = ticker.get('high', 0)
511
- low_24h = ticker.get('low', 0)
512
- open_price = ticker.get('open', 0)
513
- price_change_24h = (ticker.get('percentage', 0) or 0) * 100
514
-
515
- # ✅ الإصلاح: استخدام القيم الافتراضية للتحويل
516
- current_price = current_price or 0
517
- high_24h = high_24h or 0
518
- low_24h = low_24h or 0
519
- open_price = open_price or 0
520
- price_change_24h = price_change_24h or 0
521
-
522
- # حساب المؤشرات المتقدمة
523
- volatility = self._calculate_volatility(high_24h, low_24h, current_price)
524
- price_strength = self._calculate_price_strength(current_price, open_price, price_change_24h)
525
- momentum = self._calculate_momentum(price_change_24h)
526
-
527
- return {
528
- 'price_change_24h': price_change_24h,
529
- 'high_24h': high_24h,
530
- 'low_24h': low_24h,
531
- 'open_price': open_price,
532
- 'volatility': volatility,
533
- 'price_strength': price_strength,
534
- 'momentum': momentum,
535
- 'reasons': []
536
- }
537
-
538
- except Exception as e:
539
- return None
540
-
541
- def _calculate_advanced_score(self, symbol_data: Dict[str, Any]) -> float:
542
- """حساب درجة متقدمة تجمع بين الحجم والمؤشرات الأخرى"""
543
- dollar_volume = symbol_data.get('dollar_volume', 0)
544
- price_change = symbol_data.get('price_change_24h', 0)
545
- volatility = symbol_data.get('volatility', 0)
546
- price_strength = symbol_data.get('price_strength', 0)
547
- momentum = symbol_data.get('momentum', 0)
548
-
549
- # 1. درجة الحجم (40%) - الأهم
550
- volume_score = self._calculate_volume_score(dollar_volume)
551
-
552
- # 2. درجة الزخم (25%)
553
- momentum_score = momentum
554
-
555
- # 3. درجة التقلب (20%)
556
- volatility_score = self._calculate_volatility_score(volatility)
557
-
558
- # 4. درجة قوة السعر (15%)
559
- strength_score = price_strength
560
-
561
- # الدرجة النهائية
562
- final_score = (
563
- volume_score * 0.40 +
564
- momentum_score * 0.25 +
565
- volatility_score * 0.20 +
566
- strength_score * 0.15
567
- )
568
-
569
- # تحديث أسباب الترشيح
570
- reasons = []
571
- if volume_score >= 0.7:
572
- reasons.append('high_volume')
573
- if momentum_score >= 0.7:
574
- reasons.append('strong_momentum')
575
- if volatility_score >= 0.7:
576
- reasons.append('good_volatility')
577
-
578
- symbol_data['reasons'] = reasons
579
-
580
- return final_score
581
-
582
- def _calculate_volume_score(self, dollar_volume: float) -> float:
583
- """حساب درجة الحجم"""
584
- if dollar_volume >= 10000000: # 10M+
585
- return 1.0
586
- elif dollar_volume >= 5000000: # 5M+
587
- return 0.9
588
- elif dollar_volume >= 2000000: # 2M+
589
- return 0.8
590
- elif dollar_volume >= 1000000: # 1M+
591
- return 0.7
592
- elif dollar_volume >= 500000: # 500K+
593
- return 0.6
594
- elif dollar_volume >= 250000: # 250K+
595
- return 0.5
596
- elif dollar_volume >= 100000: # 100K+
597
- return 0.4
598
- else:
599
- return 0.3
600
-
601
- def _calculate_volatility(self, high_24h: float, low_24h: float, current_price: float) -> float:
602
- """حساب التقلب"""
603
- if current_price == 0:
604
- return 0
605
- return (high_24h - low_24h) / current_price
606
-
607
- # 🔴 --- START OF CHANGE (V6.2 - STABLECOIN FILTER) --- 🔴
608
- def _calculate_volatility_score(self, volatility: float) -> float:
609
- """(محدث V6.2) حساب درجة التقلب - معاقبة التقلب المنخفض جداً"""
610
- if 0.02 <= volatility <= 0.15: # تقلب مثالي 2%-15%
611
- return 1.0
612
- elif 0.01 <= volatility <= 0.20: # مقبول 1%-20%
613
- return 0.8
614
- elif volatility <= 0.01: # قليل جداً (عملة مستقرة؟)
615
- return 0.0 # (تغيير من 0.4 إلى 0.0)
616
- elif volatility > 0.20: # عالي جداً
617
- return 0.3
618
- else:
619
- return 0.5
620
- # 🔴 --- END OF CHANGE --- 🔴
621
-
622
- def _calculate_price_strength(self, current_price: float, open_price: float, price_change: float) -> float:
623
- """حساب قوة السعر"""
624
- if open_price == 0:
625
- return 0.5
626
-
627
- # قوة السعر تعتمد على المسافة من سعر الافتتاح ونسبة التغير
628
- distance_from_open = abs(current_price - open_price) / open_price
629
- change_strength = min(abs(price_change) / 50, 1.0)
630
-
631
- return (distance_from_open * 0.6 + change_strength * 0.4)
632
-
633
- # 🔴 --- START OF CHANGE (V6.2 - STABLECOIN FILTER) --- 🔴
634
- def _calculate_momentum(self, price_change: float) -> float:
635
- """(محدث V6.2) حساب الزخم - معاقبة التغيير شبه الصفري"""
636
-
637
- # (إضافة: معاقبة التغيير شبه الصفري)
638
- if abs(price_change) < 0.5: # أقل من 0.5%
639
- return 0.0
640
-
641
- if price_change >= 15: # +15%+
642
- return 1.0
643
- elif price_change >= 10: # +10%+
644
- return 0.9
645
- elif price_change >= 5: # +5%+
646
- return 0.8
647
- elif price_change >= 2: # +2%+
648
- return 0.7
649
- elif price_change >= 0.5: # موجب (تغيير من 0 إلى 0.5)
650
- return 0.6
651
- elif price_change >= -5: # حتى -5%
652
- return 0.5
653
- elif price_change >= -10: # حتى -10%
654
- return 0.4
655
- else: # أكثر من -10%
656
- return 0.3
657
- # 🔴 --- END OF CHANGE --- 🔴
658
-
659
- # (لا تغيير في دالة التدفق)
660
  async def stream_ohlcv_data(self, symbols: List[str], queue: asyncio.Queue):
661
  """
662
- (معدلة) جلب بيانات OHLCV بشكل متدفق وإرسالها إلى طابور
663
  """
664
- print(f"📊 بدء تدفق بيانات OHLCV لـ {len(symbols)} عملة...")
665
 
666
  batch_size = 15
667
  batches = [symbols[i:i + batch_size] for i in range(0, len(symbols), batch_size)]
@@ -678,7 +545,6 @@ class DataManager:
678
 
679
  batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True)
680
 
681
- # معالجة نتائج الدفعة
682
  successful_data_for_batch = []
683
  successful_count = 0
684
  for i, result in enumerate(batch_results):
@@ -695,8 +561,6 @@ class DataManager:
695
 
696
  print(f" 📦 [المنتج] اكتملت الدفعة {batch_num + 1}: {successful_count}/{len(batch)} ناجحة")
697
 
698
- # 🔴 الإرسال إلى الطابور
699
- # نرسل فقط إذا كانت هناك بيانات ناجحة في الدفعة
700
  if successful_data_for_batch:
701
  try:
702
  await queue.put(successful_data_for_batch)
@@ -705,11 +569,10 @@ class DataManager:
705
  except Exception as q_err:
706
  print(f" ❌ [المنتج] فشل إرسال الدفعة للطابور: {q_err}")
707
 
708
- # انتظار قصير بين الدفعات لتجنب rate limits
709
  if batch_num < len(batches) - 1:
710
  await asyncio.sleep(1)
711
 
712
- print(f"✅ [المنتج] اكتمل تدفق بيانات OHLCV. تم إرسال {total_successful} عملة للمعالجة.")
713
 
714
  try:
715
  await queue.put(None)
@@ -723,7 +586,6 @@ class DataManager:
723
  try:
724
  ohlcv_data = {}
725
 
726
- # جلب 200 شمعة لكل إطار زمني مع تحسين التعامل مع الأخطاء
727
  timeframes = [
728
  ('5m', 200),
729
  ('15m', 200),
@@ -733,54 +595,45 @@ class DataManager:
733
  ('1w', 200),
734
  ]
735
 
736
- # إنشاء مهام لجميع الإطارات الزمنية بشكل متوازي
737
  timeframe_tasks = []
738
  for timeframe, limit in timeframes:
739
  task = asyncio.create_task(self._fetch_single_timeframe_improved(symbol, timeframe, limit))
740
  timeframe_tasks.append(task)
741
 
742
- # انتظار جميع المهام
743
  timeframe_results = await asyncio.gather(*timeframe_tasks, return_exceptions=True)
744
 
745
- # تحسين: قبول البيانات حتى لو كانت غير مكتملة
746
  successful_timeframes = 0
747
- min_required_timeframes = 2 # تخفيف الشرط من 3 إلى 2 أطر زمنية
748
 
749
- # معالجة النتائج
750
  for i, (timeframe, limit) in enumerate(timeframes):
751
  result = timeframe_results[i]
752
 
753
  if isinstance(result, Exception):
754
  continue
755
 
756
- if result and len(result) >= 10: # تخفيف الشرط من 50 إلى 10 شموع
757
  ohlcv_data[timeframe] = result
758
  successful_timeframes += 1
759
 
760
- # تحسين: قبول العملة إذا كان لديها عدد كافٍ من الأطر الزمنية
761
  if successful_timeframes >= min_required_timeframes and ohlcv_data:
762
  try:
763
- # ✅ الحصول على السعر الحالي مباشرة
764
  current_price = await self.get_latest_price_async(symbol)
765
 
766
- # ✅ الإصلاح: إذا لم نتمكن من جلب السعر، نستخدم آخر سعر إغلاق
767
  if current_price is None:
768
- # البحث عن آخر سعر إغلاق من بيانات OHLCV
769
  for timeframe_data in ohlcv_data.values():
770
  if timeframe_data and len(timeframe_data) > 0:
771
  last_candle = timeframe_data[-1]
772
  if len(last_candle) >= 5:
773
- current_price = last_candle[4] # سعر الإغلاق
774
  break
775
 
776
  if current_price is None:
777
- print(f"❌ فشل جلب السعر لـ {symbol} من جميع المصادر")
778
  return None
779
 
780
  result_data = {
781
  'symbol': symbol,
782
  'ohlcv': ohlcv_data,
783
- 'raw_ohlcv': ohlcv_data, # ✅ إضافة البيانات الخام مباشرة
784
  'current_price': current_price,
785
  'timestamp': datetime.now().isoformat(),
786
  'candles_count': {tf: len(data) for tf, data in ohlcv_data.items()},
@@ -790,13 +643,11 @@ class DataManager:
790
  return result_data
791
 
792
  except Exception as price_error:
793
- print(f"❌ فشل جلب السعر لـ {symbol}: {price_error}")
794
  return None
795
  else:
796
  return None
797
 
798
  except Exception as e:
799
- print(f"❌ خطأ في جلب بيانات OHLCV لـ {symbol}: {e}")
800
  return None
801
 
802
  async def _fetch_single_timeframe_improved(self, symbol: str, timeframe: str, limit: int):
@@ -820,72 +671,30 @@ class DataManager:
820
  return []
821
 
822
  async def get_latest_price_async(self, symbol):
823
- """جلب السعر ��لحالي لعملة محددة - الإصلاح الرئيسي هنا"""
824
  try:
825
  if not self.exchange:
826
- print(f"❌ Exchange غير مهيأ لـ {symbol}")
827
  return None
828
 
829
- # ✅ الإصلاح الرئيسي: استخدام fetch_ticker مباشرة بدون asyncio.create_task
830
- # التأكد من أن symbol صالح للاستخدام
831
  if not symbol or '/' not in symbol:
832
- print(f"❌ رمز غير صالح: {symbol}")
833
  return None
834
 
835
- # ✅ الإصلاح: استخدام fetch_ticker بشكل متزامن (ليست async)
836
  ticker = self.exchange.fetch_ticker(symbol)
837
 
838
  if not ticker:
839
- print(f"❌ لم يتم العثور على ticker لـ {symbol}")
840
  return None
841
 
842
  current_price = ticker.get('last')
843
 
844
  if current_price is None:
845
- print(f"❌ لا يوجد سعر حالي في ticker لـ {symbol}")
846
  return None
847
 
848
  return float(current_price)
849
 
850
  except Exception as e:
851
- print(f"❌ فشل جلب السعر من KuCoin لـ {symbol}: {e}")
852
  return None
853
 
854
- async def get_available_symbols(self):
855
- """الحصول على جميع الرموز المتاحة"""
856
- try:
857
- if not self.exchange:
858
- return []
859
-
860
- if not self.market_cache:
861
- await self._load_markets()
862
-
863
- usdt_symbols = [
864
- symbol for symbol in self.market_cache.keys()
865
- if symbol.endswith('/USDT') and self.market_cache[symbol].get('active', False)
866
- ]
867
-
868
- return usdt_symbols
869
-
870
- except Exception as e:
871
- return []
872
-
873
- async def validate_symbol(self, symbol):
874
- """التحقق من صحة الرمز"""
875
- try:
876
- if not self.exchange:
877
- return False
878
-
879
- if not self.market_cache:
880
- await self._load_markets()
881
-
882
- return symbol in self.market_cache and self.market_cache[symbol].get('active', False)
883
-
884
- except Exception as e:
885
- return False
886
-
887
- # === الدوال الجديدة لدعم بيانات الحيتان ===
888
-
889
  async def get_whale_data_for_symbol(self, symbol):
890
  """جلب بيانات الحيتان لعملة محددة"""
891
  try:
@@ -917,4 +726,4 @@ class DataManager:
917
  'source': 'whale_analysis'
918
  }
919
 
920
- print("✅ DataManager loaded - (FIXED: Added stream_ohlcv_data 'None' terminator)")
 
1
+ # data_manager.py (Updated to V7.0 - Strategy Qualifier Screening)
2
  import os
3
  import asyncio
4
  import httpx
 
9
  import numpy as np
10
  import logging
11
  from typing import List, Dict, Any
12
+ import pandas as pd
13
+
14
+ # 🔴 --- START OF CHANGE (V7.0) --- 🔴
15
+ # (إضافة المكتبات اللازمة للغربلة المتقدمة)
16
+ try:
17
+ import pandas_ta as ta
18
+ except ImportError:
19
+ print("⚠️ مكتبة pandas_ta غير موجودة، فلتر الغربلة المتقدم سيفشل.")
20
+ ta = None
21
+ # 🔴 --- END OF CHANGE --- 🔴
22
 
23
  logging.getLogger("httpx").setLevel(logging.WARNING)
24
  logging.getLogger("httpcore").setLevel(logging.WARNING)
 
47
  async def initialize(self):
48
  self.http_client = httpx.AsyncClient(timeout=30.0)
49
  await self._load_markets()
50
+ print("✅ DataManager initialized - V7.0 (Strategy Qualifier Screening)")
51
 
52
  async def _load_markets(self):
53
  try:
 
64
  print(f"❌ فشل تحميل بيانات الأسواق: {e}")
65
 
66
  async def close(self):
67
+ # (إصلاح تسريب الموارد - لا تغيير هنا)
 
68
  if self.http_client and not self.http_client.is_closed:
69
  await self.http_client.aclose()
70
  print(" ✅ DataManager: http_client closed.")
 
75
  print(" ✅ DataManager: ccxt.kucoin exchange closed.")
76
  except Exception as e:
77
  print(f" ⚠️ DataManager: Error closing ccxt.kucoin: {e}")
78
+
79
+ # (الدوال المساعدة لسياق السوق وأسعار BTC/ETH تبقى كما هي - لا تغيير)
80
  async def get_market_context_async(self):
81
  """جلب سياق السوق الأساسي فقط"""
82
  try:
 
234
  'data_quality': 'LOW'
235
  }
236
 
237
+ # 🔴 --- START OF REFACTOR (V7.0 - Strategy Qualifier) --- 🔴
238
+
239
  async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
240
  """
241
+ الطبقة 1: فحص سريع - (محدث بالكامل V7.0)
242
+ 1. جلب أفضل 100 عملة حسب الحجم (24 ساعة).
243
+ 2. جلب 100 شمعة (1H) لهذه الـ 100 عملة.
244
+ 3. تطبيق "المؤهل الاستراتيجي" (زخم أو انعكاس).
245
+ 4. إرجاع العملات الناجحة فقط للطبقة 2.
246
  """
247
+ print("📊 الطبقة 1 (V7.0): بدء الغربلة الذكية (المؤهل الاستراتيجي)...")
 
 
 
248
 
249
+ # الخطوة 1: جلب أفضل 100 عملة حسب الحجم
250
+ volume_data = await self._get_volume_data_optimal() # (أو _get_volume_data_direct_api)
251
  if not volume_data:
 
252
  volume_data = await self._get_volume_data_direct_api()
253
 
254
  if not volume_data:
255
+ print("❌ فشل جلب بيانات الأحجام للطبقة 1")
 
 
 
 
256
  return []
257
 
 
258
  volume_data.sort(key=lambda x: x['dollar_volume'], reverse=True)
259
+ top_100_by_volume = volume_data[:100]
260
 
261
+ print(f"✅ تم تحديد أفضل {len(top_100_by_volume)} عملة حسب الحجم. بدء فلترة 1H...")
262
 
263
+ final_candidates = []
 
264
 
265
+ # (استخدام دفعات لمعالجة الـ 100 عملة بكفاءة)
266
+ batch_size = 20
267
+ for i in range(0, len(top_100_by_volume), batch_size):
268
+ batch_symbols_data = top_100_by_volume[i:i + batch_size]
269
+ batch_symbols = [s['symbol'] for s in batch_symbols_data]
270
+
271
+ print(f" 🔄 معالجة دفعة {int(i/batch_size) + 1}/{(len(top_100_by_volume) + batch_size - 1) // batch_size} ({len(batch_symbols)} عملة)...")
272
+
273
+ # الخطوة 2: جلب بيانات 1H بالتوازي
274
+ tasks = [self._fetch_1h_ohlcv_for_screening(symbol) for symbol in batch_symbols]
275
+ results = await asyncio.gather(*tasks, return_exceptions=True)
276
+
277
+ # الخطوة 3 و 4: تطبيق المؤهل الاستراتيجي
278
+ for j, (result_df) in enumerate(results):
279
+ symbol_data = batch_symbols_data[j]
280
+ symbol = symbol_data['symbol']
281
+
282
+ if isinstance(result_df, Exception) or result_df is None:
283
+ # print(f" - {symbol}: فشل جلب 1H")
284
+ continue
285
+
286
+ # حساب المؤشرات
287
+ indicators = self._calculate_screening_indicators(result_df)
288
+ if not indicators:
289
+ # print(f" - {symbol}: فشل حساب المؤشرات")
290
+ continue
291
+
292
+ # تطبيق المؤهل
293
+ is_qualified, hint = self._apply_strategy_qualifiers(indicators)
294
+
295
+ if is_qualified:
296
+ print(f" ✅ {symbol}: نجح (الملمح: {hint})")
297
+ # (إضافة البيانات الأولية + التلميح الاستراتيجي)
298
+ symbol_data['strategy_hint'] = hint
299
+ symbol_data['layer1_score'] = 0.5 # (الدرجة الأولية مجرد علامة نجاح)
300
+ symbol_data['reasons_for_candidacy'] = [f'QUALIFIED_{hint.upper()}']
301
+ final_candidates.append(symbol_data)
302
+ # else:
303
+ # print(f" - {symbol}: فشل في التأهيل")
304
+
305
+ print(f"🎯 اكتملت الغربلة (V7.0). تم تأهيل {len(final_candidates)} عملة من أصل 100 للطبقة 2.")
306
+
307
+ # عرض أفضل 15 عملة (إذا كان هناك عدد كافٍ)
308
+ print("🏆 المرشحون الناجحون:")
309
+ for k, candidate in enumerate(final_candidates[:15]):
310
+ hint = candidate.get('strategy_hint', 'N/A')
311
  volume = candidate.get('dollar_volume', 0)
312
+ print(f" {k+1:2d}. {candidate['symbol']}: (الملمح: {hint}) | ${volume:,.0f}")
 
313
 
314
  return final_candidates
315
 
316
+ async def _fetch_1h_ohlcv_for_screening(self, symbol: str) -> pd.DataFrame:
317
+ """(جديد V7.0) جلب 100 شمعة لإطار الساعة (1H) للغربلة السريعة"""
318
+ try:
319
+ # 100 شمعة كافية لحساب (RSI 14, EMA 21, MACD 26, BB 20)
320
+ ohlcv_data = self.exchange.fetch_ohlcv(symbol, '1h', limit=100)
321
+
322
+ if not ohlcv_data or len(ohlcv_data) < 50: # (الحد الأدنى 50)
323
+ return None
324
+
325
+ df = pd.DataFrame(ohlcv_data, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
326
+ df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float)
327
+ return df
328
+ except Exception:
329
+ return None
330
+
331
+ def _calculate_screening_indicators(self, df: pd.DataFrame) -> Dict[str, Any]:
332
+ """(جديد V7.0) حساب المؤشرات المحددة للغربلة من بيانات 1H"""
333
+ if ta is None or df is None or len(df) < 30: # (الحد الأدنى 30)
334
+ return None
335
+ try:
336
+ indicators = {}
337
+ close = df['close']
338
+
339
+ # حساب المؤشرات
340
+ indicators['rsi'] = ta.rsi(close, length=14).iloc[-1]
341
+ indicators['ema_9'] = ta.ema(close, length=9).iloc[-1]
342
+ indicators['ema_21'] = ta.ema(close, length=21).iloc[-1]
343
+
344
+ macd = ta.macd(close, fast=12, slow=26, signal=9)
345
+ indicators['macd_hist'] = macd['MACDh_12_26_9'].iloc[-1]
346
+
347
+ indicators['atr'] = ta.atr(df['high'], df['low'], close, length=14).iloc[-1]
348
+
349
+ bbands = ta.bbands(close, length=20, std=2)
350
+ indicators['bb_lower'] = bbands['BBL_20_2.0'].iloc[-1]
351
+
352
+ indicators['current_price'] = close.iloc[-1]
353
+
354
+ # التأكد من عدم وجود قيم NaN (فارغة)
355
+ if any(pd.isna(v) for v in indicators.values()):
356
+ return None
357
+
358
+ return indicators
359
+ except Exception:
360
+ return None
361
+
362
+ def _apply_strategy_qualifiers(self, indicators: Dict[str, Any]) -> (bool, str):
363
+ """(جديد V7.0) تطبيق ملامح التأهيل (زخم أو انعكاس)"""
364
+ if not indicators:
365
+ return (False, None)
366
+
367
+ try:
368
+ current_price = indicators['current_price']
369
+
370
+ # --- 1. ملمح الزخم (Momentum Profile) ---
371
+ # (نبحث عن قوة واتجاه صاعد وتسارع)
372
+ is_momentum = (
373
+ indicators['ema_9'] > indicators['ema_21'] and
374
+ indicators['rsi'] > 50 and # (في النصف القوي)
375
+ indicators['macd_hist'] > 0 and # (تسارع إيجابي)
376
+ indicators['atr'] > (current_price * 0.005) # (تقلب معقول، ليس عملة ميتة)
377
+ )
378
+ if is_momentum:
379
+ return (True, 'momentum')
380
+
381
+ # --- 2. ملمح الانعكاس (Reversion Profile) ---
382
+ # (نبحث عن بيع مبالغ فيه عند منطقة دعم محتملة)
383
+ is_reversion = (
384
+ indicators['rsi'] < 35 and # (ذروة بيع واضحة)
385
+ current_price < indicators['ema_21'] and # (بعيد عن المتوسط)
386
+ current_price <= (indicators['bb_lower'] * 1.005) # (عند أو قرب الحد السفلي لبولينجر)
387
+ )
388
+ if is_reversion:
389
+ return (True, 'reversion')
390
+
391
+ # --- 3. فشل في كلاهما ---
392
+ return (False, None)
393
+
394
+ except Exception:
395
+ return (False, None)
396
+
397
+ # 🔴 --- END OF REFACTOR (V7.0) --- 🔴
398
+
399
+
400
+ # (دوال جلب الحجم تبقى كما هي لأنها فعالة)
401
  async def _get_volume_data_optimal(self) -> List[Dict[str, Any]]:
402
  """الطريقة المثلى: استخدام fetch_tickers لجميع البيانات مرة واحدة"""
403
  try:
 
410
  processed = 0
411
 
412
  for symbol, ticker in tickers.items():
413
+ if not symbol.endswith('/USDT') or not ticker.get('active', True):
 
 
 
 
414
  continue
415
 
 
416
  current_price = ticker.get('last', 0)
417
  quote_volume = ticker.get('quoteVolume', 0)
418
 
 
419
  if current_price is None or current_price <= 0:
420
  continue
421
 
422
  if quote_volume is not None and quote_volume > 0:
423
  dollar_volume = quote_volume
424
  else:
 
425
  base_volume = ticker.get('baseVolume', 0)
426
  if base_volume is None:
427
  continue
428
  dollar_volume = base_volume * current_price
429
 
430
+ if dollar_volume is None or dollar_volume < 50000:
 
431
  continue
432
 
 
433
  price_change_24h = (ticker.get('percentage', 0) or 0) * 100
434
  if price_change_24h is None:
435
  price_change_24h = 0
 
444
 
445
  processed += 1
446
 
447
+ print(f"✅ تم معالجة {processed} عملة في الطريقة المثلى (لجلب الحجم)")
448
  return volume_data
449
 
450
  except Exception as e:
 
453
  return []
454
 
455
  async def _get_volume_data_direct_api(self) -> List[Dict[str, Any]]:
456
+ """الطريقة الثانية: استخدام KuCoin API مباشرة (احتياطي)"""
457
  try:
458
  url = "https://api.kucoin.com/api/v1/market/allTickers"
459
 
 
471
  for ticker in tickers:
472
  symbol = ticker['symbol']
473
 
 
474
  if not symbol.endswith('USDT'):
475
  continue
476
 
 
477
  formatted_symbol = symbol.replace('-', '/')
478
 
479
  try:
 
480
  vol_value = ticker.get('volValue')
481
  last_price = ticker.get('last')
482
  change_rate = ticker.get('changeRate')
 
485
  if vol_value is None or last_price is None or change_rate is None or vol is None:
486
  continue
487
 
 
488
  dollar_volume = float(vol_value) if vol_value else 0
489
  current_price = float(last_price) if last_price else 0
490
  price_change = (float(change_rate) * 100) if change_rate else 0
 
501
  except (ValueError, TypeError, KeyError) as e:
502
  continue
503
 
504
+ print(f"✅ تم معالجة {len(volume_data)} عملة في الطريقة المباشرة (لجلب الحجم)")
505
  return volume_data
506
 
507
  except Exception as e:
 
509
  traceback.print_exc()
510
  return []
511
 
512
+ # 🔴 --- START OF DELETION (V7.0) --- 🔴
513
+ # (تم حذف الدوال التالية لأنها لم تعد مستخدمة في V7.0)
514
+ # _get_volume_data_traditional
515
+ # _process_single_symbol
516
+ # _apply_advanced_indicators
517
+ # _get_detailed_symbol_data
518
+ # _calculate_advanced_score
519
+ # _calculate_volume_score
520
+ # _calculate_volatility
521
+ # _calculate_volatility_score (تم نقل منطق فلتر العملة المستقرة إلى _apply_strategy_qualifiers)
522
+ # _calculate_price_strength
523
+ # _calculate_momentum (تم نقل منطق الزخم إلى _apply_strategy_qualifiers)
524
+ # 🔴 --- END OF DELETION --- 🔴
525
+
526
+ # (دالة تدفق الشموع تبقى كما هي - لا تغيير)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  async def stream_ohlcv_data(self, symbols: List[str], queue: asyncio.Queue):
528
  """
529
+ جلب بيانات OHLCV كاملة (6 أطر زمنية) للعملات الناجحة فقط
530
  """
531
+ print(f"📊 بدء تدفق بيانات OHLCV (الكاملة) لـ {len(symbols)} عملة (الناجحين من الغربلة)...")
532
 
533
  batch_size = 15
534
  batches = [symbols[i:i + batch_size] for i in range(0, len(symbols), batch_size)]
 
545
 
546
  batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True)
547
 
 
548
  successful_data_for_batch = []
549
  successful_count = 0
550
  for i, result in enumerate(batch_results):
 
561
 
562
  print(f" 📦 [المنتج] اكتملت الدفعة {batch_num + 1}: {successful_count}/{len(batch)} ناجحة")
563
 
 
 
564
  if successful_data_for_batch:
565
  try:
566
  await queue.put(successful_data_for_batch)
 
569
  except Exception as q_err:
570
  print(f" ❌ [المنتج] فشل إرسال الدفعة للطابور: {q_err}")
571
 
 
572
  if batch_num < len(batches) - 1:
573
  await asyncio.sleep(1)
574
 
575
+ print(f"✅ [المنتج] اكتمل تدفق بيانات OHLCV (الكاملة). تم إرسال {total_successful} عملة للمعالجة.")
576
 
577
  try:
578
  await queue.put(None)
 
586
  try:
587
  ohlcv_data = {}
588
 
 
589
  timeframes = [
590
  ('5m', 200),
591
  ('15m', 200),
 
595
  ('1w', 200),
596
  ]
597
 
 
598
  timeframe_tasks = []
599
  for timeframe, limit in timeframes:
600
  task = asyncio.create_task(self._fetch_single_timeframe_improved(symbol, timeframe, limit))
601
  timeframe_tasks.append(task)
602
 
 
603
  timeframe_results = await asyncio.gather(*timeframe_tasks, return_exceptions=True)
604
 
 
605
  successful_timeframes = 0
606
+ min_required_timeframes = 2
607
 
 
608
  for i, (timeframe, limit) in enumerate(timeframes):
609
  result = timeframe_results[i]
610
 
611
  if isinstance(result, Exception):
612
  continue
613
 
614
+ if result and len(result) >= 10:
615
  ohlcv_data[timeframe] = result
616
  successful_timeframes += 1
617
 
 
618
  if successful_timeframes >= min_required_timeframes and ohlcv_data:
619
  try:
 
620
  current_price = await self.get_latest_price_async(symbol)
621
 
 
622
  if current_price is None:
 
623
  for timeframe_data in ohlcv_data.values():
624
  if timeframe_data and len(timeframe_data) > 0:
625
  last_candle = timeframe_data[-1]
626
  if len(last_candle) >= 5:
627
+ current_price = last_candle[4]
628
  break
629
 
630
  if current_price is None:
 
631
  return None
632
 
633
  result_data = {
634
  'symbol': symbol,
635
  'ohlcv': ohlcv_data,
636
+ 'raw_ohlcv': ohlcv_data,
637
  'current_price': current_price,
638
  'timestamp': datetime.now().isoformat(),
639
  'candles_count': {tf: len(data) for tf, data in ohlcv_data.items()},
 
643
  return result_data
644
 
645
  except Exception as price_error:
 
646
  return None
647
  else:
648
  return None
649
 
650
  except Exception as e:
 
651
  return None
652
 
653
  async def _fetch_single_timeframe_improved(self, symbol: str, timeframe: str, limit: int):
 
671
  return []
672
 
673
  async def get_latest_price_async(self, symbol):
674
+ """جلب السعر الحالي لعملة محددة"""
675
  try:
676
  if not self.exchange:
 
677
  return None
678
 
 
 
679
  if not symbol or '/' not in symbol:
 
680
  return None
681
 
 
682
  ticker = self.exchange.fetch_ticker(symbol)
683
 
684
  if not ticker:
 
685
  return None
686
 
687
  current_price = ticker.get('last')
688
 
689
  if current_price is None:
 
690
  return None
691
 
692
  return float(current_price)
693
 
694
  except Exception as e:
 
695
  return None
696
 
697
+ # (دوال دعم بيانات الحيتان تبقى كما هي - لا تغيير)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698
  async def get_whale_data_for_symbol(self, symbol):
699
  """جلب بيانات الحيتان لعملة محددة"""
700
  try:
 
726
  'source': 'whale_analysis'
727
  }
728
 
729
+ print("✅ DataManager loaded - V7.0 (Strategy Qualifier Screening)")