Riy777 commited on
Commit
ee44b47
·
verified ·
1 Parent(s): 55bc878

Update ml_engine/data_manager.py

Browse files
Files changed (1) hide show
  1. ml_engine/data_manager.py +273 -264
ml_engine/data_manager.py CHANGED
@@ -1,6 +1,6 @@
1
  # ============================================================
2
  # 📂 ml_engine/data_manager.py
3
- # (V45.0 - GEM-Architect: Anti-FOMO Revival)
4
  # ============================================================
5
 
6
  import asyncio
@@ -9,12 +9,12 @@ import traceback
9
  import logging
10
  import pandas as pd
11
  import numpy as np
12
- import pandas_ta as ta # سنحتاج بعض الدوال المساعدة
13
- from typing import List, Dict, Any
14
 
15
  import ccxt.async_support as ccxt
16
 
17
- # ✅ استيراد الدستور الديناميكي (للحفاظ على توافق النظام)
18
  try:
19
  from ml_engine.processor import SystemLimits
20
  except ImportError:
@@ -27,10 +27,10 @@ logging.getLogger("ccxt").setLevel(logging.WARNING)
27
 
28
  class DataManager:
29
  """
30
- DataManager V45.0 (Anti-FOMO Revival)
31
- - Restores the STRICT Logic Tree from V15.2.
32
- - Filters: 8% Max Pump, 12% Max Daily, RSI < 70 strict limit.
33
- - Targets: Clean Breakouts & Oversold Reversals ONLY.
34
  """
35
 
36
  def __init__(self, contracts_db, whale_monitor, r2_service=None):
@@ -47,20 +47,24 @@ class DataManager:
47
  self.http_client = None
48
  self.market_cache = {}
49
 
 
 
 
50
  # القائمة السوداء
51
  self.BLACKLIST_TOKENS = [
52
  'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
53
  'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', 'USDD', 'USDP', 'HT', 'KCS'
54
  ]
55
 
56
- print(f"📦 [DataManager V45.0] Anti-FOMO Shield Active.")
57
 
58
  async def initialize(self):
59
- print(" > [DataManager] Starting initialization...")
60
  try:
61
  self.http_client = httpx.AsyncClient(timeout=30.0)
62
  await self._load_markets()
63
- print(f"✅ [DataManager] Ready (Logic: STRICT Anti-FOMO).")
 
64
  except Exception as e:
65
  print(f"❌ [DataManager] Init Error: {e}")
66
  traceback.print_exc()
@@ -81,321 +85,326 @@ class DataManager:
81
  try:
82
  self.contracts_db = await self.r2_service.load_contracts_db_async()
83
  except: self.contracts_db = {}
84
-
85
  def get_contracts_db(self): return self.contracts_db
86
 
87
  # ==================================================================
88
- # 🛡️ Layer 1: The Strict Logic Tree (From V15.2)
89
  # ==================================================================
90
- async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
91
- print(f"🔍 [L1 Anti-FOMO] Filtering Universe...")
92
-
93
- # 1. المرحلة 0: فلتر الكون (السيولة العالية فقط)
94
- # V15.2 كان يطلب مليون دولار سيولة، سنبقيه كما هو للصرامة
95
- initial_candidates = await self._stage0_universe_filter()
96
-
97
- if not initial_candidates:
98
- print("⚠️ [Layer 1] Universe empty.")
99
- return []
100
-
101
- # 2. جلب البيانات الفنية لأفضل 300 عملة (كما في V15.2)
102
- top_liquid_candidates = initial_candidates[:300]
103
- print(f" -> Analyzing top {len(top_liquid_candidates)} liquid assets...")
104
-
105
- enriched_data = await self._fetch_technical_data_batch(top_liquid_candidates)
106
-
107
- # 3. تطبيق شجرة القرار الصارمة
108
- breakout_list = []
109
- reversal_list = []
110
-
111
- for item in enriched_data:
112
- # هنا نستخدم منطق V15.2 الأصلي
113
- classification = self._apply_logic_tree(item)
114
 
115
- if classification['type'] == 'BREAKOUT':
116
- item['l1_score'] = classification['score']
117
- item['type'] = 'BREAKOUT'
118
- breakout_list.append(item)
119
- elif classification['type'] == 'REVERSAL':
120
- item['l1_score'] = classification['score']
121
- item['type'] = 'REVERSAL'
122
- reversal_list.append(item)
123
 
124
- print(f" -> [L1 Logic] Found: {len(breakout_list)} Breakouts, {len(reversal_list)} Reversals.")
 
 
125
 
126
- # 4. الترتيب والدمج النهائي
127
- # الـ Breakout نرتبهم بالأعلى سكور (فوليوم)
128
- breakout_list.sort(key=lambda x: x['l1_score'], reverse=True)
129
- # الـ Reversal نرتبهم بالأعلى سكور (سكور الارتداد في V15.2 كان 100-RSI، يعني الأعلى أفضل)
130
- reversal_list.sort(key=lambda x: x['l1_score'], reverse=True)
 
 
131
 
132
- # نختار الأفضل فقط (مزيج متوازن)
133
- final_selection = breakout_list[:25] + reversal_list[:15]
134
-
135
- return [
136
- {
137
- 'symbol': c['symbol'],
138
- 'quote_volume': c.get('quote_volume', 0),
139
- 'current_price': c.get('current_price', 0),
140
- 'type': c.get('type', 'UNKNOWN'),
141
- 'l1_score': c.get('l1_score', 0)
142
- }
143
- for c in final_selection
144
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
145
 
146
  # ==================================================================
147
- # 🔗 Bridge for Backtest Engine Compatibility (IMPORTANT)
148
  # ==================================================================
149
- def _calculate_structural_score(self, df: pd.DataFrame, symbol: str, regime: str) -> (float, List[str]):
150
  """
151
- [Compatibility Wrapper]
152
- هذه الدالة موجودة لكي لا يتعطل محرك الباكتست (backtest_engine.py).
153
- تقوم بتحويل بيانات الباكتست إلى تنسيق يفهمه منطق V15.2.
154
  """
155
- # محاكاة تنسيق البيانات الذي يطلبه _apply_logic_tree
156
- # نحتاج تقسيم الـ DF إلى 1H و 15M تقريبياً
157
  try:
158
- # Resample لإنشاء بيانات 1H و 15M من البيانات المدخلة (التي غالباً تكون 15M)
159
- df_15m = df.copy()
160
-
161
- agg_dict = {'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum'}
162
- df_1h = df.resample('1H').agg(agg_dict).dropna()
163
 
164
- # تحويلها لقوائم كما يتوقع الكود القديم
165
- ohlcv_1h = df_1h.reset_index()[['timestamp', 'open', 'high', 'low', 'close', 'volume']].values.tolist()
166
- ohlcv_15m = df_15m.reset_index()[['timestamp', 'open', 'high', 'low', 'close', 'volume']].values.tolist()
167
 
168
- dummy_data = {
169
- 'ohlcv_1h': ohlcv_1h,
170
- 'ohlcv_15m': ohlcv_15m,
171
- 'change_24h': 0.0 # غير متوفر بدقة في الباكتست الجزئي، نتجاوزه
172
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
- res = self._apply_logic_tree(dummy_data)
 
175
 
176
- score = res.get('score', 0.0)
177
- # تحويل السكور ليكون متوافقاً مع الباكتست (حول 20-80)
178
- if res['type'] == 'BREAKOUT':
179
- return score * 20.0, ["BREAKOUT"] # Breakout score is usually low (ratio), boost it
180
- elif res['type'] == 'REVERSAL':
181
- return score, ["REVERSAL"] # Reversal score is already 0-100
182
 
183
- return 0.0, ["NONE"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
184
 
185
- except Exception:
186
- return 0.0, ["ERROR"]
 
 
 
 
 
187
 
188
  # ==================================================================
189
- # 🏗️ V15.2 Logic Core (Unchanged Logic)
190
  # ==================================================================
191
- def _apply_logic_tree(self, data: Dict[str, Any]) -> Dict[str, Any]:
192
  try:
193
  df_1h = self._calc_indicators(data['ohlcv_1h'])
194
- df_15m = self._calc_indicators(data['ohlcv_15m'])
195
- except:
196
- return {'type': 'NONE'}
197
-
198
- if df_1h.empty or df_15m.empty: return {'type': 'NONE'}
 
 
 
 
 
 
 
199
 
200
- curr_1h = df_1h.iloc[-1]
201
- curr_15m = df_15m.iloc[-1]
202
-
203
- # --- Stage 2: Anti-FOMO Filters (STRICT) ---
204
  try:
205
- # حساب التغير في آخر 4 ساعات
206
- if len(df_1h) >= 5:
207
- close_4h_ago = df_1h.iloc[-5]['close']
208
- change_4h = ((curr_1h['close'] - close_4h_ago) / close_4h_ago) * 100
209
- else:
210
- change_4h = 0.0
211
- except: change_4h = 0.0
 
 
 
 
212
 
213
- # 1. فلتر المضخات: ممنوع الدخول إذا صعدت أكثر من 8% في 4 ساعات
214
- if change_4h > 8.0: return {'type': 'NONE'}
 
 
 
 
 
 
 
215
 
216
- # 2. فلتر التذبذب اليومي: ممنوع أكثر من 12% (للبعد عن العملات المجنونة)
217
- if data.get('change_24h', 0) > 12.0: return {'type': 'NONE'}
218
 
219
- # 3. فلتر القمة: ممنوع RSI فوق 70 قطعاً
 
220
  if curr_1h['rsi'] > 70: return {'type': 'NONE'}
221
-
222
- # 4. فلتر الامتداد: ممنوع الابتعاد عن المتوسط كثيراً
223
- deviation = (curr_1h['close'] - curr_1h['ema20']) / curr_1h['atr'] if curr_1h['atr'] > 0 else 0
224
- if deviation > 1.8: return {'type': 'NONE'}
225
-
226
- # --- Stage 3: Setup Classification ---
227
-
228
- # === A. Breakout Logic ===
229
- is_breakout = False
230
- breakout_score = 0.0
231
 
232
- # تريند صاعد
233
- bullish_structure = (curr_1h['ema20'] > curr_1h['ema50']) or (curr_1h['close'] > curr_1h['ema20'])
234
-
235
- if bullish_structure:
236
- # RSI يجب أن يكون فيه مساحة للصعود (ليس منخفضاً جداً ولا مرتفعاً جداً)
237
- if 45 <= curr_1h['rsi'] <= 68:
238
- if curr_15m['close'] >= curr_15m['ema20']:
239
- # Volatility Squeeze (هدوء ما قبل العاصفة)
240
- avg_range = (df_15m['high'] - df_15m['low']).rolling(10).mean().iloc[-1]
241
- current_range = curr_15m['high'] - curr_15m['low']
242
-
243
- if current_range <= avg_range * 1.8:
244
- vol_ma20 = df_15m['volume'].rolling(20).mean().iloc[-1]
245
- # شرط الفوليوم: شمعة الحالية فيها سيولة 1.5 ضعف المتوسط
246
- if curr_15m['volume'] >= 1.5 * vol_ma20:
247
- is_breakout = True
248
- breakout_score = curr_15m['volume'] / vol_ma20 if vol_ma20 > 0 else 1.0
249
-
250
- if is_breakout:
251
- return {'type': 'BREAKOUT', 'score': breakout_score}
252
-
253
- # === B. Reversal Logic ===
254
- is_reversal = False
255
- reversal_score = 0.0
256
 
257
- # تشبع بيعي واضح
258
- if 20 <= curr_1h['rsi'] <= 40:
259
- # السعر هبط مؤخراً
260
- if change_4h <= -2.0:
261
- # البحث عن شمعة انعكاسية (Hammer / Green Body)
262
- last_3 = df_15m.iloc[-3:]
263
- found_rejection = False
264
- for _, row in last_3.iterrows():
265
- rng = row['high'] - row['low']
266
- if rng > 0:
267
- is_green = row['close'] > row['open']
268
- # Hammer pattern logic
269
- lower_wick = min(row['open'], row['close']) - row['low']
270
- body = abs(row['close'] - row['open'])
271
- hammer_shape = lower_wick > (body * 1.5)
272
-
273
- if is_green or hammer_shape:
274
- found_rejection = True
275
- break
276
-
277
- if found_rejection:
278
- is_reversal = True
279
- # السكور كلما قل الـ RSI كان أفضل للارتداد
280
- reversal_score = (100 - curr_1h['rsi'])
281
-
282
- if is_reversal:
283
- return {'type': 'REVERSAL', 'score': reversal_score}
284
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
285
  return {'type': 'NONE'}
286
 
287
- # ------------------------------------------------------------------
288
- # Manual Indicator Calculation (Pandas pure - Exactly like V15.2)
289
- # ------------------------------------------------------------------
290
  def _calc_indicators(self, ohlcv_list):
291
  if not ohlcv_list: return pd.DataFrame()
292
  df = pd.DataFrame(ohlcv_list, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
293
-
294
- # RSI Calculation
295
  delta = df['close'].diff()
296
  gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
297
  loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
298
  rs = gain / loss
299
  df['rsi'] = 100 - (100 / (1 + rs))
300
-
301
- # EMA
302
  df['ema20'] = df['close'].ewm(span=20, adjust=False).mean()
303
  df['ema50'] = df['close'].ewm(span=50, adjust=False).mean()
304
-
305
- # ATR
306
- high_low = df['high'] - df['low']
307
- high_close = np.abs(df['high'] - df['close'].shift())
308
- low_close = np.abs(df['low'] - df['close'].shift())
309
- ranges = pd.concat([high_low, high_close, low_close], axis=1)
310
- true_range = np.max(ranges, axis=1)
311
- df['atr'] = true_range.rolling(14).mean()
312
-
313
  df.fillna(0, inplace=True)
314
  return df
315
 
316
- # ==================================================================
317
- # 🌌 Stage 0: Universe Filter (V15.2 Logic)
318
- # ==================================================================
319
- async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
320
  try:
321
- tickers = await self.exchange.fetch_tickers()
322
- candidates = []
323
-
324
- for symbol, ticker in tickers.items():
325
- if not symbol.endswith('/USDT'): continue
326
-
327
- base_curr = symbol.split('/')[0]
328
- if any(bad in base_curr for bad in self.BLACKLIST_TOKENS): continue
329
-
330
- # شرط السيولة الصارم: 1 مليون دولار
331
- quote_vol = ticker.get('quoteVolume')
332
- if not quote_vol or quote_vol < 1_000_000: continue
333
-
334
- last_price = ticker.get('last')
335
- if not last_price or last_price < 0.0005: continue
336
-
337
- candidates.append({
338
- 'symbol': symbol,
339
- 'quote_volume': quote_vol,
340
- 'current_price': last_price,
341
- 'change_24h': float(ticker.get('percentage', 0.0))
342
- })
343
-
344
- # ترتيب مبدئي بالحجم
345
- candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
346
- return candidates
347
-
348
- except Exception as e:
349
- print(f"❌ [L1 Error] Universe filter failed: {e}")
350
- return []
351
 
352
- # ==================================================================
353
- # 🔄 Batch Fetching
354
- # ==================================================================
355
  async def _fetch_technical_data_batch(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
356
  chunk_size = 15
357
  results = []
358
  for i in range(0, len(candidates), chunk_size):
359
  chunk = candidates[i:i + chunk_size]
360
- chunk_tasks = [self._fetch_single_tech_data(c) for c in chunk]
361
- chunk_results = await asyncio.gather(*chunk_tasks)
362
- results.extend([r for r in chunk_results if r is not None])
363
  await asyncio.sleep(0.05)
364
  return results
365
 
366
  async def _fetch_single_tech_data(self, candidate: Dict[str, Any]) -> Any:
367
- symbol = candidate['symbol']
368
  try:
369
- # V15.2 Requires 1H and 15M
370
- ohlcv_1h = await self.exchange.fetch_ohlcv(symbol, '1h', limit=60)
371
- ohlcv_15m = await self.exchange.fetch_ohlcv(symbol, '15m', limit=60)
372
-
373
- if not ohlcv_1h or len(ohlcv_1h) < 55 or not ohlcv_15m or len(ohlcv_15m) < 55:
374
- return None
375
-
376
- candidate['ohlcv_1h'] = ohlcv_1h
377
- candidate['ohlcv_15m'] = ohlcv_15m
378
  return candidate
379
- except Exception:
380
- return None
381
-
382
- # ==================================================================
383
- # 🎯 Public Helpers
384
- # ==================================================================
385
  async def get_latest_price_async(self, symbol: str) -> float:
386
- try:
387
- ticker = await self.exchange.fetch_ticker(symbol)
388
- return float(ticker['last'])
389
- except Exception: return 0.0
390
-
391
  async def get_latest_ohlcv(self, symbol: str, timeframe: str = '5m', limit: int = 100) -> List[List[float]]:
392
- try:
393
- candles = await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
394
- return candles or []
395
- except Exception: return []
396
-
397
  async def get_order_book_snapshot(self, symbol: str, limit: int = 20) -> Dict[str, Any]:
398
- try:
399
- ob = await self.exchange.fetch_order_book(symbol, limit)
400
- return ob
401
- except Exception: return {}
 
1
  # ============================================================
2
  # 📂 ml_engine/data_manager.py
3
+ # (V47.5 - GEM-Architect: Flash-Pulse & Hyper-Matrix)
4
  # ============================================================
5
 
6
  import asyncio
 
9
  import logging
10
  import pandas as pd
11
  import numpy as np
12
+ import pandas_ta as ta
13
+ from typing import List, Dict, Any, Tuple
14
 
15
  import ccxt.async_support as ccxt
16
 
17
+ # ✅ استيراد الدستور الديناميكي
18
  try:
19
  from ml_engine.processor import SystemLimits
20
  except ImportError:
 
27
 
28
  class DataManager:
29
  """
30
+ DataManager V47.5 (Flash-Pulse Edition)
31
+ - Hyper-Matrix Sensor: Detects BULL, BEAR, RANGE, DEAD.
32
+ - Flash-Pulse Filter: Uses 24h Ticker Data (Change, Range Pos, Spread)
33
+ to pre-sort candidates BEFORE fetching candles.
34
  """
35
 
36
  def __init__(self, contracts_db, whale_monitor, r2_service=None):
 
47
  self.http_client = None
48
  self.market_cache = {}
49
 
50
+ self.NETWORK_HEADS = ['ETH/USDT', 'SOL/USDT', 'BNB/USDT', 'AVAX/USDT']
51
+ self.BENCHMARK_SYMBOL = 'BTC/USDT'
52
+
53
  # القائمة السوداء
54
  self.BLACKLIST_TOKENS = [
55
  'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
56
  'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', 'USDD', 'USDP', 'HT', 'KCS'
57
  ]
58
 
59
+ print(f"📦 [DataManager V47.5] Flash-Pulse Engine Active.")
60
 
61
  async def initialize(self):
62
+ print(" > [DataManager] Starting Matrix Initialization...")
63
  try:
64
  self.http_client = httpx.AsyncClient(timeout=30.0)
65
  await self._load_markets()
66
+ await self.update_hyper_regime()
67
+ print(f"✅ [DataManager] Ready | Regime: {SystemLimits.CURRENT_REGIME}")
68
  except Exception as e:
69
  print(f"❌ [DataManager] Init Error: {e}")
70
  traceback.print_exc()
 
85
  try:
86
  self.contracts_db = await self.r2_service.load_contracts_db_async()
87
  except: self.contracts_db = {}
88
+
89
  def get_contracts_db(self): return self.contracts_db
90
 
91
  # ==================================================================
92
+ # 🧠 The Hyper-Sensor (Matrix Logic)
93
  # ==================================================================
94
+ async def update_hyper_regime(self):
95
+ print(" 🧠 [Matrix] Scanning Market Dimensions...")
96
+ try:
97
+ fg_index, fg_label = await self._fetch_fear_greed()
98
+ btc_data = await self._analyze_single_asset(self.BENCHMARK_SYMBOL)
99
+ breadth_score, heads_details = await self._analyze_market_breadth()
100
+
101
+ regime = "RANGE"
102
+
103
+ # Synthesis Logic
104
+ if btc_data['trend'] == 'BULL':
105
+ if breadth_score >= 0.50: regime = "BULL"
106
+ else: regime = "RANGE"
107
+ elif btc_data['trend'] == 'BEAR':
108
+ if fg_index < 20: regime = "BEAR" # Capitulation Hunting
109
+ else: regime = "BEAR"
110
+ elif btc_data['trend'] == 'NEUTRAL':
111
+ if btc_data['volatility_state'] == 'LOW' and breadth_score < 0.3: regime = "DEAD"
112
+ else: regime = "RANGE"
113
+
114
+ SystemLimits.CURRENT_REGIME = regime
 
 
 
115
 
116
+ print(f" 🌍 Regime: {regime} | FG: {fg_index} | Breadth: {breadth_score*100:.0f}% | BTC: {btc_data['trend']}")
 
 
 
 
 
 
 
117
 
118
+ except Exception as e:
119
+ print(f"❌ [Matrix Error] {e}")
120
+ SystemLimits.CURRENT_REGIME = "RANGE"
121
 
122
+ # --- Sensor Helpers ---
123
+ async def _fetch_fear_greed(self) -> Tuple[int, str]:
124
+ try:
125
+ resp = await self.http_client.get("https://api.alternative.me/fng/?limit=1")
126
+ data = resp.json()
127
+ return int(data['data'][0]['value']), data['data'][0]['value_classification']
128
+ except: return 50, "Neutral"
129
 
130
+ async def _analyze_single_asset(self, symbol) -> Dict[str, Any]:
131
+ try:
132
+ ohlcv = await self.exchange.fetch_ohlcv(symbol, '1d', limit=100)
133
+ df = pd.DataFrame(ohlcv, columns=['ts', 'o', 'h', 'l', 'c', 'v'])
134
+ c = df['c']
135
+ ema50 = ta.ema(c, length=50).iloc[-1]
136
+ ema200 = ta.ema(c, length=200).iloc[-1]
137
+ atr = ta.atr(df['h'], df['l'], c, length=14).iloc[-1]
138
+ price = c.iloc[-1]
139
+
140
+ trend = "NEUTRAL"
141
+ if price > ema50 and ema50 > ema200: trend = "BULL"
142
+ elif price < ema50 and price < ema200: trend = "BEAR"
143
+
144
+ vol_state = "NORMAL"
145
+ if (atr / price) < 0.025: vol_state = "LOW"
146
+
147
+ return {'trend': trend, 'volatility_state': vol_state}
148
+ except: return {'trend': 'NEUTRAL', 'volatility_state': 'NORMAL'}
149
+
150
+ async def _analyze_market_breadth(self) -> Tuple[float, str]:
151
+ tasks = [self._analyze_single_asset(sym) for sym in self.NETWORK_HEADS]
152
+ results = await asyncio.gather(*tasks, return_exceptions=True)
153
+ bull_count = 0; valid = 0; details = []
154
+ for i, res in enumerate(results):
155
+ if isinstance(res, dict):
156
+ valid += 1
157
+ if res['trend'] == 'BULL': bull_count += 1
158
+ details.append(f"{self.NETWORK_HEADS[i].split('/')[0]}:{res['trend'][0]}")
159
+ return (bull_count / valid) if valid > 0 else 0.0, "|".join(details)
160
 
161
  # ==================================================================
162
+ # Stage 0: The Flash-Pulse Filter (New & Improved)
163
  # ==================================================================
164
+ async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
165
  """
166
+ Uses Ticker Snapshot (Percentage, High, Low, Volume, Spread)
167
+ to filter candidates BEFORE fetching candles.
 
168
  """
 
 
169
  try:
170
+ regime = SystemLimits.CURRENT_REGIME
171
+ print(f" ⚡ [Flash-Pulse] Sorting Universe for {regime}...")
 
 
 
172
 
173
+ tickers = await self.exchange.fetch_tickers()
174
+ candidates = []
 
175
 
176
+ for symbol, ticker in tickers.items():
177
+ if not symbol.endswith('/USDT'): continue
178
+ if any(bad in symbol for bad in self.BLACKLIST_TOKENS): continue
179
+
180
+ # Basic Data
181
+ last = ticker.get('last')
182
+ if not last or last < 0.0005: continue # Ignore penny dust
183
+
184
+ quote_vol = ticker.get('quoteVolume', 0)
185
+ if quote_vol < 1_000_000: continue # Liquidity Floor
186
+
187
+ # 1. Spread Check (Quality Control)
188
+ bid = ticker.get('bid'); ask = ticker.get('ask')
189
+ if bid and ask and bid > 0:
190
+ spread_pct = (ask - bid) / bid
191
+ if spread_pct > 0.01: continue # Skip if spread > 1% (Illiquid/Risky)
192
+
193
+ # 2. Advanced Metrics
194
+ change_24h = float(ticker.get('percentage', 0.0))
195
+ high_24h = ticker.get('high')
196
+ low_24h = ticker.get('low')
197
+
198
+ # Position in Daily Range (0.0 = Low, 1.0 = High)
199
+ range_pos = 0.5
200
+ if high_24h and low_24h and high_24h > low_24h:
201
+ range_pos = (last - low_24h) / (high_24h - low_24h)
202
+
203
+ # 3. Regime-Specific Scoring (The Magic)
204
+ sort_score = 0.0
205
+
206
+ if regime == "BULL":
207
+ # We want leaders: High Volume + Positive Change + Near Highs
208
+ # But not too extended (>30% is dangerous FOMO)
209
+ if 2.0 <= change_24h <= 25.0:
210
+ sort_score = quote_vol * range_pos # Liquidity + Strength
211
+
212
+ elif regime == "BEAR":
213
+ # We want Capitulation: High Volume + Negative Change + Near Lows
214
+ if change_24h < -5.0 and range_pos < 0.2:
215
+ # High volume on a dump = Panic Selling = Reversal Opportunity
216
+ sort_score = quote_vol * (1 - range_pos) * abs(change_24h)
217
+
218
+ elif regime == "DEAD":
219
+ # We want pure Liquidity + Stability (Squeeze Candidates)
220
+ if -3.0 < change_24h < 3.0:
221
+ sort_score = quote_vol # Just show me where the money is
222
+
223
+ else: # RANGE (Anti-FOMO)
224
+ # We want active coins but not pumped ones
225
+ if -8.0 < change_24h < 8.0:
226
+ sort_score = quote_vol
227
+
228
+ if sort_score > 0:
229
+ candidates.append({
230
+ 'symbol': symbol,
231
+ 'quote_volume': quote_vol,
232
+ 'current_price': last,
233
+ 'change_24h': change_24h,
234
+ 'range_pos': range_pos,
235
+ 'sort_score': sort_score
236
+ })
237
 
238
+ # Sort by the Regime-Specific Score
239
+ candidates.sort(key=lambda x: x['sort_score'], reverse=True)
240
 
241
+ # Return top 300 for Deep Analysis
242
+ return candidates[:300]
 
 
 
 
243
 
244
+ except Exception as e:
245
+ print(f"❌ [Flash-Pulse Error] {e}")
246
+ return []
247
+
248
+ # ==================================================================
249
+ # 🛡️ Layer 1: Polymorphic Screening
250
+ # ==================================================================
251
+ async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
252
+ # 1. Update Matrix
253
+ await self.update_hyper_regime()
254
+ regime = SystemLimits.CURRENT_REGIME
255
+
256
+ # 2. Flash-Pulse Filter (Smart Universe Selection)
257
+ initial_candidates = await self._stage0_universe_filter()
258
+ if not initial_candidates: return []
259
+
260
+ # 3. Fetch Technicals
261
+ enriched_data = await self._fetch_technical_data_batch(initial_candidates)
262
+ filtered_list = []
263
+
264
+ # 4. Logic Switcher
265
+ for item in enriched_data:
266
+ classification = {'type': 'NONE', 'score': 0.0}
267
+ if regime == "BULL": classification = self._apply_bull_logic(item)
268
+ elif regime == "BEAR": classification = self._apply_bear_logic(item)
269
+ elif regime == "DEAD": classification = self._apply_dead_logic(item)
270
+ else: classification = self._apply_range_logic(item)
271
 
272
+ if classification['type'] != 'NONE':
273
+ item['l1_score'] = classification['score']
274
+ item['type'] = classification['type']
275
+ filtered_list.append(item)
276
+
277
+ filtered_list.sort(key=lambda x: x['l1_score'], reverse=True)
278
+ return filtered_list[:40]
279
 
280
  # ==================================================================
281
+ # 🐂 Logic: BULL
282
  # ==================================================================
283
+ def _apply_bull_logic(self, data: Dict[str, Any]) -> Dict[str, Any]:
284
  try:
285
  df_1h = self._calc_indicators(data['ohlcv_1h'])
286
+ if df_1h.empty: return {'type': 'NONE'}
287
+ curr = df_1h.iloc[-1]
288
+
289
+ if not (curr['close'] > curr['ema50']): return {'type': 'NONE'}
290
+ if self._get_change_4h(df_1h) > 20.0: return {'type': 'NONE'}
291
+
292
+ if 50 <= curr['rsi'] <= 80:
293
+ vol_ma = df_1h['volume'].rolling(20).mean().iloc[-1]
294
+ if curr['volume'] > vol_ma * 1.3:
295
+ return {'type': 'BULL_MOMENTUM', 'score': curr['volume'] / vol_ma}
296
+ except: pass
297
+ return {'type': 'NONE'}
298
 
299
+ # ==================================================================
300
+ # 🐻 Logic: BEAR
301
+ # ==================================================================
302
+ def _apply_bear_logic(self, data: Dict[str, Any]) -> Dict[str, Any]:
303
  try:
304
+ df_1h = self._calc_indicators(data['ohlcv_1h'])
305
+ if df_1h.empty: return {'type': 'NONE'}
306
+ curr = df_1h.iloc[-1]
307
+
308
+ if self._get_change_4h(df_1h) > 4.0: return {'type': 'NONE'}
309
+ if curr['rsi'] < 32:
310
+ dist_ema = (curr['ema20'] - curr['close']) / curr['close']
311
+ if dist_ema > 0.04:
312
+ return {'type': 'BEAR_OVERSOLD', 'score': (32 - curr['rsi']) * 2}
313
+ except: pass
314
+ return {'type': 'NONE'}
315
 
316
+ # ==================================================================
317
+ # 🦀 Logic: RANGE
318
+ # ==================================================================
319
+ def _apply_range_logic(self, data: Dict[str, Any]) -> Dict[str, Any]:
320
+ try:
321
+ df_1h = self._calc_indicators(data['ohlcv_1h'])
322
+ df_15m = self._calc_indicators(data['ohlcv_15m'])
323
+ except: return {'type': 'NONE'}
324
+ if df_1h.empty or df_15m.empty: return {'type': 'NONE'}
325
 
326
+ curr_1h = df_1h.iloc[-1]; curr_15m = df_15m.iloc[-1]
327
+ change_4h = self._get_change_4h(df_1h)
328
 
329
+ # Strict Filters
330
+ if change_4h > 8.0 or data.get('change_24h', 0) > 12.0: return {'type': 'NONE'}
331
  if curr_1h['rsi'] > 70: return {'type': 'NONE'}
 
 
 
 
 
 
 
 
 
 
332
 
333
+ # Breakout
334
+ if (curr_1h['ema20'] > curr_1h['ema50']) and 45 <= curr_1h['rsi'] <= 68:
335
+ vol_ma20 = df_15m['volume'].rolling(20).mean().iloc[-1]
336
+ if curr_15m['volume'] >= 1.5 * vol_ma20:
337
+ return {'type': 'RANGE_BREAKOUT', 'score': curr_15m['volume'] / vol_ma20}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
338
 
339
+ # Reversal
340
+ if 20 <= curr_1h['rsi'] <= 40 and change_4h <= -2.0:
341
+ return {'type': 'RANGE_REVERSAL', 'score': (100 - curr_1h['rsi'])}
342
+ return {'type': 'NONE'}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
 
344
+ # ==================================================================
345
+ # 🧟 Logic: DEAD
346
+ # ==================================================================
347
+ def _apply_dead_logic(self, data: Dict[str, Any]) -> Dict[str, Any]:
348
+ try:
349
+ df_1h = self._calc_indicators(data['ohlcv_1h'])
350
+ if df_1h.empty: return {'type': 'NONE'}
351
+ curr = df_1h.iloc[-1]
352
+ std = df_1h['close'].rolling(20).std().iloc[-1]
353
+ bb_width = (std * 4) / curr['close']
354
+ if bb_width < 0.04:
355
+ vol_ma = df_1h['volume'].rolling(20).mean().iloc[-1]
356
+ if curr['volume'] > vol_ma * 1.5:
357
+ return {'type': 'DEAD_SQUEEZE', 'score': (1/bb_width)}
358
+ except: pass
359
  return {'type': 'NONE'}
360
 
361
+ # ==================================================================
362
+ # 🏗️ Helpers
363
+ # ==================================================================
364
  def _calc_indicators(self, ohlcv_list):
365
  if not ohlcv_list: return pd.DataFrame()
366
  df = pd.DataFrame(ohlcv_list, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
 
 
367
  delta = df['close'].diff()
368
  gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
369
  loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
370
  rs = gain / loss
371
  df['rsi'] = 100 - (100 / (1 + rs))
 
 
372
  df['ema20'] = df['close'].ewm(span=20, adjust=False).mean()
373
  df['ema50'] = df['close'].ewm(span=50, adjust=False).mean()
 
 
 
 
 
 
 
 
 
374
  df.fillna(0, inplace=True)
375
  return df
376
 
377
+ def _get_change_4h(self, df_1h):
 
 
 
378
  try:
379
+ if len(df_1h) >= 5:
380
+ return ((df_1h.iloc[-1]['close'] - df_1h.iloc[-5]['close']) / df_1h.iloc[-5]['close']) * 100
381
+ return 0.0
382
+ except: return 0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
 
 
 
 
384
  async def _fetch_technical_data_batch(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
385
  chunk_size = 15
386
  results = []
387
  for i in range(0, len(candidates), chunk_size):
388
  chunk = candidates[i:i + chunk_size]
389
+ results.extend([r for r in await asyncio.gather(*[self._fetch_single_tech_data(c) for c in chunk]) if r])
 
 
390
  await asyncio.sleep(0.05)
391
  return results
392
 
393
  async def _fetch_single_tech_data(self, candidate: Dict[str, Any]) -> Any:
 
394
  try:
395
+ h1 = await self.exchange.fetch_ohlcv(candidate['symbol'], '1h', limit=60)
396
+ m15 = await self.exchange.fetch_ohlcv(candidate['symbol'], '15m', limit=60)
397
+ if not h1 or not m15: return None
398
+ candidate['ohlcv_1h'] = h1; candidate['ohlcv_15m'] = m15
 
 
 
 
 
399
  return candidate
400
+ except: return None
401
+
 
 
 
 
402
  async def get_latest_price_async(self, symbol: str) -> float:
403
+ try: return float((await self.exchange.fetch_ticker(symbol))['last'])
404
+ except: return 0.0
 
 
 
405
  async def get_latest_ohlcv(self, symbol: str, timeframe: str = '5m', limit: int = 100) -> List[List[float]]:
406
+ try: return await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
407
+ except: return []
 
 
 
408
  async def get_order_book_snapshot(self, symbol: str, limit: int = 20) -> Dict[str, Any]:
409
+ try: return await self.exchange.fetch_order_book(symbol, limit)
410
+ except: return {}