From 5c9e46e6938da9f5904b9c613bd1ffdbd50ea873 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Sat, 11 Apr 2026 05:18:23 +0000 Subject: [PATCH] feat: add trades history modal to backtest page --- src/backend/app/api/backtest.py | 38 ++++- src/backend/app/services/backtest/engine.py | 1 + src/frontend/src/lib/api/client.ts | 10 ++ .../src/routes/bot/[id]/backtest/+page.svelte | 138 ++++++++++++++++++ 4 files changed, 186 insertions(+), 1 deletion(-) diff --git a/src/backend/app/api/backtest.py b/src/backend/app/api/backtest.py index 329294e..2dad43a 100644 --- a/src/backend/app/api/backtest.py +++ b/src/backend/app/api/backtest.py @@ -177,7 +177,43 @@ def get_backtest( return backtest -@router.get("/bots/{bot_id}/backtests", response_model=List[BacktestResponse]) +@router.get("/bots/{bot_id}/backtest/{run_id}/trades") +def get_backtest_trades( + bot_id: str, + run_id: str, + current_user: User = Depends(get_current_user), + db: Session = Depends(get_db), +): + """Get trade history for a specific backtest.""" + bot = db.query(Bot).filter(Bot.id == bot_id).first() + if not bot: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Bot not found" + ) + if bot.user_id != current_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, detail="Not authorized" + ) + + backtest = ( + db.query(Backtest) + .filter(Backtest.id == run_id, Backtest.bot_id == bot_id) + .first() + ) + if not backtest: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, detail="Backtest not found" + ) + + # Get trades from result + result = backtest.result or {} + trades = result.get("trades", []) + + return { + "backtest_id": run_id, + "trades": trades, + "total_trades": len(trades), + }@router.get("/bots/{bot_id}/backtests", response_model=List[BacktestResponse]) def list_backtests( bot_id: str, current_user: User = Depends(get_current_user), diff --git a/src/backend/app/services/backtest/engine.py b/src/backend/app/services/backtest/engine.py index 4e399cf..27d7ba8 100644 --- a/src/backend/app/services/backtest/engine.py +++ b/src/backend/app/services/backtest/engine.py @@ -393,6 +393,7 @@ class BacktestEngine: "sharpe_ratio": round(sharpe_ratio, 2), "final_balance": round(final_balance, 2), "signals": self.signals, + "trades": self.trades, # Include trades in results for storage } async def stop(self): diff --git a/src/frontend/src/lib/api/client.ts b/src/frontend/src/lib/api/client.ts index f515a0d..70b28b0 100644 --- a/src/frontend/src/lib/api/client.ts +++ b/src/frontend/src/lib/api/client.ts @@ -167,6 +167,16 @@ export const api = { if (!response.ok) { throw new Error(`HTTP error ${response.status}`); } + }, + + async getTrades(botId: string, runId: string): Promise<{ trades: any[]; total_trades: number }> { + const response = await fetch(`${API_URL}/bots/${botId}/backtest/${runId}/trades`, { + headers: getAuthHeaders() + }); + if (!response.ok) { + throw new Error(`HTTP error ${response.status}`); + } + return response.json(); } }, diff --git a/src/frontend/src/routes/bot/[id]/backtest/+page.svelte b/src/frontend/src/routes/bot/[id]/backtest/+page.svelte index 4124642..cd5beca 100644 --- a/src/frontend/src/routes/bot/[id]/backtest/+page.svelte +++ b/src/frontend/src/routes/bot/[id]/backtest/+page.svelte @@ -15,6 +15,11 @@ let endDate = $state(''); let isRunning = $state(false); let selectedBacktest = $state(null); + + // Trades modal state + let showTradesModal = $state(false); + let selectedTrades = $state([]); + let loadingTrades = $state(false); onMount(async () => { // Set default dates - yesterday only (1 day range for fast testing) @@ -112,6 +117,20 @@ } } + async function viewTrades(backtest: Backtest) { + showTradesModal = true; + loadingTrades = true; + try { + const response = await api.backtest.getTrades(botId, backtest.id); + selectedTrades = response.trades || []; + } catch (e) { + console.error('Failed to load trades:', e); + selectedTrades = []; + } finally { + loadingTrades = false; + } + } + function setBacktestHistory(backtests: any[]) { backtestStore.update(state => ({ ...state, backtestHistory: backtests })); } @@ -223,6 +242,7 @@ Max Drawdown {backtest.result.max_drawdown.toFixed(2)}% + {/if} {#if backtest.status === 'running'} @@ -249,6 +269,51 @@ {/if} + + {#if showTradesModal} + + {/if} @@ -325,6 +390,79 @@ cursor: not-allowed; } + .btn-sm { + padding: 0.4rem 0.75rem; + font-size: 0.85rem; + } + + /* Trades Modal */ + .trades-modal { + max-width: 800px; + max-height: 80vh; + overflow: hidden; + display: flex; + flex-direction: column; + } + + .trades-modal .modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; + } + + .trades-modal h3 { + margin: 0; + color: #667eea; + } + + .trades-table-wrapper { + overflow-y: auto; + flex: 1; + } + + .trades-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; + } + + .trades-table th, + .trades-table td { + padding: 0.75rem; + text-align: left; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + } + + .trades-table th { + background: rgba(255, 255, 255, 0.05); + font-weight: 600; + color: #ccc; + position: sticky; + top: 0; + } + + .trades-table td { + color: #fff; + } + + .trade-type { + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-weight: 600; + font-size: 0.8rem; + } + + .trade-type.buy { + background: rgba(76, 175, 80, 0.2); + color: #4caf50; + } + + .trade-type.sell { + background: rgba(244, 67, 54, 0.2); + color: #f44336; + } + .content { display: grid; gap: 2rem;