Riy777 commited on
Commit
eab27d3
·
verified ·
1 Parent(s): a209eab

Update backtest_engine.py

Browse files
Files changed (1) hide show
  1. backtest_engine.py +547 -624
backtest_engine.py CHANGED
@@ -1,5 +1,5 @@
1
  # ============================================================
2
- # 🧪 backtest_engine.py (V132.5 - GEM-Architect: Full Diagnostics)
3
  # ============================================================
4
 
5
  import asyncio
@@ -10,15 +10,13 @@ import time
10
  import logging
11
  import itertools
12
  import os
13
- import glob
14
  import gc
15
  import sys
16
  import traceback
17
- from numpy.lib.stride_tricks import sliding_window_view
18
  from datetime import datetime, timezone
19
  from typing import Dict, Any, List
 
20
 
21
- # ✅ استيراد المحركات
22
  try:
23
  from ml_engine.processor import MLProcessor, SystemLimits
24
  from ml_engine.data_manager import DataManager
@@ -26,303 +24,22 @@ try:
26
  from r2 import R2Service
27
  import ccxt.async_support as ccxt
28
  import xgboost as xgb
 
29
  except ImportError:
30
- print("❌ [Import Error] Critical ML modules missing.")
31
  pass
32
 
33
  logging.getLogger('ml_engine').setLevel(logging.WARNING)
34
  CACHE_DIR = "backtest_real_scores"
35
 
36
- # ============================================================
37
- # 🛡️ GLOBAL SANITIZATION & HELPERS
38
- # ============================================================
39
-
40
- def sanitize_features(df):
41
- """
42
- Cleans DataFrame from Infinity and NaNs to prevent crashes.
43
- """
44
- if df is None or df.empty: return df
45
- return df.replace([np.inf, -np.inf], np.nan).fillna(0.0)
46
-
47
- def _zv(x):
48
- with np.errstate(divide='ignore', invalid='ignore'):
49
- x = np.asarray(x, dtype="float32")
50
- m = np.nanmean(x, axis=0)
51
- s = np.nanstd(x, axis=0) + 1e-9
52
- return np.nan_to_num((x - m) / s, nan=0.0)
53
-
54
- def _z_score_rolling(x, w=500):
55
- r = x.rolling(w).mean()
56
- s = x.rolling(w).std().replace(0, np.nan)
57
- return ((x - r) / s).fillna(0)
58
-
59
- # ============================================================
60
- # 🔧 1. FEATURE ENGINEERING
61
- # ============================================================
62
-
63
- def _transform_window_for_pattern(df_window):
64
- try:
65
- c = df_window['close'].values.astype('float32')
66
- o = df_window['open'].values.astype('float32')
67
- h = df_window['high'].values.astype('float32')
68
- l = df_window['low'].values.astype('float32')
69
- v = df_window['volume'].values.astype('float32')
70
-
71
- base = np.stack([o, h, l, c, v], axis=1)
72
- base_z = _zv(base)
73
-
74
- lr = np.zeros_like(c); lr[1:] = np.diff(np.log1p(c))
75
- rng = (h - l) / (c + 1e-9)
76
- extra = np.stack([lr, rng], axis=1)
77
- extra_z = _zv(extra)
78
-
79
- def _ema(arr, n):
80
- return pd.Series(arr).ewm(span=n, adjust=False).mean().values
81
-
82
- ema9 = _ema(c, 9); ema21 = _ema(c, 21); ema50 = _ema(c, 50); ema200 = _ema(c, 200)
83
- slope21 = np.gradient(ema21); slope50 = np.gradient(ema50)
84
-
85
- delta = np.diff(c, prepend=c[0])
86
- up, down = delta.copy(), delta.copy()
87
- up[up < 0] = 0; down[down > 0] = 0
88
- roll_up = pd.Series(up).ewm(alpha=1/14, adjust=False).mean().values
89
- roll_down = pd.Series(down).abs().ewm(alpha=1/14, adjust=False).mean().values
90
- rs = roll_up / (roll_down + 1e-9)
91
- rsi = 100.0 - (100.0 / (1.0 + rs))
92
-
93
- indicators = np.stack([ema9, ema21, ema50, ema200, slope21, slope50, rsi], axis=1)
94
- padding = np.zeros((200, 5), dtype='float32')
95
- indicators_full = np.concatenate([indicators, padding], axis=1)
96
- indicators_z = _zv(indicators_full)
97
-
98
- X_seq = np.concatenate([base_z, extra_z, indicators_z], axis=1)
99
- X_flat = X_seq.flatten()
100
- X_stat = np.array([0.5, 0.0, 0.5], dtype="float32")
101
-
102
- return np.concatenate([X_flat, X_stat])
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:
171
- df['MACD'] = macd.iloc[:, 0]; df['MACD_h'] = macd.iloc[:, 1]
172
- else: df['MACD'] = 0.0; df['MACD_h'] = 0.0
173
-
174
- df['CCI'] = ta.cci(df['high'], df['low'], df['close'], 20)
175
- adx = ta.adx(df['high'], df['low'], df['close'], 14)
176
- if adx is not None: df['ADX'] = adx.iloc[:, 0]
177
- else: df['ADX'] = 0.0
178
-
179
- for p in [9, 21, 50, 200]:
180
- ema = ta.ema(df['close'], p)
181
- if ema is not None:
182
- df[f'EMA_{p}_dist'] = (df['close'] / ema.replace(0, np.nan)) - 1
183
- else: df[f'EMA_{p}_dist'] = 0.0
184
-
185
- bb = ta.bbands(df['close'], 20, 2.0)
186
- if bb is not None:
187
- df['BB_w'] = (bb.iloc[:, 2] - bb.iloc[:, 0]) / bb.iloc[:, 1].replace(0, np.nan)
188
- df['BB_p'] = (df['close'] - bb.iloc[:, 0]) / (bb.iloc[:, 2] - bb.iloc[:, 0]).replace(0, np.nan)
189
- else: df['BB_w'] = 0.0; df['BB_p'] = 0.0
190
-
191
- df['MFI'] = ta.mfi(df['high'], df['low'], df['close'], df['volume'], 14)
192
- vwap = ta.vwap(df['high'], df['low'], df['close'], df['volume'])
193
- if vwap is not None: df['VWAP_dist'] = (df['close'] / vwap.replace(0, np.nan)) - 1
194
- else: df['VWAP_dist'] = 0.0
195
-
196
- return sanitize_features(df)
197
-
198
- # ============================================================
199
- # 🔧 LEGACY GUARD (V2 & V3)
200
- # ============================================================
201
- def calculate_legacy_v2_vectorized(df_1m, df_5m, df_15m):
202
- try:
203
- def calc_basic(df, suffix):
204
- c = df['close']; h = df['high']; l = df['low']
205
- res = pd.DataFrame(index=df.index)
206
- res[f'log_ret_{suffix}'] = np.log(c / c.shift(1).replace(0, np.nan)).fillna(0)
207
- res[f'rsi_{suffix}'] = (ta.rsi(c, 14) / 100.0).fillna(0.5)
208
- roll_max = h.rolling(50).max(); roll_min = l.rolling(50).min()
209
- diff = (roll_max - roll_min).replace(0, 1e-9)
210
- res[f'fib_pos_{suffix}'] = ((c - roll_min) / diff).fillna(0.5)
211
- if suffix == '1m':
212
- res[f'volatility_{suffix}'] = (ta.atr(h, l, c, 14) / c.replace(0, np.nan)).fillna(0)
213
- else:
214
- ema = ta.ema(c, 20)
215
- if ema is not None:
216
- res[f'trend_slope_{suffix}'] = ((ema - ema.shift(5)) / ema.shift(5).replace(0, np.nan)).fillna(0)
217
- else: res[f'trend_slope_{suffix}'] = 0.0
218
- if suffix == '15m':
219
- fib618 = roll_max - (diff * 0.382)
220
- res[f'dist_fib618_{suffix}'] = ((c - fib618) / c.replace(0, np.nan)).fillna(0)
221
- return res
222
-
223
- f1 = calc_basic(df_1m, '1m')
224
- f5 = calc_basic(df_5m, '5m').reindex(df_1m.index, method='ffill')
225
- f15 = calc_basic(df_15m, '15m').reindex(df_1m.index, method='ffill')
226
-
227
- FEATS_1M = ['log_ret_1m', 'rsi_1m', 'fib_pos_1m', 'volatility_1m']
228
- FEATS_5M = ['log_ret_5m', 'rsi_5m', 'fib_pos_5m', 'trend_slope_5m']
229
- FEATS_15M = ['log_ret_15m', 'rsi_15m', 'dist_fib618_15m', 'trend_slope_15m']
230
-
231
- parts = [f1[FEATS_1M], f5[FEATS_5M], f15[FEATS_15M]]
232
- lags = [1, 2, 3, 5, 10, 20]
233
- for lag in lags: parts.append(f1[FEATS_1M].shift(lag).fillna(0))
234
-
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',
247
- 'obv', 'obv_slope', 'cmf', 'log_ret', 'mc_skew', 'mc_kurt',
248
- 'mc_prob_gain', 'mc_var_95', 'mc_shock']
249
- for t in targets: d[t] = 0.0
250
-
251
- c = d['close']; h = d['high']; l = d['low']; v = d['volume']
252
- try:
253
- d['log_ret'] = np.log(c / c.shift(1).replace(0, np.nan)).fillna(0)
254
- d['rsi'] = ta.rsi(c, 14).fillna(50)
255
- d['rsi_slope'] = (d['rsi'] - d['rsi'].shift(3).fillna(50)) / 3
256
- macd = ta.macd(c)
257
- if macd is not None:
258
- d['macd_h'] = macd.iloc[:, 1].fillna(0)
259
- d['macd_h_slope'] = (d['macd_h'] - d['macd_h'].shift(3).fillna(0)) / 3
260
- adx = ta.adx(h, l, c, 14)
261
- if adx is not None:
262
- d['adx'] = adx.iloc[:, 0].fillna(0); d['dmp'] = adx.iloc[:, 1].fillna(0); d['dmn'] = adx.iloc[:, 2].fillna(0)
263
- d['trend_net_force'] = (d['dmp'] - d['dmn']) * (d['adx'] / 100.0)
264
- d['ema_20'] = ta.ema(c, 20).fillna(c); d['ema_50'] = ta.ema(c, 50).fillna(c); d['ema_200'] = ta.ema(c, 200).fillna(c)
265
- d['dist_ema20'] = (c - d['ema_20']) / d['ema_20'].replace(0, np.nan)
266
- d['dist_ema50'] = (c - d['ema_50']) / d['ema_50'].replace(0, np.nan)
267
- d['dist_ema200'] = (c - d['ema_200']) / d['ema_200'].replace(0, np.nan)
268
- d['slope_ema50'] = (d['ema_50'] - d['ema_50'].shift(5).fillna(0)) / d['ema_50'].shift(5).replace(0, np.nan)
269
- d['atr'] = ta.atr(h, l, c, 14).fillna(0); d['atr_rel'] = d['atr'] / c.replace(0, np.nan)
270
- d['obv'] = ta.obv(c, v).fillna(0); d['obv_slope'] = d['obv'] - d['obv'].shift(5).fillna(0)
271
- d['cmf'] = ta.cmf(h, l, c, v, 20).fillna(0)
272
-
273
- win = 30; roll = d['log_ret'].rolling(win)
274
- d['mc_skew'] = roll.skew().fillna(0); d['mc_kurt'] = roll.kurt().fillna(0)
275
- d['mc_prob_gain'] = (d['log_ret'] > 0).rolling(win).mean().fillna(0.5)
276
- d['mc_var_95'] = roll.quantile(0.05).fillna(-0.02)
277
- d['mc_shock'] = ((d['log_ret'] - roll.mean()) / (roll.std().replace(0, np.nan))).fillna(0)
278
- except: pass
279
-
280
- if prefix:
281
- d.columns = [f"{col}_{prefix}" if col not in ['timestamp'] else col for col in d.columns]
282
- return sanitize_features(d)
283
- return sanitize_features(d)
284
-
285
- df1 = calc_v3_base(df_1m); df5 = calc_v3_base(df_5m, "5m").reindex(df_1m.index, method='ffill')
286
- df15 = calc_v3_base(df_15m, "15m").reindex(df_1m.index, method='ffill')
287
-
288
- final_df = pd.DataFrame(index=df_1m.index)
289
- for i, col_name in enumerate(["6", "7", "8", "9", "10", "11"], 1):
290
- final_df[col_name] = df1['log_ret'].shift(i)
291
-
292
- cols_1m = ['rsi', 'rsi_slope', 'macd_h', 'macd_h_slope', 'adx', 'dmp', 'dmn', 'trend_net_force',
293
- 'ema_20', 'ema_50', 'ema_200', 'dist_ema20', 'dist_ema50', 'dist_ema200', 'slope_ema50',
294
- 'atr', 'atr_rel', 'obv', 'obv_slope', 'cmf', 'log_ret', 'mc_skew', 'mc_kurt',
295
- 'mc_prob_gain', 'mc_var_95', 'mc_shock']
296
- for c in cols_1m: final_df[c] = df1[c]
297
-
298
- cols_5m = {'rsi_5m': 'rsi_5m', 'rsi_slope_5m': 'rsi_slope_5m', 'macd_h_5m': 'macd_h_5m',
299
- 'mc_prob_gain_5m': 'mc_prob_gain_5m', 'mc_shock_5m': 'mc_shock_5m'}
300
- for k, v in cols_5m.items(): final_df[k] = df5[v]
301
-
302
- cols_15m = {'rsi_15m': 'rsi_15m', 'macd_h_15m': 'macd_h_15m', 'trend_net_force_15m': 'trend_net_force_15m',
303
- 'mc_prob_gain_15m': 'mc_prob_gain_15m', 'dist_ema200_15m': 'dist_ema200_15m'}
304
- for k, v in cols_15m.items(): final_df[k] = df15[v]
305
-
306
- expected = ["6", "7", "8", "9", "10", "11", "rsi", "rsi_slope", "macd_h", "macd_h_slope", "adx", "dmp", "dmn",
307
- "trend_net_force", "ema_20", "ema_50", "ema_200", "dist_ema20", "dist_ema50", "dist_ema200",
308
- "slope_ema50", "atr", "atr_rel", "obv", "obv_slope", "cmf", "log_ret", "mc_skew", "mc_kurt",
309
- "mc_prob_gain", "mc_var_95", "mc_shock", "rsi_5m", "rsi_slope_5m", "macd_h_5m", "mc_prob_gain_5m",
310
- "mc_shock_5m", "rsi_15m", "macd_h_15m", "trend_net_force_15m", "mc_prob_gain_15m", "dist_ema200_15m"]
311
-
312
- return sanitize_features(final_df.reindex(columns=expected, fill_value=0.0))
313
- except: return None
314
-
315
- # ============================================================
316
- # 🧪 THE BACKTESTER
317
- # ============================================================
318
  class HeavyDutyBacktester:
319
  def __init__(self, data_manager, processor):
320
  self.dm = data_manager
321
  self.proc = processor
322
- self.GRID_DENSITY = 10
323
  self.INITIAL_CAPITAL = 10.0
324
  self.TRADING_FEES = 0.001
325
  self.MAX_SLOTS = 4
 
326
  self.TARGET_COINS = [
327
  'SOL/USDT', 'XRP/USDT', 'DOGE/USDT', 'ADA/USDT', 'AVAX/USDT', 'LINK/USDT',
328
  'TON/USDT', 'INJ/USDT', 'APT/USDT', 'OP/USDT', 'ARB/USDT', 'SUI/USDT',
@@ -334,379 +51,606 @@ class HeavyDutyBacktester:
334
  'STRK/USDT', 'BLUR/USDT', 'ALT/USDT', 'JUP/USDT', 'PENDLE/USDT', 'ETHFI/USDT',
335
  'MEME/USDT', 'ATOM/USDT'
336
  ]
337
- self.force_start_date = None; self.force_end_date = None
338
- if os.path.exists(CACHE_DIR):
339
- for f in glob.glob(os.path.join(CACHE_DIR, "*")): os.remove(f)
340
- else: os.makedirs(CACHE_DIR)
341
- self._check_engines()
342
-
343
- def _check_engines(self):
344
- status = []
345
- if self.proc.titan and self.proc.titan.model: status.append("Titan")
346
- if self.proc.pattern_engine and self.proc.pattern_engine.models: status.append("Patterns")
347
- if self.proc.oracle: status.append("Oracle")
348
- if self.proc.sniper: status.append("Sniper")
349
- if self.proc.guardian_hydra: status.append("Hydra")
350
- if self.proc.guardian_legacy: status.append("Legacy")
351
- print(f" ✅ Engines Ready: {', '.join(status)}")
352
 
353
  def set_date_range(self, start_str, end_str):
354
- self.force_start_date = start_str; self.force_end_date = end_str
355
-
356
- def _smart_predict(self, model, X, model_name="Generic"):
357
- try:
358
- if hasattr(model, "predict_proba"):
359
- raw = model.predict_proba(X)
360
- if raw.ndim == 2: return raw[:, -1]
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):
367
- if isinstance(raw_preds, list): raw_preds = np.array(raw_preds)
368
- if raw_preds.ndim == 1: return raw_preds
369
- elif raw_preds.ndim == 2:
370
- if raw_preds.shape[1] >= 2: return raw_preds[:, -1]
371
- return raw_preds.flatten()
372
- return raw_preds.flatten()
373
 
 
 
 
374
  async def _fetch_all_data_fast(self, sym, start_ms, end_ms):
375
  print(f" ⚡ [Network] Downloading {sym}...", flush=True)
376
- limit = 1000; duration = limit * 60 * 1000
377
- tasks = []; curr = start_ms
378
- while curr < end_ms: tasks.append(curr); curr += duration
379
- all_c = []; sem = asyncio.Semaphore(20)
380
- async def _fetch(ts):
 
 
 
 
 
 
381
  async with sem:
382
- try: return await self.dm.exchange.fetch_ohlcv(sym, '1m', since=ts, limit=limit)
383
- except: await asyncio.sleep(0.5); return []
384
- chunk = 50
385
- for i in range(0, len(tasks), chunk):
386
- res = await asyncio.gather(*[_fetch(t) for t in tasks[i:i+chunk]])
387
- for r in res: all_c.extend(r)
388
- seen = set(); unique = []
389
- for c in all_c:
390
- if c[0] not in seen and c[0] >= start_ms and c[0] <= end_ms:
391
- unique.append(c); seen.add(c[0])
392
- unique.sort(key=lambda x: x[0])
393
- print(f" ✅ Downloaded {len(unique)} candles.", flush=True)
394
- return unique
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
396
  async def _process_data_in_memory(self, sym, candles, start_ms, end_ms):
397
  safe_sym = sym.replace('/', '_')
398
- scores_file = f"{CACHE_DIR}/{safe_sym}_{start_ms}_{end_ms}_scores.pkl"
 
 
399
  if os.path.exists(scores_file):
400
  print(f" 📂 [{sym}] Data Exists -> Skipping.")
401
  return
402
 
403
- print(f" ⚙️ [CPU] Analyzing {sym} (ALL REAL MODELS)...", flush=True)
404
  t0 = time.time()
405
 
406
  df_1m = pd.DataFrame(candles, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
407
- for c in ['open', 'high', 'low', 'close', 'volume']: df_1m[c] = df_1m[c].astype(float)
408
  df_1m['datetime'] = pd.to_datetime(df_1m['timestamp'], unit='ms')
409
  df_1m.set_index('datetime', inplace=True)
410
  df_1m = df_1m.sort_index()
411
 
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
 
420
- valid_mask = (l1_score >= 5.0) & (np.arange(len(df_1m)) > 500)
421
- df_candidates = df_1m[valid_mask].copy()
422
- if df_candidates.empty: return
423
-
424
- print(f" 🎯 Candidates: {len(df_candidates)}. Running Deep Inference...", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
 
426
- # 2. Patterns
427
- res_patterns = np.full(len(df_candidates), 0.5)
428
- pattern_models = getattr(self.proc.pattern_engine, 'models', {})
429
- if pattern_models and '15m' in pattern_models:
430
- try:
431
- df_15m_res = df_1m.resample('15T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
432
- pat_scores_15m = np.full(len(df_15m_res), 0.5)
433
- pat_inputs = []; valid_15m_idxs = []
434
- for i in range(200, len(df_15m_res)):
435
- window = df_15m_res.iloc[i-200:i]
436
- vec = _transform_window_for_pattern(window)
437
- if vec is not None:
438
- pat_inputs.append(vec); valid_15m_idxs.append(i)
439
- if pat_inputs:
440
- X_pat = np.array(pat_inputs)
441
- pat_preds = self._smart_predict(pattern_models['15m'], xgb.DMatrix(X_pat), "Pattern")
442
- pat_scores_15m[valid_15m_idxs] = pat_preds
443
- ts_15m = df_15m_res.index.astype(np.int64) // 10**6
444
- map_idxs = np.searchsorted(ts_15m, df_candidates['timestamp'].values) - 1
445
- res_patterns = pat_scores_15m[np.clip(map_idxs, 0, len(pat_scores_15m)-1)]
446
- except Exception as e: print(f"Patterns Error: {e}")
447
-
448
- # 3. Titan
449
- res_titan = np.full(len(df_candidates), 0.5)
450
- if self.proc.titan and self.proc.titan.model:
451
  try:
452
- df_5m_feat = calculate_titan_features_real(df_5m).add_prefix('5m_')
453
- ts_5m = df_5m.index.astype(np.int64) // 10**6
454
- map_idxs = np.clip(np.searchsorted(ts_5m, df_candidates['timestamp'].values) - 1, 0, len(df_5m_feat)-1)
455
- feats = self.proc.titan.feature_names
456
- X_titan_df = sanitize_features(df_5m_feat.iloc[map_idxs].reindex(columns=feats, fill_value=0))
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()
493
-
494
- # 5. Oracle
495
- res_oracle = np.full(len(df_candidates), 0.5)
496
- oracle_model = getattr(self.proc.oracle, 'model_direction', None)
497
- if oracle_model:
498
  try:
499
- oracle_feats = getattr(self.proc.oracle, 'feature_cols', [])
500
- X_orc_df = pd.DataFrame(0.0, index=range(len(df_candidates)), columns=oracle_feats)
501
- if 'sim_titan_score' in X_orc_df: X_orc_df['sim_titan_score'] = res_titan
502
- if 'sim_pattern_score' in X_orc_df: X_orc_df['sim_pattern_score'] = res_patterns
503
- if 'sim_mc_score' in X_orc_df: X_orc_df['sim_mc_score'] = 0.5
504
- res_oracle = self._extract_probs(self._smart_predict(oracle_model, X_orc_df.values, "Oracle"))
 
 
 
 
 
 
 
 
 
505
  except Exception as e: print(f"Oracle Error: {e}")
506
 
507
- # 6. Hydra
508
- res_hydra_risk = np.zeros(len(df_candidates))
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'],
519
- (df_sniper_base['close']-df_sniper_base['close'].rolling(20).mean())/df_sniper_base['close'],
520
- df_sniper_base['rel_vol'], df_sniper_base['atr'], df_sniper_base['close']
521
- ]).astype(np.float32)
 
 
 
 
522
 
523
- global_hydra_feats = np.nan_to_num(global_hydra_feats, nan=0.0, posinf=0.0, neginf=0.0)
524
- window_view = sliding_window_view(global_hydra_feats, 240, axis=0).transpose(0, 2, 1)
525
- c_idxs = np.searchsorted(df_1m.index, df_candidates.index)
526
- valid_s = c_idxs + 1
527
- valid_mask_h = valid_s < (len(global_hydra_feats) - 240)
528
- final_s = valid_s[valid_mask_h]; res_idxs = np.where(valid_mask_h)[0]
529
 
530
- for i in range(0, len(final_s), 5000):
531
- b_idxs = final_s[i:i+5000]; r_idxs = res_idxs[i:i+5000]
532
- static = window_view[b_idxs]
533
- B = len(b_idxs)
534
- entry = df_1m['close'].values[b_idxs-1].reshape(B, 1)
535
- s_c = static[:, 6, :]; s_atr = static[:, 5, :]
536
- dist = np.maximum(1.5*s_atr, entry*0.015) + 1e-9
537
- pnl = (s_c - entry)/dist
538
- max_pnl = (np.maximum.accumulate(s_c, axis=1) - entry)/dist
539
- atr_p = s_atr/(s_c+1e-9)
540
- zeros = np.zeros((B, 240)); ones = np.ones((B, 240)); t = np.tile(np.arange(1, 241), (B, 1))
541
- X = np.stack([
542
- static[:,0], static[:,1], static[:,2], static[:,3], static[:,4],
543
- zeros, atr_p, pnl, max_pnl, zeros, zeros, t, zeros, ones*0.6, ones*0.7, ones*3
544
- ], axis=2).reshape(-1, 16)
545
- X = np.nan_to_num(X, nan=0.0, posinf=0.0, neginf=0.0)
546
- preds = hydra_models['crash'].predict_proba(X)[:, 1].reshape(B, 240)
547
- res_hydra_risk[r_idxs] = np.max(preds, axis=1)
548
  except: pass
549
 
550
- # 7. Legacy
551
- res_legacy_v2 = np.zeros(len(df_candidates))
552
- res_legacy_v3 = np.zeros(len(df_candidates))
553
- if self.proc.guardian_legacy:
554
  try:
555
- X_v2_full = calculate_legacy_v2_vectorized(df_1m, df_5m, df_15m)
556
- v3_df_full = calculate_legacy_v3_vectorized(df_1m, df_5m, df_15m)
557
- all_indices = np.arange(len(df_1m))
558
- cand_indices = all_indices[valid_mask]
559
- max_len = len(df_1m)
560
- cand_indices = cand_indices[cand_indices < max_len]
561
- if len(cand_indices) > 0:
562
- if self.proc.guardian_legacy.model_v2 and X_v2_full is not None:
563
- subset_v2 = X_v2_full[cand_indices]
564
- preds_v2 = self.proc.guardian_legacy.model_v2.predict(xgb.DMatrix(subset_v2))
565
- if len(preds_v2.shape) > 1: res_legacy_v2[:len(cand_indices)] = preds_v2[:, 2]
566
- else: res_legacy_v2[:len(cand_indices)] = preds_v2
567
- if self.proc.guardian_legacy.model_v3 and v3_df_full is not None:
568
- subset_v3_df = v3_df_full.iloc[cand_indices]
569
- preds_v3 = self.proc.guardian_legacy.model_v3.predict(xgb.DMatrix(subset_v3_df))
570
- res_legacy_v3[:len(cand_indices)] = preds_v3
571
- except Exception as e: print(f"Legacy Error: {e}")
572
-
573
- # 8. Assembly
574
- print(f" 📊 [Stats] Titan:{res_titan.mean():.2f} | Patterns:{res_patterns.mean():.2f} | Sniper:{res_sniper.mean():.2f} | Oracle:{res_oracle.mean():.2f}")
575
- ai_df = pd.DataFrame({
576
- 'timestamp': df_candidates['timestamp'],
577
- 'symbol': sym,
578
- 'close': df_candidates['close'],
579
- 'real_titan': res_titan,
580
- 'oracle_conf': res_oracle,
581
- 'sniper_score': res_sniper,
582
- 'l1_score': l1_score[valid_mask],
583
- 'risk_hydra_crash': res_hydra_risk,
584
- 'risk_legacy_v2': res_legacy_v2,
585
- 'risk_legacy_v3': res_legacy_v3
586
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
587
  dt = time.time() - t0
588
- if not ai_df.empty:
589
- ai_df.to_pickle(scores_file)
590
- print(f" ✅ [{sym}] Completed {len(ai_df)} signals in {dt:.2f} seconds.", flush=True)
 
 
 
 
591
  gc.collect()
592
 
 
 
 
593
  async def generate_truth_data(self):
594
- if self.force_start_date:
595
- dt_s = datetime.strptime(self.force_start_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
596
- dt_e = datetime.strptime(self.force_end_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
597
- ms_s = int(dt_s.timestamp()*1000); ms_e = int(dt_e.timestamp()*1000)
 
598
  print(f"\n🚜 [Phase 1] Processing Era: {self.force_start_date} -> {self.force_end_date}")
599
- for sym in self.TARGET_COINS:
600
- c = await self._fetch_all_data_fast(sym, ms_s, ms_e)
601
- if c: await self._process_data_in_memory(sym, c, ms_s, ms_e)
 
 
 
 
 
602
 
603
  @staticmethod
604
  def _worker_optimize(combinations_batch, scores_files, initial_capital, fees_pct, max_slots):
605
- print(f" ⏳ [System] Loading {len(scores_files)} datasets...", flush=True)
606
- data = []
607
- for f in scores_files:
608
- try: data.append(pd.read_pickle(f))
 
 
609
  except: pass
610
- if not data: return []
611
- df = pd.concat(data).sort_values('timestamp')
 
 
612
 
613
- ts = df['timestamp'].values; close = df['close'].values.astype(float)
614
- sym = df['symbol'].values; sym_map = {s:i for i,s in enumerate(np.unique(sym))}
615
- sym_id = np.array([sym_map[s] for s in sym])
616
-
617
- oracle = df['oracle_conf'].values; sniper = df['sniper_score'].values
618
- hydra = df['risk_hydra_crash'].values; titan = df['real_titan'].values
619
- l1 = df['l1_score'].values
620
- legacy_v2 = df['risk_legacy_v2'].values; legacy_v3 = df['risk_legacy_v3'].values
621
-
622
- N = len(ts)
623
- print(f" 🚀 [System] Testing {len(combinations_batch)} configs on {N} candles...", flush=True)
624
-
625
- res = []
626
- for cfg in combinations_batch:
627
- pos = {}; log = []
628
- bal = initial_capital; alloc = 0.0
629
- mask = (l1 >= cfg['l1_thresh']) & (oracle >= cfg['oracle_thresh']) & (sniper >= cfg['sniper_thresh'])
630
-
631
- for i in range(N):
632
- s = sym_id[i]; p = close[i]
633
- if s in pos:
634
- entry = pos[s][0]; h_r = pos[s][1]; titan_entry = pos[s][3]
635
- crash_hydra = (h_r > cfg['hydra_thresh'])
636
- panic_legacy = (legacy_v2[i] > cfg['legacy_thresh']) or (legacy_v3[i] > cfg['legacy_thresh'])
637
- pnl = (p - entry)/entry
638
-
639
- if crash_hydra or panic_legacy or pnl > 0.04 or pnl < -0.02:
640
- realized = pnl - fees_pct*2
641
- bal += pos[s][2] * (1 + realized)
642
- alloc -= pos[s][2]
643
- is_consensus = (titan_entry > 0.55)
644
- log.append({'pnl': realized, 'consensus': is_consensus})
645
- del pos[s]
646
-
647
- if len(pos) < max_slots and mask[i]:
648
- if s not in pos and bal >= 5.0:
649
- size = min(10.0, bal * 0.98)
650
- pos[s] = (p, hydra[i], size, titan[i])
651
- bal -= size; alloc += size
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
685
- cons_win_rate = (sum(1 for x in cons_trades if x['pnl']>0)/n_cons*100) if n_cons else 0
686
- cons_avg_pnl = (sum(x['pnl'] for x in cons_trades)/n_cons*100) if n_cons else 0
687
-
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
698
  })
699
- return res
 
700
 
701
  async def run_optimization(self, target_regime="RANGE"):
702
  await self.generate_truth_data()
703
- oracle_r = np.linspace(0.3, 0.7, 3); sniper_r = np.linspace(0.2, 0.6, 3)
704
- hydra_r = [0.8, 0.9]; l1_r = [5.0, 10.0]
705
 
706
  combos = []
707
- for o, s, h, l1 in itertools.product(oracle_r, sniper_r, hydra_r, l1_r):
708
  combos.append({
709
- 'w_titan': 0.4, 'w_struct': 0.3, 'thresh': l1, 'l1_thresh': l1,
710
  'oracle_thresh': o, 'sniper_thresh': s, 'hydra_thresh': h, 'legacy_thresh': 0.95
711
  })
712
 
@@ -717,18 +661,6 @@ class HeavyDutyBacktester:
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,22 +668,13 @@ class HeavyDutyBacktester:
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
752
 
753
  async def run_strategic_optimization_task():
754
- print("\n🧪 [STRATEGIC BACKTEST] Full Spectrum Mode...")
755
  r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
756
  try:
757
  await dm.initialize(); await proc.initialize()
 
1
  # ============================================================
2
+ # 🧪 backtest_engine.py (V134.0 - GEM-Architect: Feature Parity Edition)
3
  # ============================================================
4
 
5
  import asyncio
 
10
  import logging
11
  import itertools
12
  import os
 
13
  import gc
14
  import sys
15
  import traceback
 
16
  from datetime import datetime, timezone
17
  from typing import Dict, Any, List
18
+ from numpy.lib.stride_tricks import sliding_window_view
19
 
 
20
  try:
21
  from ml_engine.processor import MLProcessor, SystemLimits
22
  from ml_engine.data_manager import DataManager
 
24
  from r2 import R2Service
25
  import ccxt.async_support as ccxt
26
  import xgboost as xgb
27
+ import lightgbm as lgb
28
  except ImportError:
 
29
  pass
30
 
31
  logging.getLogger('ml_engine').setLevel(logging.WARNING)
32
  CACHE_DIR = "backtest_real_scores"
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  class HeavyDutyBacktester:
35
  def __init__(self, data_manager, processor):
36
  self.dm = data_manager
37
  self.proc = processor
38
+ self.GRID_DENSITY = 5
39
  self.INITIAL_CAPITAL = 10.0
40
  self.TRADING_FEES = 0.001
41
  self.MAX_SLOTS = 4
42
+
43
  self.TARGET_COINS = [
44
  'SOL/USDT', 'XRP/USDT', 'DOGE/USDT', 'ADA/USDT', 'AVAX/USDT', 'LINK/USDT',
45
  'TON/USDT', 'INJ/USDT', 'APT/USDT', 'OP/USDT', 'ARB/USDT', 'SUI/USDT',
 
51
  'STRK/USDT', 'BLUR/USDT', 'ALT/USDT', 'JUP/USDT', 'PENDLE/USDT', 'ETHFI/USDT',
52
  'MEME/USDT', 'ATOM/USDT'
53
  ]
54
+
55
+ self.force_start_date = None
56
+ self.force_end_date = None
57
+
58
+ if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
59
+ print(f"🧪 [Backtest V134.0] Feature Parity Mode (Exact Live System Logic).")
 
 
 
 
 
 
 
 
 
60
 
61
  def set_date_range(self, start_str, end_str):
62
+ self.force_start_date = start_str
63
+ self.force_end_date = end_str
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
+ # ==============================================================
66
+ # ⚡ FAST DATA DOWNLOADER
67
+ # ==============================================================
68
  async def _fetch_all_data_fast(self, sym, start_ms, end_ms):
69
  print(f" ⚡ [Network] Downloading {sym}...", flush=True)
70
+ limit = 1000
71
+ tasks = []
72
+ current = start_ms
73
+ duration_per_batch = limit * 60 * 1000
74
+ while current < end_ms:
75
+ tasks.append(current)
76
+ current += duration_per_batch
77
+ all_candles = []
78
+ sem = asyncio.Semaphore(15)
79
+
80
+ async def _fetch_batch(timestamp):
81
  async with sem:
82
+ for _ in range(3):
83
+ try:
84
+ return await self.dm.exchange.fetch_ohlcv(sym, '1m', since=timestamp, limit=limit)
85
+ except: await asyncio.sleep(0.5)
86
+ return []
87
+
88
+ chunk_size = 25
89
+ for i in range(0, len(tasks), chunk_size):
90
+ chunk_tasks = tasks[i:i + chunk_size]
91
+ futures = [_fetch_batch(ts) for ts in chunk_tasks]
92
+ results = await asyncio.gather(*futures)
93
+ for res in results:
94
+ if res: all_candles.extend(res)
95
+
96
+ if not all_candles: return None
97
+ df = pd.DataFrame(all_candles, columns=['timestamp', 'o', 'h', 'l', 'c', 'v'])
98
+ df.drop_duplicates('timestamp', inplace=True)
99
+ df = df[(df['timestamp'] >= start_ms) & (df['timestamp'] <= end_ms)]
100
+ df.sort_values('timestamp', inplace=True)
101
+ print(f" ✅ Downloaded {len(df)} candles.", flush=True)
102
+ return df.values.tolist()
103
+
104
+ # ==============================================================
105
+ # 🏎️ HELPER: Rolling Z-Score (For Sniper)
106
+ # ==============================================================
107
+ def _z_roll(self, x, w=500):
108
+ r = x.rolling(w).mean()
109
+ s = x.rolling(w).std().replace(0, np.nan)
110
+ return ((x - r) / s).fillna(0)
111
 
112
+ # ==============================================================
113
+ # 🏎️ VECTORIZED INDICATORS (EXACT MATCH TO LIVE SYSTEM)
114
+ # ==============================================================
115
+ def _calculate_indicators_vectorized(self, df, timeframe='1m'):
116
+ # 1. Clean Types
117
+ cols = ['close', 'high', 'low', 'volume', 'open']
118
+ for c in cols: df[c] = df[c].astype(np.float64) # Use float64 for precision match
119
+
120
+ # ---------------------------------------------------------
121
+ # 🧠 PART 1: TITAN FEATURES (Exact Replica of TitanEngine)
122
+ # ---------------------------------------------------------
123
+ # RSI
124
+ df['RSI'] = ta.rsi(df['close'], length=14).fillna(50)
125
+
126
+ # MACD
127
+ macd = ta.macd(df['close'])
128
+ if macd is not None:
129
+ df['MACD'] = macd.iloc[:, 0].fillna(0)
130
+ df['MACD_h'] = macd.iloc[:, 1].fillna(0)
131
+ else:
132
+ df['MACD'] = 0.0; df['MACD_h'] = 0.0
133
+
134
+ # CCI
135
+ df['CCI'] = ta.cci(df['high'], df['low'], df['close'], length=20).fillna(0)
136
+
137
+ # ADX
138
+ adx = ta.adx(df['high'], df['low'], df['close'], length=14)
139
+ if adx is not None: df['ADX'] = adx.iloc[:, 0].fillna(0)
140
+ else: df['ADX'] = 0.0
141
+
142
+ # EMAs & Distances
143
+ for p in [9, 21, 50, 200]:
144
+ ema = ta.ema(df['close'], length=p)
145
+ df[f'EMA_{p}_dist'] = ((df['close'] / ema) - 1).fillna(0)
146
+ df[f'ema{p}'] = ema # Keep raw for others
147
+
148
+ # Bollinger Bands (Width & %B)
149
+ bb = ta.bbands(df['close'], length=20, std=2.0)
150
+ if bb is not None:
151
+ # Width = (Upper - Lower) / Middle
152
+ df['BB_w'] = ((bb.iloc[:, 2] - bb.iloc[:, 0]) / bb.iloc[:, 1]).fillna(0)
153
+ # %B = (Price - Lower) / (Upper - Lower)
154
+ df['BB_p'] = ((df['close'] - bb.iloc[:, 0]) / (bb.iloc[:, 2] - bb.iloc[:, 0])).fillna(0)
155
+
156
+ # Helper for Hydra
157
+ df['bb_width'] = df['BB_w'] # Alias
158
+
159
+ # MFI
160
+ df['MFI'] = ta.mfi(df['high'], df['low'], df['close'], df['volume'], length=14).fillna(50)
161
+
162
+ # VWAP
163
+ vwap = ta.vwap(df['high'], df['low'], df['close'], df['volume'])
164
+ if vwap is not None:
165
+ df['VWAP_dist'] = ((df['close'] / vwap) - 1).fillna(0)
166
+ df['vwap'] = vwap
167
+ else:
168
+ df['VWAP_dist'] = 0.0
169
+ df['vwap'] = df['close']
170
+
171
+ # ATR (for others)
172
+ df['atr'] = ta.atr(df['high'], df['low'], df['close'], length=14).fillna(0)
173
+ df['atr_pct'] = df['atr'] / df['close']
174
+
175
+ # ---------------------------------------------------------
176
+ # 🎯 PART 2: SNIPER FEATURES (1m Only)
177
+ # ---------------------------------------------------------
178
+ if timeframe == '1m':
179
+ df['return_1m'] = df['close'].pct_change().fillna(0)
180
+ df['return_3m'] = df['close'].pct_change(3).fillna(0)
181
+ df['return_5m'] = df['close'].pct_change(5).fillna(0)
182
+ df['return_15m'] = df['close'].pct_change(15).fillna(0)
183
+
184
+ df['rsi_14'] = df['RSI'] # Alias
185
+
186
+ # Sniper specific derivations
187
+ df['ema_9_slope'] = ((df['ema9'] - df['ema9'].shift(1)) / df['ema9'].shift(1)).fillna(0)
188
+ df['ema_21_dist'] = df['EMA_21_dist'] # Reuse
189
+
190
+ # Z-Scores for Sniper
191
+ atr_100 = ta.atr(df['high'], df['low'], df['close'], length=100).fillna(0)
192
+ df['atr_z'] = self._z_roll(atr_100) # Mapped later
193
+
194
+ df['vol_zscore_50'] = self._z_roll(df['volume'], 50)
195
+
196
+ rng = (df['high'] - df['low']).replace(0, 1e-9)
197
+ df['candle_range'] = self._z_roll(rng, 500)
198
+ df['close_pos_in_range'] = ((df['close'] - df['low']) / rng).fillna(0.5)
199
+
200
+ # Liquidity Proxies
201
+ df['dollar_vol'] = df['close'] * df['volume']
202
+ amihud_raw = (df['return_1m'].abs() / df['dollar_vol'].replace(0, np.nan)).fillna(0)
203
+ df['amihud'] = self._z_roll(amihud_raw)
204
+
205
+ dp = df['close'].diff()
206
+ roll_cov = dp.rolling(64).cov(dp.shift(1))
207
+ roll_spread_raw = (2 * np.sqrt(np.maximum(0, -roll_cov)))
208
+ df['roll_spread'] = self._z_roll(roll_spread_raw)
209
+
210
+ sign = np.sign(df['close'].diff()).fillna(0)
211
+ signed_vol = sign * df['volume']
212
+ ofi_raw = signed_vol.rolling(30).sum()
213
+ df['ofi'] = self._z_roll(ofi_raw)
214
+
215
+ buy_vol = (sign > 0) * df['volume']
216
+ sell_vol = (sign < 0) * df['volume']
217
+ imb = (buy_vol.rolling(60).sum() - sell_vol.rolling(60).sum()).abs()
218
+ tot = df['volume'].rolling(60).sum()
219
+ df['vpin'] = (imb / tot.replace(0, np.nan)).fillna(0)
220
+
221
+ vwap_win = 20
222
+ v_short = (df['dollar_vol'].rolling(vwap_win).sum() / df['volume'].rolling(vwap_win).sum().replace(0, np.nan)).fillna(df['close'])
223
+ df['vwap_dev'] = self._z_roll(df['close'] - v_short)
224
+
225
+ rv_gk = ((np.log(df['high'] / df['low'])**2) / 2) - ((2 * np.log(2) - 1) * (np.log(df['close'] / df['open'])**2))
226
+ df['rv_gk'] = self._z_roll(rv_gk)
227
+
228
+ # L_Score approximation
229
+ df['L_score'] = (df['vol_zscore_50'] - df['amihud'] - df['roll_spread'] - df['rv_gk'].abs() - df['vwap_dev'].abs() + df['ofi']).fillna(0)
230
+
231
+ # ---------------------------------------------------------
232
+ # 🧠 PART 3: ORACLE / HYDRA / LEGACY EXTRAS
233
+ # ---------------------------------------------------------
234
+ df['slope'] = ta.slope(df['close'], length=7).fillna(0)
235
+ vol_mean = df['volume'].rolling(20).mean()
236
+ vol_std = df['volume'].rolling(20).std()
237
+ df['vol_z'] = ((df['volume'] - vol_mean) / (vol_std + 1e-9)).fillna(0)
238
+
239
+ df['rel_vol'] = df['volume'] / (df['volume'].rolling(50).mean() + 1e-9)
240
+
241
+ df['log_ret'] = np.log(df['close'] / df['close'].shift(1)).fillna(0)
242
+ roll_max = df['high'].rolling(50).max()
243
+ roll_min = df['low'].rolling(50).min()
244
+ diff = (roll_max - roll_min).replace(0, 1e-9)
245
+ df['fib_pos'] = ((df['close'] - roll_min) / diff).fillna(0.5)
246
+ df['trend_slope'] = ((df['ema20'] - df['ema20'].shift(5)) / df['ema20'].shift(5)).fillna(0)
247
+ df['volatility'] = (df['atr'] / df['close']).fillna(0)
248
+
249
+ fib618 = roll_max - (diff * 0.382)
250
+ df['dist_fib618'] = ((df['close'] - fib618) / df['close']).fillna(0)
251
+
252
+ # Legacy Lags
253
+ if timeframe == '1m':
254
+ for lag in [1, 2, 3, 5, 10, 20]:
255
+ df[f'log_ret_lag_{lag}'] = df['log_ret'].shift(lag).fillna(0)
256
+ df[f'rsi_lag_{lag}'] = (df['RSI'].shift(lag) / 100.0).fillna(0.5)
257
+ df[f'fib_pos_lag_{lag}'] = df['fib_pos'].shift(lag).fillna(0.5)
258
+ df[f'volatility_lag_{lag}'] = df['volatility'].shift(lag).fillna(0)
259
+
260
+ df.fillna(0, inplace=True)
261
+ return df
262
+
263
+ # ==============================================================
264
+ # 🧠 CPU PROCESSING (GLOBAL INFERENCE)
265
+ # ==============================================================
266
  async def _process_data_in_memory(self, sym, candles, start_ms, end_ms):
267
  safe_sym = sym.replace('/', '_')
268
+ period_suffix = f"{start_ms}_{end_ms}"
269
+ scores_file = f"{CACHE_DIR}/{safe_sym}_{period_suffix}_scores.pkl"
270
+
271
  if os.path.exists(scores_file):
272
  print(f" 📂 [{sym}] Data Exists -> Skipping.")
273
  return
274
 
275
+ print(f" ⚙️ [CPU] Analyzing {sym} (Global Inference)...", flush=True)
276
  t0 = time.time()
277
 
278
  df_1m = pd.DataFrame(candles, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
 
279
  df_1m['datetime'] = pd.to_datetime(df_1m['timestamp'], unit='ms')
280
  df_1m.set_index('datetime', inplace=True)
281
  df_1m = df_1m.sort_index()
282
 
283
+ frames = {}
284
+ agg_dict = {'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum'}
 
 
 
 
 
285
 
286
+ # 1. Calc 1m (Base)
287
+ frames['1m'] = self._calculate_indicators_vectorized(df_1m.copy(), timeframe='1m')
288
+ frames['1m']['timestamp'] = frames['1m'].index.floor('1min').astype(np.int64) // 10**6
289
+ fast_1m = {col: frames['1m'][col].values for col in frames['1m'].columns}
290
+
291
+ # 2. Calc HTF
292
+ numpy_htf = {}
293
+ for tf_str, tf_code in [('5m', '5T'), ('15m', '15T'), ('1h', '1h'), ('4h', '4h'), ('1d', '1D')]:
294
+ resampled = df_1m.resample(tf_code).agg(agg_dict).dropna()
295
+ resampled = self._calculate_indicators_vectorized(resampled, timeframe=tf_str)
296
+ resampled['timestamp'] = resampled.index.astype(np.int64) // 10**6
297
+ frames[tf_str] = resampled
298
+ numpy_htf[tf_str] = {col: resampled[col].values for col in resampled.columns}
299
+
300
+ # 3. Global Index Maps
301
+ arr_ts_1m = fast_1m['timestamp']
302
+ map_5m = np.searchsorted(numpy_htf['5m']['timestamp'], arr_ts_1m)
303
+ map_15m = np.searchsorted(numpy_htf['15m']['timestamp'], arr_ts_1m)
304
+ map_1h = np.searchsorted(numpy_htf['1h']['timestamp'], arr_ts_1m)
305
+ map_4h = np.searchsorted(numpy_htf['4h']['timestamp'], arr_ts_1m)
306
+
307
+ map_5m = np.clip(map_5m, 0, len(numpy_htf['5m']['timestamp']) - 1)
308
+ map_15m = np.clip(map_15m, 0, len(numpy_htf['15m']['timestamp']) - 1)
309
+ map_1h = np.clip(map_1h, 0, len(numpy_htf['1h']['timestamp']) - 1)
310
+ map_4h = np.clip(map_4h, 0, len(numpy_htf['4h']['timestamp']) - 1)
311
+
312
+ # 4. Load Models
313
+ hydra_models = getattr(self.proc.guardian_hydra, 'models', {}) if self.proc.guardian_hydra else {}
314
+ hydra_cols = getattr(self.proc.guardian_hydra, 'feature_cols', []) if self.proc.guardian_hydra else []
315
+ legacy_v2 = getattr(self.proc.guardian_legacy, 'model_v2', None)
316
+
317
+ oracle_dir = getattr(self.proc.oracle, 'model_direction', None)
318
+ oracle_cols = getattr(self.proc.oracle, 'feature_cols', [])
319
+
320
+ sniper_models = getattr(self.proc.sniper, 'models', [])
321
+ sniper_cols = getattr(self.proc.sniper, 'feature_names', [])
322
+
323
+ titan_model = getattr(self.proc.titan, 'model', None)
324
+ titan_cols = getattr(self.proc.titan, 'feature_names', [])
325
 
326
+ # ======================================================================
327
+ # 🔥 GLOBAL INFERENCE (Batch)
328
+ # ======================================================================
329
+
330
+ # A. TITAN (Map 5m -> 1m)
331
+ global_titan_scores = np.full(len(arr_ts_1m), 0.5, dtype=np.float32)
332
+ if titan_model and titan_cols:
333
+ print(" 🚀 Running Global Titan...", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  try:
335
+ # Titan needs 5m features aligned to 1m
336
+ # Build feature matrix from numpy_htf['5m'] using map_5m
337
+ t_vecs = []
338
+ for col in titan_cols:
339
+ # Titan features usually have no prefix in the pickle list,
340
+ # but in htf dict we have raw names.
341
+ # Need to verify if titan_cols expects "RSI" or "5m_RSI"??
342
+ # Usually Titan is trained on ONE timeframe (5m).
343
+ # So we just pull the raw column from numpy_htf['5m'].
344
+
345
+ # Fix: Clean name (e.g. if trained as 'RSI', grab 'RSI')
346
+ if col in numpy_htf['5m']:
347
+ t_vecs.append(numpy_htf['5m'][col][map_5m])
348
+ else:
349
+ t_vecs.append(np.zeros(len(arr_ts_1m)))
350
+
351
+ X_TITAN = np.column_stack(t_vecs)
352
+ preds_t = titan_model.predict(xgb.DMatrix(X_TITAN))
353
+ global_titan_scores = preds_t
354
  except Exception as e: print(f"Titan Error: {e}")
355
 
356
+ # B. SNIPER (1m Direct)
357
+ global_sniper_scores = np.full(len(arr_ts_1m), 0.5, dtype=np.float32)
358
+ if sniper_models:
359
+ print(" 🚀 Running Global Sniper...", flush=True)
360
  try:
361
+ s_vecs = []
362
+ for col in sniper_cols:
363
+ if col in fast_1m: s_vecs.append(fast_1m[col])
364
+ # Fix mapping for 'atr' -> 'atr_z' if needed
365
+ elif col == 'atr' and 'atr_z' in fast_1m: s_vecs.append(fast_1m['atr_z'])
366
+ else: s_vecs.append(np.zeros(len(arr_ts_1m)))
 
 
367
 
368
+ X_SNIPER = np.column_stack(s_vecs)
369
+ preds_list = [m.predict(X_SNIPER) for m in sniper_models]
370
+ global_sniper_scores = np.mean(preds_list, axis=0)
371
+ except Exception as e: print(f"Sniper Error: {e}")
372
+
373
+ # C. ORACLE (HTF Mix)
374
+ global_oracle_scores = np.full(len(arr_ts_1m), 0.5, dtype=np.float32)
375
+ if oracle_dir:
376
+ print(" 🚀 Running Global Oracle...", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
377
  try:
378
+ o_vecs = []
379
+ for col in oracle_cols:
380
+ if col.startswith('1h_'): o_vecs.append(numpy_htf['1h'].get(col[3:], np.zeros(len(arr_ts_1m)))[map_1h])
381
+ elif col.startswith('15m_'): o_vecs.append(numpy_htf['15m'].get(col[4:], np.zeros(len(arr_ts_1m)))[map_15m])
382
+ elif col.startswith('4h_'): o_vecs.append(numpy_htf['4h'].get(col[3:], np.zeros(len(arr_ts_1m)))[map_4h])
383
+ elif col == 'sim_titan_score': o_vecs.append(global_titan_scores)
384
+ elif col == 'sim_mc_score': o_vecs.append(np.full(len(arr_ts_1m), 0.5))
385
+ elif col == 'sim_pattern_score': o_vecs.append(np.full(len(arr_ts_1m), 0.5))
386
+ else: o_vecs.append(np.zeros(len(arr_ts_1m)))
387
+
388
+ X_ORACLE = np.column_stack(o_vecs)
389
+ preds_o = oracle_dir.predict(X_ORACLE)
390
+ global_oracle_scores = preds_o if isinstance(preds_o, np.ndarray) and len(preds_o.shape)==1 else preds_o[:, 0]
391
+ # Adjust if binary (assuming 0=Long, 1=Short or vice versa, check training)
392
+ # Usually we want Confidence > 0.6. Assuming output is Long Prob.
393
  except Exception as e: print(f"Oracle Error: {e}")
394
 
395
+ # D. LEGACY V2 (Global)
396
+ global_v2_scores = np.zeros(len(arr_ts_1m), dtype=np.float32)
397
+ if legacy_v2:
 
398
  try:
399
+ l_log = fast_1m['log_ret']
400
+ l_rsi = fast_1m['RSI'] / 100.0
401
+ l_fib = fast_1m['fib_pos']
402
+ l_vol = fast_1m['volatility']
403
+
404
+ l5_log = numpy_htf['5m']['log_ret'][map_5m]
405
+ l5_rsi = numpy_htf['5m']['RSI'][map_5m] / 100.0
406
+ l5_fib = numpy_htf['5m']['fib_pos'][map_5m]
407
+ l5_trd = numpy_htf['5m']['trend_slope'][map_5m]
408
+
409
+ l15_log = numpy_htf['15m']['log_ret'][map_15m]
410
+ l15_rsi = numpy_htf['15m']['RSI'][map_15m] / 100.0
411
+ l15_fib618 = numpy_htf['15m']['dist_fib618'][map_15m]
412
+ l15_trd = numpy_htf['15m']['trend_slope'][map_15m]
413
 
414
+ lags = []
415
+ for lag in [1, 2, 3, 5, 10, 20]:
416
+ lags.extend([fast_1m[f'log_ret_lag_{lag}'], fast_1m[f'rsi_lag_{lag}'],
417
+ fast_1m[f'fib_pos_lag_{lag}'], fast_1m[f'volatility_lag_{lag}']])
 
 
418
 
419
+ X_V2 = np.column_stack([l_log, l_rsi, l_fib, l_vol, l5_log, l5_rsi, l5_fib, l5_trd,
420
+ l15_log, l15_rsi, l15_fib618, l15_trd, *lags])
421
+ preds = legacy_v2.predict(xgb.DMatrix(X_V2))
422
+ global_v2_scores = preds[:, 2] if len(preds.shape) > 1 else preds
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
  except: pass
424
 
425
+ # E. HYDRA STATIC (Global)
426
+ global_hydra_static = None
427
+ if hydra_models:
 
428
  try:
429
+ # [rsi1, rsi5, rsi15, bb, vol, atr, close]
430
+ global_hydra_static = np.column_stack([
431
+ fast_1m['RSI'], numpy_htf['5m']['RSI'][map_5m], numpy_htf['15m']['RSI'][map_15m],
432
+ fast_1m['bb_width'], fast_1m['rel_vol'], fast_1m['atr'], fast_1m['close']
433
+ ])
434
+ except: pass
435
+
436
+ # --- 5. Filtering Candidates ---
437
+ # Using Oracle and Sniper to filter BEFORE loop
438
+ # This saves simulating trades that would never be entered
439
+
440
+ # Valid: (Titan > 0.5) & (Oracle > 0.5) & (Sniper > 0.3) & (RSI < 70)
441
+ # This reduces loop count drastically
442
+ is_candidate = (
443
+ (numpy_htf['1h']['RSI'][map_1h] <= 70) &
444
+ (global_titan_scores > 0.4) &
445
+ (global_oracle_scores > 0.4)
446
+ )
447
+
448
+ candidate_indices = np.where(is_candidate)[0]
449
+
450
+ # Date Filter
451
+ start_ts_val = frames['1m'].index[0] + pd.Timedelta(minutes=500)
452
+ start_idx_offset = np.searchsorted(arr_ts_1m, int(start_ts_val.timestamp()*1000))
453
+ candidate_indices = candidate_indices[candidate_indices >= start_idx_offset]
454
+ max_idx = len(arr_ts_1m) - 245
455
+ candidate_indices = candidate_indices[candidate_indices < max_idx]
456
+
457
+ print(f" 🎯 Candidates: {len(candidate_indices)}. Simulating Trades...", flush=True)
458
+
459
+ ai_results = []
460
+ time_vec = np.arange(1, 241)
461
+
462
+ # --- 6. SIMULATION LOOP (Lite) ---
463
+ for idx_entry in candidate_indices:
464
+
465
+ entry_price = fast_1m['close'][idx_entry]
466
+ entry_ts = int(arr_ts_1m[idx_entry])
467
+
468
+ s_titan = global_titan_scores[idx_entry]
469
+ s_oracle = global_oracle_scores[idx_entry]
470
+ s_sniper = global_sniper_scores[idx_entry]
471
+
472
+ idx_exit = idx_entry + 240
473
+
474
+ # Legacy Max Risk
475
+ max_v2 = np.max(global_v2_scores[idx_entry:idx_exit])
476
+ v2_time = 0
477
+ if max_v2 > 0.8:
478
+ rel = np.argmax(global_v2_scores[idx_entry:idx_exit])
479
+ v2_time = int(arr_ts_1m[idx_entry + rel])
480
+
481
+ # Hydra Dynamic Risk
482
+ max_hydra = 0.0; hydra_time = 0
483
+ if hydra_models and global_hydra_static is not None:
484
+ sl_st = global_hydra_static[idx_entry:idx_exit]
485
+ sl_close = sl_st[:, 6]
486
+ sl_atr = sl_st[:, 5]
487
+
488
+ dist = np.maximum(1.5 * sl_atr, entry_price * 0.015)
489
+ pnl = sl_close - entry_price
490
+ norm_pnl = pnl / dist
491
+ cum_max = np.maximum.accumulate(sl_close)
492
+ max_pnl_r = (np.maximum(cum_max, entry_price) - entry_price) / dist
493
+ atr_pct = sl_atr / sl_close
494
+
495
+ zeros = np.zeros(240)
496
+ oracle_arr = np.full(240, s_oracle)
497
+ l2_arr = np.full(240, 0.7)
498
+ tgt_arr = np.full(240, 3.0)
499
+
500
+ # [rsi1, rsi5, rsi15, bb, vol, dist_ema, atr_p, norm, max, dists, time, entry, oracle, l2, target]
501
+ X_H = np.column_stack([
502
+ sl_st[:,0], sl_st[:,1], sl_st[:,2], sl_st[:,3], sl_st[:,4],
503
+ zeros, atr_pct, norm_pnl, max_pnl_r,
504
+ zeros, zeros, time_vec, zeros,
505
+ oracle_arr, l2_arr, tgt_arr
506
+ ])
507
+
508
+ try:
509
+ probs = hydra_models['crash'].predict_proba(X_H)[:, 1]
510
+ max_hydra = np.max(probs)
511
+ if max_hydra > 0.6:
512
+ t_idx = np.argmax(probs)
513
+ hydra_time = int(arr_ts_1m[idx_entry + t_idx])
514
+ except: pass
515
+
516
+ ai_results.append({
517
+ 'timestamp': entry_ts, 'symbol': sym, 'close': entry_price,
518
+ 'real_titan': s_titan,
519
+ 'oracle_conf': s_oracle,
520
+ 'sniper_score': s_sniper,
521
+ 'risk_hydra_crash': max_hydra,
522
+ 'time_hydra_crash': hydra_time,
523
+ 'risk_legacy_v2': max_v2,
524
+ 'time_legacy_panic': v2_time,
525
+ 'signal_type': 'BREAKOUT',
526
+ 'l1_score': 50.0
527
+ })
528
+
529
  dt = time.time() - t0
530
+ if ai_results:
531
+ pd.DataFrame(ai_results).to_pickle(scores_file)
532
+ print(f" ✅ [{sym}] Completed in {dt:.2f} seconds. ({len(ai_results)} signals)", flush=True)
533
+ else:
534
+ print(f" ⚠️ [{sym}] No signals.", flush=True)
535
+
536
+ del frames, fast_1m, numpy_htf, global_v2_scores, global_oracle_scores, global_sniper_scores, global_titan_scores
537
  gc.collect()
538
 
539
+ # ==============================================================
540
+ # PHASE 1 & 2 (Standard Optimization)
541
+ # ==============================================================
542
  async def generate_truth_data(self):
543
+ if self.force_start_date and self.force_end_date:
544
+ dt_start = datetime.strptime(self.force_start_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
545
+ dt_end = datetime.strptime(self.force_end_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
546
+ start_time_ms = int(dt_start.timestamp() * 1000)
547
+ end_time_ms = int(dt_end.timestamp() * 1000)
548
  print(f"\n🚜 [Phase 1] Processing Era: {self.force_start_date} -> {self.force_end_date}")
549
+ else: return
550
+
551
+ for sym in self.TARGET_COINS:
552
+ try:
553
+ candles = await self._fetch_all_data_fast(sym, start_time_ms, end_time_ms)
554
+ if candles: await self._process_data_in_memory(sym, candles, start_time_ms, end_time_ms)
555
+ except Exception as e: print(f" ❌ SKIP {sym}: {e}", flush=True)
556
+ gc.collect()
557
 
558
  @staticmethod
559
  def _worker_optimize(combinations_batch, scores_files, initial_capital, fees_pct, max_slots):
560
+ results = []
561
+ all_data = []
562
+ for fp in scores_files:
563
+ try:
564
+ df = pd.read_pickle(fp)
565
+ if not df.empty: all_data.append(df)
566
  except: pass
567
+ if not all_data: return []
568
+ global_df = pd.concat(all_data)
569
+ global_df.sort_values('timestamp', inplace=True)
570
+ grouped_by_time = global_df.groupby('timestamp')
571
 
572
+ for config in combinations_batch:
573
+ wallet = { "balance": initial_capital, "allocated": 0.0, "positions": {}, "trades_history": [] }
574
+ w_titan = config['w_titan']; oracle_thresh = config.get('oracle_thresh', 0.6)
575
+ sniper_thresh = config.get('sniper_thresh', 0.4); hydra_thresh = config['hydra_thresh']
576
+ peak_balance = initial_capital; max_drawdown = 0.0
577
+
578
+ for ts, group in grouped_by_time:
579
+ active = list(wallet["positions"].keys())
580
+ current_prices = {row['symbol']: row['close'] for _, row in group.iterrows()}
581
+ for sym in active:
582
+ if sym in current_prices:
583
+ curr = current_prices[sym]
584
+ pos = wallet["positions"][sym]
585
+ h_risk = pos.get('risk_hydra_crash', 0)
586
+ h_time = pos.get('time_hydra_crash', 0)
587
+ is_crash = (h_risk > hydra_thresh) and (h_time > 0) and (ts >= h_time)
588
+ pnl = (curr - pos['entry']) / pos['entry']
589
+ if is_crash or pnl > 0.04 or pnl < -0.02:
590
+ wallet['balance'] += pos['size'] * (1 + pnl - (fees_pct*2))
591
+ wallet['allocated'] -= pos['size']
592
+ del wallet['positions'][sym]
593
+ wallet['trades_history'].append({'pnl': pnl})
594
+
595
+ total_eq = wallet['balance'] + wallet['allocated']
596
+ if total_eq > peak_balance: peak_balance = total_eq
597
+ dd = (peak_balance - total_eq) / peak_balance
598
+ if dd > max_drawdown: max_drawdown = dd
599
+
600
+ if len(wallet['positions']) < max_slots:
601
+ for _, row in group.iterrows():
602
+ if row['symbol'] in wallet['positions']: continue
603
+ if row['oracle_conf'] < oracle_thresh: continue
604
+ if row['sniper_score'] < sniper_thresh: continue
605
+ if row['real_titan'] < w_titan: continue # Titan Check
606
+
607
+ size = 10.0
608
+ if wallet['balance'] >= size:
609
+ wallet['positions'][row['symbol']] = {
610
+ 'entry': row['close'], 'size': size,
611
+ 'risk_hydra_crash': row['risk_hydra_crash'],
612
+ 'time_hydra_crash': row['time_hydra_crash']
613
+ }
614
+ wallet['balance'] -= size
615
+ wallet['allocated'] += size
616
 
617
+ final_bal = wallet['balance'] + wallet['allocated']
618
+ net_profit = final_bal - initial_capital
619
+ trades = wallet['trades_history']
620
+ total_t = len(trades)
621
+ win_count = len([t for t in trades if t['pnl'] > 0])
622
+ loss_count = len([t for t in trades if t['pnl'] <= 0])
623
+ win_rate = (win_count / total_t * 100) if total_t > 0 else 0
624
+ max_win = max([t['pnl'] for t in trades]) if trades else 0
625
+ max_loss = min([t['pnl'] for t in trades]) if trades else 0
626
 
627
+ max_win_streak = 0; max_loss_streak = 0; curr_w = 0; curr_l = 0
628
+ for t in trades:
 
 
 
 
 
 
 
 
 
 
 
 
629
  if t['pnl'] > 0:
630
  curr_w += 1; curr_l = 0
631
+ if curr_w > max_win_streak: max_win_streak = curr_w
632
  else:
633
  curr_l += 1; curr_w = 0
634
+ if curr_l > max_loss_streak: max_loss_streak = curr_l
635
+
636
+ results.append({
637
+ 'config': config, 'final_balance': final_bal, 'net_profit': net_profit,
638
+ 'total_trades': total_t, 'win_count': win_count, 'loss_count': loss_count,
639
+ 'win_rate': win_rate, 'max_single_win': max_win, 'max_single_loss': max_loss,
640
+ 'max_drawdown': max_drawdown * 100
 
 
 
 
 
 
 
 
 
 
 
641
  })
642
+
643
+ return results
644
 
645
  async def run_optimization(self, target_regime="RANGE"):
646
  await self.generate_truth_data()
647
+ oracle_r = np.linspace(0.4, 0.7, 3); sniper_r = np.linspace(0.4, 0.7, 3)
648
+ hydra_r = [0.85, 0.95]
649
 
650
  combos = []
651
+ for o, s, h in itertools.product(oracle_r, sniper_r, hydra_r):
652
  combos.append({
653
+ 'w_titan': 0.5, 'w_struct': 0.3, 'thresh': 0.5, 'l1_thresh': 50.0,
654
  'oracle_thresh': o, 'sniper_thresh': s, 'hydra_thresh': h, 'legacy_thresh': 0.95
655
  })
656
 
 
661
  results_list.sort(key=lambda x: x['net_profit'], reverse=True)
662
  best = results_list[0]
663
 
 
 
 
 
 
 
 
 
 
 
 
 
664
  print("\n" + "="*60)
665
  print(f"🏆 CHAMPION REPORT [{target_regime}]:")
666
  print(f" 💰 Final Balance: ${best['final_balance']:,.2f}")
 
668
  print("-" * 60)
669
  print(f" 📊 Total Trades: {best['total_trades']}")
670
  print(f" 📈 Win Rate: {best['win_rate']:.1f}%")
 
 
 
 
 
 
 
 
671
  print("-" * 60)
 
672
  print(f" ⚙️ Oracle={best['config']['oracle_thresh']:.2f} | Sniper={best['config']['sniper_thresh']:.2f} | Hydra={best['config']['hydra_thresh']:.2f}")
673
  print("="*60)
674
  return best['config'], best
675
 
676
  async def run_strategic_optimization_task():
677
+ print("\n🧪 [STRATEGIC BACKTEST] Feature Parity Mode...")
678
  r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
679
  try:
680
  await dm.initialize(); await proc.initialize()