feat: add pagination for trade history in backtest
Backend:
- Added pagination to /trades endpoint with page and per_page params
- Returns paginated trades with metadata (page, total_pages, has_next, has_prev)
Frontend:
- Added pagination controls for trade history (Prev/Next buttons)
- Shows current page info and total trades
- Trades are loaded on-demand when expanded
API changes:
- GET /bots/{id}/backtest/{runId}/trades?page=1&per_page=5
- Response includes: trades, total_trades, page, per_page, total_pages, has_next, has_prev
This commit is contained in:
@@ -169,8 +169,16 @@ export const api = {
|
||||
}
|
||||
},
|
||||
|
||||
async getTrades(botId: string, runId: string): Promise<{ trades: any[]; total_trades: number }> {
|
||||
const response = await fetch(`${API_URL}/bots/${botId}/backtest/${runId}/trades`, {
|
||||
async getTrades(botId: string, runId: string, page: number = 1, perPage: number = 5): Promise<{
|
||||
trades: any[];
|
||||
total_trades: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
total_pages: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
}> {
|
||||
const response = await fetch(`${API_URL}/bots/${botId}/backtest/${runId}/trades?page=${page}&per_page=${perPage}`, {
|
||||
headers: getAuthHeaders()
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -89,6 +89,26 @@ export interface BacktestResult {
|
||||
sharpe_ratio: number;
|
||||
}
|
||||
|
||||
export interface PaginatedTrades {
|
||||
trades: Trade[];
|
||||
total_trades: number;
|
||||
page: number;
|
||||
per_page: number;
|
||||
total_pages: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
}
|
||||
|
||||
export interface Trade {
|
||||
type: 'buy' | 'sell';
|
||||
token: string;
|
||||
price: number;
|
||||
amount: number;
|
||||
quantity: number;
|
||||
timestamp: number;
|
||||
exit_reason?: 'stop_loss' | 'take_profit' | string;
|
||||
}
|
||||
|
||||
export interface Simulation {
|
||||
id: string;
|
||||
bot_id: string;
|
||||
|
||||
@@ -19,6 +19,11 @@
|
||||
// Expandable trades state
|
||||
let expandedTrades = $state<Set<string>>(new Set());
|
||||
|
||||
// Pagination state for each backtest
|
||||
let tradesPage = $state<Record<string, number>>({});
|
||||
let tradesData = $state<Record<string, any>>({});
|
||||
const TRADES_PER_PAGE = 5;
|
||||
|
||||
onMount(async () => {
|
||||
// Set default dates - yesterday only (1 day range for fast testing)
|
||||
const yesterday = new Date();
|
||||
@@ -132,9 +137,37 @@
|
||||
expandedTrades.delete(backtestId);
|
||||
} else {
|
||||
expandedTrades.add(backtestId);
|
||||
// Load first page of trades if not loaded
|
||||
if (!tradesData[backtestId]) {
|
||||
loadTrades(backtestId, 1);
|
||||
}
|
||||
}
|
||||
expandedTrades = new Set(expandedTrades); // Trigger reactivity
|
||||
}
|
||||
|
||||
async function loadTrades(backtestId: string, page: number) {
|
||||
try {
|
||||
const data = await api.backtest.getTrades(botId, backtestId, page, TRADES_PER_PAGE);
|
||||
tradesData[backtestId] = { ...data, currentPage: page };
|
||||
tradesData = { ...tradesData }; // Trigger reactivity
|
||||
} catch (e) {
|
||||
console.error('Failed to load trades:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function nextTradesPage(backtestId: string) {
|
||||
const data = tradesData[backtestId];
|
||||
if (data && data.has_next) {
|
||||
loadTrades(backtestId, data.page + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function prevTradesPage(backtestId: string) {
|
||||
const data = tradesData[backtestId];
|
||||
if (data && data.has_prev) {
|
||||
loadTrades(backtestId, data.page - 1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -244,18 +277,34 @@
|
||||
</button>
|
||||
{#if expandedTrades.has(backtest.id)}
|
||||
<div class="trades-inline">
|
||||
<div class="trades-list">
|
||||
{#each backtest.result.trades as trade}
|
||||
<div class="trade-item">
|
||||
<span class="trade-type" class:buy={trade.type === 'buy'} class:sell={trade.type === 'sell'}>
|
||||
{trade.type.toUpperCase()}
|
||||
</span>
|
||||
<span class="trade-price">${trade.price?.toFixed(6)}</span>
|
||||
<span class="trade-amount">${trade.amount?.toFixed(2)}</span>
|
||||
<span class="trade-reason">{trade.exit_reason || 'entry'}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if tradesData[backtest.id]}
|
||||
<div class="trades-pagination-header">
|
||||
<span class="trades-count">
|
||||
Showing {((tradesData[backtest.id].page - 1) * TRADES_PER_PAGE) + 1}-{Math.min(tradesData[backtest.id].page * TRADES_PER_PAGE, tradesData[backtest.id].total_trades)} of {tradesData[backtest.id].total_trades}
|
||||
</span>
|
||||
{#if tradesData[backtest.id].total_pages > 1}
|
||||
<div class="pagination-controls">
|
||||
<button class="btn-pagination" onclick={() => prevTradesPage(backtest.id)} disabled={!tradesData[backtest.id].has_prev}>← Prev</button>
|
||||
<span class="page-indicator">Page {tradesData[backtest.id].page} of {tradesData[backtest.id].total_pages}</span>
|
||||
<button class="btn-pagination" onclick={() => nextTradesPage(backtest.id)} disabled={!tradesData[backtest.id].has_next}>Next →</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="trades-list">
|
||||
{#each tradesData[backtest.id].trades as trade}
|
||||
<div class="trade-item">
|
||||
<span class="trade-type" class:buy={trade.type === 'buy'} class:sell={trade.type === 'sell'}>
|
||||
{trade.type.toUpperCase()}
|
||||
</span>
|
||||
<span class="trade-price">${trade.price?.toFixed(6)}</span>
|
||||
<span class="trade-amount">${trade.amount?.toFixed(2)}</span>
|
||||
<span class="trade-reason">{trade.exit_reason || 'entry'}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="trades-loading">Loading trades...</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -784,4 +833,61 @@
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* Pagination styles */
|
||||
.trades-pagination-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.trades-count {
|
||||
font-size: 0.85rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.pagination-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-pagination {
|
||||
width: auto;
|
||||
padding: 0.35rem 0.75rem;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border: 1px solid rgba(102, 126, 234, 0.3);
|
||||
border-radius: 6px;
|
||||
color: #667eea;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-pagination:hover:not(:disabled) {
|
||||
background: rgba(102, 126, 234, 0.2);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.btn-pagination:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.page-indicator {
|
||||
font-size: 0.8rem;
|
||||
color: #888;
|
||||
min-width: 80px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.trades-loading {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
padding: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user