Spaces:
Paused
Paused
Update backtest_engine.py
Browse files- backtest_engine.py +185 -96
backtest_engine.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# ============================================================
|
| 2 |
-
# π§ͺ backtest_engine.py (
|
| 3 |
# ============================================================
|
| 4 |
|
| 5 |
import asyncio
|
|
@@ -34,7 +34,7 @@ logging.getLogger('ml_engine').setLevel(logging.WARNING)
|
|
| 34 |
CACHE_DIR = "backtest_real_scores"
|
| 35 |
|
| 36 |
# ============================================================
|
| 37 |
-
# π§ 1. FEATURE ENGINEERING
|
| 38 |
# ============================================================
|
| 39 |
|
| 40 |
def _zv(x):
|
|
@@ -146,6 +146,82 @@ def calculate_titan_features_real(df):
|
|
| 146 |
if vwap is not None: df['VWAP_dist'] = (df['close'] / vwap) - 1
|
| 147 |
return df.fillna(0)
|
| 148 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 149 |
# ============================================================
|
| 150 |
# π§ͺ THE BACKTESTER CLASS
|
| 151 |
# ============================================================
|
|
@@ -164,7 +240,7 @@ class HeavyDutyBacktester:
|
|
| 164 |
for f in glob.glob(os.path.join(CACHE_DIR, "*")): os.remove(f)
|
| 165 |
else: os.makedirs(CACHE_DIR)
|
| 166 |
|
| 167 |
-
print(f"π§ͺ [Backtest
|
| 168 |
self._check_engines()
|
| 169 |
|
| 170 |
def _check_engines(self):
|
|
@@ -176,23 +252,20 @@ class HeavyDutyBacktester:
|
|
| 176 |
if self.proc.oracle: status.append("Oracle(Real)")
|
| 177 |
if self.proc.sniper: status.append("Sniper(Real)")
|
| 178 |
if self.proc.guardian_hydra: status.append("Hydra(Real)")
|
|
|
|
| 179 |
print(f" β
Engines Ready: {', '.join(status)}")
|
| 180 |
|
| 181 |
def set_date_range(self, start_str, end_str):
|
| 182 |
self.force_start_date = start_str; self.force_end_date = end_str
|
| 183 |
|
| 184 |
-
# β
FIXED: Helper methods are now INSIDE the class
|
| 185 |
def _smart_predict(self, model, X):
|
| 186 |
"""Forces predict_proba and handles shape mismatch"""
|
| 187 |
try:
|
| 188 |
-
# 1. Try Proba (For Classifiers)
|
| 189 |
if hasattr(model, "predict_proba"):
|
| 190 |
raw = model.predict_proba(X)
|
| 191 |
if raw.ndim == 2:
|
| 192 |
-
return raw[:, -1]
|
| 193 |
return raw
|
| 194 |
-
|
| 195 |
-
# 2. Fallback to predict
|
| 196 |
return model.predict(X)
|
| 197 |
except:
|
| 198 |
return np.zeros(len(X) if hasattr(X, '__len__') else 0)
|
|
@@ -238,7 +311,7 @@ class HeavyDutyBacktester:
|
|
| 238 |
print(f" π [{sym}] Data Exists -> Skipping.")
|
| 239 |
return
|
| 240 |
|
| 241 |
-
print(f" βοΈ [CPU] Analyzing {sym} (
|
| 242 |
t0 = time.time()
|
| 243 |
|
| 244 |
# 1. Base Data
|
|
@@ -248,6 +321,10 @@ class HeavyDutyBacktester:
|
|
| 248 |
df_1m.set_index('datetime', inplace=True)
|
| 249 |
df_1m = df_1m.sort_index()
|
| 250 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
# 1οΈβ£ SNIPER & L1 SCORING (L4/L1)
|
| 252 |
df_sniper = calculate_sniper_features_exact(df_1m)
|
| 253 |
df_sniper['rel_vol'] = df_sniper['volume'] / (df_sniper['volume'].rolling(50).mean() + 1e-9)
|
|
@@ -264,12 +341,14 @@ class HeavyDutyBacktester:
|
|
| 264 |
pattern_models = getattr(self.proc.pattern_engine, 'models', {})
|
| 265 |
if pattern_models and '15m' in pattern_models:
|
| 266 |
try:
|
| 267 |
-
|
| 268 |
-
|
|
|
|
|
|
|
| 269 |
pat_inputs = []; valid_15m_idxs = []
|
| 270 |
|
| 271 |
-
for i in range(200, len(
|
| 272 |
-
window =
|
| 273 |
vec = _transform_window_for_pattern(window)
|
| 274 |
if vec is not None:
|
| 275 |
pat_inputs.append(vec); valid_15m_idxs.append(i)
|
|
@@ -279,7 +358,7 @@ class HeavyDutyBacktester:
|
|
| 279 |
pat_preds = self._smart_predict(pattern_models['15m'], xgb.DMatrix(X_pat))
|
| 280 |
pat_scores_15m[valid_15m_idxs] = pat_preds
|
| 281 |
|
| 282 |
-
ts_15m =
|
| 283 |
map_idxs = np.searchsorted(ts_15m, df_candidates['timestamp'].values) - 1
|
| 284 |
res_patterns = pat_scores_15m[np.clip(map_idxs, 0, len(pat_scores_15m)-1)]
|
| 285 |
except Exception as e: print(f"Patterns Error: {e}")
|
|
@@ -288,7 +367,6 @@ class HeavyDutyBacktester:
|
|
| 288 |
res_titan = np.full(len(df_candidates), 0.5)
|
| 289 |
if self.proc.titan and self.proc.titan.model:
|
| 290 |
try:
|
| 291 |
-
df_5m = df_1m.resample('5T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
|
| 292 |
df_5m_feat = calculate_titan_features_real(df_5m).add_prefix('5m_')
|
| 293 |
ts_5m = df_5m.index.astype(np.int64) // 10**6
|
| 294 |
map_idxs = np.clip(np.searchsorted(ts_5m, df_candidates['timestamp'].values) - 1, 0, len(df_5m_feat)-1)
|
|
@@ -306,7 +384,6 @@ class HeavyDutyBacktester:
|
|
| 306 |
for f in feats:
|
| 307 |
if f not in df_candidates.columns: df_candidates[f] = 0.0
|
| 308 |
X_snip = df_candidates[feats].values
|
| 309 |
-
# β
Calling internal method self._smart_predict
|
| 310 |
preds = [self._extract_probs(self._smart_predict(m, X_snip)) for m in sniper_models]
|
| 311 |
res_sniper = np.mean(preds, axis=0)
|
| 312 |
except Exception as e: print(f"Sniper Error: {e}")
|
|
@@ -325,9 +402,8 @@ class HeavyDutyBacktester:
|
|
| 325 |
res_oracle = self._extract_probs(self._smart_predict(oracle_model, X_orc_df.values))
|
| 326 |
except Exception as e: print(f"Oracle Error: {e}")
|
| 327 |
|
| 328 |
-
# 6οΈβ£ HYDRA
|
| 329 |
-
res_hydra_risk = np.zeros(len(df_candidates))
|
| 330 |
-
|
| 331 |
hydra_models = getattr(self.proc.guardian_hydra, 'models', {})
|
| 332 |
if hydra_models and 'crash' in hydra_models:
|
| 333 |
try:
|
|
@@ -366,7 +442,45 @@ class HeavyDutyBacktester:
|
|
| 366 |
res_hydra_risk[r_idxs] = np.max(preds, axis=1)
|
| 367 |
except: pass
|
| 368 |
|
| 369 |
-
# 7οΈβ£
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 370 |
print(f" π [Stats] Titan:{res_titan.mean():.2f} | Patterns:{res_patterns.mean():.2f} | Sniper:{res_sniper.mean():.2f} | Oracle:{res_oracle.mean():.2f}")
|
| 371 |
|
| 372 |
ai_df = pd.DataFrame({
|
|
@@ -378,9 +492,8 @@ class HeavyDutyBacktester:
|
|
| 378 |
'sniper_score': res_sniper,
|
| 379 |
'l1_score': df_candidates['l1_score'],
|
| 380 |
'risk_hydra_crash': res_hydra_risk,
|
| 381 |
-
'
|
| 382 |
-
'
|
| 383 |
-
'time_legacy_panic': 0
|
| 384 |
})
|
| 385 |
|
| 386 |
dt = time.time() - t0
|
|
@@ -389,7 +502,6 @@ class HeavyDutyBacktester:
|
|
| 389 |
print(f" β
[{sym}] Completed {len(ai_df)} signals in {dt:.2f} seconds.", flush=True)
|
| 390 |
gc.collect()
|
| 391 |
|
| 392 |
-
# --- Standard Methods (unchanged) ---
|
| 393 |
async def generate_truth_data(self):
|
| 394 |
if self.force_start_date:
|
| 395 |
dt_s = datetime.strptime(self.force_start_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
|
@@ -399,9 +511,7 @@ class HeavyDutyBacktester:
|
|
| 399 |
for sym in self.TARGET_COINS:
|
| 400 |
c = await self._fetch_all_data_fast(sym, ms_s, ms_e)
|
| 401 |
if c: await self._process_data_in_memory(sym, c, ms_s, ms_e)
|
| 402 |
-
|
| 403 |
-
# π§ FIX: Updated Worker with Consensus Metrics
|
| 404 |
-
# ============================================================
|
| 405 |
@staticmethod
|
| 406 |
def _worker_optimize(combinations_batch, scores_files, initial_capital, fees_pct, max_slots):
|
| 407 |
print(f" β³ [System] Loading {len(scores_files)} datasets...", flush=True)
|
|
@@ -412,7 +522,7 @@ class HeavyDutyBacktester:
|
|
| 412 |
if not data: return []
|
| 413 |
df = pd.concat(data).sort_values('timestamp')
|
| 414 |
|
| 415 |
-
# Arrays
|
| 416 |
ts = df['timestamp'].values; close = df['close'].values.astype(float)
|
| 417 |
sym = df['symbol'].values; sym_map = {s:i for i,s in enumerate(np.unique(sym))}
|
| 418 |
sym_id = np.array([sym_map[s] for s in sym])
|
|
@@ -421,117 +531,94 @@ class HeavyDutyBacktester:
|
|
| 421 |
hydra = df['risk_hydra_crash'].values; titan = df['real_titan'].values
|
| 422 |
l1 = df['l1_score'].values
|
| 423 |
|
|
|
|
|
|
|
|
|
|
| 424 |
N = len(ts)
|
| 425 |
print(f" π [System] Testing {len(combinations_batch)} configs on {N} candles...", flush=True)
|
| 426 |
|
| 427 |
res = []
|
| 428 |
for cfg in combinations_batch:
|
| 429 |
-
pos = {}; log = []
|
| 430 |
bal = initial_capital; alloc = 0.0
|
| 431 |
|
| 432 |
-
# Entry Logic Mask
|
| 433 |
mask = (l1 >= cfg['l1_thresh']) & (oracle >= cfg['oracle_thresh']) & (sniper >= cfg['sniper_thresh'])
|
| 434 |
|
| 435 |
for i in range(N):
|
| 436 |
s = sym_id[i]; p = close[i]
|
| 437 |
|
| 438 |
-
#
|
| 439 |
if s in pos:
|
| 440 |
-
|
| 441 |
-
h_r = pos[s][1]
|
| 442 |
-
entry_titan_score = pos[s][3] # Stored Titan Score
|
| 443 |
|
| 444 |
-
|
| 445 |
-
|
| 446 |
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
bal += pos[s][2] * (1 + realized_pnl)
|
| 453 |
alloc -= pos[s][2]
|
| 454 |
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
log.append({'pnl': realized_pnl, 'consensus': is_consensus})
|
| 458 |
-
|
| 459 |
del pos[s]
|
| 460 |
|
| 461 |
-
#
|
| 462 |
if len(pos) < max_slots and mask[i]:
|
| 463 |
if s not in pos and bal >= 5.0:
|
| 464 |
-
size = min(10.0, bal * 0.98)
|
| 465 |
-
# Store: (Price, HydraRisk, Size, TitanScore)
|
| 466 |
pos[s] = (p, hydra[i], size, titan[i])
|
| 467 |
bal -= size; alloc += size
|
| 468 |
|
| 469 |
-
#
|
| 470 |
final_bal = bal + alloc
|
| 471 |
-
|
| 472 |
-
|
| 473 |
wins = sum(1 for x in log if x['pnl'] > 0)
|
| 474 |
-
win_rate = (wins
|
| 475 |
-
|
| 476 |
-
# Consensus Metrics Calculation
|
| 477 |
-
consensus_trades = [x for x in log if x['consensus']]
|
| 478 |
-
num_con = len(consensus_trades)
|
| 479 |
-
|
| 480 |
-
agreement_rate = (num_con / total_trades * 100) if total_trades > 0 else 0.0
|
| 481 |
-
con_wins = sum(1 for x in consensus_trades if x['pnl'] > 0)
|
| 482 |
-
con_win_rate = (con_wins / num_con * 100) if num_con > 0 else 0.0
|
| 483 |
-
con_avg_pnl = (sum(x['pnl'] for x in consensus_trades) / num_con * 100) if num_con > 0 else 0.0
|
| 484 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
res.append({
|
| 486 |
-
'config': cfg,
|
| 487 |
-
'
|
| 488 |
-
'
|
| 489 |
-
'
|
| 490 |
-
'
|
| 491 |
-
'max_drawdown': 0.0, # Simplified for speed
|
| 492 |
-
'consensus_agreement_rate': agreement_rate, # β
Added
|
| 493 |
-
'high_consensus_win_rate': con_win_rate, # β
Added
|
| 494 |
-
'high_consensus_avg_pnl': con_avg_pnl # β
Added
|
| 495 |
})
|
| 496 |
-
|
| 497 |
return res
|
| 498 |
|
| 499 |
-
# ============================================================
|
| 500 |
-
# π§ FIX: Run Optimization with Safe Sorting
|
| 501 |
-
# ============================================================
|
| 502 |
async def run_optimization(self, target_regime="RANGE"):
|
| 503 |
await self.generate_truth_data()
|
| 504 |
|
| 505 |
-
# Grid
|
| 506 |
-
oracle_r = np.linspace(0.
|
| 507 |
-
|
| 508 |
-
hydra_r = [0.85, 0.95]
|
| 509 |
-
l1_r = [5.0]
|
| 510 |
|
| 511 |
combos = []
|
| 512 |
for o, s, h, l1 in itertools.product(oracle_r, sniper_r, hydra_r, l1_r):
|
| 513 |
combos.append({
|
| 514 |
'w_titan': 0.4, 'w_struct': 0.3,
|
| 515 |
-
'l1_thresh': l1, 'oracle_thresh': o, 'sniper_thresh': s,
|
| 516 |
-
'hydra_thresh': h, 'legacy_thresh': 0.95
|
| 517 |
})
|
| 518 |
|
| 519 |
files = glob.glob(os.path.join(CACHE_DIR, "*.pkl"))
|
| 520 |
-
if not files:
|
| 521 |
-
print("β οΈ [Warning] No data files found for backtest.")
|
| 522 |
-
return None, {'net_profit': 0.0}
|
| 523 |
-
|
| 524 |
-
# Run Worker
|
| 525 |
results_list = self._worker_optimize(combos, files, self.INITIAL_CAPITAL, self.TRADING_FEES, self.MAX_SLOTS)
|
| 526 |
|
|
|
|
| 527 |
if not results_list:
|
| 528 |
print("β οΈ [Warning] No trades generated in any config.")
|
| 529 |
return None, {'net_profit': 0.0, 'win_rate': 0.0}
|
| 530 |
|
| 531 |
-
# β
FIX: Sort and select 'best'
|
| 532 |
-
# Sort by Net Profit descending
|
| 533 |
results_list.sort(key=lambda x: x['net_profit'], reverse=True)
|
| 534 |
-
best = results_list[0]
|
| 535 |
|
| 536 |
print("\n" + "="*60)
|
| 537 |
print(f"π CHAMPION REPORT [{target_regime}]:")
|
|
@@ -540,23 +627,23 @@ class HeavyDutyBacktester:
|
|
| 540 |
print("-" * 60)
|
| 541 |
print(f" π Total Trades: {best['total_trades']}")
|
| 542 |
print(f" π Win Rate: {best['win_rate']:.1f}%")
|
| 543 |
-
print(f" π Max Drawdown: {best['max_drawdown']:.1f}%
|
| 544 |
print("-" * 60)
|
| 545 |
print(f" π§ CONSENSUS ANALYTICS:")
|
| 546 |
-
print(f" π€ Model Agreement Rate: {best['consensus_agreement_rate']:.1f}%
|
| 547 |
print(f" π High-Consensus Win Rate: {best['high_consensus_win_rate']:.1f}%")
|
| 548 |
print(f" π High-Consensus Avg PnL: {best['high_consensus_avg_pnl']:.2f}%")
|
| 549 |
print("-" * 60)
|
| 550 |
print(f" βοΈ Oracle={best['config']['oracle_thresh']:.2f} | Sniper={best['config']['sniper_thresh']:.2f} | Hydra={best['config']['hydra_thresh']:.2f}")
|
|
|
|
| 551 |
print("="*60)
|
| 552 |
-
|
| 553 |
return best['config'], best
|
| 554 |
-
|
| 555 |
async def run_strategic_optimization_task():
|
| 556 |
print("\nπ§ͺ [STRATEGIC BACKTEST] Full Spectrum Mode...")
|
| 557 |
r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
|
| 558 |
await dm.initialize(); await proc.initialize()
|
| 559 |
-
proc.guardian_hydra.set_silent_mode(True)
|
| 560 |
|
| 561 |
hub = AdaptiveHub(r2); await hub.initialize()
|
| 562 |
opt = HeavyDutyBacktester(dm, proc)
|
|
@@ -572,7 +659,9 @@ async def run_strategic_optimization_task():
|
|
| 572 |
opt.set_date_range(s["start"], s["end"])
|
| 573 |
best_cfg, best_stats = await opt.run_optimization(s["regime"])
|
| 574 |
if best_cfg: hub.submit_challenger(s["regime"], best_cfg, best_stats)
|
| 575 |
-
|
|
|
|
|
|
|
| 576 |
await hub._save_state_to_r2()
|
| 577 |
print("β
[System] DNA Updated.")
|
| 578 |
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
+
# π§ͺ backtest_engine.py (V123.0 - GEM-Architect: Real Legacy & Consensus Fix)
|
| 3 |
# ============================================================
|
| 4 |
|
| 5 |
import asyncio
|
|
|
|
| 34 |
CACHE_DIR = "backtest_real_scores"
|
| 35 |
|
| 36 |
# ============================================================
|
| 37 |
+
# π§ 1. FEATURE ENGINEERING (GLOBAL & VECTORIZED)
|
| 38 |
# ============================================================
|
| 39 |
|
| 40 |
def _zv(x):
|
|
|
|
| 146 |
if vwap is not None: df['VWAP_dist'] = (df['close'] / vwap) - 1
|
| 147 |
return df.fillna(0)
|
| 148 |
|
| 149 |
+
# ============================================================
|
| 150 |
+
# π§ NEW: LEGACY GUARD VECTORIZED FEATURES (REAL EXECUTION)
|
| 151 |
+
# ============================================================
|
| 152 |
+
def calculate_legacy_v2_vectorized(df_1m, df_5m, df_15m):
|
| 153 |
+
"""
|
| 154 |
+
Replicates HybridDeepSteward V2 features in a vectorized way for speed.
|
| 155 |
+
"""
|
| 156 |
+
try:
|
| 157 |
+
def calc_basic(df, suffix):
|
| 158 |
+
c = df['close']; h = df['high']; l = df['low']
|
| 159 |
+
res = pd.DataFrame(index=df.index)
|
| 160 |
+
res[f'log_ret_{suffix}'] = np.log(c / c.shift(1)).fillna(0)
|
| 161 |
+
res[f'rsi_{suffix}'] = (ta.rsi(c, 14) / 100.0).fillna(0.5)
|
| 162 |
+
|
| 163 |
+
roll_max = h.rolling(50).max(); roll_min = l.rolling(50).min()
|
| 164 |
+
diff = (roll_max - roll_min).replace(0, 1e-9)
|
| 165 |
+
res[f'fib_pos_{suffix}'] = ((c - roll_min) / diff).fillna(0.5)
|
| 166 |
+
|
| 167 |
+
if suffix == '1m':
|
| 168 |
+
res[f'volatility_{suffix}'] = (ta.atr(h, l, c, 14) / c).fillna(0)
|
| 169 |
+
else:
|
| 170 |
+
ema = ta.ema(c, 20)
|
| 171 |
+
res[f'trend_slope_{suffix}'] = ((ema - ema.shift(5)) / ema.shift(5)).fillna(0)
|
| 172 |
+
if suffix == '15m':
|
| 173 |
+
fib618 = roll_max - (diff * 0.382)
|
| 174 |
+
res[f'dist_fib618_{suffix}'] = ((c - fib618) / c).fillna(0)
|
| 175 |
+
return res.fillna(0)
|
| 176 |
+
|
| 177 |
+
f1 = calc_basic(df_1m, '1m')
|
| 178 |
+
f5 = calc_basic(df_5m, '5m').reindex(df_1m.index, method='ffill')
|
| 179 |
+
f15 = calc_basic(df_15m, '15m').reindex(df_1m.index, method='ffill')
|
| 180 |
+
|
| 181 |
+
FEATS_1M = ['log_ret_1m', 'rsi_1m', 'fib_pos_1m', 'volatility_1m']
|
| 182 |
+
FEATS_5M = ['log_ret_5m', 'rsi_5m', 'fib_pos_5m', 'trend_slope_5m']
|
| 183 |
+
FEATS_15M = ['log_ret_15m', 'rsi_15m', 'dist_fib618_15m', 'trend_slope_15m']
|
| 184 |
+
|
| 185 |
+
parts = [f1[FEATS_1M], f5[FEATS_5M], f15[FEATS_15M]]
|
| 186 |
+
|
| 187 |
+
lags = [1, 2, 3, 5, 10, 20]
|
| 188 |
+
for lag in lags:
|
| 189 |
+
lagged = f1[FEATS_1M].shift(lag).fillna(0)
|
| 190 |
+
parts.append(lagged)
|
| 191 |
+
|
| 192 |
+
X_df = pd.concat(parts, axis=1)
|
| 193 |
+
return X_df.values
|
| 194 |
+
except Exception as e:
|
| 195 |
+
print(f"Legacy V2 Vec Error: {e}")
|
| 196 |
+
return None
|
| 197 |
+
|
| 198 |
+
def calculate_legacy_v3_vectorized(df_1m, df_5m, df_15m, feature_names):
|
| 199 |
+
"""Replicates HybridDeepSteward V3 features"""
|
| 200 |
+
try:
|
| 201 |
+
def calc_v3(df, suffix=""):
|
| 202 |
+
d = df.copy()
|
| 203 |
+
d['rsi'] = ta.rsi(d['close'], 14).fillna(50)
|
| 204 |
+
d['ema_50'] = ta.ema(d['close'], 50).fillna(d['close'])
|
| 205 |
+
d['ema_200'] = ta.ema(d['close'], 200).fillna(d['close'])
|
| 206 |
+
d['dist_ema50'] = (d['close'] - d['ema_50'])/d['close']
|
| 207 |
+
d['dist_ema200'] = (d['close'] - d['ema_200'])/d['close']
|
| 208 |
+
d['log_ret'] = np.log(d['close']/d['close'].shift(1)).fillna(0)
|
| 209 |
+
|
| 210 |
+
cols = ['rsi', 'dist_ema50', 'dist_ema200', 'log_ret']
|
| 211 |
+
res = d[cols].copy()
|
| 212 |
+
if suffix: res.columns = [f"{c}_{suffix}" for c in cols]
|
| 213 |
+
return res.fillna(0)
|
| 214 |
+
|
| 215 |
+
v1 = calc_v3(df_1m)
|
| 216 |
+
v5 = calc_v3(df_5m, "5m").reindex(df_1m.index, method='ffill')
|
| 217 |
+
v15 = calc_v3(df_15m, "15m").reindex(df_1m.index, method='ffill')
|
| 218 |
+
|
| 219 |
+
full_df = pd.concat([v1, v5, v15], axis=1)
|
| 220 |
+
|
| 221 |
+
if not feature_names: return None
|
| 222 |
+
return full_df.reindex(columns=feature_names, fill_value=0).values
|
| 223 |
+
except: return None
|
| 224 |
+
|
| 225 |
# ============================================================
|
| 226 |
# π§ͺ THE BACKTESTER CLASS
|
| 227 |
# ============================================================
|
|
|
|
| 240 |
for f in glob.glob(os.path.join(CACHE_DIR, "*")): os.remove(f)
|
| 241 |
else: os.makedirs(CACHE_DIR)
|
| 242 |
|
| 243 |
+
print(f"π§ͺ [Backtest V123.0] Real Legacy & Consensus. Checking Engines...")
|
| 244 |
self._check_engines()
|
| 245 |
|
| 246 |
def _check_engines(self):
|
|
|
|
| 252 |
if self.proc.oracle: status.append("Oracle(Real)")
|
| 253 |
if self.proc.sniper: status.append("Sniper(Real)")
|
| 254 |
if self.proc.guardian_hydra: status.append("Hydra(Real)")
|
| 255 |
+
if self.proc.guardian_legacy: status.append("Legacy(Real)")
|
| 256 |
print(f" β
Engines Ready: {', '.join(status)}")
|
| 257 |
|
| 258 |
def set_date_range(self, start_str, end_str):
|
| 259 |
self.force_start_date = start_str; self.force_end_date = end_str
|
| 260 |
|
|
|
|
| 261 |
def _smart_predict(self, model, X):
|
| 262 |
"""Forces predict_proba and handles shape mismatch"""
|
| 263 |
try:
|
|
|
|
| 264 |
if hasattr(model, "predict_proba"):
|
| 265 |
raw = model.predict_proba(X)
|
| 266 |
if raw.ndim == 2:
|
| 267 |
+
return raw[:, -1]
|
| 268 |
return raw
|
|
|
|
|
|
|
| 269 |
return model.predict(X)
|
| 270 |
except:
|
| 271 |
return np.zeros(len(X) if hasattr(X, '__len__') else 0)
|
|
|
|
| 311 |
print(f" π [{sym}] Data Exists -> Skipping.")
|
| 312 |
return
|
| 313 |
|
| 314 |
+
print(f" βοΈ [CPU] Analyzing {sym} (ALL REAL MODELS)...", flush=True)
|
| 315 |
t0 = time.time()
|
| 316 |
|
| 317 |
# 1. Base Data
|
|
|
|
| 321 |
df_1m.set_index('datetime', inplace=True)
|
| 322 |
df_1m = df_1m.sort_index()
|
| 323 |
|
| 324 |
+
# Aux Dataframes for Legacy
|
| 325 |
+
df_5m = df_1m.resample('5T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
|
| 326 |
+
df_15m = df_1m.resample('15T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
|
| 327 |
+
|
| 328 |
# 1οΈβ£ SNIPER & L1 SCORING (L4/L1)
|
| 329 |
df_sniper = calculate_sniper_features_exact(df_1m)
|
| 330 |
df_sniper['rel_vol'] = df_sniper['volume'] / (df_sniper['volume'].rolling(50).mean() + 1e-9)
|
|
|
|
| 341 |
pattern_models = getattr(self.proc.pattern_engine, 'models', {})
|
| 342 |
if pattern_models and '15m' in pattern_models:
|
| 343 |
try:
|
| 344 |
+
# β οΈ Note: We rely on the internal feature calculation here for speed.
|
| 345 |
+
# In real execution, Pattern Engine handles this.
|
| 346 |
+
df_15m_res = df_1m.resample('15T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
|
| 347 |
+
pat_scores_15m = np.full(len(df_15m_res), 0.5)
|
| 348 |
pat_inputs = []; valid_15m_idxs = []
|
| 349 |
|
| 350 |
+
for i in range(200, len(df_15m_res)):
|
| 351 |
+
window = df_15m_res.iloc[i-200:i]
|
| 352 |
vec = _transform_window_for_pattern(window)
|
| 353 |
if vec is not None:
|
| 354 |
pat_inputs.append(vec); valid_15m_idxs.append(i)
|
|
|
|
| 358 |
pat_preds = self._smart_predict(pattern_models['15m'], xgb.DMatrix(X_pat))
|
| 359 |
pat_scores_15m[valid_15m_idxs] = pat_preds
|
| 360 |
|
| 361 |
+
ts_15m = df_15m_res.index.astype(np.int64) // 10**6
|
| 362 |
map_idxs = np.searchsorted(ts_15m, df_candidates['timestamp'].values) - 1
|
| 363 |
res_patterns = pat_scores_15m[np.clip(map_idxs, 0, len(pat_scores_15m)-1)]
|
| 364 |
except Exception as e: print(f"Patterns Error: {e}")
|
|
|
|
| 367 |
res_titan = np.full(len(df_candidates), 0.5)
|
| 368 |
if self.proc.titan and self.proc.titan.model:
|
| 369 |
try:
|
|
|
|
| 370 |
df_5m_feat = calculate_titan_features_real(df_5m).add_prefix('5m_')
|
| 371 |
ts_5m = df_5m.index.astype(np.int64) // 10**6
|
| 372 |
map_idxs = np.clip(np.searchsorted(ts_5m, df_candidates['timestamp'].values) - 1, 0, len(df_5m_feat)-1)
|
|
|
|
| 384 |
for f in feats:
|
| 385 |
if f not in df_candidates.columns: df_candidates[f] = 0.0
|
| 386 |
X_snip = df_candidates[feats].values
|
|
|
|
| 387 |
preds = [self._extract_probs(self._smart_predict(m, X_snip)) for m in sniper_models]
|
| 388 |
res_sniper = np.mean(preds, axis=0)
|
| 389 |
except Exception as e: print(f"Sniper Error: {e}")
|
|
|
|
| 402 |
res_oracle = self._extract_probs(self._smart_predict(oracle_model, X_orc_df.values))
|
| 403 |
except Exception as e: print(f"Oracle Error: {e}")
|
| 404 |
|
| 405 |
+
# 6οΈβ£ HYDRA (L0)
|
| 406 |
+
res_hydra_risk = np.zeros(len(df_candidates))
|
|
|
|
| 407 |
hydra_models = getattr(self.proc.guardian_hydra, 'models', {})
|
| 408 |
if hydra_models and 'crash' in hydra_models:
|
| 409 |
try:
|
|
|
|
| 442 |
res_hydra_risk[r_idxs] = np.max(preds, axis=1)
|
| 443 |
except: pass
|
| 444 |
|
| 445 |
+
# 7οΈβ£ REAL LEGACY GUARD (V2 & V3) - π‘οΈ NEW
|
| 446 |
+
res_legacy_v2 = np.zeros(len(df_candidates))
|
| 447 |
+
res_legacy_v3 = np.zeros(len(df_candidates))
|
| 448 |
+
|
| 449 |
+
if self.proc.guardian_legacy:
|
| 450 |
+
try:
|
| 451 |
+
# Prepare Vectorized Features for the entire timeline for correctness
|
| 452 |
+
X_v2_full = calculate_legacy_v2_vectorized(df_1m, df_5m, df_15m)
|
| 453 |
+
v3_feats = self.proc.guardian_legacy.v3_feature_names
|
| 454 |
+
X_v3_full = calculate_legacy_v3_vectorized(df_1m, df_5m, df_15m, v3_feats)
|
| 455 |
+
|
| 456 |
+
# Map candidates to full df indices
|
| 457 |
+
# valid_mask is boolean array same length as df_1m (approx)
|
| 458 |
+
# We need exact indices in X_v2_full matching df_candidates
|
| 459 |
+
all_indices = np.arange(len(df_1m))
|
| 460 |
+
cand_indices = all_indices[valid_mask]
|
| 461 |
+
|
| 462 |
+
# Safety Clip
|
| 463 |
+
max_len = len(X_v2_full)
|
| 464 |
+
cand_indices = cand_indices[cand_indices < max_len]
|
| 465 |
+
|
| 466 |
+
if len(cand_indices) > 0:
|
| 467 |
+
# Predict V2
|
| 468 |
+
if self.proc.guardian_legacy.model_v2 and X_v2_full is not None:
|
| 469 |
+
subset_v2 = X_v2_full[cand_indices]
|
| 470 |
+
preds_v2 = self.proc.guardian_legacy.model_v2.predict(xgb.DMatrix(subset_v2))
|
| 471 |
+
if len(preds_v2.shape) > 1: res_legacy_v2[:len(cand_indices)] = preds_v2[:, 2] # Panic Class
|
| 472 |
+
else: res_legacy_v2[:len(cand_indices)] = preds_v2
|
| 473 |
+
|
| 474 |
+
# Predict V3
|
| 475 |
+
if self.proc.guardian_legacy.model_v3 and X_v3_full is not None:
|
| 476 |
+
subset_v3 = X_v3_full[cand_indices]
|
| 477 |
+
preds_v3 = self.proc.guardian_legacy.model_v3.predict(xgb.DMatrix(subset_v3))
|
| 478 |
+
res_legacy_v3[:len(cand_indices)] = preds_v3
|
| 479 |
+
|
| 480 |
+
except Exception as e:
|
| 481 |
+
print(f"β Legacy Guard Error: {e}")
|
| 482 |
+
|
| 483 |
+
# 8οΈβ£ FINAL ASSEMBLY
|
| 484 |
print(f" π [Stats] Titan:{res_titan.mean():.2f} | Patterns:{res_patterns.mean():.2f} | Sniper:{res_sniper.mean():.2f} | Oracle:{res_oracle.mean():.2f}")
|
| 485 |
|
| 486 |
ai_df = pd.DataFrame({
|
|
|
|
| 492 |
'sniper_score': res_sniper,
|
| 493 |
'l1_score': df_candidates['l1_score'],
|
| 494 |
'risk_hydra_crash': res_hydra_risk,
|
| 495 |
+
'risk_legacy_v2': res_legacy_v2, # β
REAL
|
| 496 |
+
'risk_legacy_v3': res_legacy_v3 # β
REAL
|
|
|
|
| 497 |
})
|
| 498 |
|
| 499 |
dt = time.time() - t0
|
|
|
|
| 502 |
print(f" β
[{sym}] Completed {len(ai_df)} signals in {dt:.2f} seconds.", flush=True)
|
| 503 |
gc.collect()
|
| 504 |
|
|
|
|
| 505 |
async def generate_truth_data(self):
|
| 506 |
if self.force_start_date:
|
| 507 |
dt_s = datetime.strptime(self.force_start_date, "%Y-%m-%d").replace(tzinfo=timezone.utc)
|
|
|
|
| 511 |
for sym in self.TARGET_COINS:
|
| 512 |
c = await self._fetch_all_data_fast(sym, ms_s, ms_e)
|
| 513 |
if c: await self._process_data_in_memory(sym, c, ms_s, ms_e)
|
| 514 |
+
|
|
|
|
|
|
|
| 515 |
@staticmethod
|
| 516 |
def _worker_optimize(combinations_batch, scores_files, initial_capital, fees_pct, max_slots):
|
| 517 |
print(f" β³ [System] Loading {len(scores_files)} datasets...", flush=True)
|
|
|
|
| 522 |
if not data: return []
|
| 523 |
df = pd.concat(data).sort_values('timestamp')
|
| 524 |
|
| 525 |
+
# Arrays
|
| 526 |
ts = df['timestamp'].values; close = df['close'].values.astype(float)
|
| 527 |
sym = df['symbol'].values; sym_map = {s:i for i,s in enumerate(np.unique(sym))}
|
| 528 |
sym_id = np.array([sym_map[s] for s in sym])
|
|
|
|
| 531 |
hydra = df['risk_hydra_crash'].values; titan = df['real_titan'].values
|
| 532 |
l1 = df['l1_score'].values
|
| 533 |
|
| 534 |
+
legacy_v2 = df['risk_legacy_v2'].values # β
REAL
|
| 535 |
+
legacy_v3 = df['risk_legacy_v3'].values # β
REAL
|
| 536 |
+
|
| 537 |
N = len(ts)
|
| 538 |
print(f" π [System] Testing {len(combinations_batch)} configs on {N} candles...", flush=True)
|
| 539 |
|
| 540 |
res = []
|
| 541 |
for cfg in combinations_batch:
|
| 542 |
+
pos = {}; log = []
|
| 543 |
bal = initial_capital; alloc = 0.0
|
| 544 |
|
|
|
|
| 545 |
mask = (l1 >= cfg['l1_thresh']) & (oracle >= cfg['oracle_thresh']) & (sniper >= cfg['sniper_thresh'])
|
| 546 |
|
| 547 |
for i in range(N):
|
| 548 |
s = sym_id[i]; p = close[i]
|
| 549 |
|
| 550 |
+
# Exit
|
| 551 |
if s in pos:
|
| 552 |
+
entry = pos[s][0]; h_r = pos[s][1]; titan_entry = pos[s][3]
|
|
|
|
|
|
|
| 553 |
|
| 554 |
+
crash_hydra = (h_r > cfg['hydra_thresh'])
|
| 555 |
+
panic_legacy = (legacy_v2[i] > cfg['legacy_thresh']) or (legacy_v3[i] > cfg['legacy_thresh'])
|
| 556 |
|
| 557 |
+
pnl = (p - entry)/entry
|
| 558 |
+
|
| 559 |
+
if crash_hydra or panic_legacy or pnl > 0.04 or pnl < -0.02:
|
| 560 |
+
realized = pnl - fees_pct*2
|
| 561 |
+
bal += pos[s][2] * (1 + realized)
|
|
|
|
| 562 |
alloc -= pos[s][2]
|
| 563 |
|
| 564 |
+
is_consensus = (titan_entry > 0.55)
|
| 565 |
+
log.append({'pnl': realized, 'consensus': is_consensus})
|
|
|
|
|
|
|
| 566 |
del pos[s]
|
| 567 |
|
| 568 |
+
# Entry
|
| 569 |
if len(pos) < max_slots and mask[i]:
|
| 570 |
if s not in pos and bal >= 5.0:
|
| 571 |
+
size = min(10.0, bal * 0.98)
|
|
|
|
| 572 |
pos[s] = (p, hydra[i], size, titan[i])
|
| 573 |
bal -= size; alloc += size
|
| 574 |
|
| 575 |
+
# Metrics
|
| 576 |
final_bal = bal + alloc
|
| 577 |
+
profit = final_bal - initial_capital
|
| 578 |
+
tot = len(log)
|
| 579 |
wins = sum(1 for x in log if x['pnl'] > 0)
|
| 580 |
+
win_rate = (wins/tot*100) if tot else 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 581 |
|
| 582 |
+
# Consensus Metrics
|
| 583 |
+
cons_trades = [x for x in log if x['consensus']]
|
| 584 |
+
n_cons = len(cons_trades)
|
| 585 |
+
agree_rate = (n_cons/tot*100) if tot else 0
|
| 586 |
+
cons_win_rate = (sum(1 for x in cons_trades if x['pnl']>0)/n_cons*100) if n_cons else 0
|
| 587 |
+
cons_avg_pnl = (sum(x['pnl'] for x in cons_trades)/n_cons*100) if n_cons else 0
|
| 588 |
+
|
| 589 |
res.append({
|
| 590 |
+
'config': cfg, 'final_balance': final_bal, 'net_profit': profit,
|
| 591 |
+
'total_trades': tot, 'win_rate': win_rate, 'max_drawdown': 0,
|
| 592 |
+
'consensus_agreement_rate': agree_rate,
|
| 593 |
+
'high_consensus_win_rate': cons_win_rate,
|
| 594 |
+
'high_consensus_avg_pnl': cons_avg_pnl
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
})
|
|
|
|
| 596 |
return res
|
| 597 |
|
|
|
|
|
|
|
|
|
|
| 598 |
async def run_optimization(self, target_regime="RANGE"):
|
| 599 |
await self.generate_truth_data()
|
| 600 |
|
| 601 |
+
# Grid
|
| 602 |
+
oracle_r = np.linspace(0.3, 0.7, 3); sniper_r = np.linspace(0.2, 0.6, 3)
|
| 603 |
+
hydra_r = [0.8, 0.9]; l1_r = [5.0, 10.0]
|
|
|
|
|
|
|
| 604 |
|
| 605 |
combos = []
|
| 606 |
for o, s, h, l1 in itertools.product(oracle_r, sniper_r, hydra_r, l1_r):
|
| 607 |
combos.append({
|
| 608 |
'w_titan': 0.4, 'w_struct': 0.3,
|
| 609 |
+
'l1_thresh': l1, 'oracle_thresh': o, 'sniper_thresh': s, 'hydra_thresh': h, 'legacy_thresh': 0.95
|
|
|
|
| 610 |
})
|
| 611 |
|
| 612 |
files = glob.glob(os.path.join(CACHE_DIR, "*.pkl"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 613 |
results_list = self._worker_optimize(combos, files, self.INITIAL_CAPITAL, self.TRADING_FEES, self.MAX_SLOTS)
|
| 614 |
|
| 615 |
+
# β
FIX: Handle empty result & Sort for 'best'
|
| 616 |
if not results_list:
|
| 617 |
print("β οΈ [Warning] No trades generated in any config.")
|
| 618 |
return None, {'net_profit': 0.0, 'win_rate': 0.0}
|
| 619 |
|
|
|
|
|
|
|
| 620 |
results_list.sort(key=lambda x: x['net_profit'], reverse=True)
|
| 621 |
+
best = results_list[0]
|
| 622 |
|
| 623 |
print("\n" + "="*60)
|
| 624 |
print(f"π CHAMPION REPORT [{target_regime}]:")
|
|
|
|
| 627 |
print("-" * 60)
|
| 628 |
print(f" π Total Trades: {best['total_trades']}")
|
| 629 |
print(f" π Win Rate: {best['win_rate']:.1f}%")
|
| 630 |
+
print(f" π Max Drawdown: {best['max_drawdown']:.1f}%")
|
| 631 |
print("-" * 60)
|
| 632 |
print(f" π§ CONSENSUS ANALYTICS:")
|
| 633 |
+
print(f" π€ Model Agreement Rate: {best['consensus_agreement_rate']:.1f}%")
|
| 634 |
print(f" π High-Consensus Win Rate: {best['high_consensus_win_rate']:.1f}%")
|
| 635 |
print(f" π High-Consensus Avg PnL: {best['high_consensus_avg_pnl']:.2f}%")
|
| 636 |
print("-" * 60)
|
| 637 |
print(f" βοΈ Oracle={best['config']['oracle_thresh']:.2f} | Sniper={best['config']['sniper_thresh']:.2f} | Hydra={best['config']['hydra_thresh']:.2f}")
|
| 638 |
+
print(f" βοΈ Weights: Titan={best['config']['w_titan']:.2f} | Patterns={best['config']['w_struct']:.2f} | L1={best['config']['l1_thresh']}")
|
| 639 |
print("="*60)
|
|
|
|
| 640 |
return best['config'], best
|
| 641 |
+
|
| 642 |
async def run_strategic_optimization_task():
|
| 643 |
print("\nπ§ͺ [STRATEGIC BACKTEST] Full Spectrum Mode...")
|
| 644 |
r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
|
| 645 |
await dm.initialize(); await proc.initialize()
|
| 646 |
+
if proc.guardian_hydra: proc.guardian_hydra.set_silent_mode(True)
|
| 647 |
|
| 648 |
hub = AdaptiveHub(r2); await hub.initialize()
|
| 649 |
opt = HeavyDutyBacktester(dm, proc)
|
|
|
|
| 659 |
opt.set_date_range(s["start"], s["end"])
|
| 660 |
best_cfg, best_stats = await opt.run_optimization(s["regime"])
|
| 661 |
if best_cfg: hub.submit_challenger(s["regime"], best_cfg, best_stats)
|
| 662 |
+
|
| 663 |
+
# Clean up
|
| 664 |
+
await dm.close()
|
| 665 |
await hub._save_state_to_r2()
|
| 666 |
print("β
[System] DNA Updated.")
|
| 667 |
|