Spaces:
Running
Running
| # simulation_engine/sim_runner.py | |
| # V3.0 — Balanced-Fast: more trades, better expectancy, single-position, 1–2h time stops. | |
| import asyncio | |
| import os | |
| import pandas as pd | |
| from tqdm import tqdm | |
| from r2 import R2Service | |
| from data_manager import DataManager | |
| from ml_engine.processor import MLProcessor | |
| from learning_hub.hub_manager import LearningHubManager | |
| try: | |
| from .mock_kucoin import MockKuCoin | |
| from .virtual_exchange import VirtualExchange | |
| except ImportError: | |
| # إذا كانت البنية مختلفة، حدّث المسارات حسب مشروعك | |
| from simulation_engine.mock_kucoin import MockKuCoin | |
| from simulation_engine.virtual_exchange import VirtualExchange | |
| SIM_CONFIG = { | |
| # نافذة الاختبار | |
| "START_DATE": "2025-09-10", | |
| "END_DATE": "2025-11-09", | |
| # المحفظة والتنفيذ | |
| "INITIAL_BALANCE": 10.0, | |
| "FEE_RATE": 0.001, # 0.1% | |
| # سرعة القرار وجودته | |
| "STEP_TF": "5m", # قرار كل 5 دقائق | |
| "ENTRY_THRESHOLD": 0.22, # عتبة دخول متوازنة لرفع العدد دون ضوضاء مفرطة | |
| # حجم الصفقة والقيود | |
| "POSITION_FRACTION": 0.45, # 45% من الرصيد لكل صفقة | |
| "MAX_CONCURRENT_POSITIONS": 1, # صفقة واحدة في نفس الوقت | |
| "MIN_TRADE_USD": 0.10, | |
| # الخروج: هدف/وقف + إيقاف زمني مرن | |
| "TP_PCT": 1.6, # 1.6% | |
| "SL_PCT": 0.8, # 0.8% | |
| "SOFT_TIME_STOP_BARS": 12, # ≈ 1 ساعة عند STEP_TF=5m: يغلق عند التعادل أو أفضل | |
| "HARD_TIME_STOP_BARS": 24, # ≈ 2 ساعة: إغلاق قسري | |
| "COOLDOWN_BARS": 1, # تبريد قصير لمنع إعادة الدخول الفوري | |
| # الكون | |
| "TEST_SYMBOLS": [ | |
| "BTC/USDT","ETH/USDT","SOL/USDT","BNB/USDT","XRP/USDT", | |
| "DOGE/USDT","ADA/USDT","AVAX/USDT","LINK/USDT" | |
| ], | |
| "LOCAL_DATA_DIR": "./simulation_data" | |
| } | |
| SIM_STATUS = {"running": False, "progress": 0.0, "current_balance": 0.0, "trades_count": 0} | |
| TF_MS = {'5m': 300_000, '15m': 900_000, '1h': 3_600_000, '4h': 14_400_000, '1d': 86_400_000} | |
| REQUIRED_TFS = ['5m','15m','1h','4h','1d'] | |
| FETCH_LIMIT = 500 | |
| # الحد الأدنى للشموع المطلوبة لكل إطار | |
| MIN_CANDLES_BASE = {'5m':220,'15m':220,'1h':220,'4h':220,'1d':60} | |
| MIN_CANDLES_REQ = dict(MIN_CANDLES_BASE) | |
| MIN_CANDLES_REQ['1d'] = 59 # مرونة بسيطة لبدء المحاكاة | |
| async def run_realistic_simulation(): | |
| SIM_STATUS["running"] = True | |
| print( | |
| f"🚀 Sim {SIM_CONFIG['START_DATE']} -> {SIM_CONFIG['END_DATE']} | symbols={len(SIM_CONFIG['TEST_SYMBOLS'])}\n" | |
| f"STEP_TF={SIM_CONFIG['STEP_TF']} THR={SIM_CONFIG['ENTRY_THRESHOLD']} " | |
| f"TP/SL={SIM_CONFIG['TP_PCT']}/{SIM_CONFIG['SL_PCT']} " | |
| f"SOFT/HARD={SIM_CONFIG['SOFT_TIME_STOP_BARS']}/{SIM_CONFIG['HARD_TIME_STOP_BARS']} bars " | |
| f"| COOLDOWN={SIM_CONFIG['COOLDOWN_BARS']} | POS_FRAC={SIM_CONFIG['POSITION_FRACTION']}", | |
| flush=True | |
| ) | |
| r2_service = R2Service() | |
| data_dir = SIM_CONFIG["LOCAL_DATA_DIR"] | |
| if not os.path.exists(data_dir): | |
| print(f"❌ Missing data dir: {data_dir}", flush=True) | |
| SIM_STATUS["running"] = False | |
| return | |
| print("🛠️ Bootstrapping...", flush=True) | |
| mock_exchange = MockKuCoin(data_dir) | |
| await mock_exchange.load_data(SIM_CONFIG["TEST_SYMBOLS"], REQUIRED_TFS) | |
| # إنشاء المحفظة بمنطق الإيقاف الزمني المرن | |
| step_ms = TF_MS.get(SIM_CONFIG["STEP_TF"], TF_MS['5m']) | |
| virtual_wallet = VirtualExchange( | |
| initial_balance=SIM_CONFIG["INITIAL_BALANCE"], | |
| fee_rate=SIM_CONFIG["FEE_RATE"], | |
| tp_pct=SIM_CONFIG["TP_PCT"], | |
| sl_pct=SIM_CONFIG["SL_PCT"], | |
| bar_ms=step_ms, | |
| cooldown_bars=SIM_CONFIG["COOLDOWN_BARS"], | |
| min_trade_usd=SIM_CONFIG["MIN_TRADE_USD"], | |
| position_fraction=SIM_CONFIG["POSITION_FRACTION"], | |
| max_concurrent=SIM_CONFIG["MAX_CONCURRENT_POSITIONS"], | |
| soft_time_stop_bars=SIM_CONFIG["SOFT_TIME_STOP_BARS"], | |
| hard_time_stop_bars=SIM_CONFIG["HARD_TIME_STOP_BARS"], | |
| ) | |
| # تحديد مجال الزمن الآمن | |
| min_ts, max_ts = None, None | |
| for sym in SIM_CONFIG["TEST_SYMBOLS"]: | |
| for tf in REQUIRED_TFS: | |
| df = mock_exchange.data_store.get(sym, {}).get(tf) | |
| if df is None or df.empty: | |
| continue | |
| mn = int(df['timestamp'].iloc[0]); mx = int(df['timestamp'].iloc[-1]) | |
| min_ts = mn if min_ts is None else min(min_ts, mn) | |
| max_ts = mx if max_ts is None else max(max_ts, mx) | |
| if min_ts is None or max_ts is None: | |
| print("❌ لا توجد بيانات صالحة.", flush=True); SIM_STATUS["running"] = False; return | |
| boot_requirements_ms = [] | |
| for tf in REQUIRED_TFS: | |
| need = MIN_CANDLES_BASE[tf] | |
| guard = 1 if tf == '1d' else 0 | |
| eff_need = max(need - guard, 1) | |
| boot_requirements_ms.append(eff_need * TF_MS[tf]) | |
| boot_ms = max(boot_requirements_ms) | |
| cfg_start = int(pd.Timestamp(SIM_CONFIG["START_DATE"]).timestamp() * 1000) | |
| cfg_end = int(pd.Timestamp(SIM_CONFIG["END_DATE"]).timestamp() * 1000) | |
| safe_start_ts = max(cfg_start, min_ts + boot_ms) | |
| safe_end_ts = min(cfg_end, max_ts) | |
| if safe_end_ts - safe_start_ts < step_ms: | |
| print("❌ نافذة المحاكاة قصيرة.", flush=True); SIM_STATUS["running"] = False; return | |
| print( | |
| f"🗓️ Data span: [{pd.to_datetime(min_ts, unit='ms')} .. {pd.to_datetime(max_ts, unit='ms')}] | " | |
| f"Sim window: [{pd.to_datetime(safe_start_ts, unit='ms')} .. {pd.to_datetime(safe_end_ts, unit='ms')}]", | |
| flush=True | |
| ) | |
| print("🧠 Initializing hybrid system...", flush=True) | |
| data_manager = DataManager(None, None, r2_service=r2_service, mock_exchange=mock_exchange) | |
| learning_hub = LearningHubManager(r2_service, None, data_manager, disable_llm=True) # NO_LLM | |
| await learning_hub.initialize() | |
| processor = MLProcessor(None, data_manager, learning_hub) | |
| await processor.initialize() | |
| # تمرير عتبة الدخول للنظام الهجين | |
| setattr(data_manager, 'HYBRID_ENTRY_THRESHOLD', float(SIM_CONFIG["ENTRY_THRESHOLD"])) | |
| print(f"✅ Ready. Threshold={getattr(data_manager, 'HYBRID_ENTRY_THRESHOLD', 'NA')}", flush=True) | |
| # حلقة المحاكاة | |
| timeline = range(safe_start_ts, safe_end_ts, step_ms) | |
| total = len(timeline) | |
| bar = tqdm(timeline, desc="Simulating", unit="step") | |
| def log(msg): | |
| try: bar.write(msg) | |
| except Exception: print(msg, flush=True) | |
| for i, ts in enumerate(bar): | |
| # تحديث الوقت | |
| mock_exchange.set_time(ts) | |
| if i % 10 == 0: | |
| SIM_STATUS["progress"] = (i / total) * 100 | |
| SIM_STATUS["current_balance"] = virtual_wallet.get_balance() | |
| SIM_STATUS["trades_count"] = len(virtual_wallet.trade_history) | |
| # إغلاقات قائمة (TP/SL/Soft/Hard) | |
| prices = {s:(await mock_exchange.fetch_ticker(s))['last'] for s in SIM_CONFIG["TEST_SYMBOLS"]} | |
| closed = virtual_wallet.update_positions(prices, ts) | |
| for tr in closed: | |
| log(f"[CLOSE] {tr['symbol']} reason={tr['close_reason']} pnl={tr['pnl_percent']:.2f}%") | |
| await learning_hub.analyze_trade_and_learn(tr, tr['close_reason']) | |
| # فتح مركز جديد إن لم توجد صفقات مفتوحة | |
| if virtual_wallet.open_positions_count() < SIM_CONFIG["MAX_CONCURRENT_POSITIONS"]: | |
| for symbol in SIM_CONFIG["TEST_SYMBOLS"]: | |
| # تحقق توافر حزمة الفريمات | |
| enough = True | |
| for tf in REQUIRED_TFS: | |
| candles = await mock_exchange.fetch_ohlcv(symbol, tf, limit=FETCH_LIMIT) | |
| if not candles or len(candles) < MIN_CANDLES_REQ[tf]: | |
| enough = False; break | |
| if not enough: | |
| continue | |
| packet = {tf: await mock_exchange.fetch_ohlcv(symbol, tf, limit=FETCH_LIMIT) for tf in REQUIRED_TFS} | |
| cur_price = prices.get(symbol, 0.0) | |
| res = await processor.process_and_score_symbol_enhanced({ | |
| 'symbol': symbol, 'ohlcv': packet, 'current_price': cur_price | |
| }) | |
| if not res: | |
| continue | |
| final_s = float(res['enhanced_final_score']) | |
| thr = getattr(data_manager, 'HYBRID_ENTRY_THRESHOLD', 0.75) | |
| comps = dict(res.get('components', {})); comps['final_score'] = final_s | |
| log(f"[SCORE] {symbol} final={final_s:.3f} thr={thr:.3f} comps={comps}") | |
| if final_s >= thr and virtual_wallet.can_enter(symbol, ts): | |
| if virtual_wallet.execute_buy(symbol, cur_price, ts, comps): | |
| log(f"[BUY] {symbol} @ {cur_price} bal=${virtual_wallet.get_balance():.2f}") | |
| break # نحافظ على صفقة واحدة فقط | |
| # إغلاق ما تبقى عند نهاية النافذة | |
| print("\n🏁 نهاية فترة المحاكاة. إغلاق الصفقات المتبقية...", flush=True) | |
| final_ts = safe_end_ts | |
| mock_exchange.set_time(final_ts) | |
| end_prices = {s:(await mock_exchange.fetch_ticker(s))['last'] for s in SIM_CONFIG["TEST_SYMBOLS"]} | |
| for s in list(virtual_wallet.positions.keys()): | |
| tr = virtual_wallet.execute_sell(s, end_prices.get(s,0), final_ts, "END_OF_SIM") | |
| if tr: | |
| await learning_hub.analyze_trade_and_learn(tr, "END_OF_SIM") | |
| print("💾 جاري حفظ النتائج إلى R2...", flush=True) | |
| await r2_service.save_simulation_results(virtual_wallet.trade_history, virtual_wallet.metrics) | |
| await learning_hub.shutdown() | |
| SIM_STATUS.update({"running": False, "progress": 100.0}) | |
| print("\n🎉 === تقرير المحاكاة السريع ===", flush=True) | |
| print(f"💰 الرصيد النهائي: ${virtual_wallet.get_balance():.2f} (من ${SIM_CONFIG['INITIAL_BALANCE']})", flush=True) | |
| print(f"📈 إجمالي الربح/الخسارة: ${virtual_wallet.metrics['total_pnl_usd']:.2f}", flush=True) | |
| print(f"🔢 عدد الصفقات: {len(virtual_wallet.trade_history)} (فوز: {virtual_wallet.metrics['wins']} | خسارة: {virtual_wallet.metrics['losses']})", flush=True) | |
| print(f"📉 أقصى تراجع (Max Drawdown): {virtual_wallet.metrics['max_drawdown']*100:.2f}%", flush=True) | |
| print("===============================\n", flush=True) |