Crypto trading bot: engine, brokers, backtester, dashboard, deploy
FastAPI backend with a single-strategy async engine, paper and Binance brokers, pure-python indicators, a backtester that shares the engine's portfolio math, and a Vue dashboard with a live WebSocket feed. Adds the Gitea Actions CI/CD workflow that deploys to Traefik via docker_default.
This commit is contained in:
1 parent
98028248c2
commit
9c3bee87ac
73 files changed
+9759
-1
No files matched your search
@@ -0,0 +1,140 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import * as api from '../api'
|
||||
import { fmtMoney, fmtPrice, pnlClass, signed } from '../format'
|
||||
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)
|
||||
|
||||
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>
|
||||
<div class="card space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="font-semibold">Backtest</h2>
|
||||
<span v-if="result" class="text-xs text-slate-400">
|
||||
{{ result.symbol }} {{ result.timeframe }} · {{ result.equity_curve.length }} candles
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<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 & 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&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>
|
||||
</template>
|
||||
Reference in new issue
Block a user