Riy777 commited on
Commit
dbb490b
·
verified ·
1 Parent(s): e490e3d

Update backtest_engine.py

Browse files
Files changed (1) hide show
  1. backtest_engine.py +45 -49
backtest_engine.py CHANGED
@@ -1,5 +1,5 @@
1
  # ============================================================
2
- # 🧪 backtest_engine.py (V89.0 - GEM-Architect: Mass-Scale Edition)
3
  # ============================================================
4
 
5
  import asyncio
@@ -36,24 +36,15 @@ class HeavyDutyBacktester:
36
  self.TRADING_FEES = 0.001
37
  self.MAX_SLOTS = 4
38
 
39
- # القائمة الكاملة (50 عملة)
40
  self.TARGET_COINS = [
41
- 'SOL/USDT', 'XRP/USDT', 'DOGE/USDT', 'ADA/USDT', 'AVAX/USDT', 'LINK/USDT',
42
- 'TON/USDT', 'INJ/USDT', 'APT/USDT', 'OP/USDT', 'ARB/USDT', 'SUI/USDT',
43
- 'SEI/USDT', 'TIA/USDT', 'MATIC/USDT', 'NEAR/USDT', 'RUNE/USDT', 'PYTH/USDT',
44
- 'WIF/USDT', 'PEPE/USDT', 'SHIB/USDT', 'TRX/USDT', 'DOT/USDT', 'UNI/USDT',
45
- 'ONDO/USDT', 'ENA/USDT', 'HBAR/USDT', 'XLM/USDT', 'TAO/USDT', 'ZK/USDT',
46
- 'ZRO/USDT', 'KCS/USDT', 'ICP/USDT', 'SAND/USDT', 'AXS/USDT', 'APE/USDT',
47
- 'GMT/USDT', 'CHZ/USDT', 'CFX/USDT', 'LDO/USDT', 'FET/USDT', 'JTO/USDT',
48
- 'STRK/USDT', 'BLUR/USDT', 'ALT/USDT', 'JUP/USDT', 'PENDLE/USDT', 'ETHFI/USDT',
49
- 'MEME/USDT', 'ATOM/USDT'
50
- ]
51
 
52
  self.force_start_date = None
53
  self.force_end_date = None
54
 
55
  if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
56
- print(f"🧪 [Backtest V89.0] Mass-Scale Edition (50+ Coins | Fault Tolerant).")
57
 
58
  def set_date_range(self, start_str, end_str):
59
  self.force_start_date = start_str
@@ -64,33 +55,29 @@ class HeavyDutyBacktester:
64
  return df[['timestamp', 'open', 'high', 'low', 'close', 'volume']].values.tolist()
65
 
66
  # ==============================================================
67
- # ⚡ FAST DATA DOWNLOADER (Silent Burst)
68
  # ==============================================================
69
  async def _fetch_all_data_fast(self, sym, start_ms, end_ms):
70
  print(f" ⚡ [Network] Downloading {sym}...", flush=True)
71
-
72
  limit = 1000
73
  duration_per_batch = limit * 60 * 1000
74
-
75
  tasks = []
76
  current = start_ms
77
  while current < end_ms:
78
  tasks.append(current)
79
  current += duration_per_batch
80
-
81
  all_candles = []
82
- sem = asyncio.Semaphore(10) # 10 اتصالات متزامنة
83
 
84
  async def _fetch_batch(timestamp):
85
  async with sem:
86
- for _ in range(3): # 3 محاولات
87
  try:
88
  return await self.dm.exchange.fetch_ohlcv(sym, '1m', since=timestamp, limit=limit)
89
  except Exception:
90
  await asyncio.sleep(1)
91
  return []
92
 
93
- # تقسيم المهام وتشغيلها
94
  chunk_size = 20
95
  for i in range(0, len(tasks), chunk_size):
96
  chunk_tasks = tasks[i:i + chunk_size]
@@ -98,12 +85,8 @@ class HeavyDutyBacktester:
98
  results = await asyncio.gather(*futures)
99
  for res in results:
100
  if res: all_candles.extend(res)
101
-
102
- # 🚫 تم حذف طباعة النسبة المئوية حسب الطلب
103
 
104
  if not all_candles: return None
105
-
106
- # تنظيف البيانات
107
  filtered = [c for c in all_candles if c[0] >= start_ms and c[0] <= end_ms]
108
  seen = set()
109
  unique_candles = []
@@ -111,13 +94,12 @@ class HeavyDutyBacktester:
111
  if c[0] not in seen:
112
  unique_candles.append(c)
113
  seen.add(c[0])
114
-
115
  unique_candles.sort(key=lambda x: x[0])
116
  print(f" ✅ Downloaded {len(unique_candles)} candles for {sym}.", flush=True)
117
  return unique_candles
118
 
119
  # ==============================================================
120
- # 🧠 CPU PROCESSING (In-Memory & Silent)
121
  # ==============================================================
122
  async def _process_data_in_memory(self, sym, candles, start_ms, end_ms):
123
  safe_sym = sym.replace('/', '_')
@@ -148,13 +130,9 @@ class HeavyDutyBacktester:
148
  frames[tf_str]['timestamp'] = frames[tf_str].index.astype(np.int64) // 10**6
149
 
150
  ai_results = []
151
-
152
- # نبدأ التحليل بعد فترة التحمية
153
  start_analysis_dt = df_1m.index[0] + pd.Timedelta(minutes=500)
154
  valid_indices = frames['5m'].loc[start_analysis_dt:].index
155
 
156
- # 🚫 تم حذف حلقة طباعة التقدم %
157
-
158
  for t_idx in valid_indices:
159
  ohlcv_data = {}
160
  try:
@@ -215,7 +193,7 @@ class HeavyDutyBacktester:
215
  gc.collect()
216
 
217
  # ==============================================================
218
- # PHASE 1: Main Loop (Fault Tolerant)
219
  # ==============================================================
220
  async def generate_truth_data(self):
221
  if self.force_start_date and self.force_end_date:
@@ -228,28 +206,19 @@ class HeavyDutyBacktester:
228
  return
229
 
230
  for sym in self.TARGET_COINS:
231
- # 🛡️ حماية شاملة: لن يتوقف السكربت أبداً بسبب عملة واحدة
232
  try:
233
- # 1. Download Phase
234
  candles = await self._fetch_all_data_fast(sym, start_time_ms, end_time_ms)
235
-
236
  if candles:
237
- # 2. Processing Phase
238
  await self._process_data_in_memory(sym, candles, start_time_ms, end_time_ms)
239
  else:
240
  print(f" ❌ Failed/Empty data for {sym}. Continuing...", flush=True)
241
-
242
  except Exception as e:
243
- # طباعة الخطأ والمتابعة فوراً
244
  print(f" ❌ SKIP: Error processing {sym}: {e}", flush=True)
245
- # traceback.print_exc() # يمكنك تفعيلها إذا أردت التفاصيل
246
  continue
247
-
248
- # تنظيف إضافي بعد كل عملة
249
  gc.collect()
250
 
251
  # ==============================================================
252
- # PHASE 2: Portfolio Digital Twin Engine (Full Stats Restored)
253
  # ==============================================================
254
  @staticmethod
255
  def _worker_optimize(combinations_batch, scores_files, initial_capital, fees_pct, max_slots):
@@ -272,7 +241,8 @@ class HeavyDutyBacktester:
272
  for ts, group in grouped_by_time:
273
  active_symbols = list(wallet["positions"].keys())
274
  current_prices = {row['symbol']: row['close'] for _, row in group.iterrows()}
275
- # Exits
 
276
  for sym in active_symbols:
277
  if sym in current_prices:
278
  curr_p = current_prices[sym]
@@ -287,36 +257,63 @@ class HeavyDutyBacktester:
287
  wallet["balance"] += net_pnl
288
  del wallet["positions"][sym]
289
  wallet["trades_history"].append({'pnl': net_pnl})
290
- # Entries
291
  current_total_equity = wallet["balance"] + wallet["allocated"]
292
  if current_total_equity > peak_balance: peak_balance = current_total_equity
293
  dd = (peak_balance - current_total_equity) / peak_balance
294
  if dd > max_drawdown: max_drawdown = dd
295
 
 
296
  if len(wallet["positions"]) < max_slots:
297
  free_capital = wallet["balance"]
298
  slots_left = max_slots - len(wallet["positions"])
 
299
  if slots_left > 0 and free_capital > 2.0:
300
- position_size = wallet["balance"] / max_slots
301
- if wallet["balance"] < 20.0: position_size = free_capital / slots_left
302
- position_size = min(position_size, free_capital)
303
  for _, row in group.iterrows():
304
  sym = row['symbol']
305
  if sym in wallet["positions"]: continue
 
306
  sig_type = row['signal_type']
307
  l1_raw_score = row['l1_score']
308
  real_titan = row['real_titan']
 
309
  norm_struct = 0.0
310
  if sig_type == 'BREAKOUT': norm_struct = min(1.0, l1_raw_score / 3.0)
311
  elif sig_type == 'REVERSAL': norm_struct = l1_raw_score / 100.0
 
312
  score = 0.0
313
  if (w_titan + w_struct) > 0:
314
  score = ((real_titan * w_titan) + (norm_struct * w_struct)) / (w_titan + w_struct)
 
315
  if score >= entry_thresh:
316
- wallet["positions"][sym] = {'entry_price': row['close'], 'size_usd': position_size}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
  wallet["allocated"] += position_size
318
  wallet["balance"] -= position_size
319
- if len(wallet["positions"]) >= max_slots: break
320
  if wallet["balance"] < 1.0 and len(wallet["positions"]) == 0: break
321
 
322
  trades = wallet["trades_history"]
@@ -390,7 +387,6 @@ class HeavyDutyBacktester:
390
  if not final_results: return None, None
391
  best = sorted(final_results, key=lambda x: x['final_balance'], reverse=True)[0]
392
 
393
- # ✅ الطباعة الكاملة للإحصائيات (كما في الملف الأصلي)
394
  print("\n" + "="*60)
395
  print(f"🏆 CHAMPION REPORT [{target_regime}]:")
396
  print(f" 📅 Period: {self.force_start_date} -> {self.force_end_date}")
 
1
  # ============================================================
2
+ # 🧪 backtest_engine.py (V90.0 - GEM-Architect: Priority Sorting Fix)
3
  # ============================================================
4
 
5
  import asyncio
 
36
  self.TRADING_FEES = 0.001
37
  self.MAX_SLOTS = 4
38
 
39
+ # القائمة الكاملة (50 عملة)
40
  self.TARGET_COINS = [
41
+ 'SOL/USDT', 'XRP/USDT', 'DOGE/USDT, 'NEAR/USDT','SHIB/USDT']
 
 
 
 
 
 
 
 
 
42
 
43
  self.force_start_date = None
44
  self.force_end_date = None
45
 
46
  if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
47
+ print(f"🧪 [Backtest V90.0] Sniper Priority Logic (Rank-Based Allocation).")
48
 
49
  def set_date_range(self, start_str, end_str):
50
  self.force_start_date = start_str
 
55
  return df[['timestamp', 'open', 'high', 'low', 'close', 'volume']].values.tolist()
56
 
57
  # ==============================================================
58
+ # ⚡ FAST DATA DOWNLOADER
59
  # ==============================================================
60
  async def _fetch_all_data_fast(self, sym, start_ms, end_ms):
61
  print(f" ⚡ [Network] Downloading {sym}...", flush=True)
 
62
  limit = 1000
63
  duration_per_batch = limit * 60 * 1000
 
64
  tasks = []
65
  current = start_ms
66
  while current < end_ms:
67
  tasks.append(current)
68
  current += duration_per_batch
 
69
  all_candles = []
70
+ sem = asyncio.Semaphore(10)
71
 
72
  async def _fetch_batch(timestamp):
73
  async with sem:
74
+ for _ in range(3):
75
  try:
76
  return await self.dm.exchange.fetch_ohlcv(sym, '1m', since=timestamp, limit=limit)
77
  except Exception:
78
  await asyncio.sleep(1)
79
  return []
80
 
 
81
  chunk_size = 20
82
  for i in range(0, len(tasks), chunk_size):
83
  chunk_tasks = tasks[i:i + chunk_size]
 
85
  results = await asyncio.gather(*futures)
86
  for res in results:
87
  if res: all_candles.extend(res)
 
 
88
 
89
  if not all_candles: return None
 
 
90
  filtered = [c for c in all_candles if c[0] >= start_ms and c[0] <= end_ms]
91
  seen = set()
92
  unique_candles = []
 
94
  if c[0] not in seen:
95
  unique_candles.append(c)
96
  seen.add(c[0])
 
97
  unique_candles.sort(key=lambda x: x[0])
98
  print(f" ✅ Downloaded {len(unique_candles)} candles for {sym}.", flush=True)
99
  return unique_candles
100
 
101
  # ==============================================================
102
+ # 🧠 CPU PROCESSING
103
  # ==============================================================
104
  async def _process_data_in_memory(self, sym, candles, start_ms, end_ms):
105
  safe_sym = sym.replace('/', '_')
 
130
  frames[tf_str]['timestamp'] = frames[tf_str].index.astype(np.int64) // 10**6
131
 
132
  ai_results = []
 
 
133
  start_analysis_dt = df_1m.index[0] + pd.Timedelta(minutes=500)
134
  valid_indices = frames['5m'].loc[start_analysis_dt:].index
135
 
 
 
136
  for t_idx in valid_indices:
137
  ohlcv_data = {}
138
  try:
 
193
  gc.collect()
194
 
195
  # ==============================================================
196
+ # PHASE 1: Main Loop
197
  # ==============================================================
198
  async def generate_truth_data(self):
199
  if self.force_start_date and self.force_end_date:
 
206
  return
207
 
208
  for sym in self.TARGET_COINS:
 
209
  try:
 
210
  candles = await self._fetch_all_data_fast(sym, start_time_ms, end_time_ms)
 
211
  if candles:
 
212
  await self._process_data_in_memory(sym, candles, start_time_ms, end_time_ms)
213
  else:
214
  print(f" ❌ Failed/Empty data for {sym}. Continuing...", flush=True)
 
215
  except Exception as e:
 
216
  print(f" ❌ SKIP: Error processing {sym}: {e}", flush=True)
 
217
  continue
 
 
218
  gc.collect()
219
 
220
  # ==============================================================
221
+ # PHASE 2: Portfolio Digital Twin Engine ( FIX: Priority Sorting)
222
  # ==============================================================
223
  @staticmethod
224
  def _worker_optimize(combinations_batch, scores_files, initial_capital, fees_pct, max_slots):
 
241
  for ts, group in grouped_by_time:
242
  active_symbols = list(wallet["positions"].keys())
243
  current_prices = {row['symbol']: row['close'] for _, row in group.iterrows()}
244
+
245
+ # --- 1. Exit Logic ---
246
  for sym in active_symbols:
247
  if sym in current_prices:
248
  curr_p = current_prices[sym]
 
257
  wallet["balance"] += net_pnl
258
  del wallet["positions"][sym]
259
  wallet["trades_history"].append({'pnl': net_pnl})
260
+
261
  current_total_equity = wallet["balance"] + wallet["allocated"]
262
  if current_total_equity > peak_balance: peak_balance = current_total_equity
263
  dd = (peak_balance - current_total_equity) / peak_balance
264
  if dd > max_drawdown: max_drawdown = dd
265
 
266
+ # --- 2. Entry Logic (✅ REFACTORED FOR SNIPER PRIORITY) ---
267
  if len(wallet["positions"]) < max_slots:
268
  free_capital = wallet["balance"]
269
  slots_left = max_slots - len(wallet["positions"])
270
+
271
  if slots_left > 0 and free_capital > 2.0:
272
+ # 1. تجميع كل الفرص المتاحة في هذه الدقيقة
273
+ candidates = []
 
274
  for _, row in group.iterrows():
275
  sym = row['symbol']
276
  if sym in wallet["positions"]: continue
277
+
278
  sig_type = row['signal_type']
279
  l1_raw_score = row['l1_score']
280
  real_titan = row['real_titan']
281
+
282
  norm_struct = 0.0
283
  if sig_type == 'BREAKOUT': norm_struct = min(1.0, l1_raw_score / 3.0)
284
  elif sig_type == 'REVERSAL': norm_struct = l1_raw_score / 100.0
285
+
286
  score = 0.0
287
  if (w_titan + w_struct) > 0:
288
  score = ((real_titan * w_titan) + (norm_struct * w_struct)) / (w_titan + w_struct)
289
+
290
  if score >= entry_thresh:
291
+ # إضافة السكور للقائمة للترتيب لاحقاً
292
+ candidates.append({
293
+ 'symbol': sym,
294
+ 'score': score,
295
+ 'price': row['close']
296
+ })
297
+
298
+ # 2. ترتيب الفرص تنازلياً حسب القوة (الأقوى أولاً)
299
+ # هذا يضمن أن العملة الأقوى (مثل SOL) تأخذ المكان قبل العملة الأضعف
300
+ candidates.sort(key=lambda x: x['score'], reverse=True)
301
+
302
+ # 3. تنفيذ أفضل الفرص فقط حسب الخانات المتاحة
303
+ for cand in candidates[:slots_left]:
304
+ position_size = wallet["balance"] / max_slots
305
+ # تعديل لحالات الرصيد المنخفض
306
+ current_slots_left = max_slots - len(wallet["positions"])
307
+ if wallet["balance"] < 20.0 and current_slots_left > 0:
308
+ position_size = wallet["balance"] / current_slots_left
309
+
310
+ position_size = min(position_size, wallet["balance"])
311
+
312
+ if position_size > 2.0:
313
+ wallet["positions"][cand['symbol']] = {'entry_price': cand['price'], 'size_usd': position_size}
314
  wallet["allocated"] += position_size
315
  wallet["balance"] -= position_size
316
+
317
  if wallet["balance"] < 1.0 and len(wallet["positions"]) == 0: break
318
 
319
  trades = wallet["trades_history"]
 
387
  if not final_results: return None, None
388
  best = sorted(final_results, key=lambda x: x['final_balance'], reverse=True)[0]
389
 
 
390
  print("\n" + "="*60)
391
  print(f"🏆 CHAMPION REPORT [{target_regime}]:")
392
  print(f" 📅 Period: {self.force_start_date} -> {self.force_end_date}")