Spaces:
Paused
Paused
Update ml_engine/data_manager.py
Browse files- ml_engine/data_manager.py +35 -41
ml_engine/data_manager.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# ============================================================
|
| 2 |
# 📂 ml_engine/data_manager.py
|
| 3 |
-
# (V61.
|
| 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 V61.
|
| 48 |
|
| 49 |
async def initialize(self):
|
| 50 |
print(" > [DataManager] Starting initialization...")
|
|
@@ -79,14 +79,15 @@ class DataManager:
|
|
| 79 |
self.adaptive_hub_ref = adaptive_hub_ref
|
| 80 |
print(f"🔍 [Layer 1] Screening for SAFE BOTTOMS & MOMENTUM LAUNCHES...")
|
| 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 |
enriched_data = await self._fetch_technical_data_batch(top_candidates)
|
| 91 |
|
| 92 |
final_list = []
|
|
@@ -96,13 +97,12 @@ class DataManager:
|
|
| 96 |
classification = self._classify_opportunity_type(item)
|
| 97 |
|
| 98 |
if classification['type'] != 'NONE':
|
| 99 |
-
# تشخيص حالة السوق العامة (Regime)
|
| 100 |
regime_info = self._diagnose_asset_regime(item)
|
| 101 |
current_regime = regime_info['regime']
|
| 102 |
|
| 103 |
# 🔥 Regime Gating: الحماية من المصائد
|
| 104 |
# إذا السوق عرضي (RANGE) أو ميت (DEAD)، نمنع صفقات الزخم (MOMENTUM) لأنها غالباً مصائد
|
| 105 |
-
# لكن نسمح بصفقات القيعان (SAFE_BOTTOM) لأنها آمنة في التذبذب
|
| 106 |
if current_regime in ['RANGE', 'DEAD'] and classification['type'] == 'MOMENTUM_LAUNCH':
|
| 107 |
continue
|
| 108 |
|
|
@@ -127,23 +127,20 @@ class DataManager:
|
|
| 127 |
# 4. الترتيب النهائي
|
| 128 |
final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
|
| 129 |
|
| 130 |
-
|
|
|
|
| 131 |
print(f"✅ [Layer 1] Passed {len(selection)} candidates (Types: Bottom/Launch).")
|
| 132 |
return selection
|
| 133 |
|
| 134 |
# ==================================================================
|
| 135 |
-
# ⚖️ The Dual-Classifier Logic
|
| 136 |
# ==================================================================
|
| 137 |
def _classify_opportunity_type(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
| 138 |
"""
|
| 139 |
-
ت
|
| 140 |
-
1. SAFE_BOTTOM: عملات في القاع (آمنة من الهبوط الحاد).
|
| 141 |
-
2. MOMENTUM_LAUNCH: عملات تجهز للانفجار (نسبة 90%).
|
| 142 |
"""
|
| 143 |
try:
|
| 144 |
df_1h = self._calc_indicators(data['ohlcv_1h_raw'])
|
| 145 |
-
# يمكن استخدام فريم 15 دقيقة للتأكيد الإضافي مستقبلاً
|
| 146 |
-
# df_15m = self._calc_indicators(data['ohlcv_15m_raw'])
|
| 147 |
curr = df_1h.iloc[-1]
|
| 148 |
except: return {'type': 'NONE', 'score': 0}
|
| 149 |
|
|
@@ -157,44 +154,41 @@ class DataManager:
|
|
| 157 |
lower_bb = curr['lower_bb'] if 'lower_bb' in curr else (curr['ema20'] - (2*curr['atr']))
|
| 158 |
upper_bb = curr['upper_bb'] if 'upper_bb' in curr else (curr['ema20'] + (2*curr['atr']))
|
| 159 |
|
| 160 |
-
#
|
| 161 |
-
# ال
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
# التأكد من الابتعاد عن المتوسط (Oversold Extension)
|
| 165 |
dist_from_ema = (ema50 - close) / ema50
|
| 166 |
|
| 167 |
-
# الس
|
| 168 |
-
if close <= lower_bb * 1.
|
| 169 |
-
# Score
|
| 170 |
score = (50 - rsi) / 20.0
|
| 171 |
return {'type': 'SAFE_BOTTOM', 'score': min(score, 1.0)}
|
| 172 |
|
| 173 |
-
# 🚀 TYPE 2: MOMENTUM_LAUNCH (
|
| 174 |
-
# ال
|
| 175 |
-
|
| 176 |
-
elif 55 < rsi < 75:
|
| 177 |
if close > ema50 and close > ema200:
|
| 178 |
-
# السعر يضغط قرب الحد العلوي
|
| 179 |
dist_to_upper = (upper_bb - close) / close
|
| 180 |
|
| 181 |
-
#
|
| 182 |
-
if dist_to_upper < 0.
|
| 183 |
score = rsi / 100.0
|
| 184 |
return {'type': 'MOMENTUM_LAUNCH', 'score': score}
|
| 185 |
|
| 186 |
return {'type': 'NONE', 'score': 0}
|
| 187 |
|
| 188 |
# ==================================================================
|
| 189 |
-
# 🔍 Stage 0: Universe Filter (
|
| 190 |
# ==================================================================
|
| 191 |
async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
|
| 192 |
try:
|
| 193 |
-
print(" 🛡️ [Stage 0] Fetching Tickers...")
|
| 194 |
tickers = await self.exchange.fetch_tickers()
|
| 195 |
candidates = []
|
| 196 |
|
| 197 |
-
# القائمة السيادية (تمر دائماً)
|
| 198 |
SOVEREIGN_COINS = ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT']
|
| 199 |
|
| 200 |
reject_stats = {"volume": 0, "change": 0, "blacklist": 0}
|
|
@@ -207,25 +201,25 @@ class DataManager:
|
|
| 207 |
reject_stats["blacklist"] += 1
|
| 208 |
continue
|
| 209 |
|
| 210 |
-
# حساب الحجم
|
| 211 |
base_vol = float(ticker.get('baseVolume') or 0.0)
|
| 212 |
last_price = float(ticker.get('last') or 0.0)
|
| 213 |
calc_quote_vol = base_vol * last_price
|
| 214 |
|
| 215 |
is_sovereign = symbol in SOVEREIGN_COINS
|
| 216 |
|
| 217 |
-
# فلتر السيولة
|
|
|
|
| 218 |
if not is_sovereign:
|
| 219 |
-
if calc_quote_vol <
|
| 220 |
reject_stats["volume"] += 1
|
| 221 |
continue
|
| 222 |
|
| 223 |
-
# فلتر الت
|
| 224 |
-
# هنا نبقي الفلتر العام للحماية من الانهيارات التامة
|
| 225 |
change_pct = ticker.get('percentage')
|
| 226 |
if change_pct is None: change_pct = 0.0
|
| 227 |
|
| 228 |
-
if abs(change_pct) >
|
| 229 |
reject_stats["change"] += 1
|
| 230 |
continue
|
| 231 |
|
|
@@ -275,7 +269,7 @@ class DataManager:
|
|
| 275 |
except Exception: return {'regime': 'RANGE', 'conf': 0.0}
|
| 276 |
|
| 277 |
# ------------------------------------------------------------------
|
| 278 |
-
# Helpers & Indicators
|
| 279 |
# ------------------------------------------------------------------
|
| 280 |
async def _fetch_technical_data_batch(self, candidates):
|
| 281 |
chunk_size = 10; results = []
|
|
@@ -289,7 +283,7 @@ class DataManager:
|
|
| 289 |
|
| 290 |
async def _fetch_single(self, c):
|
| 291 |
try:
|
| 292 |
-
h1 = await self.exchange.fetch_ohlcv(c['symbol'], '1h', limit=210)
|
| 293 |
m15 = await self.exchange.fetch_ohlcv(c['symbol'], '15m', limit=60)
|
| 294 |
if not h1 or not m15: return None
|
| 295 |
c['ohlcv'] = {'1h': h1, '15m': m15}
|
|
@@ -309,7 +303,7 @@ class DataManager:
|
|
| 309 |
# EMAs
|
| 310 |
df['ema20'] = df['c'].ewm(span=20).mean()
|
| 311 |
df['ema50'] = df['c'].ewm(span=50).mean()
|
| 312 |
-
df['ema200'] = df['c'].ewm(span=200).mean()
|
| 313 |
|
| 314 |
# ATR
|
| 315 |
tr = np.maximum(df['h']-df['l'], np.maximum(abs(df['h']-df['c'].shift()), abs(df['l']-df['c'].shift())))
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
# 📂 ml_engine/data_manager.py
|
| 3 |
+
# (V61.1 - GEM-Architect: Relaxed Filtering & Dual Classification)
|
| 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 V61.1] Relaxed Filtering Active.")
|
| 48 |
|
| 49 |
async def initialize(self):
|
| 50 |
print(" > [DataManager] Starting initialization...")
|
|
|
|
| 79 |
self.adaptive_hub_ref = adaptive_hub_ref
|
| 80 |
print(f"🔍 [Layer 1] Screening for SAFE BOTTOMS & MOMENTUM LAUNCHES...")
|
| 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 |
final_list = []
|
|
|
|
| 97 |
classification = self._classify_opportunity_type(item)
|
| 98 |
|
| 99 |
if classification['type'] != 'NONE':
|
| 100 |
+
# تشخيص حالة السوق العامة (Regime)
|
| 101 |
regime_info = self._diagnose_asset_regime(item)
|
| 102 |
current_regime = regime_info['regime']
|
| 103 |
|
| 104 |
# 🔥 Regime Gating: الحماية من المصائد
|
| 105 |
# إذا السوق عرضي (RANGE) أو ميت (DEAD)، نمنع صفقات الزخم (MOMENTUM) لأنها غالباً مصائد
|
|
|
|
| 106 |
if current_regime in ['RANGE', 'DEAD'] and classification['type'] == 'MOMENTUM_LAUNCH':
|
| 107 |
continue
|
| 108 |
|
|
|
|
| 127 |
# 4. الترتيب النهائي
|
| 128 |
final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
|
| 129 |
|
| 130 |
+
# نمرر عدد أكبر للمعالجات اللاحقة
|
| 131 |
+
selection = final_list[:60]
|
| 132 |
print(f"✅ [Layer 1] Passed {len(selection)} candidates (Types: Bottom/Launch).")
|
| 133 |
return selection
|
| 134 |
|
| 135 |
# ==================================================================
|
| 136 |
+
# ⚖️ The Dual-Classifier Logic (RELAXED CONDITIONS)
|
| 137 |
# ==================================================================
|
| 138 |
def _classify_opportunity_type(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
| 139 |
"""
|
| 140 |
+
تم تخفيف الشروط لزيادة الحساسية (Sensitivity) وتقليل الرفض الخاطئ (False Negatives).
|
|
|
|
|
|
|
| 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 |
|
|
|
|
| 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 |
+
# ���️ TYPE 1: SAFE_BOTTOM (Relaxed)
|
| 158 |
+
# القديم: RSI < 35. الجديد: RSI < 42 (القيعان في الترند الصاعد لا تصل لـ 30 غالباً)
|
| 159 |
+
if rsi < 42:
|
| 160 |
+
# التأكد من الابتعاد عن المتوسط
|
|
|
|
| 161 |
dist_from_ema = (ema50 - close) / ema50
|
| 162 |
|
| 163 |
+
# السماح بمسافة أكبر قليلاً عن الخط السفلي (3% بدلاً من 1.5%)
|
| 164 |
+
if close <= lower_bb * 1.03 and dist_from_ema > 0.02:
|
| 165 |
+
# Score adjustment
|
| 166 |
score = (50 - rsi) / 20.0
|
| 167 |
return {'type': 'SAFE_BOTTOM', 'score': min(score, 1.0)}
|
| 168 |
|
| 169 |
+
# 🚀 TYPE 2: MOMENTUM_LAUNCH (Relaxed)
|
| 170 |
+
# القديم: 55-75. الجديد: 50-80 (للسماح بدخول مبكر أو متأخر قليلاً)
|
| 171 |
+
elif 50 < rsi < 80:
|
|
|
|
| 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 |
|
| 181 |
return {'type': 'NONE', 'score': 0}
|
| 182 |
|
| 183 |
# ==================================================================
|
| 184 |
+
# 🔍 Stage 0: Universe Filter (RELAXED USD CALC)
|
| 185 |
# ==================================================================
|
| 186 |
async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
|
| 187 |
try:
|
| 188 |
+
print(" 🛡️ [Stage 0] Fetching Tickers (Relaxed Mode)...")
|
| 189 |
tickers = await self.exchange.fetch_tickers()
|
| 190 |
candidates = []
|
| 191 |
|
|
|
|
| 192 |
SOVEREIGN_COINS = ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'XRP/USDT']
|
| 193 |
|
| 194 |
reject_stats = {"volume": 0, "change": 0, "blacklist": 0}
|
|
|
|
| 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 |
+
# ⬇️ تخفيف فلتر السيولة: من 500k إلى 150k
|
| 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 |
|
| 222 |
+
if abs(change_pct) > 35.0:
|
| 223 |
reject_stats["change"] += 1
|
| 224 |
continue
|
| 225 |
|
|
|
|
| 269 |
except Exception: return {'regime': 'RANGE', 'conf': 0.0}
|
| 270 |
|
| 271 |
# ------------------------------------------------------------------
|
| 272 |
+
# Helpers & Indicators
|
| 273 |
# ------------------------------------------------------------------
|
| 274 |
async def _fetch_technical_data_batch(self, candidates):
|
| 275 |
chunk_size = 10; results = []
|
|
|
|
| 283 |
|
| 284 |
async def _fetch_single(self, c):
|
| 285 |
try:
|
| 286 |
+
h1 = await self.exchange.fetch_ohlcv(c['symbol'], '1h', limit=210)
|
| 287 |
m15 = await self.exchange.fetch_ohlcv(c['symbol'], '15m', limit=60)
|
| 288 |
if not h1 or not m15: return None
|
| 289 |
c['ohlcv'] = {'1h': h1, '15m': m15}
|
|
|
|
| 303 |
# EMAs
|
| 304 |
df['ema20'] = df['c'].ewm(span=20).mean()
|
| 305 |
df['ema50'] = df['c'].ewm(span=50).mean()
|
| 306 |
+
df['ema200'] = df['c'].ewm(span=200).mean()
|
| 307 |
|
| 308 |
# ATR
|
| 309 |
tr = np.maximum(df['h']-df['l'], np.maximum(abs(df['h']-df['c'].shift()), abs(df['l']-df['c'].shift())))
|