fix: respect candle_delay from config, default to fast tests
- Tests now run with candle_delay=0 for fast execution - Simulation defaults to candle_delay based on interval (e.g., 30s for 1m) - Progress saved to DB every 5 seconds during simulation - User can now see real-time updates while simulation runs Tests: 14 passing in 0.15s
This commit is contained in:
@@ -35,10 +35,6 @@ 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")
|
||||
@@ -49,6 +45,8 @@ def run_simulation_sync(
|
||||
"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()
|
||||
|
||||
for signal in engine.signals:
|
||||
created_at = signal.get("created_at")
|
||||
if hasattr(created_at, "isoformat"):
|
||||
created_at = created_at.isoformat()
|
||||
async def run_with_progress_save():
|
||||
"""Run simulation and save progress periodically."""
|
||||
last_save_time = time.time()
|
||||
save_interval = 5 # Save every 5 seconds
|
||||
|
||||
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,
|
||||
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()
|
||||
)
|
||||
db.add(db_signal)
|
||||
db.commit()
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
finally:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", "")
|
||||
@@ -62,6 +70,23 @@ 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
|
||||
@@ -117,6 +146,10 @@ class SimulateEngine:
|
||||
|
||||
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"
|
||||
|
||||
except Exception as e:
|
||||
@@ -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
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user