Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef895b6b9c | ||
|
|
57dcfe7adb |
No files matched your search
@@ -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,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in new issue
Block a user