Riy777 commited on
Commit
92e3f55
·
verified ·
1 Parent(s): a925baf

Update backtest_engine.py

Browse files
Files changed (1) hide show
  1. backtest_engine.py +60 -23
backtest_engine.py CHANGED
@@ -1,5 +1,5 @@
1
  # ============================================================
2
- # 🧪 backtest_engine.py (V90.0 - GEM-Architect: Priority Sorting Fix)
3
  # ============================================================
4
 
5
  import asyncio
@@ -36,7 +36,7 @@ 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', 'NEAR/USDT','SHIB/USDT'
42
  ]
@@ -45,7 +45,7 @@ class HeavyDutyBacktester:
45
  self.force_end_date = None
46
 
47
  if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
48
- print(f"🧪 [Backtest V90.0] Sniper Priority Logic (Rank-Based Allocation).")
49
 
50
  def set_date_range(self, start_str, end_str):
51
  self.force_start_date = start_str
@@ -124,7 +124,8 @@ class HeavyDutyBacktester:
124
  frames = {}
125
  agg_dict = {'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum'}
126
  frames['1m'] = df_1m.copy()
127
- frames['1m']['timestamp'] = frames['1m'].index.astype(np.int64) // 10**6
 
128
 
129
  for tf_str, tf_code in [('5m', '5T'), ('15m', '15T'), ('1h', '1h'), ('4h', '4h'), ('1d', '1D')]:
130
  frames[tf_str] = df_1m.resample(tf_code).agg(agg_dict).dropna()
@@ -174,8 +175,11 @@ class HeavyDutyBacktester:
174
  if proc_res: real_titan = proc_res.get('titan_score', 0.5)
175
  except: pass
176
 
 
 
 
177
  ai_results.append({
178
- 'timestamp': int(t_idx.timestamp() * 1000),
179
  'symbol': sym,
180
  'close': current_price,
181
  'real_titan': real_titan,
@@ -212,26 +216,39 @@ class HeavyDutyBacktester:
212
  if candles:
213
  await self._process_data_in_memory(sym, candles, start_time_ms, end_time_ms)
214
  else:
215
- print(f" ❌ Failed/Empty data for {sym}. Continuing...", flush=True)
216
  except Exception as e:
217
- print(f" ❌ SKIP: Error processing {sym}: {e}", flush=True)
218
  continue
219
  gc.collect()
220
 
221
  # ==============================================================
222
- # PHASE 2: Portfolio Digital Twin Engine (✅ FIX: Priority Sorting)
223
  # ==============================================================
224
  @staticmethod
225
  def _worker_optimize(combinations_batch, scores_files, initial_capital, fees_pct, max_slots):
226
  results = []
227
  all_data = []
 
 
228
  for fp in scores_files:
229
  try:
230
  df = pd.read_pickle(fp)
231
  if not df.empty: all_data.append(df)
232
  except: pass
233
  if not all_data: return []
234
- global_df = pd.concat(all_data).drop_duplicates(subset=['timestamp']).sort_values('timestamp')
 
 
 
 
 
 
 
 
 
 
 
235
  grouped_by_time = global_df.groupby('timestamp')
236
 
237
  for config in combinations_batch:
@@ -241,36 +258,51 @@ class HeavyDutyBacktester:
241
 
242
  for ts, group in grouped_by_time:
243
  active_symbols = list(wallet["positions"].keys())
 
 
244
  current_prices = {row['symbol']: row['close'] for _, row in group.iterrows()}
245
 
246
  # --- 1. Exit Logic ---
 
 
 
 
247
  for sym in active_symbols:
248
  if sym in current_prices:
249
  curr_p = current_prices[sym]
250
  pos = wallet["positions"][sym]
251
  entry_p = pos['entry_price']
252
  pct_change = (curr_p - entry_p) / entry_p
 
 
253
  if pct_change >= 0.03 or pct_change <= -0.02:
254
  gross_pnl = pos['size_usd'] * pct_change
255
  fees = pos['size_usd'] * fees_pct * 2
256
- net_pnl = gross_pnl - fees
 
257
  wallet["allocated"] -= pos['size_usd']
258
- wallet["balance"] += net_pnl
 
 
 
 
 
 
259
  del wallet["positions"][sym]
260
  wallet["trades_history"].append({'pnl': net_pnl})
261
 
 
262
  current_total_equity = wallet["balance"] + wallet["allocated"]
263
  if current_total_equity > peak_balance: peak_balance = current_total_equity
264
  dd = (peak_balance - current_total_equity) / peak_balance
265
  if dd > max_drawdown: max_drawdown = dd
266
 
267
- # --- 2. Entry Logic ( REFACTORED FOR SNIPER PRIORITY) ---
268
  if len(wallet["positions"]) < max_slots:
269
  free_capital = wallet["balance"]
270
  slots_left = max_slots - len(wallet["positions"])
271
 
272
  if slots_left > 0 and free_capital > 2.0:
273
- # 1. تجميع كل الفرص المتاحة في هذه الدقيقة
274
  candidates = []
275
  for _, row in group.iterrows():
276
  sym = row['symbol']
@@ -289,24 +321,24 @@ class HeavyDutyBacktester:
289
  score = ((real_titan * w_titan) + (norm_struct * w_struct)) / (w_titan + w_struct)
290
 
291
  if score >= entry_thresh:
292
- # إضافة السكور للقائمة للترتيب لاحقاً
293
  candidates.append({
294
  'symbol': sym,
295
  'score': score,
296
  'price': row['close']
297
  })
298
 
299
- # 2. ترتيب الفرص تنازلياً حسب القوة (الأقوى أولاً)
300
- # هذا يضمن أن العملة الأقوى (مثل SOL) تأخذ المكان قبل العملة الأضعف
301
  candidates.sort(key=lambda x: x['score'], reverse=True)
302
 
303
- # 3. تنفيذ أفضل الفرص فقط حسب الخانات المتاحة
304
  for cand in candidates[:slots_left]:
 
305
  position_size = wallet["balance"] / max_slots
306
- # تعديل لحالات الرصيد المنخفض
307
- current_slots_left = max_slots - len(wallet["positions"])
308
- if wallet["balance"] < 20.0 and current_slots_left > 0:
309
- position_size = wallet["balance"] / current_slots_left
 
 
310
 
311
  position_size = min(position_size, wallet["balance"])
312
 
@@ -319,7 +351,12 @@ class HeavyDutyBacktester:
319
 
320
  trades = wallet["trades_history"]
321
  if trades:
322
- net_profit = wallet["balance"] - initial_capital + wallet["allocated"]
 
 
 
 
 
323
  pnls = [t['pnl'] for t in trades]
324
  win_count = len([p for p in pnls if p > 0]); loss_count = len([p for p in pnls if p <= 0])
325
  win_rate = (win_count / len(trades)) * 100
@@ -334,7 +371,7 @@ class HeavyDutyBacktester:
334
  current_loss_streak += 1; current_win_streak = 0
335
  if current_loss_streak > max_loss_streak: max_loss_streak = current_loss_streak
336
  results.append({
337
- 'config': config, 'final_balance': wallet["balance"] + wallet["allocated"],
338
  'net_profit': net_profit, 'total_trades': len(trades),
339
  'win_count': win_count, 'loss_count': loss_count, 'win_rate': win_rate,
340
  'max_single_win': max_single_win, 'max_single_loss': max_single_loss,
 
1
  # ============================================================
2
+ # 🧪 backtest_engine.py (V91.0 - GEM-Architect: Sync & Math Fix)
3
  # ============================================================
4
 
5
  import asyncio
 
36
  self.TRADING_FEES = 0.001
37
  self.MAX_SLOTS = 4
38
 
39
+ # القائمة الكاملة
40
  self.TARGET_COINS = [
41
  'SOL/USDT', 'XRP/USDT', 'DOGE/USDT', 'NEAR/USDT','SHIB/USDT'
42
  ]
 
45
  self.force_end_date = None
46
 
47
  if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
48
+ print(f"🧪 [Backtest V91.0] Synchronized Engine (Math Bug Fixed).")
49
 
50
  def set_date_range(self, start_str, end_str):
51
  self.force_start_date = start_str
 
124
  frames = {}
125
  agg_dict = {'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum'}
126
  frames['1m'] = df_1m.copy()
127
+ # تقريب التوقيت للدقيقة لضمان التزامن لاحقاً
128
+ frames['1m']['timestamp'] = frames['1m'].index.floor('1min').astype(np.int64) // 10**6
129
 
130
  for tf_str, tf_code in [('5m', '5T'), ('15m', '15T'), ('1h', '1h'), ('4h', '4h'), ('1d', '1D')]:
131
  frames[tf_str] = df_1m.resample(tf_code).agg(agg_dict).dropna()
 
175
  if proc_res: real_titan = proc_res.get('titan_score', 0.5)
176
  except: pass
177
 
178
+ # ✅ حفظ timestamp بوحدة الدقيقة الموحدة لضمان التزامن
179
+ ts_aligned = int(t_idx.timestamp() // 60) * 60 * 1000
180
+
181
  ai_results.append({
182
+ 'timestamp': ts_aligned,
183
  'symbol': sym,
184
  'close': current_price,
185
  'real_titan': real_titan,
 
216
  if candles:
217
  await self._process_data_in_memory(sym, candles, start_time_ms, end_time_ms)
218
  else:
219
+ print(f" ❌ Failed/Empty data for {sym}.", flush=True)
220
  except Exception as e:
221
+ print(f" ❌ SKIP {sym}: {e}", flush=True)
222
  continue
223
  gc.collect()
224
 
225
  # ==============================================================
226
+ # PHASE 2: Portfolio Digital Twin Engine (✅ FIX: MATH & SYNC)
227
  # ==============================================================
228
  @staticmethod
229
  def _worker_optimize(combinations_batch, scores_files, initial_capital, fees_pct, max_slots):
230
  results = []
231
  all_data = []
232
+
233
+ # 1. Load All Data
234
  for fp in scores_files:
235
  try:
236
  df = pd.read_pickle(fp)
237
  if not df.empty: all_data.append(df)
238
  except: pass
239
  if not all_data: return []
240
+
241
+ # 2. Merge and Align
242
+ global_df = pd.concat(all_data)
243
+
244
+ # ✅ FIX: Pivot Data for Perfect Alignment (Time x Symbol)
245
+ # نحتاج هيكلة تسمح لنا بمعرفة سعر كل العملات في كل دقيقة حتى لو لم يكن هناك إشارة
246
+ # ملاحظة: ملفات scores تحتوي فقط على الإشارات. لإدارة الخروج نحتاج أسعار مستمرة.
247
+ # الحل الوسط: نستخدم أسعار الإشارات المتاحة، ونفترض ثبات السعر (Forward Fill) عند الفجوات البسيطة
248
+ # أو نعتمد على أن الإشارة تتكرر.
249
+ # الأفضل: التجميع الزمني الموحد.
250
+
251
+ global_df.sort_values('timestamp', inplace=True)
252
  grouped_by_time = global_df.groupby('timestamp')
253
 
254
  for config in combinations_batch:
 
258
 
259
  for ts, group in grouped_by_time:
260
  active_symbols = list(wallet["positions"].keys())
261
+
262
+ # إنشاء قاموس أسعار لهذه اللحظة
263
  current_prices = {row['symbol']: row['close'] for _, row in group.iterrows()}
264
 
265
  # --- 1. Exit Logic ---
266
+ # ✅ FIX: Handle missing prices (Partial Sync)
267
+ # إذا العملة المفتوحة غير موجودة في بيانات هذه الدقيقة (لأنها لم تعط إشارة)،
268
+ # لا يمكننا فحص الخروج. ننتظر الدقيقة التالية التي تظهر فيها.
269
+
270
  for sym in active_symbols:
271
  if sym in current_prices:
272
  curr_p = current_prices[sym]
273
  pos = wallet["positions"][sym]
274
  entry_p = pos['entry_price']
275
  pct_change = (curr_p - entry_p) / entry_p
276
+
277
+ # شروط الخروج
278
  if pct_change >= 0.03 or pct_change <= -0.02:
279
  gross_pnl = pos['size_usd'] * pct_change
280
  fees = pos['size_usd'] * fees_pct * 2
281
+ net_pnl = gross_pnl - fees # هذا هو الربح/الخسارة الصافي
282
+
283
  wallet["allocated"] -= pos['size_usd']
284
+
285
+ # 🔥🔥 FATAL MATH BUG FIXED HERE 🔥🔥
286
+ # القديم: wallet["balance"] += net_pnl (كارثة!)
287
+ # الجديد: نرجع رأس المال + الربح
288
+ return_amount = pos['size_usd'] + net_pnl
289
+ wallet["balance"] += return_amount
290
+
291
  del wallet["positions"][sym]
292
  wallet["trades_history"].append({'pnl': net_pnl})
293
 
294
+ # --- Update Stats ---
295
  current_total_equity = wallet["balance"] + wallet["allocated"]
296
  if current_total_equity > peak_balance: peak_balance = current_total_equity
297
  dd = (peak_balance - current_total_equity) / peak_balance
298
  if dd > max_drawdown: max_drawdown = dd
299
 
300
+ # --- 2. Entry Logic (Sniper Priority) ---
301
  if len(wallet["positions"]) < max_slots:
302
  free_capital = wallet["balance"]
303
  slots_left = max_slots - len(wallet["positions"])
304
 
305
  if slots_left > 0 and free_capital > 2.0:
 
306
  candidates = []
307
  for _, row in group.iterrows():
308
  sym = row['symbol']
 
321
  score = ((real_titan * w_titan) + (norm_struct * w_struct)) / (w_titan + w_struct)
322
 
323
  if score >= entry_thresh:
 
324
  candidates.append({
325
  'symbol': sym,
326
  'score': score,
327
  'price': row['close']
328
  })
329
 
330
+ # ترتيب حسب القوة (Sniper Mode)
 
331
  candidates.sort(key=lambda x: x['score'], reverse=True)
332
 
 
333
  for cand in candidates[:slots_left]:
334
+ # إدارة رأس المال
335
  position_size = wallet["balance"] / max_slots
336
+ curr_slots_open = len(wallet["positions"])
337
+ curr_slots_left = max_slots - curr_slots_open
338
+
339
+ # إذا الرصيد قليل، نوزع المتبقي على الخانات المتبقية بالتساوي
340
+ if wallet["balance"] < 20.0 and curr_slots_left > 0:
341
+ position_size = wallet["balance"] / curr_slots_left
342
 
343
  position_size = min(position_size, wallet["balance"])
344
 
 
351
 
352
  trades = wallet["trades_history"]
353
  if trades:
354
+ # حساب الرصيد النهائي الصحيح
355
+ # ملاحظة: allocated هنا يجب أن يكون صفراً إذا أغلقت كل الصفقات
356
+ # أو يمثل قيمة الدخول للصفقات المفتوحة
357
+ final_equity = wallet["balance"] + wallet["allocated"]
358
+ net_profit = final_equity - initial_capital
359
+
360
  pnls = [t['pnl'] for t in trades]
361
  win_count = len([p for p in pnls if p > 0]); loss_count = len([p for p in pnls if p <= 0])
362
  win_rate = (win_count / len(trades)) * 100
 
371
  current_loss_streak += 1; current_win_streak = 0
372
  if current_loss_streak > max_loss_streak: max_loss_streak = current_loss_streak
373
  results.append({
374
+ 'config': config, 'final_balance': final_equity,
375
  'net_profit': net_profit, 'total_trades': len(trades),
376
  'win_count': win_count, 'loss_count': loss_count, 'win_rate': win_rate,
377
  'max_single_win': max_single_win, 'max_single_loss': max_single_loss,