Spaces:
Sleeping
Sleeping
Update backtest_engine.py
Browse files- backtest_engine.py +156 -125
backtest_engine.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# ============================================================
|
| 2 |
-
# ๐งช backtest_engine.py (
|
| 3 |
# ============================================================
|
| 4 |
|
| 5 |
import asyncio
|
|
@@ -52,6 +52,25 @@ def _revive_score_distribution(scores):
|
|
| 52 |
return (scores - s_min) / (s_max - s_min)
|
| 53 |
return scores
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
# ============================================================
|
| 56 |
# ๐งช THE BACKTESTER CLASS
|
| 57 |
# ============================================================
|
|
@@ -60,9 +79,9 @@ class HeavyDutyBacktester:
|
|
| 60 |
self.dm = data_manager
|
| 61 |
self.proc = processor
|
| 62 |
|
| 63 |
-
# ๐๏ธ ุฅุนุฏุงุฏุงุช ุงูุดุจูุฉ ุงูุฐููุฉ
|
| 64 |
-
self.GRID_DENSITY = 10
|
| 65 |
-
self.MAX_SAMPLES = 5000
|
| 66 |
|
| 67 |
# โ
Smart Portfolio Constants
|
| 68 |
self.INITIAL_CAPITAL = 10.0
|
|
@@ -71,7 +90,6 @@ class HeavyDutyBacktester:
|
|
| 71 |
self.MAX_SLOTS = 4
|
| 72 |
|
| 73 |
# ๐๏ธ CONTROL PANEL - HIGH PRECISION RANGES
|
| 74 |
-
# ุจู
ุง ุฃููุง ูุณุชุฎุฏู
ุงูุนููุงุชุ ูู
ูููุง ุฑูุน ุงูุฏูุฉ ุฏูู ุฎูู ู
ู ุงูููุช
|
| 75 |
self.GRID_RANGES = {
|
| 76 |
# --- Models ---
|
| 77 |
'TITAN': np.linspace(0.30, 0.80, self.GRID_DENSITY),
|
|
@@ -82,7 +100,7 @@ class HeavyDutyBacktester:
|
|
| 82 |
# --- Governance ---
|
| 83 |
'GOV_SCORE': np.linspace(50.0, 85.0, self.GRID_DENSITY),
|
| 84 |
|
| 85 |
-
# --- Guardians
|
| 86 |
'HYDRA_THRESH': np.linspace(0.60, 0.90, self.GRID_DENSITY),
|
| 87 |
'LEGACY_THRESH': np.linspace(0.85, 0.99, self.GRID_DENSITY)
|
| 88 |
}
|
|
@@ -100,12 +118,12 @@ class HeavyDutyBacktester:
|
|
| 100 |
|
| 101 |
# โ
DATE SETTINGS
|
| 102 |
self.USE_FIXED_DATES = False
|
| 103 |
-
self.LOOKBACK_DAYS = 360
|
| 104 |
self.force_start_date = "2024-01-01"
|
| 105 |
self.force_end_date = "2024-02-01"
|
| 106 |
|
| 107 |
if not os.path.exists(CACHE_DIR): os.makedirs(CACHE_DIR)
|
| 108 |
-
print(f"๐งช [Backtest
|
| 109 |
|
| 110 |
def set_date_range(self, start_str, end_str):
|
| 111 |
self.force_start_date = start_str
|
|
@@ -458,10 +476,16 @@ class HeavyDutyBacktester:
|
|
| 458 |
'market_ok': market_status[candidate_indices]
|
| 459 |
})
|
| 460 |
|
|
|
|
|
|
|
|
|
|
| 461 |
dt = time.time() - t0
|
| 462 |
if not ai_results.empty:
|
| 463 |
ai_results.to_pickle(scores_file)
|
| 464 |
print(f" โ
[{sym}] Completed in {dt:.2f} seconds. ({len(ai_results)} signals)", flush=True)
|
|
|
|
|
|
|
|
|
|
| 465 |
gc.collect()
|
| 466 |
|
| 467 |
async def generate_truth_data(self):
|
|
@@ -484,19 +508,34 @@ class HeavyDutyBacktester:
|
|
| 484 |
if c: await self._process_data_in_memory(sym, c, ms_s, ms_e)
|
| 485 |
|
| 486 |
def _worker_optimize(self, combinations_batch, scores_files, initial_capital, fees_pct, max_slots, target_state):
|
| 487 |
-
"""๐ HYPER-SPEED JUMP LOGIC (
|
| 488 |
print(f" โณ [System] Loading {len(scores_files)} datasets...", flush=True)
|
|
|
|
|
|
|
| 489 |
data = []
|
| 490 |
for f in scores_files:
|
| 491 |
-
try:
|
|
|
|
|
|
|
|
|
|
| 492 |
except: pass
|
| 493 |
if not data: return []
|
|
|
|
| 494 |
df = pd.concat(data).sort_values('timestamp').reset_index(drop=True)
|
|
|
|
|
|
|
| 495 |
|
|
|
|
|
|
|
|
|
|
| 496 |
ts = df['timestamp'].values
|
| 497 |
-
close = df['close'].values.astype(
|
| 498 |
sym = df['symbol'].values
|
| 499 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
|
| 501 |
oracle = df['oracle_conf'].values
|
| 502 |
sniper = df['sniper_score'].values
|
|
@@ -511,114 +550,111 @@ class HeavyDutyBacktester:
|
|
| 511 |
c_state = df['coin_state'].values
|
| 512 |
m_ok = df['market_ok'].values
|
| 513 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 514 |
N = len(ts)
|
| 515 |
print(f" ๐ [System] Testing {len(combinations_batch)} configs on {N} candidates...", flush=True)
|
| 516 |
|
| 517 |
res = []
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
(oracle >= cfg['ORACLE']) & \
|
| 524 |
-
(sniper >= cfg['SNIPER']) & \
|
| 525 |
-
(titan >= cfg['TITAN']) & \
|
| 526 |
-
(patt >= cfg['PATTERN']) & \
|
| 527 |
-
(h_crash <= cfg['HYDRA_THRESH']) & \
|
| 528 |
-
(h_give <= cfg['HYDRA_THRESH']) & \
|
| 529 |
-
(l_v2 <= cfg['LEGACY_THRESH'])
|
| 530 |
-
|
| 531 |
-
pos = {}
|
| 532 |
-
bal = float(initial_capital)
|
| 533 |
-
balance_history = [bal]
|
| 534 |
-
alloc = 0.0
|
| 535 |
-
log = []
|
| 536 |
-
trade_durations = []
|
| 537 |
|
| 538 |
-
for
|
| 539 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
|
| 541 |
-
|
| 542 |
-
if
|
| 543 |
-
|
| 544 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 545 |
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
bal += size_val + net_pnl_usd
|
| 552 |
-
alloc -= size_val
|
| 553 |
-
del pos[s]
|
| 554 |
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 578 |
-
|
| 579 |
-
|
| 580 |
-
|
| 581 |
-
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
| 586 |
-
|
| 587 |
-
|
| 588 |
-
|
| 589 |
-
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
gross_l = abs(sum([x['pnl'] for x in losing]))
|
| 606 |
-
pf = (gross_p / gross_l) if gross_l > 0 else 99.9
|
| 607 |
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
else: curr_l +=1; curr_w = 0; max_loss_s = max(max_loss_s, curr_l)
|
| 612 |
-
|
| 613 |
-
res.append({
|
| 614 |
-
'config': cfg, 'final_balance': final_bal, 'net_profit': net_profit_usd,
|
| 615 |
-
'total_trades': tot, 'win_rate': win_rate, 'profit_factor': pf,
|
| 616 |
-
'max_drawdown': max_dd * 100, 'expectancy': expectancy, 'sqn': sqn,
|
| 617 |
-
'avg_duration_candles': avg_dur,
|
| 618 |
-
'win_count': len(winning), 'loss_count': len(losing),
|
| 619 |
-
'avg_win_usd': avg_win_usd, 'avg_loss_usd': avg_loss_usd,
|
| 620 |
-
'max_win_streak': max_win_s, 'max_loss_streak': max_loss_s
|
| 621 |
-
})
|
| 622 |
return res
|
| 623 |
|
| 624 |
async def run_optimization(self):
|
|
@@ -626,7 +662,6 @@ class HeavyDutyBacktester:
|
|
| 626 |
|
| 627 |
files = glob.glob(os.path.join(CACHE_DIR, "*.pkl"))
|
| 628 |
|
| 629 |
-
# โ
Generate Random Samples from the Dense Grid
|
| 630 |
keys = list(self.GRID_RANGES.keys())
|
| 631 |
value_lists = [list(self.GRID_RANGES[k]) for k in keys]
|
| 632 |
|
|
@@ -634,42 +669,38 @@ class HeavyDutyBacktester:
|
|
| 634 |
combos = []
|
| 635 |
seen = set()
|
| 636 |
attempts = 0
|
| 637 |
-
|
| 638 |
while len(combos) < self.MAX_SAMPLES and attempts < self.MAX_SAMPLES * 5:
|
| 639 |
current = tuple(np.random.choice(v_list) for v_list in value_lists)
|
| 640 |
if current not in seen:
|
| 641 |
seen.add(current)
|
| 642 |
combos.append(dict(zip(keys, current)))
|
| 643 |
attempts += 1
|
| 644 |
-
|
| 645 |
print(f"โ
Generated {len(combos)} unique configs for testing.")
|
| 646 |
|
| 647 |
-
# โ
LOOP THROUGH STATES
|
| 648 |
states = [("ACCUMULATION", 1), ("SAFE_TREND", 2), ("EXPLOSIVE", 3)]
|
| 649 |
|
| 650 |
for state_name, state_id in states:
|
| 651 |
print(f"\n๐ Optimizing for [{state_name}]...")
|
|
|
|
| 652 |
results_list = self._worker_optimize(combos, files, self.INITIAL_CAPITAL, self.TRADING_FEES, self.MAX_SLOTS, state_id)
|
| 653 |
|
| 654 |
if not results_list:
|
| 655 |
print(f"โ ๏ธ No valid trades found for {state_name}.")
|
| 656 |
continue
|
| 657 |
|
| 658 |
-
results_list.sort(key=lambda x:
|
| 659 |
best = results_list[0]
|
| 660 |
|
| 661 |
diag = []
|
| 662 |
if best['total_trades'] < 10: diag.append("โ ๏ธ Starvation")
|
| 663 |
-
if best['
|
| 664 |
-
if best['win_rate'] < 40 and best['profit_factor'] < 1.0: diag.append("โ ๏ธ Failed")
|
| 665 |
-
if abs(best['avg_loss_usd']) > best['avg_win_usd']: diag.append("โ ๏ธ R/R Inv")
|
| 666 |
if not diag: diag.append("โ
Robust")
|
| 667 |
|
| 668 |
print("\n" + "="*80)
|
| 669 |
print(f"๐ CHAMPION REPORT [{state_name}]:")
|
| 670 |
print(f" ๐ฐ Initial Capital: ${self.INITIAL_CAPITAL:,.2f}")
|
| 671 |
print(f" ๐ฐ Final Balance: ${best['final_balance']:,.2f}")
|
| 672 |
-
print(f" ๐ Net PnL: ${best['net_profit']:,.2f}
|
| 673 |
print("-" * 80)
|
| 674 |
print(f" ๐ Total Trades: {best['total_trades']}")
|
| 675 |
print(f" ๐ Win Rate: {best['win_rate']:.1f}%")
|
|
@@ -677,11 +708,7 @@ class HeavyDutyBacktester:
|
|
| 677 |
print(f" โ Loss Count: {best['loss_count']} (Avg: ${best['avg_loss_usd']:.2f})")
|
| 678 |
print("-" * 80)
|
| 679 |
print(f" โ๏ธ Profit Factor: {best['profit_factor']:.2f}")
|
| 680 |
-
print(f" ๐ Max Drawdown: {best['max_drawdown']:.2f}%")
|
| 681 |
-
print(f" ๐ Expectancy: ${best['expectancy']:.2f}")
|
| 682 |
-
print(f" โญ SQN Score: {best['sqn']:.2f}")
|
| 683 |
print(f" โฑ๏ธ Avg Duration: {best['avg_duration_candles']:.0f} mins")
|
| 684 |
-
print(f" ๐ Max Streaks: Win {best['max_win_streak']} | Loss {best['max_loss_streak']}")
|
| 685 |
print("-" * 80)
|
| 686 |
print(f" ๐ฉบ DIAGNOSIS: {' '.join(diag)}")
|
| 687 |
|
|
@@ -691,9 +718,13 @@ class HeavyDutyBacktester:
|
|
| 691 |
else: p_str += f"{k}={v} | "
|
| 692 |
print(f" โ๏ธ Config: {p_str}")
|
| 693 |
print("="*80)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
|
| 695 |
async def run_strategic_optimization_task():
|
| 696 |
-
print("\n๐งช [STRATEGIC BACKTEST]
|
| 697 |
r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
|
| 698 |
try:
|
| 699 |
await dm.initialize(); await proc.initialize()
|
|
|
|
| 1 |
# ============================================================
|
| 2 |
+
# ๐งช backtest_engine.py (V195.0 - GEM-Architect: RAM Optimization & Batching)
|
| 3 |
# ============================================================
|
| 4 |
|
| 5 |
import asyncio
|
|
|
|
| 52 |
return (scores - s_min) / (s_max - s_min)
|
| 53 |
return 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()
|
| 65 |
+
c_max = df[col].max()
|
| 66 |
+
if c_min > -128 and c_max < 127:
|
| 67 |
+
df[col] = df[col].astype('int8')
|
| 68 |
+
elif c_min > -32768 and c_max < 32767:
|
| 69 |
+
df[col] = df[col].astype('int16')
|
| 70 |
+
else:
|
| 71 |
+
df[col] = df[col].astype('int32')
|
| 72 |
+
return df
|
| 73 |
+
|
| 74 |
# ============================================================
|
| 75 |
# ๐งช THE BACKTESTER CLASS
|
| 76 |
# ============================================================
|
|
|
|
| 79 |
self.dm = data_manager
|
| 80 |
self.proc = processor
|
| 81 |
|
| 82 |
+
# ๐๏ธ ุฅุนุฏุงุฏุงุช ุงูุดุจูุฉ ุงูุฐููุฉ
|
| 83 |
+
self.GRID_DENSITY = 10
|
| 84 |
+
self.MAX_SAMPLES = 5000
|
| 85 |
|
| 86 |
# โ
Smart Portfolio Constants
|
| 87 |
self.INITIAL_CAPITAL = 10.0
|
|
|
|
| 90 |
self.MAX_SLOTS = 4
|
| 91 |
|
| 92 |
# ๐๏ธ CONTROL PANEL - HIGH PRECISION RANGES
|
|
|
|
| 93 |
self.GRID_RANGES = {
|
| 94 |
# --- Models ---
|
| 95 |
'TITAN': np.linspace(0.30, 0.80, self.GRID_DENSITY),
|
|
|
|
| 100 |
# --- Governance ---
|
| 101 |
'GOV_SCORE': np.linspace(50.0, 85.0, self.GRID_DENSITY),
|
| 102 |
|
| 103 |
+
# --- Guardians ---
|
| 104 |
'HYDRA_THRESH': np.linspace(0.60, 0.90, self.GRID_DENSITY),
|
| 105 |
'LEGACY_THRESH': np.linspace(0.85, 0.99, self.GRID_DENSITY)
|
| 106 |
}
|
|
|
|
| 118 |
|
| 119 |
# โ
DATE SETTINGS
|
| 120 |
self.USE_FIXED_DATES = False
|
| 121 |
+
self.LOOKBACK_DAYS = 360
|
| 122 |
self.force_start_date = "2024-01-01"
|
| 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 V195.0] Memory Safe Mode (Chunking & Compression).")
|
| 127 |
|
| 128 |
def set_date_range(self, start_str, end_str):
|
| 129 |
self.force_start_date = start_str
|
|
|
|
| 476 |
'market_ok': market_status[candidate_indices]
|
| 477 |
})
|
| 478 |
|
| 479 |
+
# โ
Compress Types Before Saving (Crucial for RAM)
|
| 480 |
+
ai_results = optimize_dataframe_memory(ai_results)
|
| 481 |
+
|
| 482 |
dt = time.time() - t0
|
| 483 |
if not ai_results.empty:
|
| 484 |
ai_results.to_pickle(scores_file)
|
| 485 |
print(f" โ
[{sym}] Completed in {dt:.2f} seconds. ({len(ai_results)} signals)", flush=True)
|
| 486 |
+
|
| 487 |
+
# โ
Immediate Garbage Collection
|
| 488 |
+
del df_1m, frames, fast_1m, numpy_htf, X_TITAN, X_ORACLE, X_SNIPER, X_H, X_V2
|
| 489 |
gc.collect()
|
| 490 |
|
| 491 |
async def generate_truth_data(self):
|
|
|
|
| 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) # Ensure compressed in RAM
|
| 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 # Free list memory
|
| 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)
|
| 539 |
|
| 540 |
oracle = df['oracle_conf'].values
|
| 541 |
sniper = df['sniper_score'].values
|
|
|
|
| 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 |
+
|
| 557 |
N = len(ts)
|
| 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):
|
| 565 |
+
batch = combinations_batch[i:i+BATCH_SIZE]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
|
| 567 |
+
for cfg in batch:
|
| 568 |
+
entry_mask = (m_ok == 1) & \
|
| 569 |
+
(c_state == target_state) & \
|
| 570 |
+
(gov_s >= cfg['GOV_SCORE']) & \
|
| 571 |
+
(oracle >= cfg['ORACLE']) & \
|
| 572 |
+
(sniper >= cfg['SNIPER']) & \
|
| 573 |
+
(titan >= cfg['TITAN']) & \
|
| 574 |
+
(patt >= cfg['PATTERN']) & \
|
| 575 |
+
(h_crash <= cfg['HYDRA_THRESH']) & \
|
| 576 |
+
(h_give <= cfg['HYDRA_THRESH']) & \
|
| 577 |
+
(l_v2 <= cfg['LEGACY_THRESH'])
|
| 578 |
|
| 579 |
+
valid_indices = np.where(entry_mask)[0]
|
| 580 |
+
if len(valid_indices) == 0: continue
|
| 581 |
+
|
| 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 |
+
pnl_pct = (p - entry_p) / entry_p
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
|
| 604 |
+
if (pnl_pct > 0.025) or (pnl_pct < -0.02):
|
| 605 |
+
val = size * (1 + pnl_pct)
|
| 606 |
+
fee = val * fees_pct
|
| 607 |
+
net = val - fee - size
|
| 608 |
+
bal += size + net
|
| 609 |
+
del pos[s]
|
| 610 |
+
log.append(net)
|
| 611 |
+
trade_durs.append(idx - e_idx)
|
| 612 |
+
continue
|
| 613 |
+
|
| 614 |
+
# Entry Check
|
| 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 |
+
|
| 624 |
+
if size >= 5.0:
|
| 625 |
+
fee = size * fees_pct
|
| 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 |
+
# Simple DD calc (approximation to save RAM)
|
| 643 |
+
dd = 0.0 # Skipping detailed DD for speed/RAM
|
| 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': dd, 'sqn': 0, # Skip heavy calc
|
| 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
|
| 659 |
|
| 660 |
async def run_optimization(self):
|
|
|
|
| 662 |
|
| 663 |
files = glob.glob(os.path.join(CACHE_DIR, "*.pkl"))
|
| 664 |
|
|
|
|
| 665 |
keys = list(self.GRID_RANGES.keys())
|
| 666 |
value_lists = [list(self.GRID_RANGES[k]) for k in keys]
|
| 667 |
|
|
|
|
| 669 |
combos = []
|
| 670 |
seen = set()
|
| 671 |
attempts = 0
|
|
|
|
| 672 |
while len(combos) < self.MAX_SAMPLES and attempts < self.MAX_SAMPLES * 5:
|
| 673 |
current = tuple(np.random.choice(v_list) for v_list in value_lists)
|
| 674 |
if current not in seen:
|
| 675 |
seen.add(current)
|
| 676 |
combos.append(dict(zip(keys, current)))
|
| 677 |
attempts += 1
|
|
|
|
| 678 |
print(f"โ
Generated {len(combos)} unique configs for testing.")
|
| 679 |
|
|
|
|
| 680 |
states = [("ACCUMULATION", 1), ("SAFE_TREND", 2), ("EXPLOSIVE", 3)]
|
| 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:
|
| 688 |
print(f"โ ๏ธ No valid trades found for {state_name}.")
|
| 689 |
continue
|
| 690 |
|
| 691 |
+
results_list.sort(key=lambda x: x['net_profit'], reverse=True)
|
| 692 |
best = results_list[0]
|
| 693 |
|
| 694 |
diag = []
|
| 695 |
if best['total_trades'] < 10: diag.append("โ ๏ธ Starvation")
|
| 696 |
+
if best['win_rate'] < 40: diag.append("โ ๏ธ Low WR")
|
|
|
|
|
|
|
| 697 |
if not diag: diag.append("โ
Robust")
|
| 698 |
|
| 699 |
print("\n" + "="*80)
|
| 700 |
print(f"๐ CHAMPION REPORT [{state_name}]:")
|
| 701 |
print(f" ๐ฐ Initial Capital: ${self.INITIAL_CAPITAL:,.2f}")
|
| 702 |
print(f" ๐ฐ Final Balance: ${best['final_balance']:,.2f}")
|
| 703 |
+
print(f" ๐ Net PnL: ${best['net_profit']:,.2f}")
|
| 704 |
print("-" * 80)
|
| 705 |
print(f" ๐ Total Trades: {best['total_trades']}")
|
| 706 |
print(f" ๐ Win Rate: {best['win_rate']:.1f}%")
|
|
|
|
| 708 |
print(f" โ Loss Count: {best['loss_count']} (Avg: ${best['avg_loss_usd']:.2f})")
|
| 709 |
print("-" * 80)
|
| 710 |
print(f" โ๏ธ Profit Factor: {best['profit_factor']:.2f}")
|
|
|
|
|
|
|
|
|
|
| 711 |
print(f" โฑ๏ธ Avg Duration: {best['avg_duration_candles']:.0f} mins")
|
|
|
|
| 712 |
print("-" * 80)
|
| 713 |
print(f" ๐ฉบ DIAGNOSIS: {' '.join(diag)}")
|
| 714 |
|
|
|
|
| 718 |
else: p_str += f"{k}={v} | "
|
| 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] Memory Safe Mode...")
|
| 728 |
r2 = R2Service(); dm = DataManager(None, None, r2); proc = MLProcessor(dm)
|
| 729 |
try:
|
| 730 |
await dm.initialize(); await proc.initialize()
|