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")}
|
{"time": k.get("time"), "close": k.get("close")}
|
||||||
for k in engine.klines
|
for k in engine.klines
|
||||||
]
|
]
|
||||||
|
# Save trade log for dashboard
|
||||||
|
simulation.trade_log = engine.trade_log
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
for signal in engine.signals:
|
for signal in engine.signals:
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ class Simulation(Base):
|
|||||||
config = Column(JSON, nullable=False)
|
config = Column(JSON, nullable=False)
|
||||||
signals = Column(JSON)
|
signals = Column(JSON)
|
||||||
klines = Column(JSON) # Price data for chart display
|
klines = Column(JSON) # Price data for chart display
|
||||||
|
trade_log = Column(JSON) # Trade activity log
|
||||||
|
|
||||||
bot = relationship("Bot", back_populates="simulations")
|
bot = relationship("Bot", back_populates="simulations")
|
||||||
|
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ class SimulationResponse(BaseModel):
|
|||||||
config: dict
|
config: dict
|
||||||
signals: Optional[List[dict]]
|
signals: Optional[List[dict]]
|
||||||
klines: Optional[List[dict]] = None # Price data for chart
|
klines: Optional[List[dict]] = None # Price data for chart
|
||||||
|
trade_log: Optional[List[dict]] = None # Trade activity log
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
|
|||||||
@@ -59,6 +59,9 @@ class SimulateEngine:
|
|||||||
self.klines: List[Dict[str, Any]] = []
|
self.klines: List[Dict[str, Any]] = []
|
||||||
self.last_processed_time: Optional[int] = None
|
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]:
|
async def run(self) -> Dict[str, Any]:
|
||||||
self.running = True
|
self.running = True
|
||||||
self.status = "running"
|
self.status = "running"
|
||||||
@@ -130,6 +133,7 @@ class SimulateEngine:
|
|||||||
self.results["signals"] = self.signals
|
self.results["signals"] = self.signals
|
||||||
self.results["candles_processed"] = candles_processed if self.running else 0
|
self.results["candles_processed"] = candles_processed if self.running else 0
|
||||||
self.results["klines"] = self.klines # Include klines for chart display
|
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["started_at"] = self.started_at
|
||||||
self.results["ended_at"] = datetime.utcnow()
|
self.results["ended_at"] = datetime.utcnow()
|
||||||
|
|
||||||
@@ -165,20 +169,61 @@ class SimulateEngine:
|
|||||||
):
|
):
|
||||||
"""Process a single candle - check conditions and risk management."""
|
"""Process a single candle - check conditions and risk management."""
|
||||||
|
|
||||||
|
action = "hold" # Default action
|
||||||
|
reason = ""
|
||||||
|
|
||||||
# Check risk management first (for open positions)
|
# Check risk management first (for open positions)
|
||||||
if self.position > 0 and self.entry_price is not None:
|
if self.position > 0 and self.entry_price is not None:
|
||||||
exit_info = self._check_risk_management(close_price, timestamp)
|
exit_info = self._check_risk_management(close_price, timestamp)
|
||||||
if exit_info:
|
if exit_info:
|
||||||
await self._execute_risk_exit(close_price, timestamp, 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)
|
# Check conditions (only if no open position)
|
||||||
if self.position == 0:
|
if self.position == 0:
|
||||||
for condition in self.conditions:
|
for condition in self.conditions:
|
||||||
if self._check_condition(condition, close_price, volume):
|
if self._check_condition(condition, close_price, volume):
|
||||||
await self._execute_actions(close_price, timestamp, condition)
|
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
|
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(
|
def _check_risk_management(
|
||||||
self, current_price: float, timestamp: int
|
self, current_price: float, timestamp: int
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ export interface Simulation {
|
|||||||
config: SimulationConfig;
|
config: SimulationConfig;
|
||||||
signals: Signal[] | null;
|
signals: Signal[] | null;
|
||||||
klines?: { time: number; close: number }[];
|
klines?: { time: number; close: number }[];
|
||||||
|
trade_log?: TradeLogEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SimulationConfig {
|
export interface SimulationConfig {
|
||||||
@@ -125,6 +126,15 @@ export interface SimulationConfig {
|
|||||||
kline_interval?: string;
|
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 {
|
export interface Signal {
|
||||||
id: string;
|
id: string;
|
||||||
bot_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 BotSelector } from './BotSelector.svelte';
|
||||||
export { default as StrategyPreview } from './StrategyPreview.svelte';
|
export { default as StrategyPreview } from './StrategyPreview.svelte';
|
||||||
export { default as SignalChart } from './SignalChart.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 BacktestChart } from './BacktestChart.svelte';
|
||||||
export { default as ProUpgradeBanner } from './ProUpgradeBanner.svelte';
|
export { default as ProUpgradeBanner } from './ProUpgradeBanner.svelte';
|
||||||
export { default as TokenPicker } from './TokenPicker.svelte';
|
export { default as TokenPicker } from './TokenPicker.svelte';
|
||||||
|
|||||||
@@ -6,10 +6,20 @@ export interface KlineData {
|
|||||||
close: number;
|
close: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TradeLogEntry {
|
||||||
|
time: number;
|
||||||
|
price: number;
|
||||||
|
action: 'buy' | 'sell' | 'hold';
|
||||||
|
reason: string;
|
||||||
|
position: number;
|
||||||
|
entry_price: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SimulationState {
|
export interface SimulationState {
|
||||||
currentSimulation: Simulation | null;
|
currentSimulation: Simulation | null;
|
||||||
signals: Signal[];
|
signals: Signal[];
|
||||||
klines: KlineData[];
|
klines: KlineData[];
|
||||||
|
tradeLog: TradeLogEntry[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
@@ -18,6 +28,7 @@ const initialState: SimulationState = {
|
|||||||
currentSimulation: null,
|
currentSimulation: null,
|
||||||
signals: [],
|
signals: [],
|
||||||
klines: [],
|
klines: [],
|
||||||
|
tradeLog: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
error: null
|
error: null
|
||||||
};
|
};
|
||||||
@@ -28,7 +39,8 @@ export function setCurrentSimulation(simulation: Simulation | null) {
|
|||||||
simulationStore.update(state => ({
|
simulationStore.update(state => ({
|
||||||
...state,
|
...state,
|
||||||
currentSimulation: simulation,
|
currentSimulation: simulation,
|
||||||
klines: simulation?.klines || []
|
klines: simulation?.klines || [],
|
||||||
|
tradeLog: simulation?.trade_log || []
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, simulationStore, setCurrentSimulation, addSignals, clearSignals, setSimulationLoading, setSimulationError } from '$lib/stores';
|
import { isAuthenticated, isLoading, currentBotStore, setCurrentBot, simulationStore, setCurrentSimulation, addSignals, clearSignals, setSimulationLoading, setSimulationError } from '$lib/stores';
|
||||||
import { api } from '$lib/api';
|
import { api } from '$lib/api';
|
||||||
import { SignalChart } from '$lib/components';
|
import { SignalChart, TradeDashboard } from '$lib/components';
|
||||||
|
|
||||||
let botId = $derived($page.params.id);
|
let botId = $derived($page.params.id);
|
||||||
let tokenName = $state('');
|
let tokenName = $state('');
|
||||||
@@ -159,6 +159,10 @@
|
|||||||
|
|
||||||
<SignalChart signals={$simulationStore.signals} klines={$simulationStore.currentSimulation?.klines || []} height={250} />
|
<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>
|
<h2 style="margin-top: 1.5rem;">Signals ({$simulationStore.signals.length})</h2>
|
||||||
|
|
||||||
{#if $simulationStore.signals.length === 0}
|
{#if $simulationStore.signals.length === 0}
|
||||||
|
|||||||
Reference in New Issue
Block a user