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