Spaces:
Running
Running
| # whale_monitor/core.py | |
| # (V2 - محدث بالكامل لخطة "التراكم والتدفق") | |
| # المنطق الأساسي لـ EnhancedWhaleMonitor | |
| import os | |
| import asyncio | |
| import httpx | |
| import json | |
| import traceback | |
| import time | |
| from datetime import datetime, timedelta, timezone | |
| from collections import defaultdict, deque | |
| import ccxt.pro as ccxt # ✅ ملاحظة: هذا هو ccxt.pro (غير متزامن) | |
| import numpy as np | |
| import logging | |
| import ssl | |
| from botocore.exceptions import ClientError | |
| import base58 | |
| from typing import List, Dict, Any, Optional | |
| # --- استيراد من الملفات المحلية الجديدة --- | |
| from .config import ( | |
| DEFAULT_WHALE_THRESHOLD_USD, TRANSFER_EVENT_SIGNATURE, NATIVE_COINS, | |
| DEFAULT_EXCHANGE_ADDRESSES, COINGECKO_BASE_URL, COINGECKO_SYMBOL_MAPPING | |
| ) | |
| from .rpc_manager import AdaptiveRpcManager # (استيراد الموجه الذكي V2) | |
| # تعطيل تسجيل HTTP المزعج | |
| logging.getLogger("httpx").setLevel(logging.WARNING) | |
| logging.getLogger("httpcore").setLevel(logging.WARNING) | |
| class EnhancedWhaleMonitor: | |
| def __init__(self, contracts_db=None, r2_service=None): | |
| print("🔄 [WhaleMonitor V2] بدء التهيئة...") | |
| # 1. الخدمات الخارجية | |
| self.r2_service = r2_service | |
| self.data_manager = None # سيتم تعيينه لاحقاً بواسطة DataManager | |
| # 2. إعدادات الاتصال (يتم تمريرها الآن إلى RpcManager) | |
| self.ssl_context = ssl.create_default_context() | |
| self.http_client = httpx.AsyncClient( | |
| timeout=30.0, | |
| limits=httpx.Limits(max_connections=100, max_keepalive_connections=20), | |
| follow_redirects=True, | |
| verify=self.ssl_context | |
| ) | |
| # 3. تهيئة "الوكيل الذكي" (RPC Manager V2) | |
| # (لم نعد بحاجة لجلب المفاتيح هنا، المدير يفعل ذلك) | |
| self.rpc_manager = AdaptiveRpcManager(self.http_client) | |
| # 4. تحميل الإعدادات من config.py (عبر المدير) | |
| self.whale_threshold_usd = DEFAULT_WHALE_THRESHOLD_USD | |
| self.supported_networks = self.rpc_manager.get_network_configs() | |
| # 5. قواعد البيانات الداخلية و Caches | |
| self.contracts_db = {} | |
| self._initialize_contracts_db(contracts_db or {}) | |
| self.address_labels = {} | |
| self.address_categories = {'exchange': set(), 'cex': set(), 'dex': set(), 'bridge': set(), 'whale': set(), 'unknown': set()} | |
| self._initialize_comprehensive_exchange_addresses() | |
| self.token_price_cache = {} | |
| self.token_decimals_cache = {} | |
| # 6. تحميل العقود من R2 (إن وجد) | |
| if self.r2_service: | |
| asyncio.create_task(self._load_contracts_from_r2()) | |
| print("✅ [WhaleMonitor V2] تم التهيئة بنجاح باستخدام مدير RPC/API الذكي V2.") | |
| # --- (دوال التهيئة وقواعد البيانات - لا تغيير جوهري) --- | |
| def _initialize_contracts_db(self, initial_contracts): | |
| """تهيئة قاعدة بيانات العقود مع تحسين تخزين الشبكات""" | |
| print("🔄 [WhaleMonitor V2] تهيئة قاعدة بيانات العقود...") | |
| for symbol, contract_data in initial_contracts.items(): | |
| symbol_lower = symbol.lower() | |
| if isinstance(contract_data, dict) and 'address' in contract_data and 'network' in contract_data: | |
| self.contracts_db[symbol_lower] = contract_data | |
| elif isinstance(contract_data, str): | |
| self.contracts_db[symbol_lower] = { | |
| 'address': contract_data, | |
| 'network': self._detect_network_from_address(contract_data) | |
| } | |
| print(f"✅ [WhaleMonitor V2] تم تحميل {len(self.contracts_db)} عقد في قاعدة البيانات") | |
| def _detect_network_from_address(self, address): | |
| """اكتشاف الشبكة من عنوان العقد""" | |
| if not isinstance(address, str): return 'ethereum' | |
| address_lower = address.lower() | |
| if address_lower.startswith('0x') and len(address_lower) == 42: | |
| return 'ethereum' # افتراضي لـ EVM | |
| elif len(address_lower) >= 32 and len(address_lower) <= 44: | |
| base58_chars = set("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz") | |
| if all(char in base58_chars for char in address): | |
| try: base58.b58decode(address); return 'solana' | |
| except ValueError: return 'ethereum' | |
| else: return 'ethereum' | |
| else: return 'ethereum' | |
| def _initialize_comprehensive_exchange_addresses(self): | |
| """ | |
| تهيئة قاعدة بيانات عناوين المنصات (تعتمد الآن على config.py). | |
| (تستخدم كاحتياطي إذا فشل Moralis) | |
| """ | |
| for category, addresses in DEFAULT_EXCHANGE_ADDRESSES.items(): | |
| for address in addresses: | |
| if not isinstance(address, str): continue | |
| addr_lower = address.lower() | |
| self.address_labels[addr_lower] = category | |
| self.address_categories['exchange'].add(addr_lower) | |
| if category in ['kucoin', 'binance', 'coinbase', 'kraken', 'okx', 'gate', 'solana_kucoin', 'solana_binance', 'solana_coinbase']: | |
| self.address_categories['cex'].add(addr_lower) | |
| elif category in ['uniswap', 'pancakeswap']: | |
| self.address_categories['dex'].add(addr_lower) | |
| elif 'wormhole' in category: | |
| self.address_categories['bridge'].add(addr_lower) | |
| print(f"✅ [WhaleMonitor V2] تم تهيئة {len(self.address_labels)} عنوان منصة (احتياطي).") | |
| async def _load_contracts_from_r2(self): | |
| # (لا تغيير في هذه الدالة) | |
| if not self.r2_service: return | |
| try: | |
| key = "contracts.json"; response = self.r2_service.s3_client.get_object(Bucket="trading", Key=key) | |
| contracts_data = json.loads(response['Body'].read()); loaded_count = 0; updated_count = 0 | |
| updated_contracts_db = self.contracts_db.copy() | |
| for symbol, contract_data in contracts_data.items(): | |
| symbol_lower = symbol.lower() | |
| if isinstance(contract_data, dict) and 'address' in contract_data and 'network' in contract_data: | |
| updated_contracts_db[symbol_lower] = contract_data; loaded_count += 1 | |
| elif isinstance(contract_data, str): | |
| new_format = {'address': contract_data, 'network': self._detect_network_from_address(contract_data)} | |
| updated_contracts_db[symbol_lower] = new_format; loaded_count += 1; updated_count +=1 | |
| self.contracts_db = updated_contracts_db | |
| print(f"✅ [WhaleMonitor V2] تم تحميل {loaded_count} عقد من R2.") | |
| if updated_count > 0: print(f" ℹ️ تم تحديث صيغة {updated_count} عقد."); await self._save_contracts_to_r2() | |
| except ClientError as e: | |
| if e.response['Error']['Code'] == 'NoSuchKey': print("⚠️ [WhaleMonitor V2] لم يتم العثور على قاعدة بيانات العقود في R2.") | |
| else: print(f"❌ [WhaleMonitor V2] خطأ ClientError أثناء تحميل العقود من R2: {e}") | |
| except Exception as e: print(f"❌ [WhaleMonitor V2] خطأ عام أثناء تحميل العقود من R2: {e}") | |
| async def _save_contracts_to_r2(self): | |
| # (لا تغيير في هذه الدالة) | |
| if not self.r2_service: return | |
| try: | |
| key = "contracts.json"; contracts_to_save = {} | |
| for symbol, data in self.contracts_db.items(): | |
| if isinstance(data, dict) and 'address' in data and 'network' in data: contracts_to_save[symbol] = data | |
| elif isinstance(data, str): contracts_to_save[symbol] = {'address': data, 'network': self._detect_network_from_address(data)} | |
| if not contracts_to_save: print("⚠️ [WhaleMonitor V2] لا توجد بيانات عقود صالحة للحفظ في R2."); return | |
| data_json = json.dumps(contracts_to_save, indent=2).encode('utf-8') | |
| self.r2_service.s3_client.put_object(Bucket="trading", Key=key, Body=data_json, ContentType="application/json") | |
| print(f"✅ [WhaleMonitor V2] تم حفظ قاعدة بيانات العقود ({len(contracts_to_save)} إدخال) إلى R2") | |
| except Exception as e: print(f"❌ [WhaleMonitor V2] فشل حفظ قاعدة البيانات العقود: {e}") | |
| # --- (المنطق الأساسي - محدث بالكامل V2) --- | |
| async def get_symbol_whale_activity(self, symbol: str, daily_volume_usd: float = 0.0) -> Dict[str, Any]: | |
| """ | |
| (محدث V2 - "التراكم والتدفق") | |
| الدالة الرئيسية لتحليل الحيتان. | |
| تنفذ منطق "جلب 1، تحليل N" وتسجل البيانات للتعلم. | |
| """ | |
| try: | |
| print(f"🔍 [WhaleMonitor V2] بدء مراقبة الحيتان (تدفق + تراكم) لـ: {symbol}") | |
| # 1. تصفير إحصائيات الجلسة | |
| self.rpc_manager.reset_session_stats() | |
| # 2. التحقق من العملة الأصلية | |
| base_symbol = symbol.split("/")[0].upper() if '/' in symbol else symbol.upper() | |
| if base_symbol in NATIVE_COINS: | |
| return self._create_native_coin_response(symbol) | |
| # 3. العثور على العقد والشبكة | |
| contract_info = await self._find_contract_address_enhanced(symbol) | |
| if not contract_info or not contract_info.get('address') or not contract_info.get('network'): | |
| print(f"❌ لم يتم العثور على عقد أو شبكة للعملة {symbol}") | |
| return self._create_no_contract_response(symbol) | |
| contract_address = contract_info['address'] | |
| network = contract_info['network'] | |
| print(f"🌐 البحث في الشبكة المحددة: {network} للعقد: {contract_address}") | |
| # 4. جلب بيانات 24 ساعة (جلب 1) | |
| # (سيتم جلب السعر والكسور العشرية داخل هذه الدالة) | |
| current_price = await self._get_token_price(symbol) | |
| if current_price == 0: | |
| print(f"❌ لا يمكن المتابعة بدون سعر لـ {symbol}") | |
| return self._create_error_response(symbol, "Failed to get token price") | |
| decimals = await self._get_token_decimals(contract_address, network) | |
| if decimals is None: | |
| print(f"❌ لا يمكن المتابعة بدون decimals لـ {symbol} على {network}.") | |
| return self._create_error_response(symbol, f"Failed to get decimals on {network}") | |
| # (نطلب 24 ساعة كاملة) | |
| all_transfers_24h = await self._get_targeted_transfer_data(contract_address, network, hours=24, price=current_price, decimals=decimals) | |
| if not all_transfers_24h: | |
| print(f"⚠️ لم يتم العثور على أي تحويلات للعملة {symbol} في آخر 24 ساعة.") | |
| return self._create_no_transfers_response(symbol) | |
| print(f"📊 [WhaleMonitor V2] تم جلب {len(all_transfers_24h)} تحويلة (24س). بدء التحليل المقارن...") | |
| # 5. التقسيم والتحليل (تحليل N) | |
| analysis_windows = [ | |
| {'name': '30m', 'minutes': 30}, | |
| {'name': '1h', 'minutes': 60}, | |
| {'name': '2h', 'minutes': 120}, | |
| {'name': '4h', 'minutes': 240}, | |
| {'name': '24h', 'minutes': 1440} # (24 * 60) | |
| ] | |
| multi_window_analysis = {} | |
| current_time_utc = datetime.now(timezone.utc) | |
| for window in analysis_windows: | |
| window_name = window['name'] | |
| cutoff_timestamp = int((current_time_utc - timedelta(minutes=window['minutes'])).timestamp()) | |
| # (التقسيم - مجاني) | |
| window_transfers = [t for t in all_transfers_24h if int(t.get('timeStamp', 0)) >= cutoff_timestamp] | |
| # (التحليل - مجاني) | |
| analysis_result = self._analyze_transfer_list( | |
| symbol=symbol, | |
| transfers=window_transfers, | |
| daily_volume_usd=daily_volume_usd | |
| ) | |
| multi_window_analysis[window_name] = analysis_result | |
| print(f" -> {window_name}: {analysis_result.get('whale_transfers_count')} تحويلة حوت، صافي ${analysis_result.get('net_flow_usd'):,.0f}") | |
| # 6. تسجيل البيانات للتعلم (إرسالها إلى R2) | |
| api_stats = self.rpc_manager.get_session_stats() | |
| await self._save_learning_record( | |
| symbol=symbol, | |
| start_price_usd=current_price, | |
| multi_window_analysis=multi_window_analysis, | |
| api_stats=api_stats | |
| ) | |
| # 7. إعداد الاستجابة النهائية (للنموذج الضخم) | |
| # (إرسال أفضل نافذة قصيرة المدى + نافذة التراكم 24س) | |
| short_term_analysis = multi_window_analysis.get('1h') # (استخدام ساعة واحدة كافتراضي للنموذج) | |
| long_term_analysis = multi_window_analysis.get('24h') | |
| signal = self._generate_enhanced_trading_signal(short_term_analysis) | |
| llm_summary = self._create_enhanced_llm_summary(signal, short_term_analysis) | |
| final_response = { | |
| 'symbol': symbol, | |
| 'data_available': True, | |
| 'analysis_timestamp': current_time_utc.isoformat(), | |
| 'summary': { # (ملخص عام) | |
| 'total_transfers_24h': len(all_transfers_24h), | |
| 'whale_transfers_24h': long_term_analysis.get('whale_transfers_count', 0), | |
| 'time_window_minutes': 1440, | |
| }, | |
| 'exchange_flows': short_term_analysis, # (بيانات 1س للنموذج) | |
| 'accumulation_analysis_24h': long_term_analysis, # (بيانات 24س للنموذج) | |
| 'trading_signal': signal, | |
| 'llm_friendly_summary': llm_summary | |
| } | |
| return final_response | |
| except Exception as e: | |
| print(f"❌ خطأ فادح في مراقبة الحيتان لـ {symbol}: {e}"); traceback.print_exc() | |
| return self._create_error_response(symbol, str(e)) | |
| async def _save_learning_record(self, symbol: str, start_price_usd: float, multi_window_analysis: Dict, api_stats: Dict): | |
| """ | |
| (جديد V2) | |
| إنشاء السجل الأولي "PENDING" وإرساله إلى R2Service. | |
| """ | |
| if not self.r2_service: | |
| print("⚠️ [WhaleMonitor V2] خدمة R2 غير متاحة، تخطي تسجيل التعلم.") | |
| return | |
| try: | |
| now_utc = datetime.now(timezone.utc) | |
| target_time_utc = now_utc + timedelta(hours=1) | |
| record = { | |
| "record_id": f"whl_{symbol.lower()}_{int(now_utc.timestamp())}", | |
| "symbol": symbol, | |
| "analysis_start_utc": now_utc.isoformat(), | |
| "status": "PENDING_PRICE_CHECK", | |
| "start_price_usd": start_price_usd, | |
| "target_time_utc": target_time_utc.isoformat(), | |
| "target_price_usd": None, | |
| "price_change_percentage": None, | |
| "window_analysis": multi_window_analysis, # (يحتوي على جميع النوافذ والمقاييس) | |
| "api_stats": api_stats | |
| } | |
| # (نفترض أن R2Service لديها هذه الدالة - سنضيفها لاحقاً) | |
| if hasattr(self.r2_service, 'save_whale_learning_record_async'): | |
| await self.r2_service.save_whale_learning_record_async(record) | |
| print(f"✅ [WhaleMonitor V2] تم تسجيل بيانات التعلم (PENDING) لـ {symbol} بنجاح.") | |
| else: | |
| print("❌ [WhaleMonitor V2] R2Service تفتقد دالة save_whale_learning_record_async") | |
| except Exception as e: | |
| print(f"❌ [WhaleMonitor V2] فشل في حفظ سجل التعلم لـ {symbol}: {e}") | |
| traceback.print_exc() | |
| # --- (دوال جلب البيانات - محدثة بالكامل V2) --- | |
| async def _get_targeted_transfer_data(self, contract_address: str, network: str, hours: int, price: float, decimals: int) -> List[Dict[str, Any]]: | |
| """ | |
| (محدث V2) | |
| الوظيفة الرئيسية لجلب البيانات. تعطي الأولوية لـ Moralis. | |
| """ | |
| print(f"🌐 [WhaleMonitor V2] جلب بيانات {hours} ساعة لشبكة {network}...") | |
| # 1. تحديد الأولوية | |
| moralis_key = self.rpc_manager.get_api_key('moralis') | |
| net_config = self.supported_networks.get(network, {}) | |
| moralis_chain_id = net_config.get('moralis_chain_id') | |
| all_transfers = [] | |
| # الأولوية 1: Moralis (لأنه "ذكي" ويعطي الأسماء) | |
| if moralis_key and moralis_chain_id: | |
| try: | |
| print(f" -> [أولوية 1] محاولة Moralis (Chain: {moralis_chain_id})...") | |
| moralis_transfers = await self._get_moralis_token_data(contract_address, moralis_chain_id, hours, price, decimals) | |
| if moralis_transfers: | |
| all_transfers.extend(moralis_transfers) | |
| print(f" ✅ [Moralis] تم جلب {len(moralis_transfers)} تحويلة.") | |
| except Exception as e: | |
| print(f" ⚠️ [Moralis] فشل: {e}. اللجوء إلى Scanners.") | |
| # الأولوية 2: Scanners (Etherscan, BscScan, ...) | |
| # (نستخدمها إذا فشل Moralis أو لجلب المزيد من البيانات) | |
| if not all_transfers: | |
| try: | |
| print(" -> [أولوية 2] محاولة Scanners (Etherscan/BscScan...)...") | |
| scanner_transfers = await self._get_scanner_token_data(contract_address, network, hours, price, decimals) | |
| if scanner_transfers: | |
| all_transfers.extend(scanner_transfers) | |
| print(f" ✅ [Scanners] تم جلب {len(scanner_transfers)} تحويلة.") | |
| except Exception as e: | |
| print(f" ⚠️ [Scanners] فشل: {e}. اللجوء إلى RPC.") | |
| # الأولوية 3: RPC (eth_getLogs) (كآخر حل) | |
| if not all_transfers and net_config.get('type') == 'evm': | |
| try: | |
| print(" -> [أولوية 3] محاولة RPC (eth_getLogs)...") | |
| rpc_transfers = await self._get_rpc_token_data(contract_address, network, hours, price, decimals) | |
| if rpc_transfers: | |
| all_transfers.extend(rpc_transfers) | |
| print(f" ✅ [RPC] تم جلب {len(rpc_transfers)} تحويلة.") | |
| except Exception as e: | |
| print(f" ⚠️ [RPC] فشل: {e}.") | |
| # (ملاحظة: Solana RPC معقد ويحتاج معالجة خاصة، Moralis هو الأفضل لـ Solana) | |
| if not all_transfers: | |
| print(f"❌ [WhaleMonitor V2] فشلت جميع المصادر في جلب التحويلات لـ {network}.") | |
| return [] | |
| # إزالة التكرار (الأولوية لـ Moralis/Scanner) | |
| final_transfers = []; seen_keys = set() | |
| for t in all_transfers: | |
| key = f"{t.get('hash')}-{t.get('logIndex','N/A')}" | |
| if key not in seen_keys: | |
| final_transfers.append(t); seen_keys.add(key) | |
| return final_transfers | |
| async def _get_moralis_token_data(self, contract_address: str, chain_id: str, hours: int, price: float, decimals: int) -> List[Dict]: | |
| """(جديد V2) جلب التحويلات باستخدام Moralis API""" | |
| cutoff_date = datetime.now(timezone.utc) - timedelta(hours=hours) | |
| params = { | |
| "chain": chain_id, | |
| "contract_address": contract_address, | |
| "from_date": cutoff_date.isoformat(), | |
| "limit": 100 # (الحد الأقصى للصفحة) | |
| } | |
| # (استدعاء Moralis API عبر RpcManager) | |
| data = await self.rpc_manager.get_moralis_api( | |
| "https://deep-index.moralis.io/api/v2.2/erc20/transfers", | |
| params=params | |
| ) | |
| if not data or not data.get('result'): | |
| print(" ⚠️ [Moralis] لا توجد نتائج.") | |
| return [] | |
| transfers = [] | |
| for tx in data['result']: | |
| try: | |
| value_raw = tx.get('value') | |
| if not value_raw or not value_raw.isdigit(): continue | |
| value_usd = self._calculate_value_usd(int(value_raw), decimals, price) | |
| if value_usd < 1000: continue # (فلترة أولية للضجيج) | |
| transfers.append({ | |
| 'hash': tx.get('transaction_hash'), | |
| 'logIndex': tx.get('log_index', 'N/A'), | |
| 'from': tx.get('from_address', '').lower(), | |
| 'to': tx.get('to_address', '').lower(), | |
| 'value': value_raw, | |
| 'value_usd': value_usd, | |
| 'timeStamp': str(int(datetime.fromisoformat(tx.get('block_timestamp')).timestamp())), | |
| 'blockNumber': tx.get('block_number'), | |
| 'network': chain_id, | |
| 'source': 'moralis', | |
| # (البيانات الذكية من Moralis) | |
| 'from_label': tx.get('from_address_label'), | |
| 'to_label': tx.get('to_address_label') | |
| }) | |
| except Exception as e: | |
| print(f" ⚠️ [Moralis] خطأ في تحليل تحويلة: {e}") | |
| continue | |
| return transfers | |
| async def _get_scanner_token_data(self, contract_address: str, network: str, hours: int, price: float, decimals: int) -> List[Dict]: | |
| """(محدث V2) جلب التحويلات باستخدام Etherscan/BscScan API""" | |
| explorer_config = self.rpc_manager.get_explorer_config(network) | |
| if not explorer_config or not explorer_config.get('api_url'): | |
| print(f" ⚠️ لا توجد إعدادات مستكشف لشبكة {network}") | |
| return [] | |
| # (محاولة استخدام المفتاح المخصص للشبكة، أو اللجوء إلى مفتاح "etherscan" العام) | |
| api_key = self.rpc_manager.get_api_key(explorer_config['api_key_name']) or self.rpc_manager.get_api_key('etherscan') | |
| if not api_key: | |
| print(f" ⚠️ لا يوجد مفتاح API لـ {network} (أو مفتاح etherscan العام)") | |
| return [] | |
| config = explorer_config | |
| address_param = "contractaddress" if network == 'ethereum' else "address" | |
| params = { | |
| "module": "account", "action": "tokentx", | |
| address_param: contract_address, | |
| "page": 1, "offset": 200, # (جلب المزيد من البيانات) | |
| "sort": "desc", "apikey": api_key | |
| } | |
| data = await self.rpc_manager.get_scanner_api(config['api_url'], params=params) | |
| if not data or str(data.get('status', '0')) != '1': | |
| print(f" ⚠️ [Scanners] {network} لم يُرجع بيانات صالحة.") | |
| return [] | |
| transfers = [] | |
| cutoff_timestamp = int((datetime.now(timezone.utc) - timedelta(hours=hours)).timestamp()) | |
| for tx in data.get('result', []): | |
| try: | |
| timestamp = int(tx.get('timeStamp', '0')) | |
| if timestamp < cutoff_timestamp: | |
| break # (البيانات مرتبة تنازلياً، توقف عند الوصول للبيانات القديمة) | |
| value_raw = tx.get('value') | |
| if not value_raw or not value_raw.isdigit(): continue | |
| value_usd = self._calculate_value_usd(int(value_raw), decimals, price) | |
| if value_usd < 1000: continue # (فلترة أولية للضجيج) | |
| transfers.append({ | |
| 'hash': tx.get('hash'), | |
| 'logIndex': tx.get('logIndex', 'N/A'), | |
| 'from': tx.get('from', '').lower(), | |
| 'to': tx.get('to', '').lower(), | |
| 'value': value_raw, | |
| 'value_usd': value_usd, | |
| 'timeStamp': str(timestamp), | |
| 'blockNumber': tx.get('blockNumber'), | |
| 'network': network, | |
| 'source': 'scanner' | |
| }) | |
| except Exception as e: | |
| print(f" ⚠️ [Scanners] خطأ في تحليل تحويلة: {e}") | |
| continue | |
| return transfers | |
| async def _get_rpc_token_data(self, contract_address: str, network: str, hours: int, price: float, decimals: int) -> List[Dict]: | |
| """(محدث V2) جلب التحويلات باستخدام eth_getLogs عبر RpcManager""" | |
| try: | |
| # (تقريب عدد الكتل بناءً على الوقت - تقدير متحفظ) | |
| # (نفترض 15 ثانية للكتلة لـ ETH/Polygon, و 3 ثوان لـ BSC) | |
| avg_block_time = 15 if network not in ['bsc'] else 3 | |
| blocks_to_scan = int((hours * 3600) / avg_block_time) | |
| payload_block = {"jsonrpc": "2.0", "method": "eth_blockNumber", "params": [], "id": int(time.time())} | |
| json_response_block = await self.rpc_manager.post_rpc(network, payload_block) | |
| if not json_response_block or not json_response_block.get('result'): | |
| print(f" ❌ [RPC] لم يتم الحصول على رقم أحدث كتلة من {network}") | |
| return [] | |
| latest_block = int(json_response_block['result'], 16) | |
| from_block = max(0, latest_block - blocks_to_scan) | |
| print(f" 📦 [RPC] فحص الكتل من {from_block} إلى {latest_block} على {network}") | |
| payload_logs = { | |
| "jsonrpc": "2.0", "method": "eth_getLogs", | |
| "params": [{"fromBlock": hex(from_block), "toBlock": hex(latest_block), "address": contract_address, "topics": [TRANSFER_EVENT_SIGNATURE]}], | |
| "id": int(time.time())+2 | |
| } | |
| json_response_logs = await self.rpc_manager.post_rpc(network, payload_logs, timeout=45.0) | |
| if not json_response_logs or json_response_logs.get('result') is None: | |
| print(f" ❌ [RPC] استجابة السجلات غير صالحة من {network}."); return [] | |
| logs = json_response_logs.get('result', []) | |
| if not logs: | |
| print(f" ✅ [RPC] لا توجد سجلات تحويل لـ {contract_address}."); return [] | |
| transfers = [] | |
| # (نحتاج لجلب الوقت الفعلي للكتل) | |
| block_timestamps = await self._get_block_timestamps_rpc(network, [log.get('blockNumber') for log in logs]) | |
| for log in logs: | |
| try: | |
| value_raw = log.get('data', '0x') | |
| block_num_hex = log.get('blockNumber') | |
| if not value_raw or not block_num_hex or value_raw == '0x' or len(log.get('topics', [])) != 3: | |
| continue | |
| value_int = int(value_raw, 16) | |
| value_usd = self._calculate_value_usd(value_int, decimals, price) | |
| if value_usd < 1000: continue # (فلترة أولية للضجيج) | |
| timestamp = block_timestamps.get(block_num_hex, str(int(time.time()))) | |
| transfers.append({ | |
| 'hash': log.get('transactionHash'), | |
| 'logIndex': str(int(log.get('logIndex', '0x0'), 16)), | |
| 'from': '0x' + log['topics'][1][26:].lower(), | |
| 'to': '0x' + log['topics'][2][26:].lower(), | |
| 'value': str(value_int), | |
| 'value_usd': value_usd, | |
| 'timeStamp': timestamp, | |
| 'blockNumber': str(int(block_num_hex, 16)), | |
| 'network': network, | |
| 'source': 'rpc' | |
| }) | |
| except Exception as e: | |
| print(f" ⚠️ [RPC] خطأ في تحليل سجل: {e}") | |
| continue | |
| return transfers | |
| except Exception as e: | |
| print(f"❌ فشل عام في جلب بيانات RPC لـ {network}: {e}"); traceback.print_exc() | |
| return [] | |
| async def _get_block_timestamps_rpc(self, network: str, block_hex_numbers: List[str]) -> Dict[str, str]: | |
| """(جديد V2) جلب أوقات الكتل بكفاءة عبر RPC""" | |
| timestamps = {} | |
| unique_blocks = set(b for b in block_hex_numbers if b) | |
| if not unique_blocks: return {} | |
| tasks = [] | |
| for block_hex in unique_blocks: | |
| payload = {"jsonrpc": "2.0", "method": "eth_getBlockByNumber", "params": [block_hex, False], "id": int(time.time()*1000)} | |
| tasks.append(self.rpc_manager.post_rpc(network, payload)) | |
| results = await asyncio.gather(*tasks) | |
| for res in results: | |
| if res and res.get('result') and res['result'].get('timestamp'): | |
| try: | |
| block_num_hex = res['result']['number'] | |
| timestamp_hex = res['result']['timestamp'] | |
| timestamps[block_num_hex] = str(int(timestamp_hex, 16)) | |
| except Exception: | |
| pass | |
| return timestamps | |
| # --- (دوال جلب الأسعار والكسور العشرية - محدثة V2) --- | |
| async def _get_token_decimals(self, contract_address, network): | |
| """(محدث V2) جلب الكسور العشرية (يستخدم RpcManager)""" | |
| cache_key = f"{contract_address.lower()}_{network}" | |
| if cache_key in self.token_decimals_cache: | |
| return self.token_decimals_cache[cache_key] | |
| if network in self.supported_networks and self.supported_networks[network]['type'] == 'evm': | |
| payload = { | |
| "jsonrpc": "2.0", "method": "eth_call", | |
| "params": [{"to": contract_address, "data": "0x313ce567"}, "latest"], | |
| "id": int(time.time()) | |
| } | |
| response_json = await self.rpc_manager.post_rpc(network, payload) | |
| if response_json and response_json.get('result') not in [None, '0x', '']: | |
| try: | |
| decimals = int(response_json['result'], 16) | |
| self.token_decimals_cache[cache_key] = decimals | |
| return decimals | |
| except Exception: pass | |
| # (يمكن إضافة دعم Solana هنا إذا لزم الأمر) | |
| print(f"❌ فشل جلب الكسور العشرية لـ {contract_address} على {network}.") | |
| return None | |
| async def _get_token_price(self, symbol): | |
| """(محدث V2) جلب السعر (KuCoin أولاً، ثم CoinGecko كاحتياطي)""" | |
| base_symbol = symbol.split('/')[0].upper() | |
| cache_entry = self.token_price_cache.get(base_symbol) | |
| if cache_entry and time.time() - cache_entry.get('timestamp', 0) < 300: | |
| return cache_entry['price'] | |
| # 1. جرب KuCoin (عبر DataManager إذا كان متاحاً) | |
| if self.data_manager and hasattr(self.data_manager, 'get_latest_price_async'): | |
| price = await self.data_manager.get_latest_price_async(symbol) | |
| if price is not None and price > 0: | |
| self.token_price_cache[base_symbol] = {'price': price, 'timestamp': time.time()}; return price | |
| # 2. جرب CoinGecko (عبر RpcManager) | |
| price = await self._get_token_price_from_coingecko(symbol) | |
| if price is not None and price > 0: | |
| self.token_price_cache[base_symbol] = {'price': price, 'timestamp': time.time()}; return price | |
| print(f"❌ فشل جميع محاولات جلب سعر العملة {symbol}"); self.token_price_cache[base_symbol] = {'price': 0, 'timestamp': time.time()}; return 0 | |
| async def _get_token_price_from_coingecko(self, symbol): | |
| """(محدث V2) جلب سعر CoinGecko (يستخدم RpcManager)""" | |
| try: | |
| base_symbol = symbol.split('/')[0].upper() | |
| coingecko_id = COINGECKO_SYMBOL_MAPPING.get(base_symbol, base_symbol.lower()) | |
| params = {"ids": coingecko_id, "vs_currencies": "usd"} | |
| data = await self.rpc_manager.get_coingecko_api(params=params) | |
| if data and data.get(coingecko_id) and 'usd' in data[coingecko_id]: | |
| return data[coingecko_id]['usd'] | |
| print(f"⚠️ لم يتم العثور على سعر لـ {coingecko_id} في CoinGecko، محاولة البحث...") | |
| search_params = {"query": base_symbol} | |
| search_data = await self.rpc_manager.get_coingecko_api(params=search_params) | |
| if search_data and search_data.get('coins'): | |
| coins = search_data['coins'] | |
| found_id = next((coin.get('id') for coin in coins if coin.get('symbol', '').lower() == base_symbol.lower()), coins[0].get('id') if coins else None) | |
| if found_id: | |
| print(f" 🔄 تم العثور على معرف بديل: {found_id}. إعادة المحاولة...") | |
| params["ids"] = found_id | |
| data = await self.rpc_manager.get_coingecko_api(params=params) | |
| if data and data.get(found_id) and 'usd' in data[found_id]: | |
| return data[found_id]['usd'] | |
| return 0 | |
| except Exception as e: | |
| print(f"❌ فشل عام في _get_token_price_from_coingecko لـ {symbol}: {e}"); return 0 | |
| async def _find_contract_via_coingecko(self, symbol): | |
| """(محدث V2) البحث عن عقد CoinGecko (يستخدم RpcManager)""" | |
| try: | |
| search_params = {"query": symbol} | |
| data = await self.rpc_manager.get_coingecko_api(params=search_params) | |
| if not data or not data.get('coins'): return None | |
| coins = data.get('coins', []) | |
| best_coin = next((coin for coin in coins if coin.get('symbol', '').lower() == symbol.lower()), coins[0] if coins else None) | |
| coin_id = best_coin.get('id') | |
| if not coin_id: return None | |
| print(f" 🔍 [CoinGecko] جلب تفاصيل المعرف: {coin_id}") | |
| 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"}) | |
| if not detail_data or not detail_data.get(coin_id) or not detail_data[coin_id].get('platforms'): | |
| print(f" ❌ [CoinGecko] فشل جلب تفاصيل المنصات لـ {coin_id}"); return None | |
| platforms = detail_data[coin_id]['platforms'] | |
| network_priority = ['ethereum', 'binance-smart-chain', 'polygon-pos', 'arbitrum-one', 'optimistic-ethereum', 'avalanche', 'fantom', 'solana'] | |
| network_map = {'ethereum': 'ethereum', 'binance-smart-chain': 'bsc', 'polygon-pos': 'polygon', 'arbitrum-one': 'arbitrum', 'optimistic-ethereum': 'optimism', 'avalanche': 'avalanche', 'fantom': 'fantom', 'solana': 'solana'} | |
| for platform_cg in network_priority: | |
| address = platforms.get(platform_cg) | |
| if address and isinstance(address, str) and address.strip(): | |
| network = network_map.get(platform_cg) | |
| if network: | |
| print(f" ✅ [CoinGecko] وجد عقداً على {network}: {address}") | |
| return address, network | |
| return None | |
| except Exception as e: | |
| print(f"❌ فشل عام في _find_contract_via_coingecko لـ {symbol}: {e}"); traceback.print_exc(); return None | |
| # --- (دوال التحليل والمساعدة - محدثة V2) --- | |
| def _calculate_value_usd(self, raw_value: int, decimals: int, price: float) -> float: | |
| """(جديد V2) دالة مساعدة لحساب القيمة بالدولار بأمان""" | |
| try: | |
| if price == 0: return 0.0 | |
| token_amount = raw_value / (10 ** decimals) | |
| return token_amount * price | |
| except Exception: | |
| return 0.0 | |
| def _analyze_transfer_list(self, symbol: str, transfers: List[Dict], daily_volume_usd: float) -> Dict[str, Any]: | |
| """ | |
| (جديد V2) | |
| المنطق المركزي لتحليل قائمة التحويلات. | |
| يحسب جميع المقاييس (المطلقة والنسبية) لنافذة واحدة. | |
| """ | |
| stats = { | |
| 'to_exchanges_usd': 0.0, 'from_exchanges_usd': 0.0, | |
| 'deposit_count': 0, 'withdrawal_count': 0, | |
| 'whale_transfers_count': 0, 'total_whale_volume_usd': 0.0, | |
| 'top_deposits': [], 'top_withdrawals': [] | |
| } | |
| for tx in transfers: | |
| # (نستخدم القيمة المحسوبة مسبقاً إذا كانت موجودة، وإلا نستخدم 0) | |
| value_usd = tx.get('value_usd', 0.0) | |
| if value_usd == 0.0: continue | |
| # (نستخدم التصنيف "الذكي" من Moralis أولاً) | |
| is_to_exchange = tx.get('to_label') is not None | |
| is_from_exchange = tx.get('from_label') is not None | |
| # (إذا فشل Moralis، نستخدم القائمة الاحتياطية) | |
| if not is_to_exchange and not is_from_exchange: | |
| is_to_exchange = self._is_exchange_address(tx.get('to')) | |
| is_from_exchange = self._is_exchange_address(tx.get('from')) | |
| if value_usd >= self.whale_threshold_usd: | |
| stats['whale_transfers_count'] += 1 | |
| stats['total_whale_volume_usd'] += value_usd | |
| if is_to_exchange: | |
| stats['to_exchanges_usd'] += value_usd | |
| stats['deposit_count'] += 1 | |
| stats['top_deposits'].append({'v': value_usd, 'to': tx.get('to_label', self._classify_address(tx.get('to')))}) | |
| elif is_from_exchange: | |
| stats['from_exchanges_usd'] += value_usd | |
| stats['withdrawal_count'] += 1 | |
| stats['top_withdrawals'].append({'v': value_usd, 'from': tx.get('from_label', self._classify_address(tx.get('from')))}) | |
| # --- حساب المقاييس النهائية (المطلقة) --- | |
| net_flow_usd = stats['to_exchanges_usd'] - stats['from_exchanges_usd'] | |
| # --- حساب المقاييس النهائية (النسبية) --- | |
| relative_net_flow_percent = 0.0 | |
| transaction_density = 0.0 | |
| if daily_volume_usd > 0: | |
| # (المقياس الذكي 1: نسبة صافي التدفق) | |
| relative_net_flow_percent = (net_flow_usd / daily_volume_usd) * 100 | |
| # (المقياس الذكي 2: كثافة التحويلات) | |
| total_transactions = stats['deposit_count'] + stats['withdrawal_count'] | |
| volume_in_millions = daily_volume_usd / 1_000_000 | |
| if volume_in_millions > 0: | |
| transaction_density = total_transactions / volume_in_millions | |
| # (فرز أفضل 3 تحويلات) | |
| top_deposits = sorted(stats['top_deposits'], key=lambda x: x['v'], reverse=True)[:3] | |
| top_withdrawals = sorted(stats['top_withdrawals'], key=lambda x: x['v'], reverse=True)[:3] | |
| return { | |
| 'symbol': symbol, | |
| 'total_transfers_analyzed': len(transfers), | |
| 'whale_transfers_count': stats['whale_transfers_count'], | |
| 'total_whale_volume_usd': stats['total_whale_volume_usd'], | |
| 'to_exchanges_usd': stats['to_exchanges_usd'], | |
| 'from_exchanges_usd': stats['from_exchanges_usd'], | |
| 'net_flow_usd': net_flow_usd, | |
| 'deposit_count': stats['deposit_count'], | |
| 'withdrawal_count': stats['withdrawal_count'], | |
| # (المقاييس الذكية للتعلم) | |
| 'relative_net_flow_percent': relative_net_flow_percent, | |
| 'transaction_density': transaction_density, | |
| 'top_deposits': top_deposits, | |
| 'top_withdrawals': top_withdrawals | |
| } | |
| def _generate_enhanced_trading_signal(self, analysis: Dict[str, Any]) -> Dict[str, Any]: | |
| """(محدث V2) توليد إشارة تداول بناءً على تحليل *نافذة واحدة*""" | |
| if not analysis: | |
| return {'action': 'HOLD', 'confidence': 0.3, 'reason': 'No analysis data provided.', 'critical_alert': False} | |
| net_flow_usd = analysis.get('net_flow_usd', 0.0) | |
| deposit_count = analysis.get('deposit_count', 0) | |
| withdrawal_count = analysis.get('withdrawal_count', 0) | |
| whale_count = analysis.get('whale_transfers_count', 0) | |
| action = 'HOLD'; confidence = 0.5; reason = f'Whale activity: {whale_count} large transfers. Net flow ${net_flow_usd:,.0f}'; critical_alert = False | |
| if net_flow_usd > 500000 and deposit_count >= 2: | |
| action = 'STRONG_SELL'; confidence = 0.85 | |
| elif net_flow_usd > 150000 and deposit_count >= 1: | |
| action = 'SELL'; confidence = 0.7 | |
| elif net_flow_usd < -500000 and withdrawal_count >= 2: | |
| action = 'STRONG_BUY'; confidence = 0.85 | |
| elif net_flow_usd < -150000 and withdrawal_count >= 1: | |
| action = 'BUY'; confidence = 0.7 | |
| elif whale_count == 0: | |
| action = 'HOLD'; confidence = 0.3; reason = 'No significant whale activity detected.' | |
| return {'action': action, 'confidence': confidence, 'reason': reason, 'critical_alert': critical_alert} | |
| def _create_enhanced_llm_summary(self, signal: Dict, analysis: Dict) -> Dict[str, Any]: | |
| """(محدث V2) إنشاء ملخص للنموذج الضخم (بناءً على نافذة واحدة)""" | |
| if not analysis: | |
| return {'whale_activity_summary': 'No analysis data.', 'recommended_action': 'HOLD', 'confidence': 0.3, 'key_metrics': {}} | |
| return { | |
| 'whale_activity_summary': signal.get('reason', 'N/A'), | |
| 'recommended_action': signal.get('action', 'HOLD'), | |
| 'confidence': signal.get('confidence', 0.3), | |
| 'key_metrics': { | |
| 'total_whale_transfers': analysis.get('whale_transfers_count', 0), | |
| 'net_flow_usd': analysis.get('net_flow_usd', 0.0), | |
| 'relative_net_flow_percent': analysis.get('relative_net_flow_percent', 0.0), | |
| 'transaction_density': analysis.get('transaction_density', 0.0), | |
| 'data_quality': 'REAL_TIME' | |
| } | |
| } | |
| async def _find_contract_address_enhanced(self, symbol): | |
| """(محدث V2) بحث متقدم عن عقد العملة (يستخدم RpcManager)""" | |
| base_symbol = symbol.split("/")[0].upper() if '/' in symbol else symbol.upper(); symbol_lower = base_symbol.lower() | |
| print(f"🔍 [WhaleMonitor V2] البحث عن عقد للعملة: {symbol}") | |
| if symbol_lower in self.contracts_db: | |
| contract_info = self.contracts_db[symbol_lower] | |
| if isinstance(contract_info, str): | |
| network = self._detect_network_from_address(contract_info) | |
| contract_info = {'address': contract_info, 'network': network}; self.contracts_db[symbol_lower] = contract_info | |
| if 'address' in contract_info and 'network' in contract_info: | |
| print(f" ✅ وجد في قاعدة البيانات المحلية: {contract_info}"); return contract_info | |
| print(f" 🔍 البحث في CoinGecko عن {base_symbol}...") | |
| coingecko_result = await self._find_contract_via_coingecko(base_symbol) | |
| if coingecko_result: | |
| address, network = coingecko_result; contract_info = {'address': address, 'network': network} | |
| self.contracts_db[symbol_lower] = contract_info | |
| print(f" ✅ تم العثور على عقد {symbol} عبر CoinGecko على شبكة {network}: {address}") | |
| if self.r2_service: await self._save_contracts_to_r2() | |
| return contract_info | |
| print(f" ❌ فشل العثور على عقد لـ {symbol} في جميع المصادر"); return None | |
| # --- (الدوال المساعدة والتصنيف - لا تغيير) --- | |
| def _is_exchange_address(self, address): | |
| try: | |
| if not isinstance(address, str): return False | |
| return address.lower() in self.address_categories['exchange'] | |
| except Exception: return False | |
| def _classify_address(self, address): | |
| try: | |
| if not isinstance(address, str): return 'unknown' | |
| return self.address_labels.get(address.lower(), 'unknown') | |
| except Exception: return 'unknown' | |
| # --- (دوال إنشاء الردود - لا تغيير) --- | |
| def _create_native_coin_response(self, symbol): | |
| return {'symbol': symbol, 'data_available': False, 'error': 'NATIVE_COIN_NO_TOKEN', 'trading_signal': {'action': 'HOLD', 'confidence': 0.3, 'reason': f'{symbol} عملة أصلية - لا توجد بيانات حيتان للتوكنات'}, 'llm_friendly_summary': {'whale_activity_summary': f'{symbol} عملة أصلية - نظام مراقبة الحيتان الحالي مصمم للتوكنات فقط', 'recommended_action': 'HOLD', 'confidence': 0.3, 'key_metrics': {'whale_movement_impact': 'NOT_APPLICABLE'}}} | |
| def _create_no_contract_response(self, symbol): | |
| return {'symbol': symbol, 'data_available': False, 'error': 'NO_CONTRACT_FOUND', 'trading_signal': {'action': 'HOLD', 'confidence': 0.3, 'reason': 'لم يتم العثور على عقد العملة - لا توجد بيانات حيتان'}, 'llm_friendly_summary': {'whale_activity_summary': 'لا توجد بيانات عن تحركات الحيتان - لم يتم العثور على عقد العملة', 'recommended_action': 'HOLD', 'confidence': 0.3, 'key_metrics': {'whale_movement_impact': 'NO_DATA'}}} | |
| def _create_no_transfers_response(self, symbol): | |
| return {'symbol': symbol, 'data_available': False, 'error': 'NO_TRANSFERS_FOUND', 'trading_signal': {'action': 'HOLD', 'confidence': 0.3, 'reason': 'لا توجد تحويلات للعملة - لا توجد بيانات حيتان'}, 'llm_friendly_summary': {'whale_activity_summary': 'لا توجد تحويلات حديثة للعملة - لا توجد بيانات حيتان', 'recommended_action': 'HOLD', 'confidence': 0.3, 'key_metrics': {'whale_movement_impact': 'NO_DATA'}}} | |
| def _create_error_response(self, symbol, error_msg): | |
| return {'symbol': symbol, 'data_available': False, 'error': f'ANALYSIS_ERROR: {error_msg}', 'trading_signal': {'action': 'HOLD', 'confidence': 0.3, 'reason': f'خطأ في تحليل الحيتان: {error_msg} - لا توجد بيانات حيتان'}, 'llm_friendly_summary': {'whale_activity_summary': f'فشل في تحليل تحركات الحيتان: {error_msg} - لا توجد بيانات حيتان', 'recommended_action': 'HOLD', 'confidence': 0.3, 'key_metrics': {'whale_movement_impact': 'ERROR'}}} | |
| async def generate_whale_trading_signal(self, symbol, whale_data, market_context): | |
| """(لا تغيير) دالة مساعدة لـ DataManager""" | |
| try: | |
| if not whale_data or not whale_data.get('data_available', False): | |
| return {'action': 'HOLD', 'confidence': 0.3, 'reason': 'لا توجد بيانات كافية عن نشاط الحيتان', 'source': 'whale_analysis', 'data_available': False} | |
| whale_signal = whale_data.get('trading_signal', {}); analysis_details = whale_data | |
| return {'action': whale_signal.get('action', 'HOLD'), 'confidence': whale_signal.get('confidence', 0.3), 'reason': whale_signal.get('reason', 'تحليل الحيتان غير متوفر'), 'source': 'whale_analysis', 'critical_alert': whale_signal.get('critical_alert', False), 'data_available': True, 'whale_analysis_details': analysis_details} | |
| except Exception as e: | |
| print(f"❌ خطأ في توليد إشارة تداول الحيتان لـ {symbol}: {e}") | |
| return {'action': 'HOLD', 'confidence': 0.3, 'reason': f'خطأ في تحليل الحيتان: {str(e)} - لا توجد بيانات حيتان', 'source': 'whale_analysis', 'data_available': False} | |
| async def cleanup(self): | |
| """(محدث V2) تنظيف الموارد عند الإغلاق""" | |
| if hasattr(self, 'http_client') and self.http_client and not self.http_client.is_closed: | |
| try: | |
| await self.http_client.aclose() | |
| print("✅ تم إغلاق اتصال HTTP client لمراقب الحيتان.") | |
| except Exception as e: | |
| print(f"⚠️ خطأ أثناء إغلاق HTTP client لمراقب الحيتان: {e}") |