Spaces:
Paused
Paused
Update backtest_engine.py
Browse files- backtest_engine.py +53 -110
backtest_engine.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# ============================================================
|
| 2 |
-
# 🧪 backtest_engine.py (V122.
|
| 3 |
# ============================================================
|
| 4 |
|
| 5 |
import asyncio
|
|
@@ -34,10 +34,10 @@ logging.getLogger('ml_engine').setLevel(logging.WARNING)
|
|
| 34 |
CACHE_DIR = "backtest_real_scores"
|
| 35 |
|
| 36 |
# ============================================================
|
| 37 |
-
# 🔧 1.
|
| 38 |
# ============================================================
|
|
|
|
| 39 |
def _zv(x):
|
| 40 |
-
"""Z-Score Normalization (Safe)"""
|
| 41 |
with np.errstate(divide='ignore', invalid='ignore'):
|
| 42 |
x = np.asarray(x, dtype="float32")
|
| 43 |
m = np.nanmean(x, axis=0)
|
|
@@ -45,39 +45,27 @@ def _zv(x):
|
|
| 45 |
return np.nan_to_num((x - m) / s, nan=0.0)
|
| 46 |
|
| 47 |
def _transform_window_for_pattern(df_window):
|
| 48 |
-
"""
|
| 49 |
-
تحويل نافذة (200 شمعة) إلى متجه ميزات كما في patterns.py تماماً.
|
| 50 |
-
"""
|
| 51 |
try:
|
| 52 |
-
# Extract numpy arrays
|
| 53 |
c = df_window['close'].values.astype('float32')
|
| 54 |
o = df_window['open'].values.astype('float32')
|
| 55 |
h = df_window['high'].values.astype('float32')
|
| 56 |
l = df_window['low'].values.astype('float32')
|
| 57 |
v = df_window['volume'].values.astype('float32')
|
| 58 |
|
| 59 |
-
|
| 60 |
-
base = np.stack([o, h, l, c, v], axis=1) # (200, 5)
|
| 61 |
base_z = _zv(base)
|
| 62 |
|
| 63 |
-
# 2. Extra
|
| 64 |
lr = np.zeros_like(c); lr[1:] = np.diff(np.log1p(c))
|
| 65 |
rng = (h - l) / (c + 1e-9)
|
| 66 |
extra = np.stack([lr, rng], axis=1)
|
| 67 |
extra_z = _zv(extra)
|
| 68 |
|
| 69 |
-
# 3. Indicators (Simplified Calculation for Speed using TA-Lib vectorization if possible, else manual)
|
| 70 |
-
# Using pandas_ta on small window is slow. We use numpy optimized versions.
|
| 71 |
-
# EMA
|
| 72 |
def _ema(arr, n):
|
| 73 |
-
alpha = 2/(n+1)
|
| 74 |
-
# Fast recursive implementation or pandas ewm
|
| 75 |
return pd.Series(arr).ewm(span=n, adjust=False).mean().values
|
| 76 |
|
| 77 |
ema9 = _ema(c, 9); ema21 = _ema(c, 21); ema50 = _ema(c, 50); ema200 = _ema(c, 200)
|
| 78 |
slope21 = np.gradient(ema21); slope50 = np.gradient(ema50)
|
| 79 |
|
| 80 |
-
# RSI (Numpy version approx)
|
| 81 |
delta = np.diff(c, prepend=c[0])
|
| 82 |
up, down = delta.copy(), delta.copy()
|
| 83 |
up[up < 0] = 0; down[down > 0] = 0
|
|
@@ -86,30 +74,18 @@ def _transform_window_for_pattern(df_window):
|
|
| 86 |
rs = roll_up / (roll_down + 1e-9)
|
| 87 |
rsi = 100.0 - (100.0 / (1.0 + rs))
|
| 88 |
|
| 89 |
-
# ATR & BB & OBV (Simplified for speed match)
|
| 90 |
-
# ... (Assuming simplified indicators for pattern matching structure)
|
| 91 |
-
|
| 92 |
-
# Stack indicators
|
| 93 |
indicators = np.stack([ema9, ema21, ema50, ema200, slope21, slope50, rsi], axis=1)
|
| 94 |
-
# Padding to match 12 features if needed (Patterns.py has 12)
|
| 95 |
-
# We fill zeros for heavy ones like MACD/OBV to save time, relying on EMA structure
|
| 96 |
padding = np.zeros((200, 5), dtype='float32')
|
| 97 |
indicators_full = np.concatenate([indicators, padding], axis=1)
|
| 98 |
indicators_z = _zv(indicators_full)
|
| 99 |
|
| 100 |
-
|
| 101 |
-
X_seq = np.concatenate([base_z, extra_z, indicators_z], axis=1) # (200, Features)
|
| 102 |
X_flat = X_seq.flatten()
|
| 103 |
-
|
| 104 |
-
# 5. Stat (MC/Hurst - Mocked for speed as 0.5)
|
| 105 |
X_stat = np.array([0.5, 0.0, 0.5], dtype="float32")
|
| 106 |
|
| 107 |
return np.concatenate([X_flat, X_stat])
|
| 108 |
except: return None
|
| 109 |
|
| 110 |
-
# ============================================================
|
| 111 |
-
# 🔧 2. SNIPER FEATURES (EXACT REPLICA)
|
| 112 |
-
# ============================================================
|
| 113 |
def _z_score_rolling(x, w=500):
|
| 114 |
r = x.rolling(w).mean(); s = x.rolling(w).std().replace(0, np.nan); return ((x - r) / s).fillna(0)
|
| 115 |
|
|
@@ -150,9 +126,6 @@ def calculate_sniper_features_exact(df):
|
|
| 150 |
|
| 151 |
return df.fillna(0)
|
| 152 |
|
| 153 |
-
# ============================================================
|
| 154 |
-
# 🔧 3. TITAN FEATURES (REAL)
|
| 155 |
-
# ============================================================
|
| 156 |
def calculate_titan_features_real(df):
|
| 157 |
df = df.copy()
|
| 158 |
df['RSI'] = ta.rsi(df['close'], 14)
|
|
@@ -191,29 +164,47 @@ class HeavyDutyBacktester:
|
|
| 191 |
for f in glob.glob(os.path.join(CACHE_DIR, "*")): os.remove(f)
|
| 192 |
else: os.makedirs(CACHE_DIR)
|
| 193 |
|
| 194 |
-
print(f"🧪 [Backtest V122.
|
| 195 |
self._check_engines()
|
| 196 |
|
| 197 |
def _check_engines(self):
|
| 198 |
status = []
|
| 199 |
-
if self.proc.titan and self.proc.titan.model: status.append("Titan")
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
if self.proc.
|
| 204 |
-
if self.proc.
|
|
|
|
| 205 |
print(f" ✅ Engines Ready: {', '.join(status)}")
|
| 206 |
|
| 207 |
def set_date_range(self, start_str, end_str):
|
| 208 |
self.force_start_date = start_str; self.force_end_date = end_str
|
| 209 |
|
|
|
|
| 210 |
def _smart_predict(self, model, X):
|
|
|
|
| 211 |
try:
|
|
|
|
| 212 |
if hasattr(model, "predict_proba"):
|
| 213 |
raw = model.predict_proba(X)
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 215 |
return model.predict(X)
|
| 216 |
-
except:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
async def _fetch_all_data_fast(self, sym, start_ms, end_ms):
|
| 219 |
print(f" ⚡ [Network] Downloading {sym}...", flush=True)
|
|
@@ -257,9 +248,7 @@ class HeavyDutyBacktester:
|
|
| 257 |
df_1m.set_index('datetime', inplace=True)
|
| 258 |
df_1m = df_1m.sort_index()
|
| 259 |
|
| 260 |
-
# -----------------------------------------------------------
|
| 261 |
# 1️⃣ SNIPER & L1 SCORING (L4/L1)
|
| 262 |
-
# -----------------------------------------------------------
|
| 263 |
df_sniper = calculate_sniper_features_exact(df_1m)
|
| 264 |
df_sniper['rel_vol'] = df_sniper['volume'] / (df_sniper['volume'].rolling(50).mean() + 1e-9)
|
| 265 |
df_sniper['l1_score'] = (df_sniper['rel_vol'] * 10) + ((df_sniper['atr']/df_sniper['close']) * 1000)
|
|
@@ -270,71 +259,45 @@ class HeavyDutyBacktester:
|
|
| 270 |
|
| 271 |
print(f" 🎯 Candidates: {len(df_candidates)}. Running Deep Inference...", flush=True)
|
| 272 |
|
| 273 |
-
#
|
| 274 |
-
# 2️⃣ PATTERNS (L2) - RUN ON 15M -> MAP TO 1M
|
| 275 |
-
# -----------------------------------------------------------
|
| 276 |
res_patterns = np.full(len(df_candidates), 0.5)
|
| 277 |
pattern_models = getattr(self.proc.pattern_engine, 'models', {})
|
| 278 |
-
|
| 279 |
if pattern_models and '15m' in pattern_models:
|
| 280 |
try:
|
| 281 |
-
# Resample to 15m
|
| 282 |
df_15m = df_1m.resample('15T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
|
| 283 |
-
|
| 284 |
-
# Create Windows (200 candles)
|
| 285 |
-
# Need to iterate efficiently
|
| 286 |
pat_scores_15m = np.full(len(df_15m), 0.5)
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
# Simplified: Loop through 15m (8k candles) is fast enough
|
| 290 |
-
pat_inputs = []
|
| 291 |
-
valid_15m_idxs = []
|
| 292 |
|
| 293 |
for i in range(200, len(df_15m)):
|
| 294 |
window = df_15m.iloc[i-200:i]
|
| 295 |
vec = _transform_window_for_pattern(window)
|
| 296 |
if vec is not None:
|
| 297 |
-
pat_inputs.append(vec)
|
| 298 |
-
valid_15m_idxs.append(i)
|
| 299 |
|
| 300 |
if pat_inputs:
|
| 301 |
X_pat = np.array(pat_inputs)
|
| 302 |
pat_preds = self._smart_predict(pattern_models['15m'], xgb.DMatrix(X_pat))
|
| 303 |
pat_scores_15m[valid_15m_idxs] = pat_preds
|
| 304 |
|
| 305 |
-
# Map 15m scores back to candidates (Forward Fill)
|
| 306 |
-
cand_ts = df_candidates['timestamp'].values
|
| 307 |
-
# 15m timestamps (end of candle approx)
|
| 308 |
ts_15m = df_15m.index.astype(np.int64) // 10**6
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
map_idxs = np.searchsorted(ts_15m, cand_ts) - 1
|
| 312 |
-
map_idxs = np.clip(map_idxs, 0, len(pat_scores_15m)-1)
|
| 313 |
-
res_patterns = pat_scores_15m[map_idxs]
|
| 314 |
-
|
| 315 |
except Exception as e: print(f"Patterns Error: {e}")
|
| 316 |
|
| 317 |
-
#
|
| 318 |
-
# 3️⃣ TITAN (L2) - RESAMPLE 5M
|
| 319 |
-
# -----------------------------------------------------------
|
| 320 |
res_titan = np.full(len(df_candidates), 0.5)
|
| 321 |
if self.proc.titan and self.proc.titan.model:
|
| 322 |
try:
|
| 323 |
df_5m = df_1m.resample('5T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
|
| 324 |
df_5m_feat = calculate_titan_features_real(df_5m).add_prefix('5m_')
|
| 325 |
-
|
| 326 |
-
cand_ts = df_candidates['timestamp'].values
|
| 327 |
ts_5m = df_5m.index.astype(np.int64) // 10**6
|
| 328 |
-
map_idxs = np.clip(np.searchsorted(ts_5m,
|
| 329 |
-
|
| 330 |
feats = self.proc.titan.feature_names
|
| 331 |
X_titan = df_5m_feat.iloc[map_idxs].reindex(columns=feats, fill_value=0).values
|
| 332 |
res_titan = self.proc.titan.model.predict(xgb.DMatrix(X_titan, feature_names=feats))
|
| 333 |
except Exception as e: print(f"Titan Error: {e}")
|
| 334 |
|
| 335 |
-
# -----------------------------------------------------------
|
| 336 |
# 4️⃣ SNIPER (L4)
|
| 337 |
-
# -----------------------------------------------------------
|
| 338 |
res_sniper = np.full(len(df_candidates), 0.5)
|
| 339 |
sniper_models = getattr(self.proc.sniper, 'models', [])
|
| 340 |
if sniper_models:
|
|
@@ -343,28 +306,17 @@ class HeavyDutyBacktester:
|
|
| 343 |
for f in feats:
|
| 344 |
if f not in df_candidates.columns: df_candidates[f] = 0.0
|
| 345 |
X_snip = df_candidates[feats].values
|
|
|
|
| 346 |
preds = [self._extract_probs(self._smart_predict(m, X_snip)) for m in sniper_models]
|
| 347 |
res_sniper = np.mean(preds, axis=0)
|
| 348 |
except Exception as e: print(f"Sniper Error: {e}")
|
| 349 |
|
| 350 |
-
# -----------------------------------------------------------
|
| 351 |
# 5️⃣ ORACLE (L3)
|
| 352 |
-
# -----------------------------------------------------------
|
| 353 |
res_oracle = np.full(len(df_candidates), 0.5)
|
| 354 |
oracle_model = getattr(self.proc.oracle, 'model_direction', None)
|
| 355 |
if oracle_model:
|
| 356 |
try:
|
| 357 |
-
# We reuse res_titan and res_patterns here!
|
| 358 |
oracle_feats = getattr(self.proc.oracle, 'feature_cols', [])
|
| 359 |
-
# Placeholder HTF logic (reusing L2/L4 data for speed as 'best effort')
|
| 360 |
-
# In real system, Processor gathers these. Here we simulate the gather.
|
| 361 |
-
# Titan Score -> 'sim_titan_score'
|
| 362 |
-
# Pattern Score -> 'sim_pattern_score'
|
| 363 |
-
|
| 364 |
-
# Build Oracle Vector
|
| 365 |
-
# We cheat slightly by filling HTF cols with zeros (neutral) or proxies
|
| 366 |
-
# because fetching full 1h/4h features for 128k candles is too heavy.
|
| 367 |
-
# But we provide the Critical Scores.
|
| 368 |
X_orc_df = pd.DataFrame(0.0, index=range(len(df_candidates)), columns=oracle_feats)
|
| 369 |
if 'sim_titan_score' in X_orc_df: X_orc_df['sim_titan_score'] = res_titan
|
| 370 |
if 'sim_pattern_score' in X_orc_df: X_orc_df['sim_pattern_score'] = res_patterns
|
|
@@ -373,15 +325,11 @@ class HeavyDutyBacktester:
|
|
| 373 |
res_oracle = self._extract_probs(self._smart_predict(oracle_model, X_orc_df.values))
|
| 374 |
except Exception as e: print(f"Oracle Error: {e}")
|
| 375 |
|
| 376 |
-
# -----------------------------------------------------------
|
| 377 |
# 6️⃣ HYDRA & LEGACY (L0)
|
| 378 |
-
# -----------------------------------------------------------
|
| 379 |
res_hydra_risk = np.zeros(len(df_candidates)); res_hydra_time = np.zeros(len(df_candidates), dtype=int)
|
| 380 |
-
res_legacy_risk = np.zeros(len(df_candidates))
|
| 381 |
|
| 382 |
hydra_models = getattr(self.proc.guardian_hydra, 'models', {})
|
| 383 |
if hydra_models and 'crash' in hydra_models:
|
| 384 |
-
# Sliding window approach
|
| 385 |
try:
|
| 386 |
global_hydra_feats = np.column_stack([
|
| 387 |
df_sniper['rsi_14'], df_sniper['rsi_14'], df_sniper['rsi_14'],
|
|
@@ -390,14 +338,11 @@ class HeavyDutyBacktester:
|
|
| 390 |
]).astype(np.float32)
|
| 391 |
|
| 392 |
window_view = sliding_window_view(global_hydra_feats, 240, axis=0).transpose(0, 2, 1)
|
| 393 |
-
|
| 394 |
-
# Map indices
|
| 395 |
c_idxs = np.searchsorted(df_sniper.index, df_candidates.index)
|
| 396 |
valid_s = c_idxs + 1
|
| 397 |
valid_mask_h = valid_s < (len(global_hydra_feats) - 240)
|
| 398 |
final_s = valid_s[valid_mask_h]; res_idxs = np.where(valid_mask_h)[0]
|
| 399 |
|
| 400 |
-
# Batch Loop
|
| 401 |
for i in range(0, len(final_s), 5000):
|
| 402 |
b_idxs = final_s[i:i+5000]; r_idxs = res_idxs[i:i+5000]
|
| 403 |
static = window_view[b_idxs]
|
|
@@ -411,7 +356,6 @@ class HeavyDutyBacktester:
|
|
| 411 |
max_pnl = (np.maximum.accumulate(s_c, axis=1) - entry)/dist
|
| 412 |
atr_p = s_atr/(s_c+1e-9)
|
| 413 |
|
| 414 |
-
# Stack
|
| 415 |
zeros = np.zeros((B, 240)); ones = np.ones((B, 240)); t = np.tile(np.arange(1, 241), (B, 1))
|
| 416 |
X = np.stack([
|
| 417 |
static[:,0], static[:,1], static[:,2], static[:,3], static[:,4],
|
|
@@ -422,9 +366,7 @@ class HeavyDutyBacktester:
|
|
| 422 |
res_hydra_risk[r_idxs] = np.max(preds, axis=1)
|
| 423 |
except: pass
|
| 424 |
|
| 425 |
-
# -----------------------------------------------------------
|
| 426 |
# 7️⃣ FINAL ASSEMBLY
|
| 427 |
-
# -----------------------------------------------------------
|
| 428 |
print(f" 📊 [Stats] Titan:{res_titan.mean():.2f} | Patterns:{res_patterns.mean():.2f} | Sniper:{res_sniper.mean():.2f} | Oracle:{res_oracle.mean():.2f}")
|
| 429 |
|
| 430 |
ai_df = pd.DataFrame({
|
|
@@ -437,7 +379,7 @@ class HeavyDutyBacktester:
|
|
| 437 |
'l1_score': df_candidates['l1_score'],
|
| 438 |
'risk_hydra_crash': res_hydra_risk,
|
| 439 |
'time_hydra_crash': res_hydra_time,
|
| 440 |
-
'risk_legacy_v2':
|
| 441 |
'time_legacy_panic': 0
|
| 442 |
})
|
| 443 |
|
|
@@ -493,9 +435,6 @@ class HeavyDutyBacktester:
|
|
| 493 |
# Exit
|
| 494 |
if s in pos:
|
| 495 |
entry = pos[s][0]; h_r = pos[s][1]
|
| 496 |
-
# Check hydra risk vs config thresh (dynamic exit)
|
| 497 |
-
# For backtest, simplified: if risk > thresh AND current PnL < -1% (validation)
|
| 498 |
-
# Or just risk > thresh
|
| 499 |
crash = (h_r > cfg['hydra_thresh'])
|
| 500 |
pnl = (p - entry)/entry
|
| 501 |
|
|
@@ -526,21 +465,24 @@ class HeavyDutyBacktester:
|
|
| 526 |
await self.generate_truth_data()
|
| 527 |
|
| 528 |
# Grid
|
| 529 |
-
# Using wider ranges to ensure we find *something*
|
| 530 |
oracle_r = np.linspace(0.3, 0.7, 3); sniper_r = np.linspace(0.2, 0.6, 3)
|
| 531 |
hydra_r = [0.8, 0.9]; l1_r = [5.0, 10.0]
|
| 532 |
|
| 533 |
combos = []
|
| 534 |
for o, s, h, l1 in itertools.product(oracle_r, sniper_r, hydra_r, l1_r):
|
| 535 |
combos.append({
|
| 536 |
-
'w_titan': 0.4, 'w_struct': 0.3,
|
| 537 |
-
'l1_thresh': l1, 'oracle_thresh': o, 'sniper_thresh': s, 'hydra_thresh': h
|
| 538 |
})
|
| 539 |
|
| 540 |
files = glob.glob(os.path.join(CACHE_DIR, "*.pkl"))
|
| 541 |
best_res = self._worker_optimize(combos, files, self.INITIAL_CAPITAL, self.TRADING_FEES, self.MAX_SLOTS)
|
| 542 |
-
if not best_res: return None, None
|
| 543 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 544 |
print("\n" + "="*60)
|
| 545 |
print(f"🏆 CHAMPION REPORT [{target_regime}]:")
|
| 546 |
print(f" 💰 Final Balance: ${best['final_balance']:,.2f}")
|
|
@@ -559,7 +501,6 @@ class HeavyDutyBacktester:
|
|
| 559 |
print(f" ⚖️ Weights: Titan={best['config']['w_titan']:.2f} | Patterns={best['config']['w_struct']:.2f} | L1={best['config']['l1_thresh']}")
|
| 560 |
print("="*60)
|
| 561 |
return best['config'], best
|
| 562 |
-
|
| 563 |
async def run_strategic_optimization_task():
|
| 564 |
print("\n🧪 [STRATEGIC BACKTEST] Full Spectrum Mode...")
|
| 565 |
r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
|
|
@@ -570,9 +511,11 @@ async def run_strategic_optimization_task():
|
|
| 570 |
opt = HeavyDutyBacktester(dm, proc)
|
| 571 |
|
| 572 |
scenarios = [
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
|
|
|
|
|
|
| 576 |
|
| 577 |
for s in scenarios:
|
| 578 |
opt.set_date_range(s["start"], s["end"])
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
+
# 🧪 backtest_engine.py (V122.1 - GEM-Architect: Method Scope Fix)
|
| 3 |
# ============================================================
|
| 4 |
|
| 5 |
import asyncio
|
|
|
|
| 34 |
CACHE_DIR = "backtest_real_scores"
|
| 35 |
|
| 36 |
# ============================================================
|
| 37 |
+
# 🔧 1. FEATURE ENGINEERING REPLICAS (GLOBAL SCOPE)
|
| 38 |
# ============================================================
|
| 39 |
+
|
| 40 |
def _zv(x):
|
|
|
|
| 41 |
with np.errstate(divide='ignore', invalid='ignore'):
|
| 42 |
x = np.asarray(x, dtype="float32")
|
| 43 |
m = np.nanmean(x, axis=0)
|
|
|
|
| 45 |
return np.nan_to_num((x - m) / s, nan=0.0)
|
| 46 |
|
| 47 |
def _transform_window_for_pattern(df_window):
|
|
|
|
|
|
|
|
|
|
| 48 |
try:
|
|
|
|
| 49 |
c = df_window['close'].values.astype('float32')
|
| 50 |
o = df_window['open'].values.astype('float32')
|
| 51 |
h = df_window['high'].values.astype('float32')
|
| 52 |
l = df_window['low'].values.astype('float32')
|
| 53 |
v = df_window['volume'].values.astype('float32')
|
| 54 |
|
| 55 |
+
base = np.stack([o, h, l, c, v], axis=1)
|
|
|
|
| 56 |
base_z = _zv(base)
|
| 57 |
|
|
|
|
| 58 |
lr = np.zeros_like(c); lr[1:] = np.diff(np.log1p(c))
|
| 59 |
rng = (h - l) / (c + 1e-9)
|
| 60 |
extra = np.stack([lr, rng], axis=1)
|
| 61 |
extra_z = _zv(extra)
|
| 62 |
|
|
|
|
|
|
|
|
|
|
| 63 |
def _ema(arr, n):
|
|
|
|
|
|
|
| 64 |
return pd.Series(arr).ewm(span=n, adjust=False).mean().values
|
| 65 |
|
| 66 |
ema9 = _ema(c, 9); ema21 = _ema(c, 21); ema50 = _ema(c, 50); ema200 = _ema(c, 200)
|
| 67 |
slope21 = np.gradient(ema21); slope50 = np.gradient(ema50)
|
| 68 |
|
|
|
|
| 69 |
delta = np.diff(c, prepend=c[0])
|
| 70 |
up, down = delta.copy(), delta.copy()
|
| 71 |
up[up < 0] = 0; down[down > 0] = 0
|
|
|
|
| 74 |
rs = roll_up / (roll_down + 1e-9)
|
| 75 |
rsi = 100.0 - (100.0 / (1.0 + rs))
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
indicators = np.stack([ema9, ema21, ema50, ema200, slope21, slope50, rsi], axis=1)
|
|
|
|
|
|
|
| 78 |
padding = np.zeros((200, 5), dtype='float32')
|
| 79 |
indicators_full = np.concatenate([indicators, padding], axis=1)
|
| 80 |
indicators_z = _zv(indicators_full)
|
| 81 |
|
| 82 |
+
X_seq = np.concatenate([base_z, extra_z, indicators_z], axis=1)
|
|
|
|
| 83 |
X_flat = X_seq.flatten()
|
|
|
|
|
|
|
| 84 |
X_stat = np.array([0.5, 0.0, 0.5], dtype="float32")
|
| 85 |
|
| 86 |
return np.concatenate([X_flat, X_stat])
|
| 87 |
except: return None
|
| 88 |
|
|
|
|
|
|
|
|
|
|
| 89 |
def _z_score_rolling(x, w=500):
|
| 90 |
r = x.rolling(w).mean(); s = x.rolling(w).std().replace(0, np.nan); return ((x - r) / s).fillna(0)
|
| 91 |
|
|
|
|
| 126 |
|
| 127 |
return df.fillna(0)
|
| 128 |
|
|
|
|
|
|
|
|
|
|
| 129 |
def calculate_titan_features_real(df):
|
| 130 |
df = df.copy()
|
| 131 |
df['RSI'] = ta.rsi(df['close'], 14)
|
|
|
|
| 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 V122.1] Method Scope Fix. Checking Engines...")
|
| 168 |
self._check_engines()
|
| 169 |
|
| 170 |
def _check_engines(self):
|
| 171 |
status = []
|
| 172 |
+
if self.proc.titan and self.proc.titan.model: status.append("Titan(Real)")
|
| 173 |
+
else: print("❌ Titan Model MISSING")
|
| 174 |
+
if self.proc.pattern_engine and self.proc.pattern_engine.models: status.append("Patterns(Real)")
|
| 175 |
+
else: print("⚠️ Pattern Models MISSING")
|
| 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] # Positive/Buy Class
|
| 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)
|
| 199 |
+
|
| 200 |
+
def _extract_probs(self, raw_preds):
|
| 201 |
+
"""Standardizes output to 1D probability array"""
|
| 202 |
+
if isinstance(raw_preds, list): raw_preds = np.array(raw_preds)
|
| 203 |
+
if raw_preds.ndim == 1: return raw_preds
|
| 204 |
+
elif raw_preds.ndim == 2:
|
| 205 |
+
if raw_preds.shape[1] >= 2: return raw_preds[:, -1]
|
| 206 |
+
return raw_preds.flatten()
|
| 207 |
+
return raw_preds.flatten()
|
| 208 |
|
| 209 |
async def _fetch_all_data_fast(self, sym, start_ms, end_ms):
|
| 210 |
print(f" ⚡ [Network] Downloading {sym}...", flush=True)
|
|
|
|
| 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)
|
| 254 |
df_sniper['l1_score'] = (df_sniper['rel_vol'] * 10) + ((df_sniper['atr']/df_sniper['close']) * 1000)
|
|
|
|
| 259 |
|
| 260 |
print(f" 🎯 Candidates: {len(df_candidates)}. Running Deep Inference...", flush=True)
|
| 261 |
|
| 262 |
+
# 2️⃣ PATTERNS (L2)
|
|
|
|
|
|
|
| 263 |
res_patterns = np.full(len(df_candidates), 0.5)
|
| 264 |
pattern_models = getattr(self.proc.pattern_engine, 'models', {})
|
|
|
|
| 265 |
if pattern_models and '15m' in pattern_models:
|
| 266 |
try:
|
|
|
|
| 267 |
df_15m = df_1m.resample('15T').agg({'open':'first', 'high':'max', 'low':'min', 'close':'last', 'volume':'sum'}).dropna()
|
|
|
|
|
|
|
|
|
|
| 268 |
pat_scores_15m = np.full(len(df_15m), 0.5)
|
| 269 |
+
pat_inputs = []; valid_15m_idxs = []
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
|
| 271 |
for i in range(200, len(df_15m)):
|
| 272 |
window = df_15m.iloc[i-200:i]
|
| 273 |
vec = _transform_window_for_pattern(window)
|
| 274 |
if vec is not None:
|
| 275 |
+
pat_inputs.append(vec); valid_15m_idxs.append(i)
|
|
|
|
| 276 |
|
| 277 |
if pat_inputs:
|
| 278 |
X_pat = np.array(pat_inputs)
|
| 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 = df_15m.index.astype(np.int64) // 10**6
|
| 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}")
|
| 286 |
|
| 287 |
+
# 3️⃣ TITAN (L2)
|
|
|
|
|
|
|
| 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)
|
|
|
|
| 295 |
feats = self.proc.titan.feature_names
|
| 296 |
X_titan = df_5m_feat.iloc[map_idxs].reindex(columns=feats, fill_value=0).values
|
| 297 |
res_titan = self.proc.titan.model.predict(xgb.DMatrix(X_titan, feature_names=feats))
|
| 298 |
except Exception as e: print(f"Titan Error: {e}")
|
| 299 |
|
|
|
|
| 300 |
# 4️⃣ SNIPER (L4)
|
|
|
|
| 301 |
res_sniper = np.full(len(df_candidates), 0.5)
|
| 302 |
sniper_models = getattr(self.proc.sniper, 'models', [])
|
| 303 |
if sniper_models:
|
|
|
|
| 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}")
|
| 313 |
|
|
|
|
| 314 |
# 5️⃣ ORACLE (L3)
|
|
|
|
| 315 |
res_oracle = np.full(len(df_candidates), 0.5)
|
| 316 |
oracle_model = getattr(self.proc.oracle, 'model_direction', None)
|
| 317 |
if oracle_model:
|
| 318 |
try:
|
|
|
|
| 319 |
oracle_feats = getattr(self.proc.oracle, 'feature_cols', [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
X_orc_df = pd.DataFrame(0.0, index=range(len(df_candidates)), columns=oracle_feats)
|
| 321 |
if 'sim_titan_score' in X_orc_df: X_orc_df['sim_titan_score'] = res_titan
|
| 322 |
if 'sim_pattern_score' in X_orc_df: X_orc_df['sim_pattern_score'] = res_patterns
|
|
|
|
| 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 & LEGACY (L0)
|
|
|
|
| 329 |
res_hydra_risk = np.zeros(len(df_candidates)); res_hydra_time = np.zeros(len(df_candidates), dtype=int)
|
|
|
|
| 330 |
|
| 331 |
hydra_models = getattr(self.proc.guardian_hydra, 'models', {})
|
| 332 |
if hydra_models and 'crash' in hydra_models:
|
|
|
|
| 333 |
try:
|
| 334 |
global_hydra_feats = np.column_stack([
|
| 335 |
df_sniper['rsi_14'], df_sniper['rsi_14'], df_sniper['rsi_14'],
|
|
|
|
| 338 |
]).astype(np.float32)
|
| 339 |
|
| 340 |
window_view = sliding_window_view(global_hydra_feats, 240, axis=0).transpose(0, 2, 1)
|
|
|
|
|
|
|
| 341 |
c_idxs = np.searchsorted(df_sniper.index, df_candidates.index)
|
| 342 |
valid_s = c_idxs + 1
|
| 343 |
valid_mask_h = valid_s < (len(global_hydra_feats) - 240)
|
| 344 |
final_s = valid_s[valid_mask_h]; res_idxs = np.where(valid_mask_h)[0]
|
| 345 |
|
|
|
|
| 346 |
for i in range(0, len(final_s), 5000):
|
| 347 |
b_idxs = final_s[i:i+5000]; r_idxs = res_idxs[i:i+5000]
|
| 348 |
static = window_view[b_idxs]
|
|
|
|
| 356 |
max_pnl = (np.maximum.accumulate(s_c, axis=1) - entry)/dist
|
| 357 |
atr_p = s_atr/(s_c+1e-9)
|
| 358 |
|
|
|
|
| 359 |
zeros = np.zeros((B, 240)); ones = np.ones((B, 240)); t = np.tile(np.arange(1, 241), (B, 1))
|
| 360 |
X = np.stack([
|
| 361 |
static[:,0], static[:,1], static[:,2], static[:,3], static[:,4],
|
|
|
|
| 366 |
res_hydra_risk[r_idxs] = np.max(preds, axis=1)
|
| 367 |
except: pass
|
| 368 |
|
|
|
|
| 369 |
# 7️⃣ FINAL ASSEMBLY
|
|
|
|
| 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({
|
|
|
|
| 379 |
'l1_score': df_candidates['l1_score'],
|
| 380 |
'risk_hydra_crash': res_hydra_risk,
|
| 381 |
'time_hydra_crash': res_hydra_time,
|
| 382 |
+
'risk_legacy_v2': 0.0,
|
| 383 |
'time_legacy_panic': 0
|
| 384 |
})
|
| 385 |
|
|
|
|
| 435 |
# Exit
|
| 436 |
if s in pos:
|
| 437 |
entry = pos[s][0]; h_r = pos[s][1]
|
|
|
|
|
|
|
|
|
|
| 438 |
crash = (h_r > cfg['hydra_thresh'])
|
| 439 |
pnl = (p - entry)/entry
|
| 440 |
|
|
|
|
| 465 |
await self.generate_truth_data()
|
| 466 |
|
| 467 |
# Grid
|
|
|
|
| 468 |
oracle_r = np.linspace(0.3, 0.7, 3); sniper_r = np.linspace(0.2, 0.6, 3)
|
| 469 |
hydra_r = [0.8, 0.9]; l1_r = [5.0, 10.0]
|
| 470 |
|
| 471 |
combos = []
|
| 472 |
for o, s, h, l1 in itertools.product(oracle_r, sniper_r, hydra_r, l1_r):
|
| 473 |
combos.append({
|
| 474 |
+
'w_titan': 0.4, 'w_struct': 0.3,
|
| 475 |
+
'l1_thresh': l1, 'oracle_thresh': o, 'sniper_thresh': s, 'hydra_thresh': h, 'legacy_thresh': 0.95
|
| 476 |
})
|
| 477 |
|
| 478 |
files = glob.glob(os.path.join(CACHE_DIR, "*.pkl"))
|
| 479 |
best_res = self._worker_optimize(combos, files, self.INITIAL_CAPITAL, self.TRADING_FEES, self.MAX_SLOTS)
|
|
|
|
| 480 |
|
| 481 |
+
# ✅ FIX: Handle empty result
|
| 482 |
+
if not best_res:
|
| 483 |
+
print("⚠️ [Warning] No trades generated in any config.")
|
| 484 |
+
return None, {'net_profit': 0.0, 'win_rate': 0.0}
|
| 485 |
+
|
| 486 |
print("\n" + "="*60)
|
| 487 |
print(f"🏆 CHAMPION REPORT [{target_regime}]:")
|
| 488 |
print(f" 💰 Final Balance: ${best['final_balance']:,.2f}")
|
|
|
|
| 501 |
print(f" ⚖️ Weights: Titan={best['config']['w_titan']:.2f} | Patterns={best['config']['w_struct']:.2f} | L1={best['config']['l1_thresh']}")
|
| 502 |
print("="*60)
|
| 503 |
return best['config'], best
|
|
|
|
| 504 |
async def run_strategic_optimization_task():
|
| 505 |
print("\n🧪 [STRATEGIC BACKTEST] Full Spectrum Mode...")
|
| 506 |
r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
|
|
|
|
| 511 |
opt = HeavyDutyBacktester(dm, proc)
|
| 512 |
|
| 513 |
scenarios = [
|
| 514 |
+
{"regime": "BULL", "start": "2024-01-01", "end": "2024-03-30"},
|
| 515 |
+
{"regime": "BEAR", "start": "2023-08-01", "end": "2023-09-15"},
|
| 516 |
+
{"regime": "DEAD", "start": "2023-06-01", "end": "2023-08-01"},
|
| 517 |
+
{"regime": "RANGE", "start": "2024-07-01", "end": "2024-09-30"}
|
| 518 |
+
]
|
| 519 |
|
| 520 |
for s in scenarios:
|
| 521 |
opt.set_date_range(s["start"], s["end"])
|