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

Trade Activity

+ + {tradeActions.length} trades + +
+ + {#if tradeActions.length === 0} +
+

No trades executed yet. Check the strategy configuration.

+
+ {:else} +
+ {#each tradeActions as entry} +
+
+ {getActionIcon(entry.action)} + + {entry.action.toUpperCase()} + + {formatTime(entry.time)} +
+
+
+ Price: + ${entry.price.toFixed(8)} +
+
+ Reason: + {entry.reason} +
+ {#if entry.action === 'sell' && entry.position > 0} +
+ Position: + {entry.position.toFixed(6)} +
+ {/if} +
+
+ {/each} +
+ {/if} +
+ + diff --git a/src/frontend/src/lib/components/index.ts b/src/frontend/src/lib/components/index.ts index ba33297..a137d66 100644 --- a/src/frontend/src/lib/components/index.ts +++ b/src/frontend/src/lib/components/index.ts @@ -3,6 +3,7 @@ export { default as BotCard } from './BotCard.svelte'; export { default as BotSelector } from './BotSelector.svelte'; export { default as StrategyPreview } from './StrategyPreview.svelte'; export { default as SignalChart } from './SignalChart.svelte'; +export { default as TradeDashboard } from './TradeDashboard.svelte'; export { default as BacktestChart } from './BacktestChart.svelte'; export { default as ProUpgradeBanner } from './ProUpgradeBanner.svelte'; export { default as TokenPicker } from './TokenPicker.svelte'; diff --git a/src/frontend/src/lib/stores/simulationStore.ts b/src/frontend/src/lib/stores/simulationStore.ts index 9a6ec86..83bd896 100644 --- a/src/frontend/src/lib/stores/simulationStore.ts +++ b/src/frontend/src/lib/stores/simulationStore.ts @@ -6,10 +6,20 @@ export interface KlineData { close: number; } +export interface TradeLogEntry { + time: number; + price: number; + action: 'buy' | 'sell' | 'hold'; + reason: string; + position: number; + entry_price: number | null; +} + export interface SimulationState { currentSimulation: Simulation | null; signals: Signal[]; klines: KlineData[]; + tradeLog: TradeLogEntry[]; isLoading: boolean; error: string | null; } @@ -18,6 +28,7 @@ const initialState: SimulationState = { currentSimulation: null, signals: [], klines: [], + tradeLog: [], isLoading: false, error: null }; @@ -28,7 +39,8 @@ export function setCurrentSimulation(simulation: Simulation | null) { simulationStore.update(state => ({ ...state, currentSimulation: simulation, - klines: simulation?.klines || [] + klines: simulation?.klines || [], + tradeLog: simulation?.trade_log || [] })); } diff --git a/src/frontend/src/routes/bot/[id]/simulate/+page.svelte b/src/frontend/src/routes/bot/[id]/simulate/+page.svelte index a36e13a..4f1fe86 100644 --- a/src/frontend/src/routes/bot/[id]/simulate/+page.svelte +++ b/src/frontend/src/routes/bot/[id]/simulate/+page.svelte @@ -4,7 +4,7 @@ import { goto } from '$app/navigation'; import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, simulationStore, setCurrentSimulation, addSignals, clearSignals, setSimulationLoading, setSimulationError } from '$lib/stores'; import { api } from '$lib/api'; - import { SignalChart } from '$lib/components'; + import { SignalChart, TradeDashboard } from '$lib/components'; let botId = $derived($page.params.id); let tokenName = $state(''); @@ -159,6 +159,10 @@ +

Trade Activity

+ + +

Signals ({$simulationStore.signals.length})

{#if $simulationStore.signals.length === 0}