Spaces:
Paused
Paused
Update ml_engine/data_manager.py
Browse files- ml_engine/data_manager.py +44 -112
ml_engine/data_manager.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# ============================================================
|
| 2 |
# 📂 ml_engine/data_manager.py
|
| 3 |
-
# (
|
| 4 |
# ============================================================
|
| 5 |
|
| 6 |
import asyncio
|
|
@@ -38,13 +38,13 @@ class DataManager:
|
|
| 38 |
self.http_client = None
|
| 39 |
self.market_cache = {}
|
| 40 |
|
| 41 |
-
# القائمة السوداء
|
| 42 |
self.BLACKLIST_TOKENS = [
|
| 43 |
'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
|
| 44 |
'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L'
|
| 45 |
]
|
| 46 |
|
| 47 |
-
print(f"📦 [DataManager
|
| 48 |
|
| 49 |
async def initialize(self):
|
| 50 |
print(" > [DataManager] Starting initialization...")
|
|
@@ -77,15 +77,15 @@ class DataManager:
|
|
| 77 |
# ==================================================================
|
| 78 |
async def layer1_rapid_screening(self, adaptive_hub_ref=None) -> List[Dict[str, Any]]:
|
| 79 |
self.adaptive_hub_ref = adaptive_hub_ref
|
| 80 |
-
print(f"🔍 [Layer 1] Screening for
|
| 81 |
|
| 82 |
-
# 1. فلتر السيولة الأساسي
|
| 83 |
initial_candidates = await self._stage0_universe_filter()
|
| 84 |
if not initial_candidates:
|
| 85 |
print("⚠️ [Layer 1] Stage 0 returned 0 candidates.")
|
| 86 |
return []
|
| 87 |
|
| 88 |
-
# 2. جلب البيانات الفنية
|
| 89 |
top_candidates = initial_candidates[:600]
|
| 90 |
enriched_data = await self._fetch_technical_data_batch(top_candidates)
|
| 91 |
|
|
@@ -96,81 +96,65 @@ class DataManager:
|
|
| 96 |
classification = self._classify_opportunity_type(item)
|
| 97 |
|
| 98 |
if classification['type'] != 'NONE':
|
| 99 |
-
# تشخيص حالة السوق
|
| 100 |
regime_info = self._diagnose_asset_regime(item)
|
| 101 |
item['asset_regime'] = regime_info['regime']
|
| 102 |
item['asset_regime_conf'] = regime_info['conf']
|
| 103 |
|
| 104 |
-
# تخزين نوع الاستراتيجية والنتيجة الأولية
|
| 105 |
item['strategy_type'] = classification['type']
|
| 106 |
item['l1_sort_score'] = classification['score']
|
| 107 |
item['strategy_tag'] = classification['type']
|
| 108 |
|
| 109 |
-
# فحص الـ Regime
|
| 110 |
if regime_info['regime'] == 'DEAD' and classification['type'] == 'MOMENTUM_LAUNCH':
|
| 111 |
-
# استثناء: إذا كان "Squeeze" (تجميع)، نسمح بمروره حتى لو السوق ميت
|
| 112 |
if not classification.get('is_squeeze', False):
|
| 113 |
continue
|
| 114 |
|
| 115 |
semi_final_list.append(item)
|
| 116 |
|
| 117 |
-
# 4. 🧱 فحص عمق السوق (Order Book Check)
|
| 118 |
-
# هذا هو "مصدر الكشف" الجديد الذي طلبته
|
| 119 |
final_list = []
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
# نأخذ أفضل 50 مرشحاً فقط لفحص دفتر الطلبات لتجنب حظر API
|
| 123 |
semi_final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
|
| 124 |
-
candidates_for_depth = semi_final_list[:
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
for item in candidates_for_depth:
|
| 127 |
-
# إذا كانت تجميع (Squeeze) أو قاع، نفحص هل هناك دعم حقيقي في الأسفل؟
|
| 128 |
if item['strategy_type'] in ['ACCUMULATION_SQUEEZE', 'SAFE_BOTTOM']:
|
| 129 |
try:
|
| 130 |
-
# حساب نطاق الساعتين القادمتين (2 * ATR)
|
| 131 |
atr_val = item.get('atr_value', 0.0)
|
| 132 |
curr_price = item.get('current_price', 0.0)
|
| 133 |
|
| 134 |
if atr_val > 0 and curr_price > 0:
|
| 135 |
range_2h = atr_val * 2.0
|
| 136 |
-
# فحص ضغط دفتر الطلبات في هذا النطاق
|
| 137 |
ob_score = await self._check_ob_pressure(item['symbol'], curr_price, range_2h)
|
| 138 |
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
item['l1_sort_score'] += 0.15 # بونص
|
| 142 |
item['note'] = f"Strong Depth Support ({ob_score:.2f})"
|
| 143 |
-
elif ob_score < 0.4:
|
| 144 |
-
item['l1_sort_score'] -= 0.10
|
| 145 |
except Exception: pass
|
| 146 |
|
| 147 |
-
# حقن الإعدادات الديناميكية
|
| 148 |
if self.adaptive_hub_ref:
|
| 149 |
dynamic_config = self.adaptive_hub_ref.get_regime_config(item['asset_regime'])
|
| 150 |
item['dynamic_limits'] = dynamic_config
|
| 151 |
|
| 152 |
final_list.append(item)
|
| 153 |
|
| 154 |
-
# 5. الترتيب النهائي والإرجاع
|
| 155 |
final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
|
| 156 |
-
selection = final_list[:
|
| 157 |
|
| 158 |
-
print(f"✅ [Layer 1] Passed {len(selection)} candidates
|
| 159 |
return selection
|
| 160 |
|
| 161 |
# ==================================================================
|
| 162 |
-
# 🧱 Order Book Depth Scanner
|
| 163 |
# ==================================================================
|
| 164 |
async def _check_ob_pressure(self, symbol: str, current_price: float, price_range: float) -> float:
|
| 165 |
-
"""
|
| 166 |
-
يفحص دفتر الطلبات داخل النطاق السعري المتوقع للساعتين القادمتين.
|
| 167 |
-
النطاق = السعر الحالي +/- (ATR_1H * 2)
|
| 168 |
-
يعيد نسبة قوة المشترين (0.0 إلى 1.0).
|
| 169 |
-
"""
|
| 170 |
try:
|
| 171 |
-
# جلب لقطة سريعة لدفتر الطلبات (50 مستوى كافية للتجميع القريب)
|
| 172 |
ob = await self.exchange.fetch_order_book(symbol, limit=50)
|
| 173 |
-
|
| 174 |
bids = ob['bids']
|
| 175 |
asks = ob['asks']
|
| 176 |
|
|
@@ -180,43 +164,29 @@ class DataManager:
|
|
| 180 |
support_vol = 0.0
|
| 181 |
resistance_vol = 0.0
|
| 182 |
|
| 183 |
-
# تجميع طلبات الشراء داخل نطاق الدعم المتوقع
|
| 184 |
for p, v in bids:
|
| 185 |
-
if p >= min_price:
|
| 186 |
-
|
| 187 |
-
else: break # بما أن Bids مرتبة تنازلياً
|
| 188 |
|
| 189 |
-
# تجميع طلبات البيع داخل نطاق المقاومة المتوقع
|
| 190 |
for p, v in asks:
|
| 191 |
-
if p <= max_price:
|
| 192 |
-
|
| 193 |
-
else: break # بما أن Asks مرتبة تصاعدياً
|
| 194 |
|
| 195 |
if (support_vol + resistance_vol) == 0: return 0.5
|
| 196 |
-
|
| 197 |
-
# نسبة ضغط الشراء (Imbalance)
|
| 198 |
-
pressure_ratio = support_vol / (support_vol + resistance_vol)
|
| 199 |
-
return pressure_ratio
|
| 200 |
-
|
| 201 |
except Exception:
|
| 202 |
-
return 0.5
|
| 203 |
|
| 204 |
# ==================================================================
|
| 205 |
-
# ⚖️ The Dual-Classifier Logic
|
| 206 |
# ==================================================================
|
| 207 |
def _classify_opportunity_type(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
| 208 |
-
"""
|
| 209 |
-
تم التعديل ليشمل منطق التجميع (RSI 45-60) وفلتر الحياة (ATR).
|
| 210 |
-
"""
|
| 211 |
try:
|
| 212 |
df_1h = self._calc_indicators(data['ohlcv_1h_raw'])
|
| 213 |
curr = df_1h.iloc[-1]
|
| 214 |
-
|
| 215 |
-
# تخزين ATR لاستخدامه لاحقاً في فحص دفتر الطلبات
|
| 216 |
data['atr_value'] = curr['atr']
|
| 217 |
except: return {'type': 'NONE', 'score': 0}
|
| 218 |
|
| 219 |
-
# --- المؤشرات الأساسية ---
|
| 220 |
rsi = curr['rsi']
|
| 221 |
close = curr['close']
|
| 222 |
ema20 = curr['ema20']
|
|
@@ -224,40 +194,29 @@ class DataManager:
|
|
| 224 |
ema200 = curr['ema200'] if 'ema200' in curr else ema50
|
| 225 |
atr = curr['atr']
|
| 226 |
|
| 227 |
-
# Bollinger Bands
|
| 228 |
lower_bb = curr['lower_bb'] if 'lower_bb' in curr else (curr['ema20'] - (2*curr['atr']))
|
| 229 |
upper_bb = curr['upper_bb'] if 'upper_bb' in curr else (curr['ema20'] + (2*curr['atr']))
|
| 230 |
bb_width = (upper_bb - lower_bb) / curr['ema20'] if curr['ema20'] > 0 else 1.0
|
| 231 |
|
| 232 |
-
# 🔥 1. Dead Coin Filter (فلتر
|
| 233 |
-
# نحسب نسبة الـ ATR للسعر. إذا كانت الحركة أقل من 0.4% في الساعة، نرفضها.
|
| 234 |
volatility_pct = (atr / close) * 100 if close > 0 else 0
|
| 235 |
-
if volatility_pct < 0.4:
|
| 236 |
-
return {'type': 'NONE', 'score': 0}
|
| 237 |
|
| 238 |
-
# 🛡️ TYPE 1: SAFE_BOTTOM
|
| 239 |
if rsi < 45:
|
| 240 |
dist_from_ema = (ema50 - close) / ema50
|
| 241 |
-
# السماح بمسافة أكبر قليلاً عن الخط السفلي (5%)
|
| 242 |
if close <= lower_bb * 1.05 and dist_from_ema > 0.015:
|
| 243 |
score = (55 - rsi) / 20.0
|
| 244 |
return {'type': 'SAFE_BOTTOM', 'score': min(score, 1.0)}
|
| 245 |
|
| 246 |
-
# 🔋 TYPE 2: ACCUMULATION_SQUEEZE
|
| 247 |
-
# المنطق: RSI محايد (45-60) + قناة ضيقة (Squeeze) + دعم دفتر الطلبات (سيفحص لاحقاً)
|
| 248 |
elif 45 <= rsi <= 60:
|
| 249 |
-
if bb_width < 0.12:
|
| 250 |
-
|
| 251 |
-
if close > ema20 * 0.995: # السماح بكسر وهمي بسيط
|
| 252 |
score = 1.0 - (bb_width * 4.0)
|
| 253 |
-
|
| 254 |
-
return {
|
| 255 |
-
'type': 'ACCUMULATION_SQUEEZE',
|
| 256 |
-
'score': max(score, 0.5),
|
| 257 |
-
'is_squeeze': True
|
| 258 |
-
}
|
| 259 |
|
| 260 |
-
# 🚀 TYPE 3: MOMENTUM_LAUNCH
|
| 261 |
elif 60 < rsi < 80:
|
| 262 |
if close > ema50 and close > ema200:
|
| 263 |
dist_to_upper = (upper_bb - close) / close
|
|
@@ -268,16 +227,18 @@ class DataManager:
|
|
| 268 |
return {'type': 'NONE', 'score': 0}
|
| 269 |
|
| 270 |
# ==================================================================
|
| 271 |
-
# 🔍 Stage 0: Universe Filter (
|
| 272 |
# ==================================================================
|
| 273 |
async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
|
| 274 |
try:
|
| 275 |
-
|
|
|
|
|
|
|
|
|
|
| 276 |
tickers = await self.exchange.fetch_tickers()
|
| 277 |
candidates = []
|
| 278 |
|
| 279 |
SOVEREIGN_COINS = ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT']
|
| 280 |
-
|
| 281 |
reject_stats = {"volume": 0, "change": 0, "blacklist": 0}
|
| 282 |
|
| 283 |
for symbol, ticker in tickers.items():
|
|
@@ -294,9 +255,9 @@ class DataManager:
|
|
| 294 |
|
| 295 |
is_sovereign = symbol in SOVEREIGN_COINS
|
| 296 |
|
| 297 |
-
#
|
| 298 |
if not is_sovereign:
|
| 299 |
-
if calc_quote_vol <
|
| 300 |
reject_stats["volume"] += 1
|
| 301 |
continue
|
| 302 |
|
|
@@ -315,6 +276,7 @@ class DataManager:
|
|
| 315 |
})
|
| 316 |
|
| 317 |
candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
|
|
|
|
| 318 |
return candidates
|
| 319 |
|
| 320 |
except Exception as e:
|
|
@@ -322,36 +284,6 @@ class DataManager:
|
|
| 322 |
traceback.print_exc()
|
| 323 |
return []
|
| 324 |
|
| 325 |
-
# ------------------------------------------------------------------
|
| 326 |
-
# 🧭 The Diagnoser
|
| 327 |
-
# ------------------------------------------------------------------
|
| 328 |
-
def _diagnose_asset_regime(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
| 329 |
-
try:
|
| 330 |
-
if 'df_1h' not in item: return {'regime': 'RANGE', 'conf': 0.0}
|
| 331 |
-
df = item['df_1h']
|
| 332 |
-
curr = df.iloc[-1]
|
| 333 |
-
price = curr['close']
|
| 334 |
-
ema20 = curr['ema20']
|
| 335 |
-
ema50 = curr['ema50']
|
| 336 |
-
rsi = curr['rsi']
|
| 337 |
-
atr = curr['atr']
|
| 338 |
-
atr_pct = (atr / price) * 100 if price > 0 else 0
|
| 339 |
-
|
| 340 |
-
regime = "RANGE"
|
| 341 |
-
conf = 0.5
|
| 342 |
-
|
| 343 |
-
if atr_pct < 0.4: return {'regime': 'DEAD', 'conf': 0.9}
|
| 344 |
-
|
| 345 |
-
if price > ema20 and ema20 > ema50 and rsi > 50:
|
| 346 |
-
regime = "BULL"
|
| 347 |
-
conf = 0.8 if rsi > 55 else 0.6
|
| 348 |
-
elif price < ema20 and ema20 < ema50 and rsi < 50:
|
| 349 |
-
regime = "BEAR"
|
| 350 |
-
conf = 0.8 if rsi < 45 else 0.6
|
| 351 |
-
|
| 352 |
-
return {'regime': regime, 'conf': conf}
|
| 353 |
-
except Exception: return {'regime': 'RANGE', 'conf': 0.0}
|
| 354 |
-
|
| 355 |
# ------------------------------------------------------------------
|
| 356 |
# Helpers & Indicators
|
| 357 |
# ------------------------------------------------------------------
|
|
@@ -393,7 +325,7 @@ class DataManager:
|
|
| 393 |
tr = np.maximum(df['h']-df['l'], np.maximum(abs(df['h']-df['c'].shift()), abs(df['l']-df['c'].shift())))
|
| 394 |
df['atr'] = tr.rolling(14).mean()
|
| 395 |
|
| 396 |
-
# Bollinger Bands
|
| 397 |
std = df['c'].rolling(20).std()
|
| 398 |
df['upper_bb'] = df['ema20'] + (2 * std)
|
| 399 |
df['lower_bb'] = df['ema20'] - (2 * std)
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
# 📂 ml_engine/data_manager.py
|
| 3 |
+
# (V63.0 - GEM-Architect: High Volume Filter & Depth Scan)
|
| 4 |
# ============================================================
|
| 5 |
|
| 6 |
import asyncio
|
|
|
|
| 38 |
self.http_client = None
|
| 39 |
self.market_cache = {}
|
| 40 |
|
| 41 |
+
# القائمة السوداء
|
| 42 |
self.BLACKLIST_TOKENS = [
|
| 43 |
'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
|
| 44 |
'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L'
|
| 45 |
]
|
| 46 |
|
| 47 |
+
print(f"📦 [DataManager V63.0] High Liquidity (1M+) & Depth Scan Active.")
|
| 48 |
|
| 49 |
async def initialize(self):
|
| 50 |
print(" > [DataManager] Starting initialization...")
|
|
|
|
| 77 |
# ==================================================================
|
| 78 |
async def layer1_rapid_screening(self, adaptive_hub_ref=None) -> List[Dict[str, Any]]:
|
| 79 |
self.adaptive_hub_ref = adaptive_hub_ref
|
| 80 |
+
print(f"🔍 [Layer 1] Screening for High Vol Assets (Bottom/Acc/Mom)...")
|
| 81 |
|
| 82 |
+
# 1. فلتر السيولة الأساسي (الآن صارم: 1 مليون دولار)
|
| 83 |
initial_candidates = await self._stage0_universe_filter()
|
| 84 |
if not initial_candidates:
|
| 85 |
print("⚠️ [Layer 1] Stage 0 returned 0 candidates.")
|
| 86 |
return []
|
| 87 |
|
| 88 |
+
# 2. جلب البيانات الفنية
|
| 89 |
top_candidates = initial_candidates[:600]
|
| 90 |
enriched_data = await self._fetch_technical_data_batch(top_candidates)
|
| 91 |
|
|
|
|
| 96 |
classification = self._classify_opportunity_type(item)
|
| 97 |
|
| 98 |
if classification['type'] != 'NONE':
|
|
|
|
| 99 |
regime_info = self._diagnose_asset_regime(item)
|
| 100 |
item['asset_regime'] = regime_info['regime']
|
| 101 |
item['asset_regime_conf'] = regime_info['conf']
|
| 102 |
|
|
|
|
| 103 |
item['strategy_type'] = classification['type']
|
| 104 |
item['l1_sort_score'] = classification['score']
|
| 105 |
item['strategy_tag'] = classification['type']
|
| 106 |
|
| 107 |
+
# فحص الـ Regime
|
| 108 |
if regime_info['regime'] == 'DEAD' and classification['type'] == 'MOMENTUM_LAUNCH':
|
|
|
|
| 109 |
if not classification.get('is_squeeze', False):
|
| 110 |
continue
|
| 111 |
|
| 112 |
semi_final_list.append(item)
|
| 113 |
|
| 114 |
+
# 4. 🧱 فحص عمق السوق (Order Book Check)
|
|
|
|
| 115 |
final_list = []
|
| 116 |
+
# نأخذ أفضل 50 مرشحاً لفحص دفتر الطلبات
|
|
|
|
|
|
|
| 117 |
semi_final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
|
| 118 |
+
candidates_for_depth = semi_final_list[:50]
|
| 119 |
+
|
| 120 |
+
if candidates_for_depth:
|
| 121 |
+
print(f" 🛡️ [Layer 1.5] Checking Depth Support for {len(candidates_for_depth)} candidates...")
|
| 122 |
|
| 123 |
for item in candidates_for_depth:
|
|
|
|
| 124 |
if item['strategy_type'] in ['ACCUMULATION_SQUEEZE', 'SAFE_BOTTOM']:
|
| 125 |
try:
|
|
|
|
| 126 |
atr_val = item.get('atr_value', 0.0)
|
| 127 |
curr_price = item.get('current_price', 0.0)
|
| 128 |
|
| 129 |
if atr_val > 0 and curr_price > 0:
|
| 130 |
range_2h = atr_val * 2.0
|
|
|
|
| 131 |
ob_score = await self._check_ob_pressure(item['symbol'], curr_price, range_2h)
|
| 132 |
|
| 133 |
+
if ob_score > 0.6:
|
| 134 |
+
item['l1_sort_score'] += 0.15
|
|
|
|
| 135 |
item['note'] = f"Strong Depth Support ({ob_score:.2f})"
|
| 136 |
+
elif ob_score < 0.4:
|
| 137 |
+
item['l1_sort_score'] -= 0.10
|
| 138 |
except Exception: pass
|
| 139 |
|
|
|
|
| 140 |
if self.adaptive_hub_ref:
|
| 141 |
dynamic_config = self.adaptive_hub_ref.get_regime_config(item['asset_regime'])
|
| 142 |
item['dynamic_limits'] = dynamic_config
|
| 143 |
|
| 144 |
final_list.append(item)
|
| 145 |
|
|
|
|
| 146 |
final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
|
| 147 |
+
selection = final_list[:60]
|
| 148 |
|
| 149 |
+
print(f"✅ [Layer 1] Passed {len(selection)} active candidates.")
|
| 150 |
return selection
|
| 151 |
|
| 152 |
# ==================================================================
|
| 153 |
+
# 🧱 Order Book Depth Scanner
|
| 154 |
# ==================================================================
|
| 155 |
async def _check_ob_pressure(self, symbol: str, current_price: float, price_range: float) -> float:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
try:
|
|
|
|
| 157 |
ob = await self.exchange.fetch_order_book(symbol, limit=50)
|
|
|
|
| 158 |
bids = ob['bids']
|
| 159 |
asks = ob['asks']
|
| 160 |
|
|
|
|
| 164 |
support_vol = 0.0
|
| 165 |
resistance_vol = 0.0
|
| 166 |
|
|
|
|
| 167 |
for p, v in bids:
|
| 168 |
+
if p >= min_price: support_vol += v
|
| 169 |
+
else: break
|
|
|
|
| 170 |
|
|
|
|
| 171 |
for p, v in asks:
|
| 172 |
+
if p <= max_price: resistance_vol += v
|
| 173 |
+
else: break
|
|
|
|
| 174 |
|
| 175 |
if (support_vol + resistance_vol) == 0: return 0.5
|
| 176 |
+
return support_vol / (support_vol + resistance_vol)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
except Exception:
|
| 178 |
+
return 0.5
|
| 179 |
|
| 180 |
# ==================================================================
|
| 181 |
+
# ⚖️ The Dual-Classifier Logic
|
| 182 |
# ==================================================================
|
| 183 |
def _classify_opportunity_type(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
| 184 |
try:
|
| 185 |
df_1h = self._calc_indicators(data['ohlcv_1h_raw'])
|
| 186 |
curr = df_1h.iloc[-1]
|
|
|
|
|
|
|
| 187 |
data['atr_value'] = curr['atr']
|
| 188 |
except: return {'type': 'NONE', 'score': 0}
|
| 189 |
|
|
|
|
| 190 |
rsi = curr['rsi']
|
| 191 |
close = curr['close']
|
| 192 |
ema20 = curr['ema20']
|
|
|
|
| 194 |
ema200 = curr['ema200'] if 'ema200' in curr else ema50
|
| 195 |
atr = curr['atr']
|
| 196 |
|
|
|
|
| 197 |
lower_bb = curr['lower_bb'] if 'lower_bb' in curr else (curr['ema20'] - (2*curr['atr']))
|
| 198 |
upper_bb = curr['upper_bb'] if 'upper_bb' in curr else (curr['ema20'] + (2*curr['atr']))
|
| 199 |
bb_width = (upper_bb - lower_bb) / curr['ema20'] if curr['ema20'] > 0 else 1.0
|
| 200 |
|
| 201 |
+
# 🔥 1. Dead Coin Filter (فلتر النبض)
|
|
|
|
| 202 |
volatility_pct = (atr / close) * 100 if close > 0 else 0
|
| 203 |
+
if volatility_pct < 0.4: return {'type': 'NONE', 'score': 0}
|
|
|
|
| 204 |
|
| 205 |
+
# 🛡️ TYPE 1: SAFE_BOTTOM
|
| 206 |
if rsi < 45:
|
| 207 |
dist_from_ema = (ema50 - close) / ema50
|
|
|
|
| 208 |
if close <= lower_bb * 1.05 and dist_from_ema > 0.015:
|
| 209 |
score = (55 - rsi) / 20.0
|
| 210 |
return {'type': 'SAFE_BOTTOM', 'score': min(score, 1.0)}
|
| 211 |
|
| 212 |
+
# 🔋 TYPE 2: ACCUMULATION_SQUEEZE
|
|
|
|
| 213 |
elif 45 <= rsi <= 60:
|
| 214 |
+
if bb_width < 0.12:
|
| 215 |
+
if close > ema20 * 0.995:
|
|
|
|
| 216 |
score = 1.0 - (bb_width * 4.0)
|
| 217 |
+
return {'type': 'ACCUMULATION_SQUEEZE', 'score': max(score, 0.5), 'is_squeeze': True}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
|
| 219 |
+
# 🚀 TYPE 3: MOMENTUM_LAUNCH
|
| 220 |
elif 60 < rsi < 80:
|
| 221 |
if close > ema50 and close > ema200:
|
| 222 |
dist_to_upper = (upper_bb - close) / close
|
|
|
|
| 227 |
return {'type': 'NONE', 'score': 0}
|
| 228 |
|
| 229 |
# ==================================================================
|
| 230 |
+
# 🔍 Stage 0: Universe Filter (STRICT 1M FILTER)
|
| 231 |
# ==================================================================
|
| 232 |
async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
|
| 233 |
try:
|
| 234 |
+
# 🔥 إعداد الحد الأدنى لحجم التداول (يمكنك تغييره إلى 5 مليون من هنا)
|
| 235 |
+
MIN_VOLUME_THRESHOLD = 1000000.0 # 1 Million USDT
|
| 236 |
+
|
| 237 |
+
print(f" 🛡️ [Stage 0] Fetching Tickers (Min Vol: ${MIN_VOLUME_THRESHOLD:,.0f})...")
|
| 238 |
tickers = await self.exchange.fetch_tickers()
|
| 239 |
candidates = []
|
| 240 |
|
| 241 |
SOVEREIGN_COINS = ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT']
|
|
|
|
| 242 |
reject_stats = {"volume": 0, "change": 0, "blacklist": 0}
|
| 243 |
|
| 244 |
for symbol, ticker in tickers.items():
|
|
|
|
| 255 |
|
| 256 |
is_sovereign = symbol in SOVEREIGN_COINS
|
| 257 |
|
| 258 |
+
# 🔥 الفلتر الصارم الجديد: رفض أي عملة تحت المليون
|
| 259 |
if not is_sovereign:
|
| 260 |
+
if calc_quote_vol < MIN_VOLUME_THRESHOLD:
|
| 261 |
reject_stats["volume"] += 1
|
| 262 |
continue
|
| 263 |
|
|
|
|
| 276 |
})
|
| 277 |
|
| 278 |
candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
|
| 279 |
+
print(f" ℹ️ [Stage 0] Ignored {reject_stats['volume']} low-vol coins.")
|
| 280 |
return candidates
|
| 281 |
|
| 282 |
except Exception as e:
|
|
|
|
| 284 |
traceback.print_exc()
|
| 285 |
return []
|
| 286 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 287 |
# ------------------------------------------------------------------
|
| 288 |
# Helpers & Indicators
|
| 289 |
# ------------------------------------------------------------------
|
|
|
|
| 325 |
tr = np.maximum(df['h']-df['l'], np.maximum(abs(df['h']-df['c'].shift()), abs(df['l']-df['c'].shift())))
|
| 326 |
df['atr'] = tr.rolling(14).mean()
|
| 327 |
|
| 328 |
+
# Bollinger Bands
|
| 329 |
std = df['c'].rolling(20).std()
|
| 330 |
df['upper_bb'] = df['ema20'] + (2 * std)
|
| 331 |
df['lower_bb'] = df['ema20'] - (2 * std)
|