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

View File

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

View File

@@ -0,0 +1,180 @@
<script lang="ts">
import type { TradeLogEntry } from '$lib/stores/simulationStore';
interface Props {
tradeLog: TradeLogEntry[];
}
let { tradeLog }: Props = $props();
function formatTime(timestamp: number): string {
const date = new Date(timestamp * 1000);
return date.toLocaleString();
}
function getActionColor(action: string): string {
switch (action) {
case 'buy': return '#22c55e';
case 'sell': return '#ef4444';
default: return '#666';
}
}
function getActionIcon(action: string): string {
switch (action) {
case 'buy': return '📈';
case 'sell': return '📉';
default: return '➡️';
}
}
// Filter to show only buy/sell actions
let tradeActions = $derived(tradeLog.filter(t => t.action !== 'hold'));
</script>
<div class="trade-dashboard">
<div class="dashboard-header">
<h3>Trade Activity</h3>
<span class="trade-count">
{tradeActions.length} trades
</span>
</div>
{#if tradeActions.length === 0}
<div class="empty-state">
<p>No trades executed yet. Check the strategy configuration.</p>
</div>
{:else}
<div class="trade-list">
{#each tradeActions as entry}
<div class="trade-entry action-{entry.action}">
<div class="trade-time">
<span class="action-icon">{getActionIcon(entry.action)}</span>
<span class="action-badge" style="background: {getActionColor(entry.action)}">
{entry.action.toUpperCase()}
</span>
<span class="time">{formatTime(entry.time)}</span>
</div>
<div class="trade-details">
<div class="price">
<span class="label">Price:</span>
<span class="value">${entry.price.toFixed(8)}</span>
</div>
<div class="reason">
<span class="label">Reason:</span>
<span class="value">{entry.reason}</span>
</div>
{#if entry.action === 'sell' && entry.position > 0}
<div class="pnl">
<span class="label">Position:</span>
<span class="value">{entry.position.toFixed(6)}</span>
</div>
{/if}
</div>
</div>
{/each}
</div>
{/if}
</div>
<style>
.trade-dashboard {
background: rgba(255, 255, 255, 0.02);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
overflow: hidden;
}
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background: rgba(255, 255, 255, 0.02);
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.dashboard-header h3 {
margin: 0;
font-size: 1rem;
color: #fff;
}
.trade-count {
font-size: 0.85rem;
color: #888;
}
.empty-state {
padding: 2rem;
text-align: center;
color: #666;
}
.trade-list {
max-height: 300px;
overflow-y: auto;
}
.trade-entry {
padding: 0.75rem 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
transition: background 0.2s;
}
.trade-entry:hover {
background: rgba(255, 255, 255, 0.02);
}
.trade-entry.action-buy {
border-left: 3px solid #22c55e;
}
.trade-entry.action-sell {
border-left: 3px solid #ef4444;
}
.trade-time {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.action-icon {
font-size: 1rem;
}
.action-badge {
padding: 0.125rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: bold;
color: #fff;
}
.time {
font-size: 0.85rem;
color: #888;
}
.trade-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.5rem;
font-size: 0.85rem;
}
.trade-details .label {
color: #666;
}
.trade-details .value {
color: #fff;
font-family: monospace;
}
.pnl .value {
color: #fbbf24;
}
</style>

View File

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

View File

@@ -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 || []
}));
}

View File

@@ -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 @@
<SignalChart signals={$simulationStore.signals} klines={$simulationStore.currentSimulation?.klines || []} height={250} />
<h2 style="margin-top: 1.5rem;">Trade Activity</h2>
<TradeDashboard tradeLog={$simulationStore.currentSimulation?.trade_log || []} />
<h2 style="margin-top: 1.5rem;">Signals ({$simulationStore.signals.length})</h2>
{#if $simulationStore.signals.length === 0}