Spaces:
Sleeping
Sleeping
Create backtest_engine.py
Browse files- backtest_engine.py +280 -0
backtest_engine.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ============================================================
|
| 2 |
+
# 🧪 backtest_engine.py (V36.1 - GEM-Architect: Full Spectrum)
|
| 3 |
+
# ============================================================
|
| 4 |
+
# التحديثات:
|
| 5 |
+
# 1. إدراج Sniper في المحاكاة (ML Only Mode).
|
| 6 |
+
# 2. تحييد وزن دفتر الطلبات داخل Sniper مؤقتاً أثناء الاختبار.
|
| 7 |
+
# 3. تحسين نظام التوليد (Resampling) ليكون Async بالكامل.
|
| 8 |
+
# ============================================================
|
| 9 |
+
|
| 10 |
+
import asyncio
|
| 11 |
+
import pandas as pd
|
| 12 |
+
import numpy as np
|
| 13 |
+
import json
|
| 14 |
+
import time
|
| 15 |
+
from datetime import datetime, timedelta
|
| 16 |
+
|
| 17 |
+
# استيراد المكونات الأساسية
|
| 18 |
+
from ml_engine.processor import MLProcessor, SystemLimits
|
| 19 |
+
from ml_engine.data_manager import DataManager
|
| 20 |
+
from learning_hub.adaptive_hub import StrategyDNA
|
| 21 |
+
|
| 22 |
+
class BacktestSimulator:
|
| 23 |
+
def __init__(self, data_manager, processor):
|
| 24 |
+
self.dm = data_manager
|
| 25 |
+
self.proc = processor
|
| 26 |
+
self.history_cache = {} # {symbol: DataFrame_1m_Full}
|
| 27 |
+
|
| 28 |
+
# إعدادات المحاكاة
|
| 29 |
+
self.DAYS_TO_FETCH = 30
|
| 30 |
+
self.CHUNK_LIMIT = 1500
|
| 31 |
+
|
| 32 |
+
print("🧪 [Backtest Engine V36.1] Full-Spectrum Simulation Initialized.")
|
| 33 |
+
|
| 34 |
+
# ==========================================================================
|
| 35 |
+
# 1. نظام جلب البيانات (كما هو)
|
| 36 |
+
# ==========================================================================
|
| 37 |
+
async def fetch_deep_history_1m(self, symbols: list):
|
| 38 |
+
"""جلب شهر كامل بدقة دقيقة واحدة"""
|
| 39 |
+
print(f"⏳ [Backtest] Fetching 1m candles for {self.DAYS_TO_FETCH} days...")
|
| 40 |
+
end_time = int(time.time() * 1000)
|
| 41 |
+
start_time = end_time - (self.DAYS_TO_FETCH * 24 * 60 * 60 * 1000)
|
| 42 |
+
|
| 43 |
+
for sym in symbols:
|
| 44 |
+
print(f" 📥 Downloading {sym}...", end="", flush=True)
|
| 45 |
+
all_candles = []
|
| 46 |
+
current_end = end_time
|
| 47 |
+
|
| 48 |
+
while current_end > start_time:
|
| 49 |
+
candles = await self.dm.exchange.fetch_ohlcv(
|
| 50 |
+
sym, '1m', limit=self.CHUNK_LIMIT, params={'endAt': int(current_end / 1000)}
|
| 51 |
+
)
|
| 52 |
+
if not candles: break
|
| 53 |
+
|
| 54 |
+
first_candle_time = candles[0][0]
|
| 55 |
+
current_end = first_candle_time - 60000
|
| 56 |
+
all_candles = candles + all_candles
|
| 57 |
+
await asyncio.sleep(0.05)
|
| 58 |
+
|
| 59 |
+
if all_candles:
|
| 60 |
+
df = pd.DataFrame(all_candles, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
|
| 61 |
+
df['datetime'] = pd.to_datetime(df['timestamp'], unit='ms')
|
| 62 |
+
df = df.set_index('datetime').sort_index()
|
| 63 |
+
mask = (df.index >= pd.to_datetime(start_time, unit='ms')) & (df.index <= pd.to_datetime(end_time, unit='ms'))
|
| 64 |
+
self.history_cache[sym] = df.loc[mask]
|
| 65 |
+
print(f" ✅ ({len(self.history_cache[sym])} candles)")
|
| 66 |
+
else:
|
| 67 |
+
print(" ❌ No Data")
|
| 68 |
+
|
| 69 |
+
# ==========================================================================
|
| 70 |
+
# 2. نظام توليد الإطارات (Resampling)
|
| 71 |
+
# ==========================================================================
|
| 72 |
+
def resample_data(self, df_1m, end_idx):
|
| 73 |
+
"""توليد الهرم المقلوب من البيانات"""
|
| 74 |
+
start_pos = max(0, end_idx - 1000) # يكفي 1000 دقيقة للوراء
|
| 75 |
+
slice_1m = df_1m.iloc[start_pos : end_idx].copy()
|
| 76 |
+
|
| 77 |
+
if slice_1m.empty or len(slice_1m) < 60: return None
|
| 78 |
+
|
| 79 |
+
timeframes = {'1m': slice_1m.reset_index(drop=True).values.tolist()}
|
| 80 |
+
agg_dict = {'timestamp': 'first', 'open': 'first', 'high': 'max', 'low': 'min', 'close': 'last', 'volume': 'sum'}
|
| 81 |
+
|
| 82 |
+
try:
|
| 83 |
+
for tf, rule in [('5m', '5T'), ('15m', '15T'), ('1h', '1H'), ('4h', '4H')]:
|
| 84 |
+
resampled = slice_1m.resample(rule, on='datetime').agg(agg_dict).dropna()
|
| 85 |
+
timeframes[tf] = resampled.reset_index(drop=True).values.tolist()
|
| 86 |
+
return timeframes
|
| 87 |
+
except: return None
|
| 88 |
+
|
| 89 |
+
# ==========================================================================
|
| 90 |
+
# 3. محرك المحاكاة (Async Simulation Core)
|
| 91 |
+
# ==========================================================================
|
| 92 |
+
async def run_regime_simulation(self, regime_name: str, candidate_weights: dict):
|
| 93 |
+
"""
|
| 94 |
+
تشغيل المحاكاة مع دمج Sniper في المعادلة
|
| 95 |
+
"""
|
| 96 |
+
# 1. حقن الأوزان في الدستور (SystemLimits)
|
| 97 |
+
SystemLimits.CURRENT_REGIME = regime_name
|
| 98 |
+
SystemLimits.L2_WEIGHT_TITAN = candidate_weights['titan']
|
| 99 |
+
SystemLimits.L2_WEIGHT_PATTERNS = candidate_weights['patterns']
|
| 100 |
+
SystemLimits.L2_WEIGHT_MC = candidate_weights['mc']
|
| 101 |
+
|
| 102 |
+
# 2. تكوين Sniper لوضع "الباكتست" (تجاهل دفتر الطلبات المفقود)
|
| 103 |
+
if self.proc.sniper:
|
| 104 |
+
self.proc.sniper.configure_settings(
|
| 105 |
+
threshold=0.4, # عتبة افتراضية
|
| 106 |
+
wall_ratio=0.9, # تساهل تام مع الجدران (لأنها غير موجودة)
|
| 107 |
+
w_ml=1.0, # 🔥 الاعتماد الكلي على ML
|
| 108 |
+
w_ob=0.0 # إلغاء وزن دفتر الطلبات
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
capital = 1000.0
|
| 112 |
+
trades = 0
|
| 113 |
+
wins = 0
|
| 114 |
+
|
| 115 |
+
print(f" 🧪 Testing Weights: {candidate_weights} ...")
|
| 116 |
+
|
| 117 |
+
for sym, df_1m_full in self.history_cache.items():
|
| 118 |
+
if df_1m_full.empty: continue
|
| 119 |
+
|
| 120 |
+
# نمشي بخطوات 30 دقيقة (تسريعاً للعملية)
|
| 121 |
+
step = 30
|
| 122 |
+
for i in range(1000, len(df_1m_full) - 240, step):
|
| 123 |
+
|
| 124 |
+
ohlcv_multi = self.resample_data(df_1m_full, i)
|
| 125 |
+
if not ohlcv_multi: continue
|
| 126 |
+
|
| 127 |
+
current_price = df_1m_full.iloc[i]['close']
|
| 128 |
+
|
| 129 |
+
# --- A. Titan Prediction ---
|
| 130 |
+
titan_score = 0.5
|
| 131 |
+
if self.proc.titan:
|
| 132 |
+
res = await asyncio.to_thread(self.proc.titan.predict, ohlcv_multi)
|
| 133 |
+
titan_score = res.get('score', 0.5)
|
| 134 |
+
|
| 135 |
+
# --- B. Patterns Prediction ---
|
| 136 |
+
pattern_score = 0.5
|
| 137 |
+
if self.proc.patterns:
|
| 138 |
+
self.proc.patterns.configure_thresholds(
|
| 139 |
+
weights=SystemLimits.PATTERN_TF_WEIGHTS,
|
| 140 |
+
bull_thresh=SystemLimits.PATTERN_THRESH_BULLISH,
|
| 141 |
+
bear_thresh=SystemLimits.PATTERN_THRESH_BEARISH
|
| 142 |
+
)
|
| 143 |
+
res = await self.proc.patterns.detect_chart_patterns(ohlcv_multi)
|
| 144 |
+
pattern_score = res.get('pattern_confidence', 0.5)
|
| 145 |
+
|
| 146 |
+
# --- C. Sniper Prediction (ML Only) ---
|
| 147 |
+
sniper_score = 0.5
|
| 148 |
+
if self.proc.sniper:
|
| 149 |
+
# Sniper يحتاج قائمة 1m
|
| 150 |
+
res = await self.proc.sniper.check_entry_signal_async(ohlcv_multi['1m'], order_book_data=None)
|
| 151 |
+
sniper_score = res.get('ml_score', 0.5) # نأخذ ml_score الخام
|
| 152 |
+
|
| 153 |
+
# --- D. Monte Carlo ---
|
| 154 |
+
mc_score = 0.5
|
| 155 |
+
if self.proc.mc_analyzer:
|
| 156 |
+
closes_1h = [c[4] for c in ohlcv_multi['1h']]
|
| 157 |
+
raw_mc = self.proc.mc_analyzer.run_light_check(closes_1h)
|
| 158 |
+
mc_score = 0.5 + (raw_mc * 5.0)
|
| 159 |
+
|
| 160 |
+
# --- E. حساب النتيجة الموزونة (Weighted Score) ---
|
| 161 |
+
w_t = candidate_weights['titan']
|
| 162 |
+
w_p = candidate_weights['patterns']
|
| 163 |
+
w_s = candidate_weights['sniper'] # ✅ الوزن الجديد
|
| 164 |
+
w_m = candidate_weights['mc']
|
| 165 |
+
|
| 166 |
+
total = w_t + w_p + w_s + w_m
|
| 167 |
+
final_score = ((titan_score * w_t) + (pattern_score * w_p) + (sniper_score * w_s) + (mc_score * w_m)) / total
|
| 168 |
+
|
| 169 |
+
# قرار الدخول
|
| 170 |
+
if final_score > 0.75:
|
| 171 |
+
# فحص النتيجة بعد 4 ساعات
|
| 172 |
+
future_idx = i + 240 # 4 ساعات * 60 دقيقة
|
| 173 |
+
if future_idx >= len(df_1m_full): break
|
| 174 |
+
|
| 175 |
+
future_price = df_1m_full.iloc[future_idx]['close']
|
| 176 |
+
change = (future_price - current_price) / current_price
|
| 177 |
+
|
| 178 |
+
if change > 0.02: # ربح 2%
|
| 179 |
+
capital *= 1.02
|
| 180 |
+
wins += 1
|
| 181 |
+
i += 120 # قفزة لتجنب التكرار
|
| 182 |
+
elif change < -0.015: # خسارة 1.5%
|
| 183 |
+
capital *= 0.985
|
| 184 |
+
i += 120
|
| 185 |
+
|
| 186 |
+
trades += 1
|
| 187 |
+
|
| 188 |
+
win_rate = (wins / trades * 100) if trades > 0 else 0
|
| 189 |
+
return {'final_capital': capital, 'win_rate': win_rate, 'trades': trades}
|
| 190 |
+
|
| 191 |
+
# ==========================================================================
|
| 192 |
+
# 4. المايسترو (Genetic Optimizer with Sniper)
|
| 193 |
+
# ==========================================================================
|
| 194 |
+
async def optimize_dna(self):
|
| 195 |
+
best_dna = {}
|
| 196 |
+
regimes = ['BULL', 'BEAR', 'RANGE'] # DEAD يحتاج استراتيجية خاصة
|
| 197 |
+
|
| 198 |
+
# مساحة البحث (تشمل الآن Sniper)
|
| 199 |
+
search_space = [
|
| 200 |
+
# 1. متوازن
|
| 201 |
+
{'titan': 0.3, 'patterns': 0.3, 'sniper': 0.3, 'mc': 0.1},
|
| 202 |
+
# 2. تركيز على Titan (Trend)
|
| 203 |
+
{'titan': 0.6, 'patterns': 0.2, 'sniper': 0.1, 'mc': 0.1},
|
| 204 |
+
# 3. تركيز على Sniper (Scalping/Momentum)
|
| 205 |
+
{'titan': 0.2, 'patterns': 0.2, 'sniper': 0.5, 'mc': 0.1},
|
| 206 |
+
# 4. تركيز على Patterns (Reversal)
|
| 207 |
+
{'titan': 0.2, 'patterns': 0.6, 'sniper': 0.1, 'mc': 0.1},
|
| 208 |
+
]
|
| 209 |
+
|
| 210 |
+
for regime in regimes:
|
| 211 |
+
print(f"\n🧬 Optimizing DNA for {regime}...")
|
| 212 |
+
best_score = -9999
|
| 213 |
+
best_w = None
|
| 214 |
+
|
| 215 |
+
for weights in search_space:
|
| 216 |
+
# تشغيل المحاكاة (Async مباشرة)
|
| 217 |
+
res = await self.run_regime_simulation(regime, weights)
|
| 218 |
+
|
| 219 |
+
score = res['final_capital']
|
| 220 |
+
print(f" -> {weights} => Cap: ${score:.1f} | WR: {res['win_rate']:.1f}% ({res['trades']} trds)")
|
| 221 |
+
|
| 222 |
+
if score > best_score:
|
| 223 |
+
best_score = score
|
| 224 |
+
best_w = weights
|
| 225 |
+
|
| 226 |
+
# حفظ أفضل إعدادات (مع إضافة Hydra كوزن ثابت لأنه خروج أكثر منه دخول)
|
| 227 |
+
if best_w:
|
| 228 |
+
best_dna[regime] = {
|
| 229 |
+
"model_weights": {
|
| 230 |
+
"titan": best_w['titan'],
|
| 231 |
+
"patterns": best_w['patterns'],
|
| 232 |
+
"sniper": best_w['sniper'],
|
| 233 |
+
"hydra": 0.1, # Hydra يضاف كوزن ثابت للدخول (أو يترك للحراس)
|
| 234 |
+
"mc": best_w['mc']
|
| 235 |
+
},
|
| 236 |
+
"ob_settings": {"wall_ratio_limit": 0.4, "imbalance_thresh": 0.5},
|
| 237 |
+
"filters": {"l1_min_score": 15.0, "l3_conf_thresh": 0.65}
|
| 238 |
+
}
|
| 239 |
+
print(f"🏆 WINNER for {regime}: Ti={best_w['titan']}, Sn={best_w['sniper']}")
|
| 240 |
+
|
| 241 |
+
return best_dna
|
| 242 |
+
|
| 243 |
+
# ============================================================
|
| 244 |
+
# 🚀 نقطة التشغيل
|
| 245 |
+
# ============================================================
|
| 246 |
+
async def run_strategic_optimization_task():
|
| 247 |
+
print("\n🧪 [STRATEGIC BACKTEST] Starting Sequence...")
|
| 248 |
+
|
| 249 |
+
# 1. تهيئة الخدمات
|
| 250 |
+
from r2 import R2Service
|
| 251 |
+
r2 = R2Service()
|
| 252 |
+
dm = DataManager(None, None, r2)
|
| 253 |
+
await dm.initialize()
|
| 254 |
+
proc = MLProcessor(dm)
|
| 255 |
+
await proc.initialize()
|
| 256 |
+
|
| 257 |
+
sim = BacktestSimulator(dm, proc)
|
| 258 |
+
|
| 259 |
+
# 2. تحميل البيانات
|
| 260 |
+
benchmark_coins = ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'XRP/USDT']
|
| 261 |
+
await sim.fetch_deep_history_1m(benchmark_coins)
|
| 262 |
+
|
| 263 |
+
# 3. التشغيل والتحسين
|
| 264 |
+
optimized_strategies = await sim.optimize_dna()
|
| 265 |
+
|
| 266 |
+
# 4. حفظ النتائج وتحديث AdaptiveHub
|
| 267 |
+
from learning_hub.adaptive_hub import AdaptiveHub
|
| 268 |
+
hub = AdaptiveHub(r2)
|
| 269 |
+
await hub.initialize()
|
| 270 |
+
|
| 271 |
+
for reg, data in optimized_strategies.items():
|
| 272 |
+
if reg in hub.strategies:
|
| 273 |
+
hub.strategies[reg].model_weights.update(data['model_weights'])
|
| 274 |
+
|
| 275 |
+
await hub._save_state_to_r2()
|
| 276 |
+
await dm.close()
|
| 277 |
+
print("✅ [STRATEGIC BACKTEST] Completed Successfully.")
|
| 278 |
+
|
| 279 |
+
if __name__ == "__main__":
|
| 280 |
+
asyncio.run(run_strategic_optimization_task())
|