Spaces:
Paused
Paused
Update backtest_engine.py
Browse files- backtest_engine.py +49 -58
backtest_engine.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# ============================================================
|
| 2 |
-
# 🧪 backtest_engine.py (
|
| 3 |
# ============================================================
|
| 4 |
|
| 5 |
import asyncio
|
|
@@ -54,11 +54,9 @@ def _revive_score_distribution(scores):
|
|
| 54 |
|
| 55 |
def optimize_dataframe_memory(df):
|
| 56 |
"""⬇️ Reduces memory usage by downcasting types"""
|
| 57 |
-
# Floats: float64 -> float32
|
| 58 |
float_cols = df.select_dtypes(include=['float64']).columns
|
| 59 |
df[float_cols] = df[float_cols].astype('float32')
|
| 60 |
|
| 61 |
-
# Ints: int64 -> int16/int8
|
| 62 |
int_cols = df.select_dtypes(include=['int64', 'int32']).columns
|
| 63 |
for col in int_cols:
|
| 64 |
c_min = df[col].min()
|
|
@@ -92,17 +90,17 @@ class HeavyDutyBacktester:
|
|
| 92 |
# 🎛️ CONTROL PANEL - HIGH PRECISION RANGES
|
| 93 |
self.GRID_RANGES = {
|
| 94 |
# --- Models ---
|
| 95 |
-
'TITAN': np.linspace(0.30, 0.
|
| 96 |
-
'ORACLE': np.linspace(0.50, 0.
|
| 97 |
-
'SNIPER': np.linspace(0.30, 0.
|
| 98 |
-
'PATTERN': np.linspace(0.30, 0.
|
| 99 |
|
| 100 |
# --- Governance ---
|
| 101 |
-
'GOV_SCORE': np.linspace(50.0,
|
| 102 |
|
| 103 |
# --- Guardians ---
|
| 104 |
-
'HYDRA_THRESH': np.linspace(0.
|
| 105 |
-
'LEGACY_THRESH': np.linspace(0.
|
| 106 |
}
|
| 107 |
|
| 108 |
self.TARGET_COINS = [
|
|
@@ -123,7 +121,7 @@ class HeavyDutyBacktester:
|
|
| 123 |
self.force_end_date = "2024-02-01"
|
| 124 |
|
| 125 |
if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
|
| 126 |
-
print(f"🧪 [Backtest
|
| 127 |
|
| 128 |
def set_date_range(self, start_str, end_str):
|
| 129 |
self.force_start_date = start_str
|
|
@@ -281,7 +279,7 @@ class HeavyDutyBacktester:
|
|
| 281 |
is_market_dead = (h1_chop > 61.8) | ((h1_atr_pct < 0.3) & (h1_adx < 20))
|
| 282 |
market_status = np.where(is_market_dead, 0, 1)
|
| 283 |
|
| 284 |
-
# 2. COIN STATES
|
| 285 |
h1_rsi = numpy_htf['1h']['RSI'][map_1h]
|
| 286 |
h1_close = numpy_htf['1h']['close'][map_1h]
|
| 287 |
h1_bbw = numpy_htf['1h']['bb_width'][map_1h]
|
|
@@ -293,7 +291,12 @@ class HeavyDutyBacktester:
|
|
| 293 |
|
| 294 |
coin_state = np.zeros(len(arr_ts_1m), dtype=np.int8)
|
| 295 |
is_trash_vol = (h1_rel_vol < 0.5) | (h1_atr_pct < 0.2)
|
| 296 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 297 |
mask_safe = (h1_adx > 25) & (h1_ema20 > h1_ema50) & (h1_ema50 > h1_ema200) & (h1_rsi > 50) & (h1_rsi < 75)
|
| 298 |
mask_exp = (h1_rsi > 65) & (h1_close > h1_upper) & (h1_rel_vol > 1.5)
|
| 299 |
|
|
@@ -508,31 +511,26 @@ class HeavyDutyBacktester:
|
|
| 508 |
if c: await self._process_data_in_memory(sym, c, ms_s, ms_e)
|
| 509 |
|
| 510 |
def _worker_optimize(self, combinations_batch, scores_files, initial_capital, fees_pct, max_slots, target_state):
|
| 511 |
-
"""🚀 HYPER-SPEED JUMP LOGIC (RAM Optimized Batching)"""
|
| 512 |
print(f" ⏳ [System] Loading {len(scores_files)} datasets...", flush=True)
|
| 513 |
|
| 514 |
-
# ✅ Load & Compress Immediately
|
| 515 |
data = []
|
| 516 |
for f in scores_files:
|
| 517 |
try:
|
| 518 |
d = pd.read_pickle(f)
|
| 519 |
-
d = optimize_dataframe_memory(d)
|
| 520 |
data.append(d)
|
| 521 |
except: pass
|
| 522 |
if not data: return []
|
| 523 |
|
| 524 |
df = pd.concat(data).sort_values('timestamp').reset_index(drop=True)
|
| 525 |
-
del data
|
| 526 |
gc.collect()
|
| 527 |
|
| 528 |
-
# Pre-load arrays (Optimized types)
|
| 529 |
-
# Convert to numpy and allow GC to reclaim DF memory if possible
|
| 530 |
-
# (Though we keep DF for indices, extracting numpy arrays is safer)
|
| 531 |
ts = df['timestamp'].values
|
| 532 |
close = df['close'].values.astype(np.float32)
|
| 533 |
sym = df['symbol'].values
|
| 534 |
|
| 535 |
-
# Map symbols to int for faster lookup
|
| 536 |
u_syms = np.unique(sym)
|
| 537 |
sym_map = {s: i for i, s in enumerate(u_syms)}
|
| 538 |
sym_id = np.array([sym_map[s] for s in sym], dtype=np.int16)
|
|
@@ -550,7 +548,6 @@ class HeavyDutyBacktester:
|
|
| 550 |
c_state = df['coin_state'].values
|
| 551 |
m_ok = df['market_ok'].values
|
| 552 |
|
| 553 |
-
# ✅ FREE BIG DATAFRAME FROM RAM
|
| 554 |
del df, sym
|
| 555 |
gc.collect()
|
| 556 |
|
|
@@ -558,7 +555,6 @@ class HeavyDutyBacktester:
|
|
| 558 |
print(f" 🚀 [System] Testing {len(combinations_batch)} configs on {N} candidates...", flush=True)
|
| 559 |
|
| 560 |
res = []
|
| 561 |
-
# ✅ Process in Batches to avoid RAM spike from result list
|
| 562 |
BATCH_SIZE = 500
|
| 563 |
|
| 564 |
for i in range(0, len(combinations_batch), BATCH_SIZE):
|
|
@@ -582,42 +578,38 @@ class HeavyDutyBacktester:
|
|
| 582 |
pos = {}
|
| 583 |
bal = float(initial_capital)
|
| 584 |
log = []
|
| 585 |
-
# Don't store full balance history to save RAM
|
| 586 |
trade_durs = []
|
| 587 |
|
| 588 |
-
# Jump Logic on Valid Indices is tricky because we need sequential exit checks.
|
| 589 |
-
# So we iterate fully, but only check entry if mask is true.
|
| 590 |
-
# Optimization: We only need to iterate indices relevant to current positions OR new entries.
|
| 591 |
-
# Since N can be 20M+, simple iteration is slow in Python.
|
| 592 |
-
# We stick to the robust loop for correctness but assume num_trades is low.
|
| 593 |
-
|
| 594 |
-
# ⚡ FAST LOOP
|
| 595 |
for idx in range(N):
|
| 596 |
s = sym_id[idx]
|
| 597 |
p = close[idx]
|
| 598 |
|
| 599 |
-
# Exit Check
|
| 600 |
if s in pos:
|
| 601 |
entry_p, size, e_idx = pos[s]
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
if entry_mask[idx] and len(pos) < max_slots:
|
| 616 |
if s not in pos and bal >= 5.0:
|
| 617 |
-
# Grade
|
| 618 |
sc = gov_s[idx]
|
| 619 |
mult = 1.0 if sc >= 85 else (0.75 if sc >= 70 else 0.5)
|
| 620 |
-
|
| 621 |
base = bal * 0.95 if bal < self.MIN_CAPITAL_FOR_SPLIT else bal / max_slots
|
| 622 |
size = base * mult
|
| 623 |
|
|
@@ -626,33 +618,34 @@ class HeavyDutyBacktester:
|
|
| 626 |
pos[s] = (p, size - fee, idx)
|
| 627 |
bal -= size
|
| 628 |
|
| 629 |
-
# Stats
|
| 630 |
tot = len(log)
|
| 631 |
if tot == 0: continue
|
| 632 |
|
|
|
|
| 633 |
net_p = bal + sum([v[1] for v in pos.values()]) - initial_capital
|
|
|
|
|
|
|
| 634 |
wins = [x for x in log if x > 0]
|
| 635 |
losses = [x for x in log if x <= 0]
|
| 636 |
|
| 637 |
wr = len(wins)/tot*100
|
| 638 |
-
avg_w = np.mean(wins) if wins else 0
|
| 639 |
-
avg_l = np.mean(losses) if losses else 0
|
| 640 |
-
pf = sum(wins)/abs(sum(losses)) if losses else 99
|
| 641 |
|
| 642 |
-
|
| 643 |
-
|
|
|
|
| 644 |
|
| 645 |
res.append({
|
| 646 |
'config': cfg, 'final_balance': bal, 'net_profit': net_p,
|
| 647 |
'total_trades': tot, 'win_rate': wr, 'profit_factor': pf,
|
| 648 |
-
'max_drawdown':
|
| 649 |
'avg_duration_candles': np.mean(trade_durs) if trade_durs else 0,
|
| 650 |
'win_count': len(wins), 'loss_count': len(losses),
|
| 651 |
'avg_win_usd': avg_w, 'avg_loss_usd': avg_l,
|
| 652 |
'max_win_streak': 0, 'max_loss_streak': 0
|
| 653 |
})
|
| 654 |
|
| 655 |
-
# Clean loop memory
|
| 656 |
gc.collect()
|
| 657 |
|
| 658 |
return res
|
|
@@ -681,7 +674,6 @@ class HeavyDutyBacktester:
|
|
| 681 |
|
| 682 |
for state_name, state_id in states:
|
| 683 |
print(f"\n🌀 Optimizing for [{state_name}]...")
|
| 684 |
-
# Re-read files fresh for each state to ensure clean RAM
|
| 685 |
results_list = self._worker_optimize(combos, files, self.INITIAL_CAPITAL, self.TRADING_FEES, self.MAX_SLOTS, state_id)
|
| 686 |
|
| 687 |
if not results_list:
|
|
@@ -719,12 +711,11 @@ class HeavyDutyBacktester:
|
|
| 719 |
print(f" ⚙️ Config: {p_str}")
|
| 720 |
print("="*80)
|
| 721 |
|
| 722 |
-
# Flush for next state
|
| 723 |
del results_list
|
| 724 |
gc.collect()
|
| 725 |
|
| 726 |
async def run_strategic_optimization_task():
|
| 727 |
-
print("\n🧪 [STRATEGIC BACKTEST]
|
| 728 |
r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
|
| 729 |
try:
|
| 730 |
await dm.initialize(); await proc.initialize()
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
+
# 🧪 backtest_engine.py (V200.0 - GEM-Architect: Stable & Robust)
|
| 3 |
# ============================================================
|
| 4 |
|
| 5 |
import asyncio
|
|
|
|
| 54 |
|
| 55 |
def optimize_dataframe_memory(df):
|
| 56 |
"""⬇️ Reduces memory usage by downcasting types"""
|
|
|
|
| 57 |
float_cols = df.select_dtypes(include=['float64']).columns
|
| 58 |
df[float_cols] = df[float_cols].astype('float32')
|
| 59 |
|
|
|
|
| 60 |
int_cols = df.select_dtypes(include=['int64', 'int32']).columns
|
| 61 |
for col in int_cols:
|
| 62 |
c_min = df[col].min()
|
|
|
|
| 90 |
# 🎛️ CONTROL PANEL - HIGH PRECISION RANGES
|
| 91 |
self.GRID_RANGES = {
|
| 92 |
# --- Models ---
|
| 93 |
+
'TITAN': np.linspace(0.30, 0.70, self.GRID_DENSITY),
|
| 94 |
+
'ORACLE': np.linspace(0.50, 0.75, self.GRID_DENSITY),
|
| 95 |
+
'SNIPER': np.linspace(0.30, 0.65, self.GRID_DENSITY),
|
| 96 |
+
'PATTERN': np.linspace(0.30, 0.70, self.GRID_DENSITY),
|
| 97 |
|
| 98 |
# --- Governance ---
|
| 99 |
+
'GOV_SCORE': np.linspace(50.0, 80.0, self.GRID_DENSITY),
|
| 100 |
|
| 101 |
# --- Guardians ---
|
| 102 |
+
'HYDRA_THRESH': np.linspace(0.65, 0.90, self.GRID_DENSITY),
|
| 103 |
+
'LEGACY_THRESH': np.linspace(0.88, 0.99, self.GRID_DENSITY)
|
| 104 |
}
|
| 105 |
|
| 106 |
self.TARGET_COINS = [
|
|
|
|
| 121 |
self.force_end_date = "2024-02-01"
|
| 122 |
|
| 123 |
if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
|
| 124 |
+
print(f"🧪 [Backtest V200.0] Stable Core (RAM Optimized + Math Fixes).")
|
| 125 |
|
| 126 |
def set_date_range(self, start_str, end_str):
|
| 127 |
self.force_start_date = start_str
|
|
|
|
| 279 |
is_market_dead = (h1_chop > 61.8) | ((h1_atr_pct < 0.3) & (h1_adx < 20))
|
| 280 |
market_status = np.where(is_market_dead, 0, 1)
|
| 281 |
|
| 282 |
+
# 2. COIN STATES (Relaxed for ACCUMULATION)
|
| 283 |
h1_rsi = numpy_htf['1h']['RSI'][map_1h]
|
| 284 |
h1_close = numpy_htf['1h']['close'][map_1h]
|
| 285 |
h1_bbw = numpy_htf['1h']['bb_width'][map_1h]
|
|
|
|
| 291 |
|
| 292 |
coin_state = np.zeros(len(arr_ts_1m), dtype=np.int8)
|
| 293 |
is_trash_vol = (h1_rel_vol < 0.5) | (h1_atr_pct < 0.2)
|
| 294 |
+
|
| 295 |
+
# ✅ Relaxed Accumulation Logic
|
| 296 |
+
# Old: bbw < 0.15, RSI 40-60
|
| 297 |
+
# New: bbw < 0.20, RSI 35-65
|
| 298 |
+
mask_acc = (h1_bbw < 0.20) & (h1_rsi >= 35) & (h1_rsi <= 65)
|
| 299 |
+
|
| 300 |
mask_safe = (h1_adx > 25) & (h1_ema20 > h1_ema50) & (h1_ema50 > h1_ema200) & (h1_rsi > 50) & (h1_rsi < 75)
|
| 301 |
mask_exp = (h1_rsi > 65) & (h1_close > h1_upper) & (h1_rel_vol > 1.5)
|
| 302 |
|
|
|
|
| 511 |
if c: await self._process_data_in_memory(sym, c, ms_s, ms_e)
|
| 512 |
|
| 513 |
def _worker_optimize(self, combinations_batch, scores_files, initial_capital, fees_pct, max_slots, target_state):
|
| 514 |
+
"""🚀 HYPER-SPEED JUMP LOGIC (RAM Optimized Batching + Math Safety)"""
|
| 515 |
print(f" ⏳ [System] Loading {len(scores_files)} datasets...", flush=True)
|
| 516 |
|
|
|
|
| 517 |
data = []
|
| 518 |
for f in scores_files:
|
| 519 |
try:
|
| 520 |
d = pd.read_pickle(f)
|
| 521 |
+
d = optimize_dataframe_memory(d)
|
| 522 |
data.append(d)
|
| 523 |
except: pass
|
| 524 |
if not data: return []
|
| 525 |
|
| 526 |
df = pd.concat(data).sort_values('timestamp').reset_index(drop=True)
|
| 527 |
+
del data
|
| 528 |
gc.collect()
|
| 529 |
|
|
|
|
|
|
|
|
|
|
| 530 |
ts = df['timestamp'].values
|
| 531 |
close = df['close'].values.astype(np.float32)
|
| 532 |
sym = df['symbol'].values
|
| 533 |
|
|
|
|
| 534 |
u_syms = np.unique(sym)
|
| 535 |
sym_map = {s: i for i, s in enumerate(u_syms)}
|
| 536 |
sym_id = np.array([sym_map[s] for s in sym], dtype=np.int16)
|
|
|
|
| 548 |
c_state = df['coin_state'].values
|
| 549 |
m_ok = df['market_ok'].values
|
| 550 |
|
|
|
|
| 551 |
del df, sym
|
| 552 |
gc.collect()
|
| 553 |
|
|
|
|
| 555 |
print(f" 🚀 [System] Testing {len(combinations_batch)} configs on {N} candidates...", flush=True)
|
| 556 |
|
| 557 |
res = []
|
|
|
|
| 558 |
BATCH_SIZE = 500
|
| 559 |
|
| 560 |
for i in range(0, len(combinations_batch), BATCH_SIZE):
|
|
|
|
| 578 |
pos = {}
|
| 579 |
bal = float(initial_capital)
|
| 580 |
log = []
|
|
|
|
| 581 |
trade_durs = []
|
| 582 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 583 |
for idx in range(N):
|
| 584 |
s = sym_id[idx]
|
| 585 |
p = close[idx]
|
| 586 |
|
|
|
|
| 587 |
if s in pos:
|
| 588 |
entry_p, size, e_idx = pos[s]
|
| 589 |
+
if entry_p > 0: # Safety check
|
| 590 |
+
pnl_pct = (p - entry_p) / entry_p
|
| 591 |
+
|
| 592 |
+
if (pnl_pct > 0.025) or (pnl_pct < -0.02):
|
| 593 |
+
val = size * (1 + pnl_pct)
|
| 594 |
+
fee = val * fees_pct
|
| 595 |
+
net = val - fee - size
|
| 596 |
+
|
| 597 |
+
# ✅ Safety: Check for NaN/Inf before committing
|
| 598 |
+
if np.isfinite(net) and abs(net) < 10000:
|
| 599 |
+
bal += size + net
|
| 600 |
+
del pos[s]
|
| 601 |
+
log.append(net)
|
| 602 |
+
trade_durs.append(idx - e_idx)
|
| 603 |
+
else:
|
| 604 |
+
# Fallback close if corrupted
|
| 605 |
+
bal += size
|
| 606 |
+
del pos[s]
|
| 607 |
+
continue
|
| 608 |
+
|
| 609 |
if entry_mask[idx] and len(pos) < max_slots:
|
| 610 |
if s not in pos and bal >= 5.0:
|
|
|
|
| 611 |
sc = gov_s[idx]
|
| 612 |
mult = 1.0 if sc >= 85 else (0.75 if sc >= 70 else 0.5)
|
|
|
|
| 613 |
base = bal * 0.95 if bal < self.MIN_CAPITAL_FOR_SPLIT else bal / max_slots
|
| 614 |
size = base * mult
|
| 615 |
|
|
|
|
| 618 |
pos[s] = (p, size - fee, idx)
|
| 619 |
bal -= size
|
| 620 |
|
|
|
|
| 621 |
tot = len(log)
|
| 622 |
if tot == 0: continue
|
| 623 |
|
| 624 |
+
# ✅ Math Safety Checks
|
| 625 |
net_p = bal + sum([v[1] for v in pos.values()]) - initial_capital
|
| 626 |
+
if not np.isfinite(net_p): net_p = -999.0
|
| 627 |
+
|
| 628 |
wins = [x for x in log if x > 0]
|
| 629 |
losses = [x for x in log if x <= 0]
|
| 630 |
|
| 631 |
wr = len(wins)/tot*100
|
| 632 |
+
avg_w = np.mean(wins) if wins else 0.0
|
| 633 |
+
avg_l = np.mean(losses) if losses else 0.0
|
|
|
|
| 634 |
|
| 635 |
+
sum_w = np.sum(wins)
|
| 636 |
+
sum_l = abs(np.sum(losses))
|
| 637 |
+
pf = (sum_w / sum_l) if sum_l > 0.001 else 99.0
|
| 638 |
|
| 639 |
res.append({
|
| 640 |
'config': cfg, 'final_balance': bal, 'net_profit': net_p,
|
| 641 |
'total_trades': tot, 'win_rate': wr, 'profit_factor': pf,
|
| 642 |
+
'max_drawdown': 0.0, 'sqn': 0,
|
| 643 |
'avg_duration_candles': np.mean(trade_durs) if trade_durs else 0,
|
| 644 |
'win_count': len(wins), 'loss_count': len(losses),
|
| 645 |
'avg_win_usd': avg_w, 'avg_loss_usd': avg_l,
|
| 646 |
'max_win_streak': 0, 'max_loss_streak': 0
|
| 647 |
})
|
| 648 |
|
|
|
|
| 649 |
gc.collect()
|
| 650 |
|
| 651 |
return res
|
|
|
|
| 674 |
|
| 675 |
for state_name, state_id in states:
|
| 676 |
print(f"\n🌀 Optimizing for [{state_name}]...")
|
|
|
|
| 677 |
results_list = self._worker_optimize(combos, files, self.INITIAL_CAPITAL, self.TRADING_FEES, self.MAX_SLOTS, state_id)
|
| 678 |
|
| 679 |
if not results_list:
|
|
|
|
| 711 |
print(f" ⚙️ Config: {p_str}")
|
| 712 |
print("="*80)
|
| 713 |
|
|
|
|
| 714 |
del results_list
|
| 715 |
gc.collect()
|
| 716 |
|
| 717 |
async def run_strategic_optimization_task():
|
| 718 |
+
print("\n🧪 [STRATEGIC BACKTEST] Stable Core (V200.0)...")
|
| 719 |
r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
|
| 720 |
try:
|
| 721 |
await dm.initialize(); await proc.initialize()
|