Update data_manager.py
Browse files- data_manager.py +311 -828
data_manager.py
CHANGED
|
@@ -8,8 +8,8 @@ from datetime import datetime
|
|
| 8 |
import ccxt.pro as ccxt
|
| 9 |
import numpy as np
|
| 10 |
import logging
|
|
|
|
| 11 |
|
| 12 |
-
# تعطيل تسجيل HTTP المزعج فقط
|
| 13 |
logging.getLogger("httpx").setLevel(logging.WARNING)
|
| 14 |
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
| 15 |
|
|
@@ -31,33 +31,15 @@ class DataManager:
|
|
| 31 |
print(f"❌ فشل تهيئة اتصال KuCoin: {e}")
|
| 32 |
self.exchange = None
|
| 33 |
|
| 34 |
-
self._whale_data_cache = {}
|
| 35 |
self.http_client = None
|
| 36 |
-
self.fetch_stats = {'successful_fetches': 0, 'failed_fetches': 0, 'rate_limit_hits': 0}
|
| 37 |
-
self.price_cache = {}
|
| 38 |
self.market_cache = {}
|
| 39 |
self.last_market_load = None
|
| 40 |
|
| 41 |
-
self.price_sources = {
|
| 42 |
-
'kucoin': self._get_prices_from_kucoin_safe,
|
| 43 |
-
'coingecko': self._get_prices_from_coingecko
|
| 44 |
-
}
|
| 45 |
-
|
| 46 |
async def initialize(self):
|
| 47 |
self.http_client = httpx.AsyncClient(timeout=20.0)
|
| 48 |
-
|
| 49 |
-
api_status = {
|
| 50 |
-
'KUCOIN': '🟢 عام (بدون مفتاح)',
|
| 51 |
-
'MORALIS_KEY': "🟢 متوفر" if os.getenv('MORALIS_KEY') else "🔴 غير متوفر",
|
| 52 |
-
'ETHERSCAN_KEY': "🟢 متوفر" if os.getenv('ETHERSCAN_KEY') else "🔴 غير متوفر",
|
| 53 |
-
'INFURA_KEY': "🟢 متوفر" if os.getenv('INFURA_KEY') else "🔴 غير متوفر"
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
for key, status in api_status.items():
|
| 57 |
-
print(f" {key}: {status}")
|
| 58 |
-
|
| 59 |
await self._load_markets()
|
| 60 |
-
|
|
|
|
| 61 |
async def _load_markets(self):
|
| 62 |
try:
|
| 63 |
if not self.exchange:
|
|
@@ -78,232 +60,78 @@ class DataManager:
|
|
| 78 |
if self.exchange:
|
| 79 |
await self.exchange.close()
|
| 80 |
|
| 81 |
-
async def
|
| 82 |
-
|
| 83 |
-
cache_key = f"{network}_price"
|
| 84 |
-
|
| 85 |
-
if cache_key in self.price_cache and (now - self.price_cache[cache_key]['timestamp']) < 300:
|
| 86 |
-
return self.price_cache[cache_key]['price']
|
| 87 |
-
|
| 88 |
-
symbol_map = {
|
| 89 |
-
'ethereum': 'ETH',
|
| 90 |
-
'bsc': 'BNB',
|
| 91 |
-
'bitcoin': 'BTC'
|
| 92 |
-
}
|
| 93 |
-
|
| 94 |
-
symbol = symbol_map.get(network)
|
| 95 |
-
if not symbol:
|
| 96 |
-
return await self._get_price_from_coingecko_fallback(network)
|
| 97 |
-
|
| 98 |
-
try:
|
| 99 |
-
price = await self._get_price_from_kucoin(symbol)
|
| 100 |
-
if price and price > 0:
|
| 101 |
-
self.price_cache[cache_key] = {'price': price, 'timestamp': now, 'source': 'kucoin'}
|
| 102 |
-
return price
|
| 103 |
-
|
| 104 |
-
price = await self._get_price_from_coingecko_fallback(network)
|
| 105 |
-
if price and price > 0:
|
| 106 |
-
self.price_cache[cache_key] = {'price': price, 'timestamp': now, 'source': 'coingecko'}
|
| 107 |
-
return price
|
| 108 |
-
|
| 109 |
-
return None
|
| 110 |
-
|
| 111 |
-
except Exception as e:
|
| 112 |
-
print(f"❌ فشل جلب سعر {network}: {e}")
|
| 113 |
-
return None
|
| 114 |
-
|
| 115 |
-
async def _get_price_from_kucoin(self, symbol):
|
| 116 |
-
if not self.exchange:
|
| 117 |
-
return None
|
| 118 |
-
|
| 119 |
try:
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
-
|
| 123 |
-
print(f"⚠️ السوق {trading_symbol} غير متوفر في KuCoin")
|
| 124 |
-
return None
|
| 125 |
|
| 126 |
-
ticker = await self.exchange.fetch_ticker(trading_symbol)
|
| 127 |
-
price = ticker.get('last')
|
| 128 |
-
if price and price > 0:
|
| 129 |
-
return float(price)
|
| 130 |
-
else:
|
| 131 |
-
print(f"⚠️ لم يتم العثور على سعر صالح لـ {symbol}")
|
| 132 |
-
return None
|
| 133 |
-
|
| 134 |
except Exception as e:
|
| 135 |
-
print(f"❌ فشل جلب
|
| 136 |
-
return
|
| 137 |
-
|
| 138 |
-
async def _get_price_from_coingecko_fallback(self, network):
|
| 139 |
-
coin_map = {
|
| 140 |
-
'ethereum': 'ethereum',
|
| 141 |
-
'bsc': 'binancecoin',
|
| 142 |
-
'bitcoin': 'bitcoin'
|
| 143 |
-
}
|
| 144 |
-
|
| 145 |
-
coin_id = coin_map.get(network)
|
| 146 |
-
if not coin_id:
|
| 147 |
-
return None
|
| 148 |
|
|
|
|
|
|
|
| 149 |
try:
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
async with httpx.AsyncClient() as client:
|
| 153 |
-
response = await client.get(url, timeout=10)
|
| 154 |
response.raise_for_status()
|
| 155 |
data = response.json()
|
| 156 |
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
print(f"✅ سعر {network.upper()} (CoinGecko): ${price:,.2f}")
|
| 160 |
-
return price
|
| 161 |
-
else:
|
| 162 |
-
print(f"⚠️ لم يتم العثور على سعر {network} في CoinGecko")
|
| 163 |
-
return None
|
| 164 |
-
|
| 165 |
-
except Exception as e:
|
| 166 |
-
print(f"❌ فشل جلب سعر {network} من CoinGecko: {e}")
|
| 167 |
-
return None
|
| 168 |
-
|
| 169 |
-
async def get_sentiment_safe_async(self):
|
| 170 |
-
max_retries = 2
|
| 171 |
-
for attempt in range(max_retries):
|
| 172 |
-
try:
|
| 173 |
-
async with httpx.AsyncClient(timeout=8) as client:
|
| 174 |
-
response = await client.get("https://api.alternative.me/fng/")
|
| 175 |
-
response.raise_for_status()
|
| 176 |
-
data = response.json()
|
| 177 |
-
|
| 178 |
-
if 'data' not in data or not data['data']:
|
| 179 |
-
raise ValueError("بيانات المشاعر غير متوفرة في الاستجابة")
|
| 180 |
-
|
| 181 |
-
latest_data = data['data'][0]
|
| 182 |
-
return {
|
| 183 |
-
"feargreed_value": int(latest_data['value']),
|
| 184 |
-
"feargreed_class": latest_data['value_classification'],
|
| 185 |
-
"source": "alternative.me",
|
| 186 |
-
"timestamp": datetime.now().isoformat()
|
| 187 |
-
}
|
| 188 |
-
except Exception as e:
|
| 189 |
-
print(f"❌ فشل جلب بيانات المشاعر (المحاولة {attempt + 1}): {e}")
|
| 190 |
-
if attempt < max_retries - 1:
|
| 191 |
-
await asyncio.sleep(1)
|
| 192 |
-
|
| 193 |
-
return None
|
| 194 |
-
|
| 195 |
-
async def get_market_context_async(self):
|
| 196 |
-
max_retries = 2
|
| 197 |
-
for attempt in range(max_retries):
|
| 198 |
-
try:
|
| 199 |
-
sentiment_task = asyncio.wait_for(self.get_sentiment_safe_async(), timeout=10)
|
| 200 |
-
price_task = asyncio.wait_for(self._get_prices_with_fallback(), timeout=15)
|
| 201 |
-
|
| 202 |
-
results = await asyncio.gather(sentiment_task, price_task, return_exceptions=True)
|
| 203 |
-
|
| 204 |
-
sentiment_data = results[0] if not isinstance(results[0], Exception) else None
|
| 205 |
-
price_data = results[1] if not isinstance(results[1], Exception) else {}
|
| 206 |
-
|
| 207 |
-
bitcoin_price = price_data.get('bitcoin')
|
| 208 |
-
ethereum_price = price_data.get('ethereum')
|
| 209 |
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
market_trend = self._determine_market_trend(bitcoin_price, sentiment_data)
|
| 218 |
-
|
| 219 |
-
trading_decision = self._analyze_market_trading_signals(sentiment_data)
|
| 220 |
-
|
| 221 |
-
market_context = {
|
| 222 |
-
'timestamp': datetime.now().isoformat(),
|
| 223 |
-
'bitcoin_price_usd': bitcoin_price,
|
| 224 |
-
'ethereum_price_usd': ethereum_price,
|
| 225 |
-
'fear_and_greed_index': sentiment_data.get('feargreed_value') if sentiment_data else None,
|
| 226 |
-
'sentiment_class': sentiment_data.get('feargreed_class') if sentiment_data else 'UNKNOWN',
|
| 227 |
-
'general_whale_activity': {
|
| 228 |
-
'data_available': False,
|
| 229 |
-
'description': 'نظام الحيتان العام معطل - يركز النظام على تحليل الحيتان للعملات المرشحة فقط',
|
| 230 |
-
'critical_alert': False,
|
| 231 |
-
'sentiment': 'NEUTRAL',
|
| 232 |
-
'trading_signals': []
|
| 233 |
-
},
|
| 234 |
-
'market_trend': market_trend,
|
| 235 |
-
'trading_decision': trading_decision,
|
| 236 |
-
'btc_sentiment': self._get_btc_sentiment(bitcoin_price),
|
| 237 |
-
'data_sources': {
|
| 238 |
-
'prices': bitcoin_price is not None and ethereum_price is not None,
|
| 239 |
-
'sentiment': sentiment_data is not None,
|
| 240 |
-
'general_whale_data': False,
|
| 241 |
-
'netflow_analysis': 'DISABLED'
|
| 242 |
-
},
|
| 243 |
-
'data_quality': 'HIGH',
|
| 244 |
-
'risk_assessment': self._assess_market_risk(sentiment_data)
|
| 245 |
}
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
except Exception as e:
|
| 250 |
-
print(f"❌ فشل جلب سياق السوق (المحاولة {attempt + 1}): {e}")
|
| 251 |
-
if attempt < max_retries - 1:
|
| 252 |
-
await asyncio.sleep(3)
|
| 253 |
-
|
| 254 |
-
return self._get_minimal_market_context()
|
| 255 |
|
| 256 |
-
def
|
| 257 |
-
"""
|
| 258 |
-
if
|
| 259 |
-
return
|
| 260 |
-
'action': 'HOLD',
|
| 261 |
-
'confidence': 0.0,
|
| 262 |
-
'reason': 'غير متوفر - لا توجد بيانات كافية عن مشاعر السوق',
|
| 263 |
-
'risk_level': 'UNKNOWN'
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
fear_greed = sentiment_data.get('feargreed_value', 50)
|
| 267 |
-
sentiment_class = sentiment_data.get('feargreed_class', 'NEUTRAL')
|
| 268 |
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
'risk_level': 'LOW'
|
| 275 |
-
}
|
| 276 |
-
elif fear_greed >= 75:
|
| 277 |
-
return {
|
| 278 |
-
'action': 'SELL',
|
| 279 |
-
'confidence': 0.6,
|
| 280 |
-
'reason': f'مستوى جشع مرتفع في السوق: {fear_greed} - احتياط بيعي',
|
| 281 |
-
'risk_level': 'MEDIUM'
|
| 282 |
-
}
|
| 283 |
-
else:
|
| 284 |
-
return {
|
| 285 |
-
'action': 'HOLD',
|
| 286 |
-
'confidence': 0.5,
|
| 287 |
-
'reason': f'مشاعر السوق متوازنة: {sentiment_class} ({fear_greed})',
|
| 288 |
-
'risk_level': 'LOW'
|
| 289 |
-
}
|
| 290 |
-
|
| 291 |
-
def _assess_market_risk(self, sentiment_data):
|
| 292 |
-
"""تقييم مخاطر السوق بناءً على المشاعر فقط"""
|
| 293 |
-
risk_factors = []
|
| 294 |
-
risk_score = 0
|
| 295 |
|
| 296 |
-
if sentiment_data and sentiment_data.get('feargreed_value'
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
|
| 303 |
-
if
|
| 304 |
-
return
|
|
|
|
|
|
|
| 305 |
else:
|
| 306 |
-
return
|
| 307 |
|
| 308 |
def _get_btc_sentiment(self, bitcoin_price):
|
| 309 |
if bitcoin_price is None:
|
|
@@ -316,17 +144,12 @@ class DataManager:
|
|
| 316 |
return 'NEUTRAL'
|
| 317 |
|
| 318 |
async def _get_prices_with_fallback(self):
|
|
|
|
| 319 |
try:
|
| 320 |
prices = await self._get_prices_from_kucoin_safe()
|
| 321 |
if prices.get('bitcoin') and prices.get('ethereum'):
|
| 322 |
return prices
|
| 323 |
-
|
| 324 |
-
prices = await self._get_prices_from_coingecko()
|
| 325 |
-
if prices.get('bitcoin') and prices.get('ethereum'):
|
| 326 |
-
return prices
|
| 327 |
-
|
| 328 |
-
return {'bitcoin': None, 'ethereum': None}
|
| 329 |
-
|
| 330 |
except Exception as e:
|
| 331 |
print(f"❌ فشل جلب الأسعار: {e}")
|
| 332 |
return {'bitcoin': None, 'ethereum': None}
|
|
@@ -338,37 +161,24 @@ class DataManager:
|
|
| 338 |
try:
|
| 339 |
prices = {'bitcoin': None, 'ethereum': None}
|
| 340 |
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
prices['bitcoin'] = btc_price
|
| 346 |
-
self.price_cache['bitcoin'] = btc_price
|
| 347 |
-
print(f"✅ سعر BTC: ${btc_price:,.2f}")
|
| 348 |
-
else:
|
| 349 |
-
print("⚠️ لم يتم العثور على سعر BTC صالح")
|
| 350 |
-
except Exception as e:
|
| 351 |
-
print(f"❌ فشل جلب سعر BTC: {e}")
|
| 352 |
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
prices['ethereum'] = eth_price
|
| 358 |
-
self.price_cache['ethereum'] = eth_price
|
| 359 |
-
print(f"✅ سعر ETH: ${eth_price:,.2f}")
|
| 360 |
-
else:
|
| 361 |
-
print("⚠️ لم يتم العثور على سعر ETH صالح")
|
| 362 |
-
except Exception as e:
|
| 363 |
-
print(f"❌ فشل جلب سعر ETH: {e}")
|
| 364 |
|
| 365 |
return prices
|
| 366 |
|
| 367 |
except Exception as e:
|
| 368 |
-
print(f"❌ خطأ في
|
| 369 |
return {'bitcoin': None, 'ethereum': None}
|
| 370 |
|
| 371 |
async def _get_prices_from_coingecko(self):
|
|
|
|
| 372 |
try:
|
| 373 |
await asyncio.sleep(0.5)
|
| 374 |
url = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd"
|
|
@@ -381,13 +191,8 @@ class DataManager:
|
|
| 381 |
eth_price = data.get('ethereum', {}).get('usd')
|
| 382 |
|
| 383 |
if btc_price and eth_price:
|
| 384 |
-
self.price_cache['bitcoin'] = btc_price
|
| 385 |
-
self.price_cache['ethereum'] = eth_price
|
| 386 |
-
print(f"✅ سعر BTC (CoinGecko): ${btc_price:,.2f}")
|
| 387 |
-
print(f"✅ سعر ETH (CoinGecko): ${eth_price:,.2f}")
|
| 388 |
return {'bitcoin': btc_price, 'ethereum': eth_price}
|
| 389 |
else:
|
| 390 |
-
print("⚠️ لم يتم العثور على أسعار صالحة من CoinGecko")
|
| 391 |
return {'bitcoin': None, 'ethereum': None}
|
| 392 |
|
| 393 |
except Exception as e:
|
|
@@ -395,593 +200,270 @@ class DataManager:
|
|
| 395 |
return {'bitcoin': None, 'ethereum': None}
|
| 396 |
|
| 397 |
def _get_minimal_market_context(self):
|
|
|
|
| 398 |
return {
|
| 399 |
'timestamp': datetime.now().isoformat(),
|
| 400 |
'data_available': False,
|
| 401 |
-
'data_sources': {'prices': False, 'sentiment': False, 'general_whale_data': False},
|
| 402 |
-
'error': 'غير متوفر - فشل في جلب بيانات السوق من المصادر الخارجية',
|
| 403 |
'market_trend': 'UNKNOWN',
|
| 404 |
'btc_sentiment': 'UNKNOWN',
|
| 405 |
-
'data_quality': 'LOW'
|
| 406 |
-
'general_whale_activity': {
|
| 407 |
-
'data_available': False,
|
| 408 |
-
'description': 'نظام الحيتان العام معطل - يركز النظام على تحليل الحيتان للعملات المرشحة فقط',
|
| 409 |
-
'critical_alert': False,
|
| 410 |
-
'sentiment': 'NEUTRAL'
|
| 411 |
-
},
|
| 412 |
-
'bitcoin_price_usd': None,
|
| 413 |
-
'ethereum_price_usd': None,
|
| 414 |
-
'fear_and_greed_index': None,
|
| 415 |
-
'sentiment_class': 'UNKNOWN',
|
| 416 |
-
'missing_data': ['غير متوفر - أسعار البيتكوين', 'غير متوفر - أسعار الإيثيريوم', 'غير متوفر - بيانات المشاعر']
|
| 417 |
}
|
| 418 |
|
| 419 |
-
def
|
| 420 |
-
"""
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 427 |
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
elif bitcoin_price < 55000:
|
| 431 |
-
score -= 1
|
| 432 |
|
| 433 |
-
|
| 434 |
-
fear_greed = sentiment_data.get('feargreed_value')
|
| 435 |
-
if fear_greed > 60:
|
| 436 |
-
score += 1
|
| 437 |
-
elif fear_greed < 40:
|
| 438 |
-
score -= 1
|
| 439 |
-
data_points += 1
|
| 440 |
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
if score >= 2:
|
| 445 |
-
return "bull_market"
|
| 446 |
-
elif score <= -2:
|
| 447 |
-
return "bear_market"
|
| 448 |
-
elif -1 <= score <= 1:
|
| 449 |
-
return "sideways_market"
|
| 450 |
-
else:
|
| 451 |
-
return "volatile_market"
|
| 452 |
-
|
| 453 |
-
except Exception as e:
|
| 454 |
-
return "UNKNOWN"
|
| 455 |
-
|
| 456 |
-
def get_performance_stats(self):
|
| 457 |
-
total_attempts = self.fetch_stats['successful_fetches'] + self.fetch_stats['failed_fetches']
|
| 458 |
-
success_rate = (self.fetch_stats['successful_fetches'] / total_attempts * 100) if total_attempts > 0 else 0
|
| 459 |
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
'failed_fetches': self.fetch_stats['failed_fetches'],
|
| 464 |
-
'rate_limit_hits': self.fetch_stats['rate_limit_hits'],
|
| 465 |
-
'success_rate': f"{success_rate:.1f}%",
|
| 466 |
-
'timestamp': datetime.now().isoformat(),
|
| 467 |
-
'exchange_available': self.exchange is not None,
|
| 468 |
-
'markets_loaded': len(self.market_cache) if self.market_cache else 0,
|
| 469 |
-
'last_market_load': self.last_market_load.isoformat() if self.last_market_load else None
|
| 470 |
-
}
|
| 471 |
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 475 |
|
| 476 |
-
return
|
| 477 |
|
| 478 |
-
async def
|
| 479 |
-
"""
|
| 480 |
-
|
| 481 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
else:
|
| 483 |
-
return
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
|
| 489 |
-
|
| 490 |
-
"""
|
| 491 |
-
if
|
| 492 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 493 |
else:
|
| 494 |
-
return
|
| 495 |
-
'action': 'HOLD',
|
| 496 |
-
'confidence': 0.3,
|
| 497 |
-
'reason': 'نظام تحليل الحيتان غير متوفر',
|
| 498 |
-
'source': 'whale_analysis'
|
| 499 |
-
}
|
| 500 |
|
| 501 |
-
|
| 502 |
-
"""
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
closes = np.array([candle[4] for candle in hourly_data])
|
| 519 |
-
volumes = np.array([candle[5] for candle in hourly_data])
|
| 520 |
-
highs = np.array([candle[2] for candle in hourly_data])
|
| 521 |
-
lows = np.array([candle[3] for candle in hourly_data])
|
| 522 |
-
opens = np.array([candle[1] for candle in hourly_data])
|
| 523 |
-
|
| 524 |
-
current_price = closes[-1]
|
| 525 |
-
|
| 526 |
-
# 1. تقييم الزخم السعري (Momentum) - الوزن الأكبر
|
| 527 |
-
momentum_score = 0.0
|
| 528 |
-
|
| 529 |
-
# زخم قصير المدى (1-4 ساعات)
|
| 530 |
-
if len(closes) >= 5:
|
| 531 |
-
price_change_1h = ((closes[-1] - closes[-2]) / closes[-2]) * 100
|
| 532 |
-
price_change_4h = ((closes[-1] - closes[-5]) / closes[-5]) * 100
|
| 533 |
-
|
| 534 |
-
# نفضل الزخم الإيجابي المعتدل (2%-10%)
|
| 535 |
-
if 2 <= price_change_1h <= 10:
|
| 536 |
-
momentum_score += 0.3
|
| 537 |
-
elif 2 <= price_change_4h <= 15:
|
| 538 |
-
momentum_score += 0.2
|
| 539 |
-
|
| 540 |
-
# زخم متوسط المدى (12-24 ساعة)
|
| 541 |
-
if len(closes) >= 25:
|
| 542 |
-
price_change_12h = ((closes[-1] - closes[-13]) / closes[-13]) * 100
|
| 543 |
-
price_change_24h = ((closes[-1] - closes[-25]) / closes[-25]) * 100
|
| 544 |
-
|
| 545 |
-
if 5 <= price_change_12h <= 20:
|
| 546 |
-
momentum_score += 0.2
|
| 547 |
-
if 8 <= price_change_24h <= 25:
|
| 548 |
-
momentum_score += 0.2
|
| 549 |
-
|
| 550 |
-
# 2. تقييم السيولة والحجم (Liquidity & Volume)
|
| 551 |
-
liquidity_score = 0.0
|
| 552 |
-
|
| 553 |
-
# متوسط الحجم بالدولار
|
| 554 |
-
avg_dollar_volume = np.mean(volumes[-50:]) * np.mean(closes[-50:])
|
| 555 |
-
if avg_dollar_volume > 1000000: # أكثر من 1 مليون دولار
|
| 556 |
-
liquidity_score += 0.4
|
| 557 |
-
elif avg_dollar_volume > 500000: # أكثر من 500 ألف دولار
|
| 558 |
-
liquidity_score += 0.3
|
| 559 |
-
elif avg_dollar_volume > 100000: # أكثر من 100 ألف دولار
|
| 560 |
-
liquidity_score += 0.2
|
| 561 |
-
|
| 562 |
-
# نسبة الحجم الحديث إلى المتوسط
|
| 563 |
-
recent_volume = np.mean(volumes[-6:]) # آخر 6 ساعات
|
| 564 |
-
avg_volume_50 = np.mean(volumes[-50:])
|
| 565 |
-
volume_ratio = recent_volume / avg_volume_50 if avg_volume_50 > 0 else 1.0
|
| 566 |
-
|
| 567 |
-
if volume_ratio > 1.5: # حجم مرتفع حديثاً
|
| 568 |
-
liquidity_score += 0.3
|
| 569 |
-
elif volume_ratio > 1.2:
|
| 570 |
-
liquidity_score += 0.2
|
| 571 |
-
|
| 572 |
-
# 3. تقييم قوة الاتجاه (Trend Strength)
|
| 573 |
-
trend_score = 0.0
|
| 574 |
-
|
| 575 |
-
if len(closes) >= 20:
|
| 576 |
-
# المتوسطات المتحركة
|
| 577 |
-
ma_short = np.mean(closes[-5:]) # 5 ساعات
|
| 578 |
-
ma_medium = np.mean(closes[-13:]) # 13 ساعة
|
| 579 |
-
ma_long = np.mean(closes[-21:]) # 21 ساعة
|
| 580 |
-
|
| 581 |
-
# تأكيد الاتجاه (جميع المتوسطات في نفس الاتجاه)
|
| 582 |
-
if ma_short > ma_medium > ma_long:
|
| 583 |
-
trend_score += 0.4 # اتجاه صاعد قوي
|
| 584 |
-
elif ma_short < ma_medium < ma_long:
|
| 585 |
-
trend_score += 0.2 # اتجاه هابط
|
| 586 |
-
|
| 587 |
-
# قوة الاتجاه (المسافة بين المتوسطات)
|
| 588 |
-
trend_strength = (ma_short - ma_long) / ma_long * 100
|
| 589 |
-
if abs(trend_strength) > 3: # اتجاه قوي (>3%)
|
| 590 |
-
trend_score += 0.2
|
| 591 |
-
|
| 592 |
-
# 4. تقييم التقلب (Volatility) - نفضل التقلب المعتدل
|
| 593 |
-
volatility_score = 0.0
|
| 594 |
-
|
| 595 |
-
true_ranges = []
|
| 596 |
-
for i in range(1, len(hourly_data)):
|
| 597 |
-
high, low, prev_close = highs[i], lows[i], closes[i-1]
|
| 598 |
-
tr1 = high - low
|
| 599 |
-
tr2 = abs(high - prev_close)
|
| 600 |
-
tr3 = abs(low - prev_close)
|
| 601 |
-
true_ranges.append(max(tr1, tr2, tr3))
|
| 602 |
-
|
| 603 |
-
if true_ranges:
|
| 604 |
-
atr = np.mean(true_ranges[-14:])
|
| 605 |
-
atr_percent = (atr / current_price) * 100
|
| 606 |
-
|
| 607 |
-
# نفضل التقلب بين 2% و 8% (فرص تداول جيدة)
|
| 608 |
-
if 2 <= atr_percent <= 8:
|
| 609 |
-
volatility_score = 0.3
|
| 610 |
-
elif atr_percent > 8:
|
| 611 |
-
volatility_score = 0.1 # تقلب عالي خطير
|
| 612 |
-
else:
|
| 613 |
-
volatility_score = 0.1 # تقلب منخفض قليل الفرص
|
| 614 |
-
|
| 615 |
-
# 5. تقييم الزخم الحجمي (Volume Momentum)
|
| 616 |
-
volume_momentum_score = 0.0
|
| 617 |
-
|
| 618 |
-
if len(volumes) >= 10:
|
| 619 |
-
# تسارع الحجم (الحجم الأخير vs المتوسط)
|
| 620 |
-
volume_acceleration = (volumes[-1] - np.mean(volumes[-10:])) / np.mean(volumes[-10:])
|
| 621 |
-
if volume_acceleration > 0.5: # زيادة حجم بنسبة 50%
|
| 622 |
-
volume_momentum_score = 0.2
|
| 623 |
-
|
| 624 |
-
# استمرارية الحجم المرتفع
|
| 625 |
-
above_avg_volume_count = sum(1 for vol in volumes[-5:] if vol > np.mean(volumes[-20:]))
|
| 626 |
-
if above_avg_volume_count >= 3: # 3 من آخر 5 ساعات فوق المتوسط
|
| 627 |
-
volume_momentum_score += 0.2
|
| 628 |
-
|
| 629 |
-
# 6. تقييم كفاءة الحركة (Price Efficiency)
|
| 630 |
-
efficiency_score = 0.0
|
| 631 |
-
|
| 632 |
-
if len(closes) >= 20:
|
| 633 |
-
# نسبة الشموع الخضراء
|
| 634 |
-
green_candles = sum(1 for i in range(1, len(closes)) if closes[i] > opens[i])
|
| 635 |
-
total_candles = len(closes) - 1
|
| 636 |
-
green_ratio = green_candles / total_candles if total_candles > 0 else 0
|
| 637 |
-
|
| 638 |
-
if 0.4 <= green_ratio <= 0.7: # توازن صحي
|
| 639 |
-
efficiency_score = 0.2
|
| 640 |
-
|
| 641 |
-
# قوة الشموع (متوسط طول الجسم)
|
| 642 |
-
candle_bodies = [abs(closes[i] - opens[i]) for i in range(len(closes))]
|
| 643 |
-
avg_body = np.mean(candle_bodies)
|
| 644 |
-
body_ratio = avg_body / current_price * 100
|
| 645 |
-
|
| 646 |
-
if 0.5 <= body_ratio <= 2: # شموع ذات أحجام معقولة
|
| 647 |
-
efficiency_score += 0.2
|
| 648 |
-
|
| 649 |
-
# الحساب النهائي مع الأوزان
|
| 650 |
-
weights = {
|
| 651 |
-
'momentum': 0.30, # الزخم السعري
|
| 652 |
-
'liquidity': 0.25, # السيولة والحجم
|
| 653 |
-
'trend': 0.20, # قوة الاتجاه
|
| 654 |
-
'volatility': 0.10, # التقلب
|
| 655 |
-
'volume_momentum': 0.10, # الزخم الحجمي
|
| 656 |
-
'efficiency': 0.05 # كفاءة الحركة
|
| 657 |
-
}
|
| 658 |
-
|
| 659 |
-
final_score = (
|
| 660 |
-
momentum_score * weights['momentum'] +
|
| 661 |
-
liquidity_score * weights['liquidity'] +
|
| 662 |
-
trend_score * weights['trend'] +
|
| 663 |
-
volatility_score * weights['volatility'] +
|
| 664 |
-
volume_momentum_score * weights['volume_momentum'] +
|
| 665 |
-
efficiency_score * weights['efficiency']
|
| 666 |
-
)
|
| 667 |
-
|
| 668 |
-
# معامل تصحيح إضافي للرموز ذات الزخم القوي
|
| 669 |
-
if momentum_score > 0.6 and volume_ratio > 1.8:
|
| 670 |
-
final_score = min(final_score * 1.2, 1.0)
|
| 671 |
-
|
| 672 |
-
return min(final_score, 1.0)
|
| 673 |
-
|
| 674 |
-
except Exception as e:
|
| 675 |
-
print(f"❌ خطأ في حساب الدرجة التقنية لـ {symbol}: {e}")
|
| 676 |
-
return 0.0
|
| 677 |
|
| 678 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 679 |
"""
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
- السيولة الجيدة (أحجام تداول عالية)
|
| 683 |
-
- اتجاهات تقنية واضحة
|
| 684 |
-
- مؤشرات حجم داعمة
|
| 685 |
"""
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
]
|
| 701 |
-
|
| 702 |
-
print(f"✅ تم العثور على {len(usdt_symbols)} رمز USDT نشط في KuCoin")
|
| 703 |
-
|
| 704 |
-
# المرحلة 1: الفحص السريع بناءً على بيانات Ticker
|
| 705 |
-
print("📊 المرحلة 1: الفحص السريع بناءً على أحجام التداول والزخم...")
|
| 706 |
-
initial_candidates = []
|
| 707 |
-
|
| 708 |
-
for i, symbol in enumerate(usdt_symbols):
|
| 709 |
-
try:
|
| 710 |
-
# الحصول على بيانات التداول الأساسية
|
| 711 |
-
ticker = await self.exchange.fetch_ticker(symbol)
|
| 712 |
-
if not ticker:
|
| 713 |
-
continue
|
| 714 |
-
|
| 715 |
-
volume_24h = ticker.get('baseVolume', 0)
|
| 716 |
-
price_change_24h = ticker.get('percentage', 0)
|
| 717 |
-
current_price = ticker.get('last', 0)
|
| 718 |
-
|
| 719 |
-
# معايير الفحص الأولي:
|
| 720 |
-
# - حجم تداول > 100,000 دولار
|
| 721 |
-
# - تغير سعري > 1% (إيجابي أو سلبي)
|
| 722 |
-
# - سعر بين 0.001 و 500 دولار (تجنب العملات منخفضة/مرتفعة السعر جداً)
|
| 723 |
-
if (volume_24h * current_price > 100000 and # حجم بالدولار
|
| 724 |
-
abs(price_change_24h) > 1.0 and # زخم سعري
|
| 725 |
-
0.001 <= current_price <= 500): # نطاق سعري معقول
|
| 726 |
-
|
| 727 |
-
reasons = []
|
| 728 |
-
if price_change_24h > 5:
|
| 729 |
-
reasons.append('strong_positive_momentum')
|
| 730 |
-
elif price_change_24h > 2:
|
| 731 |
-
reasons.append('positive_momentum')
|
| 732 |
-
|
| 733 |
-
if volume_24h * current_price > 1000000:
|
| 734 |
-
reasons.append('high_liquidity')
|
| 735 |
-
elif volume_24h * current_price > 500000:
|
| 736 |
-
reasons.append('good_liquidity')
|
| 737 |
-
|
| 738 |
-
initial_candidates.append({
|
| 739 |
-
'symbol': symbol,
|
| 740 |
-
'volume_24h': volume_24h,
|
| 741 |
-
'dollar_volume': volume_24h * current_price,
|
| 742 |
-
'price_change_24h': price_change_24h,
|
| 743 |
-
'current_price': current_price,
|
| 744 |
-
'reasons': reasons
|
| 745 |
-
})
|
| 746 |
-
|
| 747 |
-
# عرض التقدم كل 100 رمز
|
| 748 |
-
if i % 100 == 0 and i > 0:
|
| 749 |
-
print(f" 🔍 تم فحص {i} رمز، وجدنا {len(initial_candidates)} مرشح أولي")
|
| 750 |
-
|
| 751 |
-
except Exception as e:
|
| 752 |
-
if "rate limit" not in str(e).lower():
|
| 753 |
-
continue
|
| 754 |
-
|
| 755 |
-
print(f"✅ انتهت المرحلة 1: تم اختيار {len(initial_candidates)} مرشح أولي")
|
| 756 |
-
|
| 757 |
-
# إذا لم نجد مرشحين كافيين، نخفف المعايير
|
| 758 |
-
if len(initial_candidates) < count * 2:
|
| 759 |
-
print("⚠️ عدد المرشحين قليل، نخفف معايير الفحص...")
|
| 760 |
-
for symbol in usdt_symbols:
|
| 761 |
-
if len(initial_candidates) >= count * 3:
|
| 762 |
-
break
|
| 763 |
-
|
| 764 |
-
if symbol in [c['symbol'] for c in initial_candidates]:
|
| 765 |
-
continue
|
| 766 |
-
|
| 767 |
try:
|
| 768 |
-
|
| 769 |
-
if
|
| 770 |
-
continue
|
| 771 |
-
|
| 772 |
-
volume_24h = ticker.get('baseVolume', 0)
|
| 773 |
-
price_change_24h = ticker.get('percentage', 0)
|
| 774 |
-
current_price = ticker.get('last', 0)
|
| 775 |
-
|
| 776 |
-
# معايير مخففة
|
| 777 |
-
if (volume_24h * current_price > 50000 and # حجم بالدولار
|
| 778 |
-
abs(price_change_24h) > 0.5 and # زخم سعري
|
| 779 |
-
0.0005 <= current_price <= 1000): # نطاق سعري أوسع
|
| 780 |
-
|
| 781 |
-
reasons = ['relaxed_criteria']
|
| 782 |
-
if price_change_24h > 0:
|
| 783 |
-
reasons.append('positive_trend')
|
| 784 |
-
|
| 785 |
-
initial_candidates.append({
|
| 786 |
-
'symbol': symbol,
|
| 787 |
-
'volume_24h': volume_24h,
|
| 788 |
-
'dollar_volume': volume_24h * current_price,
|
| 789 |
-
'price_change_24h': price_change_24h,
|
| 790 |
-
'current_price': current_price,
|
| 791 |
-
'reasons': reasons
|
| 792 |
-
})
|
| 793 |
-
|
| 794 |
-
except Exception:
|
| 795 |
-
continue
|
| 796 |
-
|
| 797 |
-
print(f"📊 بعد التخفيف: {len(initial_candidates)} مرشح")
|
| 798 |
-
|
| 799 |
-
# المرحلة 2: التحليل التقني المتعمق
|
| 800 |
-
print("📈 المرحلة 2: التحليل التقني المتعمق للمرشحين...")
|
| 801 |
-
candidates_with_scores = []
|
| 802 |
-
analyzed_count = 0
|
| 803 |
-
|
| 804 |
-
# نرتب المرشحين بحجم التداول (الأكبر أولاً)
|
| 805 |
-
initial_candidates.sort(key=lambda x: x['dollar_volume'], reverse=True)
|
| 806 |
-
|
| 807 |
-
for candidate in initial_candidates[:100]: # نأخذ أفضل 100 من حيث الحجم
|
| 808 |
-
try:
|
| 809 |
-
symbol = candidate['symbol']
|
| 810 |
-
analyzed_count += 1
|
| 811 |
-
|
| 812 |
-
if analyzed_count % 10 == 0:
|
| 813 |
-
print(f" 🔍 تحليل تقني {analyzed_count} من {min(100, len(initial_candidates))}")
|
| 814 |
-
|
| 815 |
-
# جلب بيانات OHLCV للتحليل التقني
|
| 816 |
-
ohlcv_1h = await self.exchange.fetch_ohlcv(symbol, '1h', limit=100)
|
| 817 |
-
|
| 818 |
-
if not ohlcv_1h or len(ohlcv_1h) < 50:
|
| 819 |
-
continue
|
| 820 |
-
|
| 821 |
-
ohlcv_data = {'1h': ohlcv_1h}
|
| 822 |
-
|
| 823 |
-
# حساب الدرجة التقنية الشاملة
|
| 824 |
-
technical_score = await self._calculate_technical_score(symbol, ohlcv_data)
|
| 825 |
-
|
| 826 |
-
if technical_score > 0.3: # عتبة مقبولة للجودة
|
| 827 |
-
# تحديث أسباب الترشيح بناءً على التحليل التقني
|
| 828 |
-
reasons = candidate['reasons']
|
| 829 |
-
|
| 830 |
-
if technical_score > 0.7:
|
| 831 |
-
reasons.append('excellent_technical_score')
|
| 832 |
-
elif technical_score > 0.5:
|
| 833 |
-
reasons.append('good_technical_score')
|
| 834 |
-
|
| 835 |
-
# إضافة أسباب تقنية
|
| 836 |
-
if candidate['price_change_24h'] > 8:
|
| 837 |
-
reasons.append('strong_daily_momentum')
|
| 838 |
-
elif candidate['price_change_24h'] > 3:
|
| 839 |
-
reasons.append('moderate_daily_momentum')
|
| 840 |
-
|
| 841 |
-
if candidate['dollar_volume'] > 2000000:
|
| 842 |
-
reasons.append('very_high_liquidity')
|
| 843 |
-
elif candidate['dollar_volume'] > 500000:
|
| 844 |
-
reasons.append('high_liquidity')
|
| 845 |
-
|
| 846 |
-
candidates_with_scores.append({
|
| 847 |
-
'symbol': symbol,
|
| 848 |
-
'technical_score': technical_score,
|
| 849 |
-
'reasons': reasons,
|
| 850 |
-
'volume': candidate['volume_24h'],
|
| 851 |
-
'dollar_volume': candidate['dollar_volume'],
|
| 852 |
-
'price_change': candidate['price_change_24h'],
|
| 853 |
-
'current_price': candidate['current_price'],
|
| 854 |
-
'market_cap_category': self._classify_market_cap(candidate['dollar_volume'])
|
| 855 |
-
})
|
| 856 |
-
|
| 857 |
-
except Exception as e:
|
| 858 |
-
if "rate limit" not in str(e).lower():
|
| 859 |
-
print(f"⚠️ خطأ في التحليل التقني للرمز {symbol}: {e}")
|
| 860 |
-
continue
|
| 861 |
-
|
| 862 |
-
print(f"✅ انتهت المرحلة 2: {len(candidates_with_scores)} مرشح اجتازوا التحليل التقني")
|
| 863 |
-
|
| 864 |
-
# المرحلة 3: الاختيار النهائي مع التنوع
|
| 865 |
-
if not candidates_with_scores:
|
| 866 |
-
print("❌ لم يتم العثور على أي مرشح يلبي المعايير")
|
| 867 |
-
return []
|
| 868 |
-
|
| 869 |
-
# ترتيب المرشحين حسب الدرجة التقنية
|
| 870 |
-
candidates_with_scores.sort(key=lambda x: x['technical_score'], reverse=True)
|
| 871 |
-
|
| 872 |
-
# نأخذ أفضل المرشحين مع الحفاظ على التنوع
|
| 873 |
-
final_candidates = []
|
| 874 |
-
categories_count = {'LARGE': 0, 'MID': 0, 'SMALL': 0}
|
| 875 |
-
max_per_category = max(1, count // 3)
|
| 876 |
-
|
| 877 |
-
for candidate in candidates_with_scores:
|
| 878 |
-
category = candidate['market_cap_category']
|
| 879 |
-
|
| 880 |
-
if categories_count[category] < max_per_category:
|
| 881 |
-
final_candidates.append(candidate)
|
| 882 |
-
categories_count[category] += 1
|
| 883 |
-
elif len(final_candidates) < count:
|
| 884 |
-
final_candidates.append(candidate)
|
| 885 |
-
|
| 886 |
-
if len(final_candidates) >= count:
|
| 887 |
-
break
|
| 888 |
-
|
| 889 |
-
print(f"🎯 الاختيار النهائي: {len(final_candidates)} مرشح متنوع")
|
| 890 |
-
print(f" 📊 تصنيف الأحجام: Large({categories_count['LARGE']}), Mid({categories_count['MID']}), Small({categories_count['SMALL']})")
|
| 891 |
-
|
| 892 |
-
# عرض أفضل 5 مرشحين
|
| 893 |
-
for i, candidate in enumerate(final_candidates[:5]):
|
| 894 |
-
print(f" 🥇 {i+1}. {candidate['symbol']}: درجة {candidate['technical_score']:.3f} - تغير {candidate['price_change']:.1f}% - حجم ${candidate['dollar_volume']:,.0f}")
|
| 895 |
-
|
| 896 |
-
return final_candidates
|
| 897 |
-
|
| 898 |
-
except Exception as e:
|
| 899 |
-
print(f"❌ خطأ في find_high_potential_candidates: {e}")
|
| 900 |
-
import traceback
|
| 901 |
-
traceback.print_exc()
|
| 902 |
-
return []
|
| 903 |
-
|
| 904 |
-
def _classify_market_cap(self, dollar_volume):
|
| 905 |
-
"""تصنيف العملات حسب حجم التداول"""
|
| 906 |
-
if dollar_volume > 5000000: # أكثر من 5 ملايين دولار
|
| 907 |
-
return "LARGE"
|
| 908 |
-
elif dollar_volume > 1000000: # أكثر من 1 مليون دولار
|
| 909 |
-
return "MID"
|
| 910 |
-
else: # أقل من 1 مليون دولار
|
| 911 |
-
return "SMALL"
|
| 912 |
-
|
| 913 |
-
async def get_fast_pass_data_async(self, candidates):
|
| 914 |
-
try:
|
| 915 |
-
print(f"📊 جلب بيانات OHLCV لـ {len(candidates)} مرشح من KuCoin...")
|
| 916 |
-
results = []
|
| 917 |
-
|
| 918 |
-
timeframes = [
|
| 919 |
-
('5m', 200),
|
| 920 |
-
('15m', 200),
|
| 921 |
-
('1h', 200),
|
| 922 |
-
('4h', 200),
|
| 923 |
-
('1d', 200),
|
| 924 |
-
('1w', 200)
|
| 925 |
-
]
|
| 926 |
-
|
| 927 |
-
for candidate in candidates:
|
| 928 |
-
symbol = candidate['symbol']
|
| 929 |
-
|
| 930 |
-
try:
|
| 931 |
-
ohlcv_data = {}
|
| 932 |
-
has_sufficient_data = True
|
| 933 |
-
|
| 934 |
-
for timeframe, limit in timeframes:
|
| 935 |
-
try:
|
| 936 |
-
ohlcv = await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
|
| 937 |
-
|
| 938 |
-
if not ohlcv or len(ohlcv) < 50:
|
| 939 |
-
print(f"⚠️ بيانات غير كافية للرمز {symbol} في الإطار {timeframe}: {len(ohlcv) if ohlcv else 0} شمعة")
|
| 940 |
-
has_sufficient_data = False
|
| 941 |
-
break
|
| 942 |
-
|
| 943 |
ohlcv_data[timeframe] = ohlcv
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
await asyncio.sleep(0.1)
|
| 947 |
-
|
| 948 |
-
except Exception as e:
|
| 949 |
-
print(f"❌ خطأ في جلب بيانات {symbol} للإطار {timeframe}: {e}")
|
| 950 |
has_sufficient_data = False
|
| 951 |
break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 952 |
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
| 964 |
-
|
| 965 |
-
|
| 966 |
-
|
| 967 |
-
|
| 968 |
-
|
| 969 |
-
|
| 970 |
-
print(f"✅ تم تجميع بيانات لـ {len(results)} مرشح بنجاح")
|
| 971 |
-
return results
|
| 972 |
-
|
| 973 |
-
except Exception as e:
|
| 974 |
-
print(f"❌ خطأ في get_fast_pass_data_async: {e}")
|
| 975 |
-
return []
|
| 976 |
|
| 977 |
async def get_latest_price_async(self, symbol):
|
|
|
|
| 978 |
try:
|
| 979 |
if not self.exchange:
|
| 980 |
-
print("❌ لا يوجد اتصال بـ KuCoin")
|
| 981 |
-
return None
|
| 982 |
-
|
| 983 |
-
if symbol not in self.market_cache:
|
| 984 |
-
print(f"⚠️ السوق {symbol} غير متوفر في KuCoin")
|
| 985 |
return None
|
| 986 |
|
| 987 |
ticker = await self.exchange.fetch_ticker(symbol)
|
|
@@ -990,14 +472,14 @@ class DataManager:
|
|
| 990 |
if current_price:
|
| 991 |
return float(current_price)
|
| 992 |
else:
|
| 993 |
-
print(f"❌ لم يتم العثور على سعر لـ {symbol}")
|
| 994 |
return None
|
| 995 |
|
| 996 |
except Exception as e:
|
| 997 |
-
print(f"❌ خطأ في
|
| 998 |
return None
|
| 999 |
|
| 1000 |
async def get_available_symbols(self):
|
|
|
|
| 1001 |
try:
|
| 1002 |
if not self.exchange:
|
| 1003 |
return []
|
|
@@ -1013,10 +495,11 @@ class DataManager:
|
|
| 1013 |
return usdt_symbols
|
| 1014 |
|
| 1015 |
except Exception as e:
|
| 1016 |
-
print(f"❌ خطأ في
|
| 1017 |
return []
|
| 1018 |
|
| 1019 |
async def validate_symbol(self, symbol):
|
|
|
|
| 1020 |
try:
|
| 1021 |
if not self.exchange:
|
| 1022 |
return False
|
|
@@ -1027,7 +510,7 @@ class DataManager:
|
|
| 1027 |
return symbol in self.market_cache and self.market_cache[symbol].get('active', False)
|
| 1028 |
|
| 1029 |
except Exception as e:
|
| 1030 |
-
print(f"❌ خطأ في
|
| 1031 |
return False
|
| 1032 |
|
| 1033 |
-
print("✅ DataManager loaded -
|
|
|
|
| 8 |
import ccxt.pro as 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)
|
| 15 |
|
|
|
|
| 31 |
print(f"❌ فشل تهيئة اتصال KuCoin: {e}")
|
| 32 |
self.exchange = None
|
| 33 |
|
|
|
|
| 34 |
self.http_client = None
|
|
|
|
|
|
|
| 35 |
self.market_cache = {}
|
| 36 |
self.last_market_load = None
|
| 37 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
async def initialize(self):
|
| 39 |
self.http_client = httpx.AsyncClient(timeout=20.0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
await self._load_markets()
|
| 41 |
+
print("✅ DataManager initialized - Focused on data retrieval only")
|
| 42 |
+
|
| 43 |
async def _load_markets(self):
|
| 44 |
try:
|
| 45 |
if not self.exchange:
|
|
|
|
| 60 |
if self.exchange:
|
| 61 |
await self.exchange.close()
|
| 62 |
|
| 63 |
+
async def get_market_context_async(self):
|
| 64 |
+
"""جلب سياق السوق الأساسي فقط"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
try:
|
| 66 |
+
sentiment_data = await self.get_sentiment_safe_async()
|
| 67 |
+
price_data = await self._get_prices_with_fallback()
|
| 68 |
+
|
| 69 |
+
bitcoin_price = price_data.get('bitcoin')
|
| 70 |
+
ethereum_price = price_data.get('ethereum')
|
| 71 |
+
|
| 72 |
+
market_context = {
|
| 73 |
+
'timestamp': datetime.now().isoformat(),
|
| 74 |
+
'bitcoin_price_usd': bitcoin_price,
|
| 75 |
+
'ethereum_price_usd': ethereum_price,
|
| 76 |
+
'fear_and_greed_index': sentiment_data.get('feargreed_value') if sentiment_data else None,
|
| 77 |
+
'sentiment_class': sentiment_data.get('feargreed_class') if sentiment_data else 'NEUTRAL',
|
| 78 |
+
'market_trend': self._determine_market_trend(bitcoin_price, sentiment_data),
|
| 79 |
+
'btc_sentiment': self._get_btc_sentiment(bitcoin_price),
|
| 80 |
+
'data_quality': 'HIGH' if bitcoin_price and ethereum_price else 'LOW'
|
| 81 |
+
}
|
| 82 |
|
| 83 |
+
return market_context
|
|
|
|
|
|
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
except Exception as e:
|
| 86 |
+
print(f"❌ فشل جلب سياق السوق: {e}")
|
| 87 |
+
return self._get_minimal_market_context()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
+
async def get_sentiment_safe_async(self):
|
| 90 |
+
"""جلب بيانات المشاعر"""
|
| 91 |
try:
|
| 92 |
+
async with httpx.AsyncClient(timeout=8) as client:
|
| 93 |
+
response = await client.get("https://api.alternative.me/fng/")
|
|
|
|
|
|
|
| 94 |
response.raise_for_status()
|
| 95 |
data = response.json()
|
| 96 |
|
| 97 |
+
if 'data' not in data or not data['data']:
|
| 98 |
+
raise ValueError("بيانات المشاعر غير متوفرة")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
+
latest_data = data['data'][0]
|
| 101 |
+
return {
|
| 102 |
+
"feargreed_value": int(latest_data['value']),
|
| 103 |
+
"feargreed_class": latest_data['value_classification'],
|
| 104 |
+
"source": "alternative.me",
|
| 105 |
+
"timestamp": datetime.now().isoformat()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
+
except Exception as e:
|
| 108 |
+
print(f"❌ فشل جلب بيانات المشاعر: {e}")
|
| 109 |
+
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
def _determine_market_trend(self, bitcoin_price, sentiment_data):
|
| 112 |
+
"""تحديد اتجاه السوق"""
|
| 113 |
+
if bitcoin_price is None:
|
| 114 |
+
return "UNKNOWN"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
| 116 |
+
score = 0
|
| 117 |
+
if bitcoin_price > 60000:
|
| 118 |
+
score += 1
|
| 119 |
+
elif bitcoin_price < 55000:
|
| 120 |
+
score -= 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
+
if sentiment_data and sentiment_data.get('feargreed_value') is not None:
|
| 123 |
+
fear_greed = sentiment_data.get('feargreed_value')
|
| 124 |
+
if fear_greed > 60:
|
| 125 |
+
score += 1
|
| 126 |
+
elif fear_greed < 40:
|
| 127 |
+
score -= 1
|
| 128 |
|
| 129 |
+
if score >= 1:
|
| 130 |
+
return "bull_market"
|
| 131 |
+
elif score <= -1:
|
| 132 |
+
return "bear_market"
|
| 133 |
else:
|
| 134 |
+
return "sideways_market"
|
| 135 |
|
| 136 |
def _get_btc_sentiment(self, bitcoin_price):
|
| 137 |
if bitcoin_price is None:
|
|
|
|
| 144 |
return 'NEUTRAL'
|
| 145 |
|
| 146 |
async def _get_prices_with_fallback(self):
|
| 147 |
+
"""جلب أسعار البيتكوين والإيثيريوم"""
|
| 148 |
try:
|
| 149 |
prices = await self._get_prices_from_kucoin_safe()
|
| 150 |
if prices.get('bitcoin') and prices.get('ethereum'):
|
| 151 |
return prices
|
| 152 |
+
return await self._get_prices_from_coingecko()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
except Exception as e:
|
| 154 |
print(f"❌ فشل جلب الأسعار: {e}")
|
| 155 |
return {'bitcoin': None, 'ethereum': None}
|
|
|
|
| 161 |
try:
|
| 162 |
prices = {'bitcoin': None, 'ethereum': None}
|
| 163 |
|
| 164 |
+
btc_ticker = await self.exchange.fetch_ticker('BTC/USDT')
|
| 165 |
+
btc_price = float(btc_ticker.get('last', 0)) if btc_ticker.get('last') else None
|
| 166 |
+
if btc_price and btc_price > 0:
|
| 167 |
+
prices['bitcoin'] = btc_price
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
|
| 169 |
+
eth_ticker = await self.exchange.fetch_ticker('ETH/USDT')
|
| 170 |
+
eth_price = float(eth_ticker.get('last', 0)) if eth_ticker.get('last') else None
|
| 171 |
+
if eth_price and eth_price > 0:
|
| 172 |
+
prices['ethereum'] = eth_price
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
return prices
|
| 175 |
|
| 176 |
except Exception as e:
|
| 177 |
+
print(f"❌ خطأ في جلب الأسعار من KuCoin: {e}")
|
| 178 |
return {'bitcoin': None, 'ethereum': None}
|
| 179 |
|
| 180 |
async def _get_prices_from_coingecko(self):
|
| 181 |
+
"""الاحتياطي: جلب الأسعار من CoinGecko"""
|
| 182 |
try:
|
| 183 |
await asyncio.sleep(0.5)
|
| 184 |
url = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum&vs_currencies=usd"
|
|
|
|
| 191 |
eth_price = data.get('ethereum', {}).get('usd')
|
| 192 |
|
| 193 |
if btc_price and eth_price:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
return {'bitcoin': btc_price, 'ethereum': eth_price}
|
| 195 |
else:
|
|
|
|
| 196 |
return {'bitcoin': None, 'ethereum': None}
|
| 197 |
|
| 198 |
except Exception as e:
|
|
|
|
| 200 |
return {'bitcoin': None, 'ethereum': None}
|
| 201 |
|
| 202 |
def _get_minimal_market_context(self):
|
| 203 |
+
"""سياق سوق بدائي عند الفشل"""
|
| 204 |
return {
|
| 205 |
'timestamp': datetime.now().isoformat(),
|
| 206 |
'data_available': False,
|
|
|
|
|
|
|
| 207 |
'market_trend': 'UNKNOWN',
|
| 208 |
'btc_sentiment': 'UNKNOWN',
|
| 209 |
+
'data_quality': 'LOW'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
}
|
| 211 |
|
| 212 |
+
async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
|
| 213 |
+
"""
|
| 214 |
+
الطبقة 1: فحص سريع لجميع عملات KuCoin
|
| 215 |
+
يركز فقط على جلب البيانات الأساسية بدون تحليل متقدم
|
| 216 |
+
"""
|
| 217 |
+
candidates = []
|
| 218 |
+
|
| 219 |
+
# الحصول على جميع الرموز النشطة
|
| 220 |
+
usdt_symbols = [
|
| 221 |
+
symbol for symbol in self.market_cache.keys()
|
| 222 |
+
if symbol.endswith('/USDT') and self.market_cache[symbol].get('active', False)
|
| 223 |
+
]
|
| 224 |
+
|
| 225 |
+
print(f"📊 الطبقة 1: فحص سريع لـ {len(usdt_symbols)} عملة في KuCoin...")
|
| 226 |
+
|
| 227 |
+
batch_size = 50
|
| 228 |
+
for i in range(0, len(usdt_symbols), batch_size):
|
| 229 |
+
batch = usdt_symbols[i:i + batch_size]
|
| 230 |
+
print(f" 🔍 معالجة مجموعة {i//batch_size + 1}/{(len(usdt_symbols)//batch_size)+1}...")
|
| 231 |
|
| 232 |
+
batch_candidates = await self._process_batch_rapid_screening(batch)
|
| 233 |
+
candidates.extend(batch_candidates)
|
|
|
|
|
|
|
| 234 |
|
| 235 |
+
await asyncio.sleep(1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
|
| 237 |
+
# ترتيب المرشحين حسب قوة الأساسيات
|
| 238 |
+
candidates.sort(key=lambda x: x.get('layer1_score', 0), reverse=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
|
| 240 |
+
# نأخذ أفضل 80-180 مرشح للطبقة 2
|
| 241 |
+
target_count = min(max(80, len(candidates) // 5), 180)
|
| 242 |
+
final_candidates = candidates[:target_count]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
|
| 244 |
+
print(f"✅ تم اختيار {len(final_candidates)} عملة من الطبقة 1")
|
| 245 |
+
|
| 246 |
+
# عرض أفضل 5 مرشحين
|
| 247 |
+
print(" 🏆 أفضل 5 مرشحين من الطبقة 1:")
|
| 248 |
+
for i, candidate in enumerate(final_candidates[:5]):
|
| 249 |
+
score = candidate.get('layer1_score', 0)
|
| 250 |
+
volume = candidate.get('dollar_volume', 0)
|
| 251 |
+
change = candidate.get('price_change_24h', 0)
|
| 252 |
+
print(f" {i+1}. {candidate['symbol']}: {score:.3f} | ${volume:,.0f} | {change:+.1f}%")
|
| 253 |
|
| 254 |
+
return final_candidates
|
| 255 |
|
| 256 |
+
async def _process_batch_rapid_screening(self, symbols_batch: List[str]) -> List[Dict[str, Any]]:
|
| 257 |
+
"""معالجة دفعة من الرموز في الطبقة 1"""
|
| 258 |
+
candidates = []
|
| 259 |
+
|
| 260 |
+
for symbol in symbols_batch:
|
| 261 |
+
try:
|
| 262 |
+
# جلب بيانات التداول الأساسية فقط
|
| 263 |
+
ticker = await self.exchange.fetch_ticker(symbol)
|
| 264 |
+
if not ticker:
|
| 265 |
+
continue
|
| 266 |
+
|
| 267 |
+
current_price = ticker.get('last', 0)
|
| 268 |
+
volume_24h = ticker.get('baseVolume', 0)
|
| 269 |
+
dollar_volume = volume_24h * current_price
|
| 270 |
+
price_change_24h = ticker.get('percentage', 0)
|
| 271 |
+
high_24h = ticker.get('high', 0)
|
| 272 |
+
low_24h = ticker.get('low', 0)
|
| 273 |
+
|
| 274 |
+
# المعايير الأساسية المطلوبة
|
| 275 |
+
meets_criteria = all([
|
| 276 |
+
dollar_volume >= 1000000, # حجم تداول لا يقل عن 1M دولار
|
| 277 |
+
current_price > 0.00000001, # أي سعر مقبول
|
| 278 |
+
current_price <= 100000, # حد أقصى معقول
|
| 279 |
+
high_24h > 0, # بيانات سعر صالحة
|
| 280 |
+
low_24h > 0
|
| 281 |
+
])
|
| 282 |
+
|
| 283 |
+
if not meets_criteria:
|
| 284 |
+
continue
|
| 285 |
+
|
| 286 |
+
# حساب مؤشرات أساسية بسيطة فقط
|
| 287 |
+
volume_strength = self._calculate_volume_strength(dollar_volume)
|
| 288 |
+
price_momentum = self._calculate_price_momentum(price_change_24h)
|
| 289 |
+
price_position = self._calculate_price_position(current_price, high_24h, low_24h)
|
| 290 |
+
volatility = self._calculate_volatility(high_24h, low_24h, current_price)
|
| 291 |
+
|
| 292 |
+
# الدرجة النهائية للطبقة 1 (أساسيات فقط)
|
| 293 |
+
layer1_score = (
|
| 294 |
+
volume_strength * 0.35 +
|
| 295 |
+
price_momentum * 0.30 +
|
| 296 |
+
price_position * 0.20 +
|
| 297 |
+
volatility * 0.15
|
| 298 |
+
)
|
| 299 |
+
|
| 300 |
+
if layer1_score >= 0.3:
|
| 301 |
+
candidate_data = {
|
| 302 |
+
'symbol': symbol,
|
| 303 |
+
'current_price': current_price,
|
| 304 |
+
'volume_24h': volume_24h,
|
| 305 |
+
'dollar_volume': dollar_volume,
|
| 306 |
+
'price_change_24h': price_change_24h,
|
| 307 |
+
'high_24h': high_24h,
|
| 308 |
+
'low_24h': low_24h,
|
| 309 |
+
'layer1_score': layer1_score,
|
| 310 |
+
'volume_strength': volume_strength,
|
| 311 |
+
'price_momentum': price_momentum,
|
| 312 |
+
'reasons': self._generate_layer1_reasons(volume_strength, price_momentum, dollar_volume, price_change_24h)
|
| 313 |
+
}
|
| 314 |
+
|
| 315 |
+
candidates.append(candidate_data)
|
| 316 |
+
|
| 317 |
+
except Exception as e:
|
| 318 |
+
if "rate limit" not in str(e).lower():
|
| 319 |
+
continue
|
| 320 |
+
|
| 321 |
+
return candidates
|
| 322 |
+
|
| 323 |
+
def _calculate_volume_strength(self, dollar_volume: float) -> float:
|
| 324 |
+
"""حساب قوة الحجم (بيانات فقط)"""
|
| 325 |
+
if dollar_volume >= 10000000:
|
| 326 |
+
return 1.0
|
| 327 |
+
elif dollar_volume >= 5000000:
|
| 328 |
+
return 0.8
|
| 329 |
+
elif dollar_volume >= 2000000:
|
| 330 |
+
return 0.6
|
| 331 |
+
elif dollar_volume >= 1000000:
|
| 332 |
+
return 0.4
|
| 333 |
else:
|
| 334 |
+
return 0.2
|
| 335 |
+
|
| 336 |
+
def _calculate_price_momentum(self, price_change_24h: float) -> float:
|
| 337 |
+
"""حساب زخم السعر (بيانات فقط)"""
|
| 338 |
+
if price_change_24h >= 15:
|
| 339 |
+
return 0.9
|
| 340 |
+
elif price_change_24h >= 8:
|
| 341 |
+
return 0.7
|
| 342 |
+
elif price_change_24h >= 3:
|
| 343 |
+
return 0.5
|
| 344 |
+
elif price_change_24h <= -15:
|
| 345 |
+
return 0.8
|
| 346 |
+
elif price_change_24h <= -8:
|
| 347 |
+
return 0.6
|
| 348 |
+
elif price_change_24h <= -3:
|
| 349 |
+
return 0.4
|
| 350 |
+
else:
|
| 351 |
+
return 0.3
|
| 352 |
|
| 353 |
+
def _calculate_price_position(self, current_price: float, high_24h: float, low_24h: float) -> float:
|
| 354 |
+
"""حساب موقع السعر (بيانات فقط)"""
|
| 355 |
+
if high_24h == low_24h:
|
| 356 |
+
return 0.5
|
| 357 |
+
|
| 358 |
+
position = (current_price - low_24h) / (high_24h - low_24h)
|
| 359 |
+
|
| 360 |
+
if position < 0.3:
|
| 361 |
+
return 0.8
|
| 362 |
+
elif position > 0.7:
|
| 363 |
+
return 0.6
|
| 364 |
else:
|
| 365 |
+
return 0.5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
|
| 367 |
+
def _calculate_volatility(self, high_24h: float, low_24h: float, current_price: float) -> float:
|
| 368 |
+
"""حساب التقلب (بيانات فقط)"""
|
| 369 |
+
if current_price == 0:
|
| 370 |
+
return 0.5
|
| 371 |
+
|
| 372 |
+
volatility = (high_24h - low_24h) / current_price
|
| 373 |
+
|
| 374 |
+
if volatility > 0.5:
|
| 375 |
+
return 0.2
|
| 376 |
+
elif volatility > 0.2:
|
| 377 |
+
return 0.4
|
| 378 |
+
elif volatility > 0.1:
|
| 379 |
+
return 0.8
|
| 380 |
+
elif volatility > 0.05:
|
| 381 |
+
return 0.6
|
| 382 |
+
else:
|
| 383 |
+
return 0.3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
|
| 385 |
+
def _generate_layer1_reasons(self, volume_strength: float, price_momentum: float,
|
| 386 |
+
dollar_volume: float, price_change: float) -> List[str]:
|
| 387 |
+
"""توليد أسباب الترشيح بناءً على البيانات فقط"""
|
| 388 |
+
reasons = []
|
| 389 |
+
|
| 390 |
+
if volume_strength >= 0.6:
|
| 391 |
+
reasons.append('high_liquidity')
|
| 392 |
+
elif volume_strength >= 0.4:
|
| 393 |
+
reasons.append('good_liquidity')
|
| 394 |
+
|
| 395 |
+
if price_change >= 8:
|
| 396 |
+
reasons.append('strong_positive_momentum')
|
| 397 |
+
elif price_change >= 3:
|
| 398 |
+
reasons.append('positive_momentum')
|
| 399 |
+
elif price_change <= -8:
|
| 400 |
+
reasons.append('oversold_opportunity')
|
| 401 |
+
elif price_change <= -3:
|
| 402 |
+
reasons.append('dip_opportunity')
|
| 403 |
+
|
| 404 |
+
if dollar_volume >= 5000000:
|
| 405 |
+
reasons.append('very_high_volume')
|
| 406 |
+
elif dollar_volume >= 2000000:
|
| 407 |
+
reasons.append('high_volume')
|
| 408 |
+
|
| 409 |
+
return reasons
|
| 410 |
+
|
| 411 |
+
async def get_ohlcv_data_for_symbols(self, symbols: List[str]) -> List[Dict[str, Any]]:
|
| 412 |
"""
|
| 413 |
+
جلب بيانات OHLCV كاملة للرموز المحددة
|
| 414 |
+
يستخدم في الطبقة 2 للتحليل المتقدم
|
|
|
|
|
|
|
|
|
|
| 415 |
"""
|
| 416 |
+
results = []
|
| 417 |
+
|
| 418 |
+
print(f"📊 جلب بيانات OHLCV لـ {len(symbols)} عملة...")
|
| 419 |
+
|
| 420 |
+
for symbol in symbols:
|
| 421 |
+
try:
|
| 422 |
+
ohlcv_data = {}
|
| 423 |
+
timeframes = [
|
| 424 |
+
('5m', 100), ('15m', 100), ('1h', 100),
|
| 425 |
+
('4h', 100), ('1d', 100), ('1w', 50)
|
| 426 |
+
]
|
| 427 |
+
|
| 428 |
+
has_sufficient_data = True
|
| 429 |
+
for timeframe, limit in timeframes:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 430 |
try:
|
| 431 |
+
ohlcv = await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
|
| 432 |
+
if ohlcv and len(ohlcv) >= 20:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
ohlcv_data[timeframe] = ohlcv
|
| 434 |
+
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 435 |
has_sufficient_data = False
|
| 436 |
break
|
| 437 |
+
except Exception:
|
| 438 |
+
has_sufficient_data = False
|
| 439 |
+
break
|
| 440 |
+
|
| 441 |
+
if has_sufficient_data:
|
| 442 |
+
# الحصول على بيانات التداول الحالية
|
| 443 |
+
ticker = await self.exchange.fetch_ticker(symbol)
|
| 444 |
+
current_price = ticker.get('last', 0) if ticker else 0
|
| 445 |
|
| 446 |
+
result_data = {
|
| 447 |
+
'symbol': symbol,
|
| 448 |
+
'ohlcv': ohlcv_data,
|
| 449 |
+
'current_price': current_price,
|
| 450 |
+
'timestamp': datetime.now().isoformat()
|
| 451 |
+
}
|
| 452 |
+
results.append(result_data)
|
| 453 |
+
|
| 454 |
+
await asyncio.sleep(0.1)
|
| 455 |
+
|
| 456 |
+
except Exception as symbol_error:
|
| 457 |
+
print(f"❌ خطأ في جلب بيانات {symbol}: {symbol_error}")
|
| 458 |
+
continue
|
| 459 |
+
|
| 460 |
+
print(f"✅ تم تجميع بيانات OHLCV لـ {len(results)} عملة")
|
| 461 |
+
return results
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 462 |
|
| 463 |
async def get_latest_price_async(self, symbol):
|
| 464 |
+
"""جلب السعر الحالي لعملة محددة"""
|
| 465 |
try:
|
| 466 |
if not self.exchange:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 467 |
return None
|
| 468 |
|
| 469 |
ticker = await self.exchange.fetch_ticker(symbol)
|
|
|
|
| 472 |
if current_price:
|
| 473 |
return float(current_price)
|
| 474 |
else:
|
|
|
|
| 475 |
return None
|
| 476 |
|
| 477 |
except Exception as e:
|
| 478 |
+
print(f"❌ خطأ في جلب السعر لـ {symbol}: {e}")
|
| 479 |
return None
|
| 480 |
|
| 481 |
async def get_available_symbols(self):
|
| 482 |
+
"""الحصول على جميع الرموز المتاحة"""
|
| 483 |
try:
|
| 484 |
if not self.exchange:
|
| 485 |
return []
|
|
|
|
| 495 |
return usdt_symbols
|
| 496 |
|
| 497 |
except Exception as e:
|
| 498 |
+
print(f"❌ خطأ في جلب الرموز المتاحة: {e}")
|
| 499 |
return []
|
| 500 |
|
| 501 |
async def validate_symbol(self, symbol):
|
| 502 |
+
"""التحقق من صحة الرمز"""
|
| 503 |
try:
|
| 504 |
if not self.exchange:
|
| 505 |
return False
|
|
|
|
| 510 |
return symbol in self.market_cache and self.market_cache[symbol].get('active', False)
|
| 511 |
|
| 512 |
except Exception as e:
|
| 513 |
+
print(f"❌ خطأ في التحقق من الرمز {symbol}: {e}")
|
| 514 |
return False
|
| 515 |
|
| 516 |
+
print("✅ DataManager loaded - Focused on data retrieval only")
|