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,164 @@
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import * as api from './api'
|
||||
import BacktestPanel from './components/BacktestPanel.vue'
|
||||
import EquityChart from './components/EquityChart.vue'
|
||||
import EventLog from './components/EventLog.vue'
|
||||
import OrdersTable from './components/OrdersTable.vue'
|
||||
import PositionsTable from './components/PositionsTable.vue'
|
||||
import StatusCards from './components/StatusCards.vue'
|
||||
import StrategyPanel from './components/StrategyPanel.vue'
|
||||
|
||||
const status = ref({
|
||||
running: false,
|
||||
mode: 'paper',
|
||||
exchange: '',
|
||||
symbol: '',
|
||||
timeframe: '',
|
||||
strategy_name: null,
|
||||
price: null,
|
||||
cash: 0,
|
||||
equity: 0,
|
||||
starting_balance: 0,
|
||||
realized_pnl: 0,
|
||||
unrealized_pnl: 0,
|
||||
total_pnl: 0,
|
||||
open_positions: 0,
|
||||
error: null,
|
||||
})
|
||||
|
||||
const strategies = ref([])
|
||||
const positions = ref([])
|
||||
const orders = ref([])
|
||||
const events = ref([])
|
||||
const equityPoints = ref([])
|
||||
const busy = ref(false)
|
||||
const connected = ref(false)
|
||||
|
||||
let socket
|
||||
let pollTimer
|
||||
|
||||
async function refreshAll() {
|
||||
const [p, o, e, q] = await Promise.all([
|
||||
api.getPositions(),
|
||||
api.getOrders(),
|
||||
api.getEvents(),
|
||||
api.getEquity(),
|
||||
])
|
||||
positions.value = p
|
||||
orders.value = o
|
||||
events.value = e
|
||||
equityPoints.value = q
|
||||
}
|
||||
|
||||
async function loadStrategies() {
|
||||
strategies.value = await api.getStrategies()
|
||||
}
|
||||
|
||||
async function handleStart(payload) {
|
||||
busy.value = true
|
||||
try {
|
||||
status.value = await api.startBot(payload)
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
status.value = { ...status.value, error: error.response?.data?.detail || error.message }
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStop() {
|
||||
busy.value = true
|
||||
try {
|
||||
status.value = await api.stopBot()
|
||||
await refreshAll()
|
||||
} finally {
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleClose(id) {
|
||||
try {
|
||||
status.value = await api.closePosition(id)
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
status.value = { ...status.value, error: error.response?.data?.detail || error.message }
|
||||
}
|
||||
}
|
||||
|
||||
function handleMessage(message) {
|
||||
if (message.type === 'status') {
|
||||
status.value = message.data
|
||||
} else if (message.type === 'event') {
|
||||
events.value = [message.data, ...events.value].slice(0, 50)
|
||||
if (message.data.level === 'trade' || message.data.level === 'error') refreshAll()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadStrategies(), api.getStatus().then((s) => (status.value = s)), refreshAll()])
|
||||
socket = api.openSocket({
|
||||
onMessage: handleMessage,
|
||||
onOpen: () => (connected.value = true),
|
||||
onClose: () => (connected.value = false),
|
||||
})
|
||||
pollTimer = setInterval(refreshAll, 5000)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
socket?.close()
|
||||
clearInterval(pollTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto max-w-6xl space-y-4 p-4">
|
||||
<header class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<h1 class="text-xl font-bold">Crypto Trader</h1>
|
||||
<span
|
||||
class="rounded-full px-2 py-0.5 text-xs font-semibold uppercase"
|
||||
:class="status.mode === 'live' ? 'bg-rose-500/20 text-rose-300' : 'bg-amber-500/20 text-amber-300'"
|
||||
>
|
||||
{{ status.mode }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1.5 text-xs text-slate-400">
|
||||
<span
|
||||
class="h-2 w-2 rounded-full"
|
||||
:class="connected ? 'bg-emerald-400' : 'bg-slate-500'"
|
||||
></span>
|
||||
{{ connected ? 'live' : 'reconnecting' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-slate-400">
|
||||
<span v-if="status.strategy_name">Running: {{ status.strategy_name }}</span>
|
||||
<span v-else>Idle</span>
|
||||
<span v-if="status.symbol"> · {{ status.symbol }} {{ status.timeframe }}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div v-if="status.error" class="rounded-lg border border-rose-500/40 bg-rose-500/10 p-3 text-sm text-rose-200">
|
||||
{{ status.error }}
|
||||
</div>
|
||||
|
||||
<StatusCards :status="status" />
|
||||
|
||||
<StrategyPanel
|
||||
:strategies="strategies"
|
||||
:status="status"
|
||||
:busy="busy"
|
||||
@start="handleStart"
|
||||
@stop="handleStop"
|
||||
/>
|
||||
|
||||
<BacktestPanel :strategies="strategies" :status="status" />
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-2">
|
||||
<EquityChart :points="equityPoints" />
|
||||
<EventLog :events="events" />
|
||||
</div>
|
||||
|
||||
<PositionsTable :positions="positions" @close="handleClose" />
|
||||
<OrdersTable :orders="orders" />
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user