From dd61c32ea733b9eb333e09cfe2786b54e3040a41 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Sun, 12 Apr 2026 04:28:40 +0000 Subject: [PATCH] feat: add trade activity dashboard Shows what happened at each candle: - BUY/SELL/HOLD actions - Price at that time - Reason for action - Entry price for positions Trade log is stored in DB and displayed in frontend. --- src/backend/app/api/simulate.py | 2 + src/backend/app/db/models.py | 1 + src/backend/app/db/schemas.py | 1 + src/backend/app/services/simulate/engine.py | 47 ++++- src/frontend/src/lib/api/types.ts | 10 + .../src/lib/components/TradeDashboard.svelte | 180 ++++++++++++++++++ src/frontend/src/lib/components/index.ts | 1 + .../src/lib/stores/simulationStore.ts | 14 +- .../src/routes/bot/[id]/simulate/+page.svelte | 6 +- 9 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 src/frontend/src/lib/components/TradeDashboard.svelte diff --git a/src/backend/app/api/simulate.py b/src/backend/app/api/simulate.py index 0280add..db96b3d 100644 --- a/src/backend/app/api/simulate.py +++ b/src/backend/app/api/simulate.py @@ -52,6 +52,8 @@ def run_simulation_sync( {"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() for signal in engine.signals: diff --git a/src/backend/app/db/models.py b/src/backend/app/db/models.py index 118e242..2eccf85 100644 --- a/src/backend/app/db/models.py +++ b/src/backend/app/db/models.py @@ -94,6 +94,7 @@ class Simulation(Base): config = Column(JSON, nullable=False) signals = Column(JSON) klines = Column(JSON) # Price data for chart display + trade_log = Column(JSON) # Trade activity log bot = relationship("Bot", back_populates="simulations") diff --git a/src/backend/app/db/schemas.py b/src/backend/app/db/schemas.py index 636de95..17c41cf 100644 --- a/src/backend/app/db/schemas.py +++ b/src/backend/app/db/schemas.py @@ -118,6 +118,7 @@ class SimulationResponse(BaseModel): config: dict signals: Optional[List[dict]] klines: Optional[List[dict]] = None # Price data for chart + trade_log: Optional[List[dict]] = None # Trade activity log class Config: from_attributes = True diff --git a/src/backend/app/services/simulate/engine.py b/src/backend/app/services/simulate/engine.py index 514f1d2..ed93c18 100644 --- a/src/backend/app/services/simulate/engine.py +++ b/src/backend/app/services/simulate/engine.py @@ -58,6 +58,9 @@ class SimulateEngine: # Kline data self.klines: List[Dict[str, Any]] = [] self.last_processed_time: Optional[int] = None + + # Trade log - tracks what happened at each candle + self.trade_log: List[Dict[str, Any]] = [] async def run(self) -> Dict[str, Any]: self.running = True @@ -130,6 +133,7 @@ class SimulateEngine: 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["trade_log"] = self.trade_log # Include trade log for dashboard self.results["started_at"] = self.started_at self.results["ended_at"] = datetime.utcnow() @@ -165,19 +169,60 @@ class SimulateEngine: ): """Process a single candle - check conditions and risk management.""" + action = "hold" # Default action + reason = "" + # Check risk management first (for open positions) if self.position > 0 and self.entry_price is not None: exit_info = self._check_risk_management(close_price, timestamp) if exit_info: await self._execute_risk_exit(close_price, timestamp, exit_info) - return # Skip condition check if we just exited + action = "sell" + reason = exit_info["reason"] + # Log the action + self.trade_log.append({ + "time": timestamp, + "price": close_price, + "action": action, + "reason": reason, + "position": self.position, + "entry_price": self.entry_price, + }) + return # Check conditions (only if no open position) if self.position == 0: for condition in self.conditions: if self._check_condition(condition, close_price, volume): await self._execute_actions(close_price, timestamp, condition) + action = "buy" + reason = f"{condition.get('type')} {condition.get('threshold')}%".format( + type=condition.get('type'), + threshold=condition.get('threshold') + ) + # Log the action + self.trade_log.append({ + "time": timestamp, + "price": close_price, + "action": action, + "reason": reason, + "position": self.position, + "entry_price": self.entry_price, + }) break + + # Log hold action (no signal) + if action == "hold": + # Only log every 10th candle to reduce data + if len(self.trade_log) == 0 or (len(self.klines) - len(self.trade_log) > 10): + self.trade_log.append({ + "time": timestamp, + "price": close_price, + "action": "hold", + "reason": "no_signal", + "position": self.position, + "entry_price": self.entry_price, + }) def _check_risk_management( self, current_price: float, timestamp: int diff --git a/src/frontend/src/lib/api/types.ts b/src/frontend/src/lib/api/types.ts index 0182e1b..52862fb 100644 --- a/src/frontend/src/lib/api/types.ts +++ b/src/frontend/src/lib/api/types.ts @@ -117,6 +117,7 @@ export interface Simulation { config: SimulationConfig; signals: Signal[] | null; klines?: { time: number; close: number }[]; + trade_log?: TradeLogEntry[]; } export interface SimulationConfig { @@ -125,6 +126,15 @@ export interface SimulationConfig { kline_interval?: string; } +export interface TradeLogEntry { + time: number; + price: number; + action: 'buy' | 'sell' | 'hold'; + reason: string; + position: number; + entry_price: number | null; +} + export interface Signal { id: string; bot_id: string; diff --git a/src/frontend/src/lib/components/TradeDashboard.svelte b/src/frontend/src/lib/components/TradeDashboard.svelte new file mode 100644 index 0000000..c0deec7 --- /dev/null +++ b/src/frontend/src/lib/components/TradeDashboard.svelte @@ -0,0 +1,180 @@ + + +
No trades executed yet. Check the strategy configuration.
+