Spaces:
Sleeping
Sleeping
Update ml_engine/data_manager.py
Browse files- ml_engine/data_manager.py +60 -56
ml_engine/data_manager.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
# ============================================================
|
| 2 |
# 📂 ml_engine/data_manager.py
|
| 3 |
-
# (V67.
|
| 4 |
# ============================================================
|
| 5 |
|
| 6 |
import asyncio
|
|
@@ -44,7 +44,7 @@ class DataManager:
|
|
| 44 |
'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L'
|
| 45 |
]
|
| 46 |
|
| 47 |
-
print(f"📦 [DataManager V67.
|
| 48 |
|
| 49 |
async def initialize(self):
|
| 50 |
print(" > [DataManager] Starting initialization...")
|
|
@@ -73,14 +73,16 @@ class DataManager:
|
|
| 73 |
return self.contracts_db
|
| 74 |
|
| 75 |
# ==================================================================
|
| 76 |
-
# 🌍 Global Market Validator (
|
| 77 |
# ==================================================================
|
| 78 |
async def check_global_market_health(self) -> Dict[str, Any]:
|
| 79 |
"""
|
| 80 |
-
يفحص صحة السوق العامة
|
|
|
|
|
|
|
| 81 |
"""
|
| 82 |
try:
|
| 83 |
-
#
|
| 84 |
btc_ohlcv = await self.exchange.fetch_ohlcv('BTC/USDT', '1d', limit=30)
|
| 85 |
if not btc_ohlcv: return {'is_safe': True, 'reason': 'No BTC Data - Bypassed'}
|
| 86 |
|
|
@@ -88,52 +90,75 @@ class DataManager:
|
|
| 88 |
current_close = df['c'].iloc[-1]
|
| 89 |
prev_close = df['c'].iloc[-2]
|
| 90 |
|
| 91 |
-
#
|
|
|
|
| 92 |
daily_change = (current_close - prev_close) / prev_close
|
| 93 |
-
if daily_change < -0.
|
| 94 |
-
return {'is_safe': False, 'reason': '🚨 BTC
|
| 95 |
|
| 96 |
-
#
|
| 97 |
sma20 = df['c'].rolling(20).mean().iloc[-1]
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
dist_to_sma = (sma20 - current_close) / sma20
|
| 101 |
-
if current_close < sma20 and dist_to_sma > 0.10: # بعيد جداً عن المتوسط لأسفل
|
| 102 |
-
return {'is_safe': False, 'reason': '📉 Deep Bear Market (Below SMA20)'}
|
| 103 |
|
| 104 |
-
#
|
|
|
|
|
|
|
| 105 |
avg_vol = df['v'].rolling(7).mean().iloc[-1]
|
| 106 |
curr_vol = df['v'].iloc[-1]
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
-
return {'is_safe': True, 'reason': '✅ Market
|
| 113 |
|
| 114 |
except Exception as e:
|
| 115 |
print(f"⚠️ [Market Validator] Error: {e}")
|
| 116 |
-
# في حال الخطأ التقني، نفترض الأمان لتجنب التوقف الكامل
|
| 117 |
return {'is_safe': True, 'reason': 'Error Bypass'}
|
| 118 |
|
| 119 |
# ==================================================================
|
| 120 |
-
# 🧠 Layer 1: Classification (
|
| 121 |
# ==================================================================
|
| 122 |
async def layer1_rapid_screening(self, adaptive_hub_ref=None) -> List[Dict[str, Any]]:
|
| 123 |
self.adaptive_hub_ref = adaptive_hub_ref
|
| 124 |
-
print(f"🔍 [Layer 1] Screening
|
| 125 |
|
| 126 |
-
# 0. فحص صحة السوق
|
| 127 |
market_health = await self.check_global_market_health()
|
| 128 |
|
| 129 |
if not market_health['is_safe']:
|
| 130 |
print(f"⛔ [Market Validator] Trading Halted: {market_health['reason']}")
|
| 131 |
return []
|
| 132 |
else:
|
| 133 |
-
|
| 134 |
-
print(f" 🌍 [Market Validator] Global Status: {market_health['reason']}")
|
| 135 |
|
| 136 |
-
# 1. فلتر السيولة الأساسي
|
| 137 |
initial_candidates = await self._stage0_universe_filter()
|
| 138 |
if not initial_candidates:
|
| 139 |
print("⚠️ [Layer 1] Stage 0 returned 0 candidates.")
|
|
@@ -145,39 +170,36 @@ class DataManager:
|
|
| 145 |
|
| 146 |
semi_final_list = []
|
| 147 |
|
| 148 |
-
# 3. التصنيف الفني
|
| 149 |
for item in enriched_data:
|
| 150 |
classification = self._classify_opportunity_type(item)
|
| 151 |
|
| 152 |
if classification['type'] != 'NONE':
|
| 153 |
-
# الاحتفاظ بتشخيص Regime القديم (للاستئناس فقط)
|
| 154 |
regime_info = self._diagnose_asset_regime(item)
|
| 155 |
item['asset_regime'] = regime_info['regime']
|
| 156 |
item['asset_regime_conf'] = regime_info['conf']
|
| 157 |
|
| 158 |
-
# الاعتماد الأساسي على التصنيف الجديد
|
| 159 |
item['strategy_type'] = classification['type']
|
| 160 |
item['l1_sort_score'] = classification['score']
|
| 161 |
item['strategy_tag'] = classification['type']
|
| 162 |
|
| 163 |
-
#
|
| 164 |
if regime_info['regime'] == 'DEAD' and classification['type'] == 'MOMENTUM_LAUNCH':
|
| 165 |
if not classification.get('is_squeeze', False):
|
| 166 |
continue
|
| 167 |
|
| 168 |
semi_final_list.append(item)
|
| 169 |
|
| 170 |
-
# 4.
|
| 171 |
final_list = []
|
| 172 |
-
# نأخذ أفضل 300 مرشحاً لفحص دفتر الطلبات
|
| 173 |
semi_final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
|
| 174 |
candidates_for_depth = semi_final_list[:300]
|
| 175 |
|
| 176 |
if candidates_for_depth:
|
| 177 |
-
print(f" 🛡️ [Layer 1.5] Checking Depth
|
| 178 |
|
| 179 |
for item in candidates_for_depth:
|
| 180 |
-
# أ. فحص العمق
|
| 181 |
if item['strategy_type'] in ['ACCUMULATION_SQUEEZE', 'SAFE_BOTTOM']:
|
| 182 |
try:
|
| 183 |
atr_val = item.get('atr_value', 0.0)
|
|
@@ -194,9 +216,8 @@ class DataManager:
|
|
| 194 |
item['l1_sort_score'] -= 0.10
|
| 195 |
except Exception: pass
|
| 196 |
|
| 197 |
-
# ب.
|
| 198 |
if self.adaptive_hub_ref:
|
| 199 |
-
# نستخدم نوع العملة كمفتاح لجلب الإعدادات
|
| 200 |
coin_type = item.get('strategy_type', 'SAFE_BOTTOM')
|
| 201 |
dynamic_config = self.adaptive_hub_ref.get_coin_type_config(coin_type)
|
| 202 |
item['dynamic_limits'] = dynamic_config
|
|
@@ -259,48 +280,40 @@ class DataManager:
|
|
| 259 |
bb_width = (upper_bb - lower_bb) / curr['ema20'] if curr['ema20'] > 0 else 1.0
|
| 260 |
|
| 261 |
# 🔥 1. Dead Coin Filter (Relaxed to 0.3%)
|
| 262 |
-
# تم تخفيض العتبة من 0.4 إلى 0.3 للسماح للعملات الهادئة بالدخول إذا تحركت قليلاً
|
| 263 |
volatility_pct = (atr / close) * 100 if close > 0 else 0
|
| 264 |
if volatility_pct < 0.3: return {'type': 'NONE', 'score': 0}
|
| 265 |
|
| 266 |
# 🛡️ TYPE 1: SAFE_BOTTOM (Widened)
|
| 267 |
-
# RSI < 55 (كانت 45)، والسعر عند أو تحت الحد السفلي + 8% (كانت 5%)
|
| 268 |
if rsi < 55:
|
| 269 |
-
dist_from_ema = (ema50 - close) / ema50
|
| 270 |
if close <= lower_bb * 1.08:
|
| 271 |
score = (60 - rsi) / 20.0
|
| 272 |
return {'type': 'SAFE_BOTTOM', 'score': min(score, 1.0)}
|
| 273 |
|
| 274 |
# 🔋 TYPE 2: ACCUMULATION_SQUEEZE (Widened)
|
| 275 |
-
# RSI 40-65 (كانت 45-60)، BB Width < 0.18 (كانت 0.12)
|
| 276 |
elif 40 <= rsi <= 65:
|
| 277 |
if bb_width < 0.18:
|
| 278 |
-
# إزالة الشرط الصارم لـ EMA20 للسماح للنماذج بالاختيار
|
| 279 |
score = 1.0 - (bb_width * 3.0)
|
| 280 |
return {'type': 'ACCUMULATION_SQUEEZE', 'score': max(score, 0.5), 'is_squeeze': True}
|
| 281 |
|
| 282 |
# 🚀 TYPE 3: MOMENTUM_LAUNCH (Earlier Entry)
|
| 283 |
-
# RSI > 50 (كانت 60)
|
| 284 |
elif 50 < rsi < 85:
|
| 285 |
if close > ema50:
|
| 286 |
dist_to_upper = (upper_bb - close) / close
|
| 287 |
-
if dist_to_upper < 0.12:
|
| 288 |
score = rsi / 100.0
|
| 289 |
return {'type': 'MOMENTUM_LAUNCH', 'score': score}
|
| 290 |
|
| 291 |
-
# 🃏 Special Case
|
| 292 |
-
# إذا كانت السيولة والتقلب مرتفعين جداً (> 1.5%)، مررها للنماذج
|
| 293 |
if volatility_pct > 1.5:
|
| 294 |
return {'type': 'SAFE_BOTTOM', 'score': 0.4}
|
| 295 |
|
| 296 |
return {'type': 'NONE', 'score': 0}
|
| 297 |
|
| 298 |
# ==================================================================
|
| 299 |
-
# 🔍 Stage 0: Universe Filter
|
| 300 |
# ==================================================================
|
| 301 |
async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
|
| 302 |
try:
|
| 303 |
-
# 🔥 إعداد الحد الأدنى لحجم التداول
|
| 304 |
MIN_VOLUME_THRESHOLD = 1000000.0 # 1 Million USDT
|
| 305 |
|
| 306 |
print(f" 🛡️ [Stage 0] Fetching Tickers (Min Vol: ${MIN_VOLUME_THRESHOLD:,.0f})...")
|
|
@@ -324,7 +337,6 @@ class DataManager:
|
|
| 324 |
|
| 325 |
is_sovereign = symbol in SOVEREIGN_COINS
|
| 326 |
|
| 327 |
-
# 🔥 الفلتر الصارم الجديد: رفض أي عملة تحت المليون
|
| 328 |
if not is_sovereign:
|
| 329 |
if calc_quote_vol < MIN_VOLUME_THRESHOLD:
|
| 330 |
reject_stats["volume"] += 1
|
|
@@ -357,12 +369,8 @@ class DataManager:
|
|
| 357 |
# 🧭 The Diagnoser
|
| 358 |
# ------------------------------------------------------------------
|
| 359 |
def _diagnose_asset_regime(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
| 360 |
-
"""
|
| 361 |
-
تقوم بتشخيص حالة السوق للأصل (Regime) لتحديد ما إذا كان مناسباً للدخول
|
| 362 |
-
"""
|
| 363 |
try:
|
| 364 |
if 'df_1h' not in item:
|
| 365 |
-
# محاولة استخراج الداتا فريم إذا لم تكن موجودة
|
| 366 |
if 'ohlcv_1h_raw' in item:
|
| 367 |
item['df_1h'] = self._calc_indicators(item['ohlcv_1h_raw'])
|
| 368 |
else:
|
|
@@ -415,7 +423,6 @@ class DataManager:
|
|
| 415 |
c['ohlcv'] = {'1h': h1, '15m': m15}
|
| 416 |
c['ohlcv_1h_raw'] = h1
|
| 417 |
c['ohlcv_15m_raw'] = m15
|
| 418 |
-
# حساب المؤشرات هنا لتوفير الوقت لاحقاً
|
| 419 |
c['df_1h'] = self._calc_indicators(h1)
|
| 420 |
return c
|
| 421 |
except: return None
|
|
@@ -428,16 +435,13 @@ class DataManager:
|
|
| 428 |
rs = gain/loss
|
| 429 |
df['rsi'] = 100 - (100/(1+rs))
|
| 430 |
|
| 431 |
-
# EMAs
|
| 432 |
df['ema20'] = df['c'].ewm(span=20).mean()
|
| 433 |
df['ema50'] = df['c'].ewm(span=50).mean()
|
| 434 |
df['ema200'] = df['c'].ewm(span=200).mean()
|
| 435 |
|
| 436 |
-
# ATR
|
| 437 |
tr = np.maximum(df['h']-df['l'], np.maximum(abs(df['h']-df['c'].shift()), abs(df['l']-df['c'].shift())))
|
| 438 |
df['atr'] = tr.rolling(14).mean()
|
| 439 |
|
| 440 |
-
# Bollinger Bands
|
| 441 |
std = df['c'].rolling(20).std()
|
| 442 |
df['upper_bb'] = df['ema20'] + (2 * std)
|
| 443 |
df['lower_bb'] = df['ema20'] - (2 * std)
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
# 📂 ml_engine/data_manager.py
|
| 3 |
+
# (V67.5 - GEM-Architect: Smart Market Breadth Scanner)
|
| 4 |
# ============================================================
|
| 5 |
|
| 6 |
import asyncio
|
|
|
|
| 44 |
'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L'
|
| 45 |
]
|
| 46 |
|
| 47 |
+
print(f"📦 [DataManager V67.5] Initialized (Smart Breadth Scanner).")
|
| 48 |
|
| 49 |
async def initialize(self):
|
| 50 |
print(" > [DataManager] Starting initialization...")
|
|
|
|
| 73 |
return self.contracts_db
|
| 74 |
|
| 75 |
# ==================================================================
|
| 76 |
+
# 🌍 Global Market Validator V2 (Smart Breadth Scanner)
|
| 77 |
# ==================================================================
|
| 78 |
async def check_global_market_health(self) -> Dict[str, Any]:
|
| 79 |
"""
|
| 80 |
+
يفحص صحة السوق العامة باستخدام منطق مزدوج:
|
| 81 |
+
1. فحص سلامة BTC (تجنب الانهيارات).
|
| 82 |
+
2. فحص نشاط العملات البديلة (Altcoin Pulse) للسماح بالتداول حتى لو كان BTC هادئاً.
|
| 83 |
"""
|
| 84 |
try:
|
| 85 |
+
# 1. جلب بيانات البيتكوين الأساسية
|
| 86 |
btc_ohlcv = await self.exchange.fetch_ohlcv('BTC/USDT', '1d', limit=30)
|
| 87 |
if not btc_ohlcv: return {'is_safe': True, 'reason': 'No BTC Data - Bypassed'}
|
| 88 |
|
|
|
|
| 90 |
current_close = df['c'].iloc[-1]
|
| 91 |
prev_close = df['c'].iloc[-2]
|
| 92 |
|
| 93 |
+
# --- [ CRITICAL CHECK ] ---
|
| 94 |
+
# إذا كان البيتكوين ينهار، نوقف كل شيء لأن السيولة ستجف
|
| 95 |
daily_change = (current_close - prev_close) / prev_close
|
| 96 |
+
if daily_change < -0.045: # السماح بمرونة أكبر قليلاً (-4.5%)
|
| 97 |
+
return {'is_safe': False, 'reason': f'🚨 BTC CRASHING ({daily_change*100:.2f}%)'}
|
| 98 |
|
| 99 |
+
# فحص المتوسطات (Trend Check)
|
| 100 |
sma20 = df['c'].rolling(20).mean().iloc[-1]
|
| 101 |
+
if current_close < sma20 * 0.92: # إذا كان السعر تحت المتوسط بـ 8% (سوق هابط عنيف)
|
| 102 |
+
return {'is_safe': False, 'reason': '📉 Deep Bear Market (Risk Off)'}
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
+
# --- [ ALTCOIN PULSE CHECK ] ---
|
| 105 |
+
# بدلاً من إيقاف السوق بسبب ضعف فوليوم البيتكوين، نفحص هل هناك عملات تتحرك؟
|
| 106 |
+
|
| 107 |
avg_vol = df['v'].rolling(7).mean().iloc[-1]
|
| 108 |
curr_vol = df['v'].iloc[-1]
|
| 109 |
|
| 110 |
+
btc_is_dead = curr_vol < (avg_vol * 0.4) # البيتكوين ميت
|
| 111 |
+
|
| 112 |
+
if btc_is_dead:
|
| 113 |
+
# 🕵️ فحص النبض: هل هناك عملات تنفصل عن البيتكوين؟
|
| 114 |
+
print(" ⚠️ [Validator] BTC Volume Low.. Scanning Altcoin Pulse...")
|
| 115 |
+
tickers = await self.exchange.fetch_tickers()
|
| 116 |
+
|
| 117 |
+
green_coins = 0
|
| 118 |
+
pump_coins = 0
|
| 119 |
+
total_checked = 0
|
| 120 |
+
|
| 121 |
+
# نفحص العملات ذات الفوليوم العالي فقط
|
| 122 |
+
for sym, data in tickers.items():
|
| 123 |
+
if not sym.endswith('/USDT'): continue
|
| 124 |
+
vol = float(data.get('quoteVolume') or 0)
|
| 125 |
+
if vol < 5000000: continue # تجاهل العملات الصغيرة جداً في هذا الفحص
|
| 126 |
+
|
| 127 |
+
change = float(data.get('percentage') or 0)
|
| 128 |
+
total_checked += 1
|
| 129 |
+
|
| 130 |
+
if change > 1.0: green_coins += 1
|
| 131 |
+
if change > 5.0: pump_coins += 1
|
| 132 |
+
|
| 133 |
+
# المنطق: إذا وجدنا أكثر من 3 عملات تضخ بقوة، أو 40% من السوق أخضر -> السوق يعمل
|
| 134 |
+
if pump_coins >= 3 or (total_checked > 0 and (green_coins / total_checked) > 0.4):
|
| 135 |
+
return {'is_safe': True, 'reason': f'✅ Decoupled Alts Active ({pump_coins} Pumping)'}
|
| 136 |
+
else:
|
| 137 |
+
return {'is_safe': False, 'reason': '💤 Dead Market (BTC & Alts Flat)'}
|
| 138 |
|
| 139 |
+
return {'is_safe': True, 'reason': '✅ Market Healthy'}
|
| 140 |
|
| 141 |
except Exception as e:
|
| 142 |
print(f"⚠️ [Market Validator] Error: {e}")
|
|
|
|
| 143 |
return {'is_safe': True, 'reason': 'Error Bypass'}
|
| 144 |
|
| 145 |
# ==================================================================
|
| 146 |
+
# 🧠 Layer 1: Classification (Relaxed Funnel)
|
| 147 |
# ==================================================================
|
| 148 |
async def layer1_rapid_screening(self, adaptive_hub_ref=None) -> List[Dict[str, Any]]:
|
| 149 |
self.adaptive_hub_ref = adaptive_hub_ref
|
| 150 |
+
print(f"🔍 [Layer 1] Screening Market (Smart Breadth)...")
|
| 151 |
|
| 152 |
+
# 0. فحص صحة السوق
|
| 153 |
market_health = await self.check_global_market_health()
|
| 154 |
|
| 155 |
if not market_health['is_safe']:
|
| 156 |
print(f"⛔ [Market Validator] Trading Halted: {market_health['reason']}")
|
| 157 |
return []
|
| 158 |
else:
|
| 159 |
+
print(f" 🌍 [Market Validator] Status: {market_health['reason']}")
|
|
|
|
| 160 |
|
| 161 |
+
# 1. فلتر السيولة الأساسي
|
| 162 |
initial_candidates = await self._stage0_universe_filter()
|
| 163 |
if not initial_candidates:
|
| 164 |
print("⚠️ [Layer 1] Stage 0 returned 0 candidates.")
|
|
|
|
| 170 |
|
| 171 |
semi_final_list = []
|
| 172 |
|
| 173 |
+
# 3. التصنيف الفني
|
| 174 |
for item in enriched_data:
|
| 175 |
classification = self._classify_opportunity_type(item)
|
| 176 |
|
| 177 |
if classification['type'] != 'NONE':
|
|
|
|
| 178 |
regime_info = self._diagnose_asset_regime(item)
|
| 179 |
item['asset_regime'] = regime_info['regime']
|
| 180 |
item['asset_regime_conf'] = regime_info['conf']
|
| 181 |
|
|
|
|
| 182 |
item['strategy_type'] = classification['type']
|
| 183 |
item['l1_sort_score'] = classification['score']
|
| 184 |
item['strategy_tag'] = classification['type']
|
| 185 |
|
| 186 |
+
# إذا كان التشخيص العام "ميت" لكن العملة في حالة ضغط (Squeeze)، نمررها
|
| 187 |
if regime_info['regime'] == 'DEAD' and classification['type'] == 'MOMENTUM_LAUNCH':
|
| 188 |
if not classification.get('is_squeeze', False):
|
| 189 |
continue
|
| 190 |
|
| 191 |
semi_final_list.append(item)
|
| 192 |
|
| 193 |
+
# 4. فحص العمق وحقن الإعدادات
|
| 194 |
final_list = []
|
|
|
|
| 195 |
semi_final_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
|
| 196 |
candidates_for_depth = semi_final_list[:300]
|
| 197 |
|
| 198 |
if candidates_for_depth:
|
| 199 |
+
print(f" 🛡️ [Layer 1.5] Checking Depth for {len(candidates_for_depth)} candidates...")
|
| 200 |
|
| 201 |
for item in candidates_for_depth:
|
| 202 |
+
# أ. فحص العمق
|
| 203 |
if item['strategy_type'] in ['ACCUMULATION_SQUEEZE', 'SAFE_BOTTOM']:
|
| 204 |
try:
|
| 205 |
atr_val = item.get('atr_value', 0.0)
|
|
|
|
| 216 |
item['l1_sort_score'] -= 0.10
|
| 217 |
except Exception: pass
|
| 218 |
|
| 219 |
+
# ب. حقن الإعدادات
|
| 220 |
if self.adaptive_hub_ref:
|
|
|
|
| 221 |
coin_type = item.get('strategy_type', 'SAFE_BOTTOM')
|
| 222 |
dynamic_config = self.adaptive_hub_ref.get_coin_type_config(coin_type)
|
| 223 |
item['dynamic_limits'] = dynamic_config
|
|
|
|
| 280 |
bb_width = (upper_bb - lower_bb) / curr['ema20'] if curr['ema20'] > 0 else 1.0
|
| 281 |
|
| 282 |
# 🔥 1. Dead Coin Filter (Relaxed to 0.3%)
|
|
|
|
| 283 |
volatility_pct = (atr / close) * 100 if close > 0 else 0
|
| 284 |
if volatility_pct < 0.3: return {'type': 'NONE', 'score': 0}
|
| 285 |
|
| 286 |
# 🛡️ TYPE 1: SAFE_BOTTOM (Widened)
|
|
|
|
| 287 |
if rsi < 55:
|
|
|
|
| 288 |
if close <= lower_bb * 1.08:
|
| 289 |
score = (60 - rsi) / 20.0
|
| 290 |
return {'type': 'SAFE_BOTTOM', 'score': min(score, 1.0)}
|
| 291 |
|
| 292 |
# 🔋 TYPE 2: ACCUMULATION_SQUEEZE (Widened)
|
|
|
|
| 293 |
elif 40 <= rsi <= 65:
|
| 294 |
if bb_width < 0.18:
|
|
|
|
| 295 |
score = 1.0 - (bb_width * 3.0)
|
| 296 |
return {'type': 'ACCUMULATION_SQUEEZE', 'score': max(score, 0.5), 'is_squeeze': True}
|
| 297 |
|
| 298 |
# 🚀 TYPE 3: MOMENTUM_LAUNCH (Earlier Entry)
|
|
|
|
| 299 |
elif 50 < rsi < 85:
|
| 300 |
if close > ema50:
|
| 301 |
dist_to_upper = (upper_bb - close) / close
|
| 302 |
+
if dist_to_upper < 0.12:
|
| 303 |
score = rsi / 100.0
|
| 304 |
return {'type': 'MOMENTUM_LAUNCH', 'score': score}
|
| 305 |
|
| 306 |
+
# 🃏 Special Case
|
|
|
|
| 307 |
if volatility_pct > 1.5:
|
| 308 |
return {'type': 'SAFE_BOTTOM', 'score': 0.4}
|
| 309 |
|
| 310 |
return {'type': 'NONE', 'score': 0}
|
| 311 |
|
| 312 |
# ==================================================================
|
| 313 |
+
# 🔍 Stage 0: Universe Filter
|
| 314 |
# ==================================================================
|
| 315 |
async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
|
| 316 |
try:
|
|
|
|
| 317 |
MIN_VOLUME_THRESHOLD = 1000000.0 # 1 Million USDT
|
| 318 |
|
| 319 |
print(f" 🛡️ [Stage 0] Fetching Tickers (Min Vol: ${MIN_VOLUME_THRESHOLD:,.0f})...")
|
|
|
|
| 337 |
|
| 338 |
is_sovereign = symbol in SOVEREIGN_COINS
|
| 339 |
|
|
|
|
| 340 |
if not is_sovereign:
|
| 341 |
if calc_quote_vol < MIN_VOLUME_THRESHOLD:
|
| 342 |
reject_stats["volume"] += 1
|
|
|
|
| 369 |
# 🧭 The Diagnoser
|
| 370 |
# ------------------------------------------------------------------
|
| 371 |
def _diagnose_asset_regime(self, item: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
|
|
| 372 |
try:
|
| 373 |
if 'df_1h' not in item:
|
|
|
|
| 374 |
if 'ohlcv_1h_raw' in item:
|
| 375 |
item['df_1h'] = self._calc_indicators(item['ohlcv_1h_raw'])
|
| 376 |
else:
|
|
|
|
| 423 |
c['ohlcv'] = {'1h': h1, '15m': m15}
|
| 424 |
c['ohlcv_1h_raw'] = h1
|
| 425 |
c['ohlcv_15m_raw'] = m15
|
|
|
|
| 426 |
c['df_1h'] = self._calc_indicators(h1)
|
| 427 |
return c
|
| 428 |
except: return None
|
|
|
|
| 435 |
rs = gain/loss
|
| 436 |
df['rsi'] = 100 - (100/(1+rs))
|
| 437 |
|
|
|
|
| 438 |
df['ema20'] = df['c'].ewm(span=20).mean()
|
| 439 |
df['ema50'] = df['c'].ewm(span=50).mean()
|
| 440 |
df['ema200'] = df['c'].ewm(span=200).mean()
|
| 441 |
|
|
|
|
| 442 |
tr = np.maximum(df['h']-df['l'], np.maximum(abs(df['h']-df['c'].shift()), abs(df['l']-df['c'].shift())))
|
| 443 |
df['atr'] = tr.rolling(14).mean()
|
| 444 |
|
|
|
|
| 445 |
std = df['c'].rolling(20).std()
|
| 446 |
df['upper_bb'] = df['ema20'] + (2 * std)
|
| 447 |
df['lower_bb'] = df['ema20'] - (2 * std)
|