Risk controls, alerts, API token, more exchanges, presets
- RiskManager halts + flattens on max daily loss / drawdown, caps position size and trades per day; adds a manual kill switch (POST /api/bot/kill). - Telegram and webhook notifications on trades, errors and halts. - Optional API bearer token on /api, injected server-side by the frontend nginx so the browser never sees it. - Kraken and Coinbase market data (public); Coinbase live broker. Live on an unsupported exchange now errors instead of silently falling back to paper. - Strategy presets backed by the StrategyConfig table (CRUD API + UI). - Idempotent additive-column migration so existing SQLite databases upgrade in place (fixes the new strategy_configs.name column).
This commit is contained in:
1 parent
9c3bee87ac
commit
03dfa369e5
35 files changed
+1409
-45
No files matched your search
@@ -8,6 +8,7 @@ defineProps({
|
||||
const levelClass = {
|
||||
info: 'text-slate-300',
|
||||
trade: 'text-sky-300',
|
||||
warning: 'text-amber-300',
|
||||
error: 'text-rose-300',
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -19,11 +19,18 @@ const cards = computed(() => [
|
||||
},
|
||||
{ label: 'Cash', value: fmtMoney(props.status.cash), cls: 'text-slate-100' },
|
||||
{ label: 'Price', value: fmtPrice(props.status.price), cls: 'text-slate-100' },
|
||||
{ label: 'Day P&L', value: signed(props.status.day_pnl), cls: pnlClass(props.status.day_pnl) },
|
||||
{ label: 'Drawdown', value: `${fmtMoney(props.status.drawdown_pct)}%`, cls: 'text-amber-300' },
|
||||
{
|
||||
label: 'Trades today',
|
||||
value: String(props.status.trades_today ?? 0),
|
||||
cls: 'text-slate-100',
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-6">
|
||||
<div class="grid grid-cols-2 gap-3 md:grid-cols-3 lg:grid-cols-5">
|
||||
<div v-for="card in cards" :key="card.label" class="card">
|
||||
<div class="label">{{ card.label }}</div>
|
||||
<div class="value" :class="card.cls">{{ card.value }}</div>
|
||||
|
||||
@@ -9,13 +9,45 @@ const props = defineProps({
|
||||
submitClass: { type: String, default: 'btn-primary' },
|
||||
symbol: { type: String, default: 'BTC/USDT' },
|
||||
timeframe: { type: String, default: '1m' },
|
||||
presets: { type: Array, default: () => [] },
|
||||
})
|
||||
const emit = defineEmits(['submit'])
|
||||
const emit = defineEmits(['submit', 'save-preset', 'delete-preset', 'load-preset'])
|
||||
|
||||
const selectedKey = ref('')
|
||||
const values = ref({})
|
||||
const symbolValue = ref(props.symbol)
|
||||
const timeframeValue = ref(props.timeframe)
|
||||
const presetName = ref('')
|
||||
|
||||
function loadPreset(event) {
|
||||
const preset = props.presets.find((p) => p.id === Number(event.target.value))
|
||||
if (!preset) return
|
||||
selectedKey.value = preset.strategy_key
|
||||
symbolValue.value = preset.symbol
|
||||
timeframeValue.value = preset.timeframe
|
||||
values.value = { ...preset.params }
|
||||
emit('load-preset', preset)
|
||||
event.target.value = ''
|
||||
}
|
||||
|
||||
function savePreset() {
|
||||
if (!selected.value) return
|
||||
emit('save-preset', { name: presetName.value, ...buildPayload() })
|
||||
presetName.value = ''
|
||||
}
|
||||
|
||||
function buildPayload() {
|
||||
const params = {}
|
||||
for (const param of selected.value.params) {
|
||||
params[param.name] = coerce(param, values.value[param.name])
|
||||
}
|
||||
return {
|
||||
strategy_key: selectedKey.value,
|
||||
params,
|
||||
symbol: symbolValue.value,
|
||||
timeframe: timeframeValue.value,
|
||||
}
|
||||
}
|
||||
|
||||
const selected = computed(() => props.strategies.find((s) => s.key === selectedKey.value))
|
||||
|
||||
@@ -58,16 +90,7 @@ function coerce(param, raw) {
|
||||
}
|
||||
|
||||
function submit() {
|
||||
const params = {}
|
||||
for (const param of selected.value.params) {
|
||||
params[param.name] = coerce(param, values.value[param.name])
|
||||
}
|
||||
emit('submit', {
|
||||
strategy_key: selectedKey.value,
|
||||
params,
|
||||
symbol: symbolValue.value,
|
||||
timeframe: timeframeValue.value,
|
||||
})
|
||||
emit('submit', buildPayload())
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -115,6 +138,34 @@ function submit() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-end gap-2 border-t border-edge/60 pt-3">
|
||||
<label class="space-y-1">
|
||||
<span class="label">Load preset</span>
|
||||
<select class="input" :disabled="disabled" @change="loadPreset">
|
||||
<option value="">—</option>
|
||||
<option v-for="p in presets" :key="p.id" :value="p.id">
|
||||
{{ p.name || `${p.strategy_key} #${p.id}` }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="space-y-1">
|
||||
<span class="label">Save as</span>
|
||||
<input v-model="presetName" class="input" placeholder="preset name" :disabled="disabled" />
|
||||
</label>
|
||||
<button class="btn-muted" :disabled="busy || disabled || !selectedKey" @click="savePreset">
|
||||
Save preset
|
||||
</button>
|
||||
<button
|
||||
v-if="presets.length"
|
||||
class="btn-muted"
|
||||
:disabled="busy || disabled"
|
||||
@click="emit('delete-preset', presets[0].id)"
|
||||
title="Delete the most recent preset"
|
||||
>
|
||||
Delete latest
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button :class="submitClass" :disabled="busy || disabled || !selectedKey" @click="submit">
|
||||
{{ submitLabel }}
|
||||
</button>
|
||||
|
||||
@@ -5,8 +5,15 @@ const props = defineProps({
|
||||
strategies: { type: Array, required: true },
|
||||
status: { type: Object, required: true },
|
||||
busy: { type: Boolean, default: false },
|
||||
presets: { type: Array, default: () => [] },
|
||||
})
|
||||
const emit = defineEmits(['start', 'stop'])
|
||||
const emit = defineEmits(['start', 'stop', 'kill', 'save-preset', 'delete-preset'])
|
||||
|
||||
function confirmKill() {
|
||||
if (window.confirm('Kill switch: flatten the position and halt trading?')) {
|
||||
emit('kill')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -28,11 +35,23 @@ const emit = defineEmits(['start', 'stop'])
|
||||
submit-label="Start bot"
|
||||
:symbol="status.symbol"
|
||||
:timeframe="status.timeframe"
|
||||
:presets="presets"
|
||||
@submit="emit('start', $event)"
|
||||
@save-preset="emit('save-preset', $event)"
|
||||
@delete-preset="emit('delete-preset', $event)"
|
||||
/>
|
||||
|
||||
<button class="btn-danger" :disabled="busy || !status.running" @click="emit('stop')">
|
||||
Stop bot
|
||||
</button>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button class="btn-danger" :disabled="busy || !status.running" @click="emit('stop')">
|
||||
Stop bot
|
||||
</button>
|
||||
<button
|
||||
class="btn bg-rose-700 font-bold uppercase text-white hover:bg-rose-600"
|
||||
:disabled="busy || (!status.running && status.open_positions === 0)"
|
||||
@click="confirmKill"
|
||||
>
|
||||
Kill switch
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in new issue
Block a user