feat: add trades history modal to backtest page

This commit is contained in:
shokollm
2026-04-11 05:18:23 +00:00
parent 194c4f8a62
commit 5c9e46e693
4 changed files with 186 additions and 1 deletions

View File

@@ -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),

View File

@@ -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):

View File

@@ -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();
}
},

View File

@@ -16,6 +16,11 @@
let isRunning = $state(false);
let selectedBacktest = $state<Backtest | null>(null);
// Trades modal state
let showTradesModal = $state(false);
let selectedTrades = $state<any[]>([]);
let loadingTrades = $state(false);
onMount(async () => {
// Set default dates - yesterday only (1 day range for fast testing)
const yesterday = new Date();
@@ -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 @@
<span class="result-label">Max Drawdown</span>
<span class="result-value negative">{backtest.result.max_drawdown.toFixed(2)}%</span>
</div>
<button onclick={() => viewTrades(backtest)} class="btn btn-secondary btn-sm">View Trades</button>
</div>
{/if}
{#if backtest.status === 'running'}
@@ -249,6 +269,51 @@
<BacktestChart results={selectedBacktest.result} />
</section>
{/if}
{#if showTradesModal}
<div class="modal-overlay" onclick={() => showTradesModal = false}>
<div class="modal-content trades-modal" onclick={(e) => e.stopPropagation()}>
<div class="modal-header">
<h3>Trade History</h3>
<button class="close-btn" onclick={() => showTradesModal = false}>×</button>
</div>
{#if loadingTrades}
<p class="loading">Loading trades...</p>
{:else if selectedTrades.length === 0}
<p class="empty-state">No trades recorded.</p>
{:else}
<div class="trades-table-wrapper">
<table class="trades-table">
<thead>
<tr>
<th>Type</th>
<th>Price</th>
<th>Amount</th>
<th>Exit Reason</th>
<th>Time</th>
</tr>
</thead>
<tbody>
{#each selectedTrades as trade}
<tr>
<td>
<span class="trade-type" class:buy={trade.type === 'buy'} class:sell={trade.type === 'sell'}>
{trade.type.toUpperCase()}
</span>
</td>
<td>${trade.price?.toFixed(6)}</td>
<td>${trade.amount?.toFixed(2)}</td>
<td>{trade.exit_reason || '-'}</td>
<td>{new Date(trade.timestamp * 1000).toLocaleString()}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</div>
</div>
{/if}
</div>
</main>
@@ -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;