Spaces:
Paused
Paused
Update backtest_engine.py
Browse files- backtest_engine.py +64 -83
backtest_engine.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# ============================================================
|
| 2 |
-
# 🧪 backtest_engine.py (
|
| 3 |
# ============================================================
|
| 4 |
|
| 5 |
import asyncio
|
|
@@ -47,7 +47,7 @@ def _z_roll(x, w=500):
|
|
| 47 |
return ((x - r) / s).fillna(0)
|
| 48 |
|
| 49 |
def _revive_score_distribution(scores):
|
| 50 |
-
scores = np.array(scores, dtype=np.float32)
|
| 51 |
if len(scores) < 10: return scores
|
| 52 |
std = np.std(scores)
|
| 53 |
if std < 0.05:
|
|
@@ -73,6 +73,9 @@ def _zv(x):
|
|
| 73 |
s = np.nanstd(x, axis=0) + 1e-9
|
| 74 |
return np.nan_to_num((x - m) / s, nan=0.0)
|
| 75 |
|
|
|
|
|
|
|
|
|
|
| 76 |
def _transform_window_for_pattern(df_window):
|
| 77 |
try:
|
| 78 |
c = df_window['close'].values.astype('float32')
|
|
@@ -80,12 +83,14 @@ def _transform_window_for_pattern(df_window):
|
|
| 80 |
h = df_window['high'].values.astype('float32')
|
| 81 |
l = df_window['low'].values.astype('float32')
|
| 82 |
v = df_window['volume'].values.astype('float32')
|
|
|
|
| 83 |
base = np.stack([o, h, l, c, v], axis=1)
|
| 84 |
base_z = _zv(base)
|
| 85 |
lr = np.zeros_like(c); lr[1:] = np.diff(np.log1p(c))
|
| 86 |
rng = (h - l) / (c + 1e-9)
|
| 87 |
extra = np.stack([lr, rng], axis=1)
|
| 88 |
extra_z = _zv(extra)
|
|
|
|
| 89 |
def _ema(arr, n): return pd.Series(arr).ewm(span=n, adjust=False).mean().values
|
| 90 |
ema9 = _ema(c, 9); ema21 = _ema(c, 21); ema50 = _ema(c, 50); ema200 = _ema(c, 200)
|
| 91 |
slope21 = np.gradient(ema21); slope50 = np.gradient(ema50)
|
|
@@ -96,6 +101,7 @@ def _transform_window_for_pattern(df_window):
|
|
| 96 |
roll_down = pd.Series(down).abs().ewm(alpha=1/14, adjust=False).mean().values
|
| 97 |
rs = roll_up / (roll_down + 1e-9)
|
| 98 |
rsi = 100.0 - (100.0 / (1.0 + rs))
|
|
|
|
| 99 |
indicators = np.stack([ema9, ema21, ema50, ema200, slope21, slope50, rsi], axis=1)
|
| 100 |
X_seq = np.concatenate([base_z, extra_z, _zv(indicators)], axis=1)
|
| 101 |
X_flat = X_seq.flatten()
|
|
@@ -114,13 +120,15 @@ class HeavyDutyBacktester:
|
|
| 114 |
self.INITIAL_CAPITAL = 10.0
|
| 115 |
self.TRADING_FEES = 0.001
|
| 116 |
self.MAX_SLOTS = 4
|
|
|
|
| 117 |
self.TARGET_COINS = [
|
| 118 |
-
'SOL/USDT'
|
| 119 |
-
|
| 120 |
self.force_start_date = None
|
| 121 |
self.force_end_date = None
|
|
|
|
| 122 |
if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
|
| 123 |
-
print(f"🧪 [Backtest
|
| 124 |
|
| 125 |
def set_date_range(self, start_str, end_str):
|
| 126 |
self.force_start_date = start_str
|
|
@@ -137,6 +145,7 @@ class HeavyDutyBacktester:
|
|
| 137 |
current += duration_per_batch
|
| 138 |
all_candles = []
|
| 139 |
sem = asyncio.Semaphore(20)
|
|
|
|
| 140 |
async def _fetch_batch(timestamp):
|
| 141 |
async with sem:
|
| 142 |
for _ in range(3):
|
|
@@ -144,6 +153,7 @@ class HeavyDutyBacktester:
|
|
| 144 |
return await self.dm.exchange.fetch_ohlcv(sym, '1m', since=timestamp, limit=limit)
|
| 145 |
except: await asyncio.sleep(0.5)
|
| 146 |
return []
|
|
|
|
| 147 |
chunk_size = 50
|
| 148 |
for i in range(0, len(tasks), chunk_size):
|
| 149 |
chunk_tasks = tasks[i:i + chunk_size]
|
|
@@ -151,6 +161,7 @@ class HeavyDutyBacktester:
|
|
| 151 |
results = await asyncio.gather(*futures)
|
| 152 |
for res in results:
|
| 153 |
if res: all_candles.extend(res)
|
|
|
|
| 154 |
if not all_candles: return None
|
| 155 |
df = pd.DataFrame(all_candles, columns=['timestamp', 'o', 'h', 'l', 'c', 'v'])
|
| 156 |
df.drop_duplicates('timestamp', inplace=True)
|
|
@@ -159,10 +170,15 @@ class HeavyDutyBacktester:
|
|
| 159 |
print(f" ✅ Downloaded {len(df)} candles.", flush=True)
|
| 160 |
return df.values.tolist()
|
| 161 |
|
|
|
|
|
|
|
|
|
|
| 162 |
def _calculate_indicators_vectorized(self, df, timeframe='1m'):
|
| 163 |
cols = ['close', 'high', 'low', 'volume', 'open']
|
| 164 |
for c in cols: df[c] = df[c].astype(np.float64)
|
| 165 |
idx = df.index
|
|
|
|
|
|
|
| 166 |
df['RSI'] = safe_ta(ta.rsi(df['close'], length=14), idx, 50)
|
| 167 |
macd = ta.macd(df['close'])
|
| 168 |
if macd is not None:
|
|
@@ -173,28 +189,35 @@ class HeavyDutyBacktester:
|
|
| 173 |
adx = ta.adx(df['high'], df['low'], df['close'], length=14)
|
| 174 |
if adx is not None: df['ADX'] = safe_ta(adx.iloc[:, 0], idx, 0)
|
| 175 |
else: df['ADX'] = 0.0
|
| 176 |
-
if timeframe == '1d':
|
|
|
|
|
|
|
| 177 |
for p in [9, 21, 50, 200]:
|
| 178 |
ema = safe_ta(ta.ema(df['close'], length=p), idx, 0)
|
| 179 |
df[f'EMA_{p}_dist'] = ((df['close'] / ema.replace(0, np.nan)) - 1).fillna(0)
|
| 180 |
df[f'ema{p}'] = ema
|
|
|
|
| 181 |
df['ema20'] = safe_ta(ta.ema(df['close'], length=20), idx, df['close'])
|
|
|
|
| 182 |
bb = ta.bbands(df['close'], length=20, std=2.0)
|
| 183 |
if bb is not None:
|
| 184 |
w = ((bb.iloc[:, 2] - bb.iloc[:, 0]) / bb.iloc[:, 1].replace(0, np.nan)).fillna(0)
|
| 185 |
p = ((df['close'] - bb.iloc[:, 0]) / (bb.iloc[:, 2] - bb.iloc[:, 0]).replace(0, np.nan)).fillna(0)
|
| 186 |
df['BB_w'] = w; df['BB_p'] = p; df['bb_width'] = w
|
| 187 |
else: df['BB_w'] = 0; df['BB_p'] = 0; df['bb_width'] = 0
|
|
|
|
| 188 |
df['MFI'] = safe_ta(ta.mfi(df['high'], df['low'], df['close'], df['volume'], length=14), idx, 50)
|
| 189 |
vwap = ta.vwap(df['high'], df['low'], df['close'], df['volume'])
|
| 190 |
if vwap is not None:
|
| 191 |
df['VWAP_dist'] = ((df['close'] / vwap.replace(0, np.nan)) - 1).fillna(0)
|
| 192 |
df['vwap'] = vwap
|
| 193 |
else: df['VWAP_dist'] = 0.0; df['vwap'] = df['close']
|
|
|
|
| 194 |
df['atr'] = safe_ta(ta.atr(df['high'], df['low'], df['close'], length=14), idx, 0)
|
| 195 |
df['atr_pct'] = (df['atr'] / df['close'].replace(0, np.nan)).fillna(0)
|
| 196 |
df['ATR_pct'] = df['atr_pct']
|
| 197 |
|
|
|
|
| 198 |
if timeframe == '1m':
|
| 199 |
df['return_1m'] = df['close'].pct_change().fillna(0)
|
| 200 |
df['return_3m'] = df['close'].pct_change(3).fillna(0)
|
|
@@ -233,14 +256,14 @@ class HeavyDutyBacktester:
|
|
| 233 |
df['rv_gk'] = _z_roll(rv_gk)
|
| 234 |
df['L_score'] = (df['vol_zscore_50'] - df['amihud'] - df['roll_spread'] - df['rv_gk'].abs() - df['vwap_dev'].abs() + df['ofi']).fillna(0)
|
| 235 |
|
|
|
|
| 236 |
df['slope'] = safe_ta(ta.slope(df['close'], length=7), idx, 0)
|
| 237 |
vol_mean = df['volume'].rolling(20).mean()
|
| 238 |
vol_std = df['volume'].rolling(20).std().replace(0, np.nan)
|
| 239 |
df['vol_z'] = ((df['volume'] - vol_mean) / vol_std).fillna(0)
|
| 240 |
df['rel_vol'] = df['volume'] / (df['volume'].rolling(50).mean() + 1e-9)
|
| 241 |
df['log_ret'] = np.log(df['close'] / df['close'].shift(1).replace(0, np.nan)).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 |
e20_s = df['ema20'].shift(5).replace(0, np.nan)
|
|
@@ -252,19 +275,25 @@ class HeavyDutyBacktester:
|
|
| 252 |
e200 = safe_ta(ta.ema(df['close'], length=200), idx, df['close'])
|
| 253 |
df['ema200'] = e200
|
| 254 |
df['dist_ema200'] = ((df['close'] - e200) / e200.replace(0, np.nan)).fillna(0)
|
|
|
|
| 255 |
if timeframe == '1m':
|
| 256 |
for lag in [1, 2, 3, 5, 10, 20]:
|
| 257 |
df[f'log_ret_lag_{lag}'] = df['log_ret'].shift(lag).fillna(0)
|
| 258 |
df[f'rsi_lag_{lag}'] = (df['RSI'].shift(lag) / 100.0).fillna(0.5)
|
| 259 |
df[f'fib_pos_lag_{lag}'] = df['fib_pos'].shift(lag).fillna(0.5)
|
| 260 |
df[f'volatility_lag_{lag}'] = df['volatility'].shift(lag).fillna(0)
|
|
|
|
| 261 |
df.fillna(0, inplace=True)
|
| 262 |
return df
|
| 263 |
|
|
|
|
|
|
|
|
|
|
| 264 |
async def _process_data_in_memory(self, sym, candles, start_ms, end_ms):
|
| 265 |
safe_sym = sym.replace('/', '_')
|
| 266 |
period_suffix = f"{start_ms}_{end_ms}"
|
| 267 |
scores_file = f"{CACHE_DIR}/{safe_sym}_{period_suffix}_scores.pkl"
|
|
|
|
| 268 |
if os.path.exists(scores_file):
|
| 269 |
print(f" 📂 [{sym}] Data Exists -> Skipping.")
|
| 270 |
return
|
|
@@ -332,7 +361,8 @@ class HeavyDutyBacktester:
|
|
| 332 |
elif target_arr and feat in ['open','high','low','close','volume']: t_vecs.append(target_arr[feat][target_map])
|
| 333 |
else: t_vecs.append(np.zeros(len(arr_ts_1m)))
|
| 334 |
X_TITAN = np.column_stack(t_vecs)
|
| 335 |
-
|
|
|
|
| 336 |
except: pass
|
| 337 |
|
| 338 |
# B. SNIPER (Global)
|
|
@@ -383,6 +413,7 @@ class HeavyDutyBacktester:
|
|
| 383 |
X_V2 = np.column_stack([l_log, l_rsi, l_fib, l_vol, l5_log, l5_rsi, l5_fib, l5_trd, l15_log, l15_rsi, l15_fib618, l15_trd, *lags])
|
| 384 |
preds = legacy_v2.predict(xgb.DMatrix(X_V2))
|
| 385 |
global_v2_scores = preds[:, 2] if len(preds.shape) > 1 else preds
|
|
|
|
| 386 |
except: pass
|
| 387 |
|
| 388 |
# Filter
|
|
@@ -407,13 +438,15 @@ class HeavyDutyBacktester:
|
|
| 407 |
for idx in chunk_idxs:
|
| 408 |
sl_st = h_static[idx:idx+240]
|
| 409 |
sl_close = sl_st[:, 6]; sl_atr = sl_st[:, 5]
|
| 410 |
-
entry = fast_1m['close'][idx]
|
| 411 |
dist = np.maximum(1.5 * sl_atr, entry * 0.015)
|
| 412 |
pnl = sl_close - entry
|
| 413 |
norm_pnl = pnl / dist
|
| 414 |
max_pnl_r = (np.maximum.accumulate(sl_close) - entry) / dist
|
| 415 |
atr_pct = sl_atr / sl_close
|
| 416 |
-
zeros = np.zeros(240); time_vec = np.arange(1, 241)
|
|
|
|
|
|
|
| 417 |
X_H = np.column_stack([
|
| 418 |
sl_st[:,0], sl_st[:,1], sl_st[:,2], sl_st[:,3], sl_st[:,4],
|
| 419 |
zeros, atr_pct, norm_pnl, max_pnl_r, zeros, zeros, time_vec, zeros,
|
|
@@ -422,22 +455,24 @@ class HeavyDutyBacktester:
|
|
| 422 |
max_hydra = 0.0; hydra_time = 0
|
| 423 |
try:
|
| 424 |
probs = hydra_models['crash'].predict_proba(X_H)[:, 1]
|
| 425 |
-
max_hydra = np.max(probs)
|
| 426 |
if max_hydra > 0.6:
|
| 427 |
t = np.argmax(probs)
|
| 428 |
hydra_time = int(fast_1m['timestamp'][idx + t])
|
| 429 |
except: pass
|
| 430 |
-
|
|
|
|
| 431 |
v2_time = 0
|
| 432 |
if max_v2 > 0.8:
|
| 433 |
t2 = np.argmax(global_v2_scores[idx:idx+240])
|
| 434 |
v2_time = int(fast_1m['timestamp'][idx + t2])
|
|
|
|
| 435 |
ai_results.append({
|
| 436 |
'timestamp': int(fast_1m['timestamp'][idx]),
|
| 437 |
'symbol': sym, 'close': entry,
|
| 438 |
-
'real_titan': global_titan_scores[idx],
|
| 439 |
'oracle_conf': s_oracle,
|
| 440 |
-
'sniper_score': global_sniper_scores[idx],
|
| 441 |
'risk_hydra_crash': max_hydra, 'time_hydra_crash': hydra_time,
|
| 442 |
'risk_legacy_v2': max_v2, 'time_legacy_panic': v2_time,
|
| 443 |
'signal_type': 'BREAKOUT', 'l1_score': 50.0
|
|
@@ -468,114 +503,70 @@ class HeavyDutyBacktester:
|
|
| 468 |
except: pass
|
| 469 |
if not data: return []
|
| 470 |
|
| 471 |
-
# ✅
|
| 472 |
df = pd.concat(data).sort_values('timestamp').reset_index(drop=True)
|
| 473 |
|
| 474 |
ts = df['timestamp'].values
|
| 475 |
close = df['close'].values.astype(float)
|
| 476 |
sym = df['symbol'].values
|
|
|
|
| 477 |
|
| 478 |
-
#
|
| 479 |
-
u_syms = np.unique(sym)
|
| 480 |
-
sym_map = {s: i for i, s in enumerate(u_syms)}
|
| 481 |
-
sym_id = np.array([sym_map[s] for s in sym])
|
| 482 |
-
|
| 483 |
-
# Extract features as pure numpy arrays (scalar safety)
|
| 484 |
oracle = df['oracle_conf'].values.astype(float)
|
| 485 |
sniper = df['sniper_score'].values.astype(float)
|
| 486 |
hydra = df['risk_hydra_crash'].values.astype(float)
|
| 487 |
titan = df['real_titan'].values.astype(float)
|
| 488 |
l1 = df['l1_score'].values.astype(float)
|
| 489 |
-
|
| 490 |
-
# Handle Legacy (fill 0 if missing)
|
| 491 |
legacy_v2 = df['risk_legacy_v2'].values.astype(float) if 'risk_legacy_v2' in df else np.zeros(len(df))
|
| 492 |
-
|
| 493 |
-
# Extra: Hydra Time (for expiry check)
|
| 494 |
-
h_times = df['time_hydra_crash'].values.astype(int)
|
| 495 |
|
| 496 |
N = len(ts)
|
| 497 |
print(f" 🚀 [System] Testing {len(combinations_batch)} configs on {N} candles...", flush=True)
|
| 498 |
|
| 499 |
res = []
|
| 500 |
for cfg in combinations_batch:
|
| 501 |
-
pos = {}
|
| 502 |
-
|
| 503 |
-
bal = float(initial_capital)
|
| 504 |
-
alloc = 0.0
|
| 505 |
|
| 506 |
-
#
|
| 507 |
-
mask = (l1 >= cfg['l1_thresh']) &
|
| 508 |
-
(oracle >= cfg['oracle_thresh']) & \
|
| 509 |
-
(sniper >= cfg['sniper_thresh']) & \
|
| 510 |
-
(titan >= 0.55)
|
| 511 |
|
| 512 |
-
# Loop
|
| 513 |
for i in range(N):
|
| 514 |
-
s = sym_id[i]
|
| 515 |
-
p = float(close[i])
|
| 516 |
-
curr_t = ts[i]
|
| 517 |
|
| 518 |
-
# 1. Exit Logic
|
| 519 |
if s in pos:
|
| 520 |
entry_p, h_risk_val, size_val, h_time_val = pos[s]
|
| 521 |
|
| 522 |
-
# Explicit Scalar bools
|
| 523 |
crash_hydra = bool(h_risk_val > cfg['hydra_thresh'])
|
| 524 |
-
|
| 525 |
-
# Logic: If current time > crash time prediction, signal is stale?
|
| 526 |
-
# Or if prediction was for a future time?
|
| 527 |
-
# Assuming h_time_val is the timestamp of predicted crash
|
| 528 |
time_match = bool(h_time_val > 0 and curr_t >= h_time_val)
|
| 529 |
-
|
| 530 |
-
# Legacy Logic (Global array check)
|
| 531 |
-
# Note: Legacy array corresponds to candle index, but here we iterate sorted time
|
| 532 |
-
# We need to trust the backtest signal 'risk_legacy_v2' is aligned.
|
| 533 |
-
# Yes, it comes from df row 'i'.
|
| 534 |
panic_legacy = bool(legacy_v2[i] > cfg['legacy_thresh'])
|
| 535 |
-
|
| 536 |
pnl = (p - entry_p) / entry_p
|
| 537 |
|
| 538 |
-
|
| 539 |
-
# Exit if: Hydra Crash AND Time Match OR Legacy Panic OR TP/SL
|
| 540 |
-
should_exit = (crash_hydra and time_match) or panic_legacy or (pnl > 0.04) or (pnl < -0.02)
|
| 541 |
-
|
| 542 |
-
if should_exit:
|
| 543 |
realized = pnl - (fees_pct * 2)
|
| 544 |
bal += size_val * (1.0 + realized)
|
| 545 |
alloc -= size_val
|
| 546 |
del pos[s]
|
| 547 |
log.append({'pnl': realized})
|
| 548 |
|
| 549 |
-
# 2. Entry Logic
|
| 550 |
-
# Use scalar boolean from mask
|
| 551 |
if len(pos) < max_slots and bool(mask[i]):
|
| 552 |
if s not in pos and bal >= 5.0:
|
| 553 |
size = min(10.0, bal * 0.98)
|
| 554 |
-
# Store: Entry, HydraRisk, Size, HydraTime
|
| 555 |
pos[s] = (p, hydra[i], size, h_times[i])
|
| 556 |
-
bal -= size
|
| 557 |
-
alloc += size
|
| 558 |
|
| 559 |
final_bal = bal + alloc
|
| 560 |
profit = final_bal - initial_capital
|
| 561 |
-
|
| 562 |
-
# Stats
|
| 563 |
tot = len(log)
|
| 564 |
winning = [x for x in log if x['pnl'] > 0]
|
| 565 |
losing = [x for x in log if x['pnl'] <= 0]
|
| 566 |
-
|
| 567 |
-
win_count = len(winning)
|
| 568 |
-
loss_count = len(losing)
|
| 569 |
win_rate = (win_count/tot*100) if tot > 0 else 0.0
|
| 570 |
-
|
| 571 |
avg_win = np.mean([x['pnl'] for x in winning]) if winning else 0.0
|
| 572 |
avg_loss = np.mean([x['pnl'] for x in losing]) if losing else 0.0
|
| 573 |
-
|
| 574 |
gross_p = sum([x['pnl'] for x in winning])
|
| 575 |
gross_l = abs(sum([x['pnl'] for x in losing]))
|
| 576 |
profit_factor = (gross_p / gross_l) if gross_l > 0 else 99.9
|
| 577 |
-
|
| 578 |
-
# Streaks
|
| 579 |
max_win_s = 0; max_loss_s = 0; curr_w = 0; curr_l = 0
|
| 580 |
for t in log:
|
| 581 |
if t['pnl'] > 0:
|
|
@@ -592,9 +583,7 @@ class HeavyDutyBacktester:
|
|
| 592 |
'avg_win': avg_win, 'avg_loss': avg_loss,
|
| 593 |
'max_win_streak': max_win_s, 'max_loss_streak': max_loss_s,
|
| 594 |
'profit_factor': profit_factor,
|
| 595 |
-
'consensus_agreement_rate': 0.0,
|
| 596 |
-
'high_consensus_win_rate': 0.0,
|
| 597 |
-
'high_consensus_avg_pnl': 0.0
|
| 598 |
})
|
| 599 |
return res
|
| 600 |
|
|
@@ -602,29 +591,23 @@ class HeavyDutyBacktester:
|
|
| 602 |
await self.generate_truth_data()
|
| 603 |
oracle_r = np.linspace(0.4, 0.7, 3); sniper_r = np.linspace(0.4, 0.7, 3)
|
| 604 |
hydra_r = [0.85, 0.95]; l1_r = [10.0]
|
| 605 |
-
|
| 606 |
combos = []
|
| 607 |
for o, s, h, l1 in itertools.product(oracle_r, sniper_r, hydra_r, l1_r):
|
| 608 |
combos.append({
|
| 609 |
'w_titan': 0.4, 'w_struct': 0.3, 'thresh': l1, 'l1_thresh': l1,
|
| 610 |
'oracle_thresh': o, 'sniper_thresh': s, 'hydra_thresh': h, 'legacy_thresh': 0.95
|
| 611 |
})
|
| 612 |
-
|
| 613 |
files = glob.glob(os.path.join(CACHE_DIR, "*.pkl"))
|
| 614 |
results_list = self._worker_optimize(combos, files, self.INITIAL_CAPITAL, self.TRADING_FEES, self.MAX_SLOTS)
|
| 615 |
if not results_list: return None, {'net_profit': 0.0, 'win_rate': 0.0}
|
| 616 |
-
|
| 617 |
results_list.sort(key=lambda x: x['net_profit'], reverse=True)
|
| 618 |
best = results_list[0]
|
| 619 |
-
|
| 620 |
-
# Auto-Diagnosis
|
| 621 |
diag = []
|
| 622 |
if best['total_trades'] > 2000 and best['net_profit'] < 10: diag.append("⚠️ Overtrading")
|
| 623 |
if best['win_rate'] > 55 and best['net_profit'] < 0: diag.append("⚠️ Fee Burn")
|
| 624 |
if abs(best['avg_loss']) > best['avg_win']: diag.append("⚠️ Risk/Reward Inversion")
|
| 625 |
if best['max_loss_streak'] > 10: diag.append("⚠️ Consecutive Loss Risk")
|
| 626 |
if not diag: diag.append("✅ System Healthy")
|
| 627 |
-
|
| 628 |
print("\n" + "="*60)
|
| 629 |
print(f"🏆 CHAMPION REPORT [{target_regime}]:")
|
| 630 |
print(f" 💰 Final Balance: ${best['final_balance']:,.2f}")
|
|
@@ -647,21 +630,19 @@ class HeavyDutyBacktester:
|
|
| 647 |
return best['config'], best
|
| 648 |
|
| 649 |
async def run_strategic_optimization_task():
|
| 650 |
-
print("\n🧪 [STRATEGIC BACKTEST]
|
| 651 |
r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
|
| 652 |
try:
|
| 653 |
await dm.initialize(); await proc.initialize()
|
| 654 |
if proc.guardian_hydra: proc.guardian_hydra.set_silent_mode(True)
|
| 655 |
hub = AdaptiveHub(r2); await hub.initialize()
|
| 656 |
opt = HeavyDutyBacktester(dm, proc)
|
| 657 |
-
|
| 658 |
scenarios = [
|
| 659 |
{"regime": "BULL", "start": "2024-01-01", "end": "2024-03-30"},
|
| 660 |
{"regime": "BEAR", "start": "2023-08-01", "end": "2023-09-15"},
|
| 661 |
{"regime": "DEAD", "start": "2023-06-01", "end": "2023-08-01"},
|
| 662 |
{"regime": "RANGE", "start": "2024-07-01", "end": "2024-09-30"}
|
| 663 |
]
|
| 664 |
-
|
| 665 |
for s in scenarios:
|
| 666 |
opt.set_date_range(s["start"], s["end"])
|
| 667 |
best_cfg, best_stats = await opt.run_optimization(s["regime"])
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
+
# 🧪 backtest_engine.py (V141.0 - GEM-Architect: Scalar Enforcement)
|
| 3 |
# ============================================================
|
| 4 |
|
| 5 |
import asyncio
|
|
|
|
| 47 |
return ((x - r) / s).fillna(0)
|
| 48 |
|
| 49 |
def _revive_score_distribution(scores):
|
| 50 |
+
scores = np.array(scores, dtype=np.float32).flatten() # ✅ Force Flatten
|
| 51 |
if len(scores) < 10: return scores
|
| 52 |
std = np.std(scores)
|
| 53 |
if std < 0.05:
|
|
|
|
| 73 |
s = np.nanstd(x, axis=0) + 1e-9
|
| 74 |
return np.nan_to_num((x - m) / s, nan=0.0)
|
| 75 |
|
| 76 |
+
# ============================================================
|
| 77 |
+
# 🧩 PATTERN RECOGNITION HELPER
|
| 78 |
+
# ============================================================
|
| 79 |
def _transform_window_for_pattern(df_window):
|
| 80 |
try:
|
| 81 |
c = df_window['close'].values.astype('float32')
|
|
|
|
| 83 |
h = df_window['high'].values.astype('float32')
|
| 84 |
l = df_window['low'].values.astype('float32')
|
| 85 |
v = df_window['volume'].values.astype('float32')
|
| 86 |
+
|
| 87 |
base = np.stack([o, h, l, c, v], axis=1)
|
| 88 |
base_z = _zv(base)
|
| 89 |
lr = np.zeros_like(c); lr[1:] = np.diff(np.log1p(c))
|
| 90 |
rng = (h - l) / (c + 1e-9)
|
| 91 |
extra = np.stack([lr, rng], axis=1)
|
| 92 |
extra_z = _zv(extra)
|
| 93 |
+
|
| 94 |
def _ema(arr, n): return pd.Series(arr).ewm(span=n, adjust=False).mean().values
|
| 95 |
ema9 = _ema(c, 9); ema21 = _ema(c, 21); ema50 = _ema(c, 50); ema200 = _ema(c, 200)
|
| 96 |
slope21 = np.gradient(ema21); slope50 = np.gradient(ema50)
|
|
|
|
| 101 |
roll_down = pd.Series(down).abs().ewm(alpha=1/14, adjust=False).mean().values
|
| 102 |
rs = roll_up / (roll_down + 1e-9)
|
| 103 |
rsi = 100.0 - (100.0 / (1.0 + rs))
|
| 104 |
+
|
| 105 |
indicators = np.stack([ema9, ema21, ema50, ema200, slope21, slope50, rsi], axis=1)
|
| 106 |
X_seq = np.concatenate([base_z, extra_z, _zv(indicators)], axis=1)
|
| 107 |
X_flat = X_seq.flatten()
|
|
|
|
| 120 |
self.INITIAL_CAPITAL = 10.0
|
| 121 |
self.TRADING_FEES = 0.001
|
| 122 |
self.MAX_SLOTS = 4
|
| 123 |
+
|
| 124 |
self.TARGET_COINS = [
|
| 125 |
+
'SOL/USDT' ]
|
| 126 |
+
|
| 127 |
self.force_start_date = None
|
| 128 |
self.force_end_date = None
|
| 129 |
+
|
| 130 |
if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
|
| 131 |
+
print(f"🧪 [Backtest V141.0] Scalar Enforcement Mode.")
|
| 132 |
|
| 133 |
def set_date_range(self, start_str, end_str):
|
| 134 |
self.force_start_date = start_str
|
|
|
|
| 145 |
current += duration_per_batch
|
| 146 |
all_candles = []
|
| 147 |
sem = asyncio.Semaphore(20)
|
| 148 |
+
|
| 149 |
async def _fetch_batch(timestamp):
|
| 150 |
async with sem:
|
| 151 |
for _ in range(3):
|
|
|
|
| 153 |
return await self.dm.exchange.fetch_ohlcv(sym, '1m', since=timestamp, limit=limit)
|
| 154 |
except: await asyncio.sleep(0.5)
|
| 155 |
return []
|
| 156 |
+
|
| 157 |
chunk_size = 50
|
| 158 |
for i in range(0, len(tasks), chunk_size):
|
| 159 |
chunk_tasks = tasks[i:i + chunk_size]
|
|
|
|
| 161 |
results = await asyncio.gather(*futures)
|
| 162 |
for res in results:
|
| 163 |
if res: all_candles.extend(res)
|
| 164 |
+
|
| 165 |
if not all_candles: return None
|
| 166 |
df = pd.DataFrame(all_candles, columns=['timestamp', 'o', 'h', 'l', 'c', 'v'])
|
| 167 |
df.drop_duplicates('timestamp', inplace=True)
|
|
|
|
| 170 |
print(f" ✅ Downloaded {len(df)} candles.", flush=True)
|
| 171 |
return df.values.tolist()
|
| 172 |
|
| 173 |
+
# ==============================================================
|
| 174 |
+
# 🏎️ VECTORIZED INDICATORS (EXACT MATCH TO LIVE SYSTEM)
|
| 175 |
+
# ==============================================================
|
| 176 |
def _calculate_indicators_vectorized(self, df, timeframe='1m'):
|
| 177 |
cols = ['close', 'high', 'low', 'volume', 'open']
|
| 178 |
for c in cols: df[c] = df[c].astype(np.float64)
|
| 179 |
idx = df.index
|
| 180 |
+
|
| 181 |
+
# --- TITAN FEATURES ---
|
| 182 |
df['RSI'] = safe_ta(ta.rsi(df['close'], length=14), idx, 50)
|
| 183 |
macd = ta.macd(df['close'])
|
| 184 |
if macd is not None:
|
|
|
|
| 189 |
adx = ta.adx(df['high'], df['low'], df['close'], length=14)
|
| 190 |
if adx is not None: df['ADX'] = safe_ta(adx.iloc[:, 0], idx, 0)
|
| 191 |
else: df['ADX'] = 0.0
|
| 192 |
+
if timeframe == '1d':
|
| 193 |
+
df['Trend_Strong'] = np.where(df['ADX'] > 25, 1.0, 0.0)
|
| 194 |
+
|
| 195 |
for p in [9, 21, 50, 200]:
|
| 196 |
ema = safe_ta(ta.ema(df['close'], length=p), idx, 0)
|
| 197 |
df[f'EMA_{p}_dist'] = ((df['close'] / ema.replace(0, np.nan)) - 1).fillna(0)
|
| 198 |
df[f'ema{p}'] = ema
|
| 199 |
+
|
| 200 |
df['ema20'] = safe_ta(ta.ema(df['close'], length=20), idx, df['close'])
|
| 201 |
+
|
| 202 |
bb = ta.bbands(df['close'], length=20, std=2.0)
|
| 203 |
if bb is not None:
|
| 204 |
w = ((bb.iloc[:, 2] - bb.iloc[:, 0]) / bb.iloc[:, 1].replace(0, np.nan)).fillna(0)
|
| 205 |
p = ((df['close'] - bb.iloc[:, 0]) / (bb.iloc[:, 2] - bb.iloc[:, 0]).replace(0, np.nan)).fillna(0)
|
| 206 |
df['BB_w'] = w; df['BB_p'] = p; df['bb_width'] = w
|
| 207 |
else: df['BB_w'] = 0; df['BB_p'] = 0; df['bb_width'] = 0
|
| 208 |
+
|
| 209 |
df['MFI'] = safe_ta(ta.mfi(df['high'], df['low'], df['close'], df['volume'], length=14), idx, 50)
|
| 210 |
vwap = ta.vwap(df['high'], df['low'], df['close'], df['volume'])
|
| 211 |
if vwap is not None:
|
| 212 |
df['VWAP_dist'] = ((df['close'] / vwap.replace(0, np.nan)) - 1).fillna(0)
|
| 213 |
df['vwap'] = vwap
|
| 214 |
else: df['VWAP_dist'] = 0.0; df['vwap'] = df['close']
|
| 215 |
+
|
| 216 |
df['atr'] = safe_ta(ta.atr(df['high'], df['low'], df['close'], length=14), idx, 0)
|
| 217 |
df['atr_pct'] = (df['atr'] / df['close'].replace(0, np.nan)).fillna(0)
|
| 218 |
df['ATR_pct'] = df['atr_pct']
|
| 219 |
|
| 220 |
+
# --- SNIPER FEATURES ---
|
| 221 |
if timeframe == '1m':
|
| 222 |
df['return_1m'] = df['close'].pct_change().fillna(0)
|
| 223 |
df['return_3m'] = df['close'].pct_change(3).fillna(0)
|
|
|
|
| 256 |
df['rv_gk'] = _z_roll(rv_gk)
|
| 257 |
df['L_score'] = (df['vol_zscore_50'] - df['amihud'] - df['roll_spread'] - df['rv_gk'].abs() - df['vwap_dev'].abs() + df['ofi']).fillna(0)
|
| 258 |
|
| 259 |
+
# --- EXTRAS ---
|
| 260 |
df['slope'] = safe_ta(ta.slope(df['close'], length=7), idx, 0)
|
| 261 |
vol_mean = df['volume'].rolling(20).mean()
|
| 262 |
vol_std = df['volume'].rolling(20).std().replace(0, np.nan)
|
| 263 |
df['vol_z'] = ((df['volume'] - vol_mean) / vol_std).fillna(0)
|
| 264 |
df['rel_vol'] = df['volume'] / (df['volume'].rolling(50).mean() + 1e-9)
|
| 265 |
df['log_ret'] = np.log(df['close'] / df['close'].shift(1).replace(0, np.nan)).fillna(0)
|
| 266 |
+
roll_max = df['high'].rolling(50).max(); roll_min = df['low'].rolling(50).min()
|
|
|
|
| 267 |
diff = (roll_max - roll_min).replace(0, 1e-9)
|
| 268 |
df['fib_pos'] = ((df['close'] - roll_min) / diff).fillna(0.5)
|
| 269 |
e20_s = df['ema20'].shift(5).replace(0, np.nan)
|
|
|
|
| 275 |
e200 = safe_ta(ta.ema(df['close'], length=200), idx, df['close'])
|
| 276 |
df['ema200'] = e200
|
| 277 |
df['dist_ema200'] = ((df['close'] - e200) / e200.replace(0, np.nan)).fillna(0)
|
| 278 |
+
|
| 279 |
if timeframe == '1m':
|
| 280 |
for lag in [1, 2, 3, 5, 10, 20]:
|
| 281 |
df[f'log_ret_lag_{lag}'] = df['log_ret'].shift(lag).fillna(0)
|
| 282 |
df[f'rsi_lag_{lag}'] = (df['RSI'].shift(lag) / 100.0).fillna(0.5)
|
| 283 |
df[f'fib_pos_lag_{lag}'] = df['fib_pos'].shift(lag).fillna(0.5)
|
| 284 |
df[f'volatility_lag_{lag}'] = df['volatility'].shift(lag).fillna(0)
|
| 285 |
+
|
| 286 |
df.fillna(0, inplace=True)
|
| 287 |
return df
|
| 288 |
|
| 289 |
+
# ==============================================================
|
| 290 |
+
# 🧠 CPU PROCESSING (GLOBAL INFERENCE)
|
| 291 |
+
# ==============================================================
|
| 292 |
async def _process_data_in_memory(self, sym, candles, start_ms, end_ms):
|
| 293 |
safe_sym = sym.replace('/', '_')
|
| 294 |
period_suffix = f"{start_ms}_{end_ms}"
|
| 295 |
scores_file = f"{CACHE_DIR}/{safe_sym}_{period_suffix}_scores.pkl"
|
| 296 |
+
|
| 297 |
if os.path.exists(scores_file):
|
| 298 |
print(f" 📂 [{sym}] Data Exists -> Skipping.")
|
| 299 |
return
|
|
|
|
| 361 |
elif target_arr and feat in ['open','high','low','close','volume']: t_vecs.append(target_arr[feat][target_map])
|
| 362 |
else: t_vecs.append(np.zeros(len(arr_ts_1m)))
|
| 363 |
X_TITAN = np.column_stack(t_vecs)
|
| 364 |
+
preds_t = titan_model.predict(xgb.DMatrix(X_TITAN, feature_names=titan_cols))
|
| 365 |
+
global_titan_scores = _revive_score_distribution(preds_t) # ✅ Flattened inside
|
| 366 |
except: pass
|
| 367 |
|
| 368 |
# B. SNIPER (Global)
|
|
|
|
| 413 |
X_V2 = np.column_stack([l_log, l_rsi, l_fib, l_vol, l5_log, l5_rsi, l5_fib, l5_trd, l15_log, l15_rsi, l15_fib618, l15_trd, *lags])
|
| 414 |
preds = legacy_v2.predict(xgb.DMatrix(X_V2))
|
| 415 |
global_v2_scores = preds[:, 2] if len(preds.shape) > 1 else preds
|
| 416 |
+
global_v2_scores = global_v2_scores.flatten() # ✅ Safety Flatten
|
| 417 |
except: pass
|
| 418 |
|
| 419 |
# Filter
|
|
|
|
| 438 |
for idx in chunk_idxs:
|
| 439 |
sl_st = h_static[idx:idx+240]
|
| 440 |
sl_close = sl_st[:, 6]; sl_atr = sl_st[:, 5]
|
| 441 |
+
entry = float(fast_1m['close'][idx]) # ✅ Scalar
|
| 442 |
dist = np.maximum(1.5 * sl_atr, entry * 0.015)
|
| 443 |
pnl = sl_close - entry
|
| 444 |
norm_pnl = pnl / dist
|
| 445 |
max_pnl_r = (np.maximum.accumulate(sl_close) - entry) / dist
|
| 446 |
atr_pct = sl_atr / sl_close
|
| 447 |
+
zeros = np.zeros(240); time_vec = np.arange(1, 241)
|
| 448 |
+
s_oracle = float(global_oracle_scores[idx]) # ✅ Scalar
|
| 449 |
+
|
| 450 |
X_H = np.column_stack([
|
| 451 |
sl_st[:,0], sl_st[:,1], sl_st[:,2], sl_st[:,3], sl_st[:,4],
|
| 452 |
zeros, atr_pct, norm_pnl, max_pnl_r, zeros, zeros, time_vec, zeros,
|
|
|
|
| 455 |
max_hydra = 0.0; hydra_time = 0
|
| 456 |
try:
|
| 457 |
probs = hydra_models['crash'].predict_proba(X_H)[:, 1]
|
| 458 |
+
max_hydra = float(np.max(probs)) # ✅ Scalar
|
| 459 |
if max_hydra > 0.6:
|
| 460 |
t = np.argmax(probs)
|
| 461 |
hydra_time = int(fast_1m['timestamp'][idx + t])
|
| 462 |
except: pass
|
| 463 |
+
|
| 464 |
+
max_v2 = float(np.max(global_v2_scores[idx:idx+240])) # ✅ Scalar
|
| 465 |
v2_time = 0
|
| 466 |
if max_v2 > 0.8:
|
| 467 |
t2 = np.argmax(global_v2_scores[idx:idx+240])
|
| 468 |
v2_time = int(fast_1m['timestamp'][idx + t2])
|
| 469 |
+
|
| 470 |
ai_results.append({
|
| 471 |
'timestamp': int(fast_1m['timestamp'][idx]),
|
| 472 |
'symbol': sym, 'close': entry,
|
| 473 |
+
'real_titan': float(global_titan_scores[idx]), # ✅ Scalar
|
| 474 |
'oracle_conf': s_oracle,
|
| 475 |
+
'sniper_score': float(global_sniper_scores[idx]), # ✅ Scalar
|
| 476 |
'risk_hydra_crash': max_hydra, 'time_hydra_crash': hydra_time,
|
| 477 |
'risk_legacy_v2': max_v2, 'time_legacy_panic': v2_time,
|
| 478 |
'signal_type': 'BREAKOUT', 'l1_score': 50.0
|
|
|
|
| 503 |
except: pass
|
| 504 |
if not data: return []
|
| 505 |
|
| 506 |
+
# ✅ Reset Index to ensure aligned access
|
| 507 |
df = pd.concat(data).sort_values('timestamp').reset_index(drop=True)
|
| 508 |
|
| 509 |
ts = df['timestamp'].values
|
| 510 |
close = df['close'].values.astype(float)
|
| 511 |
sym = df['symbol'].values
|
| 512 |
+
u_syms = np.unique(sym); sym_map = {s: i for i, s in enumerate(u_syms)}; sym_id = np.array([sym_map[s] for s in sym])
|
| 513 |
|
| 514 |
+
# ✅ Explicit Scalar Conversion for Features
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
oracle = df['oracle_conf'].values.astype(float)
|
| 516 |
sniper = df['sniper_score'].values.astype(float)
|
| 517 |
hydra = df['risk_hydra_crash'].values.astype(float)
|
| 518 |
titan = df['real_titan'].values.astype(float)
|
| 519 |
l1 = df['l1_score'].values.astype(float)
|
|
|
|
|
|
|
| 520 |
legacy_v2 = df['risk_legacy_v2'].values.astype(float) if 'risk_legacy_v2' in df else np.zeros(len(df))
|
| 521 |
+
h_times = df['time_hydra_crash'].values.astype(np.int64)
|
|
|
|
|
|
|
| 522 |
|
| 523 |
N = len(ts)
|
| 524 |
print(f" 🚀 [System] Testing {len(combinations_batch)} configs on {N} candles...", flush=True)
|
| 525 |
|
| 526 |
res = []
|
| 527 |
for cfg in combinations_batch:
|
| 528 |
+
pos = {}; log = []
|
| 529 |
+
bal = float(initial_capital); alloc = 0.0
|
|
|
|
|
|
|
| 530 |
|
| 531 |
+
# Mask is now purely boolean (True/False)
|
| 532 |
+
mask = (l1 >= cfg['l1_thresh']) & (oracle >= cfg['oracle_thresh']) & (sniper >= cfg['sniper_thresh']) & (titan >= 0.55)
|
|
|
|
|
|
|
|
|
|
| 533 |
|
|
|
|
| 534 |
for i in range(N):
|
| 535 |
+
s = sym_id[i]; p = float(close[i]); curr_t = ts[i]
|
|
|
|
|
|
|
| 536 |
|
|
|
|
| 537 |
if s in pos:
|
| 538 |
entry_p, h_risk_val, size_val, h_time_val = pos[s]
|
| 539 |
|
|
|
|
| 540 |
crash_hydra = bool(h_risk_val > cfg['hydra_thresh'])
|
|
|
|
|
|
|
|
|
|
|
|
|
| 541 |
time_match = bool(h_time_val > 0 and curr_t >= h_time_val)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
panic_legacy = bool(legacy_v2[i] > cfg['legacy_thresh'])
|
|
|
|
| 543 |
pnl = (p - entry_p) / entry_p
|
| 544 |
|
| 545 |
+
if (crash_hydra and time_match) or panic_legacy or pnl > 0.04 or pnl < -0.02:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 546 |
realized = pnl - (fees_pct * 2)
|
| 547 |
bal += size_val * (1.0 + realized)
|
| 548 |
alloc -= size_val
|
| 549 |
del pos[s]
|
| 550 |
log.append({'pnl': realized})
|
| 551 |
|
|
|
|
|
|
|
| 552 |
if len(pos) < max_slots and bool(mask[i]):
|
| 553 |
if s not in pos and bal >= 5.0:
|
| 554 |
size = min(10.0, bal * 0.98)
|
|
|
|
| 555 |
pos[s] = (p, hydra[i], size, h_times[i])
|
| 556 |
+
bal -= size; alloc += size
|
|
|
|
| 557 |
|
| 558 |
final_bal = bal + alloc
|
| 559 |
profit = final_bal - initial_capital
|
|
|
|
|
|
|
| 560 |
tot = len(log)
|
| 561 |
winning = [x for x in log if x['pnl'] > 0]
|
| 562 |
losing = [x for x in log if x['pnl'] <= 0]
|
| 563 |
+
win_count = len(winning); loss_count = len(losing)
|
|
|
|
|
|
|
| 564 |
win_rate = (win_count/tot*100) if tot > 0 else 0.0
|
|
|
|
| 565 |
avg_win = np.mean([x['pnl'] for x in winning]) if winning else 0.0
|
| 566 |
avg_loss = np.mean([x['pnl'] for x in losing]) if losing else 0.0
|
|
|
|
| 567 |
gross_p = sum([x['pnl'] for x in winning])
|
| 568 |
gross_l = abs(sum([x['pnl'] for x in losing]))
|
| 569 |
profit_factor = (gross_p / gross_l) if gross_l > 0 else 99.9
|
|
|
|
|
|
|
| 570 |
max_win_s = 0; max_loss_s = 0; curr_w = 0; curr_l = 0
|
| 571 |
for t in log:
|
| 572 |
if t['pnl'] > 0:
|
|
|
|
| 583 |
'avg_win': avg_win, 'avg_loss': avg_loss,
|
| 584 |
'max_win_streak': max_win_s, 'max_loss_streak': max_loss_s,
|
| 585 |
'profit_factor': profit_factor,
|
| 586 |
+
'consensus_agreement_rate': 0.0, 'high_consensus_win_rate': 0.0, 'high_consensus_avg_pnl': 0.0
|
|
|
|
|
|
|
| 587 |
})
|
| 588 |
return res
|
| 589 |
|
|
|
|
| 591 |
await self.generate_truth_data()
|
| 592 |
oracle_r = np.linspace(0.4, 0.7, 3); sniper_r = np.linspace(0.4, 0.7, 3)
|
| 593 |
hydra_r = [0.85, 0.95]; l1_r = [10.0]
|
|
|
|
| 594 |
combos = []
|
| 595 |
for o, s, h, l1 in itertools.product(oracle_r, sniper_r, hydra_r, l1_r):
|
| 596 |
combos.append({
|
| 597 |
'w_titan': 0.4, 'w_struct': 0.3, 'thresh': l1, 'l1_thresh': l1,
|
| 598 |
'oracle_thresh': o, 'sniper_thresh': s, 'hydra_thresh': h, 'legacy_thresh': 0.95
|
| 599 |
})
|
|
|
|
| 600 |
files = glob.glob(os.path.join(CACHE_DIR, "*.pkl"))
|
| 601 |
results_list = self._worker_optimize(combos, files, self.INITIAL_CAPITAL, self.TRADING_FEES, self.MAX_SLOTS)
|
| 602 |
if not results_list: return None, {'net_profit': 0.0, 'win_rate': 0.0}
|
|
|
|
| 603 |
results_list.sort(key=lambda x: x['net_profit'], reverse=True)
|
| 604 |
best = results_list[0]
|
|
|
|
|
|
|
| 605 |
diag = []
|
| 606 |
if best['total_trades'] > 2000 and best['net_profit'] < 10: diag.append("⚠️ Overtrading")
|
| 607 |
if best['win_rate'] > 55 and best['net_profit'] < 0: diag.append("⚠️ Fee Burn")
|
| 608 |
if abs(best['avg_loss']) > best['avg_win']: diag.append("⚠️ Risk/Reward Inversion")
|
| 609 |
if best['max_loss_streak'] > 10: diag.append("⚠️ Consecutive Loss Risk")
|
| 610 |
if not diag: diag.append("✅ System Healthy")
|
|
|
|
| 611 |
print("\n" + "="*60)
|
| 612 |
print(f"🏆 CHAMPION REPORT [{target_regime}]:")
|
| 613 |
print(f" 💰 Final Balance: ${best['final_balance']:,.2f}")
|
|
|
|
| 630 |
return best['config'], best
|
| 631 |
|
| 632 |
async def run_strategic_optimization_task():
|
| 633 |
+
print("\n🧪 [STRATEGIC BACKTEST] Scalar Enforcement Mode...")
|
| 634 |
r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
|
| 635 |
try:
|
| 636 |
await dm.initialize(); await proc.initialize()
|
| 637 |
if proc.guardian_hydra: proc.guardian_hydra.set_silent_mode(True)
|
| 638 |
hub = AdaptiveHub(r2); await hub.initialize()
|
| 639 |
opt = HeavyDutyBacktester(dm, proc)
|
|
|
|
| 640 |
scenarios = [
|
| 641 |
{"regime": "BULL", "start": "2024-01-01", "end": "2024-03-30"},
|
| 642 |
{"regime": "BEAR", "start": "2023-08-01", "end": "2023-09-15"},
|
| 643 |
{"regime": "DEAD", "start": "2023-06-01", "end": "2023-08-01"},
|
| 644 |
{"regime": "RANGE", "start": "2024-07-01", "end": "2024-09-30"}
|
| 645 |
]
|
|
|
|
| 646 |
for s in scenarios:
|
| 647 |
opt.set_date_range(s["start"], s["end"])
|
| 648 |
best_cfg, best_stats = await opt.run_optimization(s["regime"])
|