Riy777 commited on
Commit
e0ee6fd
·
verified ·
1 Parent(s): 3d9fe2b

Update ml_engine/data_manager.py

Browse files
Files changed (1) hide show
  1. ml_engine/data_manager.py +267 -165
ml_engine/data_manager.py CHANGED
@@ -1,251 +1,353 @@
1
  # ============================================================
2
  # 📂 ml_engine/data_manager.py
3
- # (V58.1 - GEM-Architect: Asset-Context Edition + Fixed)
4
  # ============================================================
5
 
6
  import asyncio
7
  import httpx
8
  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, Tuple
14
- from datetime import datetime
15
- import ccxt.async_support as ccxt
16
-
17
- # Keep SystemLimits import for fallbacks
18
- try:
19
- from ml_engine.processor import SystemLimits
20
- except ImportError:
21
- SystemLimits = None
22
 
 
23
  logging.getLogger("httpx").setLevel(logging.WARNING)
 
24
  logging.getLogger("ccxt").setLevel(logging.WARNING)
25
 
26
  class DataManager:
27
  def __init__(self, contracts_db, whale_monitor, r2_service=None):
 
 
 
28
  self.contracts_db = contracts_db or {}
29
  self.whale_monitor = whale_monitor
30
  self.r2_service = r2_service
31
- # Pass the hub instance later via method arguments
32
- self.adaptive_hub_ref = None
33
-
34
  self.exchange = ccxt.kucoin({
35
  'enableRateLimit': True,
36
- 'timeout': 30000,
37
  'options': {'defaultType': 'spot'}
38
  })
39
 
40
  self.http_client = None
41
  self.market_cache = {}
42
 
43
- # Core Blacklist (Stablecoins & Leveraged Tokens)
44
  self.BLACKLIST_TOKENS = [
45
  'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
46
- 'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L', 'USDD', 'USDP', 'HT', 'KCS'
47
  ]
48
 
49
- print(f"📦 [DataManager V58.1] Quality Gate & Context Engine Active.")
50
 
51
  async def initialize(self):
 
 
52
  self.http_client = httpx.AsyncClient(timeout=30.0)
53
  await self._load_markets()
 
 
 
54
 
55
  async def _load_markets(self):
56
  try:
57
- if self.exchange and not self.exchange.markets:
58
  await self.exchange.load_markets()
59
  self.market_cache = self.exchange.markets
60
- except Exception: pass
 
 
61
 
62
  async def close(self):
63
  if self.http_client: await self.http_client.aclose()
64
  if self.exchange: await self.exchange.close()
65
 
66
- # ✅ FIXED: Restored Missing Method for Startup Sequence
67
  async def load_contracts_from_r2(self):
68
  if not self.r2_service: return
69
  try:
70
  self.contracts_db = await self.r2_service.load_contracts_db_async()
71
- print(f" 📂 [DataManager] Contracts DB loaded: {len(self.contracts_db)} items.")
72
- except Exception as e:
73
- print(f" ⚠️ [DataManager] Failed to load contracts: {e}")
74
  self.contracts_db = {}
75
-
76
- def get_contracts_db(self): return self.contracts_db
77
 
 
 
 
78
  # ==================================================================
79
- # 🛡️ Stage 0: The "Anti-Junk" Gate (Quality Control)
80
  # ==================================================================
81
- async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
82
  """
83
- Strict Quality Control:
84
- 1. Liquidity: > $2M Quote Volume.
85
- 2. Integrity: Spread < 2.0%.
86
- 3. Sanity: 24h Change < 30% (Avoid chasing massive pumps).
87
- 4. Safety: No Blacklisted tokens.
88
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  try:
90
- print(f" 🛡️ [Stage 0] Filtering Junk (Vol>2M, Spread<2%, No Pumps)...")
91
  tickers = await self.exchange.fetch_tickers()
92
-
93
- valid_candidates = []
94
 
95
  for symbol, ticker in tickers.items():
96
  if not symbol.endswith('/USDT'): continue
97
 
98
- # 1. Blacklist Check
99
  base_curr = symbol.split('/')[0]
100
  if any(bad in base_curr for bad in self.BLACKLIST_TOKENS): continue
101
 
102
- # 2. Volume Check (Strict > 2,000,000 USD)
103
  quote_vol = ticker.get('quoteVolume')
104
- if quote_vol is None or quote_vol == 0:
105
- base_vol = ticker.get('baseVolume')
106
- last_p = ticker.get('last')
107
- if base_vol and last_p:
108
- quote_vol = float(base_vol) * float(last_p)
109
- else:
110
- quote_vol = 0.0
111
 
112
- if quote_vol < 2_000_000: continue
113
-
114
- # 3. Spread Check (Avoid Orderbook manipulation/illiquidity)
115
- bid = ticker.get('bid')
116
- ask = ticker.get('ask')
117
- if bid and ask and ask > 0:
118
- spread_pct = ((ask - bid) / ask) * 100
119
- if spread_pct > 2.0: continue # Skip if spread > 2%
120
- else:
121
- continue # Broken book
122
-
123
- # 4. Sanity Check (Avoid extreme FOMO/Dumps)
124
- change_24h = ticker.get('percentage')
125
- if change_24h is not None:
126
- if abs(change_24h) > 30.0: continue # Skip huge pumps/dumps
127
-
128
- valid_candidates.append({
129
  'symbol': symbol,
130
  'quote_volume': quote_vol,
131
- 'current_price': float(ticker.get('last', 0)),
132
- 'change_24h': change_24h,
133
- 'spread': spread_pct if 'spread_pct' in locals() else 0.0
134
  })
135
 
136
- # Sort by Volume (Liquidity King)
137
- valid_candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
 
138
 
139
- # Cap at Top 80 to ensure we have rate-limit room for 4H analysis
140
- final_list = valid_candidates[:80]
141
-
142
- print(f" -> [Stage 0] Passed {len(final_list)} High-Quality Assets.")
143
- return final_list
144
-
145
  except Exception as e:
146
- print(f"❌ [Stage 0 Error] {e}")
147
- traceback.print_exc()
148
  return []
149
 
150
- # ==================================================================
151
- # 🧭 Stage 1: Context Diagnosis (4H Regime)
152
- # ==================================================================
153
- async def _determine_4h_regime(self, symbol: str) -> Dict[str, Any]:
154
- """
155
- Diagnose the Asset's specific regime using 4H data.
156
- Returns: BULL, BEAR, DEAD, or RANGE + Tech Data.
157
- """
158
- try:
159
- ohlcv = await self.exchange.fetch_ohlcv(symbol, '4h', limit=50)
160
- if not ohlcv or len(ohlcv) < 50: return {'regime': 'RANGE', 'conf': 0.0}
 
 
 
161
 
162
- df = pd.DataFrame(ohlcv, columns=['ts', 'o', 'h', 'l', 'c', 'v'])
163
- c = df['c']
164
-
165
- # Indicators
166
- ema50 = ta.ema(c, length=50).iloc[-1]
167
- ema200 = ta.ema(c, length=200).iloc[-1]
168
- rsi = ta.rsi(c, length=14).iloc[-1]
169
- atr = ta.atr(df['h'], df['l'], c, length=14).iloc[-1]
170
- price = c.iloc[-1]
171
-
172
- # Logic
173
- regime = "RANGE"
174
- conf = 0.5
175
-
176
- # 1. Check DEAD (Low Volatility)
177
- atr_pct = (atr / price) * 100
178
- # Calculate Range of last 20 candles
179
- high_20 = df['h'].iloc[-20:].max()
180
- low_20 = df['l'].iloc[-20:].min()
181
- range_pct = ((high_20 - low_20) / low_20) * 100
182
-
183
- if atr_pct < 0.8 and range_pct < 4.0:
184
- regime = "DEAD"
185
- conf = 0.9
186
 
187
- # 2. Check BULL
188
- elif price > ema50 and ema50 > ema200 and rsi > 50:
189
- regime = "BULL"
190
- conf = 0.8 if rsi > 55 else 0.6
191
-
192
- # 3. Check BEAR
193
- elif price < ema50 and ema50 < ema200 and rsi < 50:
194
- regime = "BEAR"
195
- conf = 0.8 if rsi < 45 else 0.6
196
 
197
- return {
198
- 'regime': regime,
199
- 'conf': conf,
200
- 'tech': {
201
- 'ema50': ema50, 'ema200': ema200, 'rsi': rsi, 'atr_pct': atr_pct
202
- }
203
  }
 
 
 
 
 
204
  except Exception:
205
- return {'regime': 'RANGE', 'conf': 0.0}
206
 
207
- async def layer1_rapid_screening(self, adaptive_hub_ref=None) -> List[Dict[str, Any]]:
208
- """
209
- Orchestrates Stage 0 (Filter) -> Stage 1 (Diagnose & Inject Limits).
210
- """
211
- # 1. Get High Quality Candidates
212
- candidates = await self._stage0_universe_filter()
213
- if not candidates: return []
214
-
215
- print(f" 🧬 [Stage 1] Diagnosing 4H Regime for {len(candidates)} assets...")
216
-
217
- # 2. Parallel Diagnosis
218
- # Fetching 4H data for all candidates
219
- tasks = [self._determine_4h_regime(c['symbol']) for c in candidates]
220
- regime_results = await asyncio.gather(*tasks, return_exceptions=True)
221
-
222
- final_list = []
223
- for i, res in enumerate(regime_results):
224
- if isinstance(res, dict):
225
- cand = candidates[i]
226
- cand['asset_regime'] = res['regime']
227
- cand['asset_regime_conf'] = res['conf']
228
- cand['type'] = 'CANDIDATE'
229
-
230
- # 3. INJECT DYNAMIC LIMITS
231
- # This is where we break the "Global SystemLimits" dependency.
232
- if adaptive_hub_ref:
233
- # Get DNA for this specific asset's regime
234
- dynamic_config = adaptive_hub_ref.get_regime_config(res['regime'])
235
- cand['dynamic_limits'] = dynamic_config
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
 
237
- final_list.append(cand)
 
 
 
238
 
239
- print(f" -> [Stage 1] Prepared {len(final_list)} Context-Aware Candidates.")
240
- return final_list
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
242
- # Keep helper methods...
 
 
243
  async def get_latest_price_async(self, symbol: str) -> float:
244
- try: return float((await self.exchange.fetch_ticker(symbol))['last'])
245
- except: return 0.0
 
 
 
246
  async def get_latest_ohlcv(self, symbol: str, timeframe: str = '5m', limit: int = 100) -> List[List[float]]:
247
- try: return await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
248
- except: return []
 
 
 
249
  async def get_order_book_snapshot(self, symbol: str, limit: int = 20) -> Dict[str, Any]:
250
- try: return await self.exchange.fetch_order_book(symbol, limit)
251
- except: return {}
 
 
 
1
  # ============================================================
2
  # 📂 ml_engine/data_manager.py
3
+ # (V15.2 - GEM-Architect: Anti-FOMO Shield - Strict Marksman Mode)
4
  # ============================================================
5
 
6
  import asyncio
7
  import httpx
8
  import traceback
9
+ import ccxt.async_support as ccxt
10
  import logging
11
  import pandas as pd
12
  import numpy as np
13
+ from typing import List, Dict, Any
 
 
 
 
 
 
 
 
 
14
 
15
+ # إعدادات التسجيل
16
  logging.getLogger("httpx").setLevel(logging.WARNING)
17
+ logging.getLogger("httpcore").setLevel(logging.WARNING)
18
  logging.getLogger("ccxt").setLevel(logging.WARNING)
19
 
20
  class DataManager:
21
  def __init__(self, contracts_db, whale_monitor, r2_service=None):
22
+ # ==================================================================
23
+ # ⚙️ إعدادات التحكم
24
+ # ==================================================================
25
  self.contracts_db = contracts_db or {}
26
  self.whale_monitor = whale_monitor
27
  self.r2_service = r2_service
28
+ self.adaptive_hub_ref = None # مرجع لملف التعلم
29
+
30
+ # إعداد المنصة (KuCoin)
31
  self.exchange = ccxt.kucoin({
32
  'enableRateLimit': True,
33
+ 'timeout': 60000,
34
  'options': {'defaultType': 'spot'}
35
  })
36
 
37
  self.http_client = None
38
  self.market_cache = {}
39
 
40
+ # قوائم الاستبعاد (العملات المستقرة والعملات ذات الرافعة)
41
  self.BLACKLIST_TOKENS = [
42
  'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
43
+ 'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L'
44
  ]
45
 
46
+ print(f"📦 [DataManager V15.2] Restored 'Anti-FOMO' Logic Shield.")
47
 
48
  async def initialize(self):
49
+ """تهيئة مدير البيانات والاتصالات"""
50
+ print(" > [DataManager] Starting initialization...")
51
  self.http_client = httpx.AsyncClient(timeout=30.0)
52
  await self._load_markets()
53
+ # تحميل العقود إذا وجدت
54
+ await self.load_contracts_from_r2()
55
+ print(f"✅ [DataManager] Ready. Logic: STRICT/Anti-FOMO.")
56
 
57
  async def _load_markets(self):
58
  try:
59
+ if self.exchange:
60
  await self.exchange.load_markets()
61
  self.market_cache = self.exchange.markets
62
+ except Exception as e:
63
+ print(f"❌ [DataManager] Market load failed: {e}")
64
+ traceback.print_exc()
65
 
66
  async def close(self):
67
  if self.http_client: await self.http_client.aclose()
68
  if self.exchange: await self.exchange.close()
69
 
 
70
  async def load_contracts_from_r2(self):
71
  if not self.r2_service: return
72
  try:
73
  self.contracts_db = await self.r2_service.load_contracts_db_async()
74
+ except Exception:
 
 
75
  self.contracts_db = {}
 
 
76
 
77
+ def get_contracts_db(self) -> Dict[str, Any]:
78
+ return self.contracts_db
79
+
80
  # ==================================================================
81
+ # 🛡️ Layer 1: The Strict Logic Tree Screening (Core Logic)
82
  # ==================================================================
83
+ async def layer1_rapid_screening(self, adaptive_hub_ref=None) -> List[Dict[str, Any]]:
84
  """
85
+ يقوم بمسح السوق وتقسيم الفرص إلى نوعين:
86
+ 1. Breakout: اختراق آمن (RSI < 70).
87
+ 2. Reversal: ارتداد من القاع (RSI < 40).
88
+ أي شيء آخر يتم استبعاده فوراً.
 
89
  """
90
+ self.adaptive_hub_ref = adaptive_hub_ref
91
+ print(f"🔍 [Layer 1] Initiating STRICT Logic Tree Screening...")
92
+
93
+ # 1. المرحلة 0: فلتر الكون (السيولة الأساسية)
94
+ initial_candidates = await self._stage0_universe_filter()
95
+
96
+ if not initial_candidates:
97
+ return []
98
+
99
+ # 2. جلب البيانات الفنية لأفضل العملات سيولة (Top 100 لتقليل الضغط)
100
+ top_liquid_candidates = initial_candidates[:100]
101
+ enriched_data = await self._fetch_technical_data_batch(top_liquid_candidates)
102
+
103
+ # 3. تطبيق شجرة القرار الصارمة (Anti-FOMO)
104
+ breakout_list = []
105
+ reversal_list = []
106
+
107
+ for item in enriched_data:
108
+ classification = self._apply_logic_tree(item)
109
+
110
+ # حقن التكوين الديناميكي (إذا وجد Hub)
111
+ if self.adaptive_hub_ref:
112
+ regime = "BULL" if classification['type'] == 'BREAKOUT' else "RANGE"
113
+ item['dynamic_limits'] = self.adaptive_hub_ref.get_regime_config(regime)
114
+
115
+ if classification['type'] == 'BREAKOUT':
116
+ item['l1_sort_score'] = classification['score']
117
+ item['strategy_tag'] = 'Safe_Breakout'
118
+ breakout_list.append(item)
119
+ elif classification['type'] == 'REVERSAL':
120
+ item['l1_sort_score'] = classification['score']
121
+ item['strategy_tag'] = 'Dip_Sniper'
122
+ reversal_list.append(item)
123
+
124
+ print(f" -> [L1 Logic] Found: {len(breakout_list)} Breakouts, {len(reversal_list)} Reversals.")
125
+
126
+ # 4. الترتيب والدمج النهائي
127
+ # الاختراق: نرتب بالأعلى سكور (حجم تداول نسبي)
128
+ breakout_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
129
+ # الارتداد: نرتب بالأعلى سكور (كلما كان الـ RSI أقل كان السكور أعلى في منطقنا)
130
+ reversal_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
131
+
132
+ # نختار صفوة الصفوة
133
+ final_selection = breakout_list[:25] + reversal_list[:25]
134
+
135
+ # تنظيف البيانات للإرجاع
136
+ # نحتفظ بالبيانات الفنية لأن المعالج سيحتاجها
137
+ print(f"✅ [Layer 1] Final Selection: {len(final_selection)} candidates (Anti-FOMO Active).")
138
+ return final_selection
139
+
140
+ # ------------------------------------------------------------------
141
+ # Stage 0: Universe Filter (Basic Liquidity)
142
+ # ------------------------------------------------------------------
143
+ async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
144
  try:
 
145
  tickers = await self.exchange.fetch_tickers()
146
+ candidates = []
 
147
 
148
  for symbol, ticker in tickers.items():
149
  if not symbol.endswith('/USDT'): continue
150
 
 
151
  base_curr = symbol.split('/')[0]
152
  if any(bad in base_curr for bad in self.BLACKLIST_TOKENS): continue
153
 
154
+ # 👇 الحد الأدنى للسيولة (1 مليون دولار لضمان التنفيذ السريع)
155
  quote_vol = ticker.get('quoteVolume')
156
+ if not quote_vol or quote_vol < 1_000_000: continue
 
 
 
 
 
 
157
 
158
+ last_price = ticker.get('last')
159
+ if not last_price or last_price < 0.0005: continue
160
+
161
+ # 👇 فلتر أولي: استبعاد العملات التي انفجرت بجنون (+15% فأكثر يتم تجاهلها مبدئياً)
162
+ change_24h = ticker.get('percentage', 0.0)
163
+ if change_24h > 15.0: continue
164
+
165
+ candidates.append({
 
 
 
 
 
 
 
 
 
166
  'symbol': symbol,
167
  'quote_volume': quote_vol,
168
+ 'current_price': last_price,
169
+ 'change_24h': change_24h
 
170
  })
171
 
172
+ # الترتيب حسب السيولة
173
+ candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
174
+ return candidates
175
 
 
 
 
 
 
 
176
  except Exception as e:
177
+ print(f"❌ [L1 Error] Universe filter failed: {e}")
 
178
  return []
179
 
180
+ # ------------------------------------------------------------------
181
+ # Data Fetching Helpers (Batch Processing)
182
+ # ------------------------------------------------------------------
183
+ async def _fetch_technical_data_batch(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
184
+ chunk_size = 10
185
+ results = []
186
+ for i in range(0, len(candidates), chunk_size):
187
+ chunk = candidates[i:i + chunk_size]
188
+ chunk_tasks = [self._fetch_single_tech_data(c) for c in chunk]
189
+ chunk_results = await asyncio.gather(*chunk_tasks)
190
+ results.extend([r for r in chunk_results if r is not None])
191
+ # تأخير بسيط جداً لتجنب حظر API
192
+ await asyncio.sleep(0.05)
193
+ return results
194
 
195
+ async def _fetch_single_tech_data(self, candidate: Dict[str, Any]) -> Any:
196
+ symbol = candidate['symbol']
197
+ try:
198
+ # نحتاج 1H للاتجاه العام و 15M للدخول الدقيق
199
+ ohlcv_1h = await self.exchange.fetch_ohlcv(symbol, '1h', limit=60)
200
+ ohlcv_15m = await self.exchange.fetch_ohlcv(symbol, '15m', limit=60)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
+ if not ohlcv_1h or len(ohlcv_1h) < 55 or not ohlcv_15m or len(ohlcv_15m) < 55:
203
+ return None
 
 
 
 
 
 
 
204
 
205
+ # تخزين البيانات الخام لاستخدامها لاحقاً في المعالج
206
+ candidate['ohlcv'] = {
207
+ '1h': ohlcv_1h,
208
+ '15m': ohlcv_15m,
209
+ # يمكن إضافة 5m لاحقاً عند الحاجة للدقة القصوى
 
210
  }
211
+
212
+ # نسخ للاستخدام الداخلي في الفلتر
213
+ candidate['ohlcv_1h_raw'] = ohlcv_1h
214
+ candidate['ohlcv_15m_raw'] = ohlcv_15m
215
+ return candidate
216
  except Exception:
217
+ return None
218
 
219
+ # ------------------------------------------------------------------
220
+ # 🧠 The Logic Core: Math & Decision Tree (STRICT ANTI-FOMO MODE)
221
+ # ------------------------------------------------------------------
222
+ def _apply_logic_tree(self, data: Dict[str, Any]) -> Dict[str, Any]:
223
+ try:
224
+ df_1h = self._calc_indicators(data['ohlcv_1h_raw'])
225
+ df_15m = self._calc_indicators(data['ohlcv_15m_raw'])
226
+ except:
227
+ return {'type': 'NONE'}
228
+
229
+ curr_1h = df_1h.iloc[-1]
230
+ curr_15m = df_15m.iloc[-1]
231
+
232
+ # --- Stage 2: Overbought Filter (STRICT MODE 🛡️) ---
233
+ try:
234
+ close_4h_ago = df_1h.iloc[-5]['close']
235
+ change_4h = ((curr_1h['close'] - close_4h_ago) / close_4h_ago) * 100
236
+ except: change_4h = 0.0
237
+
238
+ # 1. ⛔ Anti-Pump: إذا صعدت أكثر من 8% في 4 ساعات، اتركها
239
+ if change_4h > 8.0: return {'type': 'NONE'}
240
+
241
+ # 2. ⛔ Anti-FOMO: إذا صعدت أكثر من 12% في اليوم، اتركها
242
+ if data.get('change_24h', 0) > 12.0: return {'type': 'NONE'}
243
+
244
+ # 3. ⛔ RSI Ceiling: ممنوع الدخول إذا RSI فوق 70 (منطقة تشبع)
245
+ if curr_1h['rsi'] > 70: return {'type': 'NONE'}
246
+
247
+ # 4. ⛔ Mean Reversion Risk: السعر بعيد جداً عن المتوسط
248
+ deviation = (curr_1h['close'] - curr_1h['ema20']) / curr_1h['atr'] if curr_1h['atr'] > 0 else 0
249
+ if deviation > 1.8: return {'type': 'NONE'}
250
+
251
+ # --- Stage 3: Classification ---
252
+
253
+ # === A. Breakout Logic (Safe & Early) ===
254
+ is_breakout = False
255
+ breakout_score = 0.0
256
+
257
+ # شروط الترند الصاعد الصحي
258
+ bullish_structure = (curr_1h['ema20'] > curr_1h['ema50']) or (curr_1h['close'] > curr_1h['ema20'])
259
+
260
+ if bullish_structure:
261
+ # RSI يجب أن يكون لديه مساحة للصعود (بين 45 و 68)
262
+ if 45 <= curr_1h['rsi'] <= 68:
263
+ if curr_15m['close'] >= curr_15m['ema20']:
264
+ # Volatility Squeeze: السعر يتحرك في نطاق ضيق
265
+ avg_range = (df_15m['high'] - df_15m['low']).rolling(10).mean().iloc[-1]
266
+ if (curr_15m['high'] - curr_15m['low']) <= avg_range * 1.8:
267
+ vol_ma20 = df_15m['volume'].rolling(20).mean().iloc[-1]
268
+
269
+ # Volume Confirmation: حجم تداول أعلى من المتوسط بـ 1.5 مرة
270
+ if curr_15m['volume'] >= 1.5 * vol_ma20:
271
+ is_breakout = True
272
+ breakout_score = curr_15m['volume'] / vol_ma20 if vol_ma20 > 0 else 1.0
273
+
274
+ if is_breakout:
275
+ return {'type': 'BREAKOUT', 'score': breakout_score}
276
+
277
+ # === B. Reversal Logic (Dip Buy / Oversold) ===
278
+ is_reversal = False
279
+ reversal_score = 100.0
280
+
281
+ # شراء التشبع البيعي فقط (RSI بين 20 و 40)
282
+ if 20 <= curr_1h['rsi'] <= 40:
283
+ # السعر هبط مؤخراً
284
+ if change_4h <= -2.0:
285
+ # البحث عن شمعة انعكاسية (مطرقة Hammer أو ابتلاعية Engulfing) في آخر 3 شموع 15m
286
+ last_3 = df_15m.iloc[-3:]
287
+ found_rejection = False
288
+ for _, row in last_3.iterrows():
289
+ rng = row['high'] - row['low']
290
+ if rng > 0:
291
+ is_green = row['close'] > row['open']
292
+ # شكل المطرقة: الذيل السفلي طويل
293
+ hammer_shape = (min(row['open'], row['close']) - row['low']) > (rng * 0.6)
294
+ if is_green or hammer_shape:
295
+ found_rejection = True
296
+ break
297
 
298
+ if found_rejection:
299
+ is_reversal = True
300
+ # كلما قل الـ RSI زادت احتمالية الارتداد (سكور أعلى)
301
+ reversal_score = (100 - curr_1h['rsi'])
302
 
303
+ if is_reversal:
304
+ return {'type': 'REVERSAL', 'score': reversal_score}
305
+
306
+ return {'type': 'NONE'}
307
+
308
+ def _calc_indicators(self, ohlcv_list):
309
+ # حسابات يدوية سريعة باستخدام Pandas (بدون مكتبات خارجية لتقليل التبعيات في الفلتر)
310
+ df = pd.DataFrame(ohlcv_list, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
311
+
312
+ # RSI
313
+ delta = df['close'].diff()
314
+ gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
315
+ loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
316
+ rs = gain / loss
317
+ df['rsi'] = 100 - (100 / (1 + rs))
318
+
319
+ # EMAs
320
+ df['ema20'] = df['close'].ewm(span=20, adjust=False).mean()
321
+ df['ema50'] = df['close'].ewm(span=50, adjust=False).mean()
322
+
323
+ # ATR
324
+ high_low = df['high'] - df['low']
325
+ high_close = np.abs(df['high'] - df['close'].shift())
326
+ low_close = np.abs(df['low'] - df['close'].shift())
327
+ ranges = pd.concat([high_low, high_close, low_close], axis=1)
328
+ true_range = np.max(ranges, axis=1)
329
+ df['atr'] = true_range.rolling(14).mean()
330
+
331
+ df.fillna(0, inplace=True)
332
+ return df
333
 
334
+ # ==================================================================
335
+ # 🎯 Public Helpers (Standard Interface)
336
+ # ==================================================================
337
  async def get_latest_price_async(self, symbol: str) -> float:
338
+ try:
339
+ ticker = await self.exchange.fetch_ticker(symbol)
340
+ return float(ticker['last'])
341
+ except Exception: return 0.0
342
+
343
  async def get_latest_ohlcv(self, symbol: str, timeframe: str = '5m', limit: int = 100) -> List[List[float]]:
344
+ try:
345
+ candles = await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
346
+ return candles or []
347
+ except Exception: return []
348
+
349
  async def get_order_book_snapshot(self, symbol: str, limit: int = 20) -> Dict[str, Any]:
350
+ try:
351
+ ob = await self.exchange.fetch_order_book(symbol, limit)
352
+ return ob
353
+ except Exception: return {}