|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
) |
|
|
|
|
|
|
|
|
self.rpc_manager = AdaptiveRpcManager(self.http_client) |
|
|
|
|
|
|
|
|
self.whale_threshold_usd = DEFAULT_WHALE_THRESHOLD_USD |
|
|
self.supported_networks = self.rpc_manager.get_network_configs() |
|
|
|
|
|
|
|
|
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 = {} |
|
|
|
|
|
|
|
|
if self.r2_service: |
|
|
asyncio.create_task(self._load_contracts_from_r2()) |
|
|
|
|
|
print("✅ [WhaleMonitor V2.1] تم التهيئة بنجاح باستخدام مدير RPC/API الذكي V2.1.") |
|
|
|
|
|
|
|
|
|
|
|
def _initialize_contracts_db(self, initial_contracts): |
|
|
"""تهيئة قاعدة بيانات العقود مع تحسين تخزين الشبكات""" |
|
|
print("🔄 [WhaleMonitor V2.1] تهيئة قاعدة بيانات العقود...") |
|
|
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.1] تم تحميل {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' |
|
|
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.1] تم تهيئة {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.1] تم تحميل {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.1] لم يتم العثور على قاعدة بيانات العقود في R2.") |
|
|
else: print(f"❌ [WhaleMonitor V2.1] خطأ ClientError أثناء تحميل العقود من R2: {e}") |
|
|
except Exception as e: print(f"❌ [WhaleMonitor V2.1] خطأ عام أثناء تحميل العقود من 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.1] لا توجد بيانات عقود صالحة للحفظ في 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.1] تم حفظ قاعدة بيانات العقود ({len(contracts_to_save)} إدخال) إلى R2") |
|
|
except Exception as e: print(f"❌ [WhaleMonitor V2.1] فشل حفظ قاعدة البيانات العقود: {e}") |
|
|
|
|
|
|
|
|
|
|
|
async def get_symbol_whale_activity(self, symbol: str, daily_volume_usd: float = 0.0) -> Dict[str, Any]: |
|
|
""" |
|
|
(محدث V2.1 - "التراكم والتدفق") |
|
|
الدالة الرئيسية لتحليل الحيتان. |
|
|
تنفذ منطق "جلب 1، تحليل N" وتسجل البيانات للتعلم. |
|
|
""" |
|
|
try: |
|
|
print(f"🔍 [WhaleMonitor V2.1] بدء مراقبة الحيتان (تدفق + تراكم) لـ: {symbol}") |
|
|
|
|
|
|
|
|
self.rpc_manager.reset_session_stats() |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
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.1] تم جلب {len(all_transfers_24h)} تحويلة (24س). بدء التحليل المقارن...") |
|
|
|
|
|
|
|
|
analysis_windows = [ |
|
|
{'name': '30m', 'minutes': 30}, |
|
|
{'name': '1h', 'minutes': 60}, |
|
|
{'name': '2h', 'minutes': 120}, |
|
|
{'name': '4h', 'minutes': 240}, |
|
|
{'name': '24h', 'minutes': 1440} |
|
|
] |
|
|
|
|
|
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}") |
|
|
|
|
|
|
|
|
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 |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
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, |
|
|
'accumulation_analysis_24h': long_term_analysis, |
|
|
'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.1] خدمة 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 |
|
|
} |
|
|
|
|
|
if hasattr(self.r2_service, 'save_whale_learning_record_async'): |
|
|
await self.r2_service.save_whale_learning_record_async(record) |
|
|
print(f"✅ [WhaleMonitor V2.1] تم تسجيل بيانات التعلم (PENDING) لـ {symbol} بنجاح.") |
|
|
else: |
|
|
print("❌ [WhaleMonitor V2.1] R2Service تفتقد دالة save_whale_learning_record_async") |
|
|
|
|
|
except Exception as e: |
|
|
print(f"❌ [WhaleMonitor V2.1] فشل في حفظ سجل التعلم لـ {symbol}: {e}") |
|
|
traceback.print_exc() |
|
|
|
|
|
|
|
|
|
|
|
async def _get_targeted_transfer_data(self, contract_address: str, network: str, hours: int, price: float, decimals: int) -> List[Dict[str, Any]]: |
|
|
""" |
|
|
(محدث V2.1) |
|
|
الوظيفة الرئيسية لجلب البيانات. تطبق منطق الأولوية الصحيح (Solscan > Moralis > Scanners > RPC) |
|
|
وتقوم بتجميع النتائج. |
|
|
""" |
|
|
print(f"🌐 [WhaleMonitor V2.1] جلب بيانات {hours} ساعة لشبكة {network} (منطق تجميعي)...") |
|
|
|
|
|
net_config = self.supported_networks.get(network, {}) |
|
|
all_transfers = [] |
|
|
|
|
|
|
|
|
|
|
|
if network == 'solana': |
|
|
|
|
|
print(f" -> [أولوية 1 - SOL] محاولة Solscan API...") |
|
|
try: |
|
|
solscan_transfers = await self._get_solscan_token_data(contract_address, hours, price) |
|
|
if solscan_transfers: |
|
|
all_transfers.extend(solscan_transfers) |
|
|
print(f" ✅ [Solscan] تم جلب {len(solscan_transfers)} تحويلة.") |
|
|
except Exception as e: |
|
|
print(f" ⚠️ [Solscan] فشل: {e}. اللجوء إلى Moralis...") |
|
|
|
|
|
|
|
|
if not all_transfers: |
|
|
print(f" -> [أولوية 2 - SOL] محاولة Moralis...") |
|
|
try: |
|
|
moralis_transfers = await self._get_moralis_token_data(contract_address, 'sol', 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}.") |
|
|
|
|
|
elif net_config.get('type') == 'evm': |
|
|
|
|
|
tasks = [] |
|
|
|
|
|
|
|
|
if self.rpc_manager.get_api_key('moralis') and net_config.get('moralis_chain_id'): |
|
|
print(f" -> [EVM] إضافة مهمة Moralis...") |
|
|
tasks.append(asyncio.create_task(self._get_moralis_token_data(contract_address, net_config['moralis_chain_id'], hours, price, decimals))) |
|
|
|
|
|
|
|
|
if self.rpc_manager.get_explorer_config(network): |
|
|
print(f" -> [EVM] إضافة مهمة Scanners...") |
|
|
tasks.append(asyncio.create_task(self._get_scanner_token_data(contract_address, network, hours, price, decimals))) |
|
|
|
|
|
|
|
|
results = await asyncio.gather(*tasks, return_exceptions=True) |
|
|
|
|
|
for res in results: |
|
|
if isinstance(res, list): all_transfers.extend(res) |
|
|
elif isinstance(res, Exception): print(f" ⚠️ [EVM Gather] فشلت إحدى المهام: {res}") |
|
|
|
|
|
|
|
|
if not all_transfers: |
|
|
print(f" -> [EVM] فشلت الواجهات الخاصة، اللجوء إلى RPC...") |
|
|
try: |
|
|
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}.") |
|
|
|
|
|
|
|
|
|
|
|
if not all_transfers: |
|
|
print(f"❌ [WhaleMonitor V2.1] فشلت جميع المصادر في جلب التحويلات لـ {network}.") |
|
|
return [] |
|
|
|
|
|
|
|
|
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_solscan_token_data(self, token_address: str, hours: int, price: float) -> List[Dict]: |
|
|
"""(جديد V2.1) جلب التحويلات باستخدام Solscan Pro API""" |
|
|
print(f" 🔍 [Solscan] جلب التحويلات لـ {token_address}") |
|
|
|
|
|
|
|
|
params = {"limit": 50} |
|
|
path = f"/v2.0/token/transfer/{token_address}" |
|
|
|
|
|
data = await self.rpc_manager.get_solscan_api(path, params) |
|
|
|
|
|
if not data or not data.get('data'): |
|
|
print(" ⚠️ [Solscan] لا توجد نتائج.") |
|
|
return [] |
|
|
|
|
|
transfers = [] |
|
|
cutoff_timestamp = int((datetime.now(timezone.utc) - timedelta(hours=hours)).timestamp()) |
|
|
|
|
|
for tx in data['data']: |
|
|
try: |
|
|
timestamp = tx.get('blockTime') |
|
|
if not timestamp or timestamp < cutoff_timestamp: |
|
|
continue |
|
|
|
|
|
change_amount_raw = tx.get('changeAmount') |
|
|
if not change_amount_raw: continue |
|
|
|
|
|
|
|
|
decimals = tx.get('token', {}).get('decimals', 9) |
|
|
value_usd = self._calculate_value_usd(int(change_amount_raw), decimals, price) |
|
|
if value_usd < 1000: continue |
|
|
|
|
|
transfers.append({ |
|
|
'hash': tx.get('signature'), |
|
|
'logIndex': tx.get('innerInstruction', [{}])[0].get('index', 0), |
|
|
'from': tx.get('changeOwner', {}).get('from'), |
|
|
'to': tx.get('changeOwner', {}).get('to'), |
|
|
'value': change_amount_raw, |
|
|
'value_usd': value_usd, |
|
|
'timeStamp': str(timestamp), |
|
|
'blockNumber': str(tx.get('slot')), |
|
|
'network': 'solana', |
|
|
'source': 'solscan' |
|
|
|
|
|
}) |
|
|
except Exception as e: |
|
|
print(f" ⚠️ [Solscan] خطأ في تحليل تحويلة: {e}") |
|
|
continue |
|
|
return 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 |
|
|
} |
|
|
|
|
|
|
|
|
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', |
|
|
|
|
|
'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 [] |
|
|
|
|
|
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: |
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
async def _get_token_decimals(self, contract_address, network): |
|
|
"""(محدث V2.1) جلب الكسور العشرية (يستخدم RpcManager ويدعم Solscan)""" |
|
|
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 |
|
|
|
|
|
|
|
|
elif network == 'solana': |
|
|
print(f" 🔍 [Solscan] جلب الكسور العشرية لـ {contract_address}...") |
|
|
path = f"/v2.0/token/meta" |
|
|
params = {"tokenAddress": contract_address} |
|
|
data = await self.rpc_manager.get_solscan_api(path, params) |
|
|
|
|
|
if data and data.get('data') and data['data'].get('decimals') is not None: |
|
|
try: |
|
|
decimals = int(data['data']['decimals']) |
|
|
self.token_decimals_cache[cache_key] = decimals |
|
|
print(f" ✅ [Solscan] تم العثور على الكسور العشرية: {decimals}") |
|
|
return decimals |
|
|
except Exception as e: |
|
|
print(f" ⚠️ [Solscan] فشل تحليل الكسور العشرية: {e}") |
|
|
|
|
|
|
|
|
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'] |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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}") |
|
|
|
|
|
path = f"/api/v3/coins/{coin_id}" |
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
base_url = COINGECKO_BASE_URL |
|
|
full_url = f"{base_url}/coins/{coin_id}" |
|
|
params={"localization": "false", "tickers": "false", "market_data": "false", "community_data": "false", "developer_data": "false", "sparkline": "false"} |
|
|
detail_data = await self.rpc_manager.get_coingecko_api(params=params, custom_base_url=full_url) |
|
|
|
|
|
|
|
|
base_url = "https://api.coingecko.com/api/v3" |
|
|
full_url = f"{base_url}/coins/{coin_id}" |
|
|
params = {"localization": "false", "tickers": "false", "market_data": "false", "community_data": "false", "developer_data": "false", "sparkline": "false"} |
|
|
|
|
|
async with self.rpc_manager.coingecko_semaphore: |
|
|
|
|
|
current_time = time.time() |
|
|
time_since_last = current_time - self.rpc_manager.last_coingecko_call |
|
|
if time_since_last < self.rpc_manager.COINGECKO_REQUEST_DELAY: |
|
|
await asyncio.sleep(self.rpc_manager.COINGECKO_REQUEST_DELAY - time_since_last) |
|
|
self.rpc_manager.last_coingecko_call = time.time() |
|
|
|
|
|
response = await self.http_client.get(full_url, params=params) |
|
|
response.raise_for_status() |
|
|
detail_data = response.json() |
|
|
|
|
|
|
|
|
if not detail_data or not detail_data.get('platforms'): |
|
|
print(f" ❌ [CoinGecko] فشل جلب تفاصيل المنصات لـ {coin_id}"); return None |
|
|
platforms = detail_data['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 |
|
|
|
|
|
|
|
|
|
|
|
def _calculate_value_usd(self, raw_value: int, decimals: int, price: float) -> float: |
|
|
"""(جديد V2) دالة مساعدة لحساب القيمة بالدولار بأمان""" |
|
|
try: |
|
|
if price == 0: return 0.0 |
|
|
if decimals is None: |
|
|
print(" ⚠️ [Calc] حساب القيمة فشل بسبب 'decimals' غير موجودة.") |
|
|
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: |
|
|
value_usd = tx.get('value_usd', 0.0) |
|
|
if value_usd == 0.0: continue |
|
|
|
|
|
is_to_exchange = tx.get('to_label') is not None |
|
|
is_from_exchange = tx.get('from_label') is not None |
|
|
|
|
|
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: |
|
|
relative_net_flow_percent = (net_flow_usd / daily_volume_usd) * 100 |
|
|
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 |
|
|
|
|
|
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.1] البحث عن عقد للعملة: {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}...") |
|
|
|
|
|
|
|
|
try: |
|
|
search_params = {"query": base_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}") |
|
|
|
|
|
|
|
|
base_url = COINGECKO_BASE_URL |
|
|
full_url = f"{base_url}/coins/{coin_id}" |
|
|
params = {"localization": "false", "tickers": "false", "market_data": "false", "community_data": "false", "developer_data": "false", "sparkline": "false"} |
|
|
|
|
|
async with self.rpc_manager.coingecko_semaphore: |
|
|
current_time = time.time() |
|
|
time_since_last = current_time - self.rpc_manager.last_coingecko_call |
|
|
if time_since_last < COINGECKO_REQUEST_DELAY: |
|
|
await asyncio.sleep(COINGECKO_REQUEST_DELAY - time_since_last) |
|
|
self.rpc_manager.last_coingecko_call = time.time() |
|
|
|
|
|
response = await self.http_client.get(full_url, params=params) |
|
|
response.raise_for_status() |
|
|
detail_data = response.json() |
|
|
|
|
|
if not detail_data or not detail_data.get('platforms'): |
|
|
print(f" ❌ [CoinGecko] فشل جلب تفاصيل المنصات لـ {coin_id}"); return None |
|
|
platforms = detail_data['platforms'] |
|
|
|
|
|
network_priority = ['solana', 'ethereum', 'binance-smart-chain', 'polygon-pos', 'arbitrum-one', 'optimistic-ethereum', 'avalanche', 'fantom'] |
|
|
network_map = {'solana': 'solana', 'ethereum': 'ethereum', 'binance-smart-chain': 'bsc', 'polygon-pos': 'polygon', 'arbitrum-one': 'arbitrum', 'optimistic-ethereum': 'optimism', 'avalanche': 'avalanche', 'fantom': 'fantom'} |
|
|
|
|
|
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}") |
|
|
contract_info = {'address': address, 'network': network} |
|
|
self.contracts_db[symbol_lower] = contract_info |
|
|
if self.r2_service: await self._save_contracts_to_r2() |
|
|
return contract_info |
|
|
return None |
|
|
except Exception as e: |
|
|
print(f"❌ فشل عام في _find_contract_via_coingecko لـ {symbol}: {e}"); traceback.print_exc(); return None |
|
|
|
|
|
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}") |