Riy777 commited on
Commit
a209eab
ยท
verified ยท
1 Parent(s): 7de5864

Update backtest_engine.py

Browse files
Files changed (1) hide show
  1. backtest_engine.py +61 -94
backtest_engine.py CHANGED
@@ -1,5 +1,5 @@
1
  # ============================================================
2
- # ๐Ÿงช backtest_engine.py (V132.4 - GEM-Architect: Deep Diagnostic Mode)
3
  # ============================================================
4
 
5
  import asyncio
@@ -103,102 +103,68 @@ def _transform_window_for_pattern(df_window):
103
  except: return None
104
 
105
  def calculate_sniper_features_exact(df):
106
- """
107
- Sniper Features Calculation - TRUE UNIVERSAL MODE (Z-SCORE).
108
- Converts structural features to Z-Scores to bypass scale issues.
109
- """
110
- # 1. Standard Features
111
  d = df.copy()
112
  c = d['close']; h = d['high']; l = d['low']; v = d['volume']; o = d['open']
113
 
114
- # --- Helper: Rolling Z-Score ---
115
- def _z_roll(x, w=200): # Window 200 is standard for regime detection
116
  r = x.rolling(w).mean()
117
  s = x.rolling(w).std().replace(0, np.nan)
118
  return ((x - r) / s).fillna(0)
119
 
120
- # Basic Returns (Keep as percentages, trees handle these well)
121
  d['return_1m'] = c.pct_change(1).fillna(0)
122
  d['return_3m'] = c.pct_change(3).fillna(0)
123
  d['return_5m'] = c.pct_change(5).fillna(0)
124
  d['return_15m'] = c.pct_change(15).fillna(0)
125
 
126
- # Technicals (RSI is bounded 0-100, usually safe)
127
  d['rsi_14'] = ta.rsi(c, length=14).fillna(50)
128
-
129
  ema_9 = ta.ema(c, length=9).fillna(c)
130
  ema_21 = ta.ema(c, length=21).fillna(c)
131
-
132
- # Slopes/Distances -> Normalized
133
  d['ema_9_slope'] = ((ema_9 - ema_9.shift(1)) / ema_9.shift(1).replace(0, np.nan)).fillna(0)
134
  d['ema_21_dist'] = ((c - ema_21) / ema_21.replace(0, np.nan)).fillna(0)
135
 
136
- # --- TRANSFORM 1: ATR (Vol) -> Z-Score ---
137
- # Instead of raw value or %, check if Volatility is spiking relative to history
138
  atr_raw = ta.atr(h, l, c, length=100).fillna(0)
139
  d['atr'] = _z_roll(atr_raw, 500)
140
-
141
- # Volume Z-Score
142
  d['vol_zscore_50'] = _z_roll(v, 50)
143
-
144
- # Candle Geometry
145
  rng = (h - l).replace(0, 1e-9)
146
- d['candle_range'] = _z_roll(rng, 500) # Is this candle huge relative to history?
147
  d['close_pos_in_range'] = ((c - l) / rng).fillna(0.5)
148
 
149
- # --- TRANSFORM 2: Liquidity Proxies -> Z-Score ---
150
- # This fixes the Amihud 1e-8 issue completely.
151
-
152
- # Amihud
153
  d['dollar_vol'] = c * v
154
  amihud_raw = (d['return_1m'].abs() / d['dollar_vol'].replace(0, np.nan)).fillna(0)
155
  d['amihud'] = _z_roll(amihud_raw, 500)
156
 
157
- # Roll Spread
158
  dp = c.diff()
159
  roll_cov = dp.rolling(64).cov(dp.shift(1)).fillna(0)
160
  roll_spread_raw = (2 * np.sqrt(np.maximum(0, -roll_cov)))
161
  d['roll_spread'] = _z_roll(roll_spread_raw, 500)
162
 
163
- # OFI (Order Flow)
164
  sign = np.sign(c.diff()).fillna(0)
165
  d['signed_vol'] = sign * v
166
  ofi_raw = d['signed_vol'].rolling(30).sum().fillna(0)
167
  d['ofi'] = _z_roll(ofi_raw, 500)
168
 
169
- # VPIN
170
  buy_vol = (sign > 0) * v
171
  sell_vol = (sign < 0) * v
172
  imb = (buy_vol.rolling(60).sum() - sell_vol.rolling(60).sum()).abs()
173
  tot = v.rolling(60).sum().replace(0, np.nan)
174
- d['vpin'] = (imb / tot).fillna(0) # VPIN is a ratio (0-1), usually safe without Z-score, but let's keep it raw
175
 
176
- # Garman-Klass Volatility
177
  rv_gk_raw = ((np.log(h / l)**2) / 2) - ((2 * np.log(2) - 1) * (np.log(c / o)**2))
178
  d['rv_gk'] = _z_roll(rv_gk_raw.fillna(0), 500)
179
 
180
- # VWAP Deviation
181
  vwap_win = 20
182
  vwap = (d['dollar_vol'].rolling(vwap_win).sum() / v.rolling(vwap_win).sum().replace(0, np.nan)).fillna(c)
183
  d['vwap_dev'] = _z_roll((c - vwap), 500)
184
 
185
- # Liquidity Score (Composite - Already using internal Z-scores in logic, but let's re-calc)
186
- # Note: We use the already Z-scored columns now where possible
187
  d['L_score'] = (
188
- d['vol_zscore_50'] +
189
- (-d['amihud']) + # Low illiquidity is good
190
- (-d['roll_spread']) +
191
- (-d['rv_gk'].abs()) +
192
- (-d['vwap_dev'].abs()) +
193
- d['ofi']
194
  ).fillna(0)
195
 
196
  return sanitize_features(d)
197
 
198
  def calculate_titan_features_real(df):
199
- """Titan features with strict Infinity handling."""
200
  df = df.copy()
201
-
202
  df['RSI'] = ta.rsi(df['close'], 14)
203
  macd = ta.macd(df['close'])
204
  if macd is not None:
@@ -269,15 +235,12 @@ def calculate_legacy_v2_vectorized(df_1m, df_5m, df_15m):
269
  X_df = pd.concat(parts, axis=1)
270
  return sanitize_features(X_df).values
271
  except Exception as e:
272
- print(f"Legacy V2 Vec Error: {e}")
273
  return None
274
 
275
  def calculate_legacy_v3_vectorized(df_1m, df_5m, df_15m):
276
- """Legacy V3 Safe Calc."""
277
  try:
278
  def calc_v3_base(df, prefix=""):
279
  d = df.copy()
280
- # Initialize All
281
  targets = ['rsi', 'rsi_slope', 'macd_h', 'macd_h_slope', 'adx', 'dmp', 'dmn',
282
  'trend_net_force', 'ema_20', 'ema_50', 'ema_200', 'dist_ema20',
283
  'dist_ema50', 'dist_ema200', 'slope_ema50', 'atr', 'atr_rel',
@@ -398,7 +361,6 @@ class HeavyDutyBacktester:
398
  return raw
399
  return model.predict(X)
400
  except Exception as e:
401
- # print(f"โš ๏ธ {model_name} Error: {e}")
402
  return np.zeros(len(X) if hasattr(X, '__len__') else 0)
403
 
404
  def _extract_probs(self, raw_preds):
@@ -450,10 +412,8 @@ class HeavyDutyBacktester:
450
  df_5m = df_1m.resample('5T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
451
  df_15m = df_1m.resample('15T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
452
 
453
- # 1. Sniper (True Match Logic)
454
  df_sniper_feats = calculate_sniper_features_exact(df_1m)
455
-
456
- # Calculate L1 Score manually (since new Sniper function returns pure features)
457
  rel_vol = df_1m['volume'] / (df_1m['volume'].rolling(50).mean() + 1e-9)
458
  l1_score = (rel_vol * 10) + ((ta.atr(df_1m['high'], df_1m['low'], df_1m['close'], 14)/df_1m['close']) * 1000)
459
 
@@ -497,72 +457,36 @@ class HeavyDutyBacktester:
497
  res_titan = self.proc.titan.model.predict(xgb.DMatrix(X_titan_df.values, feature_names=feats))
498
  except Exception as e: print(f"Titan Error: {e}")
499
 
500
- # 4. Sniper (GEM-Architect Refactor: Deep Diagnostic Mode)
501
  res_sniper = np.full(len(df_candidates), 0.5)
502
  sniper_instance = self.proc.sniper
503
-
504
  if sniper_instance and getattr(sniper_instance, 'models', []):
505
  try:
506
- # A. Identify Required Features
507
  required_features = getattr(sniper_instance, 'feature_names', [])
508
  if not required_features and hasattr(sniper_instance.models[0], 'feature_name'):
509
  required_features = sniper_instance.models[0].feature_name()
510
 
511
- # B. Build Maps
512
  source_cols = df_sniper_feats.columns
513
  def normalize_name(s): return s.lower().replace('_', '').replace('-', '').replace(' ', '')
514
  col_map_lower = {col.lower(): col for col in source_cols}
515
  col_map_fuzzy = {normalize_name(col): col for col in source_cols}
516
 
517
- # C. Construct X_final
518
  X_final = pd.DataFrame(index=df_candidates.index)
519
- missing_feats = []
520
- found_feats = []
521
-
522
  if required_features:
523
  for req_feat in required_features:
524
  req_norm = normalize_name(req_feat)
525
- if req_feat in source_cols:
526
- X_final[req_feat] = df_sniper_feats.loc[df_candidates.index, req_feat]
527
- found_feats.append(f"{req_feat}(Direct)")
528
- elif req_feat.lower() in col_map_lower:
529
- real_col = col_map_lower[req_feat.lower()]
530
- X_final[req_feat] = df_sniper_feats.loc[df_candidates.index, real_col]
531
- found_feats.append(f"{req_feat}->{real_col}")
532
- elif req_norm in col_map_fuzzy:
533
- real_col = col_map_fuzzy[req_norm]
534
- X_final[req_feat] = df_sniper_feats.loc[df_candidates.index, real_col]
535
- found_feats.append(f"{req_feat}~>{real_col}")
536
- else:
537
- missing_feats.append(req_feat)
538
- X_final[req_feat] = 0.0
539
- else:
540
- X_final = df_sniper_feats.loc[df_candidates.index]
541
-
542
- # D. Deep Diagnostics
543
- print(f" ๐Ÿ•ต๏ธ [Sniper Diag] Expected {len(required_features)} | Found {len(required_features) - len(missing_feats)} | Missing {len(missing_feats)}")
544
- if missing_feats:
545
- print(f" โŒ MISSING: {missing_feats}")
546
- if found_feats:
547
- print(f" โœ… MAPPED (First 5): {found_feats[:5]}")
548
-
549
- # E. Data Stats Check
550
- feat_stats = X_final.describe().T[['mean', 'min', 'max']]
551
- print(f" ๐Ÿ“ˆ [Sniper Input Stats] (First 3 Feats):\n{feat_stats.head(3)}")
552
 
553
- # F. Predict
554
  preds = []
555
  for m in sniper_instance.models:
556
  X_in = X_final.astype(np.float32)
557
  raw_p = self._extract_probs(self._smart_predict(m, X_in, "Sniper"))
558
  preds.append(raw_p)
559
-
560
  res_sniper = np.mean(preds, axis=0)
561
-
562
- p_min, p_max, p_mean = res_sniper.min(), res_sniper.max(), res_sniper.mean()
563
- n_high = np.sum(res_sniper > 0.6)
564
- print(f" ๐ŸŽฏ [Sniper Output] Min:{p_min:.4f} Max:{p_max:.4f} Mean:{p_mean:.4f} | Signals(>0.6): {n_high}")
565
-
566
  except Exception as e:
567
  print(f"โŒ Sniper Inference Error: {e}")
568
  traceback.print_exc()
@@ -585,10 +509,10 @@ class HeavyDutyBacktester:
585
  hydra_models = getattr(self.proc.guardian_hydra, 'models', {})
586
  if hydra_models and 'crash' in hydra_models:
587
  try:
588
- df_sniper_base = df_1m.copy() # Reuse basic DF
589
  df_sniper_base['rsi_14'] = ta.rsi(df_sniper_base['close'], 14).fillna(50)
590
  df_sniper_base['atr'] = ta.atr(df_sniper_base['high'], df_sniper_base['low'], df_sniper_base['close'], 14).fillna(0)
591
- df_sniper_base['rel_vol'] = rel_vol # Reused
592
 
593
  global_hydra_feats = np.column_stack([
594
  df_sniper_base['rsi_14'], df_sniper_base['rsi_14'], df_sniper_base['rsi_14'],
@@ -728,10 +652,33 @@ class HeavyDutyBacktester:
728
 
729
  final_bal = bal + alloc
730
  profit = final_bal - initial_capital
 
 
731
  tot = len(log)
732
- wins = sum(1 for x in log if x['pnl'] > 0)
733
- win_rate = (wins/tot*100) if tot else 0
734
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
735
  cons_trades = [x for x in log if x['consensus']]
736
  n_cons = len(cons_trades)
737
  agree_rate = (n_cons/tot*100) if tot else 0
@@ -741,6 +688,10 @@ class HeavyDutyBacktester:
741
  res.append({
742
  'config': cfg, 'final_balance': final_bal, 'net_profit': profit,
743
  'total_trades': tot, 'win_rate': win_rate, 'max_drawdown': 0,
 
 
 
 
744
  'consensus_agreement_rate': agree_rate,
745
  'high_consensus_win_rate': cons_win_rate,
746
  'high_consensus_avg_pnl': cons_avg_pnl
@@ -766,6 +717,18 @@ class HeavyDutyBacktester:
766
  results_list.sort(key=lambda x: x['net_profit'], reverse=True)
767
  best = results_list[0]
768
 
 
 
 
 
 
 
 
 
 
 
 
 
769
  print("\n" + "="*60)
770
  print(f"๐Ÿ† CHAMPION REPORT [{target_regime}]:")
771
  print(f" ๐Ÿ’ฐ Final Balance: ${best['final_balance']:,.2f}")
@@ -773,12 +736,16 @@ class HeavyDutyBacktester:
773
  print("-" * 60)
774
  print(f" ๐Ÿ“Š Total Trades: {best['total_trades']}")
775
  print(f" ๐Ÿ“ˆ Win Rate: {best['win_rate']:.1f}%")
 
 
 
 
776
  print("-" * 60)
777
  print(f" ๐Ÿง  CONSENSUS ANALYTICS:")
778
  print(f" ๐Ÿค Model Agreement Rate: {best['consensus_agreement_rate']:.1f}%")
779
  print(f" ๐ŸŒŸ High-Consensus Win Rate: {best['high_consensus_win_rate']:.1f}%")
780
- print(f" ๐Ÿ’Ž High-Consensus Avg PnL: {best['high_consensus_avg_pnl']:.2f}%")
781
  print("-" * 60)
 
782
  print(f" โš™๏ธ Oracle={best['config']['oracle_thresh']:.2f} | Sniper={best['config']['sniper_thresh']:.2f} | Hydra={best['config']['hydra_thresh']:.2f}")
783
  print("="*60)
784
  return best['config'], best
 
1
  # ============================================================
2
+ # ๐Ÿงช backtest_engine.py (V132.5 - GEM-Architect: Full Diagnostics)
3
  # ============================================================
4
 
5
  import asyncio
 
103
  except: return None
104
 
105
  def calculate_sniper_features_exact(df):
 
 
 
 
 
106
  d = df.copy()
107
  c = d['close']; h = d['high']; l = d['low']; v = d['volume']; o = d['open']
108
 
109
+ def _z_roll(x, w=200):
 
110
  r = x.rolling(w).mean()
111
  s = x.rolling(w).std().replace(0, np.nan)
112
  return ((x - r) / s).fillna(0)
113
 
 
114
  d['return_1m'] = c.pct_change(1).fillna(0)
115
  d['return_3m'] = c.pct_change(3).fillna(0)
116
  d['return_5m'] = c.pct_change(5).fillna(0)
117
  d['return_15m'] = c.pct_change(15).fillna(0)
118
 
 
119
  d['rsi_14'] = ta.rsi(c, length=14).fillna(50)
 
120
  ema_9 = ta.ema(c, length=9).fillna(c)
121
  ema_21 = ta.ema(c, length=21).fillna(c)
 
 
122
  d['ema_9_slope'] = ((ema_9 - ema_9.shift(1)) / ema_9.shift(1).replace(0, np.nan)).fillna(0)
123
  d['ema_21_dist'] = ((c - ema_21) / ema_21.replace(0, np.nan)).fillna(0)
124
 
 
 
125
  atr_raw = ta.atr(h, l, c, length=100).fillna(0)
126
  d['atr'] = _z_roll(atr_raw, 500)
 
 
127
  d['vol_zscore_50'] = _z_roll(v, 50)
 
 
128
  rng = (h - l).replace(0, 1e-9)
129
+ d['candle_range'] = _z_roll(rng, 500)
130
  d['close_pos_in_range'] = ((c - l) / rng).fillna(0.5)
131
 
 
 
 
 
132
  d['dollar_vol'] = c * v
133
  amihud_raw = (d['return_1m'].abs() / d['dollar_vol'].replace(0, np.nan)).fillna(0)
134
  d['amihud'] = _z_roll(amihud_raw, 500)
135
 
 
136
  dp = c.diff()
137
  roll_cov = dp.rolling(64).cov(dp.shift(1)).fillna(0)
138
  roll_spread_raw = (2 * np.sqrt(np.maximum(0, -roll_cov)))
139
  d['roll_spread'] = _z_roll(roll_spread_raw, 500)
140
 
 
141
  sign = np.sign(c.diff()).fillna(0)
142
  d['signed_vol'] = sign * v
143
  ofi_raw = d['signed_vol'].rolling(30).sum().fillna(0)
144
  d['ofi'] = _z_roll(ofi_raw, 500)
145
 
 
146
  buy_vol = (sign > 0) * v
147
  sell_vol = (sign < 0) * v
148
  imb = (buy_vol.rolling(60).sum() - sell_vol.rolling(60).sum()).abs()
149
  tot = v.rolling(60).sum().replace(0, np.nan)
150
+ d['vpin'] = (imb / tot).fillna(0)
151
 
 
152
  rv_gk_raw = ((np.log(h / l)**2) / 2) - ((2 * np.log(2) - 1) * (np.log(c / o)**2))
153
  d['rv_gk'] = _z_roll(rv_gk_raw.fillna(0), 500)
154
 
 
155
  vwap_win = 20
156
  vwap = (d['dollar_vol'].rolling(vwap_win).sum() / v.rolling(vwap_win).sum().replace(0, np.nan)).fillna(c)
157
  d['vwap_dev'] = _z_roll((c - vwap), 500)
158
 
 
 
159
  d['L_score'] = (
160
+ d['vol_zscore_50'] + (-d['amihud']) + (-d['roll_spread']) +
161
+ (-d['rv_gk'].abs()) + (-d['vwap_dev'].abs()) + d['ofi']
 
 
 
 
162
  ).fillna(0)
163
 
164
  return sanitize_features(d)
165
 
166
  def calculate_titan_features_real(df):
 
167
  df = df.copy()
 
168
  df['RSI'] = ta.rsi(df['close'], 14)
169
  macd = ta.macd(df['close'])
170
  if macd is not None:
 
235
  X_df = pd.concat(parts, axis=1)
236
  return sanitize_features(X_df).values
237
  except Exception as e:
 
238
  return None
239
 
240
  def calculate_legacy_v3_vectorized(df_1m, df_5m, df_15m):
 
241
  try:
242
  def calc_v3_base(df, prefix=""):
243
  d = df.copy()
 
244
  targets = ['rsi', 'rsi_slope', 'macd_h', 'macd_h_slope', 'adx', 'dmp', 'dmn',
245
  'trend_net_force', 'ema_20', 'ema_50', 'ema_200', 'dist_ema20',
246
  'dist_ema50', 'dist_ema200', 'slope_ema50', 'atr', 'atr_rel',
 
361
  return raw
362
  return model.predict(X)
363
  except Exception as e:
 
364
  return np.zeros(len(X) if hasattr(X, '__len__') else 0)
365
 
366
  def _extract_probs(self, raw_preds):
 
412
  df_5m = df_1m.resample('5T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
413
  df_15m = df_1m.resample('15T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
414
 
415
+ # 1. Sniper
416
  df_sniper_feats = calculate_sniper_features_exact(df_1m)
 
 
417
  rel_vol = df_1m['volume'] / (df_1m['volume'].rolling(50).mean() + 1e-9)
418
  l1_score = (rel_vol * 10) + ((ta.atr(df_1m['high'], df_1m['low'], df_1m['close'], 14)/df_1m['close']) * 1000)
419
 
 
457
  res_titan = self.proc.titan.model.predict(xgb.DMatrix(X_titan_df.values, feature_names=feats))
458
  except Exception as e: print(f"Titan Error: {e}")
459
 
460
+ # 4. Sniper
461
  res_sniper = np.full(len(df_candidates), 0.5)
462
  sniper_instance = self.proc.sniper
 
463
  if sniper_instance and getattr(sniper_instance, 'models', []):
464
  try:
 
465
  required_features = getattr(sniper_instance, 'feature_names', [])
466
  if not required_features and hasattr(sniper_instance.models[0], 'feature_name'):
467
  required_features = sniper_instance.models[0].feature_name()
468
 
 
469
  source_cols = df_sniper_feats.columns
470
  def normalize_name(s): return s.lower().replace('_', '').replace('-', '').replace(' ', '')
471
  col_map_lower = {col.lower(): col for col in source_cols}
472
  col_map_fuzzy = {normalize_name(col): col for col in source_cols}
473
 
 
474
  X_final = pd.DataFrame(index=df_candidates.index)
 
 
 
475
  if required_features:
476
  for req_feat in required_features:
477
  req_norm = normalize_name(req_feat)
478
+ if req_feat in source_cols: X_final[req_feat] = df_sniper_feats.loc[df_candidates.index, req_feat]
479
+ elif req_feat.lower() in col_map_lower: X_final[req_feat] = df_sniper_feats.loc[df_candidates.index, col_map_lower[req_feat.lower()]]
480
+ elif req_norm in col_map_fuzzy: X_final[req_feat] = df_sniper_feats.loc[df_candidates.index, col_map_fuzzy[req_norm]]
481
+ else: X_final[req_feat] = 0.0
482
+ else: X_final = df_sniper_feats.loc[df_candidates.index]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
 
 
484
  preds = []
485
  for m in sniper_instance.models:
486
  X_in = X_final.astype(np.float32)
487
  raw_p = self._extract_probs(self._smart_predict(m, X_in, "Sniper"))
488
  preds.append(raw_p)
 
489
  res_sniper = np.mean(preds, axis=0)
 
 
 
 
 
490
  except Exception as e:
491
  print(f"โŒ Sniper Inference Error: {e}")
492
  traceback.print_exc()
 
509
  hydra_models = getattr(self.proc.guardian_hydra, 'models', {})
510
  if hydra_models and 'crash' in hydra_models:
511
  try:
512
+ df_sniper_base = df_1m.copy()
513
  df_sniper_base['rsi_14'] = ta.rsi(df_sniper_base['close'], 14).fillna(50)
514
  df_sniper_base['atr'] = ta.atr(df_sniper_base['high'], df_sniper_base['low'], df_sniper_base['close'], 14).fillna(0)
515
+ df_sniper_base['rel_vol'] = rel_vol
516
 
517
  global_hydra_feats = np.column_stack([
518
  df_sniper_base['rsi_14'], df_sniper_base['rsi_14'], df_sniper_base['rsi_14'],
 
652
 
653
  final_bal = bal + alloc
654
  profit = final_bal - initial_capital
655
+
656
+ # --- Detailed Stats Calculation ---
657
  tot = len(log)
658
+ winning_trades = [x for x in log if x['pnl'] > 0]
659
+ losing_trades = [x for x in log if x['pnl'] <= 0]
660
 
661
+ win_count = len(winning_trades)
662
+ loss_count = len(losing_trades)
663
+ win_rate = (win_count/tot*100) if tot else 0
664
+
665
+ avg_win = np.mean([x['pnl'] for x in winning_trades]) if winning_trades else 0
666
+ avg_loss = np.mean([x['pnl'] for x in losing_trades]) if losing_trades else 0
667
+
668
+ gross_profit = sum([x['pnl'] for x in winning_trades])
669
+ gross_loss = abs(sum([x['pnl'] for x in losing_trades]))
670
+ profit_factor = (gross_profit / gross_loss) if gross_loss > 0 else 99.9
671
+
672
+ # Streaks
673
+ max_win_s = 0; max_loss_s = 0; curr_w = 0; curr_l = 0
674
+ for t in log:
675
+ if t['pnl'] > 0:
676
+ curr_w += 1; curr_l = 0
677
+ if curr_w > max_win_s: max_win_s = curr_w
678
+ else:
679
+ curr_l += 1; curr_w = 0
680
+ if curr_l > max_loss_s: max_loss_s = curr_l
681
+
682
  cons_trades = [x for x in log if x['consensus']]
683
  n_cons = len(cons_trades)
684
  agree_rate = (n_cons/tot*100) if tot else 0
 
688
  res.append({
689
  'config': cfg, 'final_balance': final_bal, 'net_profit': profit,
690
  'total_trades': tot, 'win_rate': win_rate, 'max_drawdown': 0,
691
+ 'win_count': win_count, 'loss_count': loss_count,
692
+ 'avg_win': avg_win, 'avg_loss': avg_loss,
693
+ 'max_win_streak': max_win_s, 'max_loss_streak': max_loss_s,
694
+ 'profit_factor': profit_factor,
695
  'consensus_agreement_rate': agree_rate,
696
  'high_consensus_win_rate': cons_win_rate,
697
  'high_consensus_avg_pnl': cons_avg_pnl
 
717
  results_list.sort(key=lambda x: x['net_profit'], reverse=True)
718
  best = results_list[0]
719
 
720
+ # --- AUTO-DIAGNOSIS LOGIC ---
721
+ diag = []
722
+ if best['total_trades'] > 2000 and best['net_profit'] < 10:
723
+ diag.append("โš ๏ธ Overtrading: Too many trades for low profit.")
724
+ if best['win_rate'] > 55 and best['net_profit'] < 0:
725
+ diag.append("โš ๏ธ Fee Burn: High win rate but fees are eating profits.")
726
+ if abs(best['avg_loss']) > best['avg_win']:
727
+ diag.append("โš ๏ธ Risk/Reward Inversion: Avg Loss > Avg Win.")
728
+ if best['max_loss_streak'] > 10:
729
+ diag.append("โš ๏ธ Consecutive Loss Risk: Strategy prone to streak failures.")
730
+ if not diag: diag.append("โœ… System Healthy.")
731
+
732
  print("\n" + "="*60)
733
  print(f"๐Ÿ† CHAMPION REPORT [{target_regime}]:")
734
  print(f" ๐Ÿ’ฐ Final Balance: ${best['final_balance']:,.2f}")
 
736
  print("-" * 60)
737
  print(f" ๐Ÿ“Š Total Trades: {best['total_trades']}")
738
  print(f" ๐Ÿ“ˆ Win Rate: {best['win_rate']:.1f}%")
739
+ print(f" โœ… Winning Trades: {best['win_count']} (Avg: {best['avg_win']*100:.2f}%)")
740
+ print(f" โŒ Losing Trades: {best['loss_count']} (Avg: {best['avg_loss']*100:.2f}%)")
741
+ print(f" ๐ŸŒŠ Max Streaks: Win {best['max_win_streak']} | Loss {best['max_loss_streak']}")
742
+ print(f" โš–๏ธ Profit Factor: {best['profit_factor']:.2f}")
743
  print("-" * 60)
744
  print(f" ๐Ÿง  CONSENSUS ANALYTICS:")
745
  print(f" ๐Ÿค Model Agreement Rate: {best['consensus_agreement_rate']:.1f}%")
746
  print(f" ๐ŸŒŸ High-Consensus Win Rate: {best['high_consensus_win_rate']:.1f}%")
 
747
  print("-" * 60)
748
+ print(f" ๐Ÿฉบ DIAGNOSIS: {' '.join(diag)}")
749
  print(f" โš™๏ธ Oracle={best['config']['oracle_thresh']:.2f} | Sniper={best['config']['sniper_thresh']:.2f} | Hydra={best['config']['hydra_thresh']:.2f}")
750
  print("="*60)
751
  return best['config'], best