Riy777 commited on
Commit
d602f4a
·
verified ·
1 Parent(s): 3098dff

Update whale_monitor/core.py

Browse files
Files changed (1) hide show
  1. whale_monitor/core.py +240 -100
whale_monitor/core.py CHANGED
@@ -1,5 +1,5 @@
1
  # whale_monitor/core.py
2
- # (V2 - محدث بالكامل لخطة "التراكم والتدفق")
3
  # المنطق الأساسي لـ EnhancedWhaleMonitor
4
 
5
  import os
@@ -32,7 +32,7 @@ logging.getLogger("httpcore").setLevel(logging.WARNING)
32
 
33
  class EnhancedWhaleMonitor:
34
  def __init__(self, contracts_db=None, r2_service=None):
35
- print("🔄 [WhaleMonitor V2] بدء التهيئة...")
36
 
37
  # 1. الخدمات الخارجية
38
  self.r2_service = r2_service
@@ -47,8 +47,7 @@ class EnhancedWhaleMonitor:
47
  verify=self.ssl_context
48
  )
49
 
50
- # 3. تهيئة "الوكيل الذكي" (RPC Manager V2)
51
- # (لم نعد بحاجة لجلب المفاتيح هنا، المدير يفعل ذلك)
52
  self.rpc_manager = AdaptiveRpcManager(self.http_client)
53
 
54
  # 4. تحميل الإعدادات من config.py (عبر المدير)
@@ -70,13 +69,13 @@ class EnhancedWhaleMonitor:
70
  if self.r2_service:
71
  asyncio.create_task(self._load_contracts_from_r2())
72
 
73
- print("✅ [WhaleMonitor V2] تم التهيئة بنجاح باستخدام مدير RPC/API الذكي V2.")
74
 
75
  # --- (دوال التهيئة وقواعد البيانات - لا تغيير جوهري) ---
76
 
77
  def _initialize_contracts_db(self, initial_contracts):
78
  """تهيئة قاعدة بيانات العقود مع تحسين تخزين الشبكات"""
79
- print("🔄 [WhaleMonitor V2] تهيئة قاعدة بيانات العقود...")
80
  for symbol, contract_data in initial_contracts.items():
81
  symbol_lower = symbol.lower()
82
  if isinstance(contract_data, dict) and 'address' in contract_data and 'network' in contract_data:
@@ -86,7 +85,7 @@ class EnhancedWhaleMonitor:
86
  'address': contract_data,
87
  'network': self._detect_network_from_address(contract_data)
88
  }
89
- print(f"✅ [WhaleMonitor V2] تم تحميل {len(self.contracts_db)} عقد في قاعدة البيانات")
90
 
91
  def _detect_network_from_address(self, address):
92
  """اكتشاف الشبكة من عنوان العقد"""
@@ -119,7 +118,7 @@ class EnhancedWhaleMonitor:
119
  self.address_categories['dex'].add(addr_lower)
120
  elif 'wormhole' in category:
121
  self.address_categories['bridge'].add(addr_lower)
122
- print(f"✅ [WhaleMonitor V2] تم تهيئة {len(self.address_labels)} عنوان منصة (احتياطي).")
123
 
124
  async def _load_contracts_from_r2(self):
125
  # (لا تغيير في هذه الدالة)
@@ -136,12 +135,12 @@ class EnhancedWhaleMonitor:
136
  new_format = {'address': contract_data, 'network': self._detect_network_from_address(contract_data)}
137
  updated_contracts_db[symbol_lower] = new_format; loaded_count += 1; updated_count +=1
138
  self.contracts_db = updated_contracts_db
139
- print(f"✅ [WhaleMonitor V2] تم تحميل {loaded_count} عقد من R2.")
140
  if updated_count > 0: print(f" ℹ️ تم تحديث صيغة {updated_count} عقد."); await self._save_contracts_to_r2()
141
  except ClientError as e:
142
- if e.response['Error']['Code'] == 'NoSuchKey': print("⚠️ [WhaleMonitor V2] لم يتم العثور على قاعدة بيانات العقود في R2.")
143
- else: print(f"❌ [WhaleMonitor V2] خطأ ClientError أثناء تحميل العقود من R2: {e}")
144
- except Exception as e: print(f"❌ [WhaleMonitor V2] خطأ عام أثناء تحميل العقود من R2: {e}")
145
 
146
  async def _save_contracts_to_r2(self):
147
  # (لا تغيير في هذه الدالة)
@@ -151,22 +150,22 @@ class EnhancedWhaleMonitor:
151
  for symbol, data in self.contracts_db.items():
152
  if isinstance(data, dict) and 'address' in data and 'network' in data: contracts_to_save[symbol] = data
153
  elif isinstance(data, str): contracts_to_save[symbol] = {'address': data, 'network': self._detect_network_from_address(data)}
154
- if not contracts_to_save: print("⚠️ [WhaleMonitor V2] لا توجد بيانات عقود صالحة للحفظ في R2."); return
155
  data_json = json.dumps(contracts_to_save, indent=2).encode('utf-8')
156
  self.r2_service.s3_client.put_object(Bucket="trading", Key=key, Body=data_json, ContentType="application/json")
157
- print(f"✅ [WhaleMonitor V2] تم حفظ قاعدة بيانات العقود ({len(contracts_to_save)} إدخال) إلى R2")
158
- except Exception as e: print(f"❌ [WhaleMonitor V2] فشل حفظ قاعدة البيانات العقود: {e}")
159
 
160
- # --- (المنطق الأساسي - محدث بالكامل V2) ---
161
 
162
  async def get_symbol_whale_activity(self, symbol: str, daily_volume_usd: float = 0.0) -> Dict[str, Any]:
163
  """
164
- (محدث V2 - "التراكم والتدفق")
165
  الدالة الرئيسية لتحليل الحيتان.
166
  تنفذ منطق "جلب 1، تحليل N" وتسجل البيانات للتعلم.
167
  """
168
  try:
169
- print(f"🔍 [WhaleMonitor V2] بدء مراقبة الحيتان (تدفق + تراكم) لـ: {symbol}")
170
 
171
  # 1. تصفير إحصائيات الجلسة
172
  self.rpc_manager.reset_session_stats()
@@ -204,7 +203,7 @@ class EnhancedWhaleMonitor:
204
  print(f"⚠️ لم يتم العثور على أي تحويلات للعملة {symbol} في آخر 24 ساعة.")
205
  return self._create_no_transfers_response(symbol)
206
 
207
- print(f"📊 [WhaleMonitor V2] تم جلب {len(all_transfers_24h)} تحويلة (24س). بدء التحليل المقارن...")
208
 
209
  # 5. التقسيم والتحليل (تحليل N)
210
  analysis_windows = [
@@ -278,7 +277,7 @@ class EnhancedWhaleMonitor:
278
  إنشاء السجل الأولي "PENDING" وإرساله إلى R2Service.
279
  """
280
  if not self.r2_service:
281
- print("⚠️ [WhaleMonitor V2] خدمة R2 غير متاحة، تخطي تسجيل التعلم.")
282
  return
283
 
284
  try:
@@ -300,74 +299,92 @@ class EnhancedWhaleMonitor:
300
  "api_stats": api_stats
301
  }
302
 
303
- # (نفترض أن R2Service لديها هذه الدالة - سنضيفها لاحقاً)
304
  if hasattr(self.r2_service, 'save_whale_learning_record_async'):
305
  await self.r2_service.save_whale_learning_record_async(record)
306
- print(f"✅ [WhaleMonitor V2] تم تسجيل بيانات التعلم (PENDING) لـ {symbol} بنجاح.")
307
  else:
308
- print("❌ [WhaleMonitor V2] R2Service تفتقد دالة save_whale_learning_record_async")
309
 
310
  except Exception as e:
311
- print(f"❌ [WhaleMonitor V2] فشل في حفظ سجل التعلم لـ {symbol}: {e}")
312
  traceback.print_exc()
313
 
314
- # --- (دوال جلب البيانات - محدثة بالكامل V2) ---
315
 
316
  async def _get_targeted_transfer_data(self, contract_address: str, network: str, hours: int, price: float, decimals: int) -> List[Dict[str, Any]]:
317
  """
318
- (محدث V2)
319
- الوظيفة الرئيسية لجلب البيانات. تعطي الأولوية لـ Moralis.
 
320
  """
321
- print(f"🌐 [WhaleMonitor V2] جلب بيانات {hours} ساعة لشبكة {network}...")
322
 
323
- # 1. تحديد الأولوية
324
- moralis_key = self.rpc_manager.get_api_key('moralis')
325
  net_config = self.supported_networks.get(network, {})
326
- moralis_chain_id = net_config.get('moralis_chain_id')
327
-
328
  all_transfers = []
329
 
330
- # الأولوية 1: Moralis (لأنه "ذكي" ويعطي الأسماء)
331
- if moralis_key and moralis_chain_id:
332
- try:
333
- print(f" -> [أولوية 1] محاولة Moralis (Chain: {moralis_chain_id})...")
334
- moralis_transfers = await self._get_moralis_token_data(contract_address, moralis_chain_id, hours, price, decimals)
335
- if moralis_transfers:
336
- all_transfers.extend(moralis_transfers)
337
- print(f" ✅ [Moralis] تم جلب {len(moralis_transfers)} تحويلة.")
338
- except Exception as e:
339
- print(f" ⚠️ [Moralis] فشل: {e}. اللجوء إلى Scanners.")
340
 
341
- # الأولوية 2: Scanners (Etherscan, BscScan, ...)
342
- # (نستخدمها إذا فشل Moralis أو لجلب المزيد من البيانات)
343
- if not all_transfers:
344
- try:
345
- print(" -> [أولوية 2] محاولة Scanners (Etherscan/BscScan...)...")
346
- scanner_transfers = await self._get_scanner_token_data(contract_address, network, hours, price, decimals)
347
- if scanner_transfers:
348
- all_transfers.extend(scanner_transfers)
349
- print(f" ✅ [Scanners] تم جلب {len(scanner_transfers)} تحويلة.")
350
- except Exception as e:
351
- print(f" ⚠️ [Scanners] فشل: {e}. اللجوء إلى RPC.")
352
-
353
- # الأولوية 3: RPC (eth_getLogs) (كآخر حل)
354
- if not all_transfers and net_config.get('type') == 'evm':
355
  try:
356
- print(" -> [أولوية 3] محاولة RPC (eth_getLogs)...")
357
- rpc_transfers = await self._get_rpc_token_data(contract_address, network, hours, price, decimals)
358
- if rpc_transfers:
359
- all_transfers.extend(rpc_transfers)
360
- print(f" ✅ [RPC] تم جلب {len(rpc_transfers)} تحويلة.")
361
  except Exception as e:
362
- print(f" ⚠️ [RPC] فشل: {e}.")
363
 
364
- # (ملاحظة: Solana RPC معقد ويحتاج معالجة خاصة، Moralis هو الأفضل لـ Solana)
 
 
 
 
 
 
 
 
 
365
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
366
  if not all_transfers:
367
- print(f"❌ [WhaleMonitor V2] فشلت جميع المصادر في جلب التحويلات لـ {network}.")
368
  return []
369
 
370
- # إزالة التكرار (الأولوية لـ Moralis/Scanner)
371
  final_transfers = []; seen_keys = set()
372
  for t in all_transfers:
373
  key = f"{t.get('hash')}-{t.get('logIndex','N/A')}"
@@ -376,6 +393,57 @@ class EnhancedWhaleMonitor:
376
 
377
  return final_transfers
378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
379
  async def _get_moralis_token_data(self, contract_address: str, chain_id: str, hours: int, price: float, decimals: int) -> List[Dict]:
380
  """(جديد V2) جلب التحويلات باستخدام Moralis API"""
381
 
@@ -434,7 +502,6 @@ class EnhancedWhaleMonitor:
434
  print(f" ⚠️ لا توجد إعدادات مستكشف لشبكة {network}")
435
  return []
436
 
437
- # (محاولة استخدام المفتاح المخصص للشبكة، أو اللجوء إلى مفتاح "etherscan" العام)
438
  api_key = self.rpc_manager.get_api_key(explorer_config['api_key_name']) or self.rpc_manager.get_api_key('etherscan')
439
  if not api_key:
440
  print(f" ⚠️ لا يوجد مفتاح API لـ {network} (أو مفتاح etherscan العام)")
@@ -462,13 +529,12 @@ class EnhancedWhaleMonitor:
462
  try:
463
  timestamp = int(tx.get('timeStamp', '0'))
464
  if timestamp < cutoff_timestamp:
465
- break # (البيانات مرتبة تنازلياً، توقف عند الوصول للبيانات القديمة)
466
-
467
  value_raw = tx.get('value')
468
  if not value_raw or not value_raw.isdigit(): continue
469
 
470
  value_usd = self._calculate_value_usd(int(value_raw), decimals, price)
471
- if value_usd < 1000: continue # (فلترة أولية للضجيج)
472
 
473
  transfers.append({
474
  'hash': tx.get('hash'),
@@ -491,8 +557,6 @@ class EnhancedWhaleMonitor:
491
  """(محدث V2) جلب التحويلات باستخدام eth_getLogs عبر RpcManager"""
492
 
493
  try:
494
- # (تقريب عدد الكتل بناءً على الوقت - تقدير متحفظ)
495
- # (نفترض 15 ثانية للكتلة لـ ETH/Polygon, و 3 ثوان لـ BSC)
496
  avg_block_time = 15 if network not in ['bsc'] else 3
497
  blocks_to_scan = int((hours * 3600) / avg_block_time)
498
 
@@ -522,7 +586,6 @@ class EnhancedWhaleMonitor:
522
  print(f" ✅ [RPC] لا توجد سجلات تحويل لـ {contract_address}."); return []
523
 
524
  transfers = []
525
- # (نحتاج لجلب الوقت الفعلي للكتل)
526
  block_timestamps = await self._get_block_timestamps_rpc(network, [log.get('blockNumber') for log in logs])
527
 
528
  for log in logs:
@@ -534,7 +597,7 @@ class EnhancedWhaleMonitor:
534
 
535
  value_int = int(value_raw, 16)
536
  value_usd = self._calculate_value_usd(value_int, decimals, price)
537
- if value_usd < 1000: continue # (فلترة أولية للضجيج)
538
 
539
  timestamp = block_timestamps.get(block_num_hex, str(int(time.time())))
540
 
@@ -581,10 +644,10 @@ class EnhancedWhaleMonitor:
581
  pass
582
  return timestamps
583
 
584
- # --- (دوال جلب الأسعار والكسور العشرية - محدثة V2) ---
585
 
586
  async def _get_token_decimals(self, contract_address, network):
587
- """(محدث V2) جلب الكسور العشرية (يستخدم RpcManager)"""
588
  cache_key = f"{contract_address.lower()}_{network}"
589
  if cache_key in self.token_decimals_cache:
590
  return self.token_decimals_cache[cache_key]
@@ -603,7 +666,23 @@ class EnhancedWhaleMonitor:
603
  return decimals
604
  except Exception: pass
605
 
606
- # (يمكن إضافة دعم Solana هنا إذا لزم الأمر)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
607
  print(f"❌ فشل جلب الكسور العشرية لـ {contract_address} على {network}.")
608
  return None
609
 
@@ -669,11 +748,38 @@ class EnhancedWhaleMonitor:
669
  if not coin_id: return None
670
 
671
  print(f" 🔍 [CoinGecko] جلب تفاصيل المعرف: {coin_id}")
672
- detail_data = await self.rpc_manager.get_coingecko_api(params={"ids": coin_id, "localization": "false", "tickers": "false", "market_data": "false", "community_data": "false", "developer_data": "false", "sparkline": "false"})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
673
 
674
- if not detail_data or not detail_data.get(coin_id) or not detail_data[coin_id].get('platforms'):
 
 
 
 
 
 
 
 
 
 
 
 
 
675
  print(f" ❌ [CoinGecko] فشل جلب تفاصيل المنصات لـ {coin_id}"); return None
676
- platforms = detail_data[coin_id]['platforms']
677
 
678
  network_priority = ['ethereum', 'binance-smart-chain', 'polygon-pos', 'arbitrum-one', 'optimistic-ethereum', 'avalanche', 'fantom', 'solana']
679
  network_map = {'ethereum': 'ethereum', 'binance-smart-chain': 'bsc', 'polygon-pos': 'polygon', 'arbitrum-one': 'arbitrum', 'optimistic-ethereum': 'optimism', 'avalanche': 'avalanche', 'fantom': 'fantom', 'solana': 'solana'}
@@ -689,12 +795,15 @@ class EnhancedWhaleMonitor:
689
  except Exception as e:
690
  print(f"❌ فشل عام في _find_contract_via_coingecko لـ {symbol}: {e}"); traceback.print_exc(); return None
691
 
692
- # --- (دوال التحليل والمساعدة - محدثة V2) ---
693
 
694
  def _calculate_value_usd(self, raw_value: int, decimals: int, price: float) -> float:
695
  """(جديد V2) دالة مساعدة لحساب القيمة بالدولار بأمان"""
696
  try:
697
  if price == 0: return 0.0
 
 
 
698
  token_amount = raw_value / (10 ** decimals)
699
  return token_amount * price
700
  except Exception:
@@ -714,15 +823,12 @@ class EnhancedWhaleMonitor:
714
  }
715
 
716
  for tx in transfers:
717
- # (نستخدم القيمة المحسوبة مسبقاً إذا كانت موجودة، وإلا نستخدم 0)
718
  value_usd = tx.get('value_usd', 0.0)
719
  if value_usd == 0.0: continue
720
 
721
- # (نستخدم التصنيف "الذكي" من Moralis أولاً)
722
  is_to_exchange = tx.get('to_label') is not None
723
  is_from_exchange = tx.get('from_label') is not None
724
 
725
- # (إذا فشل Moralis، نستخدم القائمة الاحتياطية)
726
  if not is_to_exchange and not is_from_exchange:
727
  is_to_exchange = self._is_exchange_address(tx.get('to'))
728
  is_from_exchange = self._is_exchange_address(tx.get('from'))
@@ -740,24 +846,18 @@ class EnhancedWhaleMonitor:
740
  stats['withdrawal_count'] += 1
741
  stats['top_withdrawals'].append({'v': value_usd, 'from': tx.get('from_label', self._classify_address(tx.get('from')))})
742
 
743
- # --- حساب المقاييس النهائية (المطلقة) ---
744
  net_flow_usd = stats['to_exchanges_usd'] - stats['from_exchanges_usd']
745
 
746
- # --- حساب المقاييس النهائية (النسبية) ---
747
  relative_net_flow_percent = 0.0
748
  transaction_density = 0.0
749
 
750
  if daily_volume_usd > 0:
751
- # (المقياس الذكي 1: نسبة صافي التدفق)
752
  relative_net_flow_percent = (net_flow_usd / daily_volume_usd) * 100
753
-
754
- # (المقياس الذكي 2: كثافة التحويلات)
755
  total_transactions = stats['deposit_count'] + stats['withdrawal_count']
756
  volume_in_millions = daily_volume_usd / 1_000_000
757
  if volume_in_millions > 0:
758
  transaction_density = total_transactions / volume_in_millions
759
 
760
- # (فرز أفضل 3 تحويلات)
761
  top_deposits = sorted(stats['top_deposits'], key=lambda x: x['v'], reverse=True)[:3]
762
  top_withdrawals = sorted(stats['top_withdrawals'], key=lambda x: x['v'], reverse=True)[:3]
763
 
@@ -771,11 +871,8 @@ class EnhancedWhaleMonitor:
771
  'net_flow_usd': net_flow_usd,
772
  'deposit_count': stats['deposit_count'],
773
  'withdrawal_count': stats['withdrawal_count'],
774
-
775
- # (المقاييس الذكية للتعلم)
776
  'relative_net_flow_percent': relative_net_flow_percent,
777
  'transaction_density': transaction_density,
778
-
779
  'top_deposits': top_deposits,
780
  'top_withdrawals': top_withdrawals
781
  }
@@ -826,7 +923,7 @@ class EnhancedWhaleMonitor:
826
  async def _find_contract_address_enhanced(self, symbol):
827
  """(محدث V2) بحث متقدم عن عقد العملة (يستخدم RpcManager)"""
828
  base_symbol = symbol.split("/")[0].upper() if '/' in symbol else symbol.upper(); symbol_lower = base_symbol.lower()
829
- print(f"🔍 [WhaleMonitor V2] البحث عن عقد للعملة: {symbol}")
830
 
831
  if symbol_lower in self.contracts_db:
832
  contract_info = self.contracts_db[symbol_lower]
@@ -837,13 +934,56 @@ class EnhancedWhaleMonitor:
837
  print(f" ✅ وجد في قاعدة البيانات المحلية: {contract_info}"); return contract_info
838
 
839
  print(f" 🔍 البحث في CoinGecko عن {base_symbol}...")
840
- coingecko_result = await self._find_contract_via_coingecko(base_symbol)
841
- if coingecko_result:
842
- address, network = coingecko_result; contract_info = {'address': address, 'network': network}
843
- self.contracts_db[symbol_lower] = contract_info
844
- print(f" ✅ تم العثور على عقد {symbol} عبر CoinGecko على شبكة {network}: {address}")
845
- if self.r2_service: await self._save_contracts_to_r2()
846
- return contract_info
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
847
 
848
  print(f" ❌ فشل العثور على عقد لـ {symbol} في جميع المصادر"); return None
849
 
 
1
  # whale_monitor/core.py
2
+ # (V2.1 - إصلاح منطق الأولوية (Gather) + إضافة دعم Solscan)
3
  # المنطق الأساسي لـ EnhancedWhaleMonitor
4
 
5
  import os
 
32
 
33
  class EnhancedWhaleMonitor:
34
  def __init__(self, contracts_db=None, r2_service=None):
35
+ print("🔄 [WhaleMonitor V2.1] بدء التهيئة...")
36
 
37
  # 1. الخدمات الخارجية
38
  self.r2_service = r2_service
 
47
  verify=self.ssl_context
48
  )
49
 
50
+ # 3. تهيئة "الوكيل الذكي" (RPC Manager V2.1)
 
51
  self.rpc_manager = AdaptiveRpcManager(self.http_client)
52
 
53
  # 4. تحميل الإعدادات من config.py (عبر المدير)
 
69
  if self.r2_service:
70
  asyncio.create_task(self._load_contracts_from_r2())
71
 
72
+ print("✅ [WhaleMonitor V2.1] تم التهيئة بنجاح باستخدام مدير RPC/API الذكي V2.1.")
73
 
74
  # --- (دوال التهيئة وقواعد البيانات - لا تغيير جوهري) ---
75
 
76
  def _initialize_contracts_db(self, initial_contracts):
77
  """تهيئة قاعدة بيانات العقود مع تحسين تخزين الشبكات"""
78
+ print("🔄 [WhaleMonitor V2.1] تهيئة قاعدة بيانات العقود...")
79
  for symbol, contract_data in initial_contracts.items():
80
  symbol_lower = symbol.lower()
81
  if isinstance(contract_data, dict) and 'address' in contract_data and 'network' in contract_data:
 
85
  'address': contract_data,
86
  'network': self._detect_network_from_address(contract_data)
87
  }
88
+ print(f"✅ [WhaleMonitor V2.1] تم تحميل {len(self.contracts_db)} عقد في قاعدة البيانات")
89
 
90
  def _detect_network_from_address(self, address):
91
  """اكتشاف الشبكة من عنوان العقد"""
 
118
  self.address_categories['dex'].add(addr_lower)
119
  elif 'wormhole' in category:
120
  self.address_categories['bridge'].add(addr_lower)
121
+ print(f"✅ [WhaleMonitor V2.1] تم تهيئة {len(self.address_labels)} عنوان منصة (احتياطي).")
122
 
123
  async def _load_contracts_from_r2(self):
124
  # (لا تغيير في هذه الدالة)
 
135
  new_format = {'address': contract_data, 'network': self._detect_network_from_address(contract_data)}
136
  updated_contracts_db[symbol_lower] = new_format; loaded_count += 1; updated_count +=1
137
  self.contracts_db = updated_contracts_db
138
+ print(f"✅ [WhaleMonitor V2.1] تم تحميل {loaded_count} عقد من R2.")
139
  if updated_count > 0: print(f" ℹ️ تم تحديث صيغة {updated_count} عقد."); await self._save_contracts_to_r2()
140
  except ClientError as e:
141
+ if e.response['Error']['Code'] == 'NoSuchKey': print("⚠️ [WhaleMonitor V2.1] لم يتم العثور على قاعدة بيانات العقود في R2.")
142
+ else: print(f"❌ [WhaleMonitor V2.1] خطأ ClientError أثناء تحميل العقود من R2: {e}")
143
+ except Exception as e: print(f"❌ [WhaleMonitor V2.1] خطأ عام أثناء تحميل العقود من R2: {e}")
144
 
145
  async def _save_contracts_to_r2(self):
146
  # (لا تغيير في هذه الدالة)
 
150
  for symbol, data in self.contracts_db.items():
151
  if isinstance(data, dict) and 'address' in data and 'network' in data: contracts_to_save[symbol] = data
152
  elif isinstance(data, str): contracts_to_save[symbol] = {'address': data, 'network': self._detect_network_from_address(data)}
153
+ if not contracts_to_save: print("⚠️ [WhaleMonitor V2.1] لا توجد بيانات عقود صالحة للحفظ في R2."); return
154
  data_json = json.dumps(contracts_to_save, indent=2).encode('utf-8')
155
  self.r2_service.s3_client.put_object(Bucket="trading", Key=key, Body=data_json, ContentType="application/json")
156
+ print(f"✅ [WhaleMonitor V2.1] تم حفظ قاعدة بيانات العقود ({len(contracts_to_save)} إدخال) إلى R2")
157
+ except Exception as e: print(f"❌ [WhaleMonitor V2.1] فشل حفظ قاعدة البيانات العقود: {e}")
158
 
159
+ # --- (المنطق الأساسي - محدث بالكامل V2.1) ---
160
 
161
  async def get_symbol_whale_activity(self, symbol: str, daily_volume_usd: float = 0.0) -> Dict[str, Any]:
162
  """
163
+ (محدث V2.1 - "التراكم والتدفق")
164
  الدالة الرئيسية لتحليل الحيتان.
165
  تنفذ منطق "جلب 1، تحليل N" وتسجل البيانات للتعلم.
166
  """
167
  try:
168
+ print(f"🔍 [WhaleMonitor V2.1] بدء مراقبة الحيتان (تدفق + تراكم) لـ: {symbol}")
169
 
170
  # 1. تصفير إحصائيات الجلسة
171
  self.rpc_manager.reset_session_stats()
 
203
  print(f"⚠️ لم يتم العثور على أي تحويلات للعملة {symbol} في آخر 24 ساعة.")
204
  return self._create_no_transfers_response(symbol)
205
 
206
+ print(f"📊 [WhaleMonitor V2.1] تم جلب {len(all_transfers_24h)} تحويلة (24س). بدء التحليل المقارن...")
207
 
208
  # 5. التقسيم والتحليل (تحليل N)
209
  analysis_windows = [
 
277
  إنشاء السجل الأولي "PENDING" وإرساله إلى R2Service.
278
  """
279
  if not self.r2_service:
280
+ print("⚠️ [WhaleMonitor V2.1] خدمة R2 غير متاحة، تخطي تسجيل التعلم.")
281
  return
282
 
283
  try:
 
299
  "api_stats": api_stats
300
  }
301
 
 
302
  if hasattr(self.r2_service, 'save_whale_learning_record_async'):
303
  await self.r2_service.save_whale_learning_record_async(record)
304
+ print(f"✅ [WhaleMonitor V2.1] تم تسجيل بيانات التعلم (PENDING) لـ {symbol} بنجاح.")
305
  else:
306
+ print("❌ [WhaleMonitor V2.1] R2Service تفتقد دالة save_whale_learning_record_async")
307
 
308
  except Exception as e:
309
+ print(f"❌ [WhaleMonitor V2.1] فشل في حفظ سجل التعلم لـ {symbol}: {e}")
310
  traceback.print_exc()
311
 
312
+ # --- (دوال جلب البيانات - محدثة بالكامل V2.1) ---
313
 
314
  async def _get_targeted_transfer_data(self, contract_address: str, network: str, hours: int, price: float, decimals: int) -> List[Dict[str, Any]]:
315
  """
316
+ (محدث V2.1)
317
+ الوظيفة الرئيسية لجلب البيانات. تطبق منطق الأولوية الصحيح (Solscan > Moralis > Scanners > RPC)
318
+ وتقوم بتجميع النتائج.
319
  """
320
+ print(f"🌐 [WhaleMonitor V2.1] جلب بيانات {hours} ساعة لشبكة {network} (منطق تجميعي)...")
321
 
 
 
322
  net_config = self.supported_networks.get(network, {})
 
 
323
  all_transfers = []
324
 
325
+ # 🔴 --- START OF CHANGE (V2.1 - Logic Fix) --- 🔴
 
 
 
 
 
 
 
 
 
326
 
327
+ if network == 'solana':
328
+ # --- منطق سولانا (أولوية Solscan) ---
329
+ print(f" -> [أولوية 1 - SOL] محاولة Solscan API...")
 
 
 
 
 
 
 
 
 
 
 
330
  try:
331
+ solscan_transfers = await self._get_solscan_token_data(contract_address, hours, price)
332
+ if solscan_transfers:
333
+ all_transfers.extend(solscan_transfers)
334
+ print(f" ✅ [Solscan] تم جلب {len(solscan_transfers)} تحويلة.")
 
335
  except Exception as e:
336
+ print(f" ⚠️ [Solscan] فشل: {e}. اللجوء إلى Moralis...")
337
 
338
+ # (اللجوء إلى Moralis فقط إذا فشل Solscan)
339
+ if not all_transfers:
340
+ print(f" -> [أولوية 2 - SOL] محاولة Moralis...")
341
+ try:
342
+ moralis_transfers = await self._get_moralis_token_data(contract_address, 'sol', hours, price, decimals)
343
+ if moralis_transfers:
344
+ all_transfers.extend(moralis_transfers)
345
+ print(f" ✅ [Moralis] تم جلب {len(moralis_transfers)} تحويلة.")
346
+ except Exception as e:
347
+ print(f" ⚠️ [Moralis] فشل: {e}.")
348
 
349
+ elif net_config.get('type') == 'evm':
350
+ # --- منطق EVM (تجميعي) ---
351
+ tasks = []
352
+
353
+ # المهمة 1: Moralis
354
+ if self.rpc_manager.get_api_key('moralis') and net_config.get('moralis_chain_id'):
355
+ print(f" -> [EVM] إضافة مهمة Moralis...")
356
+ tasks.append(asyncio.create_task(self._get_moralis_token_data(contract_address, net_config['moralis_chain_id'], hours, price, decimals)))
357
+
358
+ # المهمة 2: Scanners
359
+ if self.rpc_manager.get_explorer_config(network):
360
+ print(f" -> [EVM] إضافة مهمة Scanners...")
361
+ tasks.append(asyncio.create_task(self._get_scanner_token_data(contract_address, network, hours, price, decimals)))
362
+
363
+ # (تنفيذ Moralis و Scanners بالتوازي)
364
+ results = await asyncio.gather(*tasks, return_exceptions=True)
365
+
366
+ for res in results:
367
+ if isinstance(res, list): all_transfers.extend(res)
368
+ elif isinstance(res, Exception): print(f" ⚠️ [EVM Gather] فشلت إحدى المهام: {res}")
369
+
370
+ # المهمة 3: RPC (فقط إذا فشلت الواجهات الخاصة بالكامل)
371
+ if not all_transfers:
372
+ print(f" -> [EVM] فشلت الواجهات الخاصة، اللجوء إلى RPC...")
373
+ try:
374
+ rpc_transfers = await self._get_rpc_token_data(contract_address, network, hours, price, decimals)
375
+ if rpc_transfers:
376
+ all_transfers.extend(rpc_transfers)
377
+ print(f" ✅ [RPC] تم جلب {len(rpc_transfers)} تحويلة.")
378
+ except Exception as e:
379
+ print(f" ⚠️ [RPC] فشل: {e}.")
380
+
381
+ # 🔴 --- END OF CHANGE --- 🔴
382
+
383
  if not all_transfers:
384
+ print(f"❌ [WhaleMonitor V2.1] فشلت جميع المصادر في جلب التحويلات لـ {network}.")
385
  return []
386
 
387
+ # إزالة التكرار (الأولوية لـ Moralis/Scanner/Solscan)
388
  final_transfers = []; seen_keys = set()
389
  for t in all_transfers:
390
  key = f"{t.get('hash')}-{t.get('logIndex','N/A')}"
 
393
 
394
  return final_transfers
395
 
396
+ # 🔴 --- START OF CHANGE (V2.1) --- 🔴
397
+ async def _get_solscan_token_data(self, token_address: str, hours: int, price: float) -> List[Dict]:
398
+ """(جديد V2.1) جلب التحويلات باستخدام Solscan Pro API"""
399
+ print(f" 🔍 [Solscan] جلب التحويلات لـ {token_address}")
400
+
401
+ # (Solscan يستخدم 'limit' بدلاً من الساعات، سنأخذ آخر 50 ونفلترها)
402
+ params = {"limit": 50}
403
+ path = f"/v2.0/token/transfer/{token_address}"
404
+
405
+ data = await self.rpc_manager.get_solscan_api(path, params)
406
+
407
+ if not data or not data.get('data'):
408
+ print(" ⚠️ [Solscan] لا توجد نتائج.")
409
+ return []
410
+
411
+ transfers = []
412
+ cutoff_timestamp = int((datetime.now(timezone.utc) - timedelta(hours=hours)).timestamp())
413
+
414
+ for tx in data['data']:
415
+ try:
416
+ timestamp = tx.get('blockTime')
417
+ if not timestamp or timestamp < cutoff_timestamp:
418
+ continue # (تخطي التحويلات القديمة)
419
+
420
+ change_amount_raw = tx.get('changeAmount')
421
+ if not change_amount_raw: continue
422
+
423
+ # (Solscan يعطينا الكسور العشرية مباشرة!)
424
+ decimals = tx.get('token', {}).get('decimals', 9) # (افتراضي 9 لسولانا)
425
+ value_usd = self._calculate_value_usd(int(change_amount_raw), decimals, price)
426
+ if value_usd < 1000: continue
427
+
428
+ transfers.append({
429
+ 'hash': tx.get('signature'),
430
+ 'logIndex': tx.get('innerInstruction', [{}])[0].get('index', 0), # (محاولة للحصول على معرف فريد)
431
+ 'from': tx.get('changeOwner', {}).get('from'),
432
+ 'to': tx.get('changeOwner', {}).get('to'),
433
+ 'value': change_amount_raw,
434
+ 'value_usd': value_usd,
435
+ 'timeStamp': str(timestamp),
436
+ 'blockNumber': str(tx.get('slot')),
437
+ 'network': 'solana',
438
+ 'source': 'solscan'
439
+ # (Solscan لا يوفر تصنيفات العناوين مثل Moralis)
440
+ })
441
+ except Exception as e:
442
+ print(f" ⚠️ [Solscan] خطأ في تحليل تحويلة: {e}")
443
+ continue
444
+ return transfers
445
+ # 🔴 --- END OF CHANGE --- 🔴
446
+
447
  async def _get_moralis_token_data(self, contract_address: str, chain_id: str, hours: int, price: float, decimals: int) -> List[Dict]:
448
  """(جديد V2) جلب التحويلات باستخدام Moralis API"""
449
 
 
502
  print(f" ⚠️ لا توجد إعدادات مستكشف لشبكة {network}")
503
  return []
504
 
 
505
  api_key = self.rpc_manager.get_api_key(explorer_config['api_key_name']) or self.rpc_manager.get_api_key('etherscan')
506
  if not api_key:
507
  print(f" ⚠️ لا يوجد مفتاح API لـ {network} (أو مفتاح etherscan العام)")
 
529
  try:
530
  timestamp = int(tx.get('timeStamp', '0'))
531
  if timestamp < cutoff_timestamp:
532
+ break
 
533
  value_raw = tx.get('value')
534
  if not value_raw or not value_raw.isdigit(): continue
535
 
536
  value_usd = self._calculate_value_usd(int(value_raw), decimals, price)
537
+ if value_usd < 1000: continue
538
 
539
  transfers.append({
540
  'hash': tx.get('hash'),
 
557
  """(محدث V2) جلب التحويلات باستخدام eth_getLogs عبر RpcManager"""
558
 
559
  try:
 
 
560
  avg_block_time = 15 if network not in ['bsc'] else 3
561
  blocks_to_scan = int((hours * 3600) / avg_block_time)
562
 
 
586
  print(f" ✅ [RPC] لا توجد سجلات تحويل لـ {contract_address}."); return []
587
 
588
  transfers = []
 
589
  block_timestamps = await self._get_block_timestamps_rpc(network, [log.get('blockNumber') for log in logs])
590
 
591
  for log in logs:
 
597
 
598
  value_int = int(value_raw, 16)
599
  value_usd = self._calculate_value_usd(value_int, decimals, price)
600
+ if value_usd < 1000: continue
601
 
602
  timestamp = block_timestamps.get(block_num_hex, str(int(time.time())))
603
 
 
644
  pass
645
  return timestamps
646
 
647
+ # --- (دوال جلب الأسعار والكسور العشرية - محدثة V2.1) ---
648
 
649
  async def _get_token_decimals(self, contract_address, network):
650
+ """(محدث V2.1) جلب الكسور العشرية (يستخدم RpcManager ويدعم Solscan)"""
651
  cache_key = f"{contract_address.lower()}_{network}"
652
  if cache_key in self.token_decimals_cache:
653
  return self.token_decimals_cache[cache_key]
 
666
  return decimals
667
  except Exception: pass
668
 
669
+ # 🔴 --- START OF CHANGE (V2.1 - Solscan Decimals Fix) --- 🔴
670
+ elif network == 'solana':
671
+ print(f" 🔍 [Solscan] جلب الكسور العشرية لـ {contract_address}...")
672
+ path = f"/v2.0/token/meta"
673
+ params = {"tokenAddress": contract_address}
674
+ data = await self.rpc_manager.get_solscan_api(path, params)
675
+
676
+ if data and data.get('data') and data['data'].get('decimals') is not None:
677
+ try:
678
+ decimals = int(data['data']['decimals'])
679
+ self.token_decimals_cache[cache_key] = decimals
680
+ print(f" ✅ [Solscan] تم العثور على الكسور العشرية: {decimals}")
681
+ return decimals
682
+ except Exception as e:
683
+ print(f" ⚠️ [Solscan] فشل تحليل الكسور العشرية: {e}")
684
+ # 🔴 --- END OF CHANGE --- 🔴
685
+
686
  print(f"❌ فشل جلب الكسور العشرية لـ {contract_address} على {network}.")
687
  return None
688
 
 
748
  if not coin_id: return None
749
 
750
  print(f" 🔍 [CoinGecko] جلب تفاصيل المعرف: {coin_id}")
751
+ # (نستخدم مسار API مختلف لجلب العقود)
752
+ path = f"/api/v3/coins/{coin_id}"
753
+ detail_data = await self.rpc_manager.get_coingecko_api(params={"localization": "false", "tickers": "false", "market_data": "false", "community_data": "false", "developer_data": "false", "sparkline": "false"}, custom_path=path) # (ملاحظة: get_coingecko_api قد يحتاج تعديل لدعم custom_path)
754
+
755
+ # (تعديل: نفترض أن get_coingecko_api لا يدعم custom_path ونستخدم استدعاء مباشر)
756
+ # (هذا الكود مكرر ولكنه ضروري إذا لم يتم تعديل rpc_manager)
757
+ base_url = COINGECKO_BASE_URL
758
+ full_url = f"{base_url}/coins/{coin_id}"
759
+ params={"localization": "false", "tickers": "false", "market_data": "false", "community_data": "false", "developer_data": "false", "sparkline": "false"}
760
+ detail_data = await self.rpc_manager.get_coingecko_api(params=params, custom_base_url=full_url) # (افتراض أن الدالة تدعم هذا)
761
+
762
+ # (إعادة كتابة ليتوافق مع rpc_manager V2.1 الحالي)
763
+ base_url = "https://api.coingecko.com/api/v3" # (COINGECKO_BASE_URL)
764
+ full_url = f"{base_url}/coins/{coin_id}"
765
+ params = {"localization": "false", "tickers": "false", "market_data": "false", "community_data": "false", "developer_data": "false", "sparkline": "false"}
766
 
767
+ async with self.rpc_manager.coingecko_semaphore:
768
+ # (تطبيق "الخنق")
769
+ current_time = time.time()
770
+ time_since_last = current_time - self.rpc_manager.last_coingecko_call
771
+ if time_since_last < self.rpc_manager.COINGECKO_REQUEST_DELAY:
772
+ await asyncio.sleep(self.rpc_manager.COINGECKO_REQUEST_DELAY - time_since_last)
773
+ self.rpc_manager.last_coingecko_call = time.time()
774
+
775
+ response = await self.http_client.get(full_url, params=params)
776
+ response.raise_for_status()
777
+ detail_data = response.json()
778
+
779
+
780
+ if not detail_data or not detail_data.get('platforms'):
781
  print(f" ❌ [CoinGecko] فشل جلب تفاصيل المنصات لـ {coin_id}"); return None
782
+ platforms = detail_data['platforms']
783
 
784
  network_priority = ['ethereum', 'binance-smart-chain', 'polygon-pos', 'arbitrum-one', 'optimistic-ethereum', 'avalanche', 'fantom', 'solana']
785
  network_map = {'ethereum': 'ethereum', 'binance-smart-chain': 'bsc', 'polygon-pos': 'polygon', 'arbitrum-one': 'arbitrum', 'optimistic-ethereum': 'optimism', 'avalanche': 'avalanche', 'fantom': 'fantom', 'solana': 'solana'}
 
795
  except Exception as e:
796
  print(f"❌ فشل عام في _find_contract_via_coingecko لـ {symbol}: {e}"); traceback.print_exc(); return None
797
 
798
+ # --- (دوال التحليل والمساعدة - محدثة V2.1) ---
799
 
800
  def _calculate_value_usd(self, raw_value: int, decimals: int, price: float) -> float:
801
  """(جديد V2) دالة مساعدة لحساب القيمة بالدولار بأمان"""
802
  try:
803
  if price == 0: return 0.0
804
+ if decimals is None:
805
+ print(" ⚠️ [Calc] حساب القيمة فشل بسبب 'decimals' غير موجودة.")
806
+ return 0.0
807
  token_amount = raw_value / (10 ** decimals)
808
  return token_amount * price
809
  except Exception:
 
823
  }
824
 
825
  for tx in transfers:
 
826
  value_usd = tx.get('value_usd', 0.0)
827
  if value_usd == 0.0: continue
828
 
 
829
  is_to_exchange = tx.get('to_label') is not None
830
  is_from_exchange = tx.get('from_label') is not None
831
 
 
832
  if not is_to_exchange and not is_from_exchange:
833
  is_to_exchange = self._is_exchange_address(tx.get('to'))
834
  is_from_exchange = self._is_exchange_address(tx.get('from'))
 
846
  stats['withdrawal_count'] += 1
847
  stats['top_withdrawals'].append({'v': value_usd, 'from': tx.get('from_label', self._classify_address(tx.get('from')))})
848
 
 
849
  net_flow_usd = stats['to_exchanges_usd'] - stats['from_exchanges_usd']
850
 
 
851
  relative_net_flow_percent = 0.0
852
  transaction_density = 0.0
853
 
854
  if daily_volume_usd > 0:
 
855
  relative_net_flow_percent = (net_flow_usd / daily_volume_usd) * 100
 
 
856
  total_transactions = stats['deposit_count'] + stats['withdrawal_count']
857
  volume_in_millions = daily_volume_usd / 1_000_000
858
  if volume_in_millions > 0:
859
  transaction_density = total_transactions / volume_in_millions
860
 
 
861
  top_deposits = sorted(stats['top_deposits'], key=lambda x: x['v'], reverse=True)[:3]
862
  top_withdrawals = sorted(stats['top_withdrawals'], key=lambda x: x['v'], reverse=True)[:3]
863
 
 
871
  'net_flow_usd': net_flow_usd,
872
  'deposit_count': stats['deposit_count'],
873
  'withdrawal_count': stats['withdrawal_count'],
 
 
874
  'relative_net_flow_percent': relative_net_flow_percent,
875
  'transaction_density': transaction_density,
 
876
  'top_deposits': top_deposits,
877
  'top_withdrawals': top_withdrawals
878
  }
 
923
  async def _find_contract_address_enhanced(self, symbol):
924
  """(محدث V2) بحث متقدم عن عقد العملة (يستخدم RpcManager)"""
925
  base_symbol = symbol.split("/")[0].upper() if '/' in symbol else symbol.upper(); symbol_lower = base_symbol.lower()
926
+ print(f"🔍 [WhaleMonitor V2.1] البحث عن عقد للعملة: {symbol}")
927
 
928
  if symbol_lower in self.contracts_db:
929
  contract_info = self.contracts_db[symbol_lower]
 
934
  print(f" ✅ وجد في قاعدة البيانات المحلية: {contract_info}"); return contract_info
935
 
936
  print(f" 🔍 البحث في CoinGecko عن {base_symbol}...")
937
+
938
+ # (إصلاح V2.1: إصلاح استدعاء CoinGecko)
939
+ try:
940
+ search_params = {"query": base_symbol}
941
+ data = await self.rpc_manager.get_coingecko_api(params=search_params) # (استخدام الدالة الأساسية)
942
+ if not data or not data.get('coins'): return None
943
+
944
+ coins = data.get('coins', [])
945
+ best_coin = next((coin for coin in coins if coin.get('symbol', '').lower() == symbol_lower), coins[0] if coins else None)
946
+ coin_id = best_coin.get('id')
947
+ if not coin_id: return None
948
+
949
+ print(f" 🔍 [CoinGecko] جلب تفاصيل المعرف: {coin_id}")
950
+
951
+ # (استدعاء CoinGecko API مباشرة للحصول على تفاصيل العملة)
952
+ base_url = COINGECKO_BASE_URL
953
+ full_url = f"{base_url}/coins/{coin_id}"
954
+ params = {"localization": "false", "tickers": "false", "market_data": "false", "community_data": "false", "developer_data": "false", "sparkline": "false"}
955
+
956
+ async with self.rpc_manager.coingecko_semaphore:
957
+ current_time = time.time()
958
+ time_since_last = current_time - self.rpc_manager.last_coingecko_call
959
+ if time_since_last < COINGECKO_REQUEST_DELAY:
960
+ await asyncio.sleep(COINGECKO_REQUEST_DELAY - time_since_last)
961
+ self.rpc_manager.last_coingecko_call = time.time()
962
+
963
+ response = await self.http_client.get(full_url, params=params)
964
+ response.raise_for_status()
965
+ detail_data = response.json()
966
+
967
+ if not detail_data or not detail_data.get('platforms'):
968
+ print(f" ❌ [CoinGecko] فشل جلب تفاصيل المنصات لـ {coin_id}"); return None
969
+ platforms = detail_data['platforms']
970
+
971
+ network_priority = ['solana', 'ethereum', 'binance-smart-chain', 'polygon-pos', 'arbitrum-one', 'optimistic-ethereum', 'avalanche', 'fantom']
972
+ network_map = {'solana': 'solana', 'ethereum': 'ethereum', 'binance-smart-chain': 'bsc', 'polygon-pos': 'polygon', 'arbitrum-one': 'arbitrum', 'optimistic-ethereum': 'optimism', 'avalanche': 'avalanche', 'fantom': 'fantom'}
973
+
974
+ for platform_cg in network_priority:
975
+ address = platforms.get(platform_cg)
976
+ if address and isinstance(address, str) and address.strip():
977
+ network = network_map.get(platform_cg)
978
+ if network:
979
+ print(f" ✅ [CoinGecko] وجد عقداً على {network}: {address}")
980
+ contract_info = {'address': address, 'network': network}
981
+ self.contracts_db[symbol_lower] = contract_info
982
+ if self.r2_service: await self._save_contracts_to_r2()
983
+ return contract_info
984
+ return None
985
+ except Exception as e:
986
+ print(f"❌ فشل عام في _find_contract_via_coingecko لـ {symbol}: {e}"); traceback.print_exc(); return None
987
 
988
  print(f" ❌ فشل العثور على عقد لـ {symbol} في جميع المصادر"); return None
989