Spaces:
Sleeping
Sleeping
Update ml_engine/data_manager.py
Browse files- ml_engine/data_manager.py +76 -199
ml_engine/data_manager.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
-
# ============================================================
|
| 2 |
# 📂 ml_engine/data_manager.py
|
| 3 |
-
# (
|
| 4 |
-
# ============================================================
|
| 5 |
-
# - Removed
|
| 6 |
-
# -
|
| 7 |
-
# -
|
| 8 |
-
# ============================================================
|
| 9 |
|
| 10 |
import asyncio
|
| 11 |
import httpx
|
|
@@ -44,13 +44,13 @@ class DataManager:
|
|
| 44 |
self.http_client = None
|
| 45 |
self.market_cache = {}
|
| 46 |
|
| 47 |
-
# القائمة السوداء ل
|
| 48 |
self.BLACKLIST_TOKENS = [
|
| 49 |
'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
|
| 50 |
'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', '5S', '5L'
|
| 51 |
]
|
| 52 |
|
| 53 |
-
print(f"📦 [DataManager
|
| 54 |
|
| 55 |
async def initialize(self):
|
| 56 |
"""تهيئة الاتصال والأسواق"""
|
|
@@ -82,238 +82,130 @@ class DataManager:
|
|
| 82 |
return self.contracts_db
|
| 83 |
|
| 84 |
# ==================================================================
|
| 85 |
-
# 🌍 Global Market Validator
|
| 86 |
# ==================================================================
|
| 87 |
async def check_global_market_health(self) -> Dict[str, Any]:
|
| 88 |
"""
|
| 89 |
-
|
| 90 |
"""
|
| 91 |
try:
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
if not btc_ohlcv: return {'is_safe': True, 'reason': 'No BTC Data - Bypassed'}
|
| 95 |
|
| 96 |
df = pd.DataFrame(btc_ohlcv, columns=['ts', 'o', 'h', 'l', 'c', 'v'])
|
| 97 |
-
|
| 98 |
-
prev_close = df['c'].iloc[-2]
|
| 99 |
|
| 100 |
-
#
|
| 101 |
-
daily_change
|
| 102 |
-
if daily_change < -0.05: # -5% Crash check
|
| 103 |
return {'is_safe': False, 'reason': f'🚨 BTC CRASHING ({daily_change*100:.2f}%)'}
|
| 104 |
|
| 105 |
-
|
| 106 |
-
sma20 = df['c'].rolling(20).mean().iloc[-1]
|
| 107 |
-
if current_close < sma20 * 0.90: # Bear Market Deep
|
| 108 |
-
return {'is_safe': False, 'reason': '📉 Deep Bear Market (Risk Off)'}
|
| 109 |
-
|
| 110 |
-
return {'is_safe': True, 'reason': '✅ Market Healthy'}
|
| 111 |
|
| 112 |
except Exception as e:
|
| 113 |
-
print(f"⚠️ [Market Validator] Error: {e}")
|
| 114 |
return {'is_safe': True, 'reason': 'Error Bypass'}
|
| 115 |
|
| 116 |
# ==================================================================
|
| 117 |
-
# 🧠 Layer 1:
|
| 118 |
# ==================================================================
|
| 119 |
-
async def layer1_rapid_screening(self, limit=
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
self.adaptive_hub_ref = adaptive_hub_ref
|
| 121 |
-
print(f"🔍 [Layer 1] Screening Market (
|
| 122 |
|
| 123 |
-
# 0. فحص
|
| 124 |
market_health = await self.check_global_market_health()
|
| 125 |
-
|
| 126 |
if not market_health['is_safe']:
|
| 127 |
print(f"⛔ [Market Validator] Trading Halted: {market_health['reason']}")
|
| 128 |
return []
|
| 129 |
-
else:
|
| 130 |
-
print(f" 🌍 [Market Validator] Status: {market_health['reason']}")
|
| 131 |
|
| 132 |
-
# 1. فلتر السيولة ال
|
| 133 |
initial_candidates = await self._stage0_universe_filter()
|
| 134 |
if not initial_candidates:
|
| 135 |
-
print("⚠️ [Layer 1]
|
| 136 |
return []
|
| 137 |
|
| 138 |
-
#
|
| 139 |
-
#
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
| 141 |
enriched_data = await self._fetch_technical_data_batch(top_candidates)
|
| 142 |
|
| 143 |
-
|
| 144 |
|
| 145 |
-
#
|
| 146 |
for item in enriched_data:
|
| 147 |
-
|
|
|
|
| 148 |
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
regime_info = self._diagnose_asset_regime(item)
|
| 152 |
-
item['asset_regime'] = regime_info['regime']
|
| 153 |
-
item['asset_regime_conf'] = regime_info['conf']
|
| 154 |
-
|
| 155 |
-
item['strategy_type'] = classification['type']
|
| 156 |
-
item['l1_sort_score'] = classification['score']
|
| 157 |
-
item['strategy_tag'] = classification['type']
|
| 158 |
-
|
| 159 |
-
# تخطي العملات "الميتة" إلا إذا كانت في حالة ضغط
|
| 160 |
-
if regime_info['regime'] == 'DEAD' and classification['type'] == 'MOMENTUM_LAUNCH':
|
| 161 |
-
if not classification.get('is_squeeze', False):
|
| 162 |
-
continue
|
| 163 |
-
|
| 164 |
-
semi_final_list.append(item)
|
| 165 |
-
|
| 166 |
-
# 4. فحص العمق وحقن الإعدادات
|
| 167 |
-
final_list = []
|
| 168 |
-
# ترتيب حسب السكور الفني وليس الفوليوم
|
| 169 |
-
semi_final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
|
| 170 |
-
candidates_for_depth = semi_final_list[:limit]
|
| 171 |
-
|
| 172 |
-
if candidates_for_depth:
|
| 173 |
-
print(f" 🛡️ [Layer 1.5] Checking Depth for {len(candidates_for_depth)} candidates...")
|
| 174 |
-
|
| 175 |
-
for item in candidates_for_depth:
|
| 176 |
-
# أ. فحص العمق (Depth Check)
|
| 177 |
-
if item['strategy_type'] in ['ACCUMULATION_SQUEEZE', 'SAFE_BOTTOM']:
|
| 178 |
-
try:
|
| 179 |
-
atr_val = item.get('atr_value', 0.0)
|
| 180 |
-
curr_price = item.get('current_price', 0.0)
|
| 181 |
-
|
| 182 |
-
if atr_val > 0 and curr_price > 0:
|
| 183 |
-
range_2h = atr_val * 2.0
|
| 184 |
-
ob_score = await self._check_ob_pressure(item['symbol'], curr_price, range_2h)
|
| 185 |
-
|
| 186 |
-
if ob_score > 0.6:
|
| 187 |
-
item['l1_sort_score'] += 0.15
|
| 188 |
-
item['note'] = f"Strong Depth Support ({ob_score:.2f})"
|
| 189 |
-
elif ob_score < 0.4:
|
| 190 |
-
item['l1_sort_score'] -= 0.10
|
| 191 |
-
except Exception: pass
|
| 192 |
|
| 193 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
if self.adaptive_hub_ref:
|
| 195 |
-
|
| 196 |
-
dynamic_config = self.adaptive_hub_ref.get_coin_type_config(
|
| 197 |
item['dynamic_limits'] = dynamic_config
|
| 198 |
|
| 199 |
final_list.append(item)
|
| 200 |
|
| 201 |
-
# الترتيب ال
|
| 202 |
-
|
| 203 |
-
|
| 204 |
|
| 205 |
-
print(f"✅ [Layer 1] Passed {len(
|
| 206 |
-
return
|
| 207 |
|
| 208 |
# ==================================================================
|
| 209 |
-
# 🧱 Order Book Depth Scanner
|
| 210 |
# ==================================================================
|
| 211 |
-
async def
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
ob = await self.exchange.fetch_order_book(symbol, limit=50)
|
| 215 |
-
bids = ob['bids']
|
| 216 |
-
asks = ob['asks']
|
| 217 |
-
|
| 218 |
-
min_price = current_price - price_range
|
| 219 |
-
max_price = current_price + price_range
|
| 220 |
-
|
| 221 |
-
support_vol = 0.0
|
| 222 |
-
resistance_vol = 0.0
|
| 223 |
-
|
| 224 |
-
for p, v in bids:
|
| 225 |
-
if p >= min_price: support_vol += v
|
| 226 |
-
else: break
|
| 227 |
-
|
| 228 |
-
for p, v in asks:
|
| 229 |
-
if p <= max_price: resistance_vol += v
|
| 230 |
-
else: break
|
| 231 |
-
|
| 232 |
-
if (support_vol + resistance_vol) == 0: return 0.5
|
| 233 |
-
return support_vol / (support_vol + resistance_vol)
|
| 234 |
-
except Exception:
|
| 235 |
-
return 0.5
|
| 236 |
-
|
| 237 |
-
# ==================================================================
|
| 238 |
-
# ⚖️ The Dual-Classifier Logic (STRICT MODE)
|
| 239 |
-
# ==================================================================
|
| 240 |
-
def _classify_opportunity_type(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
| 241 |
-
"""تصنيف العملة - نسخة منقحة بدون تمرير عشوائي"""
|
| 242 |
-
try:
|
| 243 |
-
df_1h = self._calc_indicators(data['ohlcv_1h_raw'])
|
| 244 |
-
curr = df_1h.iloc[-1]
|
| 245 |
-
data['atr_value'] = curr['atr']
|
| 246 |
-
except: return {'type': 'NONE', 'score': 0}
|
| 247 |
-
|
| 248 |
-
rsi = curr['rsi']
|
| 249 |
-
close = curr['close']
|
| 250 |
-
ema20 = curr['ema20']
|
| 251 |
-
ema50 = curr['ema50']
|
| 252 |
-
atr = curr['atr']
|
| 253 |
-
|
| 254 |
-
lower_bb = curr['lower_bb'] if 'lower_bb' in curr else (curr['ema20'] - (2*curr['atr']))
|
| 255 |
-
upper_bb = curr['upper_bb'] if 'upper_bb' in curr else (curr['ema20'] + (2*curr['atr']))
|
| 256 |
-
bb_width = (upper_bb - lower_bb) / curr['ema20'] if curr['ema20'] > 0 else 1.0
|
| 257 |
-
|
| 258 |
-
# 🔥 1. Dead Coin Filter
|
| 259 |
-
volatility_pct = (atr / close) * 100 if close > 0 else 0
|
| 260 |
-
if volatility_pct < 2 : return {'type': 'NONE', 'score': 0}
|
| 261 |
-
|
| 262 |
-
# 🛡️ TYPE 1: SAFE_BOTTOM (القيعان الآمنة)
|
| 263 |
-
# تشبع بيعي حقيقي فقط
|
| 264 |
-
if rsi < 50: # Raised slightly from 55
|
| 265 |
-
# يجب أن يكون السعر قريباً جداً من القاع أو تحته
|
| 266 |
-
if close <= lower_bb * 1.05:
|
| 267 |
-
score = (60 - rsi) / 20.0
|
| 268 |
-
return {'type': 'SAFE_BOTTOM', 'score': min(score, 1.0)}
|
| 269 |
-
|
| 270 |
-
# 🔋 TYPE 2: ACCUMULATION_SQUEEZE (التجميع والضغط)
|
| 271 |
-
elif 40 <= rsi <= 65:
|
| 272 |
-
if bb_width < 0.15: # Stricter squeeze
|
| 273 |
-
score = 1.0 - (bb_width * 3.0)
|
| 274 |
-
return {'type': 'ACCUMULATION_SQUEEZE', 'score': max(score, 0.5), 'is_squeeze': True}
|
| 275 |
-
|
| 276 |
-
# 🚀 TYPE 3: MOMENTUM_LAUNCH (انطلاق الزخم)
|
| 277 |
-
elif 50 < rsi < 80:
|
| 278 |
-
if close > ema50:
|
| 279 |
-
dist_to_upper = (upper_bb - close) / close
|
| 280 |
-
if dist_to_upper < 0.10: # قريب جداً من الاختراق
|
| 281 |
-
score = rsi / 100.0
|
| 282 |
-
return {'type': 'MOMENTUM_LAUNCH', 'score': score}
|
| 283 |
-
|
| 284 |
-
# ❌ REMOVED: "Special Case: High Volatility Catch"
|
| 285 |
-
# This was causing the "Same Coin" loop.
|
| 286 |
-
# If it doesn't fit the patterns above, it's rejected.
|
| 287 |
-
|
| 288 |
-
return {'type': 'NONE', 'score': 0}
|
| 289 |
|
| 290 |
# ==================================================================
|
| 291 |
-
# 🔍 Stage 0: Universe Filter
|
| 292 |
# ==================================================================
|
| 293 |
async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
|
| 294 |
-
"""جلب كل العملات وتصفيتها حسب ال
|
| 295 |
try:
|
| 296 |
-
|
|
|
|
| 297 |
|
| 298 |
print(f" 🛡️ [Stage 0] Fetching Tickers (Min Vol: ${MIN_VOLUME_THRESHOLD:,.0f})...")
|
| 299 |
tickers = await self.exchange.fetch_tickers()
|
| 300 |
candidates = []
|
| 301 |
|
| 302 |
SOVEREIGN_COINS = ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT']
|
| 303 |
-
reject_stats = {"volume": 0, "
|
| 304 |
|
| 305 |
for symbol, ticker in tickers.items():
|
| 306 |
if not symbol.endswith('/USDT'): continue
|
| 307 |
|
|
|
|
| 308 |
base_curr = symbol.split('/')[0]
|
| 309 |
if any(bad in base_curr for bad in self.BLACKLIST_TOKENS):
|
| 310 |
reject_stats["blacklist"] += 1
|
| 311 |
continue
|
| 312 |
|
|
|
|
| 313 |
base_vol = float(ticker.get('baseVolume') or 0.0)
|
| 314 |
last_price = float(ticker.get('last') or 0.0)
|
| 315 |
calc_quote_vol = base_vol * last_price
|
| 316 |
|
|
|
|
| 317 |
is_sovereign = symbol in SOVEREIGN_COINS
|
| 318 |
|
| 319 |
if not is_sovereign:
|
|
@@ -324,10 +216,6 @@ class DataManager:
|
|
| 324 |
change_pct = ticker.get('percentage')
|
| 325 |
if change_pct is None: change_pct = 0.0
|
| 326 |
|
| 327 |
-
if abs(change_pct) > 40.0: # Relaxed slightly
|
| 328 |
-
reject_stats["change"] += 1
|
| 329 |
-
continue
|
| 330 |
-
|
| 331 |
candidates.append({
|
| 332 |
'symbol': symbol,
|
| 333 |
'quote_volume': calc_quote_vol,
|
|
@@ -335,8 +223,9 @@ class DataManager:
|
|
| 335 |
'change_24h': change_pct
|
| 336 |
})
|
| 337 |
|
|
|
|
| 338 |
candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
|
| 339 |
-
print(f" ℹ️ [Stage 0] Ignored {reject_stats['volume']} low-vol coins.")
|
| 340 |
return candidates
|
| 341 |
|
| 342 |
except Exception as e:
|
|
@@ -345,10 +234,10 @@ class DataManager:
|
|
| 345 |
return []
|
| 346 |
|
| 347 |
# ------------------------------------------------------------------
|
| 348 |
-
# 🧭 The Diagnoser
|
| 349 |
# ------------------------------------------------------------------
|
| 350 |
def _diagnose_asset_regime(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
| 351 |
-
"""تشخيص حالة العملة الفردية"""
|
| 352 |
try:
|
| 353 |
if 'df_1h' not in item:
|
| 354 |
if 'ohlcv_1h_raw' in item:
|
|
@@ -364,18 +253,14 @@ class DataManager:
|
|
| 364 |
ema20 = curr['ema20']
|
| 365 |
ema50 = curr['ema50']
|
| 366 |
rsi = curr['rsi']
|
| 367 |
-
atr = curr['atr']
|
| 368 |
-
atr_pct = (atr / price) * 100 if price > 0 else 0
|
| 369 |
|
| 370 |
regime = "RANGE"
|
| 371 |
conf = 0.5
|
| 372 |
|
| 373 |
-
if
|
| 374 |
-
|
| 375 |
-
if price > ema20 and ema20 > ema50 and rsi > 50:
|
| 376 |
regime = "BULL"
|
| 377 |
conf = 0.8 if rsi > 55 else 0.6
|
| 378 |
-
elif price < ema20 and ema20 < ema50
|
| 379 |
regime = "BEAR"
|
| 380 |
conf = 0.8 if rsi < 45 else 0.6
|
| 381 |
|
|
@@ -383,7 +268,7 @@ class DataManager:
|
|
| 383 |
except Exception: return {'regime': 'RANGE', 'conf': 0.0}
|
| 384 |
|
| 385 |
# ------------------------------------------------------------------
|
| 386 |
-
# Helpers
|
| 387 |
# ------------------------------------------------------------------
|
| 388 |
async def _fetch_technical_data_batch(self, candidates):
|
| 389 |
"""جلب البيانات الفنية (1h, 15m) على دفعات"""
|
|
@@ -398,6 +283,7 @@ class DataManager:
|
|
| 398 |
|
| 399 |
async def _fetch_single(self, c):
|
| 400 |
try:
|
|
|
|
| 401 |
h1 = await self.exchange.fetch_ohlcv(c['symbol'], '1h', limit=100)
|
| 402 |
m15 = await self.exchange.fetch_ohlcv(c['symbol'], '15m', limit=50)
|
| 403 |
if not h1 or not m15: return None
|
|
@@ -427,11 +313,6 @@ class DataManager:
|
|
| 427 |
tr = np.maximum(df['h']-df['l'], np.maximum(abs(df['h']-df['c'].shift()), abs(df['l']-df['c'].shift())))
|
| 428 |
df['atr'] = tr.rolling(14).mean()
|
| 429 |
|
| 430 |
-
# Bollinger Bands
|
| 431 |
-
std = df['c'].rolling(20).std()
|
| 432 |
-
df['upper_bb'] = df['ema20'] + (2 * std)
|
| 433 |
-
df['lower_bb'] = df['ema20'] - (2 * std)
|
| 434 |
-
|
| 435 |
df.rename(columns={'o':'open', 'h':'high', 'l':'low', 'c':'close', 'v':'volume'}, inplace=True)
|
| 436 |
return df.fillna(0)
|
| 437 |
|
|
@@ -441,8 +322,4 @@ class DataManager:
|
|
| 441 |
|
| 442 |
async def get_latest_ohlcv(self, symbol, timeframe='5m', limit=100):
|
| 443 |
try: return await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
|
| 444 |
-
except: return []
|
| 445 |
-
|
| 446 |
-
async def get_order_book_snapshot(self, symbol, limit=20):
|
| 447 |
-
try: return await self.exchange.fetch_order_book(symbol, limit)
|
| 448 |
-
except: return {}
|
|
|
|
| 1 |
+
# ==============================================================================
|
| 2 |
# 📂 ml_engine/data_manager.py
|
| 3 |
+
# (V75.0 - GEM-Architect: Open Floodgates Mode)
|
| 4 |
+
# ==============================================================================
|
| 5 |
+
# - Removed Technical Hard Filters (Safe Bottom, Momentum, etc.).
|
| 6 |
+
# - Primary Filter: Volume > $1M ONLY.
|
| 7 |
+
# - Decision Authority: Delegated 100% to Layer 2 (Neural Models).
|
| 8 |
+
# ==============================================================================
|
| 9 |
|
| 10 |
import asyncio
|
| 11 |
import httpx
|
|
|
|
| 44 |
self.http_client = None
|
| 45 |
self.market_cache = {}
|
| 46 |
|
| 47 |
+
# القائمة السوداء (العملات المستقرة والرافعة)
|
| 48 |
self.BLACKLIST_TOKENS = [
|
| 49 |
'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
|
| 50 |
'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', '5S', '5L'
|
| 51 |
]
|
| 52 |
|
| 53 |
+
print(f"📦 [DataManager V75.0] Initialized (Filter: Vol > $1M Only).")
|
| 54 |
|
| 55 |
async def initialize(self):
|
| 56 |
"""تهيئة الاتصال والأسواق"""
|
|
|
|
| 82 |
return self.contracts_db
|
| 83 |
|
| 84 |
# ==================================================================
|
| 85 |
+
# 🌍 Global Market Validator (Bypassable)
|
| 86 |
# ==================================================================
|
| 87 |
async def check_global_market_health(self) -> Dict[str, Any]:
|
| 88 |
"""
|
| 89 |
+
فحص سريع لصحة السوق. لن يوقف التداول ولكنه يعطي تحذيراً.
|
| 90 |
"""
|
| 91 |
try:
|
| 92 |
+
btc_ohlcv = await self.exchange.fetch_ohlcv('BTC/USDT', '1d', limit=7)
|
| 93 |
+
if not btc_ohlcv: return {'is_safe': True, 'reason': 'No BTC Data'}
|
|
|
|
| 94 |
|
| 95 |
df = pd.DataFrame(btc_ohlcv, columns=['ts', 'o', 'h', 'l', 'c', 'v'])
|
| 96 |
+
daily_change = (df['c'].iloc[-1] - df['c'].iloc[-2]) / df['c'].iloc[-2]
|
|
|
|
| 97 |
|
| 98 |
+
# فقط الانهيار الكبير جداً يوقف النظام
|
| 99 |
+
if daily_change < -0.10:
|
|
|
|
| 100 |
return {'is_safe': False, 'reason': f'🚨 BTC CRASHING ({daily_change*100:.2f}%)'}
|
| 101 |
|
| 102 |
+
return {'is_safe': True, 'reason': '✅ Market Open'}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
except Exception as e:
|
|
|
|
| 105 |
return {'is_safe': True, 'reason': 'Error Bypass'}
|
| 106 |
|
| 107 |
# ==================================================================
|
| 108 |
+
# 🧠 Layer 1: The Open Gate (Volume Only)
|
| 109 |
# ==================================================================
|
| 110 |
+
async def layer1_rapid_screening(self, limit=200, adaptive_hub_ref=None) -> List[Dict[str, Any]]:
|
| 111 |
+
"""
|
| 112 |
+
يقوم بفلترة العملات بناءً على الفوليوم فقط (1 مليون دولار).
|
| 113 |
+
يمرر أفضل العملات (حسب الفوليوم) إلى الطبقة الثانية للتحليل العصبي.
|
| 114 |
+
"""
|
| 115 |
self.adaptive_hub_ref = adaptive_hub_ref
|
| 116 |
+
print(f"🔍 [Layer 1] Screening Market (Volume Only > $1M)...")
|
| 117 |
|
| 118 |
+
# 0. فحص السوق
|
| 119 |
market_health = await self.check_global_market_health()
|
|
|
|
| 120 |
if not market_health['is_safe']:
|
| 121 |
print(f"⛔ [Market Validator] Trading Halted: {market_health['reason']}")
|
| 122 |
return []
|
|
|
|
|
|
|
| 123 |
|
| 124 |
+
# 1. فلتر السيولة الصارم (Stage 0)
|
| 125 |
initial_candidates = await self._stage0_universe_filter()
|
| 126 |
if not initial_candidates:
|
| 127 |
+
print("⚠️ [Layer 1] No coins met the $1M volume criteria.")
|
| 128 |
return []
|
| 129 |
|
| 130 |
+
# نأخذ أعلى العملات سيولة لنقوم بجلب بياناتها
|
| 131 |
+
# نرفع العدد هنا لأننا لن نفلتر بالفنيات، بل سنرسل للنماذج
|
| 132 |
+
# لكن يجب الحذر من الـ Rate Limits، لذا سنأخذ أفضل 150 عملة
|
| 133 |
+
top_candidates = initial_candidates[:limit]
|
| 134 |
+
print(f" 📥 Fetching data for top {len(top_candidates)} liquid assets...")
|
| 135 |
+
|
| 136 |
enriched_data = await self._fetch_technical_data_batch(top_candidates)
|
| 137 |
|
| 138 |
+
final_list = []
|
| 139 |
|
| 140 |
+
# 2. التجهيز للطبقة الثانية (بدون فلترة فنية)
|
| 141 |
for item in enriched_data:
|
| 142 |
+
# تشخيص الحالة فقط للعلم (Info) وليس للرفض
|
| 143 |
+
regime_info = self._diagnose_asset_regime(item)
|
| 144 |
|
| 145 |
+
item['asset_regime'] = regime_info['regime']
|
| 146 |
+
item['asset_regime_conf'] = regime_info['conf']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
+
# ✅ تعيين نوع موحد ليتم التعامل معه في الطبقات التالية
|
| 149 |
+
item['strategy_type'] = 'NEURAL_SCAN'
|
| 150 |
+
item['strategy_tag'] = 'NEURAL'
|
| 151 |
+
|
| 152 |
+
# السكور الأولي يعتمد على السيولة النسبية (لترتيب الأولويات فقط)
|
| 153 |
+
# يمكن تعديله لاحقاً بواسطة Neural Models
|
| 154 |
+
item['l1_sort_score'] = 0.5 # درجة حيادية للسماح للكل بالمرور
|
| 155 |
+
|
| 156 |
+
# حقن الإعدادات الديناميكية (إن وجدت)
|
| 157 |
if self.adaptive_hub_ref:
|
| 158 |
+
# نستخدم إعدادات SAFE_BOTTOM كإعداد��ت افتراضية محافظة
|
| 159 |
+
dynamic_config = self.adaptive_hub_ref.get_coin_type_config('SAFE_BOTTOM')
|
| 160 |
item['dynamic_limits'] = dynamic_config
|
| 161 |
|
| 162 |
final_list.append(item)
|
| 163 |
|
| 164 |
+
# الترتيب حسب الفوليوم (الأكثر سيولة أولاً)
|
| 165 |
+
# item['quote_volume'] موجودة من Stage 0
|
| 166 |
+
final_list.sort(key=lambda x: x.get('quote_volume', 0), reverse=True)
|
| 167 |
|
| 168 |
+
print(f"✅ [Layer 1] Passed {len(final_list)} candidates directly to Neural Layer.")
|
| 169 |
+
return final_list
|
| 170 |
|
| 171 |
# ==================================================================
|
| 172 |
+
# 🧱 Order Book Depth Scanner (Optional Helper)
|
| 173 |
# ==================================================================
|
| 174 |
+
async def get_order_book_snapshot(self, symbol: str, limit=20):
|
| 175 |
+
try: return await self.exchange.fetch_order_book(symbol, limit)
|
| 176 |
+
except: return {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
# ==================================================================
|
| 179 |
+
# 🔍 Stage 0: Universe Filter (1 Million Dollar Rule)
|
| 180 |
# ==================================================================
|
| 181 |
async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
|
| 182 |
+
"""جلب كل العملات وتصفيتها حسب شرط المليون دولار"""
|
| 183 |
try:
|
| 184 |
+
# ✅ الشرط الوحيد: 1 مليون دولار
|
| 185 |
+
MIN_VOLUME_THRESHOLD = 1000000.0
|
| 186 |
|
| 187 |
print(f" 🛡️ [Stage 0] Fetching Tickers (Min Vol: ${MIN_VOLUME_THRESHOLD:,.0f})...")
|
| 188 |
tickers = await self.exchange.fetch_tickers()
|
| 189 |
candidates = []
|
| 190 |
|
| 191 |
SOVEREIGN_COINS = ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT']
|
| 192 |
+
reject_stats = {"volume": 0, "blacklist": 0}
|
| 193 |
|
| 194 |
for symbol, ticker in tickers.items():
|
| 195 |
if not symbol.endswith('/USDT'): continue
|
| 196 |
|
| 197 |
+
# إقصاء العملات المحظورة
|
| 198 |
base_curr = symbol.split('/')[0]
|
| 199 |
if any(bad in base_curr for bad in self.BLACKLIST_TOKENS):
|
| 200 |
reject_stats["blacklist"] += 1
|
| 201 |
continue
|
| 202 |
|
| 203 |
+
# حساب الفوليوم بالدولار
|
| 204 |
base_vol = float(ticker.get('baseVolume') or 0.0)
|
| 205 |
last_price = float(ticker.get('last') or 0.0)
|
| 206 |
calc_quote_vol = base_vol * last_price
|
| 207 |
|
| 208 |
+
# استثناء العملات السيادية من شرط الفوليوم (دائماً مقبولة)
|
| 209 |
is_sovereign = symbol in SOVEREIGN_COINS
|
| 210 |
|
| 211 |
if not is_sovereign:
|
|
|
|
| 216 |
change_pct = ticker.get('percentage')
|
| 217 |
if change_pct is None: change_pct = 0.0
|
| 218 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
candidates.append({
|
| 220 |
'symbol': symbol,
|
| 221 |
'quote_volume': calc_quote_vol,
|
|
|
|
| 223 |
'change_24h': change_pct
|
| 224 |
})
|
| 225 |
|
| 226 |
+
# ترتيب القائمة حسب الفوليوم لضمان معالجة الأهم أولاً
|
| 227 |
candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
|
| 228 |
+
print(f" ℹ️ [Stage 0] Ignored {reject_stats['volume']} low-vol coins. Found {len(candidates)} candidates.")
|
| 229 |
return candidates
|
| 230 |
|
| 231 |
except Exception as e:
|
|
|
|
| 234 |
return []
|
| 235 |
|
| 236 |
# ------------------------------------------------------------------
|
| 237 |
+
# 🧭 The Diagnoser (For Context Only)
|
| 238 |
# ------------------------------------------------------------------
|
| 239 |
def _diagnose_asset_regime(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
| 240 |
+
"""تشخيص حالة العملة الفردية (للعلم فقط)"""
|
| 241 |
try:
|
| 242 |
if 'df_1h' not in item:
|
| 243 |
if 'ohlcv_1h_raw' in item:
|
|
|
|
| 253 |
ema20 = curr['ema20']
|
| 254 |
ema50 = curr['ema50']
|
| 255 |
rsi = curr['rsi']
|
|
|
|
|
|
|
| 256 |
|
| 257 |
regime = "RANGE"
|
| 258 |
conf = 0.5
|
| 259 |
|
| 260 |
+
if price > ema20 and ema20 > ema50:
|
|
|
|
|
|
|
| 261 |
regime = "BULL"
|
| 262 |
conf = 0.8 if rsi > 55 else 0.6
|
| 263 |
+
elif price < ema20 and ema20 < ema50:
|
| 264 |
regime = "BEAR"
|
| 265 |
conf = 0.8 if rsi < 45 else 0.6
|
| 266 |
|
|
|
|
| 268 |
except Exception: return {'regime': 'RANGE', 'conf': 0.0}
|
| 269 |
|
| 270 |
# ------------------------------------------------------------------
|
| 271 |
+
# Helpers & Data Fetching
|
| 272 |
# ------------------------------------------------------------------
|
| 273 |
async def _fetch_technical_data_batch(self, candidates):
|
| 274 |
"""جلب البيانات الفنية (1h, 15m) على دفعات"""
|
|
|
|
| 283 |
|
| 284 |
async def _fetch_single(self, c):
|
| 285 |
try:
|
| 286 |
+
# نحتاج هذه البيانات للمراحل القادمة (Processor)
|
| 287 |
h1 = await self.exchange.fetch_ohlcv(c['symbol'], '1h', limit=100)
|
| 288 |
m15 = await self.exchange.fetch_ohlcv(c['symbol'], '15m', limit=50)
|
| 289 |
if not h1 or not m15: return None
|
|
|
|
| 313 |
tr = np.maximum(df['h']-df['l'], np.maximum(abs(df['h']-df['c'].shift()), abs(df['l']-df['c'].shift())))
|
| 314 |
df['atr'] = tr.rolling(14).mean()
|
| 315 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 316 |
df.rename(columns={'o':'open', 'h':'high', 'l':'low', 'c':'close', 'v':'volume'}, inplace=True)
|
| 317 |
return df.fillna(0)
|
| 318 |
|
|
|
|
| 322 |
|
| 323 |
async def get_latest_ohlcv(self, symbol, timeframe='5m', limit=100):
|
| 324 |
try: return await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
|
| 325 |
+
except: return []
|
|
|
|
|
|
|
|
|
|
|
|