Update data_manager.py
Browse files- data_manager.py +239 -305
data_manager.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# data_manager.py (Updated to V7.
|
| 2 |
import os
|
| 3 |
import asyncio
|
| 4 |
import httpx
|
|
@@ -11,13 +11,18 @@ import logging
|
|
| 11 |
from typing import List, Dict, Any
|
| 12 |
import pandas as pd
|
| 13 |
|
| 14 |
-
# 🔴 --- START OF CHANGE (V7.
|
| 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)
|
|
@@ -43,11 +48,18 @@ class DataManager:
|
|
| 43 |
self.http_client = None
|
| 44 |
self.market_cache = {}
|
| 45 |
self.last_market_load = None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
async def initialize(self):
|
| 48 |
self.http_client = httpx.AsyncClient(timeout=30.0)
|
| 49 |
await self._load_markets()
|
| 50 |
-
print("✅ DataManager initialized - V7.
|
| 51 |
|
| 52 |
async def _load_markets(self):
|
| 53 |
try:
|
|
@@ -64,7 +76,6 @@ class DataManager:
|
|
| 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.")
|
|
@@ -78,7 +89,6 @@ class DataManager:
|
|
| 78 |
|
| 79 |
# (الدوال المساعدة لسياق السوق وأسعار BTC/ETH تبقى كما هي - لا تغيير)
|
| 80 |
async def get_market_context_async(self):
|
| 81 |
-
"""جلب سياق السوق الأساسي فقط"""
|
| 82 |
try:
|
| 83 |
sentiment_data = await self.get_sentiment_safe_async()
|
| 84 |
price_data = await self._get_prices_with_fallback()
|
|
@@ -103,7 +113,6 @@ class DataManager:
|
|
| 103 |
return self._get_minimal_market_context()
|
| 104 |
|
| 105 |
async def get_sentiment_safe_async(self):
|
| 106 |
-
"""جلب بيانات المشاعر"""
|
| 107 |
try:
|
| 108 |
async with httpx.AsyncClient(timeout=10) as client:
|
| 109 |
response = await client.get("https://api.alternative.me/fng/")
|
|
@@ -124,42 +133,26 @@ class DataManager:
|
|
| 124 |
return None
|
| 125 |
|
| 126 |
def _determine_market_trend(self, bitcoin_price, sentiment_data):
|
| 127 |
-
"""تحديد اتجاه السوق"""
|
| 128 |
if bitcoin_price is None:
|
| 129 |
return "UNKNOWN"
|
| 130 |
-
|
| 131 |
-
score =
|
| 132 |
-
|
| 133 |
-
score += 1
|
| 134 |
-
elif bitcoin_price < 55000:
|
| 135 |
-
score -= 1
|
| 136 |
-
|
| 137 |
if sentiment_data and sentiment_data.get('feargreed_value') is not None:
|
| 138 |
fear_greed = sentiment_data.get('feargreed_value')
|
| 139 |
-
if fear_greed > 60:
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
if score >= 1:
|
| 145 |
-
return "bull_market"
|
| 146 |
-
elif score <= -1:
|
| 147 |
-
return "bear_market"
|
| 148 |
-
else:
|
| 149 |
-
return "sideways_market"
|
| 150 |
|
| 151 |
def _get_btc_sentiment(self, bitcoin_price):
|
| 152 |
-
if bitcoin_price is None:
|
| 153 |
-
|
| 154 |
-
elif bitcoin_price
|
| 155 |
-
|
| 156 |
-
elif bitcoin_price < 55000:
|
| 157 |
-
return 'BEARISH'
|
| 158 |
-
else:
|
| 159 |
-
return 'NEUTRAL'
|
| 160 |
|
| 161 |
async def _get_prices_with_fallback(self):
|
| 162 |
-
"""جلب أسعار البيتكوين والإيثيريوم"""
|
| 163 |
try:
|
| 164 |
prices = await self._get_prices_from_kucoin_safe()
|
| 165 |
if prices.get('bitcoin') and prices.get('ethereum'):
|
|
@@ -169,63 +162,41 @@ class DataManager:
|
|
| 169 |
return {'bitcoin': None, 'ethereum': None}
|
| 170 |
|
| 171 |
async def _get_prices_from_kucoin_safe(self):
|
| 172 |
-
if not self.exchange:
|
| 173 |
-
return {'bitcoin': None, 'ethereum': None}
|
| 174 |
-
|
| 175 |
try:
|
| 176 |
prices = {'bitcoin': None, 'ethereum': None}
|
| 177 |
-
|
| 178 |
btc_ticker = self.exchange.fetch_ticker('BTC/USDT')
|
| 179 |
btc_price = float(btc_ticker.get('last', 0)) if btc_ticker.get('last') else None
|
| 180 |
-
if btc_price and btc_price > 0:
|
| 181 |
-
prices['bitcoin'] = btc_price
|
| 182 |
-
|
| 183 |
eth_ticker = self.exchange.fetch_ticker('ETH/USDT')
|
| 184 |
eth_price = float(eth_ticker.get('last', 0)) if eth_ticker.get('last') else None
|
| 185 |
-
if eth_price and eth_price > 0:
|
| 186 |
-
prices['ethereum'] = eth_price
|
| 187 |
-
|
| 188 |
return prices
|
| 189 |
-
|
| 190 |
except Exception as e:
|
| 191 |
-
print(f"⚠️ فشل جلب الأسعار من KuCoin: {e}")
|
| 192 |
return {'bitcoin': None, 'ethereum': None}
|
| 193 |
|
| 194 |
async def _get_prices_from_coingecko(self):
|
| 195 |
-
"""الاحتياطي: جلب الأسعار من CoinGecko"""
|
| 196 |
try:
|
| 197 |
await asyncio.sleep(0.5)
|
| 198 |
url = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd"
|
| 199 |
-
|
| 200 |
-
headers = {
|
| 201 |
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
| 202 |
-
'Accept': 'application/json'
|
| 203 |
-
}
|
| 204 |
-
|
| 205 |
async with httpx.AsyncClient(headers=headers) as client:
|
| 206 |
response = await client.get(url, timeout=10)
|
| 207 |
-
|
| 208 |
if response.status_code == 429:
|
| 209 |
await asyncio.sleep(2)
|
| 210 |
response = await client.get(url, timeout=10)
|
| 211 |
-
|
| 212 |
response.raise_for_status()
|
| 213 |
data = response.json()
|
| 214 |
-
|
| 215 |
btc_price = data.get('bitcoin', {}).get('usd')
|
| 216 |
eth_price = data.get('ethereum', {}).get('usd')
|
| 217 |
-
|
| 218 |
if btc_price and eth_price:
|
| 219 |
return {'bitcoin': btc_price, 'ethereum': eth_price}
|
| 220 |
else:
|
| 221 |
return {'bitcoin': None, 'ethereum': None}
|
| 222 |
-
|
| 223 |
except Exception as e:
|
| 224 |
-
print(f"⚠️ فشل جلب الأسعار من CoinGecko: {e}")
|
| 225 |
return {'bitcoin': None, 'ethereum': None}
|
| 226 |
|
| 227 |
def _get_minimal_market_context(self):
|
| 228 |
-
"""سياق سوق بدائي عند الفشل"""
|
| 229 |
return {
|
| 230 |
'timestamp': datetime.now().isoformat(),
|
| 231 |
'data_available': False,
|
|
@@ -234,20 +205,102 @@ class DataManager:
|
|
| 234 |
'data_quality': 'LOW'
|
| 235 |
}
|
| 236 |
|
| 237 |
-
# 🔴 --- START OF REFACTOR (V7.
|
| 238 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
|
| 240 |
"""
|
| 241 |
-
الطبقة 1: فحص سريع - (محدث بالكامل V7.
|
| 242 |
1. جلب أفضل 100 عملة حسب الحجم (24 ساعة).
|
| 243 |
2. جلب 100 شمعة (1H) لهذه الـ 100 عملة.
|
| 244 |
-
3.
|
| 245 |
-
4.
|
|
|
|
| 246 |
"""
|
| 247 |
-
print("📊 الطبقة 1 (V7.
|
| 248 |
|
| 249 |
# الخطوة 1: جلب أفضل 100 عملة حسب الحجم
|
| 250 |
-
volume_data = await self._get_volume_data_optimal()
|
| 251 |
if not volume_data:
|
| 252 |
volume_data = await self._get_volume_data_direct_api()
|
| 253 |
|
|
@@ -258,7 +311,7 @@ class DataManager:
|
|
| 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)}
|
| 262 |
|
| 263 |
final_candidates = []
|
| 264 |
|
|
@@ -272,257 +325,184 @@ class DataManager:
|
|
| 272 |
|
| 273 |
# الخطوة 2: جلب بيانات 1H بالتوازي
|
| 274 |
tasks = [self._fetch_1h_ohlcv_for_screening(symbol) for symbol in batch_symbols]
|
| 275 |
-
|
| 276 |
|
| 277 |
-
# الخطوة 3 و 4: تطبيق
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
| 279 |
symbol_data = batch_symbols_data[j]
|
| 280 |
symbol = symbol_data['symbol']
|
| 281 |
|
| 282 |
-
if isinstance(
|
| 283 |
-
# print(f" - {symbol}: فشل جلب 1H")
|
| 284 |
continue
|
| 285 |
|
| 286 |
-
#
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
continue
|
| 291 |
|
| 292 |
-
#
|
| 293 |
-
|
| 294 |
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
symbol_data['layer1_score'] =
|
| 300 |
-
symbol_data['reasons_for_candidacy'] = [f'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
final_candidates.append(symbol_data)
|
| 302 |
# else:
|
| 303 |
-
|
| 304 |
|
| 305 |
-
print(f"🎯 اكتملت الغربلة (V7.
|
| 306 |
|
| 307 |
-
# عرض أفضل 15 عملة (إذا كان هناك عدد كافٍ)
|
| 308 |
print("🏆 المرشحون الناجحون:")
|
| 309 |
for k, candidate in enumerate(final_candidates[:15]):
|
| 310 |
-
|
| 311 |
volume = candidate.get('dollar_volume', 0)
|
| 312 |
-
print(f" {k+1:2d}. {candidate['symbol']}: (
|
| 313 |
|
| 314 |
return final_candidates
|
| 315 |
|
| 316 |
-
async def
|
| 317 |
-
"""(جديد V7.
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
|
|
|
| 324 |
|
| 325 |
-
|
| 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 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
bbands = ta.bbands(close, length=20, std=2)
|
| 350 |
-
indicators['bb_lower'] = bbands['BBL_20_2.0'].iloc[-1]
|
| 351 |
|
| 352 |
-
|
| 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
|
| 363 |
-
"""(جديد V7.
|
| 364 |
-
if not indicators:
|
| 365 |
-
return (False, None)
|
| 366 |
-
|
| 367 |
try:
|
| 368 |
-
|
| 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
|
| 396 |
|
| 397 |
-
# 🔴 --- END OF REFACTOR (V7.
|
| 398 |
|
| 399 |
|
| 400 |
# (دوال جلب الحجم تبقى كما هي لأنها فعالة)
|
| 401 |
async def _get_volume_data_optimal(self) -> List[Dict[str, Any]]:
|
| 402 |
-
"""الطريقة المثلى: استخدام fetch_tickers لجميع البيانات مرة واحدة"""
|
| 403 |
try:
|
| 404 |
-
if not self.exchange:
|
| 405 |
-
return []
|
| 406 |
-
|
| 407 |
tickers = self.exchange.fetch_tickers()
|
| 408 |
-
|
| 409 |
volume_data = []
|
| 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
|
| 436 |
-
|
| 437 |
volume_data.append({
|
| 438 |
-
'symbol': symbol,
|
| 439 |
-
'
|
| 440 |
-
'current_price': current_price,
|
| 441 |
-
'volume_24h': ticker.get('baseVolume', 0) or 0,
|
| 442 |
'price_change_24h': price_change_24h
|
| 443 |
})
|
| 444 |
-
|
| 445 |
-
processed += 1
|
| 446 |
-
|
| 447 |
-
print(f"✅ تم معالجة {processed} عملة في الطريقة المثلى (لجلب الحجم)")
|
| 448 |
return volume_data
|
| 449 |
-
|
| 450 |
except Exception as e:
|
| 451 |
print(f"❌ خطأ في جلب بيانات الحجم المثلى: {e}")
|
| 452 |
-
traceback.print_exc()
|
| 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 |
-
|
| 460 |
async with httpx.AsyncClient(timeout=15) as client:
|
| 461 |
response = await client.get(url)
|
| 462 |
response.raise_for_status()
|
| 463 |
data = response.json()
|
| 464 |
-
|
| 465 |
-
if data.get('code') != '200000':
|
| 466 |
-
raise ValueError(f"استجابة API غير متوقعة: {data.get('code')}")
|
| 467 |
-
|
| 468 |
tickers = data['data']['ticker']
|
| 469 |
volume_data = []
|
| 470 |
-
|
| 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')
|
| 483 |
vol = ticker.get('vol')
|
| 484 |
-
|
| 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
|
| 491 |
volume_24h = float(vol) if vol else 0
|
| 492 |
-
|
| 493 |
if dollar_volume >= 50000 and current_price > 0:
|
| 494 |
volume_data.append({
|
| 495 |
-
'symbol': formatted_symbol,
|
| 496 |
-
'
|
| 497 |
-
'current_price': current_price,
|
| 498 |
-
'volume_24h': volume_24h,
|
| 499 |
'price_change_24h': price_change
|
| 500 |
})
|
| 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:
|
| 508 |
print(f"❌ خطأ في جلب بيانات الحجم المباشر: {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 |
"""
|
|
@@ -552,6 +532,9 @@ class DataManager:
|
|
| 552 |
if isinstance(result, Exception):
|
| 553 |
print(f" ❌ [المنتج] فشل جلب {symbol}: {result}")
|
| 554 |
elif result is not None:
|
|
|
|
|
|
|
|
|
|
| 555 |
successful_data_for_batch.append(result)
|
| 556 |
successful_count += 1
|
| 557 |
timeframes_count = result.get('successful_timeframes', 0)
|
|
@@ -587,12 +570,8 @@ class DataManager:
|
|
| 587 |
ohlcv_data = {}
|
| 588 |
|
| 589 |
timeframes = [
|
| 590 |
-
('5m', 200),
|
| 591 |
-
('
|
| 592 |
-
('1h', 200),
|
| 593 |
-
('4h', 200),
|
| 594 |
-
('1d', 200),
|
| 595 |
-
('1w', 200),
|
| 596 |
]
|
| 597 |
|
| 598 |
timeframe_tasks = []
|
|
@@ -607,10 +586,7 @@ class DataManager:
|
|
| 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
|
|
@@ -618,52 +594,36 @@ class DataManager:
|
|
| 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 |
-
|
| 629 |
-
|
| 630 |
-
if current_price is None:
|
| 631 |
-
return None
|
| 632 |
|
| 633 |
result_data = {
|
| 634 |
-
'symbol': symbol,
|
| 635 |
-
'
|
| 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()},
|
| 640 |
'successful_timeframes': successful_timeframes
|
| 641 |
}
|
| 642 |
-
|
| 643 |
return result_data
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 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):
|
| 654 |
"""جلب بيانات إطار زمني واحد مع تحسين التعامل مع الأخطاء"""
|
| 655 |
max_retries = 3
|
| 656 |
retry_delay = 2
|
| 657 |
-
|
| 658 |
for attempt in range(max_retries):
|
| 659 |
try:
|
| 660 |
ohlcv_data = self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
|
| 661 |
-
|
| 662 |
if ohlcv_data and len(ohlcv_data) > 0:
|
| 663 |
return ohlcv_data
|
| 664 |
else:
|
| 665 |
return []
|
| 666 |
-
|
| 667 |
except Exception as e:
|
| 668 |
if attempt < max_retries - 1:
|
| 669 |
await asyncio.sleep(retry_delay * (attempt + 1))
|
|
@@ -673,57 +633,31 @@ class DataManager:
|
|
| 673 |
async def get_latest_price_async(self, symbol):
|
| 674 |
"""جلب السعر الحالي لعملة محددة"""
|
| 675 |
try:
|
| 676 |
-
if not self.exchange:
|
| 677 |
-
|
| 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:
|
| 701 |
if self.whale_monitor:
|
| 702 |
whale_data = await self.whale_monitor.get_symbol_whale_activity(symbol)
|
| 703 |
return whale_data
|
| 704 |
-
else:
|
| 705 |
-
|
| 706 |
-
except Exception as e:
|
| 707 |
-
return None
|
| 708 |
|
| 709 |
async def get_whale_trading_signal(self, symbol, whale_data, market_context):
|
| 710 |
-
"""جلب إشارة التداول بناءً على بيانات الحيتان"""
|
| 711 |
try:
|
| 712 |
if self.whale_monitor:
|
| 713 |
return await self.whale_monitor.generate_whale_trading_signal(symbol, whale_data, market_context)
|
| 714 |
else:
|
| 715 |
-
return {
|
| 716 |
-
'action': 'HOLD',
|
| 717 |
-
'confidence': 0.3,
|
| 718 |
-
'reason': 'Whale monitor not available',
|
| 719 |
-
'source': 'whale_analysis'
|
| 720 |
-
}
|
| 721 |
except Exception as e:
|
| 722 |
-
return {
|
| 723 |
-
'action': 'HOLD',
|
| 724 |
-
'confidence': 0.3,
|
| 725 |
-
'reason': f'Error: {str(e)}',
|
| 726 |
-
'source': 'whale_analysis'
|
| 727 |
-
}
|
| 728 |
|
| 729 |
-
print("✅ DataManager loaded - V7.
|
|
|
|
| 1 |
+
# data_manager.py (Updated to V7.1 - 1H Detector Screening)
|
| 2 |
import os
|
| 3 |
import asyncio
|
| 4 |
import httpx
|
|
|
|
| 11 |
from typing import List, Dict, Any
|
| 12 |
import pandas as pd
|
| 13 |
|
| 14 |
+
# 🔴 --- START OF CHANGE (V7.1) --- 🔴
|
| 15 |
+
# (استيراد الأدوات اللازمة لـ "الكاشف المصغر")
|
| 16 |
try:
|
| 17 |
import pandas_ta as ta
|
| 18 |
except ImportError:
|
| 19 |
print("⚠️ مكتبة pandas_ta غير موجودة، فلتر الغربلة المتقدم سيفشل.")
|
| 20 |
ta = None
|
| 21 |
+
|
| 22 |
+
# (استيراد الوحدات الأساسية من محرك ML لتشغيل الكاشف المصغر)
|
| 23 |
+
from ml_engine.indicators import AdvancedTechnicalAnalyzer
|
| 24 |
+
from ml_engine.monte_carlo import MonteCarloAnalyzer
|
| 25 |
+
from ml_engine.patterns import ChartPatternAnalyzer
|
| 26 |
# 🔴 --- END OF CHANGE --- 🔴
|
| 27 |
|
| 28 |
logging.getLogger("httpx").setLevel(logging.WARNING)
|
|
|
|
| 48 |
self.http_client = None
|
| 49 |
self.market_cache = {}
|
| 50 |
self.last_market_load = None
|
| 51 |
+
|
| 52 |
+
# 🔴 --- START OF CHANGE (V7.1) --- 🔴
|
| 53 |
+
# (تهيئة أدوات الكاشف المصغر)
|
| 54 |
+
self.technical_analyzer = AdvancedTechnicalAnalyzer()
|
| 55 |
+
self.monte_carlo_analyzer = MonteCarloAnalyzer()
|
| 56 |
+
self.pattern_analyzer = ChartPatternAnalyzer()
|
| 57 |
+
# 🔴 --- END OF CHANGE --- 🔴
|
| 58 |
|
| 59 |
async def initialize(self):
|
| 60 |
self.http_client = httpx.AsyncClient(timeout=30.0)
|
| 61 |
await self._load_markets()
|
| 62 |
+
print("✅ DataManager initialized - V7.1 (1H Detector Screening)")
|
| 63 |
|
| 64 |
async def _load_markets(self):
|
| 65 |
try:
|
|
|
|
| 76 |
print(f"❌ فشل تحميل بيانات الأسواق: {e}")
|
| 77 |
|
| 78 |
async def close(self):
|
|
|
|
| 79 |
if self.http_client and not self.http_client.is_closed:
|
| 80 |
await self.http_client.aclose()
|
| 81 |
print(" ✅ DataManager: http_client closed.")
|
|
|
|
| 89 |
|
| 90 |
# (الدوال المساعدة لسياق السوق وأسعار BTC/ETH تبقى كما هي - لا تغيير)
|
| 91 |
async def get_market_context_async(self):
|
|
|
|
| 92 |
try:
|
| 93 |
sentiment_data = await self.get_sentiment_safe_async()
|
| 94 |
price_data = await self._get_prices_with_fallback()
|
|
|
|
| 113 |
return self._get_minimal_market_context()
|
| 114 |
|
| 115 |
async def get_sentiment_safe_async(self):
|
|
|
|
| 116 |
try:
|
| 117 |
async with httpx.AsyncClient(timeout=10) as client:
|
| 118 |
response = await client.get("https://api.alternative.me/fng/")
|
|
|
|
| 133 |
return None
|
| 134 |
|
| 135 |
def _determine_market_trend(self, bitcoin_price, sentiment_data):
|
|
|
|
| 136 |
if bitcoin_price is None:
|
| 137 |
return "UNKNOWN"
|
| 138 |
+
if bitcoin_price > 60000: score = 1
|
| 139 |
+
elif bitcoin_price < 55000: score = -1
|
| 140 |
+
else: score = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
if sentiment_data and sentiment_data.get('feargreed_value') is not None:
|
| 142 |
fear_greed = sentiment_data.get('feargreed_value')
|
| 143 |
+
if fear_greed > 60: score += 1
|
| 144 |
+
elif fear_greed < 40: score -= 1
|
| 145 |
+
if score >= 1: return "bull_market"
|
| 146 |
+
elif score <= -1: return "bear_market"
|
| 147 |
+
else: return "sideways_market"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
|
| 149 |
def _get_btc_sentiment(self, bitcoin_price):
|
| 150 |
+
if bitcoin_price is None: return 'UNKNOWN'
|
| 151 |
+
elif bitcoin_price > 60000: return 'BULLISH'
|
| 152 |
+
elif bitcoin_price < 55000: return 'BEARISH'
|
| 153 |
+
else: return 'NEUTRAL'
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
async def _get_prices_with_fallback(self):
|
|
|
|
| 156 |
try:
|
| 157 |
prices = await self._get_prices_from_kucoin_safe()
|
| 158 |
if prices.get('bitcoin') and prices.get('ethereum'):
|
|
|
|
| 162 |
return {'bitcoin': None, 'ethereum': None}
|
| 163 |
|
| 164 |
async def _get_prices_from_kucoin_safe(self):
|
| 165 |
+
if not self.exchange: return {'bitcoin': None, 'ethereum': None}
|
|
|
|
|
|
|
| 166 |
try:
|
| 167 |
prices = {'bitcoin': None, 'ethereum': None}
|
|
|
|
| 168 |
btc_ticker = self.exchange.fetch_ticker('BTC/USDT')
|
| 169 |
btc_price = float(btc_ticker.get('last', 0)) if btc_ticker.get('last') else None
|
| 170 |
+
if btc_price and btc_price > 0: prices['bitcoin'] = btc_price
|
|
|
|
|
|
|
| 171 |
eth_ticker = self.exchange.fetch_ticker('ETH/USDT')
|
| 172 |
eth_price = float(eth_ticker.get('last', 0)) if eth_ticker.get('last') else None
|
| 173 |
+
if eth_price and eth_price > 0: prices['ethereum'] = eth_price
|
|
|
|
|
|
|
| 174 |
return prices
|
|
|
|
| 175 |
except Exception as e:
|
|
|
|
| 176 |
return {'bitcoin': None, 'ethereum': None}
|
| 177 |
|
| 178 |
async def _get_prices_from_coingecko(self):
|
|
|
|
| 179 |
try:
|
| 180 |
await asyncio.sleep(0.5)
|
| 181 |
url = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd"
|
| 182 |
+
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept': 'application/json'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
async with httpx.AsyncClient(headers=headers) as client:
|
| 184 |
response = await client.get(url, timeout=10)
|
|
|
|
| 185 |
if response.status_code == 429:
|
| 186 |
await asyncio.sleep(2)
|
| 187 |
response = await client.get(url, timeout=10)
|
|
|
|
| 188 |
response.raise_for_status()
|
| 189 |
data = response.json()
|
|
|
|
| 190 |
btc_price = data.get('bitcoin', {}).get('usd')
|
| 191 |
eth_price = data.get('ethereum', {}).get('usd')
|
|
|
|
| 192 |
if btc_price and eth_price:
|
| 193 |
return {'bitcoin': btc_price, 'ethereum': eth_price}
|
| 194 |
else:
|
| 195 |
return {'bitcoin': None, 'ethereum': None}
|
|
|
|
| 196 |
except Exception as e:
|
|
|
|
| 197 |
return {'bitcoin': None, 'ethereum': None}
|
| 198 |
|
| 199 |
def _get_minimal_market_context(self):
|
|
|
|
| 200 |
return {
|
| 201 |
'timestamp': datetime.now().isoformat(),
|
| 202 |
'data_available': False,
|
|
|
|
| 205 |
'data_quality': 'LOW'
|
| 206 |
}
|
| 207 |
|
| 208 |
+
# 🔴 --- START OF REFACTOR (V7.1 - 1H Detector Screening) --- 🔴
|
| 209 |
|
| 210 |
+
def _create_dataframe(self, candles: List) -> pd.DataFrame:
|
| 211 |
+
"""(جديد V7.1) دالة مساعدة لإنشاء DataFrame لتحليل 1H"""
|
| 212 |
+
try:
|
| 213 |
+
if not candles: return pd.DataFrame()
|
| 214 |
+
df = pd.DataFrame(candles, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
| 215 |
+
df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float)
|
| 216 |
+
df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
|
| 217 |
+
df.set_index('timestamp', inplace=True)
|
| 218 |
+
df.sort_index(inplace=True)
|
| 219 |
+
return df
|
| 220 |
+
except Exception as e:
|
| 221 |
+
print(f"❌ خطأ في إنشاء DataFrame لمرشح 1H: {e}")
|
| 222 |
+
return pd.DataFrame()
|
| 223 |
+
|
| 224 |
+
def _calculate_1h_filter_score(self, analysis: Dict) -> float:
|
| 225 |
+
"""
|
| 226 |
+
(جديد V7.1) - "الكاشف المصغر"
|
| 227 |
+
مقتبس من processor.py لحساب درجة (0-1) بناءً على بيانات 1H فقط.
|
| 228 |
+
يستخدم (المؤشرات، مونت كارلو، الأنماط) فقط.
|
| 229 |
+
"""
|
| 230 |
+
try:
|
| 231 |
+
# 1. درجة الأنماط (Pattern Score)
|
| 232 |
+
pattern_confidence = analysis.get('pattern_analysis', {}).get('pattern_confidence', 0)
|
| 233 |
+
|
| 234 |
+
# 2. درجة مونت كارلو (Monte Carlo Score)
|
| 235 |
+
mc_distribution = analysis.get('monte_carlo_distribution')
|
| 236 |
+
monte_carlo_score = 0
|
| 237 |
+
|
| 238 |
+
if mc_distribution:
|
| 239 |
+
prob_gain = mc_distribution.get('probability_of_gain', 0)
|
| 240 |
+
var_95_value = mc_distribution.get('risk_metrics', {}).get('VaR_95_value', 0)
|
| 241 |
+
current_price = analysis.get('current_price', 1)
|
| 242 |
+
|
| 243 |
+
if current_price > 0:
|
| 244 |
+
normalized_var = var_95_value / current_price
|
| 245 |
+
risk_penalty = 1.0
|
| 246 |
+
if normalized_var > 0.05: risk_penalty = 0.5
|
| 247 |
+
elif normalized_var > 0.03: risk_penalty = 0.8
|
| 248 |
+
|
| 249 |
+
# (تطبيع الاحتمالية: 0.5 -> 0, 1.0 -> 1.0)
|
| 250 |
+
normalized_prob_score = max(0.0, (prob_gain - 0.5) * 2)
|
| 251 |
+
monte_carlo_score = normalized_prob_score * risk_penalty
|
| 252 |
+
|
| 253 |
+
# 3. درجة المؤشرات (Indicator Score)
|
| 254 |
+
# (تقييم بسيط للمؤشرات الرئيسية لـ 1H)
|
| 255 |
+
indicator_score = 0
|
| 256 |
+
indicators = analysis.get('advanced_indicators', {}).get('1h', {})
|
| 257 |
+
if indicators:
|
| 258 |
+
rsi = indicators.get('rsi', 50)
|
| 259 |
+
macd_hist = indicators.get('macd_hist', 0)
|
| 260 |
+
ema_9 = indicators.get('ema_9', 0)
|
| 261 |
+
ema_21 = indicators.get('ema_21', 0)
|
| 262 |
+
|
| 263 |
+
# ملمح الزخم
|
| 264 |
+
if rsi > 55 and macd_hist > 0 and ema_9 > ema_21:
|
| 265 |
+
indicator_score = min(0.5 + (rsi - 55) / 50 + (macd_hist / (analysis.get('current_price', 1) * 0.001)), 1.0)
|
| 266 |
+
# ملمح الانعكاس
|
| 267 |
+
elif rsi < 35:
|
| 268 |
+
indicator_score = min(0.4 + (35 - rsi) / 35, 0.8)
|
| 269 |
+
|
| 270 |
+
# 4. حساب النتيجة النهائية (بدون استراتيجيات أو حيتان)
|
| 271 |
+
# الأوزان: مونت كارلو (40%)، الأنماط (30%)، المؤشرات (30%)
|
| 272 |
+
components = []
|
| 273 |
+
weights = []
|
| 274 |
+
|
| 275 |
+
if monte_carlo_score > 0: components.append(monte_carlo_score); weights.append(0.40)
|
| 276 |
+
if pattern_confidence > 0: components.append(pattern_confidence); weights.append(0.30)
|
| 277 |
+
if indicator_score > 0: components.append(indicator_score); weights.append(0.30)
|
| 278 |
+
|
| 279 |
+
if not components: return 0
|
| 280 |
+
total_weight = sum(weights)
|
| 281 |
+
if total_weight == 0: return 0
|
| 282 |
+
|
| 283 |
+
enhanced_score = sum(comp * weight for comp, weight in zip(components, weights)) / total_weight
|
| 284 |
+
return min(max(enhanced_score, 0.0), 1.0)
|
| 285 |
+
|
| 286 |
+
except Exception as e:
|
| 287 |
+
print(f"❌ خطأ في حساب درجة فلتر 1H: {e}")
|
| 288 |
+
return 0.0
|
| 289 |
+
|
| 290 |
+
|
| 291 |
async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
|
| 292 |
"""
|
| 293 |
+
الطبقة 1: فحص سريع - (محدث بالكامل V7.1)
|
| 294 |
1. جلب أفضل 100 عملة حسب الحجم (24 ساعة).
|
| 295 |
2. جلب 100 شمعة (1H) لهذه الـ 100 عملة.
|
| 296 |
+
3. تشغيل "الكاشف المصغر" (مؤشرات، مونت كارلو، أنماط) على 1H.
|
| 297 |
+
4. استبعاد أي عملة درجتها < 0.20.
|
| 298 |
+
5. إرجاع العملات الناجحة فقط للطبقة 2.
|
| 299 |
"""
|
| 300 |
+
print("📊 الطبقة 1 (V7.1): بدء الغربلة (الكاشف المصغر 1H)...")
|
| 301 |
|
| 302 |
# الخطوة 1: جلب أفضل 100 عملة حسب الحجم
|
| 303 |
+
volume_data = await self._get_volume_data_optimal()
|
| 304 |
if not volume_data:
|
| 305 |
volume_data = await self._get_volume_data_direct_api()
|
| 306 |
|
|
|
|
| 311 |
volume_data.sort(key=lambda x: x['dollar_volume'], reverse=True)
|
| 312 |
top_100_by_volume = volume_data[:100]
|
| 313 |
|
| 314 |
+
print(f"✅ تم تحديد أفضل {len(top_100_by_volume)} عملة. بدء تشغيل الكاشف المصغر (1H)...")
|
| 315 |
|
| 316 |
final_candidates = []
|
| 317 |
|
|
|
|
| 325 |
|
| 326 |
# الخطوة 2: جلب بيانات 1H بالتوازي
|
| 327 |
tasks = [self._fetch_1h_ohlcv_for_screening(symbol) for symbol in batch_symbols]
|
| 328 |
+
results_candles = await asyncio.gather(*tasks, return_exceptions=True)
|
| 329 |
|
| 330 |
+
# الخطوة 3 و 4: تطبيق الكاشف المصغر
|
| 331 |
+
analysis_tasks = []
|
| 332 |
+
valid_symbol_data = []
|
| 333 |
+
|
| 334 |
+
for j, (candles) in enumerate(results_candles):
|
| 335 |
symbol_data = batch_symbols_data[j]
|
| 336 |
symbol = symbol_data['symbol']
|
| 337 |
|
| 338 |
+
if isinstance(candles, Exception) or not candles or len(candles) < 50:
|
|
|
|
| 339 |
continue
|
| 340 |
|
| 341 |
+
# (إعداد البيانات للمحللات)
|
| 342 |
+
ohlcv_1h_only = {'1h': candles}
|
| 343 |
+
symbol_data['ohlcv_1h'] = ohlcv_1h_only # (تخزين مؤقت)
|
| 344 |
+
symbol_data['current_price'] = candles[-1][4] # آخر سعر إغلاق
|
| 345 |
+
analysis_tasks.append(self._run_mini_detector(symbol_data))
|
| 346 |
+
valid_symbol_data.append(symbol_data)
|
| 347 |
+
|
| 348 |
+
if not analysis_tasks:
|
| 349 |
+
continue
|
| 350 |
+
|
| 351 |
+
# تشغيل التحليلات (MC, Patterns, Indicators) بالتوازي
|
| 352 |
+
analysis_results = await asyncio.gather(*analysis_tasks, return_exceptions=True)
|
| 353 |
+
|
| 354 |
+
for j, (analysis_output) in enumerate(analysis_results):
|
| 355 |
+
symbol_data = valid_symbol_data[j] # (استرجاع بيانات الرمز المطابقة)
|
| 356 |
+
symbol = symbol_data['symbol']
|
| 357 |
+
|
| 358 |
+
if isinstance(analysis_output, Exception):
|
| 359 |
+
print(f" - {symbol}: فشل الكاشف المصغر ({analysis_output})")
|
| 360 |
continue
|
| 361 |
|
| 362 |
+
# الخطوة 4: حساب النتيجة النهائية للفلتر
|
| 363 |
+
filter_score = self._calculate_1h_filter_score(analysis_output)
|
| 364 |
|
| 365 |
+
# الخطوة 5: تطبيق العتبة (Threshold)
|
| 366 |
+
if filter_score >= 0.20:
|
| 367 |
+
print(f" ✅ {symbol}: نجح (الدرجة: {filter_score:.2f})")
|
| 368 |
+
# (إضافة البيانات الأولية للطبقة 2)
|
| 369 |
+
symbol_data['layer1_score'] = filter_score
|
| 370 |
+
symbol_data['reasons_for_candidacy'] = [f'1H_DETECTOR_PASS']
|
| 371 |
+
|
| 372 |
+
# (تنظيف البيانات قبل إرسالها)
|
| 373 |
+
if 'ohlcv_1h' in symbol_data: del symbol_data['ohlcv_1h']
|
| 374 |
+
|
| 375 |
final_candidates.append(symbol_data)
|
| 376 |
# else:
|
| 377 |
+
# print(f" - {symbol}: فشل (الدرجة: {filter_score:.2f})")
|
| 378 |
|
| 379 |
+
print(f"🎯 اكتملت الغربلة (V7.1). تم تأهيل {len(final_candidates)} عملة من أصل 100 للطبقة 2.")
|
| 380 |
|
|
|
|
| 381 |
print("🏆 المرشحون الناجحون:")
|
| 382 |
for k, candidate in enumerate(final_candidates[:15]):
|
| 383 |
+
score = candidate.get('layer1_score', 0)
|
| 384 |
volume = candidate.get('dollar_volume', 0)
|
| 385 |
+
print(f" {k+1:2d}. {candidate['symbol']}: (الدرجة: {score:.2f}) | ${volume:,.0f}")
|
| 386 |
|
| 387 |
return final_candidates
|
| 388 |
|
| 389 |
+
async def _run_mini_detector(self, symbol_data: Dict) -> Dict:
|
| 390 |
+
"""(جديد V7.1) يشغل المحللات الأساسية بالتوازي على بيانات 1H فقط"""
|
| 391 |
+
ohlcv_1h = symbol_data.get('ohlcv_1h')
|
| 392 |
+
current_price = symbol_data.get('current_price')
|
| 393 |
+
|
| 394 |
+
# (إنشاء DataFrame مرة واحدة)
|
| 395 |
+
df = self._create_dataframe(ohlcv_1h.get('1h'))
|
| 396 |
+
if df.empty:
|
| 397 |
+
raise ValueError("DataFrame فارغ لتحليل 1H")
|
| 398 |
|
| 399 |
+
analysis_dict = {'current_price': current_price}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
|
| 401 |
+
# إنشاء المهام
|
| 402 |
+
# (لا حاجة لـ asyncio.create_task لأن هذه الدوال ليست async)
|
| 403 |
+
# (ملاحظة: جعلنا دوال التحليل async في ملفاتها لتكون متوافقة)
|
| 404 |
+
|
| 405 |
+
task_indicators = self.technical_analyzer.calculate_all_indicators(df, '1h')
|
| 406 |
+
task_mc = self.monte_carlo_analyzer.generate_1h_price_distribution(ohlcv_1h)
|
| 407 |
+
task_pattern = self.pattern_analyzer.detect_chart_patterns(ohlcv_1h)
|
| 408 |
+
|
| 409 |
+
# (تشغيل المهام التي هي async)
|
| 410 |
+
results = await asyncio.gather(task_mc, task_pattern, return_exceptions=True)
|
| 411 |
+
|
| 412 |
+
# (معالجة النتائج)
|
| 413 |
+
analysis_dict['advanced_indicators'] = {'1h': task_indicators} # (هذه دالة متزامنة)
|
| 414 |
+
|
| 415 |
+
if not isinstance(results[0], Exception):
|
| 416 |
+
analysis_dict['monte_carlo_distribution'] = results[0]
|
| 417 |
+
if not isinstance(results[1], Exception):
|
| 418 |
+
analysis_dict['pattern_analysis'] = results[1]
|
|
|
|
|
|
|
| 419 |
|
| 420 |
+
return analysis_dict
|
| 421 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
|
| 423 |
+
async def _fetch_1h_ohlcv_for_screening(self, symbol: str) -> List:
|
| 424 |
+
"""(جديد V7.1) جلب 100 شمعة لإطار الساعة (1H) للغربلة السريعة"""
|
|
|
|
|
|
|
|
|
|
| 425 |
try:
|
| 426 |
+
# 100 شمعة كافية لحساب (RSI 14, EMA 21, MACD 26, BB 20)
|
| 427 |
+
ohlcv_data = self.exchange.fetch_ohlcv(symbol, '1h', limit=100)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
|
| 429 |
+
if not ohlcv_data or len(ohlcv_data) < 50: # (الحد الأدنى 50)
|
| 430 |
+
return None
|
| 431 |
+
return ohlcv_data
|
| 432 |
except Exception:
|
| 433 |
+
return None
|
| 434 |
|
| 435 |
+
# 🔴 --- END OF REFACTOR (V7.1) --- 🔴
|
| 436 |
|
| 437 |
|
| 438 |
# (دوال جلب الحجم تبقى كما هي لأنها فعالة)
|
| 439 |
async def _get_volume_data_optimal(self) -> List[Dict[str, Any]]:
|
|
|
|
| 440 |
try:
|
| 441 |
+
if not self.exchange: return []
|
|
|
|
|
|
|
| 442 |
tickers = self.exchange.fetch_tickers()
|
|
|
|
| 443 |
volume_data = []
|
|
|
|
|
|
|
| 444 |
for symbol, ticker in tickers.items():
|
| 445 |
+
if not symbol.endswith('/USDT') or not ticker.get('active', True): continue
|
|
|
|
|
|
|
| 446 |
current_price = ticker.get('last', 0)
|
| 447 |
quote_volume = ticker.get('quoteVolume', 0)
|
| 448 |
+
if current_price is None or current_price <= 0: continue
|
|
|
|
|
|
|
|
|
|
| 449 |
if quote_volume is not None and quote_volume > 0:
|
| 450 |
dollar_volume = quote_volume
|
| 451 |
else:
|
| 452 |
base_volume = ticker.get('baseVolume', 0)
|
| 453 |
+
if base_volume is None: continue
|
|
|
|
| 454 |
dollar_volume = base_volume * current_price
|
| 455 |
+
if dollar_volume is None or dollar_volume < 50000: continue
|
|
|
|
|
|
|
|
|
|
| 456 |
price_change_24h = (ticker.get('percentage', 0) or 0) * 100
|
| 457 |
+
if price_change_24h is None: price_change_24h = 0
|
|
|
|
|
|
|
| 458 |
volume_data.append({
|
| 459 |
+
'symbol': symbol, 'dollar_volume': dollar_volume,
|
| 460 |
+
'current_price': current_price, 'volume_24h': ticker.get('baseVolume', 0) or 0,
|
|
|
|
|
|
|
| 461 |
'price_change_24h': price_change_24h
|
| 462 |
})
|
| 463 |
+
print(f"✅ تم معالجة {len(volume_data)} عملة في الطريقة المثلى (لجلب الحجم)")
|
|
|
|
|
|
|
|
|
|
| 464 |
return volume_data
|
|
|
|
| 465 |
except Exception as e:
|
| 466 |
print(f"❌ خطأ في جلب بيانات الحجم المثلى: {e}")
|
|
|
|
| 467 |
return []
|
| 468 |
|
| 469 |
async def _get_volume_data_direct_api(self) -> List[Dict[str, Any]]:
|
|
|
|
| 470 |
try:
|
| 471 |
url = "https://api.kucoin.com/api/v1/market/allTickers"
|
|
|
|
| 472 |
async with httpx.AsyncClient(timeout=15) as client:
|
| 473 |
response = await client.get(url)
|
| 474 |
response.raise_for_status()
|
| 475 |
data = response.json()
|
| 476 |
+
if data.get('code') != '200000': raise ValueError(f"استجابة API غير متوقعة: {data.get('code')}")
|
|
|
|
|
|
|
|
|
|
| 477 |
tickers = data['data']['ticker']
|
| 478 |
volume_data = []
|
|
|
|
| 479 |
for ticker in tickers:
|
| 480 |
symbol = ticker['symbol']
|
| 481 |
+
if not symbol.endswith('USDT'): continue
|
|
|
|
|
|
|
|
|
|
| 482 |
formatted_symbol = symbol.replace('-', '/')
|
|
|
|
| 483 |
try:
|
| 484 |
vol_value = ticker.get('volValue')
|
| 485 |
last_price = ticker.get('last')
|
| 486 |
change_rate = ticker.get('changeRate')
|
| 487 |
vol = ticker.get('vol')
|
| 488 |
+
if vol_value is None or last_price is None or change_rate is None or vol is None: continue
|
|
|
|
|
|
|
|
|
|
| 489 |
dollar_volume = float(vol_value) if vol_value else 0
|
| 490 |
current_price = float(last_price) if last_price else 0
|
| 491 |
price_change = (float(change_rate) * 100) if change_rate else 0
|
| 492 |
volume_24h = float(vol) if vol else 0
|
|
|
|
| 493 |
if dollar_volume >= 50000 and current_price > 0:
|
| 494 |
volume_data.append({
|
| 495 |
+
'symbol': formatted_symbol, 'dollar_volume': dollar_volume,
|
| 496 |
+
'current_price': current_price, 'volume_24h': volume_24h,
|
|
|
|
|
|
|
| 497 |
'price_change_24h': price_change
|
| 498 |
})
|
| 499 |
+
except (ValueError, TypeError, KeyError) as e: continue
|
|
|
|
|
|
|
| 500 |
print(f"✅ تم معالجة {len(volume_data)} عملة في الطريقة المباشرة (لجلب الحجم)")
|
| 501 |
return volume_data
|
|
|
|
| 502 |
except Exception as e:
|
| 503 |
print(f"❌ خطأ في جلب بيانات الحجم المباشر: {e}")
|
|
|
|
| 504 |
return []
|
| 505 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
# (دالة تدفق الشموع تبقى كما هي - لا تغيير)
|
| 507 |
async def stream_ohlcv_data(self, symbols: List[str], queue: asyncio.Queue):
|
| 508 |
"""
|
|
|
|
| 532 |
if isinstance(result, Exception):
|
| 533 |
print(f" ❌ [المنتج] فشل جلب {symbol}: {result}")
|
| 534 |
elif result is not None:
|
| 535 |
+
# (مهم: نمرر بيانات المرشح الأولية التي تحتوي على الدرجة)
|
| 536 |
+
original_data = next((s for s in symbols if s['symbol'] == symbol), {'symbol': symbol})
|
| 537 |
+
result.update(original_data) # (دمج الدرجة الأولية مع البيانات الكاملة)
|
| 538 |
successful_data_for_batch.append(result)
|
| 539 |
successful_count += 1
|
| 540 |
timeframes_count = result.get('successful_timeframes', 0)
|
|
|
|
| 570 |
ohlcv_data = {}
|
| 571 |
|
| 572 |
timeframes = [
|
| 573 |
+
('5m', 200), ('15m', 200), ('1h', 200),
|
| 574 |
+
('4h', 200), ('1d', 200), ('1w', 200),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 575 |
]
|
| 576 |
|
| 577 |
timeframe_tasks = []
|
|
|
|
| 586 |
|
| 587 |
for i, (timeframe, limit) in enumerate(timeframes):
|
| 588 |
result = timeframe_results[i]
|
| 589 |
+
if isinstance(result, Exception): continue
|
|
|
|
|
|
|
|
|
|
| 590 |
if result and len(result) >= 10:
|
| 591 |
ohlcv_data[timeframe] = result
|
| 592 |
successful_timeframes += 1
|
|
|
|
| 594 |
if successful_timeframes >= min_required_timeframes and ohlcv_data:
|
| 595 |
try:
|
| 596 |
current_price = await self.get_latest_price_async(symbol)
|
|
|
|
| 597 |
if current_price is None:
|
| 598 |
for timeframe_data in ohlcv_data.values():
|
| 599 |
if timeframe_data and len(timeframe_data) > 0:
|
| 600 |
last_candle = timeframe_data[-1]
|
| 601 |
if len(last_candle) >= 5:
|
| 602 |
+
current_price = last_candle[4]; break
|
| 603 |
+
if current_price is None: return None
|
|
|
|
|
|
|
|
|
|
| 604 |
|
| 605 |
result_data = {
|
| 606 |
+
'symbol': symbol, 'ohlcv': ohlcv_data, 'raw_ohlcv': ohlcv_data,
|
| 607 |
+
'current_price': current_price, 'timestamp': datetime.now().isoformat(),
|
|
|
|
|
|
|
|
|
|
| 608 |
'candles_count': {tf: len(data) for tf, data in ohlcv_data.items()},
|
| 609 |
'successful_timeframes': successful_timeframes
|
| 610 |
}
|
|
|
|
| 611 |
return result_data
|
| 612 |
+
except Exception as price_error: return None
|
| 613 |
+
else: return None
|
| 614 |
+
except Exception as e: return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
|
| 616 |
async def _fetch_single_timeframe_improved(self, symbol: str, timeframe: str, limit: int):
|
| 617 |
"""جلب بيانات إطار زمني واحد مع تحسين التعامل مع الأخطاء"""
|
| 618 |
max_retries = 3
|
| 619 |
retry_delay = 2
|
|
|
|
| 620 |
for attempt in range(max_retries):
|
| 621 |
try:
|
| 622 |
ohlcv_data = self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
|
|
|
|
| 623 |
if ohlcv_data and len(ohlcv_data) > 0:
|
| 624 |
return ohlcv_data
|
| 625 |
else:
|
| 626 |
return []
|
|
|
|
| 627 |
except Exception as e:
|
| 628 |
if attempt < max_retries - 1:
|
| 629 |
await asyncio.sleep(retry_delay * (attempt + 1))
|
|
|
|
| 633 |
async def get_latest_price_async(self, symbol):
|
| 634 |
"""جلب السعر الحالي لعملة محددة"""
|
| 635 |
try:
|
| 636 |
+
if not self.exchange: return None
|
| 637 |
+
if not symbol or '/' not in symbol: return None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
ticker = self.exchange.fetch_ticker(symbol)
|
| 639 |
+
if not ticker: return None
|
|
|
|
|
|
|
|
|
|
| 640 |
current_price = ticker.get('last')
|
| 641 |
+
if current_price is None: return None
|
|
|
|
|
|
|
|
|
|
| 642 |
return float(current_price)
|
| 643 |
+
except Exception as e: return None
|
|
|
|
|
|
|
| 644 |
|
| 645 |
# (دوال دعم بيانات الحيتان تبقى كما هي - لا تغيير)
|
| 646 |
async def get_whale_data_for_symbol(self, symbol):
|
|
|
|
| 647 |
try:
|
| 648 |
if self.whale_monitor:
|
| 649 |
whale_data = await self.whale_monitor.get_symbol_whale_activity(symbol)
|
| 650 |
return whale_data
|
| 651 |
+
else: return None
|
| 652 |
+
except Exception as e: return None
|
|
|
|
|
|
|
| 653 |
|
| 654 |
async def get_whale_trading_signal(self, symbol, whale_data, market_context):
|
|
|
|
| 655 |
try:
|
| 656 |
if self.whale_monitor:
|
| 657 |
return await self.whale_monitor.generate_whale_trading_signal(symbol, whale_data, market_context)
|
| 658 |
else:
|
| 659 |
+
return {'action': 'HOLD', 'confidence': 0.3, 'reason': 'Whale monitor not available', 'source': 'whale_analysis'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
except Exception as e:
|
| 661 |
+
return {'action': 'HOLD', 'confidence': 0.3, 'reason': f'Error: {str(e)}', 'source': 'whale_analysis'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 662 |
|
| 663 |
+
print("✅ DataManager loaded - V7.1 (1H Detector Screening)")
|