Riy777 commited on
Commit
c383866
·
verified ·
1 Parent(s): 445dda3

Update backtest_engine.py

Browse files
Files changed (1) hide show
  1. backtest_engine.py +208 -188
backtest_engine.py CHANGED
@@ -1,5 +1,5 @@
1
  # ============================================================
2
- # 🧪 backtest_engine.py (V111.0 - GEM-Architect: Full Regime Loop Restored)
3
  # ============================================================
4
 
5
  import asyncio
@@ -55,7 +55,7 @@ class HeavyDutyBacktester:
55
  self.force_end_date = None
56
 
57
  if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
58
- print(f"🧪 [Backtest V111.0] Full Stack + Multi-Regime Strategic Loop.")
59
 
60
  def set_date_range(self, start_str, end_str):
61
  self.force_start_date = start_str
@@ -104,23 +104,21 @@ class HeavyDutyBacktester:
104
  return unique_candles
105
 
106
  # ==============================================================
107
- # 🏎️ VECTORIZED INDICATORS (ALL LAYERS)
108
  # ==============================================================
109
  def _calculate_indicators_vectorized(self, df, timeframe='1m'):
110
- # 1. Basic Setup
111
  df['close'] = df['close'].astype(float)
112
  df['high'] = df['high'].astype(float)
113
  df['low'] = df['low'].astype(float)
114
  df['volume'] = df['volume'].astype(float)
115
  df['open'] = df['open'].astype(float)
116
 
117
- # 2. Standard Indicators
118
  df['rsi'] = ta.rsi(df['close'], length=14)
119
  df['ema20'] = ta.ema(df['close'], length=20)
120
  df['ema50'] = ta.ema(df['close'], length=50)
121
  df['atr'] = ta.atr(df['high'], df['low'], df['close'], length=14)
122
 
123
- # 3. Hydra
124
  if timeframe == '1m':
125
  sma20 = df['close'].rolling(20).mean()
126
  std20 = df['close'].rolling(20).std()
@@ -128,47 +126,40 @@ class HeavyDutyBacktester:
128
  df['vol_ma50'] = df['volume'].rolling(50).mean()
129
  df['rel_vol'] = df['volume'] / (df['vol_ma50'] + 1e-9)
130
 
131
- # 4. Oracle
132
  df['slope'] = ta.slope(df['close'], length=7)
133
  vol_mean = df['volume'].rolling(20).mean()
134
  vol_std = df['volume'].rolling(20).std()
135
  df['vol_z'] = (df['volume'] - vol_mean) / (vol_std + 1e-9)
136
  df['atr_pct'] = df['atr'] / df['close']
137
 
138
- # 5. Sniper (1m Only)
139
  if timeframe == '1m':
140
  df['ret'] = df['close'].pct_change()
141
  df['dollar_vol'] = df['close'] * df['volume']
142
  df['amihud'] = (df['ret'].abs() / df['dollar_vol'].replace(0, np.nan)).fillna(0)
143
-
144
  dp = df['close'].diff()
145
  roll_cov = dp.rolling(64).cov(dp.shift(1))
146
  df['roll_spread'] = (2 * np.sqrt(np.maximum(0, -roll_cov))).fillna(0)
147
-
148
  sign = np.sign(df['close'].diff()).fillna(0)
149
  df['signed_vol'] = sign * df['volume']
150
  df['ofi'] = df['signed_vol'].rolling(30).sum().fillna(0)
151
-
152
  buy_vol = (sign > 0) * df['volume']
153
  sell_vol = (sign < 0) * df['volume']
154
  imb = (buy_vol.rolling(60).sum() - sell_vol.rolling(60).sum()).abs()
155
  tot = df['volume'].rolling(60).sum()
156
  df['vpin'] = (imb / tot.replace(0, np.nan)).fillna(0)
157
-
158
  vwap = (df['close'] * df['volume']).rolling(20).sum() / df['volume'].rolling(20).sum()
159
  df['vwap_dev'] = (df['close'] - vwap).fillna(0)
160
-
161
  df['rv_gk'] = (np.log(df['high'] / df['low'])**2) / 2 - (2 * np.log(2) - 1) * (np.log(df['close'] / df['open'])**2)
162
-
163
  df['return_1m'] = df['ret']
164
  df['return_5m'] = df['close'].pct_change(5)
165
  df['return_15m'] = df['close'].pct_change(15)
166
-
167
  r = df['volume'].rolling(500).mean()
168
  s = df['volume'].rolling(500).std()
169
  df['vol_zscore_50'] = ((df['volume'] - r) / s).fillna(0)
170
 
171
- # 6. Legacy
172
  df['log_ret'] = np.log(df['close'] / df['close'].shift(1))
173
  roll_max = df['high'].rolling(50).max()
174
  roll_min = df['low'].rolling(50).min()
@@ -182,11 +173,19 @@ class HeavyDutyBacktester:
182
  df['ema200'] = ta.ema(df['close'], length=200)
183
  df['dist_ema200'] = (df['close'] - df['ema200']) / df['close']
184
 
 
 
 
 
 
 
 
 
185
  df.fillna(0, inplace=True)
186
  return df
187
 
188
  # ==============================================================
189
- # 🧠 CPU PROCESSING (Full Stack Injection + Logs)
190
  # ==============================================================
191
  async def _process_data_in_memory(self, sym, candles, start_ms, end_ms):
192
  safe_sym = sym.replace('/', '_')
@@ -197,7 +196,7 @@ class HeavyDutyBacktester:
197
  print(f" 📂 [{sym}] Data Exists -> Skipping.")
198
  return
199
 
200
- print(f" ⚙️ [CPU] Analyzing {sym} (Full Stack: Titan+Oracle+Sniper+Hydra)...", flush=True)
201
  t0 = time.time()
202
 
203
  df_1m = pd.DataFrame(candles, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
@@ -208,12 +207,12 @@ class HeavyDutyBacktester:
208
  frames = {}
209
  agg_dict = {'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum'}
210
 
211
- # --- 1. Vectorized Calculations ---
212
  frames['1m'] = self._calculate_indicators_vectorized(df_1m.copy(), timeframe='1m')
213
  frames['1m']['timestamp'] = frames['1m'].index.floor('1min').astype(np.int64) // 10**6
214
-
215
  fast_1m = {col: frames['1m'][col].values for col in frames['1m'].columns}
216
 
 
217
  numpy_htf = {}
218
  for tf_str, tf_code in [('5m', '5T'), ('15m', '15T'), ('1h', '1h'), ('4h', '4h'), ('1d', '1D')]:
219
  resampled = df_1m.resample(tf_code).agg(agg_dict).dropna()
@@ -222,77 +221,74 @@ class HeavyDutyBacktester:
222
  frames[tf_str] = resampled
223
  numpy_htf[tf_str] = {col: resampled[col].values for col in resampled.columns}
224
 
225
- # --- 2. L1 Filter ---
 
 
 
 
 
 
 
 
 
 
 
 
226
  df_1h = frames['1h'].reindex(frames['5m'].index, method='ffill')
227
  df_5m = frames['5m'].copy()
228
-
229
  is_valid = (df_1h['rsi'] <= 70)
230
  valid_indices = df_5m[is_valid].index
231
  start_dt = df_1m.index[0] + pd.Timedelta(minutes=500)
232
  final_valid_indices = [t for t in valid_indices if t >= start_dt]
233
 
234
  total_signals = len(final_valid_indices)
235
- print(f" 🎯 Candidates Found: {total_signals}. Running Models...", flush=True)
236
-
237
- # --- 3. Model Loading ---
238
- hydra_models = {}
239
- hydra_cols = []
240
- if self.proc.guardian_hydra:
241
- hydra_models = self.proc.guardian_hydra.models
242
- hydra_cols = self.proc.guardian_hydra.feature_cols
243
-
244
  legacy_v2 = getattr(self.proc.guardian_legacy, 'model_v2', None)
245
  legacy_v3 = getattr(self.proc.guardian_legacy, 'model_v3', None)
246
  v3_feat_names = getattr(self.proc.guardian_legacy, 'v3_feature_names', [])
247
 
248
  oracle_dir_model = getattr(self.proc.oracle, 'model_direction', None)
249
  oracle_cols = getattr(self.proc.oracle, 'feature_cols', [])
250
-
251
  sniper_models = getattr(self.proc.sniper, 'models', [])
252
  sniper_cols = getattr(self.proc.sniper, 'feature_names', [])
253
 
254
  ai_results = []
255
 
256
- # --- 4. Main Simulation Loop ---
257
  for i, current_time in enumerate(final_valid_indices):
258
  if i > 0 and i % 1000 == 0:
259
- percent = (i / total_signals) * 100
260
- print(f" ⏳ [{sym}] Processing... {i}/{total_signals} ({percent:.1f}%)", flush=True)
261
 
262
  ts_val = int(current_time.timestamp() * 1000)
263
 
264
- # Sync Indices
265
  idx_1m = np.searchsorted(fast_1m['timestamp'], ts_val)
266
- idx_1h = np.searchsorted(numpy_htf['1h']['timestamp'], ts_val)
267
- idx_5m = np.searchsorted(numpy_htf['5m']['timestamp'], ts_val) # ✅ FIXED
268
- idx_15m = np.searchsorted(numpy_htf['15m']['timestamp'], ts_val)
269
- idx_4h = np.searchsorted(numpy_htf['4h']['timestamp'], ts_val)
270
 
271
- if idx_1m < 500 or idx_1m >= len(fast_1m['close']) - 240: continue
272
- if idx_1h >= len(numpy_htf['1h']['close']): idx_1h = len(numpy_htf['1h']['close']) - 1
273
- if idx_5m >= len(numpy_htf['5m']['close']): idx_5m = len(numpy_htf['5m']['close']) - 1
274
- if idx_15m >= len(numpy_htf['15m']['close']): idx_15m = len(numpy_htf['15m']['close']) - 1
 
 
 
 
275
  if idx_4h >= len(numpy_htf['4h']['close']): idx_4h = len(numpy_htf['4h']['close']) - 1
276
 
277
- # === LAYER 2: Titan ===
278
- titan_score = 0.6
279
-
280
- # === LAYER 3: Oracle Injection ===
281
  oracle_conf = 0.5
282
- if oracle_dir_model and oracle_cols:
283
  o_vec = []
284
  for col in oracle_cols:
285
  val = 0.0
286
- if col.startswith('1h_'):
287
- raw = col.replace('1h_', '')
288
- val = numpy_htf['1h'].get(raw, [0])[idx_1h]
289
- elif col.startswith('15m_'):
290
- raw = col.replace('15m_', '')
291
- val = numpy_htf['15m'].get(raw, [0])[idx_15m]
292
- elif col.startswith('4h_'):
293
- raw = col.replace('4h_', '')
294
- val = numpy_htf['4h'].get(raw, [0])[idx_4h]
295
- elif col == 'sim_titan_score': val = titan_score
296
  elif col == 'sim_mc_score': val = 0.5
297
  elif col == 'sim_pattern_score': val = 0.5
298
  o_vec.append(val)
@@ -302,119 +298,154 @@ class HeavyDutyBacktester:
302
  if oracle_conf < 0.5: oracle_conf = 1 - oracle_conf
303
  except: pass
304
 
305
- # === LAYER 4: Sniper Injection ===
306
  sniper_score = 0.5
307
- if sniper_models and sniper_cols:
308
  s_vec = []
309
  for col in sniper_cols:
310
- if col in fast_1m:
311
- s_vec.append(fast_1m[col][idx_1m])
312
- elif col == 'L_score':
313
- l_val = fast_1m.get('vol_zscore_50', [0])[idx_1m]
314
- s_vec.append(l_val)
315
- else:
316
- s_vec.append(0.0)
317
  try:
318
  s_preds = [m.predict(np.array(s_vec).reshape(1, -1))[0] for m in sniper_models]
319
  sniper_score = np.mean(s_preds)
320
  except: pass
321
 
322
- # === RISK SIMULATION ===
 
 
 
 
 
 
 
 
 
 
 
323
  entry_price = fast_1m['close'][idx_1m]
324
- highest_price = entry_price
325
 
326
- max_hydra_crash = 0.0; max_hydra_giveback = 0.0; hydra_crash_time = 0
327
- max_legacy_v2 = 0.0; max_legacy_v3 = 0.0; legacy_panic_time = 0
 
328
 
329
- end_idx = min(idx_1m + 240, len(fast_1m['close']) - 1)
 
330
 
331
- for c_idx in range(idx_1m + 1, end_idx + 1):
332
- curr_price = fast_1m['close'][c_idx]
333
- curr_ts = int(fast_1m['timestamp'][c_idx])
334
- if curr_price > highest_price: highest_price = curr_price
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
 
336
- # A. Hydra
337
- if hydra_models:
338
- atr_val = fast_1m['atr'][c_idx]
339
- sl_dist = 1.5 * atr_val if atr_val > 0 else entry_price * 0.015
340
- pnl_r = (curr_price - entry_price) / sl_dist
341
- max_pnl_r = (highest_price - entry_price) / sl_dist
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
342
 
343
- row_dict = {
344
- 'rsi_1m': fast_1m['rsi'][c_idx],
345
- 'rsi_5m': numpy_htf['5m']['rsi'][idx_1h],
346
- 'rsi_15m': numpy_htf['15m']['rsi'][idx_15m],
347
- 'bb_width': fast_1m['bb_width'][c_idx],
348
- 'rel_vol': fast_1m['rel_vol'][c_idx],
349
- 'atr_pct': atr_val / curr_price,
350
- 'norm_pnl_r': pnl_r, 'max_pnl_r': max_pnl_r,
351
- 'time_in_trade': (c_idx - idx_1m),
352
- 'entry_type': 0.0, 'oracle_conf': oracle_conf, 'l2_score': 0.7, 'target_class': 3.0
353
- }
354
- vec = np.array([row_dict.get(c, 0.0) for c in hydra_cols]).reshape(1, -1)
355
- try:
356
- pc = hydra_models['crash'].predict_proba(vec)[0][1]
357
- if pc > max_hydra_crash:
358
- max_hydra_crash = pc
359
- if pc > 0.6 and hydra_crash_time == 0: hydra_crash_time = curr_ts
360
- pg = hydra_models['giveback'].predict_proba(vec)[0][1]
361
- if pg > max_hydra_giveback: max_hydra_giveback = pg
362
- except: pass
363
-
364
- # B. Legacy (Full Logic)
365
- if legacy_v2 or legacy_v3:
366
- c_5m_idx = idx_5m + (c_idx - idx_1m) // 5
367
- if c_5m_idx >= len(numpy_htf['5m']['rsi']): c_5m_idx = len(numpy_htf['5m']['rsi']) - 1
368
- c_15m_idx = idx_15m + (c_idx - idx_1m) // 15
369
- if c_15m_idx >= len(numpy_htf['15m']['rsi']): c_15m_idx = len(numpy_htf['15m']['rsi']) - 1
370
-
371
- if legacy_v2:
372
- f1 = [fast_1m['log_ret'][c_idx], fast_1m['rsi'][c_idx]/100.0, fast_1m['fib_pos'][c_idx], fast_1m['volatility'][c_idx]]
373
- f5 = [numpy_htf['5m']['log_ret'][c_5m_idx], numpy_htf['5m']['rsi'][c_5m_idx]/100.0, numpy_htf['5m']['fib_pos'][c_5m_idx], numpy_htf['5m']['trend_slope'][c_5m_idx]]
374
- f15 = [numpy_htf['15m']['log_ret'][c_15m_idx], numpy_htf['15m']['rsi'][c_15m_idx]/100.0, numpy_htf['15m']['dist_fib618'][c_15m_idx], numpy_htf['15m']['trend_slope'][c_15m_idx]]
375
- vec_v2 = f1 + f5 + f15
376
- lags = [1, 2, 3, 5, 10, 20]
377
- for lag in lags:
378
- l_idx = c_idx - lag
379
- if l_idx >= 0:
380
- vec_v2.extend([fast_1m['log_ret'][l_idx], fast_1m['rsi'][l_idx]/100.0, fast_1m['fib_pos'][l_idx], fast_1m['volatility'][l_idx]])
381
- else:
382
- vec_v2.extend([0.0, 0.5, 0.5, 0.0])
383
- try:
384
- dm_v2 = xgb.DMatrix(np.array(vec_v2).reshape(1, -1))
385
- pred_v2 = legacy_v2.predict(dm_v2)
386
- p_v2 = float(pred_v2[0][2]) if len(pred_v2.shape)>1 else float(pred_v2[0])
387
- if p_v2 > max_legacy_v2:
388
- max_legacy_v2 = p_v2
389
- if p_v2 > 0.8 and legacy_panic_time == 0: legacy_panic_time = curr_ts
390
- except: pass
391
-
392
- if legacy_v3 and v3_feat_names:
393
- v3_dict = {}
394
- v3_dict['rsi'] = fast_1m['rsi'][c_idx]
395
- v3_dict['dist_ema50'] = fast_1m['dist_ema50'][c_idx]
396
- v3_dict['dist_ema200'] = fast_1m['dist_ema200'][c_idx]
397
- v3_dict['log_ret'] = fast_1m['log_ret'][c_idx]
398
- v3_dict['rsi_5m'] = numpy_htf['5m']['rsi'][c_5m_idx]
399
- v3_dict['dist_ema50_5m'] = numpy_htf['5m']['dist_ema50'][c_5m_idx]
400
- v3_dict['dist_ema200_5m'] = numpy_htf['5m']['dist_ema200'][c_5m_idx]
401
- v3_dict['log_ret_5m'] = numpy_htf['5m']['log_ret'][c_5m_idx]
402
- v3_dict['rsi_15m'] = numpy_htf['15m']['rsi'][c_15m_idx]
403
- v3_dict['dist_ema50_15m'] = numpy_htf['15m']['dist_ema50'][c_15m_idx]
404
- v3_dict['dist_ema200_15m'] = numpy_htf['15m']['dist_ema200'][c_15m_idx]
405
- v3_dict['log_ret_15m'] = numpy_htf['15m']['log_ret'][c_15m_idx]
406
- try:
407
- df_v3 = pd.DataFrame(columns=v3_feat_names)
408
- df_v3.loc[0] = [v3_dict.get(n, 0.0) for n in v3_feat_names]
409
- df_v3 = df_v3.astype(float)
410
- pred_v3 = legacy_v3.predict(xgb.DMatrix(df_v3))
411
- p_v3 = float(pred_v3[0])
412
- if p_v3 > max_legacy_v3: max_legacy_v3 = p_v3
413
- except: pass
414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  ai_results.append({
416
  'timestamp': ts_val, 'symbol': sym, 'close': entry_price,
417
- 'real_titan': titan_score,
418
  'oracle_conf': oracle_conf,
419
  'sniper_score': sniper_score,
420
  'risk_hydra_crash': max_hydra_crash,
@@ -428,7 +459,7 @@ class HeavyDutyBacktester:
428
  dt = time.time() - t0
429
  if ai_results:
430
  pd.DataFrame(ai_results).to_pickle(scores_file)
431
- print(f" ✅ [{sym}] Finished {len(ai_results)} signals in {dt:.2f} seconds.", flush=True)
432
  else:
433
  print(f" ⚠️ [{sym}] No valid signals. Time: {dt:.2f}s", flush=True)
434
 
@@ -436,7 +467,7 @@ class HeavyDutyBacktester:
436
  gc.collect()
437
 
438
  # ==============================================================
439
- # PHASE 1: Main Loop
440
  # ==============================================================
441
  async def generate_truth_data(self):
442
  if self.force_start_date and self.force_end_date:
@@ -444,21 +475,16 @@ class HeavyDutyBacktester:
444
  dt_end = datetime.strptime(self.force_end_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
445
  start_time_ms = int(dt_start.timestamp() * 1000)
446
  end_time_ms = int(dt_end.timestamp() * 1000)
447
- print(f"\n🚜 [Phase 1] Processing Forced Era: {self.force_start_date} -> {self.force_end_date}")
448
- for sym in self.TARGET_COINS:
449
- try:
450
- candles = await self._fetch_all_data_fast(sym, start_time_ms, end_time_ms)
451
- if candles: await self._process_data_in_memory(sym, candles, start_time_ms, end_time_ms)
452
- except Exception as e: print(f" ❌ SKIP {sym}: {e}", flush=True)
453
- gc.collect()
454
- else:
455
- # If no forced date, we might rely on the Scenario Loop in run_strategic_optimization_task calling this.
456
- # But the Scenario Loop sets force_start_date.
457
- pass
458
 
459
- # ==============================================================
460
- # PHASE 2: Optimization (Grid Search)
461
- # ==============================================================
462
  @staticmethod
463
  def _worker_optimize(combinations_batch, scores_files, initial_capital, fees_pct, max_slots):
464
  results = []
@@ -475,15 +501,11 @@ class HeavyDutyBacktester:
475
 
476
  for config in combinations_batch:
477
  wallet = { "balance": initial_capital, "allocated": 0.0, "positions": {}, "trades_history": [] }
478
-
479
  w_titan = config['w_titan']; oracle_thresh = config.get('oracle_thresh', 0.6)
480
  sniper_thresh = config.get('sniper_thresh', 0.4); hydra_thresh = config['hydra_thresh']
481
-
482
- peak_balance = initial_capital
483
- max_drawdown = 0.0
484
 
485
  for ts, group in grouped_by_time:
486
- # EXIT
487
  active = list(wallet["positions"].keys())
488
  current_prices = {row['symbol']: row['close'] for _, row in group.iterrows()}
489
 
@@ -503,13 +525,11 @@ class HeavyDutyBacktester:
503
  del wallet['positions'][sym]
504
  wallet['trades_history'].append({'pnl': pnl})
505
 
506
- # Stats Update
507
  total_eq = wallet['balance'] + wallet['allocated']
508
  if total_eq > peak_balance: peak_balance = total_eq
509
  dd = (peak_balance - total_eq) / peak_balance
510
  if dd > max_drawdown: max_drawdown = dd
511
 
512
- # ENTRY
513
  if len(wallet['positions']) < max_slots:
514
  for _, row in group.iterrows():
515
  if row['symbol'] in wallet['positions']: continue
@@ -526,7 +546,6 @@ class HeavyDutyBacktester:
526
  wallet['balance'] -= size
527
  wallet['allocated'] += size
528
 
529
- # Stats
530
  final_bal = wallet['balance'] + wallet['allocated']
531
  net_profit = final_bal - initial_capital
532
  trades = wallet['trades_history']
@@ -537,18 +556,26 @@ class HeavyDutyBacktester:
537
  max_win = max([t['pnl'] for t in trades]) if trades else 0
538
  max_loss = min([t['pnl'] for t in trades]) if trades else 0
539
 
 
 
 
 
 
 
 
 
 
540
  results.append({
541
  'config': config, 'final_balance': final_bal, 'net_profit': net_profit,
542
  'total_trades': total_t, 'win_count': win_count, 'loss_count': loss_count,
543
  'win_rate': win_rate, 'max_single_win': max_win, 'max_single_loss': max_loss,
 
544
  'max_drawdown': max_drawdown * 100
545
  })
546
 
547
  return results
548
 
549
  async def run_optimization(self, target_regime="RANGE"):
550
- # Note: generate_truth_data is called by the Strategy Loop wrapper now
551
- # so we process data for the specific era set in set_date_range
552
  await self.generate_truth_data()
553
 
554
  oracle_range = [0.5, 0.6, 0.7]
@@ -591,8 +618,6 @@ class HeavyDutyBacktester:
591
  print("-" * 60)
592
  print(f" ⚙️ Oracle={best['config']['oracle_thresh']} | Sniper={best['config']['sniper_thresh']} | Hydra={best['config']['hydra_thresh']}")
593
  print("="*60)
594
-
595
- return best['config'], best
596
 
597
  async def run_strategic_optimization_task():
598
  print("\n🧪 [STRATEGIC BACKTEST] Full Stack Mode...")
@@ -606,7 +631,6 @@ async def run_strategic_optimization_task():
606
  hub = AdaptiveHub(r2); await hub.initialize()
607
  optimizer = HeavyDutyBacktester(dm, proc)
608
 
609
- # ✅ RESTORED: The Multi-Regime Strategic Loop
610
  scenarios = [
611
  {"regime": "BULL", "start": "2024-01-01", "end": "2024-03-30"},
612
  {"regime": "BEAR", "start": "2023-08-01", "end": "2023-09-15"},
@@ -617,11 +641,7 @@ async def run_strategic_optimization_task():
617
  for scen in scenarios:
618
  target = scen["regime"]
619
  optimizer.set_date_range(scen["start"], scen["end"])
620
-
621
- # Run opt
622
  best_cfg, best_stats = await optimizer.run_optimization(target_regime=target)
623
-
624
- # Save
625
  if best_cfg:
626
  hub.submit_challenger(target, best_cfg, best_stats)
627
 
 
1
  # ============================================================
2
+ # 🧪 backtest_engine.py (V112.0 - GEM-Architect: Matrix Batch Speed)
3
  # ============================================================
4
 
5
  import asyncio
 
55
  self.force_end_date = None
56
 
57
  if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
58
+ print(f"🧪 [Backtest V112.0] Matrix-Batch Speed (No Loops Inside Signals).")
59
 
60
  def set_date_range(self, start_str, end_str):
61
  self.force_start_date = start_str
 
104
  return unique_candles
105
 
106
  # ==============================================================
107
+ # 🏎️ VECTORIZED INDICATORS (ALL LAYERS + LAGS)
108
  # ==============================================================
109
  def _calculate_indicators_vectorized(self, df, timeframe='1m'):
 
110
  df['close'] = df['close'].astype(float)
111
  df['high'] = df['high'].astype(float)
112
  df['low'] = df['low'].astype(float)
113
  df['volume'] = df['volume'].astype(float)
114
  df['open'] = df['open'].astype(float)
115
 
 
116
  df['rsi'] = ta.rsi(df['close'], length=14)
117
  df['ema20'] = ta.ema(df['close'], length=20)
118
  df['ema50'] = ta.ema(df['close'], length=50)
119
  df['atr'] = ta.atr(df['high'], df['low'], df['close'], length=14)
120
 
121
+ # Hydra
122
  if timeframe == '1m':
123
  sma20 = df['close'].rolling(20).mean()
124
  std20 = df['close'].rolling(20).std()
 
126
  df['vol_ma50'] = df['volume'].rolling(50).mean()
127
  df['rel_vol'] = df['volume'] / (df['vol_ma50'] + 1e-9)
128
 
129
+ # Oracle
130
  df['slope'] = ta.slope(df['close'], length=7)
131
  vol_mean = df['volume'].rolling(20).mean()
132
  vol_std = df['volume'].rolling(20).std()
133
  df['vol_z'] = (df['volume'] - vol_mean) / (vol_std + 1e-9)
134
  df['atr_pct'] = df['atr'] / df['close']
135
 
136
+ # Sniper
137
  if timeframe == '1m':
138
  df['ret'] = df['close'].pct_change()
139
  df['dollar_vol'] = df['close'] * df['volume']
140
  df['amihud'] = (df['ret'].abs() / df['dollar_vol'].replace(0, np.nan)).fillna(0)
 
141
  dp = df['close'].diff()
142
  roll_cov = dp.rolling(64).cov(dp.shift(1))
143
  df['roll_spread'] = (2 * np.sqrt(np.maximum(0, -roll_cov))).fillna(0)
 
144
  sign = np.sign(df['close'].diff()).fillna(0)
145
  df['signed_vol'] = sign * df['volume']
146
  df['ofi'] = df['signed_vol'].rolling(30).sum().fillna(0)
 
147
  buy_vol = (sign > 0) * df['volume']
148
  sell_vol = (sign < 0) * df['volume']
149
  imb = (buy_vol.rolling(60).sum() - sell_vol.rolling(60).sum()).abs()
150
  tot = df['volume'].rolling(60).sum()
151
  df['vpin'] = (imb / tot.replace(0, np.nan)).fillna(0)
 
152
  vwap = (df['close'] * df['volume']).rolling(20).sum() / df['volume'].rolling(20).sum()
153
  df['vwap_dev'] = (df['close'] - vwap).fillna(0)
 
154
  df['rv_gk'] = (np.log(df['high'] / df['low'])**2) / 2 - (2 * np.log(2) - 1) * (np.log(df['close'] / df['open'])**2)
 
155
  df['return_1m'] = df['ret']
156
  df['return_5m'] = df['close'].pct_change(5)
157
  df['return_15m'] = df['close'].pct_change(15)
 
158
  r = df['volume'].rolling(500).mean()
159
  s = df['volume'].rolling(500).std()
160
  df['vol_zscore_50'] = ((df['volume'] - r) / s).fillna(0)
161
 
162
+ # Legacy Structure
163
  df['log_ret'] = np.log(df['close'] / df['close'].shift(1))
164
  roll_max = df['high'].rolling(50).max()
165
  roll_min = df['low'].rolling(50).min()
 
173
  df['ema200'] = ta.ema(df['close'], length=200)
174
  df['dist_ema200'] = (df['close'] - df['ema200']) / df['close']
175
 
176
+ # ✅ PRE-CALCULATE LAGS FOR V2 (This enables Batch Processing)
177
+ if timeframe == '1m':
178
+ for lag in [1, 2, 3, 5, 10, 20]:
179
+ df[f'log_ret_lag_{lag}'] = df['log_ret'].shift(lag).fillna(0)
180
+ df[f'rsi_lag_{lag}'] = (df['rsi'].shift(lag).fillna(50) / 100.0)
181
+ df[f'fib_pos_lag_{lag}'] = df['fib_pos'].shift(lag).fillna(0.5)
182
+ df[f'volatility_lag_{lag}'] = df['volatility'].shift(lag).fillna(0)
183
+
184
  df.fillna(0, inplace=True)
185
  return df
186
 
187
  # ==============================================================
188
+ # 🧠 CPU PROCESSING (Matrix Batch Mode)
189
  # ==============================================================
190
  async def _process_data_in_memory(self, sym, candles, start_ms, end_ms):
191
  safe_sym = sym.replace('/', '_')
 
196
  print(f" 📂 [{sym}] Data Exists -> Skipping.")
197
  return
198
 
199
+ print(f" ⚙️ [CPU] Analyzing {sym} (Matrix Batch Mode)...", flush=True)
200
  t0 = time.time()
201
 
202
  df_1m = pd.DataFrame(candles, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
 
207
  frames = {}
208
  agg_dict = {'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum'}
209
 
210
+ # 1. Calc 1m with Lags
211
  frames['1m'] = self._calculate_indicators_vectorized(df_1m.copy(), timeframe='1m')
212
  frames['1m']['timestamp'] = frames['1m'].index.floor('1min').astype(np.int64) // 10**6
 
213
  fast_1m = {col: frames['1m'][col].values for col in frames['1m'].columns}
214
 
215
+ # 2. Calc HTF
216
  numpy_htf = {}
217
  for tf_str, tf_code in [('5m', '5T'), ('15m', '15T'), ('1h', '1h'), ('4h', '4h'), ('1d', '1D')]:
218
  resampled = df_1m.resample(tf_code).agg(agg_dict).dropna()
 
221
  frames[tf_str] = resampled
222
  numpy_htf[tf_str] = {col: resampled[col].values for col in resampled.columns}
223
 
224
+ # 3. Create Global Index Maps (The Magic Step for Speed)
225
+ # Allows instant mapping from 1m index -> 5m/15m index without searching inside loop
226
+ # Using searchsorted on the whole array once
227
+ map_1m_to_1h = np.searchsorted(numpy_htf['1h']['timestamp'], fast_1m['timestamp'])
228
+ map_1m_to_5m = np.searchsorted(numpy_htf['5m']['timestamp'], fast_1m['timestamp'])
229
+ map_1m_to_15m = np.searchsorted(numpy_htf['15m']['timestamp'], fast_1m['timestamp'])
230
+
231
+ # Clamp indices to valid range
232
+ map_1m_to_1h = np.clip(map_1m_to_1h, 0, len(numpy_htf['1h']['timestamp']) - 1)
233
+ map_1m_to_5m = np.clip(map_1m_to_5m, 0, len(numpy_htf['5m']['timestamp']) - 1)
234
+ map_1m_to_15m = np.clip(map_1m_to_15m, 0, len(numpy_htf['15m']['timestamp']) - 1)
235
+
236
+ # 4. L1 Filter
237
  df_1h = frames['1h'].reindex(frames['5m'].index, method='ffill')
238
  df_5m = frames['5m'].copy()
 
239
  is_valid = (df_1h['rsi'] <= 70)
240
  valid_indices = df_5m[is_valid].index
241
  start_dt = df_1m.index[0] + pd.Timedelta(minutes=500)
242
  final_valid_indices = [t for t in valid_indices if t >= start_dt]
243
 
244
  total_signals = len(final_valid_indices)
245
+ print(f" 🎯 Candidates: {total_signals}. Running Matrix Models...", flush=True)
246
+
247
+ # 5. Load Models
248
+ hydra_models = getattr(self.proc.guardian_hydra, 'models', {}) if self.proc.guardian_hydra else {}
249
+ hydra_cols = getattr(self.proc.guardian_hydra, 'feature_cols', []) if self.proc.guardian_hydra else []
250
+
 
 
 
251
  legacy_v2 = getattr(self.proc.guardian_legacy, 'model_v2', None)
252
  legacy_v3 = getattr(self.proc.guardian_legacy, 'model_v3', None)
253
  v3_feat_names = getattr(self.proc.guardian_legacy, 'v3_feature_names', [])
254
 
255
  oracle_dir_model = getattr(self.proc.oracle, 'model_direction', None)
256
  oracle_cols = getattr(self.proc.oracle, 'feature_cols', [])
 
257
  sniper_models = getattr(self.proc.sniper, 'models', [])
258
  sniper_cols = getattr(self.proc.sniper, 'feature_names', [])
259
 
260
  ai_results = []
261
 
262
+ # --- 6. Main Simulation Loop (BATCH MODE) ---
263
  for i, current_time in enumerate(final_valid_indices):
264
  if i > 0 and i % 1000 == 0:
265
+ print(f" ⏳ [{sym}] Processing... {i}/{total_signals}", flush=True)
 
266
 
267
  ts_val = int(current_time.timestamp() * 1000)
268
 
269
+ # Find Entry Index
270
  idx_1m = np.searchsorted(fast_1m['timestamp'], ts_val)
 
 
 
 
271
 
272
+ # Safety Check
273
+ if idx_1m < 500 or idx_1m >= len(fast_1m['close']) - 245: continue
274
+
275
+ # Determine Indices
276
+ idx_1h = map_1m_to_1h[idx_1m]
277
+ idx_5m = map_1m_to_5m[idx_1m]
278
+ idx_15m = map_1m_to_15m[idx_1m]
279
+ idx_4h = np.searchsorted(numpy_htf['4h']['timestamp'], ts_val) # Do this once per signal (rare)
280
  if idx_4h >= len(numpy_htf['4h']['close']): idx_4h = len(numpy_htf['4h']['close']) - 1
281
 
282
+ # === Oracle (Single Call) ===
 
 
 
283
  oracle_conf = 0.5
284
+ if oracle_dir_model:
285
  o_vec = []
286
  for col in oracle_cols:
287
  val = 0.0
288
+ if col.startswith('1h_'): val = numpy_htf['1h'].get(col[3:], [0])[idx_1h]
289
+ elif col.startswith('15m_'): val = numpy_htf['15m'].get(col[4:], [0])[idx_15m]
290
+ elif col.startswith('4h_'): val = numpy_htf['4h'].get(col[3:], [0])[idx_4h]
291
+ elif col == 'sim_titan_score': val = 0.6
 
 
 
 
 
 
292
  elif col == 'sim_mc_score': val = 0.5
293
  elif col == 'sim_pattern_score': val = 0.5
294
  o_vec.append(val)
 
298
  if oracle_conf < 0.5: oracle_conf = 1 - oracle_conf
299
  except: pass
300
 
301
+ # === Sniper (Single Call) ===
302
  sniper_score = 0.5
303
+ if sniper_models:
304
  s_vec = []
305
  for col in sniper_cols:
306
+ if col in fast_1m: s_vec.append(fast_1m[col][idx_1m])
307
+ elif col == 'L_score': s_vec.append(fast_1m.get('vol_zscore_50', [0])[idx_1m])
308
+ else: s_vec.append(0.0)
 
 
 
 
309
  try:
310
  s_preds = [m.predict(np.array(s_vec).reshape(1, -1))[0] for m in sniper_models]
311
  sniper_score = np.mean(s_preds)
312
  except: pass
313
 
314
+ # === RISK SIMULATION (MATRIX BATCH) ===
315
+ # We construct a matrix of 240 rows (4 hours) at once and predict ONCE.
316
+ # This replaces the minute-by-minute loop.
317
+
318
+ future_len = 240
319
+ start_idx = idx_1m + 1
320
+ end_idx = start_idx + future_len
321
+
322
+ # Slices for 1m data
323
+ sl_close = fast_1m['close'][start_idx:end_idx]
324
+ sl_ts = fast_1m['timestamp'][start_idx:end_idx]
325
+
326
  entry_price = fast_1m['close'][idx_1m]
 
327
 
328
+ # Mapped Indices for HTF slices
329
+ sl_map_5m = map_1m_to_5m[start_idx:end_idx]
330
+ sl_map_15m = map_1m_to_15m[start_idx:end_idx]
331
 
332
+ max_hydra_crash = 0.0; hydra_crash_time = 0
333
+ max_legacy_v2 = 0.0; legacy_panic_time = 0
334
 
335
+ # --- A. Hydra Batch ---
336
+ if hydra_models:
337
+ sl_atr = fast_1m['atr'][start_idx:end_idx]
338
+ sl_rsi_1m = fast_1m['rsi'][start_idx:end_idx]
339
+ sl_bb = fast_1m['bb_width'][start_idx:end_idx]
340
+ sl_vol = fast_1m['rel_vol'][start_idx:end_idx]
341
+
342
+ # HTF Lookups (Using integer array indexing - Fast)
343
+ sl_rsi_5m = numpy_htf['5m']['rsi'][sl_map_5m]
344
+ sl_rsi_15m = numpy_htf['15m']['rsi'][sl_map_15m]
345
+
346
+ # Calc Features
347
+ sl_dist = 1.5 * sl_atr
348
+ sl_dist = np.where(sl_dist > 0, sl_dist, entry_price * 0.015)
349
+
350
+ # PnL & Max PnL
351
+ sl_pnl = sl_close - entry_price
352
+ sl_norm_pnl = sl_pnl / sl_dist
353
+
354
+ # Max PnL needs cumulative max (rolling max from start of trade)
355
+ sl_cum_max = np.maximum.accumulate(sl_close)
356
+ # Correction: cum max of trade needs to start from entry price
357
+ sl_cum_max = np.maximum(sl_cum_max, entry_price)
358
+ sl_max_pnl_r = (sl_cum_max - entry_price) / sl_dist
359
+
360
+ sl_atr_pct = sl_atr / sl_close
361
+ sl_time = np.arange(1, future_len + 1)
362
 
363
+ # Stack Matrix: (240, N_Features)
364
+ # Feature Order Must Match hydra_cols
365
+ # Map cols manually to speed up
366
+
367
+ # Create dict of vectors
368
+ feat_vecs = {
369
+ 'rsi_1m': sl_rsi_1m, 'rsi_5m': sl_rsi_5m, 'rsi_15m': sl_rsi_15m,
370
+ 'bb_width': sl_bb, 'rel_vol': sl_vol,
371
+ 'dist_ema20_1h': np.zeros(future_len),
372
+ 'atr_pct': sl_atr_pct, 'norm_pnl_r': sl_norm_pnl, 'max_pnl_r': sl_max_pnl_r,
373
+ 'dist_tp_atr': np.zeros(future_len), 'dist_sl_atr': np.zeros(future_len),
374
+ 'time_in_trade': sl_time,
375
+ 'entry_type': np.zeros(future_len), 'oracle_conf': np.full(future_len, oracle_conf),
376
+ 'l2_score': np.full(future_len, 0.7), 'target_class': np.full(future_len, 3.0)
377
+ }
378
+
379
+ # Stack
380
+ X_hydra = np.column_stack([feat_vecs.get(c, np.zeros(future_len)) for c in hydra_cols])
381
+
382
+ try:
383
+ # ONE PREDICTION FOR 240 ROWS
384
+ probs_crash = hydra_models['crash'].predict_proba(X_hydra)[:, 1]
385
 
386
+ # Find Max
387
+ max_hydra_crash = np.max(probs_crash)
388
+
389
+ # Find Time
390
+ crash_indices = np.where(probs_crash > 0.6)[0]
391
+ if len(crash_indices) > 0:
392
+ hydra_crash_time = int(sl_ts[crash_indices[0]])
393
+ except: pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
394
 
395
+ # --- B. Legacy V2 Batch ---
396
+ if legacy_v2:
397
+ # 1m Feats
398
+ l_log = fast_1m['log_ret'][start_idx:end_idx]
399
+ l_rsi = fast_1m['rsi'][start_idx:end_idx] / 100.0
400
+ l_fib = fast_1m['fib_pos'][start_idx:end_idx]
401
+ l_vol = fast_1m['volatility'][start_idx:end_idx]
402
+
403
+ # 5m Feats (Mapped)
404
+ l5_log = numpy_htf['5m']['log_ret'][sl_map_5m]
405
+ l5_rsi = numpy_htf['5m']['rsi'][sl_map_5m] / 100.0
406
+ l5_fib = numpy_htf['5m']['fib_pos'][sl_map_5m]
407
+ l5_trd = numpy_htf['5m']['trend_slope'][sl_map_5m]
408
+
409
+ # 15m Feats (Mapped)
410
+ l15_log = numpy_htf['15m']['log_ret'][sl_map_15m]
411
+ l15_rsi = numpy_htf['15m']['rsi'][sl_map_15m] / 100.0
412
+ l15_fib618 = numpy_htf['15m']['dist_fib618'][sl_map_15m]
413
+ l15_trd = numpy_htf['15m']['trend_slope'][sl_map_15m]
414
+
415
+ # Lags (Pre-calculated in _calculate_indicators_vectorized)
416
+ # We just pull them from fast_1m
417
+ lag_cols = []
418
+ for lag in [1, 2, 3, 5, 10, 20]:
419
+ lag_cols.append(fast_1m[f'log_ret_lag_{lag}'][start_idx:end_idx])
420
+ lag_cols.append(fast_1m[f'rsi_lag_{lag}'][start_idx:end_idx])
421
+ lag_cols.append(fast_1m[f'fib_pos_lag_{lag}'][start_idx:end_idx])
422
+ lag_cols.append(fast_1m[f'volatility_lag_{lag}'][start_idx:end_idx])
423
+
424
+ # Stack All
425
+ X_v2 = np.column_stack([
426
+ l_log, l_rsi, l_fib, l_vol,
427
+ l5_log, l5_rsi, l5_fib, l5_trd,
428
+ l15_log, l15_rsi, l15_fib618, l15_trd,
429
+ *lag_cols
430
+ ])
431
+
432
+ try:
433
+ # PREDICT BATCH
434
+ dm_v2 = xgb.DMatrix(X_v2)
435
+ preds_v2 = legacy_v2.predict(dm_v2)
436
+ # Handle Multiclass
437
+ probs_v2 = preds_v2[:, 2] if len(preds_v2.shape) > 1 else preds_v2
438
+
439
+ max_legacy_v2 = np.max(probs_v2)
440
+ panic_idx = np.where(probs_v2 > 0.8)[0]
441
+ if len(panic_idx) > 0 and legacy_panic_time == 0:
442
+ legacy_panic_time = int(sl_ts[panic_idx[0]])
443
+ except: pass
444
+
445
+ # --- Store Result ---
446
  ai_results.append({
447
  'timestamp': ts_val, 'symbol': sym, 'close': entry_price,
448
+ 'real_titan': 0.6,
449
  'oracle_conf': oracle_conf,
450
  'sniper_score': sniper_score,
451
  'risk_hydra_crash': max_hydra_crash,
 
459
  dt = time.time() - t0
460
  if ai_results:
461
  pd.DataFrame(ai_results).to_pickle(scores_file)
462
+ print(f" ✅ [{sym}] Batch-Processed {len(ai_results)} signals in {dt:.2f} seconds.", flush=True)
463
  else:
464
  print(f" ⚠️ [{sym}] No valid signals. Time: {dt:.2f}s", flush=True)
465
 
 
467
  gc.collect()
468
 
469
  # ==============================================================
470
+ # PHASE 1 & 2 (Unchanged - Standard Optimization Logic)
471
  # ==============================================================
472
  async def generate_truth_data(self):
473
  if self.force_start_date and self.force_end_date:
 
475
  dt_end = datetime.strptime(self.force_end_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
476
  start_time_ms = int(dt_start.timestamp() * 1000)
477
  end_time_ms = int(dt_end.timestamp() * 1000)
478
+ print(f"\n🚜 [Phase 1] Processing Era: {self.force_start_date} -> {self.force_end_date}")
479
+ else: return
480
+
481
+ for sym in self.TARGET_COINS:
482
+ try:
483
+ candles = await self._fetch_all_data_fast(sym, start_time_ms, end_time_ms)
484
+ if candles: await self._process_data_in_memory(sym, candles, start_time_ms, end_time_ms)
485
+ except Exception as e: print(f" ❌ SKIP {sym}: {e}", flush=True)
486
+ gc.collect()
 
 
487
 
 
 
 
488
  @staticmethod
489
  def _worker_optimize(combinations_batch, scores_files, initial_capital, fees_pct, max_slots):
490
  results = []
 
501
 
502
  for config in combinations_batch:
503
  wallet = { "balance": initial_capital, "allocated": 0.0, "positions": {}, "trades_history": [] }
 
504
  w_titan = config['w_titan']; oracle_thresh = config.get('oracle_thresh', 0.6)
505
  sniper_thresh = config.get('sniper_thresh', 0.4); hydra_thresh = config['hydra_thresh']
506
+ peak_balance = initial_capital; max_drawdown = 0.0
 
 
507
 
508
  for ts, group in grouped_by_time:
 
509
  active = list(wallet["positions"].keys())
510
  current_prices = {row['symbol']: row['close'] for _, row in group.iterrows()}
511
 
 
525
  del wallet['positions'][sym]
526
  wallet['trades_history'].append({'pnl': pnl})
527
 
 
528
  total_eq = wallet['balance'] + wallet['allocated']
529
  if total_eq > peak_balance: peak_balance = total_eq
530
  dd = (peak_balance - total_eq) / peak_balance
531
  if dd > max_drawdown: max_drawdown = dd
532
 
 
533
  if len(wallet['positions']) < max_slots:
534
  for _, row in group.iterrows():
535
  if row['symbol'] in wallet['positions']: continue
 
546
  wallet['balance'] -= size
547
  wallet['allocated'] += size
548
 
 
549
  final_bal = wallet['balance'] + wallet['allocated']
550
  net_profit = final_bal - initial_capital
551
  trades = wallet['trades_history']
 
556
  max_win = max([t['pnl'] for t in trades]) if trades else 0
557
  max_loss = min([t['pnl'] for t in trades]) if trades else 0
558
 
559
+ max_win_streak = 0; max_loss_streak = 0; curr_w = 0; curr_l = 0
560
+ for t in trades:
561
+ if t['pnl'] > 0:
562
+ curr_w += 1; curr_l = 0
563
+ if curr_w > max_win_streak: max_win_streak = curr_w
564
+ else:
565
+ curr_l += 1; curr_w = 0
566
+ if curr_l > max_loss_streak: max_loss_streak = curr_l
567
+
568
  results.append({
569
  'config': config, 'final_balance': final_bal, 'net_profit': net_profit,
570
  'total_trades': total_t, 'win_count': win_count, 'loss_count': loss_count,
571
  'win_rate': win_rate, 'max_single_win': max_win, 'max_single_loss': max_loss,
572
+ 'max_win_streak': max_win_streak, 'max_loss_streak': max_loss_streak,
573
  'max_drawdown': max_drawdown * 100
574
  })
575
 
576
  return results
577
 
578
  async def run_optimization(self, target_regime="RANGE"):
 
 
579
  await self.generate_truth_data()
580
 
581
  oracle_range = [0.5, 0.6, 0.7]
 
618
  print("-" * 60)
619
  print(f" ⚙️ Oracle={best['config']['oracle_thresh']} | Sniper={best['config']['sniper_thresh']} | Hydra={best['config']['hydra_thresh']}")
620
  print("="*60)
 
 
621
 
622
  async def run_strategic_optimization_task():
623
  print("\n🧪 [STRATEGIC BACKTEST] Full Stack Mode...")
 
631
  hub = AdaptiveHub(r2); await hub.initialize()
632
  optimizer = HeavyDutyBacktester(dm, proc)
633
 
 
634
  scenarios = [
635
  {"regime": "BULL", "start": "2024-01-01", "end": "2024-03-30"},
636
  {"regime": "BEAR", "start": "2023-08-01", "end": "2023-09-15"},
 
641
  for scen in scenarios:
642
  target = scen["regime"]
643
  optimizer.set_date_range(scen["start"], scen["end"])
 
 
644
  best_cfg, best_stats = await optimizer.run_optimization(target_regime=target)
 
 
645
  if best_cfg:
646
  hub.submit_challenger(target, best_cfg, best_stats)
647