Riy777 commited on
Commit
3f2975d
·
verified ·
1 Parent(s): c22273f

Update ml_engine/data_manager.py

Browse files
Files changed (1) hide show
  1. ml_engine/data_manager.py +255 -180
ml_engine/data_manager.py CHANGED
@@ -1,6 +1,6 @@
1
  # ============================================================
2
  # 📂 ml_engine/data_manager.py
3
- # (V43.0 - GEM-Architect: Hot-Flow Sort)
4
  # ============================================================
5
 
6
  import asyncio
@@ -9,29 +9,28 @@ import traceback
9
  import logging
10
  import pandas as pd
11
  import numpy as np
12
- import pandas_ta as ta
13
- import math # ✅ مكتبة مهمة للمعادلة اللوغاريتمية
14
  from typing import List, Dict, Any
15
 
16
  import ccxt.async_support as ccxt
17
 
18
- # ✅ استيراد الدستور الديناميكي
19
  try:
20
  from ml_engine.processor import SystemLimits
21
  except ImportError:
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
  logging.getLogger("httpx").setLevel(logging.WARNING)
28
  logging.getLogger("ccxt").setLevel(logging.WARNING)
29
 
30
  class DataManager:
31
  """
32
- DataManager V43.0 (Hot-Flow Sort)
33
- - Replaced 'Raw Volume' sort with 'Volume-Weighted Momentum'.
34
- - Prioritizes active mid-caps over stagnant giants (XRP, TRX, BNB).
 
35
  """
36
 
37
  def __init__(self, contracts_db, whale_monitor, r2_service=None):
@@ -41,7 +40,7 @@ class DataManager:
41
 
42
  self.exchange = ccxt.kucoin({
43
  'enableRateLimit': True,
44
- 'timeout': 30000,
45
  'options': {'defaultType': 'spot'}
46
  })
47
 
@@ -51,17 +50,17 @@ class DataManager:
51
  # القائمة السوداء
52
  self.BLACKLIST_TOKENS = [
53
  'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
54
- 'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', 'USDD', 'USDP', 'HT', 'KCS', 'WBTC'
55
  ]
56
 
57
- print(f"📦 [DataManager V43.0] Hot-Flow Engine Online.")
58
 
59
  async def initialize(self):
60
  print(" > [DataManager] Starting initialization...")
61
  try:
62
- self.http_client = httpx.AsyncClient(timeout=60.0)
63
  await self._load_markets()
64
- print(f"✅ [DataManager] Ready (Mode: {getattr(SystemLimits, 'CURRENT_REGIME', 'UNKNOWN')}).")
65
  except Exception as e:
66
  print(f"❌ [DataManager] Init Error: {e}")
67
  traceback.print_exc()
@@ -86,144 +85,238 @@ class DataManager:
86
  def get_contracts_db(self): return self.contracts_db
87
 
88
  # ==================================================================
89
- # 🛡️ Layer 1: The Titan Mirror Filter (Hot-Flow Edition)
90
  # ==================================================================
91
  async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
92
- """
93
- يقوم بفرز العملات بناءً على "السخونة" (Hotness) وليس الحجم فقط.
94
- """
95
- current_regime = getattr(SystemLimits, "CURRENT_REGIME", "RANGE")
96
- min_score = getattr(SystemLimits, "L1_MIN_AFFINITY_SCORE", 15.0)
97
-
98
- print(f"🔍 [L1 Hot-Flow] Scanning for Active Momentum (Regime: {current_regime})...")
99
-
100
- # 1. جلب Universe (تصفية أولية)
101
- # خفضنا الحد الأدنى للحجم للسماح للعملات المتوسطة "الساخنة" بالدخول
102
- # في السابق كان الرقم مرتفعاً جداً مما يقتل الفرص
103
- min_vol_floor = 1000000 if current_regime == "BEAR" else 5000000
104
 
105
- all_tickers = await self._fetch_universe_tickers(min_volume=min_vol_floor)
 
 
106
 
107
- if not all_tickers:
108
- print("⚠️ [Layer 1] Universe fetch returned empty.")
109
  return []
110
 
111
- # 2. الجلب العميق (Deep Fetch) لأكثر العملات سخونة
112
- # نأخذ أفضل 150 عملة بناءً على معادلة (الحجم × الحركة)
113
- scan_limit = 150
114
- top_candidates = all_tickers[:scan_limit]
115
 
116
- print(f" -> Deep scanning top {len(top_candidates)} active assets...")
117
-
118
- enriched_data = await self._batch_fetch_ta_data(top_candidates, timeframe='15m', limit=200)
119
-
120
- scored_candidates = []
121
 
 
 
 
 
122
  for item in enriched_data:
123
- df = item.get('df')
124
- if df is None or len(df) < 150: continue
125
 
126
- # تطبيق فلتر المرآة الهيكلية
127
- structural_score, reasons = self._calculate_structural_score(df, item['symbol'], current_regime)
128
-
129
- # 🔥 Bonus: إضافة نقاط إضافية للعملات الساخنة جداً
130
- hot_score = item.get('hot_score', 0)
131
- if hot_score > 50: structural_score += 5 # Boost for super active coins
132
-
133
- item['l1_score'] = structural_score
134
- item['type'] = " | ".join(reasons)
135
-
136
- if structural_score >= min_score:
137
- scored_candidates.append(item)
138
 
139
- # الترتيب النهائي حسب جودة الهيكل
140
- scored_candidates.sort(key=lambda x: x['l1_score'], reverse=True)
141
-
142
- print(f" -> Filter selected {len(scored_candidates)} candidates.")
 
 
 
 
 
 
143
 
144
  return [
145
  {
146
  'symbol': c['symbol'],
147
  'quote_volume': c.get('quote_volume', 0),
148
  'current_price': c.get('current_price', 0),
149
- 'type': c.get('type', 'Structural'),
150
  'l1_score': c.get('l1_score', 0)
151
  }
152
- for c in scored_candidates[:30]
153
  ]
154
 
155
  # ==================================================================
156
- # 🧬 Structural Alignment Engine
157
  # ==================================================================
158
  def _calculate_structural_score(self, df: pd.DataFrame, symbol: str, regime: str) -> (float, List[str]):
159
- # (نفس منطق الفلتر الهيكلي السابق - لم يتغير لأنه ممتاز)
160
- score = 0.0
161
- tags = []
 
 
 
 
162
  try:
163
- close = df['close']
164
- high = df['high']
165
- low = df['low']
166
- volume = df['volume']
167
- current_price = close.iloc[-1]
168
 
169
- zero_vol_candles = (volume == 0).sum()
170
- if zero_vol_candles > 5: return -100.0, ["Illiquid"]
171
 
172
- avg_vol = volume.rolling(20).mean().iloc[-1]
173
- if avg_vol * current_price < 5000: return -100.0, ["Thin Book"]
174
-
175
- ema200 = ta.ema(close, length=200)
176
- ema50 = ta.ema(close, length=50)
177
 
178
- if ema200 is not None and ema50 is not None:
179
- curr_ema200 = ema200.iloc[-1]
180
- dist_200 = (current_price - curr_ema200) / curr_ema200
181
-
182
- if regime == "BULL":
183
- if current_price > curr_ema200:
184
- score += 20
185
- if current_price > ema50.iloc[-1]: score += 10
186
- if dist_200 < 0.15: score += 10
187
- else: score -= 5
188
- else: score -= 20
189
- elif regime == "BEAR":
190
- if dist_200 < -0.20: score += 30
191
- else:
192
- if abs(dist_200) < 0.05: score += 20
193
-
194
- bb = ta.bbands(close, length=20, std=2)
195
- if bb is not None:
196
- # حساب الـ Bandwidth يدوياً للأمان
197
- upper = bb[bb.columns[0]] # Lower band usually index 0 in pandas_ta default
198
- lower = bb[bb.columns[2]] # Upper band
199
- # نستخدم أسماء الأعمدة إذا أمكن للتأكد، ولكن للسرعة:
200
- # pandas_ta returns: Lower, Mid, Upper, Bandwidth, Percent
201
- # لنتأكد من الـ Bandwidth مباشرة
202
- width_col = next((c for c in bb.columns if c.startswith('BBB')), None)
203
- if width_col:
204
- current_width = bb[width_col].iloc[-1] / 100.0 # pandas_ta returns pct
205
- if current_width < 0.05: score += 25; tags.append("Squeeze")
206
- elif current_width > 0.15: score -= 10
207
-
208
- rsi = ta.rsi(close, length=14).iloc[-1]
209
- adx = ta.adx(high, low, close, length=14)
210
- curr_adx = adx.iloc[-1, 0] if adx is not None else 0
211
 
212
- if 40 < rsi < 70: score += 15
213
- elif rsi > 75 and regime == "BULL": score += 10
214
 
215
- if curr_adx > 25: score += 10; tags.append("Trending")
 
 
 
 
 
216
 
217
- vol_sma = volume.rolling(20).mean().iloc[-1]
218
- if volume.iloc[-1] > vol_sma * 1.5: score += 15; tags.append("Vol Spike")
 
 
219
 
220
- except Exception as e: return 0.0, ["Error"]
221
- return score, tags
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
  # ==================================================================
224
- # 🌍 Universe & Batch Fetch (The Hot-Flow Logic)
225
  # ==================================================================
226
- async def _fetch_universe_tickers(self, min_volume=300_000) -> List[Dict[str, Any]]:
227
  try:
228
  tickers = await self.exchange.fetch_tickers()
229
  candidates = []
@@ -231,96 +324,78 @@ class DataManager:
231
  for symbol, ticker in tickers.items():
232
  if not symbol.endswith('/USDT'): continue
233
 
234
- base_currency = symbol.split('/')[0]
235
- if any(bad in base_currency for bad in self.BLACKLIST_TOKENS): continue
236
- if "3S" in base_currency or "3L" in base_currency: continue
237
-
238
- vol = ticker.get('quoteVolume')
239
- if vol is None: vol = ticker.get('info', {}).get('volValue')
240
- if vol is None: vol = 0.0
241
- else: vol = float(vol)
242
-
243
- # 1. فلتر الحد الأدنى المطلق (لإبعاد العملات الميتة)
244
- if vol < min_volume: continue
245
-
246
- # 2. فلتر السبريد (للحماية)
247
- bid = float(ticker.get('bid', 0) or 0)
248
- ask = float(ticker.get('ask', 0) or 0)
249
- if bid > 0 and ask > 0:
250
- spread_pct = (ask - bid) / bid
251
- if spread_pct > 0.015: continue # تساهلنا قليلاً (1.5%) للسماح بعملات الـ Meme النشطة
252
-
253
- # 3. حساب درجة السخونة 🔥 (Hot Score)
254
- # المعادلة: Log10(Volume) * (1 + Abs(Change%))
255
- # هذا يعطي وزناً للحجم، لكن يضرب بقوة في التغير السعري
256
- change_pct = float(ticker.get('percentage', 0.0))
257
-
258
- # نستخدم Log10 لتقليص الفارق بين المليار والمليون
259
- # Log10(1B) = 9, Log10(10M) = 7 (الفارق بسيط)
260
- # بينما التغير السعري: 1% vs 10% (الفارق 10 أضعاف)
261
- # هذا يجعل التغير السعري هو العامل الحاسم في الترتيب
262
 
263
- log_vol = math.log10(vol + 1)
264
- volatility_factor = abs(change_pct) + 1.0 # نضيف 1 لكي لا نضرب في صفر
 
265
 
266
- hot_score = log_vol * volatility_factor
 
267
 
268
  candidates.append({
269
  'symbol': symbol,
270
- 'quote_volume': vol,
271
- 'current_price': float(ticker['last']) if ticker.get('last') else 0.0,
272
- 'change_24h': change_pct,
273
- 'hot_score': hot_score # الدرجة الجديدة
274
  })
275
 
276
- # 🔥 الترتيب حسب درجة السخونة وليس الحجم المطلق
277
- candidates.sort(key=lambda x: x['hot_score'], reverse=True)
278
-
279
  return candidates
280
-
281
  except Exception as e:
282
- print(f"❌ [L1 Error] Fetch Tickers Failed: {e}")
283
  return []
284
 
285
- async def _batch_fetch_ta_data(self, candidates, timeframe='15m', limit=200):
 
 
 
 
286
  results = []
287
- chunk_size = 20
288
  for i in range(0, len(candidates), chunk_size):
289
- chunk = candidates[i:i+chunk_size]
290
- tasks = [self._fetch_ohlcv_safe(c, timeframe, limit) for c in chunk]
291
- chunk_res = await asyncio.gather(*tasks)
292
- results.extend([r for r in chunk_res if r is not None])
293
- await asyncio.sleep(0.05)
294
  return results
295
 
296
- async def _fetch_ohlcv_safe(self, candidate, tf, limit):
 
297
  try:
298
- ohlcv = await self.exchange.fetch_ohlcv(candidate['symbol'], tf, limit=limit)
299
- if not ohlcv: return None
300
- df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
301
- # تحويل البيانات لأرقام
302
- cols = ['open', 'high', 'low', 'close', 'volume']
303
- df[cols] = df[cols].apply(pd.to_numeric, errors='coerce')
304
 
305
- candidate['df'] = df
 
 
 
 
306
  return candidate
307
- except: return None
 
308
 
309
  # ==================================================================
310
  # 🎯 Public Helpers
311
  # ==================================================================
312
- async def get_latest_price_async(self, symbol):
313
  try:
314
  ticker = await self.exchange.fetch_ticker(symbol)
315
  return float(ticker['last'])
316
- except: return 0.0
317
 
318
- async def get_latest_ohlcv(self, symbol, timeframe='5m', limit=100):
319
  try:
320
- return await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit) or []
321
- except: return []
 
322
 
323
- async def get_order_book_snapshot(self, symbol, limit=20):
324
  try:
325
- return await self.exchange.fetch_order_book(symbol, limit)
326
- except: return {}
 
 
1
  # ============================================================
2
  # 📂 ml_engine/data_manager.py
3
+ # (V45.0 - GEM-Architect: Anti-FOMO Revival)
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
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
 
25
  logging.getLogger("httpx").setLevel(logging.WARNING)
26
  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):
 
40
 
41
  self.exchange = ccxt.kucoin({
42
  'enableRateLimit': True,
43
+ 'timeout': 60000,
44
  'options': {'defaultType': 'spot'}
45
  })
46
 
 
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()
 
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 = []
 
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 {}