feat: add price chart to simulation and unit tests

Unit tests (13 passing):
- Kline fetching and processing
- Price drop condition triggers buy
- Stop loss and take profit risk management
- Multiple positions (buy again after sell)
- Max candles limit
- Stop interruption handling

Frontend:
- SignalChart now shows price movement even before signals
- Shows candle count even with no signals
- Chart displays buy/sell markers when signals exist
- Canvas-based chart with gradient fill

Backend:
- Simulation stores klines for chart display
- Returns klines in API response
- Simplified simulation run (no periodic saving)
This commit is contained in:
shokollm
2026-04-12 02:42:52 +00:00
parent ce8a29c0a4
commit 6a20cc174f
11 changed files with 503 additions and 218 deletions

View File

@@ -31,42 +31,9 @@ def run_simulation_sync(
engine.run_id = simulation_id
running_simulations[simulation_id] = engine
def save_signals_to_db():
"""Save current signals to database."""
db = SessionLocal()
try:
simulation = (
db.query(Simulation).filter(Simulation.id == simulation_id).first()
)
if simulation:
simulation.signals = engine.signals
db.commit()
finally:
db.close()
try:
# Run engine in background with periodic signal saving
check_interval = config.get("check_interval", 60)
save_interval = min(check_interval, 30) # Save at least every 30 seconds
async def run_with_periodic_save():
last_save_time = time.time()
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_signals_to_db()
last_save_time = current_time
# Final save when done
save_signals_to_db()
# Run both the engine and periodic save concurrently
await asyncio.gather(
engine.run(),
run_with_periodic_save()
)
# Run simulation (now synchronous - processes klines quickly)
results = await engine.run()
db = SessionLocal()
try:
@@ -76,6 +43,11 @@ def run_simulation_sync(
if simulation:
simulation.status = engine.status
simulation.signals = 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
]
db.commit()
for signal in engine.signals:
@@ -163,6 +135,7 @@ async def start_simulation(
"kline_interval": config.kline_interval,
},
signals=[],
klines=[],
)
db.add(simulation)
db.commit()
@@ -237,6 +210,9 @@ def list_simulations(
if sim.id in running_simulations:
engine = running_simulations[sim.id]
sim.signals = engine.get_signals()
# Include klines from running engine for chart display
if hasattr(engine, 'klines'):
sim.klines = [{"time": k.get("time"), "close": k.get("close")} for k in engine.klines]
return simulations

View File

@@ -93,6 +93,7 @@ class Simulation(Base):
status = Column(String, nullable=False)
config = Column(JSON, nullable=False)
signals = Column(JSON)
klines = Column(JSON) # Price data for chart display
bot = relationship("Bot", back_populates="simulations")

View File

@@ -117,6 +117,7 @@ class SimulationResponse(BaseModel):
status: str
config: dict
signals: Optional[List[dict]]
klines: Optional[List[dict]] = None # Price data for chart
class Config:
from_attributes = True

View File

@@ -129,6 +129,7 @@ class SimulateEngine:
self.results["errors"] = self.errors
self.results["signals"] = self.signals
self.results["candles_processed"] = candles_processed if self.running else 0
self.results["klines"] = self.klines # Include klines for chart display
self.results["started_at"] = self.started_at
self.results["ended_at"] = datetime.utcnow()