The single "Strategy" panel and "Strategies (multi)" were redundant: the multi panel with one row is exactly the single-strategy behaviour. Remove StrategyPanel and make "Strategies" the one trading panel. Presets move into it (load adds a row, per-row Save), and the header now reports "N strategies" when several run instead of only the first. Deletes the obsolete component and its test; adds preset tests to the multi-strategy suite.
169 lines
5.0 KiB
JavaScript
169 lines
5.0 KiB
JavaScript
import { mount } from '@vue/test-utils'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import MultiStrategyPanel from './components/MultiStrategyPanel.vue'
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
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 }
|
|
|
|
const PRESETS = [
|
|
{
|
|
id: 1,
|
|
name: 'BTC fast',
|
|
strategy_key: 'ema_cross',
|
|
symbol: 'ETH/USDT',
|
|
timeframe: '15m',
|
|
params: { fast: 3 },
|
|
},
|
|
]
|
|
|
|
function mountPanel(multiStatus = EMPTY_STATUS, presets = []) {
|
|
return mount(MultiStrategyPanel, {
|
|
props: { strategies: STRATEGIES, multiStatus, presets, 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('loads a preset as an additional row', async () => {
|
|
const wrapper = mountPanel(EMPTY_STATUS, PRESETS)
|
|
const presetSelect = wrapper.findAll('select').find((s) => s.text().includes('BTC fast'))
|
|
expect(presetSelect).toBeTruthy()
|
|
|
|
await presetSelect.setValue('1')
|
|
|
|
// A second row now exists, seeded from the preset.
|
|
expect(wrapper.findAll('select').length).toBeGreaterThan(1)
|
|
const inputs = wrapper.findAll('input')
|
|
expect(inputs.some((i) => i.element.value === 'ETH/USDT')).toBe(true)
|
|
expect(inputs.some((i) => i.element.value === '15m')).toBe(true)
|
|
})
|
|
|
|
it('emits save-preset with the row config', async () => {
|
|
vi.spyOn(window, 'prompt').mockReturnValue('my preset')
|
|
const wrapper = mountPanel()
|
|
const save = wrapper.findAll('button').find((b) => b.text() === 'Save')
|
|
await save.trigger('click')
|
|
const payload = wrapper.emitted('save-preset')[0][0]
|
|
expect(payload.name).toBe('my preset')
|
|
expect(payload.strategy_key).toBe('sma_cross')
|
|
expect(payload.params).toEqual({ fast: 10 })
|
|
})
|
|
|
|
it('does not save when the prompt is cancelled', async () => {
|
|
vi.spyOn(window, 'prompt').mockReturnValue(null)
|
|
const wrapper = mountPanel()
|
|
const save = wrapper.findAll('button').find((b) => b.text() === 'Save')
|
|
await save.trigger('click')
|
|
expect(wrapper.emitted('save-preset')).toBeUndefined()
|
|
})
|
|
|
|
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')
|
|
})
|
|
})
|