release: v1.17.14 — Web UI 数据评级系统(S/A/B/C/D 五档,三入口覆盖)

纯前端 TypeScript 实现,零后端改动。回测/组合/寻优三处结果顶部
显示评级徽章,让普通人 1 秒判断「适不适合经常参与」。

评级不看收益率(避免被近期大涨误导),只看风险调整后的持有体验:
卡玛/最大回撤/胜率/利润因子/夏普/波动率六维加权 + 一票否决。
京东方「收益 126% 但胜率 35%」案例 = D 档(核心验证点)。

长线低频策略(6年6笔)不会被冤枉:交易<10笔时只降权胜率/利润因子
维度,不否决整个评级(修复用户实测反馈)。

- 新增 web-ui/src/grading/ 核心模块(engine/thresholds/combinedMetrics/index)
- 新增 GradeBadge.vue + GradeDetails.vue 组件
- 接入 BacktestView/PortfolioView/OptimizeView(寻优排名表加评级列)
- 15 个自检测试(node:test + rolldown 打包),覆盖京东方 D / 长线 B 等场景
This commit is contained in:
Justin Gu
2026-07-05 01:21:18 +08:00
parent 66c49d1a8d
commit cf843cebad
16 changed files with 1667 additions and 2 deletions
+50
View File
@@ -0,0 +1,50 @@
/**
* 把评级测试入口打包成单文件 JS,再用 node:test 跑。
*
* 项目未引入 vitest/jest,临时用 rolldownvite 自带依赖)打包,
* 避免 Node 原生 strip-types 对 ESM 无后缀 import 的限制。
*
* 运行:node scripts/run-grading-tests.mjs
*/
import { build } from 'rolldown'
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { spawnSync } from 'node:child_process'
const tmpDir = mkdtempSync(join(tmpdir(), 'grading-tests-'))
const bundlePath = join(tmpDir, 'bundle.mjs')
// 入口:导入测试文件,触发 node:test 注册
const entryPath = join(tmpDir, 'entry.mjs')
writeFileSync(
entryPath,
`import '${resolve('src/grading/__tests__/grade.test.ts').replace(/\\/g, '/')}'\n`,
)
console.log('→ 打包中…')
try {
await build({
input: entryPath,
output: {
file: bundlePath,
format: 'esm',
},
// 顶层 await / dynamic import 都 OK
platform: 'node',
// 不外部化 node 内置
external: ['node:test', 'node:assert', 'node:assert/strict', 'node:os', 'node:fs', 'node:path', 'node:child_process'],
// 强制把测试文件和 grading 模块都打进 bundle
treeshake: false,
})
} catch (e) {
console.error('打包失败:', e)
process.exit(1)
}
console.log('→ 运行测试…')
const r = spawnSync('node', ['--test', bundlePath], { stdio: 'inherit' })
rmSync(tmpDir, { recursive: true, force: true })
process.exit(r.status ?? 0)