Files
crypto_trading/frontend/src/components/BacktestPanel.vue
T
matitos 1b44e456c6 Collapse the strategy, backtest and sweep panels
Adds a reusable CollapsiblePanel (chevron header, optional summary shown when
collapsed, state persisted in localStorage) and wraps the Strategy, Backtest
and Parameter sweep panels with it, so any single part of the dashboard can be
focused on quickly. Panels stay mounted (v-show) so their form/results state is
preserved across collapse. Includes component tests.
2026-09-21 16:40:48 +00:00

143 lines
5.5 KiB
Vue

<script setup>
import { computed, ref } from 'vue'
import * as api from '../api'
import { fmtMoney, fmtPrice, pnlClass, signed } from '../format'
import CollapsiblePanel from './CollapsiblePanel.vue'
import EquityChart from './EquityChart.vue'
import StrategyForm from './StrategyForm.vue'
defineProps({
strategies: { type: Array, required: true },
status: { type: Object, required: true },
})
const running = ref(false)
const error = ref(null)
const result = ref(null)
const limit = ref(500)
const summary = computed(() => {
if (!result.value || result.value.error) return ''
const m = result.value.metrics
return `${result.value.symbol} ${result.value.timeframe} · ${signed(m.total_return_pct)}% · ${m.num_trades} trades`
})
async function run(payload) {
running.value = true
error.value = null
try {
result.value = await api.runBacktest({ ...payload, limit: Number(limit.value) })
if (result.value.error) error.value = result.value.error
} catch (e) {
error.value = e.response?.data?.detail || e.message
} finally {
running.value = false
}
}
</script>
<template>
<CollapsiblePanel title="Backtest" storage-key="backtest" :summary="summary">
<div class="space-y-3">
<StrategyForm
:strategies="strategies"
:busy="running"
:symbol="status.symbol"
:timeframe="status.timeframe"
submit-label="Run backtest"
@submit="run"
/>
<label class="block max-w-[12rem] space-y-1">
<span class="label">Historical candles</span>
<input v-model="limit" type="number" min="20" max="1000" class="input" />
</label>
<p v-if="error" class="rounded-lg border border-rose-500/40 bg-rose-500/10 p-3 text-sm text-rose-200">
{{ error }}
</p>
<template v-if="result && !result.error">
<div class="grid grid-cols-2 gap-3 md:grid-cols-4">
<div class="rounded-lg border border-edge/60 p-3">
<div class="label">Total return</div>
<div class="text-lg font-semibold tabular-nums" :class="pnlClass(result.metrics.total_return_pct)">
{{ signed(result.metrics.total_return_pct) }}%
</div>
</div>
<div class="rounded-lg border border-edge/60 p-3">
<div class="label">Buy &amp; hold</div>
<div class="text-lg font-semibold tabular-nums" :class="pnlClass(result.metrics.buy_hold_return_pct)">
{{ signed(result.metrics.buy_hold_return_pct) }}%
</div>
</div>
<div class="rounded-lg border border-edge/60 p-3">
<div class="label">Max drawdown</div>
<div class="text-lg font-semibold tabular-nums text-amber-300">
-{{ fmtMoney(result.metrics.max_drawdown_pct) }}%
</div>
</div>
<div class="rounded-lg border border-edge/60 p-3">
<div class="label">Final equity</div>
<div class="text-lg font-semibold tabular-nums">{{ fmtMoney(result.metrics.final_equity) }}</div>
</div>
<div class="rounded-lg border border-edge/60 p-3">
<div class="label">Trades</div>
<div class="text-lg font-semibold tabular-nums">{{ result.metrics.num_trades }}</div>
</div>
<div class="rounded-lg border border-edge/60 p-3">
<div class="label">Win rate</div>
<div class="text-lg font-semibold tabular-nums">{{ fmtMoney(result.metrics.win_rate_pct) }}%</div>
</div>
<div class="rounded-lg border border-edge/60 p-3">
<div class="label">Profit factor</div>
<div class="text-lg font-semibold tabular-nums">
{{ result.metrics.profit_factor === null ? '—' : fmtMoney(result.metrics.profit_factor) }}
</div>
</div>
<div class="rounded-lg border border-edge/60 p-3">
<div class="label">Sharpe</div>
<div class="text-lg font-semibold tabular-nums">
{{ result.metrics.sharpe === null ? '—' : fmtMoney(result.metrics.sharpe) }}
</div>
</div>
</div>
<EquityChart :points="result.equity_curve" title="Backtest equity" />
<div class="overflow-x-auto">
<table class="w-full text-left text-sm">
<thead class="label">
<tr>
<th class="pb-2">Side</th>
<th class="pb-2 text-right">Entry</th>
<th class="pb-2 text-right">Exit</th>
<th class="pb-2 text-right">P&amp;L</th>
<th class="pb-2 text-right">Return</th>
<th class="pb-2">Reason</th>
</tr>
</thead>
<tbody>
<tr v-for="(t, i) in result.trades" :key="i" class="border-t border-edge/60">
<td class="py-1.5" :class="t.side === 'long' ? 'text-emerald-400' : 'text-rose-400'">
{{ t.side }}
</td>
<td class="py-1.5 text-right tabular-nums">{{ fmtPrice(t.entry_price) }}</td>
<td class="py-1.5 text-right tabular-nums">{{ fmtPrice(t.exit_price) }}</td>
<td class="py-1.5 text-right tabular-nums" :class="pnlClass(t.pnl)">{{ signed(t.pnl) }}</td>
<td class="py-1.5 text-right tabular-nums" :class="pnlClass(t.return_pct)">
{{ signed(t.return_pct) }}%
</td>
<td class="py-1.5 text-slate-400">{{ t.reason }}</td>
</tr>
<tr v-if="!result.trades.length">
<td colspan="6" class="py-6 text-center text-slate-500">No trades</td>
</tr>
</tbody>
</table>
</div>
</template>
</div>
</CollapsiblePanel>
</template>