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,20 +35,18 @@ def run_simulation_sync(
|
|||||||
engine.run_id = simulation_id
|
engine.run_id = simulation_id
|
||||||
running_simulations[simulation_id] = engine
|
running_simulations[simulation_id] = engine
|
||||||
|
|
||||||
try:
|
# Serialize signals for JSON storage (convert datetime to string)
|
||||||
# Run simulation (now synchronous - processes klines quickly)
|
def serialize_signal(s):
|
||||||
results = await engine.run()
|
created = s.get("created_at")
|
||||||
|
if hasattr(created, "isoformat"):
|
||||||
# Serialize signals for JSON storage (convert datetime to string)
|
created = created.isoformat()
|
||||||
def serialize_signal(s):
|
return {
|
||||||
created = s.get("created_at")
|
**s,
|
||||||
if hasattr(created, "isoformat"):
|
"created_at": created
|
||||||
created = created.isoformat()
|
}
|
||||||
return {
|
|
||||||
**s,
|
def save_progress():
|
||||||
"created_at": created
|
"""Save current progress to database."""
|
||||||
}
|
|
||||||
|
|
||||||
db = SessionLocal()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
simulation = (
|
simulation = (
|
||||||
@@ -57,34 +55,38 @@ def run_simulation_sync(
|
|||||||
if simulation:
|
if simulation:
|
||||||
simulation.status = engine.status
|
simulation.status = engine.status
|
||||||
simulation.signals = [serialize_signal(s) for s in engine.signals]
|
simulation.signals = [serialize_signal(s) for s in engine.signals]
|
||||||
# Save klines for chart display (only time and close price)
|
|
||||||
simulation.klines = [
|
simulation.klines = [
|
||||||
{"time": k.get("time"), "close": k.get("close")}
|
{"time": k.get("time"), "close": k.get("close")}
|
||||||
for k in engine.klines
|
for k in engine.klines
|
||||||
]
|
]
|
||||||
# Save trade log for dashboard
|
|
||||||
simulation.trade_log = engine.trade_log
|
simulation.trade_log = engine.trade_log
|
||||||
db.commit()
|
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:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ class SimulationResponse(BaseModel):
|
|||||||
signals: Optional[List[dict]]
|
signals: Optional[List[dict]]
|
||||||
klines: Optional[List[dict]] = None # Price data for chart
|
klines: Optional[List[dict]] = None # Price data for chart
|
||||||
trade_log: Optional[List[dict]] = None # Trade activity log
|
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:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|||||||
@@ -30,7 +30,15 @@ class SimulateEngine:
|
|||||||
|
|
||||||
# Kline-based settings
|
# Kline-based settings
|
||||||
self.kline_interval = config.get("kline_interval", "1m")
|
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.auto_execute = config.get("auto_execute", False)
|
||||||
self.token = config.get("token", "")
|
self.token = config.get("token", "")
|
||||||
@@ -61,7 +69,24 @@ class SimulateEngine:
|
|||||||
|
|
||||||
# Trade log - tracks what happened at each candle
|
# Trade log - tracks what happened at each candle
|
||||||
self.trade_log: List[Dict[str, Any]] = []
|
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]:
|
async def run(self) -> Dict[str, Any]:
|
||||||
self.running = True
|
self.running = True
|
||||||
self.status = "running"
|
self.status = "running"
|
||||||
@@ -91,13 +116,17 @@ class SimulateEngine:
|
|||||||
|
|
||||||
# Step 2: Process candles (with limit)
|
# Step 2: Process candles (with limit)
|
||||||
candles_processed = 0
|
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:
|
if not self.running:
|
||||||
break
|
break
|
||||||
if candles_processed >= self.max_candles:
|
if candles_processed >= self.max_candles:
|
||||||
logger.info(f"Reached max candles limit ({self.max_candles})")
|
logger.info(f"Reached max candles limit ({self.max_candles})")
|
||||||
break
|
break
|
||||||
|
|
||||||
|
self.current_candle_index = candles_processed
|
||||||
candle_time = int(candle.get("time", 0))
|
candle_time = int(candle.get("time", 0))
|
||||||
|
|
||||||
# Get OHLCV data from candle
|
# Get OHLCV data from candle
|
||||||
@@ -116,6 +145,10 @@ class SimulateEngine:
|
|||||||
self.last_processed_time = candle_time
|
self.last_processed_time = candle_time
|
||||||
|
|
||||||
candles_processed += 1
|
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"
|
self.status = "completed"
|
||||||
|
|
||||||
@@ -131,7 +164,9 @@ class SimulateEngine:
|
|||||||
self.results["total_errors"] = len(self.errors)
|
self.results["total_errors"] = len(self.errors)
|
||||||
self.results["errors"] = self.errors
|
self.results["errors"] = self.errors
|
||||||
self.results["signals"] = self.signals
|
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["klines"] = self.klines # Include klines for chart display
|
||||||
self.results["trade_log"] = self.trade_log # Include trade log for dashboard
|
self.results["trade_log"] = self.trade_log # Include trade log for dashboard
|
||||||
self.results["started_at"] = self.started_at
|
self.results["started_at"] = self.started_at
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ def create_engine(config_override=None, klines_data=None):
|
|||||||
"token": "0x1234567890123456789012345678901234567890",
|
"token": "0x1234567890123456789012345678901234567890",
|
||||||
"chain": "bsc",
|
"chain": "bsc",
|
||||||
"kline_interval": "1m",
|
"kline_interval": "1m",
|
||||||
"max_candles": 100,
|
"max_candles": 10, # Small number for fast tests
|
||||||
|
"candle_delay": 0, # No delay in tests
|
||||||
"auto_execute": False,
|
"auto_execute": False,
|
||||||
"strategy_config": {
|
"strategy_config": {
|
||||||
"conditions": [
|
"conditions": [
|
||||||
|
|||||||
@@ -118,6 +118,9 @@ export interface Simulation {
|
|||||||
signals: Signal[] | null;
|
signals: Signal[] | null;
|
||||||
klines?: { time: number; close: number }[];
|
klines?: { time: number; close: number }[];
|
||||||
trade_log?: TradeLogEntry[];
|
trade_log?: TradeLogEntry[];
|
||||||
|
current_candle_index?: number;
|
||||||
|
total_candles?: number;
|
||||||
|
candles_processed?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SimulationConfig {
|
export interface SimulationConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user