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:
shokollm
2026-04-12 05:24:43 +00:00
parent 984656c83c
commit cadea23e40
5 changed files with 83 additions and 39 deletions

View File

@@ -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:

View File

@@ -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

View File

@@ -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