Riy777 commited on
Commit
2f19d31
·
verified ·
1 Parent(s): a55ec18

Update whale_monitor/core.py

Browse files
Files changed (1) hide show
  1. whale_monitor/core.py +77 -188
whale_monitor/core.py CHANGED
@@ -1,6 +1,6 @@
1
  # whale_monitor/core.py
2
- # (V3.2 - GEM-Architect: Enterprise Edition - Full Logic Restored)
3
- # يتضمن: Web3 Engine, Solscan Fixes, Complex Analysis Windows, R2 Learning, & Robust Fallbacks.
4
 
5
  import os
6
  import asyncio
@@ -12,7 +12,7 @@ from datetime import datetime, timedelta, timezone
12
  from collections import defaultdict, deque
13
  import logging
14
  import ssl
15
- from typing import List, Dict, Any, Optional
16
 
17
  # Web3 Integration
18
  from web3 import AsyncWeb3
@@ -32,17 +32,14 @@ logging.getLogger("web3").setLevel(logging.WARNING)
32
 
33
  class EnhancedWhaleMonitor:
34
  def __init__(self, contracts_db=None, r2_service=None):
35
- print("🔄 [WhaleMonitor V3.2] Initializing Enterprise Engine...")
36
 
37
- # 1. الخدمات الخارجية
38
  self.r2_service = r2_service
39
- self.data_manager = None # سيتم تعيينه لاحقاً
40
- self.rpc_manager: AdaptiveRpcManager = None # سيتم حقنه
41
 
42
- # 2. إعدادات الحدود
43
  self.whale_threshold_usd = DEFAULT_WHALE_THRESHOLD_USD
44
 
45
- # 3. قواعد البيانات الداخلية و Caches
46
  self.contracts_db = {}
47
  self._initialize_contracts_db(contracts_db or {})
48
 
@@ -53,14 +50,12 @@ class EnhancedWhaleMonitor:
53
  self.token_price_cache = {}
54
  self.token_decimals_cache = {}
55
 
56
- # 4. تحميل العقود من R2 (إن وجد)
57
  if self.r2_service:
58
  asyncio.create_task(self._load_contracts_from_r2())
59
 
60
- print("✅ [WhaleMonitor V3.2] System Ready (Waiting for RPC Manager injection).")
61
 
62
  def set_rpc_manager(self, rpc_manager: AdaptiveRpcManager):
63
- """حقن مدير الاتصالات المركزي"""
64
  self.rpc_manager = rpc_manager
65
  print("✅ [WhaleMonitor] RPC Manager Linked.")
66
 
@@ -69,7 +64,6 @@ class EnhancedWhaleMonitor:
69
  # ==============================================================================
70
 
71
  def _initialize_contracts_db(self, initial_contracts):
72
- print("🔄 [WhaleMonitor] Initializing contracts DB...")
73
  for symbol, contract_data in initial_contracts.items():
74
  symbol_lower = symbol.lower()
75
  if isinstance(contract_data, dict) and 'address' in contract_data and 'network' in contract_data:
@@ -79,89 +73,57 @@ class EnhancedWhaleMonitor:
79
  'address': contract_data,
80
  'network': self._detect_network_from_address(contract_data)
81
  }
82
- print(f"✅ [WhaleMonitor] Loaded {len(self.contracts_db)} contracts.")
83
 
84
  def _detect_network_from_address(self, address):
85
  if not isinstance(address, str): return 'ethereum'
86
  address_lower = address.lower()
87
- # Basic Heuristics
88
  if address_lower.startswith('0x') and len(address_lower) == 42:
89
- return 'ethereum' # Default to EVM
90
- # Solana Check (Base58 assumption)
91
  if len(address) > 30 and not address.startswith('0x'):
92
  return 'solana'
93
  return 'ethereum'
94
 
95
  def _initialize_comprehensive_exchange_addresses(self):
96
- """تهيئة قاعدة بيانات العناوين المعروفة لتصنيف التدفقات"""
97
- count = 0
98
  for category, addresses in DEFAULT_EXCHANGE_ADDRESSES.items():
99
  for address in addresses:
100
  if not isinstance(address, str): continue
101
  addr_lower = address.lower()
102
  self.address_labels[addr_lower] = category
103
  self.address_categories['exchange'].add(addr_lower)
104
-
105
  if category in ['uniswap', 'pancakeswap', 'sushiswap']:
106
  self.address_categories['dex'].add(addr_lower)
107
  elif 'wormhole' in category or 'bridge' in category:
108
  self.address_categories['bridge'].add(addr_lower)
109
  else:
110
  self.address_categories['cex'].add(addr_lower)
111
- count += 1
112
- print(f"✅ [WhaleMonitor] Indexed {count} exchange/bridge addresses.")
113
 
114
  async def _load_contracts_from_r2(self):
115
  if not self.r2_service: return
116
  try:
117
  key = "contracts.json"
118
- # (محاكاة للكود الأصلي للتعامل مع R2)
119
  if hasattr(self.r2_service, 's3_client'):
120
  response = self.r2_service.s3_client.get_object(Bucket="trading", Key=key)
121
  contracts_data = json.loads(response['Body'].read())
122
- # Merge logic...
123
  for s, d in contracts_data.items():
124
  if s.lower() not in self.contracts_db:
125
  self.contracts_db[s.lower()] = d
126
- print(f"✅ [WhaleMonitor] Synced contracts from R2.")
127
- except Exception as e:
128
- print(f"⚠️ [WhaleMonitor] R2 Sync skipped: {e}")
129
-
130
- async def _save_contracts_to_r2(self):
131
- if not self.r2_service: return
132
- try:
133
- key = "contracts.json"
134
- # Logic to save...
135
- pass
136
  except Exception: pass
137
 
138
  # ==============================================================================
139
- # 🧠 Core Analysis Logic (The "Brain")
140
  # ==============================================================================
141
 
142
  async def get_symbol_whale_activity(self, symbol: str, known_price: float = 0.0) -> Dict[str, Any]:
143
- """
144
- الوظيفة الرئيسية (Enterprise Logic):
145
- 1. التحقق من العملة.
146
- 2. جلب العقد والكسور العشرية.
147
- 3. جلب البيانات (Gather 1).
148
- 4. التحليل عبر نوافذ متعددة (Analyze N).
149
- 5. تسجيل النتائج للتعلم (R2).
150
- """
151
  try:
152
- start_time = time.time()
153
  if not self.rpc_manager:
154
  return self._create_error_response(symbol, "RPC Manager Not Injected")
155
 
156
- # 1. Reset Stats
157
  self.rpc_manager.reset_session_stats()
158
 
159
- # 2. Native Check
160
  base_symbol = symbol.split("/")[0].upper() if '/' in symbol else symbol.upper()
161
  if base_symbol in NATIVE_COINS:
162
  return self._create_native_coin_response(symbol)
163
 
164
- # 3. Contract & Network
165
  contract_info = await self._find_contract_address_enhanced(symbol)
166
  if not contract_info or not contract_info.get('address'):
167
  return self._create_no_contract_response(symbol)
@@ -169,10 +131,8 @@ class EnhancedWhaleMonitor:
169
  contract_address = contract_info['address']
170
  network = contract_info['network']
171
 
172
- # 4. Price & Decimals (CRITICAL FIX: Use known_price if available)
173
  current_price = known_price
174
  if current_price <= 0:
175
- print(f"⚠️ [Whale] Price missing for {symbol}. Attempting fallback fetch...")
176
  current_price = await self._get_token_price(symbol)
177
 
178
  if current_price <= 0:
@@ -182,8 +142,7 @@ class EnhancedWhaleMonitor:
182
  if decimals is None:
183
  return self._create_error_response(symbol, f"Decimals missing on {network}")
184
 
185
- # 5. Data Gathering (Gather 1 - Deep Scan)
186
- # نطلب 24 ساعة للتحليل الكامل، لكن يمكن تقليلها لـ 4 ساعات للقنص
187
  scan_hours = 4
188
  all_transfers = await self._get_targeted_transfer_data(
189
  contract_address, network, hours=scan_hours, price=current_price, decimals=decimals
@@ -194,10 +153,7 @@ class EnhancedWhaleMonitor:
194
 
195
  print(f"📊 [WhaleMonitor] Analyzed {symbol}: {len(all_transfers)} transfers found ({scan_hours}h).")
196
 
197
- # 6. Multi-Window Analysis (Analyze N)
198
- # هذا هو المنطق المعقد الذي طلبته (تحليل النوافذ)
199
  analysis_windows = [
200
- {'name': '30m', 'minutes': 30},
201
  {'name': '1h', 'minutes': 60},
202
  {'name': '4h', 'minutes': 240},
203
  {'name': '24h', 'minutes': 1440}
@@ -211,31 +167,25 @@ class EnhancedWhaleMonitor:
211
  window_name = window['name']
212
  cutoff_ts = timestamp_cutoff_base - (window['minutes'] * 60)
213
 
214
- # تصفية التحويلات لهذه النافذة
215
  window_transfers = [
216
  t for t in all_transfers
217
  if float(t.get('timeStamp', 0)) >= cutoff_ts
218
  ]
219
 
220
- # التحليل العميق
221
  analysis_result = self._analyze_transfer_list(
222
  symbol=symbol,
223
  transfers=window_transfers,
224
- daily_volume_usd=0 # يمكن جلبه إذا توفر
225
  )
226
  multi_window_analysis[window_name] = analysis_result
227
 
228
- # 7. Learning Record (R2)
229
- # تسجيل البيانات لاستخدامها لاحقاً في تدريب النماذج
230
  if self.r2_service:
231
  asyncio.create_task(self._save_learning_record(
232
  symbol, current_price, multi_window_analysis, self.rpc_manager.get_session_stats()
233
  ))
234
 
235
- # 8. Final Response Construction
236
- # نستخدم نافذة 1h للقرار اللحظي، و 24h للسياق العام
237
  short_term = multi_window_analysis.get('1h', {})
238
- long_term = multi_window_analysis.get('4h', {}) # أو 24h
239
 
240
  signal = self._generate_enhanced_trading_signal(short_term)
241
  llm_summary = self._create_enhanced_llm_summary(signal, short_term)
@@ -249,48 +199,41 @@ class EnhancedWhaleMonitor:
249
  'whale_count_1h': short_term.get('whale_transfers_count', 0),
250
  'net_flow_1h': short_term.get('net_flow_usd', 0)
251
  },
252
- 'exchange_flows': short_term, # النموذج يحتاج هذا
253
- 'accumulation_analysis_24h': long_term, # النموذج يحتاج هذا
254
  'trading_signal': signal,
255
  'llm_friendly_summary': llm_summary
256
  }
257
 
258
  except Exception as e:
259
- print(f"❌ [WhaleMonitor] Critical Error {symbol}: {e}")
260
  traceback.print_exc()
261
  return self._create_error_response(symbol, str(e))
262
 
263
  # ==============================================================================
264
- # 🕵️‍♂️ Data Fetching & Web3 Logic (Robust & Fixed)
265
  # ==============================================================================
266
 
267
  async def _get_targeted_transfer_data(self, contract_address: str, network: str, hours: int, price: float, decimals: int) -> List[Dict]:
268
- """
269
- المحرك الرئيسي لجلب البيانات.
270
- يستخدم الآن Web3 بشكل أساسي للشبكات المدعومة (EVM)، و Solscan لـ Solana.
271
- مع الاحتفاظ بالـ Fallbacks.
272
- """
273
  all_transfers = []
274
 
275
- # A. Solana Logic (Solscan API)
276
  if network == 'solana':
277
- print(f" 🌊 [Solana] Fetching via Solscan...")
278
  try:
279
- # [FIXED] استخدام المسار الصحيح /v2.0/token/transfer
280
  transfers = await self._get_solscan_token_data(contract_address, hours, price)
281
  if transfers: return transfers
282
  except Exception as e:
283
  print(f" ⚠️ [Solana] Solscan failed: {e}")
284
- return [] # No Web3 fallback for Solana yet
285
 
286
- # B. EVM Logic (Web3 -> Moralis -> Scanners)
287
- # 1. Web3 Direct (Fastest & Most Reliable for recent blocks)
288
  try:
289
  print(f" ⚡ [Web3] Scanning {network} logs...")
290
  web3_transfers = await self._get_web3_transfers(contract_address, network, hours, price, decimals)
291
  if web3_transfers:
292
  print(f" ✅ [Web3] Found {len(web3_transfers)} transfers.")
293
- return web3_transfers # إذا نجح Web3، نكتفي به للسرعة
294
  except Exception as e:
295
  print(f" ⚠️ [Web3] Failed: {e}. Trying fallbacks...")
296
 
@@ -300,46 +243,57 @@ class EnhancedWhaleMonitor:
300
  if chain_id:
301
  print(f" 🛡️ [Moralis] Fallback scan...")
302
  moralis_transfers = await self._get_moralis_token_data(contract_address, chain_id, hours, price, decimals)
303
- if moralis_transfers:
304
- return moralis_transfers
305
  except Exception: pass
306
 
307
- # 3. Scanners Fallback (Etherscan etc.)
308
  try:
309
  print(f" 🔍 [Scanner] Fallback scan...")
310
  scanner_transfers = await self._get_scanner_token_data(contract_address, network, hours, price, decimals)
311
- if scanner_transfers:
312
- return scanner_transfers
313
  except Exception: pass
314
 
315
  return []
316
 
317
- # --- Web3 Implementation ---
318
  async def _get_web3_transfers(self, address: str, network: str, hours: int, price: float, decimals: int):
 
319
  w3 = self.rpc_manager.get_web3(network)
320
  if not w3: raise Exception(f"No Web3 provider for {network}")
321
 
322
- # Calculate blocks
323
  latest = await w3.eth.block_number
324
- # Approx block times
325
  block_time = 3 if network == 'bsc' else 12 if network == 'ethereum' else 2
 
 
326
  blocks_back = int((hours * 3600) / block_time)
327
  from_block = max(0, latest - blocks_back)
328
 
329
  contract = w3.eth.contract(address=w3.to_checksum_address(address), abi=ERC20_ABI)
330
 
331
- # Fetch Logs (Standard ERC20 Transfer)
332
- logs = await w3.eth.get_logs({
333
- 'fromBlock': from_block,
334
- 'toBlock': 'latest',
335
- 'address': w3.to_checksum_address(address),
336
- 'topics': [TRANSFER_EVENT_SIGNATURE]
337
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
 
339
  transfers = []
340
  for log in logs:
341
  try:
342
- # Parsing
343
  if len(log['topics']) < 3: continue
344
  val_hex = log['data'].hex()
345
  val_int = int(val_hex, 16)
@@ -347,36 +301,27 @@ class EnhancedWhaleMonitor:
347
  amount = val_int / (10 ** decimals)
348
  val_usd = amount * price
349
 
350
- if val_usd < self.whale_threshold_usd: continue # Filter small tx
 
351
 
352
- # Extract addresses
353
  from_addr = '0x' + log['topics'][1].hex()[-40:]
354
  to_addr = '0x' + log['topics'][2].hex()[-40:]
355
 
356
- # Timestamp (Optimization: Don't fetch block time for every tx if possible,
357
- # but for accuracy we might need to batch fetch or approximate)
358
- # Here we simply use current time approx for speed or fetch if critical
359
- # For high speed, we mark 'timeStamp' as unknown or fetch block timestamp in batches.
360
- # For now, let's assume valid logs are within our window.
361
-
362
  transfers.append({
363
  'hash': log['transactionHash'].hex(),
364
  'from': from_addr,
365
  'to': to_addr,
366
  'value_usd': val_usd,
367
- 'timeStamp': time.time(), # Placeholder, real impl needs block time
368
  'network': network,
369
  'source': 'web3'
370
  })
371
  except: continue
372
  return transfers
373
 
374
- # --- Solscan Implementation (Fixed) ---
375
  async def _get_solscan_token_data(self, address: str, hours: int, price: float):
376
- # [FIXED] Using /v2.0/token/transfer
377
  params = {"address": address, "limit": 50}
378
  data = await self.rpc_manager.get_solscan_api("/v2.0/token/transfer", params)
379
-
380
  if not data or not data.get('data'): return []
381
 
382
  transfers = []
@@ -388,10 +333,10 @@ class EnhancedWhaleMonitor:
388
  if ts < cutoff: continue
389
 
390
  amount_raw = int(tx.get('amount', 0))
391
- dec = tx.get('decimals', 9) # Default SOL decimals
392
-
393
  val_usd = (amount_raw / (10**dec)) * price
394
- if val_usd < self.whale_threshold_usd: continue
 
395
 
396
  transfers.append({
397
  'hash': tx.get('signature'),
@@ -405,9 +350,7 @@ class EnhancedWhaleMonitor:
405
  except: continue
406
  return transfers
407
 
408
- # --- Moralis Implementation ---
409
  async def _get_moralis_token_data(self, address, chain, hours, price, decimals):
410
- # ... (نفس المنطق السابق مع استخدام rpc_manager.get_moralis_api)
411
  params = {
412
  "chain": chain, "contract_address": address,
413
  "order": "DESC", "limit": 100
@@ -419,7 +362,7 @@ class EnhancedWhaleMonitor:
419
  for tx in data['result']:
420
  try:
421
  val = int(tx['value']) / (10**decimals) * price
422
- if val < self.whale_threshold_usd: continue
423
  transfers.append({
424
  'hash': tx['transaction_hash'],
425
  'from': tx['from_address'],
@@ -431,7 +374,6 @@ class EnhancedWhaleMonitor:
431
  except: continue
432
  return transfers
433
 
434
- # --- Scanner Implementation ---
435
  async def _get_scanner_token_data(self, address, network, hours, price, decimals):
436
  config = self.rpc_manager.get_explorer_config(network)
437
  if not config: return []
@@ -448,7 +390,7 @@ class EnhancedWhaleMonitor:
448
  for tx in data['result']:
449
  try:
450
  val = int(tx['value']) / (10**decimals) * price
451
- if val < self.whale_threshold_usd: continue
452
  transfers.append({
453
  'hash': tx['hash'],
454
  'from': tx['from'],
@@ -461,25 +403,18 @@ class EnhancedWhaleMonitor:
461
  return transfers
462
 
463
  # ==============================================================================
464
- # 🕵️‍♂️ Contract Finding (Fixed 404)
465
  # ==============================================================================
466
 
467
  async def _find_contract_address_enhanced(self, symbol: str):
468
  base_symbol = symbol.split('/')[0].lower()
469
-
470
- # 1. Local Cache
471
- if base_symbol in self.contracts_db:
472
- return self.contracts_db[base_symbol]
473
 
474
- # 2. CoinGecko Search (FIXED: Use /search endpoint)
475
  print(f" 🔍 Searching CoinGecko for {base_symbol}...")
476
  try:
477
- # [FIXED] استخدام المسار الصحيح /search
478
  data = await self.rpc_manager.get_coingecko_api('/search', params={'query': base_symbol})
479
-
480
  if not data or not data.get('coins'): return None
481
 
482
- # Find best match
483
  best_id = None
484
  for coin in data['coins']:
485
  if coin['symbol'].lower() == base_symbol:
@@ -487,7 +422,6 @@ class EnhancedWhaleMonitor:
487
  break
488
  if not best_id: best_id = data['coins'][0]['id']
489
 
490
- # 3. Get Details
491
  details = await self.rpc_manager.get_coingecko_api(f'/coins/{best_id}', params={
492
  "localization": "false", "tickers": "false", "market_data": "false",
493
  "community_data": "false", "developer_data": "false"
@@ -496,29 +430,22 @@ class EnhancedWhaleMonitor:
496
  if not details or 'platforms' not in details: return None
497
 
498
  platforms = details['platforms']
499
- priority_nets = ['binance-smart-chain', 'polygon-pos', 'ethereum', 'arbitrum-one', 'optimistic-ethereum', 'solana', 'avalanche']
500
- mapping = {'binance-smart-chain': 'bsc', 'polygon-pos': 'polygon', 'ethereum': 'ethereum', 'arbitrum-one': 'arbitrum', 'optimistic-ethereum': 'optimism', 'solana': 'solana', 'avalanche': 'avalanche'}
501
 
502
  for net_key in priority_nets:
503
  if net_key in platforms and platforms[net_key]:
504
  res = {'address': platforms[net_key], 'network': mapping[net_key]}
505
- self.contracts_db[base_symbol] = res # Cache it
506
- if self.r2_service: asyncio.create_task(self._save_contracts_to_r2())
507
  return res
508
-
509
- except Exception as e:
510
- print(f"❌ [FindContract] Error: {e}")
511
-
512
  return None
513
 
514
  # ==============================================================================
515
- # 📐 Analysis Logic (The Math)
516
  # ==============================================================================
517
 
518
  def _analyze_transfer_list(self, symbol: str, transfers: List[Dict], daily_volume_usd: float) -> Dict[str, Any]:
519
- """
520
- تحليل القائمة وحساب التدفقات (Logic Preserved)
521
- """
522
  stats = {
523
  'to_exchanges_usd': 0.0, 'from_exchanges_usd': 0.0,
524
  'deposit_count': 0, 'withdrawal_count': 0,
@@ -530,8 +457,6 @@ class EnhancedWhaleMonitor:
530
  stats['whale_transfers_count'] += 1
531
  stats['total_volume'] += val
532
 
533
- # Classification
534
- # Simple Check first, then DB check
535
  to_addr = tx.get('to', '').lower()
536
  from_addr = tx.get('from', '').lower()
537
 
@@ -546,41 +471,25 @@ class EnhancedWhaleMonitor:
546
  stats['withdrawal_count'] += 1
547
 
548
  stats['net_flow_usd'] = stats['to_exchanges_usd'] - stats['from_exchanges_usd']
549
-
550
- # Derived Metrics
551
- stats['relative_net_flow'] = 0.0
552
- if daily_volume_usd > 0:
553
- stats['relative_net_flow'] = (stats['net_flow_usd'] / daily_volume_usd) * 100
554
-
555
  return stats
556
 
557
  def _generate_enhanced_trading_signal(self, analysis: Dict) -> Dict:
558
  net = analysis.get('net_flow_usd', 0)
559
  whales = analysis.get('whale_transfers_count', 0)
560
 
561
- if whales < 3:
562
- return {'action': 'HOLD', 'confidence': 0.1, 'reason': 'Low Activity'}
563
-
564
- if net > 1_000_000: # High Inflow -> Sell Pressure
565
- return {'action': 'SELL', 'confidence': 0.8, 'reason': 'Major Exchange Inflow'}
566
- elif net < -1_000_000: # High Outflow -> Buy Pressure
567
- return {'action': 'BUY', 'confidence': 0.8, 'reason': 'Major Exchange Outflow'}
568
-
569
  return {'action': 'WATCH', 'confidence': 0.5, 'reason': 'Mixed Activity'}
570
 
571
  def _create_enhanced_llm_summary(self, signal, analysis):
572
- return {
573
- 'summary': f"Whale Activity: {analysis.get('whale_transfers_count')} txs.",
574
- 'action': signal['action'],
575
- 'metrics': analysis
576
- }
577
 
578
  # ==============================================================================
579
  # ⚙️ Helpers
580
  # ==============================================================================
581
 
582
  async def _get_token_price(self, symbol):
583
- # Fallback only
584
  try:
585
  res = await self.rpc_manager.get_coingecko_api('/simple/price',
586
  params={'ids': COINGECKO_SYMBOL_MAPPING.get(symbol.split('/')[0], symbol), 'vs_currencies': 'usd'})
@@ -588,11 +497,9 @@ class EnhancedWhaleMonitor:
588
  except: return 0.0
589
 
590
  async def _get_token_decimals(self, address, network):
591
- # Check cache
592
  key = f"{address}_{network}"
593
  if key in self.token_decimals_cache: return self.token_decimals_cache[key]
594
 
595
- # Web3 Fetch
596
  try:
597
  w3 = self.rpc_manager.get_web3(network)
598
  if w3:
@@ -602,37 +509,19 @@ class EnhancedWhaleMonitor:
602
  return dec
603
  except: pass
604
 
605
- # Solscan Fetch
606
- if network == 'solana':
607
- return 9 # Default/Approx
608
-
609
- return 18 # EVM Default
610
 
611
  async def _save_learning_record(self, symbol, price, analysis, api_stats):
612
  if not self.r2_service: return
613
  try:
614
- record = {
615
- 'id': f"{symbol}_{int(time.time())}",
616
- 'symbol': symbol,
617
- 'price': price,
618
- 'analysis': analysis,
619
- 'api_stats': api_stats,
620
- 'timestamp': datetime.now(timezone.utc).isoformat()
621
- }
622
- # (Assuming R2 Service has this method, or using S3 put directly)
623
- if hasattr(self.r2_service, 'save_record'):
624
- await self.r2_service.save_record(record)
625
  except: pass
626
 
627
- # Error Responses
628
- def _create_error_response(self, symbol, err):
629
- return {'symbol': symbol, 'data_available': False, 'error': err, 'trading_signal': {}}
630
- def _create_native_coin_response(self, symbol):
631
- return {'symbol': symbol, 'data_available': False, 'error': 'Native Coin', 'trading_signal': {}}
632
- def _create_no_contract_response(self, symbol):
633
- return {'symbol': symbol, 'data_available': False, 'error': 'No Contract', 'trading_signal': {}}
634
- def _create_no_transfers_response(self, symbol):
635
- return {'symbol': symbol, 'data_available': True, 'summary': {'total': 0}, 'trading_signal': {'action': 'HOLD'}}
636
-
637
- async def cleanup(self):
638
- print("🛑 [WhaleMonitor] Cleanup.")
 
1
  # whale_monitor/core.py
2
+ # (V3.3 - GEM-Architect: Enterprise Edition - Smart Web3 Limits)
3
+ # يتضمن: Web3 Engine, Smart Retry for Limits, Solscan Fixes, & Full Logic.
4
 
5
  import os
6
  import asyncio
 
12
  from collections import defaultdict, deque
13
  import logging
14
  import ssl
15
+ from typing import List, Dict, Any
16
 
17
  # Web3 Integration
18
  from web3 import AsyncWeb3
 
32
 
33
  class EnhancedWhaleMonitor:
34
  def __init__(self, contracts_db=None, r2_service=None):
35
+ print("🔄 [WhaleMonitor V3.3] Initializing Enterprise Engine...")
36
 
 
37
  self.r2_service = r2_service
38
+ self.data_manager = None
39
+ self.rpc_manager: AdaptiveRpcManager = None
40
 
 
41
  self.whale_threshold_usd = DEFAULT_WHALE_THRESHOLD_USD
42
 
 
43
  self.contracts_db = {}
44
  self._initialize_contracts_db(contracts_db or {})
45
 
 
50
  self.token_price_cache = {}
51
  self.token_decimals_cache = {}
52
 
 
53
  if self.r2_service:
54
  asyncio.create_task(self._load_contracts_from_r2())
55
 
56
+ print("✅ [WhaleMonitor V3.3] System Ready.")
57
 
58
  def set_rpc_manager(self, rpc_manager: AdaptiveRpcManager):
 
59
  self.rpc_manager = rpc_manager
60
  print("✅ [WhaleMonitor] RPC Manager Linked.")
61
 
 
64
  # ==============================================================================
65
 
66
  def _initialize_contracts_db(self, initial_contracts):
 
67
  for symbol, contract_data in initial_contracts.items():
68
  symbol_lower = symbol.lower()
69
  if isinstance(contract_data, dict) and 'address' in contract_data and 'network' in contract_data:
 
73
  'address': contract_data,
74
  'network': self._detect_network_from_address(contract_data)
75
  }
 
76
 
77
  def _detect_network_from_address(self, address):
78
  if not isinstance(address, str): return 'ethereum'
79
  address_lower = address.lower()
 
80
  if address_lower.startswith('0x') and len(address_lower) == 42:
81
+ return 'ethereum'
 
82
  if len(address) > 30 and not address.startswith('0x'):
83
  return 'solana'
84
  return 'ethereum'
85
 
86
  def _initialize_comprehensive_exchange_addresses(self):
 
 
87
  for category, addresses in DEFAULT_EXCHANGE_ADDRESSES.items():
88
  for address in addresses:
89
  if not isinstance(address, str): continue
90
  addr_lower = address.lower()
91
  self.address_labels[addr_lower] = category
92
  self.address_categories['exchange'].add(addr_lower)
 
93
  if category in ['uniswap', 'pancakeswap', 'sushiswap']:
94
  self.address_categories['dex'].add(addr_lower)
95
  elif 'wormhole' in category or 'bridge' in category:
96
  self.address_categories['bridge'].add(addr_lower)
97
  else:
98
  self.address_categories['cex'].add(addr_lower)
 
 
99
 
100
  async def _load_contracts_from_r2(self):
101
  if not self.r2_service: return
102
  try:
103
  key = "contracts.json"
 
104
  if hasattr(self.r2_service, 's3_client'):
105
  response = self.r2_service.s3_client.get_object(Bucket="trading", Key=key)
106
  contracts_data = json.loads(response['Body'].read())
 
107
  for s, d in contracts_data.items():
108
  if s.lower() not in self.contracts_db:
109
  self.contracts_db[s.lower()] = d
 
 
 
 
 
 
 
 
 
 
110
  except Exception: pass
111
 
112
  # ==============================================================================
113
+ # 🧠 Core Analysis Logic
114
  # ==============================================================================
115
 
116
  async def get_symbol_whale_activity(self, symbol: str, known_price: float = 0.0) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
117
  try:
 
118
  if not self.rpc_manager:
119
  return self._create_error_response(symbol, "RPC Manager Not Injected")
120
 
 
121
  self.rpc_manager.reset_session_stats()
122
 
 
123
  base_symbol = symbol.split("/")[0].upper() if '/' in symbol else symbol.upper()
124
  if base_symbol in NATIVE_COINS:
125
  return self._create_native_coin_response(symbol)
126
 
 
127
  contract_info = await self._find_contract_address_enhanced(symbol)
128
  if not contract_info or not contract_info.get('address'):
129
  return self._create_no_contract_response(symbol)
 
131
  contract_address = contract_info['address']
132
  network = contract_info['network']
133
 
 
134
  current_price = known_price
135
  if current_price <= 0:
 
136
  current_price = await self._get_token_price(symbol)
137
 
138
  if current_price <= 0:
 
142
  if decimals is None:
143
  return self._create_error_response(symbol, f"Decimals missing on {network}")
144
 
145
+ # [CONFIG] Scan window
 
146
  scan_hours = 4
147
  all_transfers = await self._get_targeted_transfer_data(
148
  contract_address, network, hours=scan_hours, price=current_price, decimals=decimals
 
153
 
154
  print(f"📊 [WhaleMonitor] Analyzed {symbol}: {len(all_transfers)} transfers found ({scan_hours}h).")
155
 
 
 
156
  analysis_windows = [
 
157
  {'name': '1h', 'minutes': 60},
158
  {'name': '4h', 'minutes': 240},
159
  {'name': '24h', 'minutes': 1440}
 
167
  window_name = window['name']
168
  cutoff_ts = timestamp_cutoff_base - (window['minutes'] * 60)
169
 
 
170
  window_transfers = [
171
  t for t in all_transfers
172
  if float(t.get('timeStamp', 0)) >= cutoff_ts
173
  ]
174
 
 
175
  analysis_result = self._analyze_transfer_list(
176
  symbol=symbol,
177
  transfers=window_transfers,
178
+ daily_volume_usd=0
179
  )
180
  multi_window_analysis[window_name] = analysis_result
181
 
 
 
182
  if self.r2_service:
183
  asyncio.create_task(self._save_learning_record(
184
  symbol, current_price, multi_window_analysis, self.rpc_manager.get_session_stats()
185
  ))
186
 
 
 
187
  short_term = multi_window_analysis.get('1h', {})
188
+ long_term = multi_window_analysis.get('4h', {})
189
 
190
  signal = self._generate_enhanced_trading_signal(short_term)
191
  llm_summary = self._create_enhanced_llm_summary(signal, short_term)
 
199
  'whale_count_1h': short_term.get('whale_transfers_count', 0),
200
  'net_flow_1h': short_term.get('net_flow_usd', 0)
201
  },
202
+ 'exchange_flows': short_term,
203
+ 'accumulation_analysis_24h': long_term,
204
  'trading_signal': signal,
205
  'llm_friendly_summary': llm_summary
206
  }
207
 
208
  except Exception as e:
209
+ # print(f"❌ [WhaleMonitor] Critical Error {symbol}: {e}")
210
  traceback.print_exc()
211
  return self._create_error_response(symbol, str(e))
212
 
213
  # ==============================================================================
214
+ # 🕵️‍♂️ Data Fetching & Web3 Logic (Smart Limits)
215
  # ==============================================================================
216
 
217
  async def _get_targeted_transfer_data(self, contract_address: str, network: str, hours: int, price: float, decimals: int) -> List[Dict]:
 
 
 
 
 
218
  all_transfers = []
219
 
220
+ # A. Solana Logic
221
  if network == 'solana':
 
222
  try:
 
223
  transfers = await self._get_solscan_token_data(contract_address, hours, price)
224
  if transfers: return transfers
225
  except Exception as e:
226
  print(f" ⚠️ [Solana] Solscan failed: {e}")
227
+ return []
228
 
229
+ # B. EVM Logic
230
+ # 1. Web3 Direct (with Smart Retry)
231
  try:
232
  print(f" ⚡ [Web3] Scanning {network} logs...")
233
  web3_transfers = await self._get_web3_transfers(contract_address, network, hours, price, decimals)
234
  if web3_transfers:
235
  print(f" ✅ [Web3] Found {len(web3_transfers)} transfers.")
236
+ return web3_transfers
237
  except Exception as e:
238
  print(f" ⚠️ [Web3] Failed: {e}. Trying fallbacks...")
239
 
 
243
  if chain_id:
244
  print(f" 🛡️ [Moralis] Fallback scan...")
245
  moralis_transfers = await self._get_moralis_token_data(contract_address, chain_id, hours, price, decimals)
246
+ if moralis_transfers: return moralis_transfers
 
247
  except Exception: pass
248
 
249
+ # 3. Scanners Fallback
250
  try:
251
  print(f" 🔍 [Scanner] Fallback scan...")
252
  scanner_transfers = await self._get_scanner_token_data(contract_address, network, hours, price, decimals)
253
+ if scanner_transfers: return scanner_transfers
 
254
  except Exception: pass
255
 
256
  return []
257
 
 
258
  async def _get_web3_transfers(self, address: str, network: str, hours: int, price: float, decimals: int):
259
+ """Web3 Fetcher with Limit Exceeded Handling"""
260
  w3 = self.rpc_manager.get_web3(network)
261
  if not w3: raise Exception(f"No Web3 provider for {network}")
262
 
 
263
  latest = await w3.eth.block_number
 
264
  block_time = 3 if network == 'bsc' else 12 if network == 'ethereum' else 2
265
+
266
+ # محاولة أولية لكامل المدة
267
  blocks_back = int((hours * 3600) / block_time)
268
  from_block = max(0, latest - blocks_back)
269
 
270
  contract = w3.eth.contract(address=w3.to_checksum_address(address), abi=ERC20_ABI)
271
 
272
+ try:
273
+ # المحاولة الأولى
274
+ logs = await w3.eth.get_logs({
275
+ 'fromBlock': from_block, 'toBlock': 'latest',
276
+ 'address': w3.to_checksum_address(address),
277
+ 'topics': [TRANSFER_EVENT_SIGNATURE]
278
+ })
279
+ except Exception as e:
280
+ # [SMART RETRY] إذا فشل بسبب الحد، قلل المدة للربع (1 ساعة)
281
+ err_str = str(e).lower()
282
+ if 'limit' in err_str or 'range' in err_str or 'exceeded' in err_str:
283
+ print(f" ⚠️ [Web3] Range too big. Retrying with 1 hour window...")
284
+ blocks_back = int((1 * 3600) / block_time) # 1 hour
285
+ from_block = max(0, latest - blocks_back)
286
+ logs = await w3.eth.get_logs({
287
+ 'fromBlock': from_block, 'toBlock': 'latest',
288
+ 'address': w3.to_checksum_address(address),
289
+ 'topics': [TRANSFER_EVENT_SIGNATURE]
290
+ })
291
+ else:
292
+ raise e # خطأ آخر لا يمكن التعامل معه
293
 
294
  transfers = []
295
  for log in logs:
296
  try:
 
297
  if len(log['topics']) < 3: continue
298
  val_hex = log['data'].hex()
299
  val_int = int(val_hex, 16)
 
301
  amount = val_int / (10 ** decimals)
302
  val_usd = amount * price
303
 
304
+ # استخدام حد أدنى 20k للمضاربة السريعة
305
+ if val_usd < 20000.0: continue
306
 
 
307
  from_addr = '0x' + log['topics'][1].hex()[-40:]
308
  to_addr = '0x' + log['topics'][2].hex()[-40:]
309
 
 
 
 
 
 
 
310
  transfers.append({
311
  'hash': log['transactionHash'].hex(),
312
  'from': from_addr,
313
  'to': to_addr,
314
  'value_usd': val_usd,
315
+ 'timeStamp': time.time(),
316
  'network': network,
317
  'source': 'web3'
318
  })
319
  except: continue
320
  return transfers
321
 
 
322
  async def _get_solscan_token_data(self, address: str, hours: int, price: float):
 
323
  params = {"address": address, "limit": 50}
324
  data = await self.rpc_manager.get_solscan_api("/v2.0/token/transfer", params)
 
325
  if not data or not data.get('data'): return []
326
 
327
  transfers = []
 
333
  if ts < cutoff: continue
334
 
335
  amount_raw = int(tx.get('amount', 0))
336
+ dec = tx.get('decimals', 9)
 
337
  val_usd = (amount_raw / (10**dec)) * price
338
+
339
+ if val_usd < 20000.0: continue
340
 
341
  transfers.append({
342
  'hash': tx.get('signature'),
 
350
  except: continue
351
  return transfers
352
 
 
353
  async def _get_moralis_token_data(self, address, chain, hours, price, decimals):
 
354
  params = {
355
  "chain": chain, "contract_address": address,
356
  "order": "DESC", "limit": 100
 
362
  for tx in data['result']:
363
  try:
364
  val = int(tx['value']) / (10**decimals) * price
365
+ if val < 20000.0: continue
366
  transfers.append({
367
  'hash': tx['transaction_hash'],
368
  'from': tx['from_address'],
 
374
  except: continue
375
  return transfers
376
 
 
377
  async def _get_scanner_token_data(self, address, network, hours, price, decimals):
378
  config = self.rpc_manager.get_explorer_config(network)
379
  if not config: return []
 
390
  for tx in data['result']:
391
  try:
392
  val = int(tx['value']) / (10**decimals) * price
393
+ if val < 20000.0: continue
394
  transfers.append({
395
  'hash': tx['hash'],
396
  'from': tx['from'],
 
403
  return transfers
404
 
405
  # ==============================================================================
406
+ # 🕵️‍♂️ Contract Finding & Search
407
  # ==============================================================================
408
 
409
  async def _find_contract_address_enhanced(self, symbol: str):
410
  base_symbol = symbol.split('/')[0].lower()
411
+ if base_symbol in self.contracts_db: return self.contracts_db[base_symbol]
 
 
 
412
 
 
413
  print(f" 🔍 Searching CoinGecko for {base_symbol}...")
414
  try:
 
415
  data = await self.rpc_manager.get_coingecko_api('/search', params={'query': base_symbol})
 
416
  if not data or not data.get('coins'): return None
417
 
 
418
  best_id = None
419
  for coin in data['coins']:
420
  if coin['symbol'].lower() == base_symbol:
 
422
  break
423
  if not best_id: best_id = data['coins'][0]['id']
424
 
 
425
  details = await self.rpc_manager.get_coingecko_api(f'/coins/{best_id}', params={
426
  "localization": "false", "tickers": "false", "market_data": "false",
427
  "community_data": "false", "developer_data": "false"
 
430
  if not details or 'platforms' not in details: return None
431
 
432
  platforms = details['platforms']
433
+ priority_nets = ['binance-smart-chain', 'polygon-pos', 'ethereum', 'solana', 'avalanche']
434
+ mapping = {'binance-smart-chain': 'bsc', 'polygon-pos': 'polygon', 'ethereum': 'ethereum', 'solana': 'solana', 'avalanche': 'avalanche'}
435
 
436
  for net_key in priority_nets:
437
  if net_key in platforms and platforms[net_key]:
438
  res = {'address': platforms[net_key], 'network': mapping[net_key]}
439
+ self.contracts_db[base_symbol] = res
 
440
  return res
441
+ except Exception: pass
 
 
 
442
  return None
443
 
444
  # ==============================================================================
445
+ # 📐 Analysis Logic
446
  # ==============================================================================
447
 
448
  def _analyze_transfer_list(self, symbol: str, transfers: List[Dict], daily_volume_usd: float) -> Dict[str, Any]:
 
 
 
449
  stats = {
450
  'to_exchanges_usd': 0.0, 'from_exchanges_usd': 0.0,
451
  'deposit_count': 0, 'withdrawal_count': 0,
 
457
  stats['whale_transfers_count'] += 1
458
  stats['total_volume'] += val
459
 
 
 
460
  to_addr = tx.get('to', '').lower()
461
  from_addr = tx.get('from', '').lower()
462
 
 
471
  stats['withdrawal_count'] += 1
472
 
473
  stats['net_flow_usd'] = stats['to_exchanges_usd'] - stats['from_exchanges_usd']
 
 
 
 
 
 
474
  return stats
475
 
476
  def _generate_enhanced_trading_signal(self, analysis: Dict) -> Dict:
477
  net = analysis.get('net_flow_usd', 0)
478
  whales = analysis.get('whale_transfers_count', 0)
479
 
480
+ if whales < 3: return {'action': 'HOLD', 'confidence': 0.1, 'reason': 'Low Activity'}
481
+ if net > 500_000: return {'action': 'SELL', 'confidence': 0.8, 'reason': 'Exchange Inflow'}
482
+ elif net < -500_000: return {'action': 'BUY', 'confidence': 0.8, 'reason': 'Exchange Outflow'}
 
 
 
 
 
483
  return {'action': 'WATCH', 'confidence': 0.5, 'reason': 'Mixed Activity'}
484
 
485
  def _create_enhanced_llm_summary(self, signal, analysis):
486
+ return {'summary': f"Whales: {analysis.get('whale_transfers_count')}", 'action': signal['action']}
 
 
 
 
487
 
488
  # ==============================================================================
489
  # ⚙️ Helpers
490
  # ==============================================================================
491
 
492
  async def _get_token_price(self, symbol):
 
493
  try:
494
  res = await self.rpc_manager.get_coingecko_api('/simple/price',
495
  params={'ids': COINGECKO_SYMBOL_MAPPING.get(symbol.split('/')[0], symbol), 'vs_currencies': 'usd'})
 
497
  except: return 0.0
498
 
499
  async def _get_token_decimals(self, address, network):
 
500
  key = f"{address}_{network}"
501
  if key in self.token_decimals_cache: return self.token_decimals_cache[key]
502
 
 
503
  try:
504
  w3 = self.rpc_manager.get_web3(network)
505
  if w3:
 
509
  return dec
510
  except: pass
511
 
512
+ if network == 'solana': return 9
513
+ return 18
 
 
 
514
 
515
  async def _save_learning_record(self, symbol, price, analysis, api_stats):
516
  if not self.r2_service: return
517
  try:
518
+ record = {'id': f"{symbol}_{int(time.time())}", 'symbol': symbol, 'price': price, 'analysis': analysis, 'api_stats': api_stats, 'timestamp': datetime.now(timezone.utc).isoformat()}
519
+ if hasattr(self.r2_service, 'save_record'): await self.r2_service.save_record(record)
 
 
 
 
 
 
 
 
 
520
  except: pass
521
 
522
+ def _create_error_response(self, symbol, err): return {'symbol': symbol, 'data_available': False, 'error': err, 'trading_signal': {}}
523
+ def _create_native_coin_response(self, symbol): return {'symbol': symbol, 'data_available': False, 'error': 'Native Coin', 'trading_signal': {}}
524
+ def _create_no_contract_response(self, symbol): return {'symbol': symbol, 'data_available': False, 'error': 'No Contract', 'trading_signal': {}}
525
+ def _create_no_transfers_response(self, symbol): return {'symbol': symbol, 'data_available': True, 'summary': {'total': 0}, 'trading_signal': {'action': 'HOLD'}}
526
+
527
+ async def cleanup(self): print("🛑 [WhaleMonitor] Cleanup.")