Riy777 commited on
Commit
d11dbf3
·
verified ·
1 Parent(s): 576a6dc

Update ml_engine/data_manager.py

Browse files
Files changed (1) hide show
  1. ml_engine/data_manager.py +77 -111
ml_engine/data_manager.py CHANGED
@@ -1,5 +1,5 @@
1
  # ml_engine/data_manager.py
2
- # (V15.0 - GEM-Architect: Advanced L1 Decision Tree Filter)
3
 
4
  import asyncio
5
  import httpx
@@ -8,7 +8,7 @@ import ccxt.async_support as ccxt
8
  import logging
9
  import pandas as pd
10
  import numpy as np
11
- from typing import List, Dict, Any, Tuple
12
 
13
  # إعدادات التسجيل
14
  logging.getLogger("httpx").setLevel(logging.WARNING)
@@ -25,7 +25,6 @@ class DataManager:
25
  self.r2_service = r2_service
26
 
27
  # إعداد المنصة (KuCoin)
28
- # نرفع التايم أوت ونفعل Rate Limit لضمان استقرار جلب الشموع الكثيف
29
  self.exchange = ccxt.kucoin({
30
  'enableRateLimit': True,
31
  'timeout': 60000,
@@ -35,7 +34,7 @@ class DataManager:
35
  self.http_client = None
36
  self.market_cache = {}
37
 
38
- # قوائم الاستبعاد (Stablecoins & Leverage Tokens)
39
  self.BLACKLIST_TOKENS = [
40
  'USDT', 'USDC', 'DAI', 'TUSD', 'BUSD', 'FDUSD', 'EUR', 'PAX',
41
  'UP', 'DOWN', 'BEAR', 'BULL', '3S', '3L'
@@ -46,10 +45,9 @@ class DataManager:
46
  print(" > [DataManager] Starting initialization...")
47
  self.http_client = httpx.AsyncClient(timeout=30.0)
48
  await self._load_markets()
49
- print(f"✅ [DataManager V15.0] Ready (Advanced L1 Tree Filter).")
50
 
51
  async def _load_markets(self):
52
- """تحميل بيانات الأسواق وتخزينها مؤقتاً"""
53
  try:
54
  if self.exchange:
55
  await self.exchange.load_markets()
@@ -59,8 +57,6 @@ class DataManager:
59
  traceback.print_exc()
60
 
61
  async def close(self):
62
- """إغلاق جميع الاتصالات بأمان"""
63
- print(" > [DataManager] Closing connections...")
64
  if self.http_client: await self.http_client.aclose()
65
  if self.exchange: await self.exchange.close()
66
 
@@ -71,7 +67,6 @@ class DataManager:
71
  if not self.r2_service: return
72
  try:
73
  self.contracts_db = await self.r2_service.load_contracts_db_async()
74
- print(f"✅ [DataManager] Contracts loaded: {len(self.contracts_db)}")
75
  except Exception:
76
  self.contracts_db = {}
77
 
@@ -79,58 +74,58 @@ class DataManager:
79
  return self.contracts_db
80
 
81
  # ==================================================================
82
- # 🛡️ Layer 1: The Advanced Decision Tree Screening
83
  # ==================================================================
84
  async def layer1_rapid_screening(self) -> List[Dict[str, Any]]:
85
- """
86
- تنفيذ شجرة القرار (Universe -> Overbought -> Breakout/Reversal).
87
- الهدف: استخراج 150 عملة نقية فنيًا.
88
- """
89
- print(f"🔍 [Layer 1] Initiating Advanced Logic Tree Screening...")
90
 
91
- # 1. المرحلة 0: فلتر الكون (Universe Filter) - تصفية أولية سريعة
92
  initial_candidates = await self._stage0_universe_filter()
93
- print(f" -> [L1 Stage 0] Universe Passed: {len(initial_candidates)} tickers.")
94
-
95
  if not initial_candidates:
96
  return []
97
 
98
- # 2. جلب البيانات الفنية (OHLCV) للناجين فقط
99
- # نحدد عددًا أقصى للمعالجة لتجنب بطء شديد (مثلاً أفضل 300 سيولة)
100
  top_liquid_candidates = initial_candidates[:300]
101
  enriched_data = await self._fetch_technical_data_batch(top_liquid_candidates)
102
- print(f" -> [L1 Data] Fetched technicals for {len(enriched_data)} tickers.")
103
-
104
  # 3. تطبيق شجرة القرار (Overbought -> Classify)
105
  breakout_list = []
106
  reversal_list = []
107
 
108
  for item in enriched_data:
109
  classification = self._apply_logic_tree(item)
 
110
  if classification['type'] == 'BREAKOUT':
111
- item['l1_score'] = classification['score'] # Volume Ratio
112
  breakout_list.append(item)
113
  elif classification['type'] == 'REVERSAL':
114
- item['l1_score'] = classification['score'] # RSI (Low is better)
115
  reversal_list.append(item)
116
 
117
  print(f" -> [L1 Logic] Found: {len(breakout_list)} Breakouts, {len(reversal_list)} Reversals.")
118
 
119
- # 4. الترتيب والدمج النهائي (Selection)
120
- # ترتيب الاختراق: الأكبر في نسبة انفجار الحجم ��ولاً
121
- breakout_list.sort(key=lambda x: x['l1_score'], reverse=True)
122
-
123
- # ترتيب الانعكاس: الـ RSI الأقل أولاً (لأن السكور هو RSI الخام)
124
- reversal_list.sort(key=lambda x: x['l1_score'], reverse=False)
125
 
126
- # الدمج: 80 اختراق + 70 انعكاس = 150
127
  final_selection = breakout_list[:80] + reversal_list[:70]
128
 
129
- print(f"✅ [Layer 1] Final Selection: {len(final_selection)} candidates ready for models.")
130
- return final_selection
 
 
 
 
 
 
 
 
 
 
131
 
132
  # ------------------------------------------------------------------
133
- # Stage 0: Universe Filter (Liquidity & Valid Pairs)
134
  # ------------------------------------------------------------------
135
  async def _stage0_universe_filter(self) -> List[Dict[str, Any]]:
136
  try:
@@ -138,18 +133,15 @@ class DataManager:
138
  candidates = []
139
 
140
  for symbol, ticker in tickers.items():
141
- # 1. فلتر الزوج (USDT Only)
142
  if not symbol.endswith('/USDT'): continue
143
 
144
- # 2. فلتر العملات المحظورة والمستقرة
145
  base_curr = symbol.split('/')[0]
146
  if any(bad in base_curr for bad in self.BLACKLIST_TOKENS): continue
147
 
148
- # 3. فلتر السيولة (2M USDT)
149
  quote_vol = ticker.get('quoteVolume')
150
- if not quote_vol or quote_vol < 2_000_000: continue
151
 
152
- # 4. فلتر السعر الميت (Dust)
153
  last_price = ticker.get('last')
154
  if not last_price or last_price < 0.0005: continue
155
 
@@ -157,11 +149,9 @@ class DataManager:
157
  'symbol': symbol,
158
  'quote_volume': quote_vol,
159
  'current_price': last_price,
160
- # نحتفظ بهذه القيم للفلتر الأولي
161
  'change_24h': ticker.get('percentage', 0.0)
162
  })
163
 
164
- # ترتيب مبدئي بالسيولة لضمان أننا نختار أفضل 300 للمعالجة العميقة
165
  candidates.sort(key=lambda x: x['quote_volume'], reverse=True)
166
  return candidates
167
 
@@ -170,30 +160,22 @@ class DataManager:
170
  return []
171
 
172
  # ------------------------------------------------------------------
173
- # Data Fetching Helpers (Parallel)
174
  # ------------------------------------------------------------------
175
  async def _fetch_technical_data_batch(self, candidates: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
176
- """جلب بيانات 1h و 15m بالتوازي مع Rate Limiting"""
177
- tasks = []
178
- # نقسم العمل إلى دفعات صغيرة لتجنب إغراق الـ Rate Limit
179
  chunk_size = 10
180
  results = []
181
-
182
  for i in range(0, len(candidates), chunk_size):
183
  chunk = candidates[i:i + chunk_size]
184
  chunk_tasks = [self._fetch_single_tech_data(c) for c in chunk]
185
  chunk_results = await asyncio.gather(*chunk_tasks)
186
  results.extend([r for r in chunk_results if r is not None])
187
- # انتظار بسيط بين الدفعات
188
- await asyncio.sleep(0.2)
189
-
190
  return results
191
 
192
  async def _fetch_single_tech_data(self, candidate: Dict[str, Any]) -> Any:
193
  symbol = candidate['symbol']
194
  try:
195
- # نحتاج 1h (للترند والـ RSI) و 15m (للدخول والانفجار)
196
- # نطلب عدد شموع يكفي لحساب EMA50 و RSI14
197
  ohlcv_1h = await self.exchange.fetch_ohlcv(symbol, '1h', limit=60)
198
  ohlcv_15m = await self.exchange.fetch_ohlcv(symbol, '15m', limit=60)
199
 
@@ -207,115 +189,102 @@ class DataManager:
207
  return None
208
 
209
  # ------------------------------------------------------------------
210
- # 🧠 The Logic Core: Math & Decision Tree
211
  # ------------------------------------------------------------------
212
  def _apply_logic_tree(self, data: Dict[str, Any]) -> Dict[str, Any]:
213
- """
214
- تطبيق شروط الفلتر:
215
- Returns: {'type': 'BREAKOUT'|'REVERSAL'|'NONE', 'score': float}
216
- """
217
- # 1. Prepare DataFrames & Indicators
218
  try:
219
  df_1h = self._calc_indicators(data['ohlcv_1h'])
220
  df_15m = self._calc_indicators(data['ohlcv_15m'])
221
  except:
222
  return {'type': 'NONE'}
223
 
224
- # Last Candles
225
  curr_1h = df_1h.iloc[-1]
226
  curr_15m = df_15m.iloc[-1]
227
 
228
- # --- Stage 2: Overbought Filter (Reject Pumped Coins) ---
229
- # أ) التغير السعري المبالغ فيه (4h)
230
- # نحسب تغير 4 ساعات تقريبياً من إغلاق الشمعة الحالية وإغلاق الشمعة قبل 4
231
  try:
232
  close_4h_ago = df_1h.iloc[-5]['close']
233
  change_4h = ((curr_1h['close'] - close_4h_ago) / close_4h_ago) * 100
234
  except: change_4h = 0.0
235
 
236
- if change_4h > 15.0: return {'type': 'NONE', 'reason': 'Pumped 4h'}
237
- if data.get('change_24h', 0) > 25.0: return {'type': 'NONE', 'reason': 'Pumped 24h'}
238
-
239
- # ب) RSI المبالغ فيه
240
- if curr_1h['rsi'] > 75: return {'type': 'NONE', 'reason': 'Overbought RSI'}
241
 
242
- # ج) الامتداد السعري (Deviation)
243
- deviation = (curr_1h['close'] - curr_1h['ema20']) / curr_1h['atr']
244
- if deviation > 2.0: return {'type': 'NONE', 'reason': 'Overextended'}
245
 
246
  # --- Stage 3: Classification ---
247
 
248
- # === A. Breakout Candidate Logic ===
249
  is_breakout = False
250
  breakout_score = 0.0
251
 
252
- # 1. Trend Filter (1h)
253
- if (curr_1h['ema20'] > curr_1h['ema50']) and (curr_1h['close'] > curr_1h['ema20']):
254
- # 2. RSI Check (Healthy Zone)
255
- if 40 <= curr_1h['rsi'] <= 65:
256
- # 3. 15m Confirmation
257
- if (curr_15m['ema20'] >= curr_15m['ema50']) and (curr_15m['close'] >= curr_15m['ema20']):
258
- # 4. Squeeze / Low Volatility Check (Range Compression)
259
- # بديل BBW: نقارن ATR الحالي بمتوسط الـ ATR القديم أو مدى الشموع
260
- avg_range_last_10 = (df_15m['high'] - df_15m['low']).rolling(10).mean().iloc[-1]
261
- if curr_15m['high'] - curr_15m['low'] <= avg_range_last_10 * 1.2: # ليس شرطاً صارماً جداً
262
- # 5. Volume Spike (Trigger)
 
263
  vol_ma20 = df_15m['volume'].rolling(20).mean().iloc[-1]
264
- if curr_15m['volume'] >= 1.5 * vol_ma20:
 
265
  is_breakout = True
266
- # Score = Volume Ratio
267
  breakout_score = curr_15m['volume'] / vol_ma20 if vol_ma20 > 0 else 1.0
268
 
269
  if is_breakout:
 
270
  return {'type': 'BREAKOUT', 'score': breakout_score}
271
 
272
- # === B. Reversal Candidate Logic ===
273
  is_reversal = False
274
- reversal_score = 100.0 # Default high RSI
275
 
276
- # 1. RSI Oversold (1h)
277
- if 10 <= curr_1h['rsi'] <= 35: # وسعنا النطاق قليلاً لـ 35
278
- # 2. Strong Drop Logic (4h Change is significantly negative)
279
- if change_4h <= -5.0: # هبوط 5% على ا��أقل في 4 ساعات
280
- # 3. Rejection Candle on 15m (Wick Check)
281
- # نبحث في آخر 3 شموع
282
- last_3_candles = df_15m.iloc[-3:]
283
  found_rejection = False
284
- for idx, row in last_3_candles.iterrows():
285
- body_size = abs(row['close'] - row['open'])
286
- lower_wick = min(row['close'], row['open']) - row['low']
287
- candle_range = row['high'] - row['low']
288
-
289
- # شرط الذيل: الذيل السفلي أطول من الجسم، أو يمثل نسبة معتبرة من الشمعة
290
- if (lower_wick > body_size) and (row['close'] > row['low'] + (candle_range * 0.3)):
291
- found_rejection = True
292
- break
293
 
294
  if found_rejection:
295
  is_reversal = True
296
- reversal_score = curr_1h['rsi'] # Lower is better
297
 
298
  if is_reversal:
 
299
  return {'type': 'REVERSAL', 'score': reversal_score}
300
 
301
  return {'type': 'NONE'}
302
 
303
  def _calc_indicators(self, ohlcv_list):
304
- """حساب المؤشرات الأساسية باستخدام Pandas"""
305
  df = pd.DataFrame(ohlcv_list, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
306
 
307
- # RSI 14
308
  delta = df['close'].diff()
309
  gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
310
  loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
311
  rs = gain / loss
312
  df['rsi'] = 100 - (100 / (1 + rs))
313
 
314
- # EMAs
315
  df['ema20'] = df['close'].ewm(span=20, adjust=False).mean()
316
  df['ema50'] = df['close'].ewm(span=50, adjust=False).mean()
317
 
318
- # ATR 14
319
  high_low = df['high'] - df['low']
320
  high_close = np.abs(df['high'] - df['close'].shift())
321
  low_close = np.abs(df['low'] - df['close'].shift())
@@ -323,12 +292,11 @@ class DataManager:
323
  true_range = np.max(ranges, axis=1)
324
  df['atr'] = true_range.rolling(14).mean()
325
 
326
- # تنظيف NaN
327
  df.fillna(0, inplace=True)
328
  return df
329
 
330
  # ==================================================================
331
- # 🎯 Data Fetching Helpers (Public)
332
  # ==================================================================
333
  async def get_latest_price_async(self, symbol: str) -> float:
334
  try:
@@ -346,6 +314,4 @@ class DataManager:
346
  try:
347
  ob = await self.exchange.fetch_order_book(symbol, limit)
348
  return ob
349
- except Exception: return {}
350
-
351
- print("✅ [DataManager V15.0] Loaded with Advanced Decision Tree Filter.")
 
1
  # ml_engine/data_manager.py
2
+ # (V15.1 - GEM-Architect: Tuned Logic Tree - Marksman Mode)
3
 
4
  import asyncio
5
  import httpx
 
8
  import logging
9
  import pandas as pd
10
  import numpy as np
11
+ from typing import List, Dict, Any
12
 
13
  # إعدادات التسجيل
14
  logging.getLogger("httpx").setLevel(logging.WARNING)
 
25
  self.r2_service = r2_service
26
 
27
  # إعداد المنصة (KuCoin)
 
28
  self.exchange = ccxt.kucoin({
29
  'enableRateLimit': True,
30
  'timeout': 60000,
 
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'
 
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:
52
  if self.exchange:
53
  await self.exchange.load_markets()
 
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
 
 
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
 
 
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:
 
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
 
 
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
 
 
160
  return []
161
 
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
 
 
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())
 
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:
 
314
  try:
315
  ob = await self.exchange.fetch_order_book(symbol, limit)
316
  return ob
317
+ except Exception: return {}