diff --git a/src/backend/app/api/simulate.py b/src/backend/app/api/simulate.py index 1ccbc4c..e2c867a 100644 --- a/src/backend/app/api/simulate.py +++ b/src/backend/app/api/simulate.py @@ -35,20 +35,18 @@ def run_simulation_sync( engine.run_id = simulation_id running_simulations[simulation_id] = engine - try: - # Run simulation (now synchronous - processes klines quickly) - results = await engine.run() - - # Serialize signals for JSON storage (convert datetime to string) - def serialize_signal(s): - created = s.get("created_at") - if hasattr(created, "isoformat"): - created = created.isoformat() - return { - **s, - "created_at": created - } - + # Serialize signals for JSON storage (convert datetime to string) + def serialize_signal(s): + created = s.get("created_at") + if hasattr(created, "isoformat"): + created = created.isoformat() + return { + **s, + "created_at": created + } + + def save_progress(): + """Save current progress to database.""" db = SessionLocal() try: simulation = ( @@ -57,34 +55,38 @@ def run_simulation_sync( if simulation: simulation.status = engine.status simulation.signals = [serialize_signal(s) for s in engine.signals] - # Save klines for chart display (only time and close price) simulation.klines = [ {"time": k.get("time"), "close": k.get("close")} for k in engine.klines ] - # Save trade log for dashboard simulation.trade_log = engine.trade_log db.commit() + finally: + db.close() + + async def run_with_progress_save(): + """Run simulation and save progress periodically.""" + last_save_time = time.time() + save_interval = 5 # Save every 5 seconds + + while engine.running and engine.status == "running": + await asyncio.sleep(1) # Check every second + + current_time = time.time() + if current_time - last_save_time >= save_interval: + save_progress() + last_save_time = current_time + + # Final save when done + save_progress() + + try: + # Run both simulation and progress saving concurrently + await asyncio.gather( + engine.run(), + run_with_progress_save() + ) - for signal in engine.signals: - created_at = signal.get("created_at") - if hasattr(created_at, "isoformat"): - created_at = created_at.isoformat() - - db_signal = Signal( - id=signal["id"], - bot_id=signal["bot_id"], - run_id=signal["run_id"], - signal_type=signal["signal_type"], - token=signal["token"], - price=signal["price"], - confidence=signal.get("confidence"), - reasoning=signal.get("reasoning"), - executed=signal.get("executed", False), - created_at=created_at, - ) - db.add(db_signal) - db.commit() finally: db.close() finally: diff --git a/src/backend/app/db/schemas.py b/src/backend/app/db/schemas.py index 17c41cf..0631066 100644 --- a/src/backend/app/db/schemas.py +++ b/src/backend/app/db/schemas.py @@ -119,6 +119,9 @@ class SimulationResponse(BaseModel): signals: Optional[List[dict]] klines: Optional[List[dict]] = None # Price data for chart trade_log: Optional[List[dict]] = None # Trade activity log + current_candle_index: Optional[int] = None # Progress: current candle + total_candles: Optional[int] = None # Progress: total candles + candles_processed: Optional[int] = None # Progress: candles processed class Config: from_attributes = True diff --git a/src/backend/app/services/simulate/engine.py b/src/backend/app/services/simulate/engine.py index ed93c18..bdf6711 100644 --- a/src/backend/app/services/simulate/engine.py +++ b/src/backend/app/services/simulate/engine.py @@ -30,7 +30,15 @@ class SimulateEngine: # Kline-based settings self.kline_interval = config.get("kline_interval", "1m") - self.max_candles = config.get("max_candles", 500) # Limit candles to process + self.max_candles = config.get("max_candles", 100) # Limit candles to simulate real-time + + # Delay between candles (in seconds) to simulate real-time + # e.g., 1m interval -> 30s delay between candles + # Use config value if provided, otherwise calculate + if "candle_delay" in config and config["candle_delay"] is not None: + self.candle_delay = config["candle_delay"] + else: + self.candle_delay = self._get_interval_seconds(self.kline_interval) / 2 self.auto_execute = config.get("auto_execute", False) self.token = config.get("token", "") @@ -61,7 +69,24 @@ class SimulateEngine: # Trade log - tracks what happened at each candle self.trade_log: List[Dict[str, Any]] = [] + + # Current candle being processed (for frontend to show progress) + self.current_candle_index = 0 + self.total_candles = 0 + def _get_interval_seconds(self, interval: str) -> int: + """Convert kline interval to seconds.""" + mapping = { + "1m": 60, + "5m": 300, + "15m": 900, + "30m": 1800, + "1h": 3600, + "4h": 14400, + "1d": 86400, + } + return mapping.get(interval, 60) + async def run(self) -> Dict[str, Any]: self.running = True self.status = "running" @@ -91,13 +116,17 @@ class SimulateEngine: # Step 2: Process candles (with limit) candles_processed = 0 - for candle in self.klines: + self.total_candles = min(len(self.klines), self.max_candles) + self.current_candle_index = 0 + + for i, candle in enumerate(self.klines): if not self.running: break if candles_processed >= self.max_candles: logger.info(f"Reached max candles limit ({self.max_candles})") break + self.current_candle_index = candles_processed candle_time = int(candle.get("time", 0)) # Get OHLCV data from candle @@ -116,6 +145,10 @@ class SimulateEngine: self.last_processed_time = candle_time candles_processed += 1 + + # Delay to simulate real-time (only for visible candles, not initial batch) + if candles_processed > 1 and self.candle_delay > 0: + await asyncio.sleep(self.candle_delay) self.status = "completed" @@ -131,7 +164,9 @@ class SimulateEngine: self.results["total_errors"] = len(self.errors) self.results["errors"] = self.errors self.results["signals"] = self.signals - self.results["candles_processed"] = candles_processed if self.running else 0 + self.results["candles_processed"] = candles_processed + self.results["current_candle_index"] = self.current_candle_index + self.results["total_candles"] = self.total_candles self.results["klines"] = self.klines # Include klines for chart display self.results["trade_log"] = self.trade_log # Include trade log for dashboard self.results["started_at"] = self.started_at diff --git a/src/backend/tests/test_simulate_engine.py b/src/backend/tests/test_simulate_engine.py index e31bd19..5b3f09a 100644 --- a/src/backend/tests/test_simulate_engine.py +++ b/src/backend/tests/test_simulate_engine.py @@ -26,7 +26,8 @@ def create_engine(config_override=None, klines_data=None): "token": "0x1234567890123456789012345678901234567890", "chain": "bsc", "kline_interval": "1m", - "max_candles": 100, + "max_candles": 10, # Small number for fast tests + "candle_delay": 0, # No delay in tests "auto_execute": False, "strategy_config": { "conditions": [ diff --git a/src/frontend/src/lib/api/types.ts b/src/frontend/src/lib/api/types.ts index 52862fb..aaa70bb 100644 --- a/src/frontend/src/lib/api/types.ts +++ b/src/frontend/src/lib/api/types.ts @@ -118,6 +118,9 @@ export interface Simulation { signals: Signal[] | null; klines?: { time: number; close: number }[]; trade_log?: TradeLogEntry[]; + current_candle_index?: number; + total_candles?: number; + candles_processed?: number; } export interface SimulationConfig {