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:
@@ -181,10 +181,17 @@ def get_backtest(
|
|||||||
def get_backtest_trades(
|
def get_backtest_trades(
|
||||||
bot_id: str,
|
bot_id: str,
|
||||||
run_id: str,
|
run_id: str,
|
||||||
|
page: int = 1,
|
||||||
|
per_page: int = 5,
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: Session = Depends(get_db),
|
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()
|
bot = db.query(Bot).filter(Bot.id == bot_id).first()
|
||||||
if not bot:
|
if not bot:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -211,12 +218,30 @@ def get_backtest_trades(
|
|||||||
if isinstance(result, str):
|
if isinstance(result, str):
|
||||||
import json
|
import json
|
||||||
result = json.loads(result)
|
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 {
|
return {
|
||||||
"backtest_id": run_id,
|
"backtest_id": run_id,
|
||||||
"trades": trades,
|
"trades": paginated_trades,
|
||||||
"total_trades": len(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])
|
@router.get("/bots/{bot_id}/backtests", response_model=List[BacktestResponse])
|
||||||
|
|||||||
@@ -169,8 +169,16 @@ export const api = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async getTrades(botId: string, runId: string): Promise<{ trades: any[]; total_trades: number }> {
|
async getTrades(botId: string, runId: string, page: number = 1, perPage: number = 5): Promise<{
|
||||||
const response = await fetch(`${API_URL}/bots/${botId}/backtest/${runId}/trades`, {
|
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()
|
headers: getAuthHeaders()
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
@@ -89,6 +89,26 @@ export interface BacktestResult {
|
|||||||
sharpe_ratio: number;
|
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 {
|
export interface Simulation {
|
||||||
id: string;
|
id: string;
|
||||||
bot_id: string;
|
bot_id: string;
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
// Expandable trades state
|
// Expandable trades state
|
||||||
let expandedTrades = $state<Set<string>>(new Set());
|
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 () => {
|
onMount(async () => {
|
||||||
// Set default dates - yesterday only (1 day range for fast testing)
|
// Set default dates - yesterday only (1 day range for fast testing)
|
||||||
const yesterday = new Date();
|
const yesterday = new Date();
|
||||||
@@ -132,9 +137,37 @@
|
|||||||
expandedTrades.delete(backtestId);
|
expandedTrades.delete(backtestId);
|
||||||
} else {
|
} else {
|
||||||
expandedTrades.add(backtestId);
|
expandedTrades.add(backtestId);
|
||||||
|
// Load first page of trades if not loaded
|
||||||
|
if (!tradesData[backtestId]) {
|
||||||
|
loadTrades(backtestId, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
expandedTrades = new Set(expandedTrades); // Trigger reactivity
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -244,18 +277,34 @@
|
|||||||
</button>
|
</button>
|
||||||
{#if expandedTrades.has(backtest.id)}
|
{#if expandedTrades.has(backtest.id)}
|
||||||
<div class="trades-inline">
|
<div class="trades-inline">
|
||||||
<div class="trades-list">
|
{#if tradesData[backtest.id]}
|
||||||
{#each backtest.result.trades as trade}
|
<div class="trades-pagination-header">
|
||||||
<div class="trade-item">
|
<span class="trades-count">
|
||||||
<span class="trade-type" class:buy={trade.type === 'buy'} class:sell={trade.type === 'sell'}>
|
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}
|
||||||
{trade.type.toUpperCase()}
|
</span>
|
||||||
</span>
|
{#if tradesData[backtest.id].total_pages > 1}
|
||||||
<span class="trade-price">${trade.price?.toFixed(6)}</span>
|
<div class="pagination-controls">
|
||||||
<span class="trade-amount">${trade.amount?.toFixed(2)}</span>
|
<button class="btn-pagination" onclick={() => prevTradesPage(backtest.id)} disabled={!tradesData[backtest.id].has_prev}>← Prev</button>
|
||||||
<span class="trade-reason">{trade.exit_reason || 'entry'}</span>
|
<span class="page-indicator">Page {tradesData[backtest.id].page} of {tradesData[backtest.id].total_pages}</span>
|
||||||
</div>
|
<button class="btn-pagination" onclick={() => nextTradesPage(backtest.id)} disabled={!tradesData[backtest.id].has_next}>Next →</button>
|
||||||
{/each}
|
</div>
|
||||||
</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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -784,4 +833,61 @@
|
|||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
color: #fff;
|
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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user