Multi-strategy UI

Adds a "Strategies (multi)" panel: add/remove strategy rows (strategy, symbol,
timeframe, per-strategy params and weight), start all, stop all and kill all,
plus a per-strategy status table (allocation, equity, P&L, halted). Polls
/api/bot/status alongside the existing status. Includes component tests.
This commit is contained in:
matitos committed 2026-09-22 11:59:21 +00:00
1 parent 3229a0e2aa
commit ce2b11a803
6 files changed
+499 -1

No files matched your search

+120
View File
@@ -0,0 +1,120 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it } from 'vitest'
import MultiStrategyPanel from './components/MultiStrategyPanel.vue'
const STRATEGIES = [
{
key: 'sma_cross',
name: 'SMA Crossover',
description: '',
params: [{ name: 'fast', type: 'int', default: 10, description: '' }],
},
{
key: 'ema_cross',
name: 'EMA Crossover',
description: '',
params: [{ name: 'fast', type: 'int', default: 9, description: '' }],
},
]
const EMPTY_STATUS = { running: false, strategies: [], total_pnl: 0, equity: 0 }
function mountPanel(multiStatus = EMPTY_STATUS) {
return mount(MultiStrategyPanel, {
props: { strategies: STRATEGIES, multiStatus, busy: false, loading: false },
})
}
describe('MultiStrategyPanel', () => {
it('seeds one row by default', () => {
const wrapper = mountPanel()
expect(wrapper.findAll('select')).toHaveLength(1)
})
it('adds and removes rows', async () => {
const wrapper = mountPanel()
const addBtn = wrapper.findAll('button').find((b) => b.text().includes('Add strategy'))
await addBtn.trigger('click')
expect(wrapper.findAll('select')).toHaveLength(2)
const removeBtn = wrapper.findAll('button').find((b) => b.text() === 'Remove')
await removeBtn.trigger('click')
expect(wrapper.findAll('select')).toHaveLength(1)
})
it('emits start-many with normalised weights and params', async () => {
const wrapper = mountPanel()
const addBtn = wrapper.findAll('button').find((b) => b.text().includes('Add strategy'))
await addBtn.trigger('click')
const startBtn = wrapper.findAll('button').find((b) => b.text().includes('Start all'))
await startBtn.trigger('click')
const payload = wrapper.emitted('start-many')[0][0]
expect(payload.strategies).toHaveLength(2)
expect(payload.strategies[0].weight).toBe(1)
expect(payload.strategies[0].params).toEqual({ fast: 10 })
expect(payload.starting_balance).toBe(10000)
})
it('shows the per-strategy status table', () => {
const wrapper = mountPanel({
running: true,
total_pnl: 42,
equity: 10042,
strategies: [
{
strategy_key: 'sma_cross',
strategy_name: 'SMA Crossover',
params: {},
symbol: 'BTC/USDT',
timeframe: '5m',
allocation: 0.4,
cash: 4000,
equity: 4200,
realized_pnl: 200,
unrealized_pnl: 0,
total_pnl: 200,
open_positions: 1,
halted: false,
halt_reason: null,
trades_today: 2,
error: null,
},
],
})
const text = wrapper.text()
expect(text).toContain('SMA Crossover')
expect(text).toContain('40.0%')
expect(text).toContain('running')
})
it('marks a halted strategy', () => {
const wrapper = mountPanel({
running: true,
total_pnl: -10,
equity: 9990,
strategies: [
{
strategy_key: 'sma_cross',
strategy_name: 'SMA Crossover',
params: {},
symbol: 'BTC/USDT',
timeframe: '5m',
allocation: 1,
cash: 0,
equity: 9990,
realized_pnl: -10,
unrealized_pnl: 0,
total_pnl: -10,
open_positions: 0,
halted: true,
halt_reason: 'daily loss limit hit',
trades_today: 1,
error: null,
},
],
})
expect(wrapper.text()).toContain('halted')
})
})