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:
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
180
src/frontend/src/lib/components/TradeDashboard.svelte
Normal file
180
src/frontend/src/lib/components/TradeDashboard.svelte
Normal 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>
|
||||
@@ -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';
|
||||
|
||||
@@ -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 || []
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user