Crypto trading bot: engine, brokers, backtester, dashboard, deploy
CI/CD / backend (push) Successful in 5m18s
CI/CD / frontend (push) Successful in 5m2s
CI/CD / build (push) Successful in 34s
CI/CD / cleanup (push) Successful in 6s
CI/CD / deploy (push) Successful in 22s

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:
matitos committed 2026-09-18 17:02:33 +00:00
1 parent 98028248c2
commit 9c3bee87ac
73 files changed
+9759 -1

No files matched your search

+164
View File
@@ -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>