fix(portfolio): 收益单位口径统一小数+用户验收四问题——①1044%真因:bullet_trade返回百分数字面值(10.44=10.44%),组合结果页直显(对)但档案回写/策略库按小数×100(错1044%);_extract_metrics统一÷100归一小数(注释单位契约),组合结果页fmtPct改×100(与CTA/empyrical全平台一致;历史任务NAS一次性迁移)②回测灯/回测历史'找不到结果':done无task_id(老回测)兜底跳任务中心不过滤(instance过滤对老任务为空)③实走/实盘灯'还是参数页':三级fallback——同类型在跑直达监控,实例任何在跑(如只有影子)跳列表过滤,无在跑才发起④模式列折行:宽118+日终并入单chip(实走·日终,title悬浮说明)⑤模拟盘名加版本号={实例名}·{模式}v{N}(同实例同模式递增,对齐实盘);927绿+build绿 [vps]
CI/CD / test (push) Successful in 10s
CI/CD / nas-deploy (push) Successful in 26s
CI/CD / nas-verify (push) Successful in 10s

This commit is contained in:
2026-08-16 10:06:12 +08:00
parent bab7c4d893
commit 0453de9321
5 changed files with 56 additions and 18 deletions
@@ -467,7 +467,8 @@ watch(drawdownCurve, renderDrawdown, { deep: true, flush: 'post' })
function fmtPct(v: number | null | undefined): string {
if (v === null || v === undefined || !Number.isFinite(v)) return '-'
return `${v.toFixed(2)}%`
// metrics 已统一小数口径(2026-08-16;历史字面值已在 NAS 一次性迁移)
return `${(v * 100).toFixed(2)}%`
}
function fmtNum(v: number | null | undefined, digits = 2): string {
if (v === null || v === undefined || !Number.isFinite(v)) return '-'
+5 -3
View File
@@ -254,10 +254,12 @@ async function saveEdit(): Promise<void> {
<div class="cell-id mono muted">{{ instanceNameOf(row.instance_id) || '未绑实例' }}</div>
</template>
</el-table-column>
<el-table-column label="模式" width="96">
<el-table-column label="模式" width="118">
<template #default="{ row }">
<span class="chip chip-paper" :class="{ 'chip-shadow': row.mode === 'shadow' }">{{ modeLabel[row.mode] || row.mode }}</span>
<span v-if="row.strategy_type === 'portfolio' && row.mode === 'live'" class="chip chip-shadow" title="日终回放=每晚 20:30 全量重放结算">日终</span>
<span
class="chip chip-paper" :class="{ 'chip-shadow': row.mode === 'shadow' }"
:title="row.strategy_type === 'portfolio' && row.mode === 'live' ? '日终回放=每晚 20:30 全量重放结算' : ''"
>{{ modeLabel[row.mode] || row.mode }}{{ row.strategy_type === 'portfolio' && row.mode === 'live' ? '·日终' : '' }}</span>
</template>
</el-table-column>
<el-table-column label="频率" width="60">
+17 -2
View File
@@ -112,8 +112,8 @@ async function applyInstance(instId: number | string): Promise<void> {
])
const inst = ir.instance
fromInstance.value = inst.name || String(instId)
// #72 ·
form.value.name = `${inst.name || String(instId)}·${form.value.mode}`
// #72 +
form.value.name = await autoName(inst.name || String(instId), Number(instId), form.value.mode)
const file = fr.files.find((f) => f.name === inst.code_file)
const cls = file?.class_name || inst.code_file
const sym = inst.symbol_or_pool || '600000'
@@ -139,6 +139,21 @@ function onPickInstance(id: number | null): void {
if (id != null) applyInstance(id)
}
// {}·{}v{N}N=
async function autoName(instName: string, instId: number, mode: string): Promise<string> {
const prefix = `${instName}·${mode}v`
let n = 0
try {
const { listPapers } = await import('@/api/paper')
const accs = await listPapers()
n = accs.filter((a: { instance_id?: number | null; name?: string }) =>
a.instance_id === instId && (a.name || '').startsWith(prefix)).length
} catch {
/* 列表拉不到 → v0 */
}
return `${prefix}${n}`
}
const isPortfolio = computed(() => strategyType.value === 'portfolio')
const portfolioStrategy = ref('all_weather')
const instancesOfPortfolio = computed(() =>
+18 -6
View File
@@ -155,7 +155,8 @@ function fileTypeOf(i: Instance): 'portfolio' | 'cta' {
// #68/#75 /done//
// ×
function runBacktest(i: Instance): void {
// task_id /
// task_id task_id
// instance =
const st = i.status?.backtest || '-'
const tid = (i as Instance & { run_meta?: Record<string, { task_id?: string }> }).run_meta?.backtest?.task_id
if (st === 'done' && tid) {
@@ -163,7 +164,7 @@ function runBacktest(i: Instance): void {
return
}
if (st !== '-') {
router.push(`/backtest/history?instance=${i.id}`)
router.push(tid ? `/backtest/history?instance=${i.id}` : '/backtest/history')
return
}
const isPf = fileTypeOf(i) === 'portfolio'
@@ -191,18 +192,29 @@ function runReplay(i: Instance): void {
}
router.push({ path: '/paper/new', query: { mode: 'replay', instance: String(i.id) } })
}
//
function runPaper(i: Instance, mode: 'live' | 'shadow'): void {
const run = (i.running_accounts || []).find((a) => a.kind === mode || (mode === 'live' && a.kind === 'paper'))
const runs = i.running_accounts || []
const run = runs.find((a) => a.kind === mode || (mode === 'live' && a.kind === 'paper'))
if (run) {
router.push(`/paper/live/${run.aid}`) //
router.push(`/paper/live/${run.aid}`)
return
}
if (runs.length) {
router.push(`/paper?instance=${i.id}`)
return
}
router.push({ path: '/paper/new', query: { mode, instance: String(i.id) } })
}
function runLive(i: Instance): void {
const run = (i.running_accounts || []).find((a) => a.kind === 'live')
const runs = i.running_accounts || []
const run = runs.find((a) => a.kind === 'live')
if (run) {
router.push(`/live/monitor/${run.aid}`) //
router.push(`/live/monitor/${run.aid}`)
return
}
if (runs.length) {
router.push(`/live?instance=${i.id}`)
return
}
router.push({ path: '/live/new', query: { instance: String(i.id) } })
+14 -6
View File
@@ -513,15 +513,23 @@ def _to_float(v: Any) -> float | None:
def _extract_metrics(summary: Dict[str, Any]) -> Dict[str, float | None]:
"""bullet-trade summary 用中文 key('策略收益'/'最大回撤'/...)。
百分比按字面数值(12.34% 12.34),前端按需 /100 显示
单位契约2026-08-16 统一metrics 百分比类一律**小数**10.44% 0.1044
CTA(empyrical)/模拟盘/对账全平台一致前端统一 ×100 显示
bullet_trade 返回字面百分数值 此处 ÷100 归一
历史任务库存的是字面值NAS 已做一次性迁移勿重复除
"""
def _pct(key: str) -> float | None:
v = _to_float(summary.get(key))
return v / 100 if v is not None else None
return {
"total_return": _to_float(summary.get("策略收益")),
"annual_return": _to_float(summary.get("策略年化收益")),
"max_drawdown": _to_float(summary.get("最大回撤")),
"total_return": _pct("策略收益"),
"annual_return": _pct("策略年化收益"),
"max_drawdown": _pct("最大回撤"),
"sharpe": _to_float(summary.get("夏普比率")),
"win_rate_daily": _to_float(summary.get("日胜率")),
"win_rate_trade": _to_float(summary.get("交易胜率")),
"win_rate_daily": _pct("日胜率"),
"win_rate_trade": _pct("交易胜率"),
"trading_days": _to_float(summary.get("交易天数")),
}