Spaces:
Sleeping
Sleeping
File size: 17,613 Bytes
c994b92 3f2975d c994b92 c1bc90c 691c3dd 3f2975d d11dbf3 c1bc90c de7e115 3f2975d c994b92 de7e115 c994b92 c1bc90c f60c8b6 3f2975d f60c8b6 fa9f6bd 9180094 fa9f6bd f60c8b6 9180094 3f2975d 9180094 f60c8b6 fa9f6bd f60c8b6 c22273f f60c8b6 3f2975d f60c8b6 3f2975d c1bc90c f60c8b6 3f2975d f60c8b6 3f2975d f60c8b6 c1bc90c f60c8b6 b938152 f60c8b6 b938152 c1bc90c b938152 916aa63 f60c8b6 b938152 f60c8b6 b938152 f60c8b6 3f2975d f60c8b6 c1bc90c 3f2975d c22273f 3f2975d 490e43e 3f2975d f60c8b6 3f2975d c22273f 3f2975d e8ae4fb 3f2975d e71ba1c 3f2975d a1e2912 3f2975d 490e43e 3f2975d 642bf96 de7e115 f60c8b6 3f2975d f60c8b6 de7e115 3f2975d de7e115 e71ba1c f60c8b6 3f2975d f60c8b6 e8ae4fb 3f2975d 642bf96 3f2975d 642bf96 3f2975d 642bf96 3f2975d 642bf96 3f2975d e378be0 3f2975d 642bf96 3f2975d e8ae4fb 3f2975d a1e2912 3f2975d a1e2912 f60c8b6 3f2975d f60c8b6 3f2975d c1bc90c 9180094 f60c8b6 c1bc90c e378be0 f60c8b6 3f2975d c22273f 3f2975d c22273f 3f2975d f60c8b6 9180094 3f2975d 9180094 f60c8b6 3f2975d e71ba1c 3f2975d f60c8b6 3f2975d f60c8b6 3f2975d 9180094 3f2975d 9180094 fa9f6bd 3f2975d 9180094 3f2975d c22273f 3f2975d 9180094 3f2975d f60c8b6 3f2975d f60c8b6 3f2975d f60c8b6 3f2975d f60c8b6 3f2975d f60c8b6 3f2975d f60c8b6 3f2975d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 | # ============================================================
# 📂 ml_engine/data_manager.py
# (V45.0 - GEM-Architect: Anti-FOMO Revival)
# ============================================================
import asyncio
import httpx
import traceback
import logging
import pandas as pd
import numpy as np
import pandas_ta as ta # سنحتاج بعض الدوال المساعدة
from typing import List, Dict, Any
import ccxt.async_support as ccxt
# ✅ استيراد الدستور الديناميكي (للحفاظ على توافق النظام)
try:
from ml_engine.processor import SystemLimits
except ImportError:
class SystemLimits:
L1_MIN_AFFINITY_SCORE = 15.0
CURRENT_REGIME = "RANGE"
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("ccxt").setLevel(logging.WARNING)
class DataManager:
"""
DataManager V45.0 (Anti-FOMO Revival)
- Restores the STRICT Logic Tree from V15.2.
- Filters: 8% Max Pump, 12% Max Daily, RSI < 70 strict limit.
- Targets: Clean Breakouts & Oversold Reversals ONLY.
"""
def __init__(self, contracts_db, whale_monitor, r2_service=None):
self.contracts_db = contracts_db or {}
self.whale_monitor = whale_monitor
self.r2_service = r2_service
self.exchange = ccxt.kucoin({
'enableRateLimit': True,
'timeout': 60000,
'options': {'defaultType': 'spot'}
})
self.http_client = None
self.market_cache = {}
# القائمة السوداء
self.BLACKLIST_TOKENS = [
'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', 'USDD', 'USDP', 'HT', 'KCS'
]
print(f"📦 [DataManager V45.0] Anti-FOMO Shield Active.")
async def initialize(self):
print(" > [DataManager] Starting initialization...")
try:
self.http_client = httpx.AsyncClient(timeout=30.0)
await self._load_markets()
print(f"✅ [DataManager] Ready (Logic: STRICT Anti-FOMO).")
except Exception as e:
print(f"❌ [DataManager] Init Error: {e}")
traceback.print_exc()
async def _load_markets(self):
try:
if self.exchange and not self.exchange.markets:
await self.exchange.load_markets()
self.market_cache = self.exchange.markets
except Exception: pass
async def close(self):
if self.http_client: await self.http_client.aclose()
if self.exchange: await self.exchange.close()
async def load_contracts_from_r2(self):
if not self.r2_service: return
try:
self.contracts_db = await self.r2_service.load_contracts_db_async()
except: self.contracts_db = {}
def get_contracts_db(self): return self.contracts_db
# ==================================================================
# 🛡️ Layer 1: The Strict Logic Tree (From V15.2)
# ==================================================================
async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
print(f"🔍 [L1 Anti-FOMO] Filtering Universe...")
# 1. المرحلة 0: فلتر الكون (السيولة العالية فقط)
# V15.2 كان يطلب مليون دولار سيولة، سنبقيه كما هو للصرامة
initial_candidates = await self._stage0_universe_filter()
if not initial_candidates:
print("⚠️ [Layer 1] Universe empty.")
return []
# 2. جلب البيانات الفنية لأفضل 300 عملة (كما في V15.2)
top_liquid_candidates = initial_candidates[:300]
print(f" -> Analyzing top {len(top_liquid_candidates)} liquid assets...")
enriched_data = await self._fetch_technical_data_batch(top_liquid_candidates)
# 3. تطبيق شجرة القرار الصارمة
breakout_list = []
reversal_list = []
for item in enriched_data:
# هنا نستخدم منطق V15.2 الأصلي
classification = self._apply_logic_tree(item)
if classification['type'] == 'BREAKOUT':
item['l1_score'] = classification['score']
item['type'] = 'BREAKOUT'
breakout_list.append(item)
elif classification['type'] == 'REVERSAL':
item['l1_score'] = classification['score']
item['type'] = 'REVERSAL'
reversal_list.append(item)
print(f" -> [L1 Logic] Found: {len(breakout_list)} Breakouts, {len(reversal_list)} Reversals.")
# 4. الترتيب والدمج النهائي
# الـ Breakout نرتبهم بالأعلى سكور (فوليوم)
breakout_list.sort(key=lambda x: x['l1_score'], reverse=True)
# الـ Reversal نرتبهم بالأعلى سكور (سكور الارتداد في V15.2 كان 100-RSI، يعني الأعلى أفضل)
reversal_list.sort(key=lambda x: x['l1_score'], reverse=True)
# نختار الأفضل فقط (مزيج متوازن)
final_selection = breakout_list[:25] + reversal_list[:15]
return [
{
'symbol': c['symbol'],
'quote_volume': c.get('quote_volume', 0),
'current_price': c.get('current_price', 0),
'type': c.get('type', 'UNKNOWN'),
'l1_score': c.get('l1_score', 0)
}
for c in final_selection
]
# ==================================================================
# 🔗 Bridge for Backtest Engine Compatibility (IMPORTANT)
# ==================================================================
def _calculate_structural_score(self, df: pd.DataFrame, symbol: str, regime: str) -> (float, List[str]):
"""
[Compatibility Wrapper]
هذه الدالة موجودة لكي لا يتعطل محرك الباكتست (backtest_engine.py).
تقوم بتحويل بيانات الباكتست إلى تنسيق يفهمه منطق V15.2.
"""
# محاكاة تنسيق البيانات الذي يطلبه _apply_logic_tree
# نحتاج تقسيم الـ DF إلى 1H و 15M تقريبياً
try:
# Resample لإنشاء بيانات 1H و 15M من البيانات المدخلة (التي غالباً تكون 15M)
df_15m = df.copy()
agg_dict = {'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum'}
df_1h = df.resample('1H').agg(agg_dict).dropna()
# تحويلها لقوائم كما يتوقع الكود القديم
ohlcv_1h = df_1h.reset_index()[['timestamp', 'open', 'high', 'low', 'close', 'volume']].values.tolist()
ohlcv_15m = df_15m.reset_index()[['timestamp', 'open', 'high', 'low', 'close', 'volume']].values.tolist()
dummy_data = {
'ohlcv_1h': ohlcv_1h,
'ohlcv_15m': ohlcv_15m,
'change_24h': 0.0 # غير متوفر بدقة في الباكتست الجزئي، نتجاوزه
}
res = self._apply_logic_tree(dummy_data)
score = res.get('score', 0.0)
# تحويل السكور ليكون متوافقاً مع الباكتست (حول 20-80)
if res['type'] == 'BREAKOUT':
return score * 20.0, ["BREAKOUT"] # Breakout score is usually low (ratio), boost it
elif res['type'] == 'REVERSAL':
return score, ["REVERSAL"] # Reversal score is already 0-100
return 0.0, ["NONE"]
except Exception:
return 0.0, ["ERROR"]
# ==================================================================
# 🏗️ V15.2 Logic Core (Unchanged Logic)
# ==================================================================
def _apply_logic_tree(self, data: Dict[str, Any]) -> Dict[str, Any]:
try:
df_1h = self._calc_indicators(data['ohlcv_1h'])
df_15m = self._calc_indicators(data['ohlcv_15m'])
except:
return {'type': 'NONE'}
if df_1h.empty or df_15m.empty: return {'type': 'NONE'}
curr_1h = df_1h.iloc[-1]
curr_15m = df_15m.iloc[-1]
# --- Stage 2: Anti-FOMO Filters (STRICT) ---
try:
# حساب التغير في آخر 4 ساعات
if len(df_1h) >= 5:
close_4h_ago = df_1h.iloc[-5]['close']
change_4h = ((curr_1h['close'] - close_4h_ago) / close_4h_ago) * 100
else:
change_4h = 0.0
except: change_4h = 0.0
# 1. فلتر المضخات: ممنوع الدخول إذا صعدت أكثر من 8% في 4 ساعات
if change_4h > 8.0: return {'type': 'NONE'}
# 2. فلتر التذبذب اليومي: ممنوع أكثر من 12% (للبعد عن العملات المجنونة)
if data.get('change_24h', 0) > 12.0: return {'type': 'NONE'}
# 3. فلتر القمة: ممنوع RSI فوق 70 قطعاً
if curr_1h['rsi'] > 70: return {'type': 'NONE'}
# 4. فلتر الامتداد: ممنوع الابتعاد عن المتوسط كثيراً
deviation = (curr_1h['close'] - curr_1h['ema20']) / curr_1h['atr'] if curr_1h['atr'] > 0 else 0
if deviation > 1.8: return {'type': 'NONE'}
# --- Stage 3: Setup Classification ---
# === A. Breakout Logic ===
is_breakout = False
breakout_score = 0.0
# تريند صاعد
bullish_structure = (curr_1h['ema20'] > curr_1h['ema50']) or (curr_1h['close'] > curr_1h['ema20'])
if bullish_structure:
# RSI يجب أن يكون فيه مساحة للصعود (ليس منخفضاً جداً ولا مرتفعاً جداً)
if 45 <= curr_1h['rsi'] <= 68:
if curr_15m['close'] >= curr_15m['ema20']:
# Volatility Squeeze (هدوء ما قبل العاصفة)
avg_range = (df_15m['high'] - df_15m['low']).rolling(10).mean().iloc[-1]
current_range = curr_15m['high'] - curr_15m['low']
if current_range <= avg_range * 1.8:
vol_ma20 = df_15m['volume'].rolling(20).mean().iloc[-1]
# شرط الفوليوم: شمعة الحالية فيها سيولة 1.5 ضعف المتوسط
if curr_15m['volume'] >= 1.5 * vol_ma20:
is_breakout = True
breakout_score = curr_15m['volume'] / vol_ma20 if vol_ma20 > 0 else 1.0
if is_breakout:
return {'type': 'BREAKOUT', 'score': breakout_score}
# === B. Reversal Logic ===
is_reversal = False
reversal_score = 0.0
# تشبع بيعي واضح
if 20 <= curr_1h['rsi'] <= 40:
# السعر هبط مؤخراً
if change_4h <= -2.0:
# البحث عن شمعة انعكاسية (Hammer / Green Body)
last_3 = df_15m.iloc[-3:]
found_rejection = False
for _, row in last_3.iterrows():
rng = row['high'] - row['low']
if rng > 0:
is_green = row['close'] > row['open']
# Hammer pattern logic
lower_wick = min(row['open'], row['close']) - row['low']
body = abs(row['close'] - row['open'])
hammer_shape = lower_wick > (body * 1.5)
if is_green or hammer_shape:
found_rejection = True
break
if found_rejection:
is_reversal = True
# السكور كلما قل الـ RSI كان أفضل للارتداد
reversal_score = (100 - curr_1h['rsi'])
if is_reversal:
return {'type': 'REVERSAL', 'score': reversal_score}
return {'type': 'NONE'}
# ------------------------------------------------------------------
# Manual Indicator Calculation (Pandas pure - Exactly like V15.2)
# ------------------------------------------------------------------
def _calc_indicators(self, ohlcv_list):
if not ohlcv_list: return pd.DataFrame()
df = pd.DataFrame(ohlcv_list, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
# RSI Calculation
delta = df['close'].diff()
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
rs = gain / loss
df['rsi'] = 100 - (100 / (1 + rs))
# EMA
df['ema20'] = df['close'].ewm(span=20, adjust=False).mean()
df['ema50'] = df['close'].ewm(span=50, adjust=False).mean()
# ATR
high_low = df['high'] - df['low']
high_close = np.abs(df['high'] - df['close'].shift())
low_close = np.abs(df['low'] - df['close'].shift())
ranges = pd.concat([high_low, high_close, low_close], axis=1)
true_range = np.max(ranges, axis=1)
df['atr'] = true_range.rolling(14).mean()
df.fillna(0, inplace=True)
return df
# ==================================================================
# 🌌 Stage 0: Universe Filter (V15.2 Logic)
# ==================================================================
async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
try:
tickers = await self.exchange.fetch_tickers()
candidates = []
for symbol, ticker in tickers.items():
if not symbol.endswith('/USDT'): continue
base_curr = symbol.split('/')[0]
if any(bad in base_curr for bad in self.BLACKLIST_TOKENS): continue
# شرط السيولة الصارم: 1 مليون دولار
quote_vol = ticker.get('quoteVolume')
if not quote_vol or quote_vol < 1_000_000: continue
last_price = ticker.get('last')
if not last_price or last_price < 0.0005: continue
candidates.append({
'symbol': symbol,
'quote_volume': quote_vol,
'current_price': last_price,
'change_24h': float(ticker.get('percentage', 0.0))
})
# ترتيب مبدئي بالحجم
candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
return candidates
except Exception as e:
print(f"❌ [L1 Error] Universe filter failed: {e}")
return []
# ==================================================================
# 🔄 Batch Fetching
# ==================================================================
async def _fetch_technical_data_batch(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
chunk_size = 15
results = []
for i in range(0, len(candidates), chunk_size):
chunk = candidates[i:i + chunk_size]
chunk_tasks = [self._fetch_single_tech_data(c) for c in chunk]
chunk_results = await asyncio.gather(*chunk_tasks)
results.extend([r for r in chunk_results if r is not None])
await asyncio.sleep(0.05)
return results
async def _fetch_single_tech_data(self, candidate: Dict[str, Any]) -> Any:
symbol = candidate['symbol']
try:
# V15.2 Requires 1H and 15M
ohlcv_1h = await self.exchange.fetch_ohlcv(symbol, '1h', limit=60)
ohlcv_15m = await self.exchange.fetch_ohlcv(symbol, '15m', limit=60)
if not ohlcv_1h or len(ohlcv_1h) < 55 or not ohlcv_15m or len(ohlcv_15m) < 55:
return None
candidate['ohlcv_1h'] = ohlcv_1h
candidate['ohlcv_15m'] = ohlcv_15m
return candidate
except Exception:
return None
# ==================================================================
# 🎯 Public Helpers
# ==================================================================
async def get_latest_price_async(self, symbol: str) -> float:
try:
ticker = await self.exchange.fetch_ticker(symbol)
return float(ticker['last'])
except Exception: return 0.0
async def get_latest_ohlcv(self, symbol: str, timeframe: str = '5m', limit: int = 100) -> List[List[float]]:
try:
candles = await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
return candles or []
except Exception: return []
async def get_order_book_snapshot(self, symbol: str, limit: int = 20) -> Dict[str, Any]:
try:
ob = await self.exchange.fetch_order_book(symbol, limit)
return ob
except Exception: return {} |