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:
1 parent
3229a0e2aa
commit
ce2b11a803
6 files changed
+499
-1
No files matched your search
@@ -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')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user