Update data_manager.py
Browse files- data_manager.py +184 -593
data_manager.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
| 1 |
-
# data_manager.py (Updated to
|
| 2 |
import os
|
| 3 |
import asyncio
|
| 4 |
import httpx
|
|
@@ -14,30 +14,29 @@ import pandas as pd
|
|
| 14 |
try:
|
| 15 |
import pandas_ta as ta
|
| 16 |
except ImportError:
|
| 17 |
-
print("⚠️ مكتبة pandas_ta غير
|
| 18 |
ta = None
|
| 19 |
|
|
|
|
| 20 |
from ml_engine.indicators import AdvancedTechnicalAnalyzer
|
| 21 |
from ml_engine.monte_carlo import MonteCarloAnalyzer
|
| 22 |
-
# (V8-MODIFICATION) استيراد المحرك الصحيح (V8)
|
| 23 |
from ml_engine.patterns import ChartPatternAnalyzer
|
|
|
|
|
|
|
| 24 |
|
| 25 |
logging.getLogger("httpx").setLevel(logging.WARNING)
|
| 26 |
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
| 27 |
|
| 28 |
class DataManager:
|
| 29 |
-
# (V8-MODIFICATION) قبول r2_service
|
| 30 |
def __init__(self, contracts_db, whale_monitor, r2_service=None):
|
| 31 |
self.contracts_db = contracts_db or {}
|
| 32 |
self.whale_monitor = whale_monitor
|
| 33 |
-
self.r2_service = r2_service
|
| 34 |
|
| 35 |
try:
|
| 36 |
self.exchange = ccxt.kucoin({
|
| 37 |
-
'sandbox': False,
|
| 38 |
-
'
|
| 39 |
-
'timeout': 30000,
|
| 40 |
-
'verbose': False,
|
| 41 |
})
|
| 42 |
print("✅ تم تهيئة اتصال KuCoin بنجاح")
|
| 43 |
except Exception as e:
|
|
@@ -48,100 +47,88 @@ class DataManager:
|
|
| 48 |
self.market_cache = {}
|
| 49 |
self.last_market_load = None
|
| 50 |
|
|
|
|
| 51 |
self.technical_analyzer = AdvancedTechnicalAnalyzer()
|
| 52 |
self.monte_carlo_analyzer = MonteCarloAnalyzer()
|
| 53 |
-
self.pattern_analyzer = None
|
|
|
|
|
|
|
| 54 |
|
| 55 |
async def initialize(self):
|
| 56 |
self.http_client = httpx.AsyncClient(timeout=30.0)
|
| 57 |
await self._load_markets()
|
| 58 |
|
| 59 |
-
# (V8-MODIFICATION) تهيئة محرك الأنماط V8 (ML-Based)
|
| 60 |
print(" > [DataManager] تهيئة محرك الأنماط V8 (ML-Based)...")
|
| 61 |
try:
|
| 62 |
self.pattern_analyzer = ChartPatternAnalyzer(r2_service=self.r2_service)
|
| 63 |
-
await self.pattern_analyzer.initialize()
|
| 64 |
except Exception as e:
|
| 65 |
print(f"❌ [DataManager] فشل تهيئة محرك الأنماط V8: {e}")
|
| 66 |
-
# (العودة للوضع الآمن إذا فشل التحميل)
|
| 67 |
self.pattern_analyzer = ChartPatternAnalyzer(r2_service=None)
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
-
print("✅ DataManager initialized -
|
| 71 |
|
| 72 |
async def _load_markets(self):
|
| 73 |
try:
|
| 74 |
-
if not self.exchange:
|
| 75 |
-
return
|
| 76 |
-
|
| 77 |
print("🔄 جلب أحدث بيانات الأسواق من KuCoin...")
|
| 78 |
self.exchange.load_markets()
|
| 79 |
self.market_cache = self.exchange.markets
|
| 80 |
self.last_market_load = datetime.now()
|
| 81 |
print(f"✅ تم تحميل {len(self.market_cache)} سوق من KuCoin")
|
| 82 |
-
|
| 83 |
-
except Exception as e:
|
| 84 |
-
print(f"❌ ف��ل تحميل بيانات الأسواق: {e}")
|
| 85 |
|
| 86 |
async def close(self):
|
| 87 |
-
if self.http_client and not self.http_client.is_closed:
|
| 88 |
-
await self.http_client.aclose()
|
| 89 |
-
print(" ✅ DataManager: http_client closed.")
|
| 90 |
-
|
| 91 |
if self.exchange:
|
| 92 |
-
try:
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
except Exception as e:
|
| 96 |
-
print(f" ⚠️ DataManager: Error closing ccxt.kucoin: {e}")
|
| 97 |
-
|
| 98 |
async def get_market_context_async(self):
|
| 99 |
try:
|
| 100 |
sentiment_data = await self.get_sentiment_safe_async()
|
| 101 |
price_data = await self._get_prices_with_fallback()
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
ethereum_price = price_data.get('ethereum')
|
| 105 |
-
|
| 106 |
-
market_context = {
|
| 107 |
'timestamp': datetime.now().isoformat(),
|
| 108 |
-
'bitcoin_price_usd': bitcoin_price,
|
| 109 |
-
'ethereum_price_usd': ethereum_price,
|
| 110 |
'fear_and_greed_index': sentiment_data.get('feargreed_value') if sentiment_data else None,
|
| 111 |
'sentiment_class': sentiment_data.get('feargreed_class') if sentiment_data else 'NEUTRAL',
|
| 112 |
'market_trend': self._determine_market_trend(bitcoin_price, sentiment_data),
|
| 113 |
'btc_sentiment': self._get_btc_sentiment(bitcoin_price),
|
| 114 |
'data_quality': 'HIGH' if bitcoin_price and ethereum_price else 'LOW'
|
| 115 |
}
|
| 116 |
-
|
| 117 |
-
return market_context
|
| 118 |
-
|
| 119 |
-
except Exception as e:
|
| 120 |
-
return self._get_minimal_market_context()
|
| 121 |
|
|
|
|
| 122 |
async def get_sentiment_safe_async(self):
|
| 123 |
try:
|
| 124 |
async with httpx.AsyncClient(timeout=10) as client:
|
| 125 |
response = await client.get("https://api.alternative.me/fng/")
|
| 126 |
-
response.raise_for_status()
|
| 127 |
-
data
|
| 128 |
-
|
| 129 |
-
if 'data' not in data or not data['data']:
|
| 130 |
-
raise ValueError("بيانات المشاعر غير متوفرة")
|
| 131 |
-
|
| 132 |
latest_data = data['data'][0]
|
| 133 |
-
return {
|
| 134 |
-
|
| 135 |
-
"feargreed_class": latest_data['value_classification'],
|
| 136 |
-
"source": "alternative.me",
|
| 137 |
-
"timestamp": datetime.now().isoformat()
|
| 138 |
-
}
|
| 139 |
-
except Exception as e:
|
| 140 |
-
return None
|
| 141 |
-
|
| 142 |
def _determine_market_trend(self, bitcoin_price, sentiment_data):
|
| 143 |
-
if bitcoin_price is None:
|
| 144 |
-
return "UNKNOWN"
|
| 145 |
if bitcoin_price > 60000: score = 1
|
| 146 |
elif bitcoin_price < 55000: score = -1
|
| 147 |
else: score = 0
|
|
@@ -152,36 +139,27 @@ class DataManager:
|
|
| 152 |
if score >= 1: return "bull_market"
|
| 153 |
elif score <= -1: return "bear_market"
|
| 154 |
else: return "sideways_market"
|
| 155 |
-
|
| 156 |
def _get_btc_sentiment(self, bitcoin_price):
|
| 157 |
if bitcoin_price is None: return 'UNKNOWN'
|
| 158 |
elif bitcoin_price > 60000: return 'BULLISH'
|
| 159 |
elif bitcoin_price < 55000: return 'BEARISH'
|
| 160 |
else: return 'NEUTRAL'
|
| 161 |
-
|
| 162 |
async def _get_prices_with_fallback(self):
|
| 163 |
try:
|
| 164 |
prices = await self._get_prices_from_kucoin_safe()
|
| 165 |
-
if prices.get('bitcoin') and prices.get('ethereum'):
|
| 166 |
-
return prices
|
| 167 |
return await self._get_prices_from_coingecko()
|
| 168 |
-
except Exception as e:
|
| 169 |
-
return {'bitcoin': None, 'ethereum': None}
|
| 170 |
-
|
| 171 |
async def _get_prices_from_kucoin_safe(self):
|
| 172 |
if not self.exchange: return {'bitcoin': None, 'ethereum': None}
|
| 173 |
try:
|
| 174 |
prices = {'bitcoin': None, 'ethereum': None}
|
| 175 |
-
btc_ticker = self.exchange.fetch_ticker('BTC/USDT')
|
| 176 |
-
btc_price = float(btc_ticker.get('last', 0)) if btc_ticker.get('last') else None
|
| 177 |
if btc_price and btc_price > 0: prices['bitcoin'] = btc_price
|
| 178 |
-
eth_ticker = self.exchange.fetch_ticker('ETH/USDT')
|
| 179 |
-
eth_price = float(eth_ticker.get('last', 0)) if eth_ticker.get('last') else None
|
| 180 |
if eth_price and eth_price > 0: prices['ethereum'] = eth_price
|
| 181 |
return prices
|
| 182 |
-
except Exception as e:
|
| 183 |
-
return {'bitcoin': None, 'ethereum': None}
|
| 184 |
-
|
| 185 |
async def _get_prices_from_coingecko(self):
|
| 186 |
try:
|
| 187 |
await asyncio.sleep(0.5)
|
|
@@ -189,32 +167,17 @@ class DataManager:
|
|
| 189 |
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept': 'application/json'}
|
| 190 |
async with httpx.AsyncClient(headers=headers) as client:
|
| 191 |
response = await client.get(url, timeout=10)
|
| 192 |
-
if response.status_code == 429:
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
eth_price = data.get('ethereum', {}).get('usd')
|
| 199 |
-
if btc_price and eth_price:
|
| 200 |
-
return {'bitcoin': btc_price, 'ethereum': eth_price}
|
| 201 |
-
else:
|
| 202 |
-
return {'bitcoin': None, 'ethereum': None}
|
| 203 |
-
except Exception as e:
|
| 204 |
-
return {'bitcoin': None, 'ethereum': None}
|
| 205 |
-
|
| 206 |
def _get_minimal_market_context(self):
|
| 207 |
-
return {
|
| 208 |
-
'timestamp': datetime.now().isoformat(),
|
| 209 |
-
'data_available': False,
|
| 210 |
-
'market_trend': 'UNKNOWN',
|
| 211 |
-
'btc_sentiment': 'UNKNOWN',
|
| 212 |
-
'data_quality': 'LOW'
|
| 213 |
-
}
|
| 214 |
|
| 215 |
-
|
| 216 |
def _create_dataframe(self, candles: List) -> pd.DataFrame:
|
| 217 |
-
"""(
|
| 218 |
try:
|
| 219 |
if not candles: return pd.DataFrame()
|
| 220 |
df = pd.DataFrame(candles, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
|
@@ -224,583 +187,211 @@ class DataManager:
|
|
| 224 |
df.sort_index(inplace=True)
|
| 225 |
return df
|
| 226 |
except Exception as e:
|
| 227 |
-
print(f"❌ خطأ في إنشاء DataFrame
|
| 228 |
return pd.DataFrame()
|
| 229 |
|
| 230 |
-
|
| 231 |
-
"""(V7.4) (دالة مساعدة) لحساب درجة مونت كارلو للفلتر"""
|
| 232 |
-
mc_distribution = analysis.get('monte_carlo_distribution')
|
| 233 |
-
monte_carlo_score = 0
|
| 234 |
-
|
| 235 |
-
if mc_distribution and mc_distribution.get('error') is None:
|
| 236 |
-
prob_gain = mc_distribution.get('probability_of_gain', 0)
|
| 237 |
-
var_95_value = mc_distribution.get('risk_metrics', {}).get('VaR_95_value', 0)
|
| 238 |
-
current_price = analysis.get('current_price', 1)
|
| 239 |
-
|
| 240 |
-
if current_price > 0:
|
| 241 |
-
normalized_var = var_95_value / current_price
|
| 242 |
-
risk_penalty = 1.0
|
| 243 |
-
if normalized_var > 0.05: risk_penalty = 0.5
|
| 244 |
-
elif normalized_var > 0.03: risk_penalty = 0.8
|
| 245 |
-
|
| 246 |
-
normalized_prob_score = max(0.0, (prob_gain - 0.5) * 2)
|
| 247 |
-
monte_carlo_score = normalized_prob_score * risk_penalty
|
| 248 |
-
return monte_carlo_score
|
| 249 |
-
|
| 250 |
-
def _calculate_1h_momentum_burst_score(self, analysis: Dict) -> float:
|
| 251 |
-
"""
|
| 252 |
-
(محدث V7.7) - الفلتر الأول: "فلتر شمس منتصف الظهر" (الانفجار)
|
| 253 |
-
"""
|
| 254 |
-
try:
|
| 255 |
-
ohlcv_candles = analysis.get('ohlcv_1h', {}).get('1h', [])
|
| 256 |
-
if not ohlcv_candles or len(ohlcv_candles) < 30:
|
| 257 |
-
return 0.0
|
| 258 |
-
|
| 259 |
-
closes_1h = [c[4] for c in ohlcv_candles]
|
| 260 |
-
if len(closes_1h) > 20:
|
| 261 |
-
std_dev = np.std(closes_1h[-20:])
|
| 262 |
-
if std_dev < 1e-5:
|
| 263 |
-
return 0.0 # (واقي العملات المستقرة)
|
| 264 |
-
|
| 265 |
-
if ta is None: return 0.0
|
| 266 |
-
df = self._create_dataframe(ohlcv_candles)
|
| 267 |
-
if df.empty: return 0.0
|
| 268 |
-
|
| 269 |
-
# 1. حساب مؤشرات الانفجار
|
| 270 |
-
volume = df['volume']
|
| 271 |
-
vol_ma = ta.sma(volume, length=20)
|
| 272 |
-
if vol_ma is None or vol_ma.empty: return 0.0
|
| 273 |
-
current_volume = volume.iloc[-1]
|
| 274 |
-
avg_volume = vol_ma.iloc[-1]
|
| 275 |
-
adx_data = ta.adx(df['high'], df['low'], df['close'], length=14)
|
| 276 |
-
current_adx = adx_data['ADX_14'].iloc[-1] if adx_data is not None and not adx_data.empty else 0
|
| 277 |
-
|
| 278 |
-
# 2. جلب المؤشرات الأساسية (المحسوبة مسبقاً)
|
| 279 |
-
indicators = analysis.get('advanced_indicators', {}).get('1h', {})
|
| 280 |
-
rsi = indicators.get('rsi', 50)
|
| 281 |
-
|
| 282 |
-
# 3. جلب درجة مونت كارلو (المحسوبة مسبقاً)
|
| 283 |
-
monte_carlo_score = self._get_mc_score_for_filter(analysis)
|
| 284 |
-
|
| 285 |
-
# 4. جلب درجة الأنماط (المحسوبة مسبقاً)
|
| 286 |
-
pattern_confidence = analysis.get('pattern_analysis', {}).get('pattern_confidence', 0)
|
| 287 |
-
|
| 288 |
-
# --- (منطق فلترة الانفجار) ---
|
| 289 |
-
VOL_MULTIPLIER = 1.75 # (صارم)
|
| 290 |
-
ADX_THRESHOLD = 25.0 # (صارم)
|
| 291 |
-
RSI_MIN = 60 # (صارم)
|
| 292 |
-
RSI_MAX = 85
|
| 293 |
-
|
| 294 |
-
vol_score = 0.0
|
| 295 |
-
if avg_volume > 0:
|
| 296 |
-
vol_score = min(1.0, max(0.0, (current_volume / avg_volume) / VOL_MULTIPLIER))
|
| 297 |
-
|
| 298 |
-
adx_score = min(1.0, max(0.0, (current_adx - ADX_THRESHOLD) / 15.0))
|
| 299 |
-
|
| 300 |
-
rsi_score = 0.0
|
| 301 |
-
if RSI_MIN <= rsi <= RSI_MAX:
|
| 302 |
-
rsi_score = 1.0
|
| 303 |
-
elif rsi > RSI_MAX:
|
| 304 |
-
rsi_score = 0.5
|
| 305 |
-
|
| 306 |
-
# (الأوزان لفلتر الانفجار)
|
| 307 |
-
WEIGHT_VOL = 0.30
|
| 308 |
-
WEIGHT_ADX = 0.30
|
| 309 |
-
WEIGHT_RSI = 0.15
|
| 310 |
-
WEIGHT_MC = 0.15
|
| 311 |
-
WEIGHT_PATTERN = 0.10
|
| 312 |
-
|
| 313 |
-
final_score = (
|
| 314 |
-
(vol_score * WEIGHT_VOL) +
|
| 315 |
-
(adx_score * WEIGHT_ADX) +
|
| 316 |
-
(rsi_score * WEIGHT_RSI) +
|
| 317 |
-
(monte_carlo_score * WEIGHT_MC) +
|
| 318 |
-
(pattern_confidence * WEIGHT_PATTERN)
|
| 319 |
-
)
|
| 320 |
-
|
| 321 |
-
return min(max(final_score, 0.0), 1.0)
|
| 322 |
-
|
| 323 |
-
except Exception as e:
|
| 324 |
-
return 0.0
|
| 325 |
-
|
| 326 |
-
# 🔴 --- START OF CHANGE (V7.9) --- 🔴
|
| 327 |
-
def _calculate_1h_reversal_spark_score(self, analysis: Dict) -> float:
|
| 328 |
-
"""
|
| 329 |
-
(محدث V7.9) - الفلتر الثاني: "فلتر شرارة الانعكاس" (أوزان معدلة)
|
| 330 |
-
"""
|
| 331 |
-
try:
|
| 332 |
-
# (1. واقي العملات المستقرة)
|
| 333 |
-
ohlcv_candles = analysis.get('ohlcv_1h', {}).get('1h', [])
|
| 334 |
-
if not ohlcv_candles or len(ohlcv_candles) < 30:
|
| 335 |
-
return 0.0
|
| 336 |
-
|
| 337 |
-
closes_1h = [c[4] for c in ohlcv_candles]
|
| 338 |
-
if len(closes_1h) > 20:
|
| 339 |
-
std_dev = np.std(closes_1h[-20:])
|
| 340 |
-
if std_dev < 1e-5:
|
| 341 |
-
return 0.0
|
| 342 |
-
|
| 343 |
-
if ta is None: return 0.0
|
| 344 |
-
df = self._create_dataframe(ohlcv_candles)
|
| 345 |
-
if df.empty: return 0.0
|
| 346 |
-
|
| 347 |
-
# 2. حساب مؤشرات "الشرارة"
|
| 348 |
-
volume = df['volume']
|
| 349 |
-
vol_ma = ta.sma(volume, length=20)
|
| 350 |
-
if vol_ma is None or vol_ma.empty: return 0.0
|
| 351 |
-
current_volume = volume.iloc[-1]
|
| 352 |
-
avg_volume = vol_ma.iloc[-1]
|
| 353 |
-
adx_data = ta.adx(df['high'], df['low'], df['close'], length=14)
|
| 354 |
-
current_adx = adx_data['ADX_14'].iloc[-1] if adx_data is not None and not adx_data.empty else 0
|
| 355 |
-
|
| 356 |
-
# 3. جلب المؤشرات الأساسية (المحسوبة مسبقاً)
|
| 357 |
-
indicators = analysis.get('advanced_indicators', {}).get('1h', {})
|
| 358 |
-
rsi = indicators.get('rsi', 50)
|
| 359 |
-
|
| 360 |
-
# 4. جلب درجة مونت كارلو (المحسوبة مسبقاً)
|
| 361 |
-
monte_carlo_score = self._get_mc_score_for_filter(analysis)
|
| 362 |
-
|
| 363 |
-
# 5. جلب درجة الأنماط (المحسوبة مسبقاً)
|
| 364 |
-
pattern_analysis = analysis.get('pattern_analysis', {})
|
| 365 |
-
|
| 366 |
-
# --- (منطق فلترة "الشرارة") ---
|
| 367 |
-
|
| 368 |
-
pattern_score = 0.0
|
| 369 |
-
if pattern_analysis.get('predicted_direction') == 'up':
|
| 370 |
-
pattern_score = pattern_analysis.get('pattern_confidence', 0)
|
| 371 |
-
|
| 372 |
-
# (RSI في منطقة الاشتعال 45-60)
|
| 373 |
-
RSI_IGNITION_MIN = 45
|
| 374 |
-
RSI_IGNITION_MAX = 60
|
| 375 |
-
rsi_score = 0.0
|
| 376 |
-
if RSI_IGNITION_MIN <= rsi <= RSI_IGNITION_MAX:
|
| 377 |
-
rsi_score = 1.0 # (مثالي)
|
| 378 |
-
elif rsi > RSI_IGNITION_MAX:
|
| 379 |
-
rsi_score = 0.5 # (لا بأس)
|
| 380 |
-
|
| 381 |
-
# (ADX منخفض، يدل على "تجميع" أو "نطاق")
|
| 382 |
-
ADX_COIL_THRESHOLD = 20.0
|
| 383 |
-
adx_score = 0.0
|
| 384 |
-
if current_adx < ADX_COIL_THRESHOLD:
|
| 385 |
-
adx_score = 1.0 # (مثالي)
|
| 386 |
-
|
| 387 |
-
# (الحجم بدأ في الارتفاع (أعلى من المتوسط))
|
| 388 |
-
vol_score = 0.0
|
| 389 |
-
if avg_volume > 0 and (current_volume / avg_volume) > 1.1:
|
| 390 |
-
vol_score = 0.5 # (مجرد اهتمام بسيط)
|
| 391 |
-
|
| 392 |
-
# (الأوزان الجديدة V7.9 - بناءً على طلبك)
|
| 393 |
-
WEIGHT_PATTERN = 0.10 # (تم الخفض)
|
| 394 |
-
WEIGHT_RSI_SPARK = 0.35 # (تم الرفع)
|
| 395 |
-
WEIGHT_ADX_COIL = 0.35 # (تم الرفع)
|
| 396 |
-
WEIGHT_MC = 0.10
|
| 397 |
-
WEIGHT_VOL = 0.10
|
| 398 |
-
# (المجموع = 1.0)
|
| 399 |
-
|
| 400 |
-
final_score = (
|
| 401 |
-
(pattern_score * WEIGHT_PATTERN) +
|
| 402 |
-
(rsi_score * WEIGHT_RSI_SPARK) +
|
| 403 |
-
(adx_score * WEIGHT_ADX_COIL) +
|
| 404 |
-
(monte_carlo_score * WEIGHT_MC) +
|
| 405 |
-
(vol_score * WEIGHT_VOL)
|
| 406 |
-
)
|
| 407 |
-
|
| 408 |
-
return min(max(final_score, 0.0), 1.0)
|
| 409 |
-
|
| 410 |
-
except Exception as e:
|
| 411 |
-
return 0.0
|
| 412 |
-
# 🔴 --- END OF CHANGE (V7.9) --- 🔴
|
| 413 |
-
|
| 414 |
|
|
|
|
| 415 |
async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
|
| 416 |
"""
|
| 417 |
-
الطبقة 1: فحص سريع - (محدث
|
|
|
|
|
|
|
| 418 |
"""
|
| 419 |
-
print("📊 الطبقة 1 (
|
| 420 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 421 |
# الخطوة 1: جلب أفضل 100 عملة حسب الحجم
|
| 422 |
volume_data = await self._get_volume_data_optimal()
|
|
|
|
| 423 |
if not volume_data:
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
if not volume_data:
|
| 427 |
-
print("❌ فشل جلب بيانات الأحجام للطبقة 1")
|
| 428 |
return []
|
| 429 |
|
| 430 |
volume_data.sort(key=lambda x: x['dollar_volume'], reverse=True)
|
| 431 |
top_100_by_volume = volume_data[:100]
|
|
|
|
| 432 |
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
batch_size = 20
|
| 438 |
-
for i in range(0, len(top_100_by_volume), batch_size):
|
| 439 |
-
batch_symbols_data = top_100_by_volume[i:i + batch_size]
|
| 440 |
-
batch_symbols = [s['symbol'] for s in batch_symbols_data]
|
| 441 |
-
|
| 442 |
-
print(f" 🔄 معالجة دفعة {int(i/batch_size) + 1}/{(len(top_100_by_volume) + batch_size - 1) // batch_size} ({len(batch_symbols)} عملة)...")
|
| 443 |
-
|
| 444 |
-
# الخطوة 2: جلب بيانات 1H بالتوازي
|
| 445 |
-
tasks = [self._fetch_1h_ohlcv_for_screening(symbol) for symbol in batch_symbols]
|
| 446 |
-
results_candles = await asyncio.gather(*tasks, return_exceptions=True)
|
| 447 |
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
for j, (candles) in enumerate(results_candles):
|
| 452 |
-
symbol_data = batch_symbols_data[j]
|
| 453 |
-
symbol = symbol_data['symbol']
|
| 454 |
-
|
| 455 |
-
if isinstance(candles, Exception) or not candles or len(candles) < 50:
|
| 456 |
-
continue
|
| 457 |
-
|
| 458 |
-
ohlcv_1h_only = {'1h': candles}
|
| 459 |
-
symbol_data['ohlcv_1h'] = ohlcv_1h_only
|
| 460 |
-
symbol_data['current_price'] = candles[-1][4]
|
| 461 |
-
analysis_tasks.append(self._run_mini_detector(symbol_data))
|
| 462 |
-
valid_symbol_data.append(symbol_data)
|
| 463 |
-
|
| 464 |
-
if not analysis_tasks:
|
| 465 |
-
continue
|
| 466 |
-
|
| 467 |
-
analysis_results = await asyncio.gather(*analysis_tasks, return_exceptions=True)
|
| 468 |
-
|
| 469 |
-
# (تطبيق منطق الفلتر المزدوج)
|
| 470 |
-
for j, (analysis_output) in enumerate(analysis_results):
|
| 471 |
-
symbol_data = valid_symbol_data[j]
|
| 472 |
-
symbol = symbol_data['symbol']
|
| 473 |
-
|
| 474 |
-
if isinstance(analysis_output, Exception):
|
| 475 |
-
print(f" - {symbol}: فشل الكاشف المصغر ({analysis_output})")
|
| 476 |
-
continue
|
| 477 |
-
|
| 478 |
-
analysis_output['ohlcv_1h'] = symbol_data['ohlcv_1h']
|
| 479 |
-
analysis_output['symbol'] = symbol
|
| 480 |
-
|
| 481 |
-
# (1. محاولة فلتر "الانفجار" أولاً)
|
| 482 |
-
burst_score = self._calculate_1h_momentum_burst_score(analysis_output)
|
| 483 |
-
|
| 484 |
-
final_score = burst_score
|
| 485 |
-
candidacy_reason = "1H_MOMENTUM_BURST" # (السبب الافتراضي)
|
| 486 |
-
|
| 487 |
-
# (2. إذا فشل فلتر الانفجار، جرب فلتر "الشرارة")
|
| 488 |
-
if burst_score < 0.50:
|
| 489 |
-
# (استخدام V7.9 بالأوزان الجديدة)
|
| 490 |
-
spark_score = self._calculate_1h_reversal_spark_score(analysis_output)
|
| 491 |
-
|
| 492 |
-
final_score = spark_score # (استخدام درجة الشرارة)
|
| 493 |
-
candidacy_reason = "1H_REVERSAL_SPARK" # (تغيير السبب)
|
| 494 |
-
|
| 495 |
-
# (3. التحقق من العتبة النهائية 0.50)
|
| 496 |
-
if final_score >= 0.50:
|
| 497 |
-
print(f" ✅ {symbol}: نجح ({candidacy_reason} | الدرجة: {final_score:.2f})")
|
| 498 |
-
symbol_data['layer1_score'] = final_score
|
| 499 |
-
symbol_data['reasons_for_candidacy'] = [candidacy_reason]
|
| 500 |
-
|
| 501 |
-
if 'ohlcv_1h' in symbol_data: del symbol_data['ohlcv_1h']
|
| 502 |
-
|
| 503 |
-
final_candidates.append(symbol_data)
|
| 504 |
-
|
| 505 |
-
print(f"🎯 اكتملت الغربلة (V7.9). تم تأهيل {len(final_candidates)} عملة من أصل 100 للطبقة 2.")
|
| 506 |
-
|
| 507 |
-
if final_candidates:
|
| 508 |
-
print("🏆 المرشحون الناجحون:")
|
| 509 |
-
for k, candidate in enumerate(final_candidates[:15]):
|
| 510 |
-
score = candidate.get('layer1_score', 0)
|
| 511 |
-
reason = candidate.get('reasons_for_candidacy', ['N/A'])[0]
|
| 512 |
-
print(f" {k+1:2d}. {candidate['symbol']}: (الدرجة: {score:.2f}) | السبب: {reason}")
|
| 513 |
|
| 514 |
-
|
| 515 |
-
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
raise ValueError("DataFrame فارغ لتحليل 1H")
|
| 525 |
-
|
| 526 |
-
analysis_dict = {'current_price': current_price}
|
| 527 |
-
|
| 528 |
-
task_indicators = self.technical_analyzer.calculate_all_indicators(df, '1h')
|
| 529 |
-
task_mc = self.monte_carlo_analyzer.generate_1h_price_distribution(ohlcv_1h)
|
| 530 |
-
task_pattern = self.pattern_analyzer.detect_chart_patterns(ohlcv_1h)
|
| 531 |
|
| 532 |
-
|
| 533 |
|
| 534 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 535 |
|
| 536 |
-
if not
|
| 537 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 538 |
|
| 539 |
-
# (
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
analysis_dict['pattern_analysis'] = pattern_result
|
| 543 |
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
|
| 548 |
-
|
| 549 |
-
print(f" 💡 [ML Pattern] {symbol}: نمط مُكتشف! "
|
| 550 |
-
f"'{pattern_name}' (الثقة: {confidence:.2f}) على إطار {tf}")
|
| 551 |
-
elif pattern_name == "Neutral / No Pattern":
|
| 552 |
-
pass
|
| 553 |
-
else:
|
| 554 |
-
print(f" - [ML Pattern] {symbol}: نمط ضعيف. "
|
| 555 |
-
f"'{pattern_name}' (الثقة: {confidence:.2f}) على إطار {tf}")
|
| 556 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 557 |
else:
|
| 558 |
-
print(
|
| 559 |
-
analysis_dict['pattern_analysis'] = {}
|
| 560 |
-
|
| 561 |
-
return analysis_dict
|
| 562 |
|
|
|
|
|
|
|
| 563 |
|
| 564 |
-
async def _fetch_1h_ohlcv_for_screening(self, symbol: str) -> List:
|
| 565 |
-
"""(V7.1) جلب 100 شمعة لإطار الساعة (1H) للغربلة السريعة"""
|
| 566 |
try:
|
| 567 |
-
ohlcv_data = self.exchange.fetch_ohlcv(symbol, '1h', limit=
|
| 568 |
-
|
| 569 |
-
if not ohlcv_data or len(ohlcv_data) < 50:
|
| 570 |
-
return None
|
| 571 |
return ohlcv_data
|
| 572 |
-
except Exception:
|
| 573 |
-
return None
|
| 574 |
|
|
|
|
| 575 |
async def _get_volume_data_optimal(self) -> List[Dict[str, Any]]:
|
| 576 |
try:
|
| 577 |
if not self.exchange: return []
|
| 578 |
-
tickers = self.exchange.fetch_tickers()
|
| 579 |
-
volume_data = []
|
| 580 |
for symbol, ticker in tickers.items():
|
| 581 |
if not symbol.endswith('/USDT') or not ticker.get('active', True): continue
|
| 582 |
-
current_price = ticker.get('last', 0)
|
| 583 |
-
quote_volume = ticker.get('quoteVolume', 0)
|
| 584 |
if current_price is None or current_price <= 0: continue
|
| 585 |
-
if quote_volume is not None and quote_volume > 0:
|
| 586 |
-
dollar_volume = quote_volume
|
| 587 |
else:
|
| 588 |
base_volume = ticker.get('baseVolume', 0)
|
| 589 |
if base_volume is None: continue
|
| 590 |
dollar_volume = base_volume * current_price
|
| 591 |
if dollar_volume is None or dollar_volume < 50000: continue
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
volume_data.append({
|
| 597 |
-
'symbol': symbol, 'dollar_volume': dollar_volume,
|
| 598 |
-
'current_price': current_price, 'volume_24h': ticker.get('baseVolume', 0) or 0,
|
| 599 |
-
'price_change_24h': price_change_24h
|
| 600 |
-
})
|
| 601 |
-
print(f"✅ تم معالجة {len(volume_data)} عملة في الطريقة المثلى (لجلب الحجم)")
|
| 602 |
return volume_data
|
| 603 |
-
except Exception as e:
|
| 604 |
-
print(f"❌ خطأ في جلب بيانات الحجم المثلى: {e}")
|
| 605 |
-
return []
|
| 606 |
-
|
| 607 |
async def _get_volume_data_direct_api(self) -> List[Dict[str, Any]]:
|
| 608 |
try:
|
| 609 |
url = "https://api.kucoin.com/api/v1/market/allTickers"
|
| 610 |
async with httpx.AsyncClient(timeout=15) as client:
|
| 611 |
-
response = await client.get(url)
|
| 612 |
-
response.raise_for_status()
|
| 613 |
-
data = response.json()
|
| 614 |
if data.get('code') != '200000': raise ValueError(f"استجابة API غير متوقعة: {data.get('code')}")
|
| 615 |
-
tickers = data['data']['ticker']
|
| 616 |
-
volume_data = []
|
| 617 |
for ticker in tickers:
|
| 618 |
symbol = ticker['symbol']
|
| 619 |
if not symbol.endswith('USDT'): continue
|
| 620 |
formatted_symbol = symbol.replace('-', '/')
|
| 621 |
try:
|
| 622 |
-
vol_value = ticker.get('volValue')
|
| 623 |
-
last_price = ticker.get('last')
|
| 624 |
-
change_rate = ticker.get('changeRate')
|
| 625 |
-
vol = ticker.get('vol')
|
| 626 |
if vol_value is None or last_price is None or change_rate is None or vol is None: continue
|
| 627 |
-
dollar_volume = float(vol_value) if vol_value else 0
|
| 628 |
-
current_price
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
if dollar_volume >= 50000 and current_price > 0:
|
| 632 |
-
volume_data.append({
|
| 633 |
-
'symbol': formatted_symbol, 'dollar_volume': dollar_volume,
|
| 634 |
-
'current_price': current_price, 'volume_24h': volume_24h,
|
| 635 |
-
'price_change_24h': price_change
|
| 636 |
-
})
|
| 637 |
-
except (ValueError, TypeError, KeyError) as e: continue
|
| 638 |
-
print(f"✅ تم معالجة {len(volume_data)} عملة في الطريقة المباشرة (لجلب الحجم)")
|
| 639 |
return volume_data
|
| 640 |
-
except Exception as e:
|
| 641 |
-
print(f"❌ خطأ في جلب بيانات الحجم المباشر: {e}")
|
| 642 |
-
return []
|
| 643 |
-
|
| 644 |
async def stream_ohlcv_data(self, symbols: List[Dict[str, Any]], queue: asyncio.Queue):
|
| 645 |
-
""
|
| 646 |
-
(
|
| 647 |
-
جلب بيانات OHLCV كاملة (6 أطر زمنية) للعملات الناجحة فقط
|
| 648 |
-
"""
|
| 649 |
-
print(f"📊 بدء تدفق بيانات OHLCV (الكاملة) لـ {len(symbols)} عملة (الناجحين من الغربلة)...")
|
| 650 |
-
|
| 651 |
-
batch_size = 15
|
| 652 |
-
batches = [symbols[i:i + batch_size] for i in range(0, len(symbols), batch_size)]
|
| 653 |
-
|
| 654 |
-
total_successful = 0
|
| 655 |
-
|
| 656 |
for batch_num, batch in enumerate(batches):
|
| 657 |
print(f" 🔄 [المنتج] جلب الدفعة {batch_num + 1}/{len(batches)} ({len(batch)} عملة)...")
|
| 658 |
-
|
| 659 |
-
batch_tasks = []
|
| 660 |
-
|
| 661 |
-
# (V7.2 FIX)
|
| 662 |
-
for symbol_data in batch:
|
| 663 |
-
symbol_str = symbol_data['symbol']
|
| 664 |
-
task = asyncio.create_task(self._fetch_complete_ohlcv_parallel(symbol_str))
|
| 665 |
-
batch_tasks.append(task)
|
| 666 |
-
|
| 667 |
batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True)
|
| 668 |
-
|
| 669 |
successful_data_for_batch = []
|
| 670 |
-
successful_count = 0
|
| 671 |
for i, result in enumerate(batch_results):
|
| 672 |
-
|
| 673 |
-
original_symbol_data = batch[i]
|
| 674 |
-
symbol_str = original_symbol_data['symbol']
|
| 675 |
-
|
| 676 |
-
if isinstance(result, Exception):
|
| 677 |
-
print(f" ❌ [المنتج] فشل جلب {symbol_str}: {result}")
|
| 678 |
elif result is not None:
|
| 679 |
-
result.update(
|
| 680 |
-
|
| 681 |
-
successful_count += 1
|
| 682 |
-
timeframes_count = result.get('successful_timeframes', 0)
|
| 683 |
-
print(f" ✅ [المنتج] {symbol_str}: {timeframes_count}/6 أطر زمنية")
|
| 684 |
-
else:
|
| 685 |
-
print(f" ⚠️ [المنتج] {symbol_str}: بيانات غير كافية، تم التجاهل")
|
| 686 |
-
|
| 687 |
-
print(f" 📦 [المنتج] اكتملت الدفعة {batch_num + 1}: {successful_count}/{len(batch)} ناجحة")
|
| 688 |
-
|
| 689 |
if successful_data_for_batch:
|
| 690 |
-
try:
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
except Exception as q_err:
|
| 695 |
-
print(f" ❌ [المنتج] فشل إرسال الدفعة للطابور: {q_err}")
|
| 696 |
-
|
| 697 |
-
if batch_num < len(batches) - 1:
|
| 698 |
-
await asyncio.sleep(1)
|
| 699 |
-
|
| 700 |
-
print(f"✅ [المنتج] اكتمل تدفق بيانات OHLCV (الكاملة). تم إرسال {total_successful} عملة للمعالجة.")
|
| 701 |
-
|
| 702 |
-
try:
|
| 703 |
-
await queue.put(None)
|
| 704 |
-
print(" 📬 [المنتج] تم إرسال إشارة الإنهاء (None) إلى الطابور.")
|
| 705 |
-
except Exception as q_err:
|
| 706 |
-
print(f" ❌ [المنتج] فشل إرسال إشارة الإنهاء (None) للطابور.")
|
| 707 |
-
|
| 708 |
|
| 709 |
async def _fetch_complete_ohlcv_parallel(self, symbol: str) -> Dict[str, Any]:
|
| 710 |
-
"""(V7.2) جلب بيانات OHLCV كاملة - يتوقع 'symbol' كنص"""
|
| 711 |
try:
|
| 712 |
-
ohlcv_data = {}
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
('5m', 200), ('15m', 200), ('1h', 200),
|
| 716 |
-
('4h', 200), ('1d', 200), ('1w', 200),
|
| 717 |
-
]
|
| 718 |
-
|
| 719 |
-
timeframe_tasks = []
|
| 720 |
-
for timeframe, limit in timeframes:
|
| 721 |
-
task = asyncio.create_task(self._fetch_single_timeframe_improved(symbol, timeframe, limit))
|
| 722 |
-
timeframe_tasks.append(task)
|
| 723 |
-
|
| 724 |
-
timeframe_results = await asyncio.gather(*timeframe_tasks, return_exceptions=True)
|
| 725 |
-
|
| 726 |
-
successful_timeframes = 0
|
| 727 |
-
min_required_timeframes = 2
|
| 728 |
-
|
| 729 |
for i, (timeframe, limit) in enumerate(timeframes):
|
| 730 |
result = timeframe_results[i]
|
| 731 |
if isinstance(result, Exception): continue
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
ohlcv_data[timeframe] = result
|
| 735 |
-
successful_timeframes += 1
|
| 736 |
-
|
| 737 |
-
# (V8-MODIFICATION) زيادة الحد الأدنى للأطر الزمنية
|
| 738 |
-
if successful_timeframes >= 3 and ohlcv_data: # (كان 2)
|
| 739 |
try:
|
| 740 |
current_price = await self.get_latest_price_async(symbol)
|
| 741 |
if current_price is None:
|
| 742 |
-
for
|
| 743 |
-
if
|
| 744 |
-
last_candle = timeframe_data[-1]
|
| 745 |
-
if len(last_candle) >= 5:
|
| 746 |
-
current_price = last_candle[4]; break
|
| 747 |
if current_price is None: return None
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
'symbol': symbol, 'ohlcv': ohlcv_data, 'raw_ohlcv': ohlcv_data,
|
| 751 |
-
'current_price': current_price, 'timestamp': datetime.now().isoformat(),
|
| 752 |
-
'candles_count': {tf: len(data) for tf, data in ohlcv_data.items()},
|
| 753 |
-
'successful_timeframes': successful_timeframes
|
| 754 |
-
}
|
| 755 |
-
return result_data
|
| 756 |
-
except Exception as price_error: return None
|
| 757 |
else: return None
|
| 758 |
-
except Exception
|
| 759 |
-
|
| 760 |
async def _fetch_single_timeframe_improved(self, symbol: str, timeframe: str, limit: int):
|
| 761 |
-
|
| 762 |
-
max_retries = 3
|
| 763 |
-
retry_delay = 2
|
| 764 |
for attempt in range(max_retries):
|
| 765 |
try:
|
| 766 |
ohlcv_data = self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
|
| 767 |
-
if ohlcv_data and len(ohlcv_data) > 0:
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
if attempt < max_retries - 1:
|
| 773 |
-
await asyncio.sleep(retry_delay * (attempt + 1))
|
| 774 |
-
else:
|
| 775 |
-
return []
|
| 776 |
-
|
| 777 |
async def get_latest_price_async(self, symbol):
|
| 778 |
-
"""(V7.2) جلب السعر الحالي - يتوقع 'symbol' كنص"""
|
| 779 |
try:
|
| 780 |
if not self.exchange: return None
|
| 781 |
-
if not symbol or '/' not in symbol: return None
|
| 782 |
ticker = self.exchange.fetch_ticker(symbol)
|
| 783 |
-
if
|
| 784 |
-
|
| 785 |
-
if current_price is None: return None
|
| 786 |
-
return float(current_price)
|
| 787 |
-
except Exception as e: return None
|
| 788 |
-
|
| 789 |
async def get_whale_data_for_symbol(self, symbol):
|
| 790 |
-
try:
|
| 791 |
-
|
| 792 |
-
whale_data = await self.whale_monitor.get_symbol_whale_activity(symbol)
|
| 793 |
-
return whale_data
|
| 794 |
-
else: return None
|
| 795 |
-
except Exception as e: return None
|
| 796 |
-
|
| 797 |
async def get_whale_trading_signal(self, symbol, whale_data, market_context):
|
| 798 |
-
try:
|
| 799 |
-
|
| 800 |
-
return await self.whale_monitor.generate_whale_trading_signal(symbol, whale_data, market_context)
|
| 801 |
-
else:
|
| 802 |
-
return {'action': 'HOLD', 'confidence': 0.3, 'reason': 'Whale monitor not available', 'source': 'whale_analysis'}
|
| 803 |
-
except Exception as e:
|
| 804 |
-
return {'action': 'HOLD', 'confidence': 0.3, 'reason': f'Error: {str(e)}', 'source': 'whale_analysis'}
|
| 805 |
|
| 806 |
-
print("✅ DataManager loaded -
|
|
|
|
| 1 |
+
# data_manager.py (Updated to V9.3 - High Confidence ML Ranker)
|
| 2 |
import os
|
| 3 |
import asyncio
|
| 4 |
import httpx
|
|
|
|
| 14 |
try:
|
| 15 |
import pandas_ta as ta
|
| 16 |
except ImportError:
|
| 17 |
+
print("⚠️ مكتبة pandas_ta غير موجودة. النظام سيفشل.")
|
| 18 |
ta = None
|
| 19 |
|
| 20 |
+
# (V9.1) استيراد العقل الحسابي (الذي يحتوي على V9.1 Features)
|
| 21 |
from ml_engine.indicators import AdvancedTechnicalAnalyzer
|
| 22 |
from ml_engine.monte_carlo import MonteCarloAnalyzer
|
|
|
|
| 23 |
from ml_engine.patterns import ChartPatternAnalyzer
|
| 24 |
+
# (V9.1) استيراد "العقل الذكي" الجديد
|
| 25 |
+
from ml_engine.ranker import Layer1Ranker
|
| 26 |
|
| 27 |
logging.getLogger("httpx").setLevel(logging.WARNING)
|
| 28 |
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
| 29 |
|
| 30 |
class DataManager:
|
|
|
|
| 31 |
def __init__(self, contracts_db, whale_monitor, r2_service=None):
|
| 32 |
self.contracts_db = contracts_db or {}
|
| 33 |
self.whale_monitor = whale_monitor
|
| 34 |
+
self.r2_service = r2_service
|
| 35 |
|
| 36 |
try:
|
| 37 |
self.exchange = ccxt.kucoin({
|
| 38 |
+
'sandbox': False, 'enableRateLimit': True,
|
| 39 |
+
'timeout': 30000, 'verbose': False,
|
|
|
|
|
|
|
| 40 |
})
|
| 41 |
print("✅ تم تهيئة اتصال KuCoin بنجاح")
|
| 42 |
except Exception as e:
|
|
|
|
| 47 |
self.market_cache = {}
|
| 48 |
self.last_market_load = None
|
| 49 |
|
| 50 |
+
# (V9.1) استيراد العقل الحسابي (الذي يحتوي على V9.1 Features)
|
| 51 |
self.technical_analyzer = AdvancedTechnicalAnalyzer()
|
| 52 |
self.monte_carlo_analyzer = MonteCarloAnalyzer()
|
| 53 |
+
self.pattern_analyzer = None
|
| 54 |
+
# (V9.1) تهيئة "العقل الذكي" (النموذج)
|
| 55 |
+
self.layer1_ranker = None
|
| 56 |
|
| 57 |
async def initialize(self):
|
| 58 |
self.http_client = httpx.AsyncClient(timeout=30.0)
|
| 59 |
await self._load_markets()
|
| 60 |
|
|
|
|
| 61 |
print(" > [DataManager] تهيئة محرك الأنماط V8 (ML-Based)...")
|
| 62 |
try:
|
| 63 |
self.pattern_analyzer = ChartPatternAnalyzer(r2_service=self.r2_service)
|
| 64 |
+
await self.pattern_analyzer.initialize()
|
| 65 |
except Exception as e:
|
| 66 |
print(f"❌ [DataManager] فشل تهيئة محرك الأنماط V8: {e}")
|
|
|
|
| 67 |
self.pattern_analyzer = ChartPatternAnalyzer(r2_service=None)
|
| 68 |
+
|
| 69 |
+
# 🔴 --- START OF CHANGE (V9.1) --- 🔴
|
| 70 |
+
# (تهيئة "العقل الذكي" - نموذج الرانكر V9.1)
|
| 71 |
+
print(" > [DataManager] تهيئة الكاشف المصغر (Layer1 Ranker V9.1)...")
|
| 72 |
+
try:
|
| 73 |
+
# (نتوقع أن يكون النموذج موجوداً في هذا المسار)
|
| 74 |
+
model_file_path = "ml_models/layer1_ranker.lgbm"
|
| 75 |
+
self.layer1_ranker = Layer1Ranker(model_path=model_file_path)
|
| 76 |
+
await self.layer1_ranker.initialize()
|
| 77 |
+
if self.layer1_ranker.model is None:
|
| 78 |
+
print(" ⚠️ [DataManager V9.1] الرانكر في وضع 'وهمي' (Placeholder).")
|
| 79 |
+
else:
|
| 80 |
+
print(f" ✅ [DataManager V9.1] الرانكر {self.layer1_ranker.model_name} جاهز للعمل.")
|
| 81 |
+
except Exception as e:
|
| 82 |
+
print(f"❌ [DataManager V9.1] فشل تهيئة الرانكر V9.1: {e}")
|
| 83 |
+
self.layer1_ranker = None
|
| 84 |
+
# 🔴 --- END OF CHANGE (V9.1) --- 🔴
|
| 85 |
|
| 86 |
+
print("✅ DataManager initialized - V9.3 (High Confidence Ranker)")
|
| 87 |
|
| 88 |
async def _load_markets(self):
|
| 89 |
try:
|
| 90 |
+
if not self.exchange: return
|
|
|
|
|
|
|
| 91 |
print("🔄 جلب أحدث بيانات الأسواق من KuCoin...")
|
| 92 |
self.exchange.load_markets()
|
| 93 |
self.market_cache = self.exchange.markets
|
| 94 |
self.last_market_load = datetime.now()
|
| 95 |
print(f"✅ تم تحميل {len(self.market_cache)} سوق من KuCoin")
|
| 96 |
+
except Exception as e: print(f"❌ فشل تحميل بيانات الأسواق: {e}")
|
|
|
|
|
|
|
| 97 |
|
| 98 |
async def close(self):
|
| 99 |
+
if self.http_client and not self.http_client.is_closed: await self.http_client.aclose()
|
|
|
|
|
|
|
|
|
|
| 100 |
if self.exchange:
|
| 101 |
+
try: await self.exchange.close()
|
| 102 |
+
except Exception: pass
|
| 103 |
+
|
|
|
|
|
|
|
|
|
|
| 104 |
async def get_market_context_async(self):
|
| 105 |
try:
|
| 106 |
sentiment_data = await self.get_sentiment_safe_async()
|
| 107 |
price_data = await self._get_prices_with_fallback()
|
| 108 |
+
bitcoin_price = price_data.get('bitcoin'); ethereum_price = price_data.get('ethereum')
|
| 109 |
+
return {
|
|
|
|
|
|
|
|
|
|
| 110 |
'timestamp': datetime.now().isoformat(),
|
| 111 |
+
'bitcoin_price_usd': bitcoin_price, 'ethereum_price_usd': ethereum_price,
|
|
|
|
| 112 |
'fear_and_greed_index': sentiment_data.get('feargreed_value') if sentiment_data else None,
|
| 113 |
'sentiment_class': sentiment_data.get('feargreed_class') if sentiment_data else 'NEUTRAL',
|
| 114 |
'market_trend': self._determine_market_trend(bitcoin_price, sentiment_data),
|
| 115 |
'btc_sentiment': self._get_btc_sentiment(bitcoin_price),
|
| 116 |
'data_quality': 'HIGH' if bitcoin_price and ethereum_price else 'LOW'
|
| 117 |
}
|
| 118 |
+
except Exception as e: return self._get_minimal_market_context()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
+
# ... (دوال get_sentiment_safe_async, _determine_market_trend... إلخ - لا تغيير) ...
|
| 121 |
async def get_sentiment_safe_async(self):
|
| 122 |
try:
|
| 123 |
async with httpx.AsyncClient(timeout=10) as client:
|
| 124 |
response = await client.get("https://api.alternative.me/fng/")
|
| 125 |
+
response.raise_for_status(); data = response.json()
|
| 126 |
+
if 'data' not in data or not data['data']: raise ValueError("بيانات المشاعر غير متوفرة")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
latest_data = data['data'][0]
|
| 128 |
+
return { "feargreed_value": int(latest_data['value']), "feargreed_class": latest_data['value_classification'], "source": "alternative.me", "timestamp": datetime.now().isoformat() }
|
| 129 |
+
except Exception as e: return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
def _determine_market_trend(self, bitcoin_price, sentiment_data):
|
| 131 |
+
if bitcoin_price is None: return "UNKNOWN"
|
|
|
|
| 132 |
if bitcoin_price > 60000: score = 1
|
| 133 |
elif bitcoin_price < 55000: score = -1
|
| 134 |
else: score = 0
|
|
|
|
| 139 |
if score >= 1: return "bull_market"
|
| 140 |
elif score <= -1: return "bear_market"
|
| 141 |
else: return "sideways_market"
|
|
|
|
| 142 |
def _get_btc_sentiment(self, bitcoin_price):
|
| 143 |
if bitcoin_price is None: return 'UNKNOWN'
|
| 144 |
elif bitcoin_price > 60000: return 'BULLISH'
|
| 145 |
elif bitcoin_price < 55000: return 'BEARISH'
|
| 146 |
else: return 'NEUTRAL'
|
|
|
|
| 147 |
async def _get_prices_with_fallback(self):
|
| 148 |
try:
|
| 149 |
prices = await self._get_prices_from_kucoin_safe()
|
| 150 |
+
if prices.get('bitcoin') and prices.get('ethereum'): return prices
|
|
|
|
| 151 |
return await self._get_prices_from_coingecko()
|
| 152 |
+
except Exception as e: return {'bitcoin': None, 'ethereum': None}
|
|
|
|
|
|
|
| 153 |
async def _get_prices_from_kucoin_safe(self):
|
| 154 |
if not self.exchange: return {'bitcoin': None, 'ethereum': None}
|
| 155 |
try:
|
| 156 |
prices = {'bitcoin': None, 'ethereum': None}
|
| 157 |
+
btc_ticker = self.exchange.fetch_ticker('BTC/USDT'); btc_price = float(btc_ticker.get('last', 0)) if btc_ticker.get('last') else None
|
|
|
|
| 158 |
if btc_price and btc_price > 0: prices['bitcoin'] = btc_price
|
| 159 |
+
eth_ticker = self.exchange.fetch_ticker('ETH/USDT'); eth_price = float(eth_ticker.get('last', 0)) if eth_ticker.get('last') else None
|
|
|
|
| 160 |
if eth_price and eth_price > 0: prices['ethereum'] = eth_price
|
| 161 |
return prices
|
| 162 |
+
except Exception as e: return {'bitcoin': None, 'ethereum': None}
|
|
|
|
|
|
|
| 163 |
async def _get_prices_from_coingecko(self):
|
| 164 |
try:
|
| 165 |
await asyncio.sleep(0.5)
|
|
|
|
| 167 |
headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', 'Accept': 'application/json'}
|
| 168 |
async with httpx.AsyncClient(headers=headers) as client:
|
| 169 |
response = await client.get(url, timeout=10)
|
| 170 |
+
if response.status_code == 429: await asyncio.sleep(2); response = await client.get(url, timeout=10)
|
| 171 |
+
response.raise_for_status(); data = response.json()
|
| 172 |
+
btc_price = data.get('bitcoin', {}).get('usd'); eth_price = data.get('ethereum', {}).get('usd')
|
| 173 |
+
if btc_price and eth_price: return {'bitcoin': btc_price, 'ethereum': eth_price}
|
| 174 |
+
else: return {'bitcoin': None, 'ethereum': None}
|
| 175 |
+
except Exception as e: return {'bitcoin': None, 'ethereum': None}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
def _get_minimal_market_context(self):
|
| 177 |
+
return { 'timestamp': datetime.now().isoformat(), 'data_available': False, 'market_trend': 'UNKNOWN', 'btc_sentiment': 'UNKNOWN', 'data_quality': 'LOW' }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
|
|
|
| 179 |
def _create_dataframe(self, candles: List) -> pd.DataFrame:
|
| 180 |
+
"""(V9.1) إنشاء DataFrame (تحتاج 200 شمعة للميزات)"""
|
| 181 |
try:
|
| 182 |
if not candles: return pd.DataFrame()
|
| 183 |
df = pd.DataFrame(candles, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
|
|
|
| 187 |
df.sort_index(inplace=True)
|
| 188 |
return df
|
| 189 |
except Exception as e:
|
| 190 |
+
print(f"❌ خطأ في إنشاء DataFrame: {e}")
|
| 191 |
return pd.DataFrame()
|
| 192 |
|
| 193 |
+
# 🔴 --- (تم حذف جميع الفلاتر الغبية V7.x نهائياً) --- 🔴
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
+
# 🔴 --- START OF CHANGE (V9.3 - High Confidence ML Ranker) --- 🔴
|
| 196 |
async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
|
| 197 |
"""
|
| 198 |
+
الطبقة 1: فحص سريع - (محدث V9.3)
|
| 199 |
+
- يستخدم "العقل الذكي" (V9.2 Model).
|
| 200 |
+
- يطبق عتبة ثقة صارمة (70%) لضمان الجودة.
|
| 201 |
"""
|
| 202 |
+
print("📊 الطبقة 1 (V9.3 - High Confidence Ranker): بدء الغربلة...")
|
| 203 |
|
| 204 |
+
if not self.layer1_ranker:
|
| 205 |
+
print("❌ [V9.3] الرانكر غير مهيأ. إيقاف الغربلة.")
|
| 206 |
+
return []
|
| 207 |
+
|
| 208 |
# الخطوة 1: جلب أفضل 100 عملة حسب الحجم
|
| 209 |
volume_data = await self._get_volume_data_optimal()
|
| 210 |
+
if not volume_data: volume_data = await self._get_volume_data_direct_api()
|
| 211 |
if not volume_data:
|
| 212 |
+
print("❌ [V9.3] فشل جلب بيانات الأحجام.")
|
|
|
|
|
|
|
|
|
|
| 213 |
return []
|
| 214 |
|
| 215 |
volume_data.sort(key=lambda x: x['dollar_volume'], reverse=True)
|
| 216 |
top_100_by_volume = volume_data[:100]
|
| 217 |
+
print(f"✅ [V9.3] تم تحديد أفضل {len(top_100_by_volume)} عملة. بدء حساب الميزات الذكية...")
|
| 218 |
|
| 219 |
+
final_candidates_with_scores = []
|
| 220 |
+
batch_symbols_data = top_100_by_volume
|
| 221 |
+
batch_symbols = [s['symbol'] for s in batch_symbols_data]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
+
# الخطوة 2: جلب بيانات 1H (200 شمعة)
|
| 224 |
+
tasks = [self._fetch_1h_ohlcv_for_screening(symbol, limit=200) for symbol in batch_symbols]
|
| 225 |
+
results_candles = await asyncio.gather(*tasks, return_exceptions=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
|
| 227 |
+
valid_symbol_data_for_ranking = []
|
| 228 |
+
for j, (candles) in enumerate(results_candles):
|
| 229 |
+
symbol_data = batch_symbols_data[j]
|
| 230 |
+
if isinstance(candles, Exception) or not candles or len(candles) < 200: continue
|
| 231 |
+
symbol_data['ohlcv_1h_raw'] = candles
|
| 232 |
+
valid_symbol_data_for_ranking.append(symbol_data)
|
| 233 |
+
|
| 234 |
+
if not valid_symbol_data_for_ranking:
|
| 235 |
+
print("❌ [V9.3] لا توجد عملات صالحة (تحتاج 200 شمعة 1H).")
|
| 236 |
+
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 237 |
|
| 238 |
+
print(f" 🔄 [V9.3] حساب الميزات الذكية لـ {len(valid_symbol_data_for_ranking)} عملة...")
|
| 239 |
|
| 240 |
+
# الخطوة 3: حساب "الميزات الذكية V9.1"
|
| 241 |
+
all_features_list = []
|
| 242 |
+
symbols_in_order = []
|
| 243 |
+
for symbol_data in valid_symbol_data_for_ranking:
|
| 244 |
+
try:
|
| 245 |
+
df = self._create_dataframe(symbol_data['ohlcv_1h_raw'])
|
| 246 |
+
if df.empty: continue
|
| 247 |
+
smart_features = self.technical_analyzer.calculate_v9_smart_features(df)
|
| 248 |
+
if smart_features:
|
| 249 |
+
all_features_list.append(smart_features)
|
| 250 |
+
symbols_in_order.append(symbol_data)
|
| 251 |
+
except Exception: pass
|
| 252 |
|
| 253 |
+
if not all_features_list:
|
| 254 |
+
print("❌ [V9.3] فشل حساب الميزات الذكية.")
|
| 255 |
+
return []
|
| 256 |
+
|
| 257 |
+
# الخطوة 4: التنبؤ (التصنيف)
|
| 258 |
+
print(f" 🧠 [V9.3] إرسال {len(all_features_list)} عملة إلى نموذج الرانكر...")
|
| 259 |
+
features_dataframe = pd.DataFrame(all_features_list)
|
| 260 |
+
probabilities = self.layer1_ranker.predict_proba(features_dataframe)
|
| 261 |
|
| 262 |
+
# الخطوة 5: تجميع النتائج (مع عتبة الثقة العالية 70%)
|
| 263 |
+
for i, (symbol_data) in enumerate(symbols_in_order):
|
| 264 |
+
score = probabilities[i]
|
|
|
|
| 265 |
|
| 266 |
+
# 🔴 (V9.3: عتبة الثقة الصارمة - طلب المستخدم) 🔴
|
| 267 |
+
if score >= 0.70:
|
| 268 |
+
symbol = symbol_data['symbol']
|
| 269 |
+
print(f" ✅ {symbol}: نجح بامتياز (الاحتمالية: {score:.3f})")
|
| 270 |
+
symbol_data['layer1_score'] = float(score)
|
| 271 |
+
symbol_data['reasons_for_candidacy'] = ["V9_SMART_RANKER_HIGH_CONFIDENCE"]
|
| 272 |
+
if 'ohlcv_1h_raw' in symbol_data: del symbol_data['ohlcv_1h_raw']
|
| 273 |
+
final_candidates_with_scores.append(symbol_data)
|
| 274 |
|
| 275 |
+
print(f"🎯 اكتملت الغربلة (V9.3). تم تأهيل {len(final_candidates_with_scores)} عملة (ثقة >= 70%).")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 276 |
|
| 277 |
+
if final_candidates_with_scores:
|
| 278 |
+
final_candidates_with_scores.sort(key=lambda x: x['layer1_score'], reverse=True)
|
| 279 |
+
print("🏆 المرشحون الناجحون (Top Candidates):")
|
| 280 |
+
for k, candidate in enumerate(final_candidates_with_scores[:5]):
|
| 281 |
+
print(f" {k+1}. {candidate['symbol']}: (Score: {candidate.get('layer1_score'):.3f})")
|
| 282 |
else:
|
| 283 |
+
print("⚠️ [V9.3] لم تنجح أي عملة في تجاوز عتبة الثقة 70%. (هذا جيد، النظام حذر)")
|
|
|
|
|
|
|
|
|
|
| 284 |
|
| 285 |
+
return final_candidates_with_scores[:20]
|
| 286 |
+
# 🔴 --- END OF CHANGE (V9.3) --- 🔴
|
| 287 |
|
| 288 |
+
async def _fetch_1h_ohlcv_for_screening(self, symbol: str, limit: int = 200) -> List:
|
|
|
|
| 289 |
try:
|
| 290 |
+
ohlcv_data = self.exchange.fetch_ohlcv(symbol, '1h', limit=limit)
|
| 291 |
+
if not ohlcv_data or len(ohlcv_data) < limit: return None
|
|
|
|
|
|
|
| 292 |
return ohlcv_data
|
| 293 |
+
except Exception: return None
|
|
|
|
| 294 |
|
| 295 |
+
# ... (باقي الدوال _get_volume_data_optimal, stream_ohlcv_data... إلخ - لا تغيير) ...
|
| 296 |
async def _get_volume_data_optimal(self) -> List[Dict[str, Any]]:
|
| 297 |
try:
|
| 298 |
if not self.exchange: return []
|
| 299 |
+
tickers = self.exchange.fetch_tickers(); volume_data = []
|
|
|
|
| 300 |
for symbol, ticker in tickers.items():
|
| 301 |
if not symbol.endswith('/USDT') or not ticker.get('active', True): continue
|
| 302 |
+
current_price = ticker.get('last', 0); quote_volume = ticker.get('quoteVolume', 0)
|
|
|
|
| 303 |
if current_price is None or current_price <= 0: continue
|
| 304 |
+
if quote_volume is not None and quote_volume > 0: dollar_volume = quote_volume
|
|
|
|
| 305 |
else:
|
| 306 |
base_volume = ticker.get('baseVolume', 0)
|
| 307 |
if base_volume is None: continue
|
| 308 |
dollar_volume = base_volume * current_price
|
| 309 |
if dollar_volume is None or dollar_volume < 50000: continue
|
| 310 |
+
price_change_24h = ticker.get('percentage', 0) or 0; if price_change_24h is None: price_change_24h = 0
|
| 311 |
+
volume_data.append({ 'symbol': symbol, 'dollar_volume': dollar_volume, 'current_price': current_price, 'volume_24h': ticker.get('baseVolume', 0) or 0, 'price_change_24h': price_change_24h })
|
| 312 |
+
print(f"✅ تم معالجة {len(volume_data)} عملة في الطريقة المثلى")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 313 |
return volume_data
|
| 314 |
+
except Exception as e: print(f"❌ خطأ في جلب بيانات الحجم المثلى: {e}"); return []
|
|
|
|
|
|
|
|
|
|
| 315 |
async def _get_volume_data_direct_api(self) -> List[Dict[str, Any]]:
|
| 316 |
try:
|
| 317 |
url = "https://api.kucoin.com/api/v1/market/allTickers"
|
| 318 |
async with httpx.AsyncClient(timeout=15) as client:
|
| 319 |
+
response = await client.get(url); response.raise_for_status(); data = response.json()
|
|
|
|
|
|
|
| 320 |
if data.get('code') != '200000': raise ValueError(f"استجابة API غير متوقعة: {data.get('code')}")
|
| 321 |
+
tickers = data['data']['ticker']; volume_data = []
|
|
|
|
| 322 |
for ticker in tickers:
|
| 323 |
symbol = ticker['symbol']
|
| 324 |
if not symbol.endswith('USDT'): continue
|
| 325 |
formatted_symbol = symbol.replace('-', '/')
|
| 326 |
try:
|
| 327 |
+
vol_value = ticker.get('volValue'); last_price = ticker.get('last'); change_rate = ticker.get('changeRate'); vol = ticker.get('vol')
|
|
|
|
|
|
|
|
|
|
| 328 |
if vol_value is None or last_price is None or change_rate is None or vol is None: continue
|
| 329 |
+
dollar_volume = float(vol_value) if vol_value else 0; current_price = float(last_price) if last_price else 0; price_change = (float(change_rate) * 100) if change_rate else 0; volume_24h = float(vol) if vol else 0
|
| 330 |
+
if dollar_volume >= 50000 and current_price > 0: volume_data.append({ 'symbol': formatted_symbol, 'dollar_volume': dollar_volume, 'current_price': current_price, 'volume_24h': volume_24h, 'price_change_24h': price_change })
|
| 331 |
+
except (ValueError, TypeError, KeyError): continue
|
| 332 |
+
print(f"✅ تم معالجة {len(volume_data)} عملة في الطريقة المباشرة")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
return volume_data
|
| 334 |
+
except Exception as e: print(f"❌ خطأ ف�� جلب بيانات الحجم المباشر: {e}"); return []
|
|
|
|
|
|
|
|
|
|
| 335 |
async def stream_ohlcv_data(self, symbols: List[Dict[str, Any]], queue: asyncio.Queue):
|
| 336 |
+
print(f"📊 بدء تدفق بيانات OHLCV (الكاملة) لـ {len(symbols)} عملة (مصنفة)...")
|
| 337 |
+
batch_size = 15; batches = [symbols[i:i + batch_size] for i in range(0, len(symbols), batch_size)]; total_successful = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
for batch_num, batch in enumerate(batches):
|
| 339 |
print(f" 🔄 [المنتج] جلب الدفعة {batch_num + 1}/{len(batches)} ({len(batch)} عملة)...")
|
| 340 |
+
batch_tasks = []; for symbol_data in batch: task = asyncio.create_task(self._fetch_complete_ohlcv_parallel(symbol_data['symbol'])); batch_tasks.append(task)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 341 |
batch_results = await asyncio.gather(*batch_tasks, return_exceptions=True)
|
|
|
|
| 342 |
successful_data_for_batch = []
|
|
|
|
| 343 |
for i, result in enumerate(batch_results):
|
| 344 |
+
if isinstance(result, Exception): print(f" ❌ [المنتج] فشل جلب {batch[i]['symbol']}: {result}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
elif result is not None:
|
| 346 |
+
result.update(batch[i]); successful_data_for_batch.append(result)
|
| 347 |
+
print(f" ✅ [المنتج] {batch[i]['symbol']}: {result.get('successful_timeframes', 0)}/6 أطر زمنية")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 348 |
if successful_data_for_batch:
|
| 349 |
+
try: await queue.put(successful_data_for_batch); total_successful += len(successful_data_for_batch)
|
| 350 |
+
except Exception as q_err: print(f" ❌ [المنتج] فشل إرسال الدفعة للطابور: {q_err}")
|
| 351 |
+
if batch_num < len(batches) - 1: await asyncio.sleep(1)
|
| 352 |
+
print(f"✅ [المنتج] اكتمل التدفق. تم إرسال {total_successful} عملة."); await queue.put(None)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 353 |
|
| 354 |
async def _fetch_complete_ohlcv_parallel(self, symbol: str) -> Dict[str, Any]:
|
|
|
|
| 355 |
try:
|
| 356 |
+
ohlcv_data = {}; timeframes = [('5m', 200), ('15m', 200), ('1h', 200), ('4h', 200), ('1d', 200), ('1w', 200)]; timeframe_tasks = []
|
| 357 |
+
for timeframe, limit in timeframes: task = asyncio.create_task(self._fetch_single_timeframe_improved(symbol, timeframe, limit)); timeframe_tasks.append(task)
|
| 358 |
+
timeframe_results = await asyncio.gather(*timeframe_tasks, return_exceptions=True); successful_timeframes = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 359 |
for i, (timeframe, limit) in enumerate(timeframes):
|
| 360 |
result = timeframe_results[i]
|
| 361 |
if isinstance(result, Exception): continue
|
| 362 |
+
if result and len(result) >= 200: ohlcv_data[timeframe] = result; successful_timeframes += 1
|
| 363 |
+
if successful_timeframes >= 3 and ohlcv_data:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 364 |
try:
|
| 365 |
current_price = await self.get_latest_price_async(symbol)
|
| 366 |
if current_price is None:
|
| 367 |
+
for tf_data in ohlcv_data.values():
|
| 368 |
+
if tf_data and len(tf_data) > 0: current_price = tf_data[-1][4]; break
|
|
|
|
|
|
|
|
|
|
| 369 |
if current_price is None: return None
|
| 370 |
+
return { 'symbol': symbol, 'ohlcv': ohlcv_data, 'raw_ohlcv': ohlcv_data, 'current_price': current_price, 'timestamp': datetime.now().isoformat(), 'candles_count': {tf: len(data) for tf, data in ohlcv_data.items()}, 'successful_timeframes': successful_timeframes }
|
| 371 |
+
except Exception: return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
else: return None
|
| 373 |
+
except Exception: return None
|
|
|
|
| 374 |
async def _fetch_single_timeframe_improved(self, symbol: str, timeframe: str, limit: int):
|
| 375 |
+
max_retries = 3; retry_delay = 2
|
|
|
|
|
|
|
| 376 |
for attempt in range(max_retries):
|
| 377 |
try:
|
| 378 |
ohlcv_data = self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
|
| 379 |
+
if ohlcv_data and len(ohlcv_data) > 0: return ohlcv_data
|
| 380 |
+
else: return []
|
| 381 |
+
except Exception:
|
| 382 |
+
if attempt < max_retries - 1: await asyncio.sleep(retry_delay * (attempt + 1))
|
| 383 |
+
else: return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
async def get_latest_price_async(self, symbol):
|
|
|
|
| 385 |
try:
|
| 386 |
if not self.exchange: return None
|
|
|
|
| 387 |
ticker = self.exchange.fetch_ticker(symbol)
|
| 388 |
+
return float(ticker['last']) if ticker and ticker.get('last') else None
|
| 389 |
+
except Exception: return None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
async def get_whale_data_for_symbol(self, symbol):
|
| 391 |
+
try: return await self.whale_monitor.get_symbol_whale_activity(symbol) if self.whale_monitor else None
|
| 392 |
+
except Exception: return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
async def get_whale_trading_signal(self, symbol, whale_data, market_context):
|
| 394 |
+
try: return await self.whale_monitor.generate_whale_trading_signal(symbol, whale_data, market_context) if self.whale_monitor else {'action': 'HOLD', 'confidence': 0.3, 'reason': 'Whale monitor not available', 'source': 'whale_analysis'}
|
| 395 |
+
except Exception as e: return {'action': 'HOLD', 'confidence': 0.3, 'reason': f'Error: {str(e)}', 'source': 'whale_analysis'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 396 |
|
| 397 |
+
print("✅ DataManager loaded - V9.3 (High Confidence Ranker)")
|