Riy777 commited on
Commit
9180094
·
verified ·
1 Parent(s): fa9f6bd

Update ml_engine/data_manager.py

Browse files
Files changed (1) hide show
  1. ml_engine/data_manager.py +174 -292
ml_engine/data_manager.py CHANGED
@@ -1,5 +1,5 @@
1
  # ml_engine/data_manager.py
2
- # (V15.2 - GEM-Architect: Tuned Logic Tree - Marksman Mode, R2-Compatible)
3
 
4
  import asyncio
5
  import httpx
@@ -15,66 +15,37 @@ logging.getLogger("httpx").setLevel(logging.WARNING)
15
  logging.getLogger("httpcore").setLevel(logging.WARNING)
16
  logging.getLogger("ccxt").setLevel(logging.WARNING)
17
 
18
-
19
  class DataManager:
20
  def __init__(self, contracts_db, whale_monitor, r2_service=None):
21
- """
22
- DataManager
23
- ----------
24
- contracts_db : dict
25
- قاعدة بيانات العقود (تُحمّل من R2 عند توفره).
26
- whale_monitor : أي كائن حيتان خارجي (EnhancedWhaleMonitor) أو None
27
- يتم تمريره من app.py وربطه لاحقاً.
28
- r2_service : كائن خدمة R2 أو None
29
- يُستخدم لتحميل/حفظ قاعدة العقود.
30
- """
31
- # إعدادات التحكم
32
- self.contracts_db: Dict[str, Any] = contracts_db or {}
33
  self.whale_monitor = whale_monitor
34
  self.r2_service = r2_service
35
-
36
- # إعداد المنصة (KuCoin Spot)
37
- self.exchange = ccxt.kucoin(
38
- {
39
- "enableRateLimit": True,
40
- "timeout": 60000,
41
- "options": {"defaultType": "spot"},
42
- }
43
- )
44
-
45
- # HTTP client + كاش الأسواق
46
  self.http_client = None
47
- self.market_cache: Dict[str, Any] = {}
48
-
49
  # قوائم الاستبعاد
50
  self.BLACKLIST_TOKENS = [
51
- "USDT",
52
- "USDC",
53
- "DAI",
54
- "TUSD",
55
- "BUSD",
56
- "FDUSD",
57
- "EUR",
58
- "PAX",
59
- "UP",
60
- "DOWN",
61
- "BEAR",
62
- "BULL",
63
- "3S",
64
- "3L",
65
  ]
66
 
67
- # ==================================================================
68
- # 🚀 Lifecycle
69
- # ==================================================================
70
  async def initialize(self):
71
  """تهيئة مدير البيانات والاتصالات"""
72
  print(" > [DataManager] Starting initialization...")
73
  self.http_client = httpx.AsyncClient(timeout=30.0)
74
  await self._load_markets()
75
- # تحميل العقود من R2 إن وجد
76
- await self.load_contracts_from_r2()
77
- print("✅ [DataManager] Ready (Logic Tree: Tuned/Flexible).")
78
 
79
  async def _load_markets(self):
80
  try:
@@ -86,150 +57,104 @@ class DataManager:
86
  traceback.print_exc()
87
 
88
  async def close(self):
89
- try:
90
- if self.http_client:
91
- await self.http_client.aclose()
92
- finally:
93
- self.http_client = None
94
-
95
- try:
96
- if self.exchange:
97
- await self.exchange.close()
98
- except Exception:
99
- pass
100
-
101
- def get_contracts_db(self) -> Dict[str, Any]:
102
- return self.contracts_db
103
 
104
  # ==================================================================
105
  # 🚀 R2 Compatibility
106
  # ==================================================================
107
  async def load_contracts_from_r2(self):
108
- """تحميل contracts_db من R2 إن كانت الخدمة متاحة"""
109
- if not self.r2_service:
110
- return
111
  try:
112
  self.contracts_db = await self.r2_service.load_contracts_db_async()
113
- except Exception as e:
114
- print(f"❌ [DataManager] load_contracts_from_r2 failed: {e}")
115
 
 
 
 
116
  # ==================================================================
117
- # 🛡️ Layer 1: Tuned Decision Tree Screening
118
  # ==================================================================
119
  async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
120
- print("🔍 [Layer 1] Initiating Tuned Logic Tree Screening...")
121
-
122
  # 1. المرحلة 0: فلتر الكون (مخفف)
123
  initial_candidates = await self._stage0_universe_filter()
 
124
  if not initial_candidates:
125
  return []
126
 
127
  # 2. جلب البيانات الفنية
128
- top_liquid_candidates = initial_candidates[:300]
129
  enriched_data = await self._fetch_technical_data_batch(top_liquid_candidates)
130
-
131
  # 3. تطبيق شجرة القرار (Overbought -> Classify)
132
- breakout_list: List[Dict[str, Any]] = []
133
- reversal_list: List[Dict[str, Any]] = []
134
- neutral_list: List[Dict[str, Any]] = []
135
 
136
  for item in enriched_data:
137
  classification = self._apply_logic_tree(item)
138
- ctype = classification.get("type")
139
-
140
- # Stage 2 hard filter: تخلص فقط من الحالات المرفوضة تماماً
141
- if ctype == "FILTERED":
142
- continue
143
-
144
- if ctype == "BREAKOUT":
145
- item["l1_sort_score"] = float(classification.get("score", 0.0))
146
  breakout_list.append(item)
147
- elif ctype == "REVERSAL":
148
- item["l1_sort_score"] = float(classification.get("score", 0.0))
149
  reversal_list.append(item)
150
- else:
151
- # عملات "عادية" تمر كفلتر أولي فقط – نرتبها بالسيولة
152
- item["l1_sort_score"] = float(item.get("quote_volume", 0.0) or 0.0)
153
- neutral_list.append(item)
154
 
155
- print(
156
- f" -> [L1 Logic] Found: {len(breakout_list)} Breakouts, "
157
- f"{len(reversal_list)} Reversals, {len(neutral_list)} Neutral."
158
- )
159
 
160
  # 4. الترتيب والدمج النهائي
161
- breakout_list.sort(key=lambda x: x["l1_sort_score"], reverse=True)
162
- reversal_list.sort(key=lambda x: x["l1_sort_score"], reverse=False)
163
- neutral_list.sort(key=lambda x: x["l1_sort_score"], reverse=True)
164
-
165
- MAX_L1_CANDIDATES = 150
166
- top_breakouts = breakout_list[:80]
167
- top_reversals = reversal_list[:70]
168
 
169
- remaining_slots = max(
170
- 0, MAX_L1_CANDIDATES - len(top_breakouts) - len(top_reversals)
171
- )
172
- top_neutrals = neutral_list[:remaining_slots] if remaining_slots > 0 else []
173
-
174
- final_selection = top_breakouts + top_reversals + top_neutrals
175
-
176
- cleaned_selection: List[Dict[str, Any]] = []
177
  for item in final_selection:
178
- cleaned_selection.append(
179
- {
180
- "symbol": item["symbol"],
181
- "quote_volume": item.get("quote_volume", 0.0),
182
- "current_price": item.get("current_price", 0.0),
183
- "type": item.get("type", "UNKNOWN"),
184
- "l1_score": item.get("l1_sort_score", 0.0),
185
- }
186
- )
187
-
188
- print(
189
- f"✅ [Layer 1] Final Selection: {len(cleaned_selection)} candidates passed to models."
190
- )
191
  return cleaned_selection
192
 
193
  # ------------------------------------------------------------------
194
- # Stage 0: Universe Filter (RELAXED) + إزالة شرط الحد الأدنى للسعر
195
  # ------------------------------------------------------------------
196
  async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
197
  try:
198
  tickers = await self.exchange.fetch_tickers()
199
- candidates: List[Dict[str, Any]] = []
200
-
201
  for symbol, ticker in tickers.items():
202
- # نعمل فقط على أزواج USDT
203
- if not symbol.endswith("/USDT"):
204
- continue
205
-
206
- base_curr = symbol.split("/")[0]
207
- if any(bad in base_curr for bad in self.BLACKLIST_TOKENS):
208
- continue
209
-
210
- # سيولة دنيا
211
- quote_vol = ticker.get("quoteVolume")
212
- if not quote_vol or quote_vol < 1_000_000:
213
- continue
214
-
215
- last_price = ticker.get("last")
216
- # تم إلغاء شرط 0.0005 بالكامل – نسمح بأي سعر > 0 ما دام موجوداً
217
- if last_price is None:
218
- continue
219
-
220
- candidates.append(
221
- {
222
- "symbol": symbol,
223
- "quote_volume": quote_vol,
224
- "current_price": last_price,
225
- "change_24h": ticker.get("percentage", 0.0),
226
- }
227
- )
228
-
229
- candidates.sort(key=lambda x: x["quote_volume"], reverse=True)
230
- print(f" -> [Stage0] Universe Filter found {len(candidates)} USDT pairs.")
231
  return candidates
232
-
233
  except Exception as e:
234
  print(f"❌ [L1 Error] Universe filter failed: {e}")
235
  return []
@@ -237,199 +162,156 @@ class DataManager:
237
  # ------------------------------------------------------------------
238
  # Data Fetching Helpers
239
  # ------------------------------------------------------------------
240
- async def _fetch_technical_data_batch(
241
- self, candidates: List[Dict[str, Any]]
242
- ) -> List[Dict[str, Any]]:
243
- results: List[Dict[str, Any]] = []
244
-
245
- async def process_symbol(item: Dict[str, Any]):
246
- symbol = item["symbol"]
247
- try:
248
- ohlcv_1h = await self.exchange.fetch_ohlcv(symbol, "1h", limit=60)
249
- ohlcv_15m = await self.exchange.fetch_ohlcv(symbol, "15m", limit=60)
250
-
251
- if not ohlcv_1h or len(ohlcv_1h) < 55:
252
- return None
253
- if not ohlcv_15m or len(ohlcv_15m) < 55:
254
- return None
255
 
256
- item["ohlcv_1h"] = ohlcv_1h
257
- item["ohlcv_15m"] = ohlcv_15m
258
- return item
259
- except Exception as e:
260
- print(f" -> [Stage1.5] Error fetching OHLCV for {symbol}: {e}")
 
 
261
  return None
262
-
263
- batch_size = 25
264
- for i in range(0, len(candidates), batch_size):
265
- batch = candidates[i : i + batch_size]
266
- tasks = [process_symbol(c) for c in batch]
267
- batch_results = await asyncio.gather(*tasks, return_exceptions=False)
268
- for r in batch_results:
269
- if r is not None:
270
- results.append(r)
271
- await asyncio.sleep(0.1)
272
-
273
- print(f" -> [Stage1.5] Enriched OHLCV for {len(results)} symbols.")
274
- return results
275
 
276
  # ------------------------------------------------------------------
277
- # Stage 2 + 3: Overbought Filter + Classification
278
  # ------------------------------------------------------------------
279
  def _apply_logic_tree(self, data: Dict[str, Any]) -> Dict[str, Any]:
280
  try:
281
- df_1h = self._calc_indicators(data["ohlcv_1h"])
282
- df_15m = self._calc_indicators(data["ohlcv_15m"])
283
- except Exception as e:
284
- print(f"❌ [LogicTree] Indicator calc failed: {e}")
285
- return {"type": "FILTERED"}
286
 
287
  curr_1h = df_1h.iloc[-1]
288
  curr_15m = df_15m.iloc[-1]
289
-
290
- # --- Stage 2: Overbought / Extreme Pump Filter (RELAXED HARD FILTER) ---
291
  try:
292
- close_4h_ago = df_1h.iloc[-5]["close"]
293
- change_4h = ((curr_1h["close"] - close_4h_ago) / close_4h_ago) * 100.0
294
- except Exception:
295
- change_4h = 0.0
296
 
297
- # فلتر "الجنون" فقط
298
- if change_4h > 25.0:
299
- return {"type": "FILTERED"}
300
- if data.get("change_24h", 0.0) > 40.0:
301
- return {"type": "FILTERED"}
302
- if curr_1h["rsi"] > 88.0:
303
- return {"type": "FILTERED"}
304
 
305
- deviation = (
306
- (curr_1h["close"] - curr_1h["ema20"]) / curr_1h["atr"]
307
- if curr_1h["atr"] and curr_1h["atr"] > 0
308
- else 0.0
309
- )
310
- if deviation > 3.5:
311
- return {"type": "FILTERED"}
312
 
313
  # --- Stage 3: Classification ---
314
-
315
  # === A. Breakout Logic (RELAXED) ===
316
  is_breakout = False
317
  breakout_score = 0.0
318
-
319
- # Trend check (EMA Cross OR Price above EMA20)
320
- bullish_structure = (curr_1h["ema20"] > curr_1h["ema50"]) or (
321
- curr_1h["close"] > curr_1h["ema20"]
322
- )
323
-
324
  if bullish_structure:
325
- # RSI صحي للزخم
326
- if 40.0 <= curr_1h["rsi"] <= 70.0:
327
  # 15m bullish
328
- if curr_15m["close"] >= curr_15m["ema20"]:
329
  # Volatility check (Range)
330
- avg_range = (
331
- (df_15m["high"] - df_15m["low"])
332
- .rolling(10)
333
- .mean()
334
- .iloc[-1]
335
- )
336
- # Less strict squeeze check
337
- if (curr_15m["high"] - curr_15m["low"]) <= avg_range * 1.5:
338
- vol_ma20 = df_15m["volume"].rolling(20).mean().iloc[-1]
339
- if vol_ma20 and vol_ma20 > 0:
340
- if curr_15m["volume"] >= 1.2 * vol_ma20:
341
- is_breakout = True
342
- breakout_score = curr_15m["volume"] / vol_ma20
343
- else:
344
- # حجم تداول بدون MA موثوق
345
  is_breakout = True
346
- breakout_score = 1.0
347
 
348
  if is_breakout:
349
- data["type"] = "BREAKOUT"
350
- return {"type": "BREAKOUT", "score": breakout_score}
351
 
352
  # === B. Reversal Logic (RELAXED) ===
353
  is_reversal = False
354
- reversal_score = 100.0
355
-
356
- if 10.0 <= curr_1h["rsi"] <= 40.0:
357
- if change_4h <= -3.0:
 
 
 
358
  last_3 = df_15m.iloc[-3:]
359
  found_rejection = False
360
  for _, row in last_3.iterrows():
361
- rng = row["high"] - row["low"]
 
362
  if rng > 0:
363
- is_green = row["close"] > row["open"]
364
- upper_half = row["close"] > (row["low"] + rng * 0.5)
365
  if is_green or upper_half:
366
  found_rejection = True
367
  break
368
-
369
  if found_rejection:
370
  is_reversal = True
371
- reversal_score = float(curr_1h["rsi"])
372
 
373
  if is_reversal:
374
- data["type"] = "REVERSAL"
375
- return {"type": "REVERSAL", "score": reversal_score}
376
 
377
- return {"type": "NONE"}
378
-
379
- # ------------------------------------------------------------------
380
- # Indicator Helper
381
- # ------------------------------------------------------------------
382
- def _calc_indicators(self, ohlcv_list: List[List[float]]) -> pd.DataFrame:
383
- df = pd.DataFrame(
384
- ohlcv_list,
385
- columns=["timestamp", "open", "high", "low", "close", "volume"],
386
- )
387
 
388
- # RSI
389
- delta = df["close"].diff()
390
- gain = delta.where(delta > 0, 0.0).rolling(window=14).mean()
391
- loss = (-delta.where(delta < 0, 0.0)).rolling(window=14).mean()
 
 
392
  rs = gain / loss
393
- df["rsi"] = 100.0 - (100.0 / (1.0 + rs))
394
-
395
- # EMAs
396
- df["ema20"] = df["close"].ewm(span=20, adjust=False).mean()
397
- df["ema50"] = df["close"].ewm(span=50, adjust=False).mean()
398
-
399
- # ATR
400
- high_low = df["high"] - df["low"]
401
- high_close = (df["high"] - df["close"].shift()).abs()
402
- low_close = (df["low"] - df["close"].shift()).abs()
403
  ranges = pd.concat([high_low, high_close, low_close], axis=1)
404
- true_range = ranges.max(axis=1)
405
- df["atr"] = true_range.rolling(14).mean()
406
-
 
407
  return df
408
 
409
  # ==================================================================
410
- # 🎯 Public Helpers (used by TradeManager / Processor / app.py)
411
  # ==================================================================
412
  async def get_latest_price_async(self, symbol: str) -> float:
413
  try:
414
  ticker = await self.exchange.fetch_ticker(symbol)
415
- return float(ticker["last"])
416
- except Exception:
417
- return 0.0
418
 
419
- async def get_latest_ohlcv(
420
- self, symbol: str, timeframe: str = "5m", limit: int = 100
421
- ) -> List[List[float]]:
422
  try:
423
  candles = await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
424
  return candles or []
425
- except Exception:
426
- return []
427
 
428
- async def get_order_book_snapshot(
429
- self, symbol: str, limit: int = 20
430
- ) -> Dict[str, Any]:
431
  try:
432
  ob = await self.exchange.fetch_order_book(symbol, limit)
433
  return ob
434
- except Exception:
435
- return {}
 
1
  # ml_engine/data_manager.py
2
+ # (V15.1 - GEM-Architect: Tuned Logic Tree - Marksman Mode)
3
 
4
  import asyncio
5
  import httpx
 
15
  logging.getLogger("httpcore").setLevel(logging.WARNING)
16
  logging.getLogger("ccxt").setLevel(logging.WARNING)
17
 
 
18
  class DataManager:
19
  def __init__(self, contracts_db, whale_monitor, r2_service=None):
20
+ # ==================================================================
21
+ # ⚙️ إعدادات التحكم
22
+ # ==================================================================
23
+ self.contracts_db = contracts_db or {}
 
 
 
 
 
 
 
 
24
  self.whale_monitor = whale_monitor
25
  self.r2_service = r2_service
26
+
27
+ # إعداد المنصة (KuCoin)
28
+ self.exchange = ccxt.kucoin({
29
+ 'enableRateLimit': True,
30
+ 'timeout': 60000,
31
+ 'options': {'defaultType': 'spot'}
32
+ })
33
+
 
 
 
34
  self.http_client = None
35
+ self.market_cache = {}
36
+
37
  # قوائم الاستبعاد
38
  self.BLACKLIST_TOKENS = [
39
+ 'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
40
+ 'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L'
 
 
 
 
 
 
 
 
 
 
 
 
41
  ]
42
 
 
 
 
43
  async def initialize(self):
44
  """تهيئة مدير البيانات والاتصالات"""
45
  print(" > [DataManager] Starting initialization...")
46
  self.http_client = httpx.AsyncClient(timeout=30.0)
47
  await self._load_markets()
48
+ print(f"✅ [DataManager V15.1] Ready (Logic Tree: Tuned/Flexible).")
 
 
49
 
50
  async def _load_markets(self):
51
  try:
 
57
  traceback.print_exc()
58
 
59
  async def close(self):
60
+ if self.http_client: await self.http_client.aclose()
61
+ if self.exchange: await self.exchange.close()
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
  # ==================================================================
64
  # 🚀 R2 Compatibility
65
  # ==================================================================
66
  async def load_contracts_from_r2(self):
67
+ if not self.r2_service: return
 
 
68
  try:
69
  self.contracts_db = await self.r2_service.load_contracts_db_async()
70
+ except Exception:
71
+ self.contracts_db = {}
72
 
73
+ def get_contracts_db(self) -> Dict[str, Any]:
74
+ return self.contracts_db
75
+
76
  # ==================================================================
77
+ # 🛡️ Layer 1: The Tuned Decision Tree Screening
78
  # ==================================================================
79
  async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
80
+ print(f"🔍 [Layer 1] Initiating Tuned Logic Tree Screening...")
81
+
82
  # 1. المرحلة 0: فلتر الكون (مخفف)
83
  initial_candidates = await self._stage0_universe_filter()
84
+
85
  if not initial_candidates:
86
  return []
87
 
88
  # 2. جلب البيانات الفنية
89
+ top_liquid_candidates = initial_candidates[:300]
90
  enriched_data = await self._fetch_technical_data_batch(top_liquid_candidates)
91
+
92
  # 3. تطبيق شجرة القرار (Overbought -> Classify)
93
+ breakout_list = []
94
+ reversal_list = []
 
95
 
96
  for item in enriched_data:
97
  classification = self._apply_logic_tree(item)
98
+
99
+ if classification['type'] == 'BREAKOUT':
100
+ item['l1_sort_score'] = classification['score']
 
 
 
 
 
101
  breakout_list.append(item)
102
+ elif classification['type'] == 'REVERSAL':
103
+ item['l1_sort_score'] = classification['score']
104
  reversal_list.append(item)
 
 
 
 
105
 
106
+ print(f" -> [L1 Logic] Found: {len(breakout_list)} Breakouts, {len(reversal_list)} Reversals.")
 
 
 
107
 
108
  # 4. الترتيب والدمج النهائي
109
+ breakout_list.sort(key=lambda x: x['l1_sort_score'], reverse=True)
110
+ reversal_list.sort(key=lambda x: x['l1_sort_score'], reverse=False)
 
 
 
 
 
111
 
112
+ final_selection = breakout_list[:80] + reversal_list[:70]
113
+
114
+ cleaned_selection = []
 
 
 
 
 
115
  for item in final_selection:
116
+ cleaned_selection.append({
117
+ 'symbol': item['symbol'],
118
+ 'quote_volume': item.get('quote_volume', 0),
119
+ 'current_price': item.get('current_price', 0),
120
+ 'type': item.get('type', 'UNKNOWN'), # نمرر النوع لـ app.py إذا رغب باستخدامه
121
+ 'l1_score': item.get('l1_sort_score', 0)
122
+ })
123
+
124
+ print(f"✅ [Layer 1] Final Selection: {len(cleaned_selection)} candidates passed to models.")
 
 
 
 
125
  return cleaned_selection
126
 
127
  # ------------------------------------------------------------------
128
+ # Stage 0: Universe Filter (RELAXED)
129
  # ------------------------------------------------------------------
130
  async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
131
  try:
132
  tickers = await self.exchange.fetch_tickers()
133
+ candidates = []
134
+
135
  for symbol, ticker in tickers.items():
136
+ if not symbol.endswith('/USDT'): continue
137
+
138
+ base_curr = symbol.split('/')[0]
139
+ if any(bad in base_curr for bad in self.BLACKLIST_TOKENS): continue
140
+
141
+ # 👇 [Tuning] خفضنا السيولة المطلوبة لمليون واحد فقط
142
+ quote_vol = ticker.get('quoteVolume')
143
+ if not quote_vol or quote_vol < 1_000_000: continue
144
+
145
+ last_price = ticker.get('last')
146
+ if not last_price or last_price < 0.0005: continue
147
+
148
+ candidates.append({
149
+ 'symbol': symbol,
150
+ 'quote_volume': quote_vol,
151
+ 'current_price': last_price,
152
+ 'change_24h': ticker.get('percentage', 0.0)
153
+ })
154
+
155
+ candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
 
 
 
 
 
 
 
 
 
156
  return candidates
157
+
158
  except Exception as e:
159
  print(f"❌ [L1 Error] Universe filter failed: {e}")
160
  return []
 
162
  # ------------------------------------------------------------------
163
  # Data Fetching Helpers
164
  # ------------------------------------------------------------------
165
+ async def _fetch_technical_data_batch(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
166
+ chunk_size = 10
167
+ results = []
168
+ for i in range(0, len(candidates), chunk_size):
169
+ chunk = candidates[i:i + chunk_size]
170
+ chunk_tasks = [self._fetch_single_tech_data(c) for c in chunk]
171
+ chunk_results = await asyncio.gather(*chunk_tasks)
172
+ results.extend([r for r in chunk_results if r is not None])
173
+ await asyncio.sleep(0.1) # تسريع قليل
174
+ return results
 
 
 
 
 
175
 
176
+ async def _fetch_single_tech_data(self, candidate: Dict[str, Any]) -> Any:
177
+ symbol = candidate['symbol']
178
+ try:
179
+ ohlcv_1h = await self.exchange.fetch_ohlcv(symbol, '1h', limit=60)
180
+ ohlcv_15m = await self.exchange.fetch_ohlcv(symbol, '15m', limit=60)
181
+
182
+ if not ohlcv_1h or len(ohlcv_1h) < 55 or not ohlcv_15m or len(ohlcv_15m) < 55:
183
  return None
184
+
185
+ candidate['ohlcv_1h'] = ohlcv_1h
186
+ candidate['ohlcv_15m'] = ohlcv_15m
187
+ return candidate
188
+ except Exception:
189
+ return None
 
 
 
 
 
 
 
190
 
191
  # ------------------------------------------------------------------
192
+ # 🧠 The Logic Core: Math & Decision Tree (RELAXED)
193
  # ------------------------------------------------------------------
194
  def _apply_logic_tree(self, data: Dict[str, Any]) -> Dict[str, Any]:
195
  try:
196
+ df_1h = self._calc_indicators(data['ohlcv_1h'])
197
+ df_15m = self._calc_indicators(data['ohlcv_15m'])
198
+ except:
199
+ return {'type': 'NONE'}
 
200
 
201
  curr_1h = df_1h.iloc[-1]
202
  curr_15m = df_15m.iloc[-1]
203
+
204
+ # --- Stage 2: Overbought Filter ---
205
  try:
206
+ close_4h_ago = df_1h.iloc[-5]['close']
207
+ change_4h = ((curr_1h['close'] - close_4h_ago) / close_4h_ago) * 100
208
+ except: change_4h = 0.0
 
209
 
210
+ if change_4h > 15.0: return {'type': 'NONE'}
211
+ if data.get('change_24h', 0) > 25.0: return {'type': 'NONE'}
212
+ if curr_1h['rsi'] > 80: return {'type': 'NONE'} # 👇 [Tuning] سمحنا بـ RSI أعلى قليلاً
 
 
 
 
213
 
214
+ deviation = (curr_1h['close'] - curr_1h['ema20']) / curr_1h['atr'] if curr_1h['atr'] > 0 else 0
215
+ if deviation > 2.5: return {'type': 'NONE'} # 👇 [Tuning] سمحنا بإنحراف أكبر قليلاً
 
 
 
 
 
216
 
217
  # --- Stage 3: Classification ---
218
+
219
  # === A. Breakout Logic (RELAXED) ===
220
  is_breakout = False
221
  breakout_score = 0.0
222
+
223
+ # Trend check (EMA Cross OR Price above both EMAs)
224
+ bullish_structure = (curr_1h['ema20'] > curr_1h['ema50']) or (curr_1h['close'] > curr_1h['ema20'])
225
+
 
 
226
  if bullish_structure:
227
+ # 👇 [Tuning] RSI range expanded
228
+ if 40 <= curr_1h['rsi'] <= 70:
229
  # 15m bullish
230
+ if curr_15m['close'] >= curr_15m['ema20']:
231
  # Volatility check (Range)
232
+ avg_range = (df_15m['high'] - df_15m['low']).rolling(10).mean().iloc[-1]
233
+ # 👇 [Tuning] Less strict squeeze check (1.5x avg range allowed)
234
+ if (curr_15m['high'] - curr_15m['low']) <= avg_range * 1.5:
235
+ vol_ma20 = df_15m['volume'].rolling(20).mean().iloc[-1]
236
+ # 👇 [Tuning] Volume Spike lowered to 1.2x
237
+ if curr_15m['volume'] >= 1.2 * vol_ma20:
 
 
 
 
 
 
 
 
 
238
  is_breakout = True
239
+ breakout_score = curr_15m['volume'] / vol_ma20 if vol_ma20 > 0 else 1.0
240
 
241
  if is_breakout:
242
+ data['type'] = 'BREAKOUT'
243
+ return {'type': 'BREAKOUT', 'score': breakout_score}
244
 
245
  # === B. Reversal Logic (RELAXED) ===
246
  is_reversal = False
247
+ reversal_score = 100.0
248
+
249
+ # 👇 [Tuning] RSI threshold increased to 40
250
+ if 10 <= curr_1h['rsi'] <= 40:
251
+ # 👇 [Tuning] Drop requirement reduced to -3%
252
+ if change_4h <= -3.0:
253
+ # Rejection check (Any bullish closing in last 3 candles)
254
  last_3 = df_15m.iloc[-3:]
255
  found_rejection = False
256
  for _, row in last_3.iterrows():
257
+ # 👇 [Tuning] Simple logic: Green candle OR Close in upper half
258
+ rng = row['high'] - row['low']
259
  if rng > 0:
260
+ is_green = row['close'] > row['open']
261
+ upper_half = row['close'] > (row['low'] + rng * 0.5)
262
  if is_green or upper_half:
263
  found_rejection = True
264
  break
265
+
266
  if found_rejection:
267
  is_reversal = True
268
+ reversal_score = curr_1h['rsi']
269
 
270
  if is_reversal:
271
+ data['type'] = 'REVERSAL'
272
+ return {'type': 'REVERSAL', 'score': reversal_score}
273
 
274
+ return {'type': 'NONE'}
 
 
 
 
 
 
 
 
 
275
 
276
+ def _calc_indicators(self, ohlcv_list):
277
+ df = pd.DataFrame(ohlcv_list, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
278
+
279
+ delta = df['close'].diff()
280
+ gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
281
+ loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
282
  rs = gain / loss
283
+ df['rsi'] = 100 - (100 / (1 + rs))
284
+
285
+ df['ema20'] = df['close'].ewm(span=20, adjust=False).mean()
286
+ df['ema50'] = df['close'].ewm(span=50, adjust=False).mean()
287
+
288
+ high_low = df['high'] - df['low']
289
+ high_close = np.abs(df['high'] - df['close'].shift())
290
+ low_close = np.abs(df['low'] - df['close'].shift())
 
 
291
  ranges = pd.concat([high_low, high_close, low_close], axis=1)
292
+ true_range = np.max(ranges, axis=1)
293
+ df['atr'] = true_range.rolling(14).mean()
294
+
295
+ df.fillna(0, inplace=True)
296
  return df
297
 
298
  # ==================================================================
299
+ # 🎯 Public Helpers
300
  # ==================================================================
301
  async def get_latest_price_async(self, symbol: str) -> float:
302
  try:
303
  ticker = await self.exchange.fetch_ticker(symbol)
304
+ return float(ticker['last'])
305
+ except Exception: return 0.0
 
306
 
307
+ async def get_latest_ohlcv(self, symbol: str, timeframe: str = '5m', limit: int = 100) -> List[List[float]]:
 
 
308
  try:
309
  candles = await self.exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
310
  return candles or []
311
+ except Exception: return []
 
312
 
313
+ async def get_order_book_snapshot(self, symbol: str, limit: int = 20) -> Dict[str, Any]:
 
 
314
  try:
315
  ob = await self.exchange.fetch_order_book(symbol, limit)
316
  return ob
317
+ except Exception: return {}