Spaces:
Paused
Paused
Update ml_engine/data_manager.py
Browse files- ml_engine/data_manager.py +149 -60
ml_engine/data_manager.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
# 📂 ml_engine/data_manager.py
|
| 3 |
-
# (
|
| 4 |
# ============================================================
|
| 5 |
|
| 6 |
import asyncio
|
|
@@ -10,7 +14,7 @@ import ccxt.async_support as ccxt
|
|
| 10 |
import logging
|
| 11 |
import pandas as pd
|
| 12 |
import numpy as np
|
| 13 |
-
from typing import List, Dict, Any
|
| 14 |
|
| 15 |
# محاولة استيراد حدود النظام
|
| 16 |
try:
|
|
@@ -44,7 +48,7 @@ class DataManager:
|
|
| 44 |
'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L'
|
| 45 |
]
|
| 46 |
|
| 47 |
-
print(f"📦 [DataManager
|
| 48 |
|
| 49 |
async def initialize(self):
|
| 50 |
print(" > [DataManager] Starting initialization...")
|
|
@@ -73,108 +77,195 @@ class DataManager:
|
|
| 73 |
return self.contracts_db
|
| 74 |
|
| 75 |
# ==================================================================
|
| 76 |
-
# 🧠 Layer 1: Classification (Bottom
|
| 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 |
-
# نأخذ أكبر عدد ممكن بعد التخفيف لزيادة الفرص
|
| 90 |
top_candidates = initial_candidates[:600]
|
| 91 |
enriched_data = await self._fetch_technical_data_batch(top_candidates)
|
| 92 |
|
| 93 |
-
|
| 94 |
|
|
|
|
| 95 |
for item in enriched_data:
|
| 96 |
-
# 3. التصنيف الجديد: قاع آمن أم انفجار وشيك؟
|
| 97 |
classification = self._classify_opportunity_type(item)
|
| 98 |
|
| 99 |
if classification['type'] != 'NONE':
|
| 100 |
-
# تشخيص حالة السوق
|
| 101 |
regime_info = self._diagnose_asset_regime(item)
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
# 🔥 Regime Gating: الحماية من المصائد
|
| 105 |
-
# إذا السوق عرضي (RANGE) أو ميت (DEAD)، نمنع صفقات الزخم (MOMENTUM) لأنها غالباً مصائد
|
| 106 |
-
if current_regime in ['RANGE', 'DEAD'] and classification['type'] == 'MOMENTUM_LAUNCH':
|
| 107 |
-
continue
|
| 108 |
-
|
| 109 |
-
# إضافة البيانات
|
| 110 |
-
item['asset_regime'] = current_regime
|
| 111 |
item['asset_regime_conf'] = regime_info['conf']
|
| 112 |
|
| 113 |
-
#
|
| 114 |
item['strategy_type'] = classification['type']
|
| 115 |
item['l1_sort_score'] = classification['score']
|
| 116 |
-
|
| 117 |
-
# حقن العتبات الديناميكية
|
| 118 |
-
if self.adaptive_hub_ref:
|
| 119 |
-
dynamic_config = self.adaptive_hub_ref.get_regime_config(current_regime)
|
| 120 |
-
item['dynamic_limits'] = dynamic_config
|
| 121 |
-
|
| 122 |
-
# حفظ التاج القديم للتوافق
|
| 123 |
item['strategy_tag'] = classification['type']
|
| 124 |
|
| 125 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
-
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
selection = final_list[:60]
|
| 132 |
-
|
|
|
|
| 133 |
return selection
|
| 134 |
|
| 135 |
# ==================================================================
|
| 136 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
# ==================================================================
|
| 138 |
def _classify_opportunity_type(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
| 139 |
"""
|
| 140 |
-
تم
|
| 141 |
"""
|
| 142 |
try:
|
| 143 |
df_1h = self._calc_indicators(data['ohlcv_1h_raw'])
|
| 144 |
curr = df_1h.iloc[-1]
|
|
|
|
|
|
|
|
|
|
| 145 |
except: return {'type': 'NONE', 'score': 0}
|
| 146 |
|
| 147 |
# --- المؤشرات الأساسية ---
|
| 148 |
rsi = curr['rsi']
|
| 149 |
close = curr['close']
|
|
|
|
| 150 |
ema50 = curr['ema50']
|
| 151 |
ema200 = curr['ema200'] if 'ema200' in curr else ema50
|
|
|
|
| 152 |
|
| 153 |
-
# Bollinger Bands
|
| 154 |
lower_bb = curr['lower_bb'] if 'lower_bb' in curr else (curr['ema20'] - (2*curr['atr']))
|
| 155 |
upper_bb = curr['upper_bb'] if 'upper_bb' in curr else (curr['ema20'] + (2*curr['atr']))
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
#
|
| 159 |
-
|
| 160 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 161 |
dist_from_ema = (ema50 - close) / ema50
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
# Score adjustment
|
| 166 |
-
score = (50 - rsi) / 20.0
|
| 167 |
return {'type': 'SAFE_BOTTOM', 'score': min(score, 1.0)}
|
| 168 |
|
| 169 |
-
#
|
| 170 |
-
#
|
| 171 |
-
elif
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
if close > ema50 and close > ema200:
|
| 173 |
-
# السعر يضغط قرب الحد العلوي
|
| 174 |
dist_to_upper = (upper_bb - close) / close
|
| 175 |
-
|
| 176 |
-
# توسيع مجال الـ Squeeze المسموح به إلى 6% بدلاً من 3%
|
| 177 |
-
if dist_to_upper < 0.06:
|
| 178 |
score = rsi / 100.0
|
| 179 |
return {'type': 'MOMENTUM_LAUNCH', 'score': score}
|
| 180 |
|
|
@@ -185,7 +276,7 @@ class DataManager:
|
|
| 185 |
# ==================================================================
|
| 186 |
async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
|
| 187 |
try:
|
| 188 |
-
print(" 🛡️ [Stage 0] Fetching Tickers (
|
| 189 |
tickers = await self.exchange.fetch_tickers()
|
| 190 |
candidates = []
|
| 191 |
|
|
@@ -201,21 +292,18 @@ class DataManager:
|
|
| 201 |
reject_stats["blacklist"] += 1
|
| 202 |
continue
|
| 203 |
|
| 204 |
-
# حساب الحجم
|
| 205 |
base_vol = float(ticker.get('baseVolume') or 0.0)
|
| 206 |
last_price = float(ticker.get('last') or 0.0)
|
| 207 |
calc_quote_vol = base_vol * last_price
|
| 208 |
|
| 209 |
is_sovereign = symbol in SOVEREIGN_COINS
|
| 210 |
|
| 211 |
-
#
|
| 212 |
-
# ��ذا يسمح باكتشاف الجواهر قبل ارتفاعها الكبير
|
| 213 |
if not is_sovereign:
|
| 214 |
if calc_quote_vol < 150000:
|
| 215 |
reject_stats["volume"] += 1
|
| 216 |
continue
|
| 217 |
|
| 218 |
-
# ⬇️ تخفيف فلتر التذبذب: من 25% إلى 35%
|
| 219 |
change_pct = ticker.get('percentage')
|
| 220 |
if change_pct is None: change_pct = 0.0
|
| 221 |
|
|
@@ -256,7 +344,7 @@ class DataManager:
|
|
| 256 |
regime = "RANGE"
|
| 257 |
conf = 0.5
|
| 258 |
|
| 259 |
-
if atr_pct < 0.
|
| 260 |
|
| 261 |
if price > ema20 and ema20 > ema50 and rsi > 50:
|
| 262 |
regime = "BULL"
|
|
@@ -327,4 +415,5 @@ class DataManager:
|
|
| 327 |
|
| 328 |
async def get_order_book_snapshot(self, symbol, limit=20):
|
| 329 |
try: return await self.exchange.fetch_order_book(symbol, limit)
|
| 330 |
-
except: return {}
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
type: uploaded file
|
| 3 |
+
fileName: data_manager.py
|
| 4 |
+
fullContent:
|
| 5 |
# ============================================================
|
| 6 |
# 📂 ml_engine/data_manager.py
|
| 7 |
+
# (V62.0 - GEM-Architect: Accumulation Depth & Vitality Check)
|
| 8 |
# ============================================================
|
| 9 |
|
| 10 |
import asyncio
|
|
|
|
| 14 |
import logging
|
| 15 |
import pandas as pd
|
| 16 |
import numpy as np
|
| 17 |
+
from typing import List, Dict, Any, Optional
|
| 18 |
|
| 19 |
# محاولة استيراد حدود النظام
|
| 20 |
try:
|
|
|
|
| 48 |
'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L'
|
| 49 |
]
|
| 50 |
|
| 51 |
+
print(f"📦 [DataManager V62.0] Accumulation Depth & Vitality Check Active.")
|
| 52 |
|
| 53 |
async def initialize(self):
|
| 54 |
print(" > [DataManager] Starting initialization...")
|
|
|
|
| 77 |
return self.contracts_db
|
| 78 |
|
| 79 |
# ==================================================================
|
| 80 |
+
# 🧠 Layer 1: Classification (Bottom, Momentum, Accumulation)
|
| 81 |
# ==================================================================
|
| 82 |
async def layer1_rapid_screening(self, adaptive_hub_ref=None) -> List[Dict[str, Any]]:
|
| 83 |
self.adaptive_hub_ref = adaptive_hub_ref
|
| 84 |
+
print(f"🔍 [Layer 1] Screening for Safe Bottoms, Accumulation & Momentum...")
|
| 85 |
|
| 86 |
+
# 1. فلتر السيولة الأساسي
|
| 87 |
initial_candidates = await self._stage0_universe_filter()
|
| 88 |
if not initial_candidates:
|
| 89 |
print("⚠️ [Layer 1] Stage 0 returned 0 candidates.")
|
| 90 |
return []
|
| 91 |
|
| 92 |
+
# 2. جلب البيانات الفنية (دفعة أكبر لزيادة الفرص)
|
|
|
|
| 93 |
top_candidates = initial_candidates[:600]
|
| 94 |
enriched_data = await self._fetch_technical_data_batch(top_candidates)
|
| 95 |
|
| 96 |
+
semi_final_list = []
|
| 97 |
|
| 98 |
+
# 3. التصنيف الفني الأولي
|
| 99 |
for item in enriched_data:
|
|
|
|
| 100 |
classification = self._classify_opportunity_type(item)
|
| 101 |
|
| 102 |
if classification['type'] != 'NONE':
|
| 103 |
+
# تشخيص حالة السوق
|
| 104 |
regime_info = self._diagnose_asset_regime(item)
|
| 105 |
+
item['asset_regime'] = regime_info['regime']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
item['asset_regime_conf'] = regime_info['conf']
|
| 107 |
|
| 108 |
+
# تخزين نوع الاستراتيجية والنتيجة الأولية
|
| 109 |
item['strategy_type'] = classification['type']
|
| 110 |
item['l1_sort_score'] = classification['score']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
item['strategy_tag'] = classification['type']
|
| 112 |
|
| 113 |
+
# فحص الـ Regime (نرفض الانفجارات في الأسواق الميتة، لكن نقبل التجميع)
|
| 114 |
+
if regime_info['regime'] == 'DEAD' and classification['type'] == 'MOMENTUM_LAUNCH':
|
| 115 |
+
# استثناء: إذا كان "Squeeze" (تجميع)، نسمح بمروره حتى لو السوق ميت
|
| 116 |
+
if not classification.get('is_squeeze', False):
|
| 117 |
+
continue
|
| 118 |
|
| 119 |
+
semi_final_list.append(item)
|
| 120 |
+
|
| 121 |
+
# 4. 🧱 فحص عمق السوق (Order Book Check) للمرشحين للتجميع
|
| 122 |
+
# هذا هو "مصدر الكشف" الجديد الذي طلبته
|
| 123 |
+
final_list = []
|
| 124 |
+
print(f" 🛡️ [Layer 1.5] Checking Depth Support for {len(semi_final_list)} candidates...")
|
| 125 |
|
| 126 |
+
# نأخذ أفضل 50 مرشحاً فقط لفحص دفتر الطلبات لتجنب حظر API
|
| 127 |
+
semi_final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
|
| 128 |
+
candidates_for_depth = semi_final_list[:50]
|
| 129 |
+
|
| 130 |
+
for item in candidates_for_depth:
|
| 131 |
+
# إذا كانت تجميع (Squeeze) أو قاع، نفحص هل هناك دعم حقيقي في الأسفل؟
|
| 132 |
+
if item['strategy_type'] in ['ACCUMULATION_SQUEEZE', 'SAFE_BOTTOM']:
|
| 133 |
+
try:
|
| 134 |
+
# حساب نطاق الساعتين القادمتين (2 * ATR)
|
| 135 |
+
atr_val = item.get('atr_value', 0.0)
|
| 136 |
+
curr_price = item.get('current_price', 0.0)
|
| 137 |
+
|
| 138 |
+
if atr_val > 0 and curr_price > 0:
|
| 139 |
+
range_2h = atr_val * 2.0
|
| 140 |
+
# فحص ضغط دفتر الطلبات في هذا النطاق
|
| 141 |
+
ob_score = await self._check_ob_pressure(item['symbol'], curr_price, range_2h)
|
| 142 |
+
|
| 143 |
+
# تعزيز النتيجة بناءً على دعم دفتر الطلبات
|
| 144 |
+
if ob_score > 0.6: # دعم قوي
|
| 145 |
+
item['l1_sort_score'] += 0.15 # بونص
|
| 146 |
+
item['note'] = f"Strong Depth Support ({ob_score:.2f})"
|
| 147 |
+
elif ob_score < 0.4: # دعم ضعيف
|
| 148 |
+
item['l1_sort_score'] -= 0.10 # عقوبة
|
| 149 |
+
except Exception: pass
|
| 150 |
+
|
| 151 |
+
# حقن الإعدادات الديناميكية
|
| 152 |
+
if self.adaptive_hub_ref:
|
| 153 |
+
dynamic_config = self.adaptive_hub_ref.get_regime_config(item['asset_regime'])
|
| 154 |
+
item['dynamic_limits'] = dynamic_config
|
| 155 |
+
|
| 156 |
+
final_list.append(item)
|
| 157 |
+
|
| 158 |
+
# 5. الترتيب النهائي والإرجاع
|
| 159 |
+
final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
|
| 160 |
selection = final_list[:60]
|
| 161 |
+
|
| 162 |
+
print(f"✅ [Layer 1] Passed {len(selection)} candidates (With Depth Verification).")
|
| 163 |
return selection
|
| 164 |
|
| 165 |
# ==================================================================
|
| 166 |
+
# 🧱 Order Book Depth Scanner (2-Hour Range Logic)
|
| 167 |
+
# ==================================================================
|
| 168 |
+
async def _check_ob_pressure(self, symbol: str, current_price: float, price_range: float) -> float:
|
| 169 |
+
"""
|
| 170 |
+
يفحص دفتر الطلبات داخل النطاق السعري المتوقع للساعتين القادمتين.
|
| 171 |
+
النطاق = السعر الحالي +/- (ATR_1H * 2)
|
| 172 |
+
يعيد نسبة قوة المشترين (0.0 إلى 1.0).
|
| 173 |
+
"""
|
| 174 |
+
try:
|
| 175 |
+
# جلب لقطة سريعة لدفتر الطلبات (50 مستوى كافية للتجميع القريب)
|
| 176 |
+
ob = await self.exchange.fetch_order_book(symbol, limit=50)
|
| 177 |
+
|
| 178 |
+
bids = ob['bids']
|
| 179 |
+
asks = ob['asks']
|
| 180 |
+
|
| 181 |
+
min_price = current_price - price_range
|
| 182 |
+
max_price = current_price + price_range
|
| 183 |
+
|
| 184 |
+
support_vol = 0.0
|
| 185 |
+
resistance_vol = 0.0
|
| 186 |
+
|
| 187 |
+
# تجميع طلبات الشراء داخل نطاق الدعم المتوقع
|
| 188 |
+
for p, v in bids:
|
| 189 |
+
if p >= min_price:
|
| 190 |
+
support_vol += v
|
| 191 |
+
else: break # بما أن Bids مرتبة تنازلياً
|
| 192 |
+
|
| 193 |
+
# تجميع طلبات البيع داخل نطاق المقاومة المتوقع
|
| 194 |
+
for p, v in asks:
|
| 195 |
+
if p <= max_price:
|
| 196 |
+
resistance_vol += v
|
| 197 |
+
else: break # بما أن Asks مرتبة تصاعدياً
|
| 198 |
+
|
| 199 |
+
if (support_vol + resistance_vol) == 0: return 0.5
|
| 200 |
+
|
| 201 |
+
# نسبة ضغط الشراء (Imbalance)
|
| 202 |
+
pressure_ratio = support_vol / (support_vol + resistance_vol)
|
| 203 |
+
return pressure_ratio
|
| 204 |
+
|
| 205 |
+
except Exception:
|
| 206 |
+
return 0.5 # حياد في حال الفشل
|
| 207 |
+
|
| 208 |
+
# ==================================================================
|
| 209 |
+
# ⚖️ The Dual-Classifier Logic (GEM-Architect: ACCUMULATION UPGRADE)
|
| 210 |
# ==================================================================
|
| 211 |
def _classify_opportunity_type(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
| 212 |
"""
|
| 213 |
+
تم التعديل ليشمل منطق التجميع (RSI 45-60) وفلتر الحياة (ATR).
|
| 214 |
"""
|
| 215 |
try:
|
| 216 |
df_1h = self._calc_indicators(data['ohlcv_1h_raw'])
|
| 217 |
curr = df_1h.iloc[-1]
|
| 218 |
+
|
| 219 |
+
# تخزين ATR لاستخدامه لاحقاً في فحص دفتر الطلبات
|
| 220 |
+
data['atr_value'] = curr['atr']
|
| 221 |
except: return {'type': 'NONE', 'score': 0}
|
| 222 |
|
| 223 |
# --- المؤشرات الأساسية ---
|
| 224 |
rsi = curr['rsi']
|
| 225 |
close = curr['close']
|
| 226 |
+
ema20 = curr['ema20']
|
| 227 |
ema50 = curr['ema50']
|
| 228 |
ema200 = curr['ema200'] if 'ema200' in curr else ema50
|
| 229 |
+
atr = curr['atr']
|
| 230 |
|
| 231 |
+
# Bollinger Bands
|
| 232 |
lower_bb = curr['lower_bb'] if 'lower_bb' in curr else (curr['ema20'] - (2*curr['atr']))
|
| 233 |
upper_bb = curr['upper_bb'] if 'upper_bb' in curr else (curr['ema20'] + (2*curr['atr']))
|
| 234 |
+
bb_width = (upper_bb - lower_bb) / curr['ema20'] if curr['ema20'] > 0 else 1.0
|
| 235 |
+
|
| 236 |
+
# 🔥 1. Dead Coin Filter (فلتر العملات الميتة)
|
| 237 |
+
# نحسب نسبة الـ ATR للسعر. إذا كانت الحركة أقل من 0.4% في الساعة، نرفضها.
|
| 238 |
+
volatility_pct = (atr / close) * 100 if close > 0 else 0
|
| 239 |
+
if volatility_pct < 0.4:
|
| 240 |
+
return {'type': 'NONE', 'score': 0}
|
| 241 |
+
|
| 242 |
+
# 🛡️ TYPE 1: SAFE_BOTTOM (RSI < 45)
|
| 243 |
+
if rsi < 45:
|
| 244 |
dist_from_ema = (ema50 - close) / ema50
|
| 245 |
+
# السماح بمسافة أكبر قليلاً عن الخط السفلي (5%)
|
| 246 |
+
if close <= lower_bb * 1.05 and dist_from_ema > 0.015:
|
| 247 |
+
score = (55 - rsi) / 20.0
|
|
|
|
|
|
|
| 248 |
return {'type': 'SAFE_BOTTOM', 'score': min(score, 1.0)}
|
| 249 |
|
| 250 |
+
# 🔋 TYPE 2: ACCUMULATION_SQUEEZE (The Missing Link)
|
| 251 |
+
# المنطق: RSI محايد (45-60) + قناة ضيقة (Squeeze) + دعم دفتر الطلبات (سيفحص لاحقاً)
|
| 252 |
+
elif 45 <= rsi <= 60:
|
| 253 |
+
if bb_width < 0.12: # وسعنا النطاق قليلاً لـ 12% لزيادة المرونة
|
| 254 |
+
# نفضل أن يكون السعر يحاول الصعود فوق متوسط 20
|
| 255 |
+
if close > ema20 * 0.995: # السماح بكسر وهمي بسيط
|
| 256 |
+
score = 1.0 - (bb_width * 4.0)
|
| 257 |
+
# نستخدم وسم خاص لتمييزها
|
| 258 |
+
return {
|
| 259 |
+
'type': 'ACCUMULATION_SQUEEZE',
|
| 260 |
+
'score': max(score, 0.5),
|
| 261 |
+
'is_squeeze': True
|
| 262 |
+
}
|
| 263 |
+
|
| 264 |
+
# 🚀 TYPE 3: MOMENTUM_LAUNCH (Classic)
|
| 265 |
+
elif 60 < rsi < 80:
|
| 266 |
if close > ema50 and close > ema200:
|
|
|
|
| 267 |
dist_to_upper = (upper_bb - close) / close
|
| 268 |
+
if dist_to_upper < 0.08:
|
|
|
|
|
|
|
| 269 |
score = rsi / 100.0
|
| 270 |
return {'type': 'MOMENTUM_LAUNCH', 'score': score}
|
| 271 |
|
|
|
|
| 276 |
# ==================================================================
|
| 277 |
async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
|
| 278 |
try:
|
| 279 |
+
print(" 🛡️ [Stage 0] Fetching Tickers (Accumulation Mode)...")
|
| 280 |
tickers = await self.exchange.fetch_tickers()
|
| 281 |
candidates = []
|
| 282 |
|
|
|
|
| 292 |
reject_stats["blacklist"] += 1
|
| 293 |
continue
|
| 294 |
|
|
|
|
| 295 |
base_vol = float(ticker.get('baseVolume') or 0.0)
|
| 296 |
last_price = float(ticker.get('last') or 0.0)
|
| 297 |
calc_quote_vol = base_vol * last_price
|
| 298 |
|
| 299 |
is_sovereign = symbol in SOVEREIGN_COINS
|
| 300 |
|
| 301 |
+
# تخفيف فلتر السيولة للسماح بعملات التجميع الصغيرة
|
|
|
|
| 302 |
if not is_sovereign:
|
| 303 |
if calc_quote_vol < 150000:
|
| 304 |
reject_stats["volume"] += 1
|
| 305 |
continue
|
| 306 |
|
|
|
|
| 307 |
change_pct = ticker.get('percentage')
|
| 308 |
if change_pct is None: change_pct = 0.0
|
| 309 |
|
|
|
|
| 344 |
regime = "RANGE"
|
| 345 |
conf = 0.5
|
| 346 |
|
| 347 |
+
if atr_pct < 0.4: return {'regime': 'DEAD', 'conf': 0.9}
|
| 348 |
|
| 349 |
if price > ema20 and ema20 > ema50 and rsi > 50:
|
| 350 |
regime = "BULL"
|
|
|
|
| 415 |
|
| 416 |
async def get_order_book_snapshot(self, symbol, limit=20):
|
| 417 |
try: return await self.exchange.fetch_order_book(symbol, limit)
|
| 418 |
+
except: return {}
|
| 419 |
+
}
|