Aggregate the top status cards across strategies
CI/CD / backend (push) Successful in 9m59s
CI/CD / frontend (push) Successful in 9m32s
CI/CD / build (push) Successful in 20s
CI/CD / cleanup (push) Successful in 5s
CI/CD / deploy (push) Successful in 1m8s

This commit is contained in:
matitos committed 2026-09-22 23:11:19 +00:00
commit ef895b6b9c
6 files changed
+133 -3

No files matched your search

+2
View File
@@ -158,6 +158,8 @@ class MultiStrategyManager:
halted=engine_status.halted, halted=engine_status.halted,
halt_reason=engine_status.halt_reason, halt_reason=engine_status.halt_reason,
trades_today=engine_status.trades_today, trades_today=engine_status.trades_today,
day_pnl=engine_status.day_pnl,
drawdown_pct=engine_status.drawdown_pct,
error=engine_status.error, error=engine_status.error,
) )
) )
+2
View File
@@ -177,6 +177,8 @@ class StrategyStatusOut(BaseModel):
halted: bool = False halted: bool = False
halt_reason: str | None = None halt_reason: str | None = None
trades_today: int = 0 trades_today: int = 0
day_pnl: float = 0.0
drawdown_pct: float = 0.0
error: str | None = None error: str | None = None
+2
View File
@@ -134,6 +134,8 @@ def test_start_many_and_multi_status(runtime: Runtime, market: FakeMarketData) -
assert status["running"] is True assert status["running"] is True
assert len(status["strategies"]) == 2 assert len(status["strategies"]) == 2
assert status["equity"] == 10000 assert status["equity"] == 10000
# Per-strategy fields the dashboard aggregates must be present.
assert {"day_pnl", "drawdown_pct"} <= set(status["strategies"][0])
client.post("/api/bot/stop") client.post("/api/bot/stop")
+6 -3
View File
@@ -1,5 +1,6 @@
<script setup> <script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { aggregateStatus } from './aggregate'
import * as api from './api' import * as api from './api'
import BacktestPanel from './components/BacktestPanel.vue' import BacktestPanel from './components/BacktestPanel.vue'
import EquityChart from './components/EquityChart.vue' import EquityChart from './components/EquityChart.vue'
@@ -44,6 +45,8 @@ const runningLabel = computed(() => {
return `Running: ${list.length} strategies` return `Running: ${list.length} strategies`
}) })
const displayStatus = computed(() => aggregateStatus(status.value, multiStatus.value))
const scopeLabel = computed(() => { const scopeLabel = computed(() => {
const list = multiStatus.value.strategies || [] const list = multiStatus.value.strategies || []
if (!list.length) { if (!list.length) {
@@ -298,10 +301,10 @@ onUnmounted(() => {
</header> </header>
<div <div
v-if="status.halted" v-if="displayStatus.halted"
class="rounded-lg border border-rose-500 bg-rose-500/20 p-3 text-sm font-semibold text-rose-100" class="rounded-lg border border-rose-500 bg-rose-500/20 p-3 text-sm font-semibold text-rose-100"
> >
HALTED — {{ status.halt_reason }}. Trading is stopped and any open position was HALTED — {{ displayStatus.halt_reason }}. Trading is stopped and any open position was
flattened. Restart the bot to resume (limits reset). flattened. Restart the bot to resume (limits reset).
</div> </div>
@@ -319,7 +322,7 @@ onUnmounted(() => {
{{ status.error }} {{ status.error }}
</div> </div>
<StatusCards :status="status" /> <StatusCards :status="displayStatus" />
<MultiStrategyPanel <MultiStrategyPanel
:strategies="strategies" :strategies="strategies"
+34
View File
@@ -0,0 +1,34 @@
/**
* Top-of-dashboard status. With one strategy it is that strategy's status;
* with several, the per-strategy numbers are aggregated for the whole account.
*/
export function aggregateStatus(status, multiStatus) {
const list = multiStatus?.strategies || []
if (list.length <= 1) return status
const sum = (field) => list.reduce((acc, s) => acc + (s[field] || 0), 0)
const halted = list.filter((s) => s.halted)
return {
...status,
running: multiStatus.running,
price: multiStatus.price ?? status.price,
cash: sum('cash'),
equity: multiStatus.equity,
starting_balance: multiStatus.starting_balance,
realized_pnl: sum('realized_pnl'),
unrealized_pnl: sum('unrealized_pnl'),
total_pnl: multiStatus.total_pnl,
day_pnl: sum('day_pnl'),
// Worst individual strategy drawdown; the portfolio peak is not tracked.
drawdown_pct: Math.max(0, ...list.map((s) => s.drawdown_pct || 0)),
trades_today: sum('trades_today'),
open_positions: sum('open_positions'),
halted: halted.length > 0,
halt_reason:
halted
.map((s) => s.halt_reason)
.filter(Boolean)
.join('; ') || null,
}
}
+87
View File
@@ -0,0 +1,87 @@
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)
})
})