Riy777 commited on
Commit
b938152
·
verified ·
1 Parent(s): 400cb58

Update ml_engine/data_manager.py

Browse files
Files changed (1) hide show
  1. ml_engine/data_manager.py +59 -95
ml_engine/data_manager.py CHANGED
@@ -1,6 +1,6 @@
1
  # ============================================================
2
  # 📂 ml_engine/data_manager.py
3
- # (V41.2 - GEM-Architect: The Fixed Matrix & Debugger)
4
  # ============================================================
5
 
6
  import asyncio
@@ -14,38 +14,30 @@ 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:
21
- # Fallback إذا لم يتم التحميل بعد
22
  class SystemLimits:
23
  L1_MIN_AFFINITY_SCORE = 15.0
24
  CURRENT_REGIME = "RANGE"
25
  SCANNER_WEIGHTS = {"RSI_MOMENTUM": 0.3, "BB_BREAKOUT": 0.3, "MACD_CROSS": 0.2, "VOLUME_FLOW": 0.2}
26
 
27
- # إعدادات التسجيل لإسكات الإزعاج
28
  logging.getLogger("httpx").setLevel(logging.WARNING)
29
- logging.getLogger("httpcore").setLevel(logging.WARNING)
30
  logging.getLogger("ccxt").setLevel(logging.WARNING)
31
 
32
  class DataManager:
33
  """
34
- DataManager V41.2 (The Fixed Matrix)
35
- - Fixes the 'Suicidal Blacklist' bug.
36
- - Uses Scanner Matrix for L1 filtering.
37
- - Includes Deep Debugging logs.
38
  """
39
 
40
  def __init__(self, contracts_db, whale_monitor, r2_service=None):
41
- # ==================================================================
42
- # ⚙️ إعدادات التحكم والتهيئة
43
- # ==================================================================
44
  self.contracts_db = contracts_db or {}
45
  self.whale_monitor = whale_monitor
46
  self.r2_service = r2_service
47
 
48
- # إعداد عميل المنصة (KuCoin) مع تفعيل حدود السرعة
49
  self.exchange = ccxt.kucoin({
50
  'enableRateLimit': True,
51
  'timeout': 30000,
@@ -55,16 +47,14 @@ class DataManager:
55
  self.http_client = None
56
  self.market_cache = {}
57
 
58
- # 🚫 قوائم الاستبعاد (العملات المستقرة، الرافعة، والعملات المحظورة)
59
  self.BLACKLIST_TOKENS = [
60
  'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
61
  'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', 'USDD', 'USDP'
62
  ]
63
 
64
- print(f"📦 [DataManager V41.2] Scanner Matrix (Fixed & Debug) Online.")
65
 
66
  async def initialize(self):
67
- """تهيئة مدير البيانات والاتصالات"""
68
  print(" > [DataManager] Starting initialization...")
69
  try:
70
  self.http_client = httpx.AsyncClient(timeout=60.0)
@@ -75,92 +65,74 @@ class DataManager:
75
  traceback.print_exc()
76
 
77
  async def _load_markets(self):
78
- """تحميل أزواج التداول وحفظها في الذاكرة"""
79
  try:
80
- if self.exchange:
81
- # التأكد من عدم تحميل الأسواق مرتين إذا كانت محملة
82
- if not self.exchange.markets:
83
- await self.exchange.load_markets()
84
  self.market_cache = self.exchange.markets
85
- except Exception as e:
86
- print(f"❌ [DataManager] Market load failed: {e}")
87
- traceback.print_exc()
88
 
89
  async def close(self):
90
- """إغلاق الاتصالات بأمان"""
91
- if self.http_client:
92
- await self.http_client.aclose()
93
- if self.exchange:
94
- await self.exchange.close()
95
- print("🛑 [DataManager] Connections Closed.")
96
 
97
- # ==================================================================
98
- # 🚀 R2 Compatibility
99
- # ==================================================================
100
  async def load_contracts_from_r2(self):
101
  if not self.r2_service: return
102
  try:
103
  self.contracts_db = await self.r2_service.load_contracts_db_async()
104
- print(f" -> [DataManager] Contracts DB updated from R2: {len(self.contracts_db)} records.")
105
- except Exception as e:
106
- print(f"⚠️ [DataManager] R2 Load Warning: {e}")
107
- self.contracts_db = {}
108
 
109
- def get_contracts_db(self) -> Dict[str, Any]:
110
- return self.contracts_db
111
 
112
  # ==================================================================
113
- # 🛡️ Layer 1: Matrix Screening (The Core)
114
  # ==================================================================
115
  async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
116
- """
117
- تقوم باختيار استراتيجية المسح بناءً على مصفوفة الكاشفات (Scanner Matrix).
118
- """
119
- # 1. قراءة الإعدادات الحالية
120
  current_regime = getattr(SystemLimits, "CURRENT_REGIME", "RANGE")
121
  scanner_weights = getattr(SystemLimits, "SCANNER_WEIGHTS", {"RSI_MOMENTUM": 1.0})
122
  min_score = getattr(SystemLimits, "L1_MIN_AFFINITY_SCORE", 15.0)
123
 
124
  print(f"🔍 [L1 Matrix] Regime: {current_regime} | Weights: {scanner_weights}")
125
 
126
- # 2. جلب الكون الأولي (Universe) - باستخدام الدالة المصححة
127
  all_tickers = await self._fetch_universe_tickers()
128
  if not all_tickers:
129
  print("⚠️ [Layer 1] Universe fetch returned empty.")
130
  return []
131
 
132
- # 3. الجلب العميق للبيانات (Batch Fetch 15m)
133
- # نأخذ أفضل 80 عملة فقط لتجنب استهلاك الـ Rate Limit
134
- top_candidates = all_tickers[:80]
135
  enriched_data = await self._batch_fetch_ta_data(top_candidates, timeframe='15m', limit=100)
136
 
137
  scored_candidates = []
138
- debug_log_sample = [] # لعرض عينة من الدرجات للمراقبة
 
 
 
 
 
 
 
139
 
140
- # 4. تطبيق الكاشفات وحساب النقاط
141
  for item in enriched_data:
142
  df = item.get('df')
143
  if df is None or len(df) < 50: continue
144
 
145
- # تطبيق استراتيجيات الكشف (RSI, BB, MACD, etc.)
146
- scores = self._apply_scanner_strategies(df)
147
 
148
  final_score = 0.0
149
  tags = []
150
 
151
- # حساب المجموع الموزون
152
  for strategy, val in scores.items():
153
  w = scanner_weights.get(strategy, 0.0)
154
  final_score += (val['score'] * w)
155
  if val['active']: tags.append(strategy)
156
 
157
- # Boost: إضافة نقاط بسيطة إذا كان السعر يرتفع بقوة
158
  if item['change_24h'] > 3.0 and current_regime == "BULL": final_score += 10
159
 
160
  item['l1_score'] = final_score
161
  item['tags'] = tags
162
 
163
- # تسجيل عينة للمراقبة (للتشخيص)
164
  if len(debug_log_sample) < 3:
165
  rsi_val = scores.get('RSI_MOMENTUM', {}).get('val', 0)
166
  debug_details = f"{item['symbol']}: {final_score:.1f} (RSI:{rsi_val:.1f})"
@@ -169,16 +141,13 @@ class DataManager:
169
  if final_score >= min_score:
170
  scored_candidates.append(item)
171
 
172
- # ترتيب النتائج
173
  scored_candidates.sort(key=lambda x: x['l1_score'], reverse=True)
174
 
175
- # طباعة تقرير
176
  if debug_log_sample:
177
  print(f" -> [DEBUG L1] Sample Scores: { ' | '.join(debug_log_sample) }")
178
 
179
  print(f" -> Matrix selected {len(scored_candidates)} candidates (Threshold: {min_score}).")
180
 
181
- # نمرر أفضل 40 للمعالج
182
  return [
183
  {
184
  'symbol': c['symbol'],
@@ -191,33 +160,38 @@ class DataManager:
191
  ]
192
 
193
  # ==================================================================
194
- # 🧩 Scanner Strategies Logic (The Engines)
195
  # ==================================================================
196
- def _apply_scanner_strategies(self, df: pd.DataFrame) -> Dict[str, Any]:
197
- """تطبيق مؤشرات فنية متعددة على البيانات"""
198
  results = {}
199
  try:
 
 
 
200
  close = df['close']
201
 
202
- # 1. RSI (معالجة المنطقة 30-50)
203
  rsi = ta.rsi(close, length=14)
204
- curr_rsi = rsi.iloc[-1] if rsi is not None else 50
 
 
205
 
206
  score_rsi = 0
207
  active_rsi = False
208
 
209
  if 50 < curr_rsi < 75:
210
- score_rsi = 100 # زخم صاعد
211
  active_rsi = True
212
  elif curr_rsi <= 30:
213
- score_rsi = 80 # ارتداد
214
  active_rsi = True
215
  elif 30 < curr_rsi <= 50:
216
- score_rsi = 40 # ✅ منطقة محايدة (نقاط جزئية)
217
 
218
  results["RSI_MOMENTUM"] = {'score': score_rsi, 'active': active_rsi, 'val': curr_rsi}
219
 
220
- # 2. Bollinger Band Breakout
221
  bb = ta.bbands(close, length=20, std=2)
222
  if bb is not None:
223
  upper = bb[f'BBU_20_2.0'].iloc[-1]
@@ -232,20 +206,20 @@ class DataManager:
232
  score_bb = 0; active_bb = False
233
  results["BB_BREAKOUT"] = {'score': score_bb, 'active': active_bb}
234
 
235
- # 3. MACD Cross
236
  macd = ta.macd(close)
237
  if macd is not None:
238
  hist = macd[f'MACDh_12_26_9'].iloc[-1]
239
  score_macd = 0
240
  active_macd = False
241
- if hist > 0: # إيجابي
242
  score_macd = 100
243
  active_macd = True
244
  else:
245
  score_macd = 0; active_macd = False
246
  results["MACD_CROSS"] = {'score': score_macd, 'active': active_macd}
247
 
248
- # 4. Volume Flow
249
  vol = df['volume']
250
  vol_ma = ta.sma(vol, length=20).iloc[-1]
251
  curr_vol = vol.iloc[-1]
@@ -256,18 +230,18 @@ class DataManager:
256
  active_vol = True
257
  results["VOLUME_FLOW"] = {'score': score_vol, 'active': active_vol}
258
 
259
- except Exception:
 
 
 
260
  return {k: {'score': 0, 'active': False, 'val': 0} for k in ["RSI_MOMENTUM", "BB_BREAKOUT", "MACD_CROSS", "VOLUME_FLOW"]}
261
 
262
  return results
263
 
264
  # ==================================================================
265
- # 🌍 Stage 0 & Pre-Filters (FIXED)
266
  # ==================================================================
267
  async def _fetch_universe_tickers(self) -> List[Dict[str, Any]]:
268
- """
269
- جلب وتصفية العملات الأولية (مصحح لتجنب حظر USDT بالخطأ).
270
- """
271
  print(" -> 📡 [Debug] Contacting Exchange for Tickers...")
272
  try:
273
  tickers = await self.exchange.fetch_tickers()
@@ -277,20 +251,16 @@ class DataManager:
277
  skipped_reason = {"pair": 0, "blacklist": 0, "volume": 0}
278
 
279
  for symbol, ticker in tickers.items():
280
- # 1. فلتر الزوج (USDT Only)
281
  if not symbol.endswith('/USDT'):
282
  skipped_reason["pair"] += 1
283
  continue
284
 
285
- # 🔥 FIX: نفصل العملة الأساسية للحظر 🔥
286
  base_currency = symbol.split('/')[0]
287
 
288
- # 2. القائمة السوداء (فحص العملة الأساسية فقط)
289
  if any(bad in base_currency for bad in self.BLACKLIST_TOKENS):
290
  skipped_reason["blacklist"] += 1
291
  continue
292
 
293
- # 3. فلتر السيولة
294
  vol = ticker.get('quoteVolume')
295
  if vol is None:
296
  vol = ticker.get('info', {}).get('volValue')
@@ -298,7 +268,7 @@ class DataManager:
298
  if vol is None: vol = 0.0
299
  else: vol = float(vol)
300
 
301
- if vol < 100_000: # 100k للتشخيص والتجربة
302
  skipped_reason["volume"] += 1
303
  continue
304
 
@@ -317,15 +287,11 @@ class DataManager:
317
 
318
  except Exception as e:
319
  print(f"❌ [L1 Error] Fetch Tickers Failed: {e}")
320
- traceback.print_exc()
321
  return []
322
 
323
- # ==================================================================
324
- # ⚡ Batch Fetching Utilities
325
- # ==================================================================
326
  async def _batch_fetch_ta_data(self, candidates, timeframe='15m', limit=100):
327
  results = []
328
- chunk_size = 15 # لعدم تجاوز الحدود
329
  for i in range(0, len(candidates), chunk_size):
330
  chunk = candidates[i:i+chunk_size]
331
  tasks = [self._fetch_ohlcv_safe(c, timeframe, limit) for c in chunk]
@@ -348,23 +314,21 @@ class DataManager:
348
  # ==================================================================
349
  # 🎯 Public Helpers
350
  # ==================================================================
351
- async def get_latest_price_async(self, symbol: str) -> float:
352
  try:
353
  ticker = await self.exchange.fetch_ticker(symbol)
354
  return float(ticker['last'])
355
- except Exception: return 0.0
356
 
357
- async def get_latest_ohlcv(self, symbol: str, timeframe: str = '5m', limit: int = 100) -> List[List[float]]:
358
  try:
359
- candles = await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
360
- return candles or []
361
- except Exception: return []
362
 
363
- async def get_order_book_snapshot(self, symbol: str, limit: int = 20) -> Dict[str, Any]:
364
  try:
365
- ob = await self.exchange.fetch_order_book(symbol, limit)
366
- return ob
367
- except Exception: return {}
368
 
369
  def get_supported_timeframes(self):
370
  return list(self.exchange.timeframes.keys()) if self.exchange else []
 
1
  # ============================================================
2
  # 📂 ml_engine/data_manager.py
3
+ # (V41.3 - GEM-Architect: Math Debugger Edition)
4
  # ============================================================
5
 
6
  import asyncio
 
14
 
15
  import ccxt.async_support as ccxt
16
 
17
+ # ✅ استيراد الدستور الديناميكي
18
  try:
19
  from ml_engine.processor import SystemLimits
20
  except ImportError:
 
21
  class SystemLimits:
22
  L1_MIN_AFFINITY_SCORE = 15.0
23
  CURRENT_REGIME = "RANGE"
24
  SCANNER_WEIGHTS = {"RSI_MOMENTUM": 0.3, "BB_BREAKOUT": 0.3, "MACD_CROSS": 0.2, "VOLUME_FLOW": 0.2}
25
 
 
26
  logging.getLogger("httpx").setLevel(logging.WARNING)
 
27
  logging.getLogger("ccxt").setLevel(logging.WARNING)
28
 
29
  class DataManager:
30
  """
31
+ DataManager V41.3 (Math Debugger)
32
+ - Exposes calculation errors in _apply_scanner_strategies.
33
+ - Inspects DataFrame integrity.
 
34
  """
35
 
36
  def __init__(self, contracts_db, whale_monitor, r2_service=None):
 
 
 
37
  self.contracts_db = contracts_db or {}
38
  self.whale_monitor = whale_monitor
39
  self.r2_service = r2_service
40
 
 
41
  self.exchange = ccxt.kucoin({
42
  'enableRateLimit': True,
43
  'timeout': 30000,
 
47
  self.http_client = None
48
  self.market_cache = {}
49
 
 
50
  self.BLACKLIST_TOKENS = [
51
  'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
52
  'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', 'USDD', 'USDP'
53
  ]
54
 
55
+ print(f"📦 [DataManager V41.3] Math Debugger Online.")
56
 
57
  async def initialize(self):
 
58
  print(" > [DataManager] Starting initialization...")
59
  try:
60
  self.http_client = httpx.AsyncClient(timeout=60.0)
 
65
  traceback.print_exc()
66
 
67
  async def _load_markets(self):
 
68
  try:
69
+ if self.exchange and not self.exchange.markets:
70
+ await self.exchange.load_markets()
 
 
71
  self.market_cache = self.exchange.markets
72
+ except Exception: pass
 
 
73
 
74
  async def close(self):
75
+ if self.http_client: await self.http_client.aclose()
76
+ if self.exchange: await self.exchange.close()
 
 
 
 
77
 
 
 
 
78
  async def load_contracts_from_r2(self):
79
  if not self.r2_service: return
80
  try:
81
  self.contracts_db = await self.r2_service.load_contracts_db_async()
82
+ except: self.contracts_db = {}
 
 
 
83
 
84
+ def get_contracts_db(self): return self.contracts_db
 
85
 
86
  # ==================================================================
87
+ # 🛡️ Layer 1: Matrix Screening
88
  # ==================================================================
89
  async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
 
 
 
 
90
  current_regime = getattr(SystemLimits, "CURRENT_REGIME", "RANGE")
91
  scanner_weights = getattr(SystemLimits, "SCANNER_WEIGHTS", {"RSI_MOMENTUM": 1.0})
92
  min_score = getattr(SystemLimits, "L1_MIN_AFFINITY_SCORE", 15.0)
93
 
94
  print(f"🔍 [L1 Matrix] Regime: {current_regime} | Weights: {scanner_weights}")
95
 
96
+ # 1. جلب العملات
97
  all_tickers = await self._fetch_universe_tickers()
98
  if not all_tickers:
99
  print("⚠️ [Layer 1] Universe fetch returned empty.")
100
  return []
101
 
102
+ # 2. الجلب العميق (أخذ عينة أكبر قليلاً للتجربة)
103
+ top_candidates = all_tickers[:60]
 
104
  enriched_data = await self._batch_fetch_ta_data(top_candidates, timeframe='15m', limit=100)
105
 
106
  scored_candidates = []
107
+ debug_log_sample = []
108
+
109
+ # 🔍 فحص سلامة البيانات لأول عملة (Data Integrity Check)
110
+ if enriched_data:
111
+ first_coin = enriched_data[0]
112
+ if 'df' in first_coin:
113
+ print(f" -> 📊 [Data Inspect] {first_coin['symbol']} DF Shape: {first_coin['df'].shape}")
114
+ print(f" -> 📊 [Data Inspect] Tail:\n{first_coin['df'].tail(3)[['close', 'volume']]}")
115
 
 
116
  for item in enriched_data:
117
  df = item.get('df')
118
  if df is None or len(df) < 50: continue
119
 
120
+ # 🔥 تطبيق الكاشفات (سيتم طباعة الخطأ إذا حدث)
121
+ scores = self._apply_scanner_strategies(df, item['symbol'])
122
 
123
  final_score = 0.0
124
  tags = []
125
 
 
126
  for strategy, val in scores.items():
127
  w = scanner_weights.get(strategy, 0.0)
128
  final_score += (val['score'] * w)
129
  if val['active']: tags.append(strategy)
130
 
 
131
  if item['change_24h'] > 3.0 and current_regime == "BULL": final_score += 10
132
 
133
  item['l1_score'] = final_score
134
  item['tags'] = tags
135
 
 
136
  if len(debug_log_sample) < 3:
137
  rsi_val = scores.get('RSI_MOMENTUM', {}).get('val', 0)
138
  debug_details = f"{item['symbol']}: {final_score:.1f} (RSI:{rsi_val:.1f})"
 
141
  if final_score >= min_score:
142
  scored_candidates.append(item)
143
 
 
144
  scored_candidates.sort(key=lambda x: x['l1_score'], reverse=True)
145
 
 
146
  if debug_log_sample:
147
  print(f" -> [DEBUG L1] Sample Scores: { ' | '.join(debug_log_sample) }")
148
 
149
  print(f" -> Matrix selected {len(scored_candidates)} candidates (Threshold: {min_score}).")
150
 
 
151
  return [
152
  {
153
  'symbol': c['symbol'],
 
160
  ]
161
 
162
  # ==================================================================
163
+ # 🧩 Scanner Strategies Logic (With Error Exposure)
164
  # ==================================================================
165
+ def _apply_scanner_strategies(self, df: pd.DataFrame, symbol: str) -> Dict[str, Any]:
166
+ """تطبيق المؤشرات مع كشف الأخطاء"""
167
  results = {}
168
  try:
169
+ # التأكد من عدم وجود قيم فارغة (NaN)
170
+ # ملء الفراغات بآخر قيمة صالحة (Forward Fill)
171
+ df = df.ffill().bfill()
172
  close = df['close']
173
 
174
+ # 1. RSI
175
  rsi = ta.rsi(close, length=14)
176
+ # قد يعود RSI بقيم NaN في البداية، نأخذ القيمة الأخيرة ونتأكد أنها رقم
177
+ curr_rsi = rsi.iloc[-1] if rsi is not None else 50.0
178
+ if np.isnan(curr_rsi): curr_rsi = 50.0 # حماية ضد NaN
179
 
180
  score_rsi = 0
181
  active_rsi = False
182
 
183
  if 50 < curr_rsi < 75:
184
+ score_rsi = 100
185
  active_rsi = True
186
  elif curr_rsi <= 30:
187
+ score_rsi = 80
188
  active_rsi = True
189
  elif 30 < curr_rsi <= 50:
190
+ score_rsi = 40
191
 
192
  results["RSI_MOMENTUM"] = {'score': score_rsi, 'active': active_rsi, 'val': curr_rsi}
193
 
194
+ # 2. BB
195
  bb = ta.bbands(close, length=20, std=2)
196
  if bb is not None:
197
  upper = bb[f'BBU_20_2.0'].iloc[-1]
 
206
  score_bb = 0; active_bb = False
207
  results["BB_BREAKOUT"] = {'score': score_bb, 'active': active_bb}
208
 
209
+ # 3. MACD
210
  macd = ta.macd(close)
211
  if macd is not None:
212
  hist = macd[f'MACDh_12_26_9'].iloc[-1]
213
  score_macd = 0
214
  active_macd = False
215
+ if hist > 0:
216
  score_macd = 100
217
  active_macd = True
218
  else:
219
  score_macd = 0; active_macd = False
220
  results["MACD_CROSS"] = {'score': score_macd, 'active': active_macd}
221
 
222
+ # 4. Volume
223
  vol = df['volume']
224
  vol_ma = ta.sma(vol, length=20).iloc[-1]
225
  curr_vol = vol.iloc[-1]
 
230
  active_vol = True
231
  results["VOLUME_FLOW"] = {'score': score_vol, 'active': active_vol}
232
 
233
+ except Exception as e:
234
+ # 🔥 طباعة الخطأ الحقيقي هنا 🔥
235
+ print(f"❌ [Scanner Error] {symbol}: {e}")
236
+ # traceback.print_exc() # قم بتفعيل هذا إذا أردت التفاصيل المملة
237
  return {k: {'score': 0, 'active': False, 'val': 0} for k in ["RSI_MOMENTUM", "BB_BREAKOUT", "MACD_CROSS", "VOLUME_FLOW"]}
238
 
239
  return results
240
 
241
  # ==================================================================
242
+ # 🌍 Universe & Batch Fetch (Fixed)
243
  # ==================================================================
244
  async def _fetch_universe_tickers(self) -> List[Dict[str, Any]]:
 
 
 
245
  print(" -> 📡 [Debug] Contacting Exchange for Tickers...")
246
  try:
247
  tickers = await self.exchange.fetch_tickers()
 
251
  skipped_reason = {"pair": 0, "blacklist": 0, "volume": 0}
252
 
253
  for symbol, ticker in tickers.items():
 
254
  if not symbol.endswith('/USDT'):
255
  skipped_reason["pair"] += 1
256
  continue
257
 
 
258
  base_currency = symbol.split('/')[0]
259
 
 
260
  if any(bad in base_currency for bad in self.BLACKLIST_TOKENS):
261
  skipped_reason["blacklist"] += 1
262
  continue
263
 
 
264
  vol = ticker.get('quoteVolume')
265
  if vol is None:
266
  vol = ticker.get('info', {}).get('volValue')
 
268
  if vol is None: vol = 0.0
269
  else: vol = float(vol)
270
 
271
+ if vol < 100_000:
272
  skipped_reason["volume"] += 1
273
  continue
274
 
 
287
 
288
  except Exception as e:
289
  print(f"❌ [L1 Error] Fetch Tickers Failed: {e}")
 
290
  return []
291
 
 
 
 
292
  async def _batch_fetch_ta_data(self, candidates, timeframe='15m', limit=100):
293
  results = []
294
+ chunk_size = 15
295
  for i in range(0, len(candidates), chunk_size):
296
  chunk = candidates[i:i+chunk_size]
297
  tasks = [self._fetch_ohlcv_safe(c, timeframe, limit) for c in chunk]
 
314
  # ==================================================================
315
  # 🎯 Public Helpers
316
  # ==================================================================
317
+ async def get_latest_price_async(self, symbol):
318
  try:
319
  ticker = await self.exchange.fetch_ticker(symbol)
320
  return float(ticker['last'])
321
+ except: return 0.0
322
 
323
+ async def get_latest_ohlcv(self, symbol, timeframe='5m', limit=100):
324
  try:
325
+ return await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit) or []
326
+ except: return []
 
327
 
328
+ async def get_order_book_snapshot(self, symbol, limit=20):
329
  try:
330
+ return await self.exchange.fetch_order_book(symbol, limit)
331
+ except: return {}
 
332
 
333
  def get_supported_timeframes(self):
334
  return list(self.exchange.timeframes.keys()) if self.exchange else []