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.
This commit is contained in:
shokollm
2026-04-12 04:28:40 +00:00
parent 01ec8bc539
commit dd61c32ea7
9 changed files with 259 additions and 3 deletions

View File

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

View File

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

View File

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

View File

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