From cd4583ca90424d8cc5571f7acebfb22721afa57e Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:52:45 +0000 Subject: [PATCH] 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 --- src/backend/app/api/backtest.py | 33 ++++- src/frontend/src/lib/api/client.ts | 12 +- src/frontend/src/lib/api/types.ts | 20 +++ .../src/routes/bot/[id]/backtest/+page.svelte | 130 ++++++++++++++++-- 4 files changed, 177 insertions(+), 18 deletions(-) diff --git a/src/backend/app/api/backtest.py b/src/backend/app/api/backtest.py index d716926..a0debd1 100644 --- a/src/backend/app/api/backtest.py +++ b/src/backend/app/api/backtest.py @@ -181,10 +181,17 @@ def get_backtest( def get_backtest_trades( bot_id: str, run_id: str, + page: int = 1, + per_page: int = 5, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), ): - """Get trade history for a specific backtest.""" + """Get paginated trade history for a specific backtest. + + Args: + page: Page number (1-indexed) + per_page: Number of trades per page (default 5, max 20) + """ bot = db.query(Bot).filter(Bot.id == bot_id).first() if not bot: raise HTTPException( @@ -211,12 +218,30 @@ def get_backtest_trades( if isinstance(result, str): import json result = json.loads(result) - trades = result.get("trades", []) or [] + all_trades = result.get("trades", []) or [] + total_trades = len(all_trades) + + # Validate pagination params + per_page = min(max(per_page, 1), 20) # Clamp between 1 and 20 + page = max(page, 1) + + # Calculate pagination + total_pages = max(1, (total_trades + per_page - 1) // per_page) if total_trades > 0 else 1 + start_idx = (page - 1) * per_page + end_idx = start_idx + per_page + + # Get page of trades (return empty list if start_idx >= total_trades) + paginated_trades = all_trades[start_idx:end_idx] if start_idx < total_trades else [] return { "backtest_id": run_id, - "trades": trades, - "total_trades": len(trades), + "trades": paginated_trades, + "total_trades": total_trades, + "page": page, + "per_page": per_page, + "total_pages": total_pages, + "has_next": page < total_pages, + "has_prev": page > 1, } @router.get("/bots/{bot_id}/backtests", response_model=List[BacktestResponse]) diff --git a/src/frontend/src/lib/api/client.ts b/src/frontend/src/lib/api/client.ts index 70b28b0..3e891e5 100644 --- a/src/frontend/src/lib/api/client.ts +++ b/src/frontend/src/lib/api/client.ts @@ -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) { diff --git a/src/frontend/src/lib/api/types.ts b/src/frontend/src/lib/api/types.ts index b1a2885..4e29e46 100644 --- a/src/frontend/src/lib/api/types.ts +++ b/src/frontend/src/lib/api/types.ts @@ -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; diff --git a/src/frontend/src/routes/bot/[id]/backtest/+page.svelte b/src/frontend/src/routes/bot/[id]/backtest/+page.svelte index 0493a6c..5a99185 100644 --- a/src/frontend/src/routes/bot/[id]/backtest/+page.svelte +++ b/src/frontend/src/routes/bot/[id]/backtest/+page.svelte @@ -19,6 +19,11 @@ // Expandable trades state let expandedTrades = $state>(new Set()); + // Pagination state for each backtest + let tradesPage = $state>({}); + let tradesData = $state>({}); + 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); + } + } @@ -244,18 +277,34 @@ {#if expandedTrades.has(backtest.id)}
-
- {#each backtest.result.trades as trade} -
- - {trade.type.toUpperCase()} - - ${trade.price?.toFixed(6)} - ${trade.amount?.toFixed(2)} - {trade.exit_reason || 'entry'} -
- {/each} -
+ {#if tradesData[backtest.id]} +
+ + 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} + + {#if tradesData[backtest.id].total_pages > 1} +
+ + Page {tradesData[backtest.id].page} of {tradesData[backtest.id].total_pages} + +
+ {/if} +
+
+ {#each tradesData[backtest.id].trades as trade} +
+ + {trade.type.toUpperCase()} + + ${trade.price?.toFixed(6)} + ${trade.amount?.toFixed(2)} + {trade.exit_reason || 'entry'} +
+ {/each} +
+ {:else} +
Loading trades...
+ {/if}
{/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; + }