Riy777 commited on
Commit
344f0ef
·
verified ·
1 Parent(s): d621f1d

Update backtest_engine.py

Browse files
Files changed (1) hide show
  1. backtest_engine.py +49 -58
backtest_engine.py CHANGED
@@ -1,5 +1,5 @@
1
  # ============================================================
2
- # 🧪 backtest_engine.py (V195.0 - GEM-Architect: RAM Optimization & Batching)
3
  # ============================================================
4
 
5
  import asyncio
@@ -54,11 +54,9 @@ def _revive_score_distribution(scores):
54
 
55
  def optimize_dataframe_memory(df):
56
  """⬇️ Reduces memory usage by downcasting types"""
57
- # Floats: float64 -> float32
58
  float_cols = df.select_dtypes(include=['float64']).columns
59
  df[float_cols] = df[float_cols].astype('float32')
60
 
61
- # Ints: int64 -> int16/int8
62
  int_cols = df.select_dtypes(include=['int64', 'int32']).columns
63
  for col in int_cols:
64
  c_min = df[col].min()
@@ -92,17 +90,17 @@ class HeavyDutyBacktester:
92
  # 🎛️ CONTROL PANEL - HIGH PRECISION RANGES
93
  self.GRID_RANGES = {
94
  # --- Models ---
95
- 'TITAN': np.linspace(0.30, 0.80, self.GRID_DENSITY),
96
- 'ORACLE': np.linspace(0.50, 0.80, self.GRID_DENSITY),
97
- 'SNIPER': np.linspace(0.30, 0.70, self.GRID_DENSITY),
98
- 'PATTERN': np.linspace(0.30, 0.80, self.GRID_DENSITY),
99
 
100
  # --- Governance ---
101
- 'GOV_SCORE': np.linspace(50.0, 85.0, self.GRID_DENSITY),
102
 
103
  # --- Guardians ---
104
- 'HYDRA_THRESH': np.linspace(0.60, 0.90, self.GRID_DENSITY),
105
- 'LEGACY_THRESH': np.linspace(0.85, 0.99, self.GRID_DENSITY)
106
  }
107
 
108
  self.TARGET_COINS = [
@@ -123,7 +121,7 @@ class HeavyDutyBacktester:
123
  self.force_end_date = "2024-02-01"
124
 
125
  if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
126
- print(f"🧪 [Backtest V195.0] Memory Safe Mode (Chunking & Compression).")
127
 
128
  def set_date_range(self, start_str, end_str):
129
  self.force_start_date = start_str
@@ -281,7 +279,7 @@ class HeavyDutyBacktester:
281
  is_market_dead = (h1_chop > 61.8) | ((h1_atr_pct < 0.3) & (h1_adx < 20))
282
  market_status = np.where(is_market_dead, 0, 1)
283
 
284
- # 2. COIN STATES
285
  h1_rsi = numpy_htf['1h']['RSI'][map_1h]
286
  h1_close = numpy_htf['1h']['close'][map_1h]
287
  h1_bbw = numpy_htf['1h']['bb_width'][map_1h]
@@ -293,7 +291,12 @@ class HeavyDutyBacktester:
293
 
294
  coin_state = np.zeros(len(arr_ts_1m), dtype=np.int8)
295
  is_trash_vol = (h1_rel_vol < 0.5) | (h1_atr_pct < 0.2)
296
- mask_acc = (h1_bbw < 0.15) & (h1_rsi >= 40) & (h1_rsi <= 60)
 
 
 
 
 
297
  mask_safe = (h1_adx > 25) & (h1_ema20 > h1_ema50) & (h1_ema50 > h1_ema200) & (h1_rsi > 50) & (h1_rsi < 75)
298
  mask_exp = (h1_rsi > 65) & (h1_close > h1_upper) & (h1_rel_vol > 1.5)
299
 
@@ -508,31 +511,26 @@ class HeavyDutyBacktester:
508
  if c: await self._process_data_in_memory(sym, c, ms_s, ms_e)
509
 
510
  def _worker_optimize(self, combinations_batch, scores_files, initial_capital, fees_pct, max_slots, target_state):
511
- """🚀 HYPER-SPEED JUMP LOGIC (RAM Optimized Batching)"""
512
  print(f" ⏳ [System] Loading {len(scores_files)} datasets...", flush=True)
513
 
514
- # ✅ Load & Compress Immediately
515
  data = []
516
  for f in scores_files:
517
  try:
518
  d = pd.read_pickle(f)
519
- d = optimize_dataframe_memory(d) # Ensure compressed in RAM
520
  data.append(d)
521
  except: pass
522
  if not data: return []
523
 
524
  df = pd.concat(data).sort_values('timestamp').reset_index(drop=True)
525
- del data # Free list memory
526
  gc.collect()
527
 
528
- # Pre-load arrays (Optimized types)
529
- # Convert to numpy and allow GC to reclaim DF memory if possible
530
- # (Though we keep DF for indices, extracting numpy arrays is safer)
531
  ts = df['timestamp'].values
532
  close = df['close'].values.astype(np.float32)
533
  sym = df['symbol'].values
534
 
535
- # Map symbols to int for faster lookup
536
  u_syms = np.unique(sym)
537
  sym_map = {s: i for i, s in enumerate(u_syms)}
538
  sym_id = np.array([sym_map[s] for s in sym], dtype=np.int16)
@@ -550,7 +548,6 @@ class HeavyDutyBacktester:
550
  c_state = df['coin_state'].values
551
  m_ok = df['market_ok'].values
552
 
553
- # ✅ FREE BIG DATAFRAME FROM RAM
554
  del df, sym
555
  gc.collect()
556
 
@@ -558,7 +555,6 @@ class HeavyDutyBacktester:
558
  print(f" 🚀 [System] Testing {len(combinations_batch)} configs on {N} candidates...", flush=True)
559
 
560
  res = []
561
- # ✅ Process in Batches to avoid RAM spike from result list
562
  BATCH_SIZE = 500
563
 
564
  for i in range(0, len(combinations_batch), BATCH_SIZE):
@@ -582,42 +578,38 @@ class HeavyDutyBacktester:
582
  pos = {}
583
  bal = float(initial_capital)
584
  log = []
585
- # Don't store full balance history to save RAM
586
  trade_durs = []
587
 
588
- # Jump Logic on Valid Indices is tricky because we need sequential exit checks.
589
- # So we iterate fully, but only check entry if mask is true.
590
- # Optimization: We only need to iterate indices relevant to current positions OR new entries.
591
- # Since N can be 20M+, simple iteration is slow in Python.
592
- # We stick to the robust loop for correctness but assume num_trades is low.
593
-
594
- # ⚡ FAST LOOP
595
  for idx in range(N):
596
  s = sym_id[idx]
597
  p = close[idx]
598
 
599
- # Exit Check
600
  if s in pos:
601
  entry_p, size, e_idx = pos[s]
602
- pnl_pct = (p - entry_p) / entry_p
603
-
604
- if (pnl_pct > 0.025) or (pnl_pct < -0.02):
605
- val = size * (1 + pnl_pct)
606
- fee = val * fees_pct
607
- net = val - fee - size
608
- bal += size + net
609
- del pos[s]
610
- log.append(net)
611
- trade_durs.append(idx - e_idx)
612
- continue
613
-
614
- # Entry Check
 
 
 
 
 
 
 
615
  if entry_mask[idx] and len(pos) < max_slots:
616
  if s not in pos and bal >= 5.0:
617
- # Grade
618
  sc = gov_s[idx]
619
  mult = 1.0 if sc >= 85 else (0.75 if sc >= 70 else 0.5)
620
-
621
  base = bal * 0.95 if bal < self.MIN_CAPITAL_FOR_SPLIT else bal / max_slots
622
  size = base * mult
623
 
@@ -626,33 +618,34 @@ class HeavyDutyBacktester:
626
  pos[s] = (p, size - fee, idx)
627
  bal -= size
628
 
629
- # Stats
630
  tot = len(log)
631
  if tot == 0: continue
632
 
 
633
  net_p = bal + sum([v[1] for v in pos.values()]) - initial_capital
 
 
634
  wins = [x for x in log if x > 0]
635
  losses = [x for x in log if x <= 0]
636
 
637
  wr = len(wins)/tot*100
638
- avg_w = np.mean(wins) if wins else 0
639
- avg_l = np.mean(losses) if losses else 0
640
- pf = sum(wins)/abs(sum(losses)) if losses else 99
641
 
642
- # Simple DD calc (approximation to save RAM)
643
- dd = 0.0 # Skipping detailed DD for speed/RAM
 
644
 
645
  res.append({
646
  'config': cfg, 'final_balance': bal, 'net_profit': net_p,
647
  'total_trades': tot, 'win_rate': wr, 'profit_factor': pf,
648
- 'max_drawdown': dd, 'sqn': 0, # Skip heavy calc
649
  'avg_duration_candles': np.mean(trade_durs) if trade_durs else 0,
650
  'win_count': len(wins), 'loss_count': len(losses),
651
  'avg_win_usd': avg_w, 'avg_loss_usd': avg_l,
652
  'max_win_streak': 0, 'max_loss_streak': 0
653
  })
654
 
655
- # Clean loop memory
656
  gc.collect()
657
 
658
  return res
@@ -681,7 +674,6 @@ class HeavyDutyBacktester:
681
 
682
  for state_name, state_id in states:
683
  print(f"\n🌀 Optimizing for [{state_name}]...")
684
- # Re-read files fresh for each state to ensure clean RAM
685
  results_list = self._worker_optimize(combos, files, self.INITIAL_CAPITAL, self.TRADING_FEES, self.MAX_SLOTS, state_id)
686
 
687
  if not results_list:
@@ -719,12 +711,11 @@ class HeavyDutyBacktester:
719
  print(f" ⚙️ Config: {p_str}")
720
  print("="*80)
721
 
722
- # Flush for next state
723
  del results_list
724
  gc.collect()
725
 
726
  async def run_strategic_optimization_task():
727
- print("\n🧪 [STRATEGIC BACKTEST] Memory Safe Mode...")
728
  r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
729
  try:
730
  await dm.initialize(); await proc.initialize()
 
1
  # ============================================================
2
+ # 🧪 backtest_engine.py (V200.0 - GEM-Architect: Stable & Robust)
3
  # ============================================================
4
 
5
  import asyncio
 
54
 
55
  def optimize_dataframe_memory(df):
56
  """⬇️ Reduces memory usage by downcasting types"""
 
57
  float_cols = df.select_dtypes(include=['float64']).columns
58
  df[float_cols] = df[float_cols].astype('float32')
59
 
 
60
  int_cols = df.select_dtypes(include=['int64', 'int32']).columns
61
  for col in int_cols:
62
  c_min = df[col].min()
 
90
  # 🎛️ CONTROL PANEL - HIGH PRECISION RANGES
91
  self.GRID_RANGES = {
92
  # --- Models ---
93
+ 'TITAN': np.linspace(0.30, 0.70, self.GRID_DENSITY),
94
+ 'ORACLE': np.linspace(0.50, 0.75, self.GRID_DENSITY),
95
+ 'SNIPER': np.linspace(0.30, 0.65, self.GRID_DENSITY),
96
+ 'PATTERN': np.linspace(0.30, 0.70, self.GRID_DENSITY),
97
 
98
  # --- Governance ---
99
+ 'GOV_SCORE': np.linspace(50.0, 80.0, self.GRID_DENSITY),
100
 
101
  # --- Guardians ---
102
+ 'HYDRA_THRESH': np.linspace(0.65, 0.90, self.GRID_DENSITY),
103
+ 'LEGACY_THRESH': np.linspace(0.88, 0.99, self.GRID_DENSITY)
104
  }
105
 
106
  self.TARGET_COINS = [
 
121
  self.force_end_date = "2024-02-01"
122
 
123
  if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
124
+ print(f"🧪 [Backtest V200.0] Stable Core (RAM Optimized + Math Fixes).")
125
 
126
  def set_date_range(self, start_str, end_str):
127
  self.force_start_date = start_str
 
279
  is_market_dead = (h1_chop > 61.8) | ((h1_atr_pct < 0.3) & (h1_adx < 20))
280
  market_status = np.where(is_market_dead, 0, 1)
281
 
282
+ # 2. COIN STATES (Relaxed for ACCUMULATION)
283
  h1_rsi = numpy_htf['1h']['RSI'][map_1h]
284
  h1_close = numpy_htf['1h']['close'][map_1h]
285
  h1_bbw = numpy_htf['1h']['bb_width'][map_1h]
 
291
 
292
  coin_state = np.zeros(len(arr_ts_1m), dtype=np.int8)
293
  is_trash_vol = (h1_rel_vol < 0.5) | (h1_atr_pct < 0.2)
294
+
295
+ # ✅ Relaxed Accumulation Logic
296
+ # Old: bbw < 0.15, RSI 40-60
297
+ # New: bbw < 0.20, RSI 35-65
298
+ mask_acc = (h1_bbw < 0.20) & (h1_rsi >= 35) & (h1_rsi <= 65)
299
+
300
  mask_safe = (h1_adx > 25) & (h1_ema20 > h1_ema50) & (h1_ema50 > h1_ema200) & (h1_rsi > 50) & (h1_rsi < 75)
301
  mask_exp = (h1_rsi > 65) & (h1_close > h1_upper) & (h1_rel_vol > 1.5)
302
 
 
511
  if c: await self._process_data_in_memory(sym, c, ms_s, ms_e)
512
 
513
  def _worker_optimize(self, combinations_batch, scores_files, initial_capital, fees_pct, max_slots, target_state):
514
+ """🚀 HYPER-SPEED JUMP LOGIC (RAM Optimized Batching + Math Safety)"""
515
  print(f" ⏳ [System] Loading {len(scores_files)} datasets...", flush=True)
516
 
 
517
  data = []
518
  for f in scores_files:
519
  try:
520
  d = pd.read_pickle(f)
521
+ d = optimize_dataframe_memory(d)
522
  data.append(d)
523
  except: pass
524
  if not data: return []
525
 
526
  df = pd.concat(data).sort_values('timestamp').reset_index(drop=True)
527
+ del data
528
  gc.collect()
529
 
 
 
 
530
  ts = df['timestamp'].values
531
  close = df['close'].values.astype(np.float32)
532
  sym = df['symbol'].values
533
 
 
534
  u_syms = np.unique(sym)
535
  sym_map = {s: i for i, s in enumerate(u_syms)}
536
  sym_id = np.array([sym_map[s] for s in sym], dtype=np.int16)
 
548
  c_state = df['coin_state'].values
549
  m_ok = df['market_ok'].values
550
 
 
551
  del df, sym
552
  gc.collect()
553
 
 
555
  print(f" 🚀 [System] Testing {len(combinations_batch)} configs on {N} candidates...", flush=True)
556
 
557
  res = []
 
558
  BATCH_SIZE = 500
559
 
560
  for i in range(0, len(combinations_batch), BATCH_SIZE):
 
578
  pos = {}
579
  bal = float(initial_capital)
580
  log = []
 
581
  trade_durs = []
582
 
 
 
 
 
 
 
 
583
  for idx in range(N):
584
  s = sym_id[idx]
585
  p = close[idx]
586
 
 
587
  if s in pos:
588
  entry_p, size, e_idx = pos[s]
589
+ if entry_p > 0: # Safety check
590
+ pnl_pct = (p - entry_p) / entry_p
591
+
592
+ if (pnl_pct > 0.025) or (pnl_pct < -0.02):
593
+ val = size * (1 + pnl_pct)
594
+ fee = val * fees_pct
595
+ net = val - fee - size
596
+
597
+ # ✅ Safety: Check for NaN/Inf before committing
598
+ if np.isfinite(net) and abs(net) < 10000:
599
+ bal += size + net
600
+ del pos[s]
601
+ log.append(net)
602
+ trade_durs.append(idx - e_idx)
603
+ else:
604
+ # Fallback close if corrupted
605
+ bal += size
606
+ del pos[s]
607
+ continue
608
+
609
  if entry_mask[idx] and len(pos) < max_slots:
610
  if s not in pos and bal >= 5.0:
 
611
  sc = gov_s[idx]
612
  mult = 1.0 if sc >= 85 else (0.75 if sc >= 70 else 0.5)
 
613
  base = bal * 0.95 if bal < self.MIN_CAPITAL_FOR_SPLIT else bal / max_slots
614
  size = base * mult
615
 
 
618
  pos[s] = (p, size - fee, idx)
619
  bal -= size
620
 
 
621
  tot = len(log)
622
  if tot == 0: continue
623
 
624
+ # ✅ Math Safety Checks
625
  net_p = bal + sum([v[1] for v in pos.values()]) - initial_capital
626
+ if not np.isfinite(net_p): net_p = -999.0
627
+
628
  wins = [x for x in log if x > 0]
629
  losses = [x for x in log if x <= 0]
630
 
631
  wr = len(wins)/tot*100
632
+ avg_w = np.mean(wins) if wins else 0.0
633
+ avg_l = np.mean(losses) if losses else 0.0
 
634
 
635
+ sum_w = np.sum(wins)
636
+ sum_l = abs(np.sum(losses))
637
+ pf = (sum_w / sum_l) if sum_l > 0.001 else 99.0
638
 
639
  res.append({
640
  'config': cfg, 'final_balance': bal, 'net_profit': net_p,
641
  'total_trades': tot, 'win_rate': wr, 'profit_factor': pf,
642
+ 'max_drawdown': 0.0, 'sqn': 0,
643
  'avg_duration_candles': np.mean(trade_durs) if trade_durs else 0,
644
  'win_count': len(wins), 'loss_count': len(losses),
645
  'avg_win_usd': avg_w, 'avg_loss_usd': avg_l,
646
  'max_win_streak': 0, 'max_loss_streak': 0
647
  })
648
 
 
649
  gc.collect()
650
 
651
  return res
 
674
 
675
  for state_name, state_id in states:
676
  print(f"\n🌀 Optimizing for [{state_name}]...")
 
677
  results_list = self._worker_optimize(combos, files, self.INITIAL_CAPITAL, self.TRADING_FEES, self.MAX_SLOTS, state_id)
678
 
679
  if not results_list:
 
711
  print(f" ⚙️ Config: {p_str}")
712
  print("="*80)
713
 
 
714
  del results_list
715
  gc.collect()
716
 
717
  async def run_strategic_optimization_task():
718
+ print("\n🧪 [STRATEGIC BACKTEST] Stable Core (V200.0)...")
719
  r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
720
  try:
721
  await dm.initialize(); await proc.initialize()