With several strategies the header cards showed only the first one. Expose day_pnl and drawdown_pct per strategy and add aggregateStatus() (frontend) so the cards show the whole account: summed cash/equity/P&L/trades/positions, worst drawdown, and any halt. Single-strategy runs are unchanged. Adds tests.
88 lines
2.6 KiB
JavaScript
88 lines
2.6 KiB
JavaScript
import { describe, expect, it } from 'vitest'
|
|
import { aggregateStatus } from './aggregate'
|
|
|
|
const SINGLE = { equity: 100, total_pnl: 5, strategies: [] }
|
|
|
|
function strategy(overrides = {}) {
|
|
return {
|
|
strategy_key: 'sma_cross',
|
|
symbol: 'BTC/USDT',
|
|
timeframe: '5m',
|
|
allocation: 0.5,
|
|
cash: 1000,
|
|
equity: 1000,
|
|
realized_pnl: 0,
|
|
unrealized_pnl: 0,
|
|
total_pnl: 0,
|
|
open_positions: 0,
|
|
halted: false,
|
|
halt_reason: null,
|
|
trades_today: 0,
|
|
day_pnl: 0,
|
|
drawdown_pct: 0,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('aggregateStatus', () => {
|
|
it('returns the plain status when there is at most one strategy', () => {
|
|
expect(aggregateStatus(SINGLE, { strategies: [] })).toBe(SINGLE)
|
|
expect(aggregateStatus(SINGLE, { strategies: [strategy()] })).toBe(SINGLE)
|
|
})
|
|
|
|
it('sums the per-strategy numbers', () => {
|
|
const multi = {
|
|
running: true,
|
|
equity: 2100,
|
|
starting_balance: 2000,
|
|
total_pnl: 100,
|
|
price: 123,
|
|
strategies: [
|
|
strategy({ cash: 1000, equity: 1100, realized_pnl: 100, unrealized_pnl: 0, total_pnl: 100, trades_today: 2, open_positions: 1, day_pnl: 40 }),
|
|
strategy({ cash: 1000, equity: 1000, realized_pnl: 0, unrealized_pnl: 0, total_pnl: 0, trades_today: 0, open_positions: 0, day_pnl: -10 }),
|
|
],
|
|
}
|
|
const result = aggregateStatus(SINGLE, multi)
|
|
expect(result.cash).toBe(2000)
|
|
expect(result.equity).toBe(2100)
|
|
expect(result.realized_pnl).toBe(100)
|
|
expect(result.total_pnl).toBe(100)
|
|
expect(result.day_pnl).toBe(30)
|
|
expect(result.trades_today).toBe(2)
|
|
expect(result.open_positions).toBe(1)
|
|
expect(result.price).toBe(123)
|
|
expect(result.running).toBe(true)
|
|
})
|
|
|
|
it('reports the worst drawdown and any halt', () => {
|
|
const multi = {
|
|
running: true,
|
|
equity: 2000,
|
|
starting_balance: 2000,
|
|
total_pnl: 0,
|
|
price: null,
|
|
strategies: [
|
|
strategy({ drawdown_pct: 3, halted: false }),
|
|
strategy({ drawdown_pct: 7.5, halted: true, halt_reason: 'daily loss limit hit' }),
|
|
],
|
|
}
|
|
const result = aggregateStatus(SINGLE, multi)
|
|
expect(result.drawdown_pct).toBe(7.5)
|
|
expect(result.halted).toBe(true)
|
|
expect(result.halt_reason).toBe('daily loss limit hit')
|
|
})
|
|
|
|
it('falls back to the plain price when multi has none', () => {
|
|
const multi = {
|
|
running: false,
|
|
equity: 2000,
|
|
starting_balance: 2000,
|
|
total_pnl: 0,
|
|
price: null,
|
|
strategies: [strategy(), strategy()],
|
|
}
|
|
const result = aggregateStatus({ ...SINGLE, price: 99 }, multi)
|
|
expect(result.price).toBe(99)
|
|
})
|
|
})
|