diff --git a/frontend/src/views/backtest/PortfolioBacktest.vue b/frontend/src/views/backtest/PortfolioBacktest.vue index b029228..4c5d6c2 100644 --- a/frontend/src/views/backtest/PortfolioBacktest.vue +++ b/frontend/src/views/backtest/PortfolioBacktest.vue @@ -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 '-' diff --git a/frontend/src/views/paper/List.vue b/frontend/src/views/paper/List.vue index 31fe7a7..ba1878e 100644 --- a/frontend/src/views/paper/List.vue +++ b/frontend/src/views/paper/List.vue @@ -254,10 +254,12 @@ async function saveEdit(): Promise {
{{ instanceNameOf(row.instance_id) || '未绑实例' }}
- + diff --git a/frontend/src/views/paper/New.vue b/frontend/src/views/paper/New.vue index 29bdbe3..3abfbe3 100644 --- a/frontend/src/views/paper/New.vue +++ b/frontend/src/views/paper/New.vue @@ -112,8 +112,8 @@ async function applyInstance(instId: number | string): Promise { ]) 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 { + 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(() => diff --git a/frontend/src/views/strategy/List.vue b/frontend/src/views/strategy/List.vue index 6ddee21..ff94f8f 100644 --- a/frontend/src/views/strategy/List.vue +++ b/frontend/src/views/strategy/List.vue @@ -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 }).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) } }) diff --git a/sanguo_portfolio/runner_backtest.py b/sanguo_portfolio/runner_backtest.py index 1363c90..94e30bc 100644 --- a/sanguo_portfolio/runner_backtest.py +++ b/sanguo_portfolio/runner_backtest.py @@ -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("交易天数")), }