From 6aab7a82b5bfe05b6e1ba7d6fc7a8e632329e5f4 Mon Sep 17 00:00:00 2001 From: Justin Gu <97915@qq.com> Date: Fri, 3 Jul 2026 03:35:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(web-ui):=20Vue3=20+=20ECharts=20=E5=9B=9E?= =?UTF-8?q?=E6=B5=8B=E5=89=8D=E7=AB=AF=EF=BC=88=E5=8D=95=E6=A0=87=E7=9A=84?= =?UTF-8?q?=20+=20=E7=BB=84=E5=90=88=E5=9B=9E=E6=B5=8B=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 独立前端工程(web-ui/),Vue3 + Vite + TypeScript + Pinia + ECharts。 通过 vite proxy 对接后端 /api,开发期无跨域。 单标的回测(/): - StrategyPicker: 策略下拉 + 按 schema 动态渲染参数表单 - SymbolPicker: 选标的 + 日期范围取行情(默认3年,结束日=今天) - KlineChart: K线主图 + 买卖点 markPoint(按 datetime 对齐) - EquityChart: 净值曲线 + 回撤双轴图 - MetricTable: 19 项绩效指标(收益/风险/交易分组) - TradeTable: 成交记录表 组合回测(/portfolio): - StocksPicker: 多标的输入(增删标签) - PortfolioCompareChart: 各标的净值叠加(归一化对比) - PortfolioSummaryTable: 各标的绩效横向对比 - 顶部导航切换单标的/组合 契约修复(/check 审计): - fetchBars 归一化 date/datetime 列名(日线返回 date) - K线买卖点按完整 datetime 对齐(分钟线不再折叠) - count 上限对齐后端 800 - ECharts 注册内联 dark 主题 - 参数表单空输入不打断编辑 - 网络错误友好提示 技术栈:Vue 3.5 / Vite 8 / TS 6 / Pinia / ECharts(按需引入 725KB) --- web-ui/.gitignore | 24 + web-ui/.vscode/extensions.json | 3 + web-ui/README.md | 71 + web-ui/index.html | 13 + web-ui/package-lock.json | 1429 +++++++++++++++++ web-ui/package.json | 25 + web-ui/public/favicon.svg | 1 + web-ui/src/App.vue | 62 + web-ui/src/api.ts | 159 ++ web-ui/src/components/EquityChart.vue | 104 ++ web-ui/src/components/KlineChart.vue | 134 ++ web-ui/src/components/MetricTable.vue | 138 ++ .../src/components/PortfolioCompareChart.vue | 96 ++ .../src/components/PortfolioSummaryTable.vue | 82 + web-ui/src/components/StocksPicker.vue | 99 ++ web-ui/src/components/StrategyPicker.vue | 124 ++ web-ui/src/components/SymbolPicker.vue | 133 ++ web-ui/src/components/TradeTable.vue | 101 ++ web-ui/src/echarts-setup.ts | 57 + web-ui/src/main.ts | 8 + web-ui/src/router.ts | 15 + web-ui/src/stores/backtest.ts | 129 ++ web-ui/src/style.css | 106 ++ web-ui/src/types.ts | 171 ++ web-ui/src/views/BacktestView.vue | 216 +++ web-ui/src/views/PortfolioView.vue | 266 +++ web-ui/tsconfig.app.json | 15 + web-ui/tsconfig.json | 7 + web-ui/tsconfig.node.json | 23 + web-ui/vite.config.ts | 18 + 30 files changed, 3829 insertions(+) create mode 100644 web-ui/.gitignore create mode 100644 web-ui/.vscode/extensions.json create mode 100644 web-ui/README.md create mode 100644 web-ui/index.html create mode 100644 web-ui/package-lock.json create mode 100644 web-ui/package.json create mode 100644 web-ui/public/favicon.svg create mode 100644 web-ui/src/App.vue create mode 100644 web-ui/src/api.ts create mode 100644 web-ui/src/components/EquityChart.vue create mode 100644 web-ui/src/components/KlineChart.vue create mode 100644 web-ui/src/components/MetricTable.vue create mode 100644 web-ui/src/components/PortfolioCompareChart.vue create mode 100644 web-ui/src/components/PortfolioSummaryTable.vue create mode 100644 web-ui/src/components/StocksPicker.vue create mode 100644 web-ui/src/components/StrategyPicker.vue create mode 100644 web-ui/src/components/SymbolPicker.vue create mode 100644 web-ui/src/components/TradeTable.vue create mode 100644 web-ui/src/echarts-setup.ts create mode 100644 web-ui/src/main.ts create mode 100644 web-ui/src/router.ts create mode 100644 web-ui/src/stores/backtest.ts create mode 100644 web-ui/src/style.css create mode 100644 web-ui/src/types.ts create mode 100644 web-ui/src/views/BacktestView.vue create mode 100644 web-ui/src/views/PortfolioView.vue create mode 100644 web-ui/tsconfig.app.json create mode 100644 web-ui/tsconfig.json create mode 100644 web-ui/tsconfig.node.json create mode 100644 web-ui/vite.config.ts diff --git a/web-ui/.gitignore b/web-ui/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/web-ui/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/web-ui/.vscode/extensions.json b/web-ui/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/web-ui/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/web-ui/README.md b/web-ui/README.md new file mode 100644 index 0000000..54bb921 --- /dev/null +++ b/web-ui/README.md @@ -0,0 +1,71 @@ +# easy-tdx 回测 Web UI + +单标的策略回测的可视化前端(Vue3 + ECharts)。对应后端回测 REST API(`/api/v1/backtest/*`)。 + +## 快速开始 + +需要两个进程:后端 FastAPI(提供回测 + 行情 API)+ 前端 Vite dev server。 + +```bash +# 1. 启动后端(仓库根目录,使用 venv) +venv/Scripts/easy-tdx serve --port 8000 + +# 2. 启动前端(本目录) +npm install # 首次 +npm run dev # 启动在 http://localhost:5173 +``` + +浏览器打开 `http://localhost:5173`。开发期 `/api` 请求由 vite proxy 转发到后端 `127.0.0.1:8000`,无需处理跨域。 + +## 功能(MVP-A) + +- **选标的取行情**:沪深北市场,日线/周线/分钟线,调 `/api/v1/bars` +- **策略选择 + 动态参数表单**:下拉选预置策略(MA交叉/MACD/布林/RSI/KDJ),参数表单按后端 schema 自动渲染 +- **回测报告**: + - K线主图 + 买卖点标注(ECharts candlestick + markPoint) + - 净值曲线 + 回撤双轴图 + - 19 项绩效指标表(收益/风险/交易分组) + - 成交记录表 + +## 技术栈 + +| 层 | 技术 | 说明 | +|----|------|------| +| 框架 | Vue 3.5 + TypeScript | Composition API + ` + + diff --git a/web-ui/package-lock.json b/web-ui/package-lock.json new file mode 100644 index 0000000..ce5125c --- /dev/null +++ b/web-ui/package-lock.json @@ -0,0 +1,1429 @@ +{ + "name": "web-ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web-ui", + "version": "0.0.0", + "dependencies": { + "echarts": "^6.1.0", + "pinia": "^3.0.4", + "vue": "^3.5.39", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@vitejs/plugin-vue": "^6.0.7", + "@vue/tsconfig": "^0.9.1", + "typescript": "~6.0.2", + "vite": "^8.1.1", + "vue-tsc": "^3.3.5" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.138.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz", + "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz", + "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz", + "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz", + "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz", + "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz", + "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz", + "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz", + "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz", + "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz", + "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz", + "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz", + "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz", + "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz", + "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz", + "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz", + "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.7.tgz", + "integrity": "sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.28.tgz", + "integrity": "sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "2.4.28" + } + }, + "node_modules/@volar/source-map": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.28.tgz", + "integrity": "sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/typescript": { + "version": "2.4.28", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.28.tgz", + "integrity": "sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.10.tgz", + "integrity": "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==", + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.7.10" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.10.tgz", + "integrity": "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.7.10", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.10.tgz", + "integrity": "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==", + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-3.3.6.tgz", + "integrity": "sha512-LgBMZAy2sR3cQWknpyaxnI6yBkqDfLBPkbdhwRhQCvzfNJRQXPilgQIrdI/v4ytJ0sAq9bWhaPsjqBqneomJ3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "2.4.28", + "@vue/compiler-dom": "^3.5.0", + "@vue/shared": "^3.5.0", + "alien-signals": "^3.2.0", + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1", + "picomatch": "^4.0.4" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.9.1.tgz", + "integrity": "sha512-buvjm+9NzLCJL29KY1j1991YYJ5e6275OiK+G4jtmfIb+z4POywbdm0wXusT9adVWqe0xqg70TbI7+mRx4uU9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 5.8", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/alien-signals": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/alien-signals/-/alien-signals-3.2.1.tgz", + "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", + "dev": true, + "license": "MIT" + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "license": "MIT", + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/echarts": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz", + "integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "2.3.0", + "zrender": "6.1.0" + } + }, + "node_modules/echarts/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "license": "MIT" + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz", + "integrity": "sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.4.tgz", + "integrity": "sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^7.7.7" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.5.0", + "vue": "^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz", + "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.138.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.4", + "@rolldown/binding-darwin-arm64": "1.1.4", + "@rolldown/binding-darwin-x64": "1.1.4", + "@rolldown/binding-freebsd-x64": "1.1.4", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.4", + "@rolldown/binding-linux-arm64-gnu": "1.1.4", + "@rolldown/binding-linux-arm64-musl": "1.1.4", + "@rolldown/binding-linux-ppc64-gnu": "1.1.4", + "@rolldown/binding-linux-s390x-gnu": "1.1.4", + "@rolldown/binding-linux-x64-gnu": "1.1.4", + "@rolldown/binding-linux-x64-musl": "1.1.4", + "@rolldown/binding-openharmony-arm64": "1.1.4", + "@rolldown/binding-wasm32-wasi": "1.1.4", + "@rolldown/binding-win32-arm64-msvc": "1.1.4", + "@rolldown/binding-win32-x64-msvc": "1.1.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "license": "MIT", + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz", + "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.16", + "rolldown": "~1.1.3", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-router": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.6.4.tgz", + "integrity": "sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.5.0" + } + }, + "node_modules/vue-router/node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/vue-tsc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-3.3.6.tgz", + "integrity": "sha512-ERXGgbKSBGFUkavrJ1Iwj0ZVxKqB/5UOx65IXy7fPf2UsoI21n3ssQLwdvx8xyUGgJe9PvSZTM/FYzIdwUDPFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "2.4.28", + "@vue/language-core": "3.3.6" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/zrender": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz", + "integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==", + "license": "BSD-3-Clause", + "dependencies": { + "tslib": "2.3.0" + } + }, + "node_modules/zrender/node_modules/tslib": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", + "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==", + "license": "0BSD" + } + } +} diff --git a/web-ui/package.json b/web-ui/package.json new file mode 100644 index 0000000..1a99966 --- /dev/null +++ b/web-ui/package.json @@ -0,0 +1,25 @@ +{ + "name": "web-ui", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vue-tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "echarts": "^6.1.0", + "pinia": "^3.0.4", + "vue": "^3.5.39", + "vue-router": "^4.6.4" + }, + "devDependencies": { + "@types/node": "^24.13.2", + "@vitejs/plugin-vue": "^6.0.7", + "@vue/tsconfig": "^0.9.1", + "typescript": "~6.0.2", + "vite": "^8.1.1", + "vue-tsc": "^3.3.5" + } +} diff --git a/web-ui/public/favicon.svg b/web-ui/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/web-ui/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/web-ui/src/App.vue b/web-ui/src/App.vue new file mode 100644 index 0000000..adf2acc --- /dev/null +++ b/web-ui/src/App.vue @@ -0,0 +1,62 @@ + + + + + diff --git a/web-ui/src/api.ts b/web-ui/src/api.ts new file mode 100644 index 0000000..b7aad8f --- /dev/null +++ b/web-ui/src/api.ts @@ -0,0 +1,159 @@ +// 后端 API 封装。统一 fetch + 错误处理,返回类型化结果。 +// 开发期通过 vite proxy 走 /api(同源),生产期由 FastAPI 同源托管。 + +import type { + ApiError, + BacktestRequest, + BacktestResult, + Bar, + Category, + PortfolioBacktestRequest, + StrategiesResponse, + TaskState, + TaskSubmitResponse, +} from './types' + +const BASE = '/api/v1' + +/** 把未知错误格式化为用户可读的消息(网络错误给友好提示)。 */ +export function formatError(e: unknown): string { + if (e instanceof TypeError && e.message.includes('fetch')) { + return '网络错误:无法连接后端服务,请确认 easy-tdx serve 已启动' + } + return e instanceof Error ? e.message : String(e) +} + +/** 把 Response 解析为 ApiError 抛出(后端统一错误格式 {error, detail})。 */ +async function throwError(resp: Response): Promise { + let detail = `${resp.status} ${resp.statusText}` + try { + const body = (await resp.json()) as ApiError + if (body?.detail) detail = body.detail + } catch { + // 非 JSON 错误体,用 statusText + } + throw new Error(detail) +} + +/** 枚举预置策略 + 参数 schema。 */ +export async function fetchStrategies(): Promise { + const resp = await fetch(`${BASE}/backtest/strategies`) + if (!resp.ok) await throwError(resp) + return (await resp.json()) as StrategiesResponse +} + +/** + * 按标的取 K 线行情(OHLCV)。 + * + * 后端 /bars 仅支持按 count 取数(上限 800 根,约 3.2 年日线),不支持日期范围。 + * 这里固定拉满 800 根,由调用方按日期范围在前端过滤。 + * 可选 startDate/endDate 对结果做闭区间过滤(ISO 日期字符串,如 "2024-01-01")。 + */ +export async function fetchBars( + market: string, + code: string, + category: Category, + startDate?: string, + endDate?: string, +): Promise { + const params = new URLSearchParams({ + market, + code, + category, + count: '800', // 后端硬上限,前端按日期过滤 + }) + const resp = await fetch(`${BASE}/bars?${params}`) + if (!resp.ok) await throwError(resp) + const body = (await resp.json()) as { data: Record[] } + // 后端 bars 列名不统一:日线及以上是 `date`,分钟线是 `datetime`。 + // 归一化为统一 `datetime` 字段(取 ISO 前 19 位)。 + let bars = body.data.map((row) => normalizeBar(row)) + // 按日期范围过滤(闭区间,比较日期部分 YYYY-MM-DD) + if (startDate) bars = bars.filter((b) => b.datetime.slice(0, 10) >= startDate) + if (endDate) bars = bars.filter((b) => b.datetime.slice(0, 10) <= endDate) + return bars +} + +/** 把后端 bars 的单条记录归一化为统一 Bar(datetime 字段)。 */ +function normalizeBar(row: Record): Bar { + const raw = (row.datetime ?? row.date) as string | undefined + if (!raw) throw new Error('行情数据缺少 datetime/date 字段') + return { + datetime: raw.slice(0, 19).replace(' ', 'T'), + open: Number(row.open), + high: Number(row.high), + low: Number(row.low), + close: Number(row.close), + vol: Number(row.vol), + amount: Number(row.amount), + } +} + +/** 同步回测(内联 OHLCV,快速)。 */ +export async function runBacktest(req: BacktestRequest): Promise { + const resp = await fetch(`${BASE}/backtest/run`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(req), + }) + if (!resp.ok) await throwError(resp) + return (await resp.json()) as BacktestResult +} + +/** 提交后台回测任务,返回 task_id。 */ +export async function submitBacktestTask(req: BacktestRequest): Promise { + const resp = await fetch(`${BASE}/backtest/run/async`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(req), + }) + if (!resp.ok) await throwError(resp) + return (await resp.json()) as TaskSubmitResponse +} + +/** 提交组合回测后台任务,返回 task_id。 */ +export async function submitPortfolioTask( + req: PortfolioBacktestRequest, +): Promise { + const resp = await fetch(`${BASE}/backtest/portfolio/run/async`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(req), + }) + if (!resp.ok) await throwError(resp) + return (await resp.json()) as TaskSubmitResponse +} + +/** 查询后台任务状态(轮询用)。 */ +export async function fetchTask(taskId: string): Promise { + const resp = await fetch(`${BASE}/backtest/tasks/${taskId}`) + if (!resp.ok) await throwError(resp) + return (await resp.json()) as TaskState +} + +/** + * 提交后台任务并轮询直到 done/failed。 + * @param req 回测请求 + * @param onPoll 每次轮询回调(可选,用于更新 UI 进度) + * @param intervalMs 轮询间隔(默认 300ms) + * @param timeoutMs 总超时(默认 120s) + */ +export async function runBacktestWithPolling( + req: BacktestRequest, + onPoll?: (state: TaskState) => void, + intervalMs = 300, + timeoutMs = 120_000, +): Promise { + const { task_id } = await submitBacktestTask(req) + const start = Date.now() + // eslint-disable-next-line no-constant-condition + while (true) { + const state = await fetchTask(task_id) + onPoll?.(state) + if (state.status === 'done' || state.status === 'failed') return state + if (Date.now() - start > timeoutMs) { + throw new Error(`回测任务超时(${timeoutMs / 1000}s)`) + } + await new Promise((r) => setTimeout(r, intervalMs)) + } +} diff --git a/web-ui/src/components/EquityChart.vue b/web-ui/src/components/EquityChart.vue new file mode 100644 index 0000000..a802a34 --- /dev/null +++ b/web-ui/src/components/EquityChart.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/web-ui/src/components/KlineChart.vue b/web-ui/src/components/KlineChart.vue new file mode 100644 index 0000000..4439837 --- /dev/null +++ b/web-ui/src/components/KlineChart.vue @@ -0,0 +1,134 @@ + + + + + diff --git a/web-ui/src/components/MetricTable.vue b/web-ui/src/components/MetricTable.vue new file mode 100644 index 0000000..ff7eee8 --- /dev/null +++ b/web-ui/src/components/MetricTable.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/web-ui/src/components/PortfolioCompareChart.vue b/web-ui/src/components/PortfolioCompareChart.vue new file mode 100644 index 0000000..e367f80 --- /dev/null +++ b/web-ui/src/components/PortfolioCompareChart.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/web-ui/src/components/PortfolioSummaryTable.vue b/web-ui/src/components/PortfolioSummaryTable.vue new file mode 100644 index 0000000..0ecfd40 --- /dev/null +++ b/web-ui/src/components/PortfolioSummaryTable.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/web-ui/src/components/StocksPicker.vue b/web-ui/src/components/StocksPicker.vue new file mode 100644 index 0000000..1ebc3c6 --- /dev/null +++ b/web-ui/src/components/StocksPicker.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/web-ui/src/components/StrategyPicker.vue b/web-ui/src/components/StrategyPicker.vue new file mode 100644 index 0000000..d0eb209 --- /dev/null +++ b/web-ui/src/components/StrategyPicker.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/web-ui/src/components/SymbolPicker.vue b/web-ui/src/components/SymbolPicker.vue new file mode 100644 index 0000000..0ba6878 --- /dev/null +++ b/web-ui/src/components/SymbolPicker.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/web-ui/src/components/TradeTable.vue b/web-ui/src/components/TradeTable.vue new file mode 100644 index 0000000..dd3bd36 --- /dev/null +++ b/web-ui/src/components/TradeTable.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/web-ui/src/echarts-setup.ts b/web-ui/src/echarts-setup.ts new file mode 100644 index 0000000..ec15116 --- /dev/null +++ b/web-ui/src/echarts-setup.ts @@ -0,0 +1,57 @@ +// ECharts 按需引入。只注册 MVP-A 用到的图表类型,避免全量引入(~1MB → ~400KB)。 +// 用到的:candlestick(K线)、line(净值/回撤曲线)、markPoint(买卖点标注)。 + +import * as echarts from 'echarts/core' +import { BarChart, CandlestickChart, LineChart } from 'echarts/charts' +import { + DataZoomComponent, + GridComponent, + LegendComponent, + MarkPointComponent, + TitleComponent, + TooltipComponent, +} from 'echarts/components' +import { CanvasRenderer } from 'echarts/renderers' + +echarts.use([ + CanvasRenderer, + CandlestickChart, + LineChart, + BarChart, + GridComponent, + TooltipComponent, + LegendComponent, + TitleComponent, + DataZoomComponent, + MarkPointComponent, +]) + +// A股惯例:红涨绿跌 +export const UP_COLOR = '#ef4146' +export const DOWN_COLOR = '#18a058' + +// 注册内联 dark 主题(echarts/core 不预置 'dark' 主题数据)。 +// 覆盖坐标轴文字、分割线等默认浅色样式,适配深色背景。 +echarts.registerTheme('dark', { + backgroundColor: 'transparent', + textStyle: { color: '#8b919e' }, + title: { textStyle: { color: '#e6e8eb' }, subtextStyle: { color: '#8b919e' } }, + legend: { textStyle: { color: '#8b919e' } }, + tooltip: { + backgroundColor: 'rgba(26,29,38,0.95)', + borderColor: '#2a2e3a', + textStyle: { color: '#e6e8eb' }, + }, + categoryAxis: { + axisLine: { lineStyle: { color: '#2a2e3a' } }, + axisLabel: { color: '#5c6370' }, + splitLine: { show: false }, + }, + valueAxis: { + axisLine: { lineStyle: { color: '#2a2e3a' } }, + axisLabel: { color: '#5c6370' }, + splitLine: { lineStyle: { color: '#2a2e3a' } }, + }, +}) + +export default echarts diff --git a/web-ui/src/main.ts b/web-ui/src/main.ts new file mode 100644 index 0000000..110b171 --- /dev/null +++ b/web-ui/src/main.ts @@ -0,0 +1,8 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' + +import App from './App.vue' +import { router } from './router' +import './style.css' + +createApp(App).use(createPinia()).use(router).mount('#app') diff --git a/web-ui/src/router.ts b/web-ui/src/router.ts new file mode 100644 index 0000000..610ba2c --- /dev/null +++ b/web-ui/src/router.ts @@ -0,0 +1,15 @@ +import { createRouter, createWebHistory } from 'vue-router' + +import BacktestView from './views/BacktestView.vue' +import PortfolioView from './views/PortfolioView.vue' + +// 单标的回测(/)+ 组合回测(/portfolio)。参数寻优/结果对比留待 Phase 4-5。 +const routes = [ + { path: '/', name: 'backtest', component: BacktestView }, + { path: '/portfolio', name: 'portfolio', component: PortfolioView }, +] + +export const router = createRouter({ + history: createWebHistory(), + routes, +}) diff --git a/web-ui/src/stores/backtest.ts b/web-ui/src/stores/backtest.ts new file mode 100644 index 0000000..0ac3a27 --- /dev/null +++ b/web-ui/src/stores/backtest.ts @@ -0,0 +1,129 @@ +// 回测状态管理(Pinia)。 +// 持有:策略列表、当前 OHLCV、回测结果、运行状态、错误信息。 + +import { defineStore } from 'pinia' +import { computed, ref } from 'vue' + +import { fetchStrategies, formatError, runBacktest, submitPortfolioTask, fetchTask } from '../api' +import type { + BacktestRequest, + BacktestResult, + Bar, + PortfolioBacktestRequest, + PortfolioResult, + StrategySchema, +} from '../types' + +export const useBacktestStore = defineStore('backtest', () => { + // ── 策略 ───────────────────────────────────────────────────────────────── + const strategies = ref([]) + const strategiesLoaded = ref(false) + + async function loadStrategies() { + if (strategiesLoaded.value) return + const resp = await fetchStrategies() + strategies.value = resp.strategies + strategiesLoaded.value = true + } + + // ── OHLCV 行情(前端始终持有,回测与 K 线共用) ─────────────────────────── + const ohlcv = ref([]) + const barsSource = ref('') // 来源描述,如 "SZ:000001 DAY×250" + + function setOhlcv(bars: Bar[], source: string) { + ohlcv.value = bars + barsSource.value = source + } + + const hasBars = computed(() => ohlcv.value.length >= 2) + + // ── 回测结果 ────────────────────────────────────────────────────────────── + const result = ref(null) + const running = ref(false) + const error = ref('') + + /** 运行同步回测(内联 OHLCV)。 */ + async function run(req: Omit) { + if (!hasBars.value) { + error.value = '请先取行情数据或粘贴 OHLCV' + return + } + running.value = true + error.value = '' + try { + const fullReq: BacktestRequest = { ...req, ohlcv: ohlcv.value } + result.value = await runBacktest(fullReq) + } catch (e) { + error.value = formatError(e) + result.value = null + } finally { + running.value = false + } + } + + function clearResult() { + result.value = null + error.value = '' + } + + // ── 组合回测(Phase 3) ─────────────────────────────────────────────────── + const portfolioResult = ref(null) + const portfolioRunning = ref(false) + + /** 提交组合回测后台任务并轮询直到完成。 */ + async function runPortfolio(req: PortfolioBacktestRequest) { + portfolioRunning.value = true + error.value = '' + portfolioResult.value = null + try { + const { task_id } = await submitPortfolioTask(req) + // 轮询 + const start = Date.now() + // eslint-disable-next-line no-constant-condition + while (true) { + const state = await fetchTask(task_id) + if (state.status === 'done' && state.result) { + portfolioResult.value = state.result as PortfolioResult + break + } + if (state.status === 'failed') { + throw new Error(state.error || '组合回测失败') + } + if (Date.now() - start > 120_000) throw new Error('组合回测超时(120s)') + await new Promise((r) => setTimeout(r, 300)) + } + } catch (e) { + error.value = formatError(e) + portfolioResult.value = null + } finally { + portfolioRunning.value = false + } + } + + function clearPortfolio() { + portfolioResult.value = null + error.value = '' + } + + return { + // state + strategies, + strategiesLoaded, + ohlcv, + barsSource, + result, + running, + error, + portfolioResult, + portfolioRunning, + // getters + hasBars, + // actions + loadStrategies, + setOhlcv, + run, + clearResult, + runPortfolio, + clearPortfolio, + } +}) diff --git a/web-ui/src/style.css b/web-ui/src/style.css new file mode 100644 index 0000000..c9f8ca9 --- /dev/null +++ b/web-ui/src/style.css @@ -0,0 +1,106 @@ +/* 全局深色金融主题。MVP-A 不引 UI 组件库,用 CSS 变量统一设计 token。 */ + +:root { + --bg: #0f1117; + --bg-elevated: #1a1d26; + --bg-panel: #161922; + --border: #2a2e3a; + --text: #e6e8eb; + --text-muted: #8b919e; + --text-dim: #5c6370; + --accent: #4a9eff; + --up: #ef4146; /* A股惯例:红涨 */ + --down: #18a058; /* 绿跌 */ + --warn: #f0a020; + --radius: 6px; + --font-mono: 'JetBrains Mono', 'Cascadia Code', Consolas, monospace; +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, +body, +#app { + height: 100%; + background: var(--bg); + color: var(--text); + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', + Roboto, Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.5; +} + +button { + cursor: pointer; + border: 1px solid var(--border); + background: var(--bg-elevated); + color: var(--text); + padding: 6px 14px; + border-radius: var(--radius); + font-size: 13px; + transition: all 0.15s; +} +button:hover:not(:disabled) { + border-color: var(--accent); +} +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} +button.primary { + background: var(--accent); + border-color: var(--accent); + color: #fff; + font-weight: 600; +} +button.primary:disabled { + background: var(--bg-elevated); + color: var(--text-dim); +} + +input, +select, +textarea { + background: var(--bg); + border: 1px solid var(--border); + color: var(--text); + padding: 6px 10px; + border-radius: var(--radius); + font-size: 13px; + width: 100%; +} +input:focus, +select:focus, +textarea:focus { + outline: none; + border-color: var(--accent); +} + +label { + display: block; + color: var(--text-muted); + font-size: 12px; + margin-bottom: 4px; +} + +.field { + margin-bottom: 12px; +} + +.row { + display: flex; + gap: 8px; + align-items: flex-end; +} +.row > * { + flex: 1; +} + +a { + color: var(--accent); +} diff --git a/web-ui/src/types.ts b/web-ui/src/types.ts new file mode 100644 index 0000000..0540c6d --- /dev/null +++ b/web-ui/src/types.ts @@ -0,0 +1,171 @@ +// 后端 API 的 TypeScript 类型镜像。 +// 与 src/easy_tdx/web/backtest_schemas.py 及 backtest router 的响应保持一致。 +// 后端是唯一事实源;这里只做类型契约。 + +// ── 策略 schema(GET /api/v1/backtest/strategies) ─────────────────────────── + +export type ParamType = 'int' | 'float' | 'bool' | 'str' + +export interface ParamSchema { + name: string + type: ParamType + default: number | string | boolean + label: string + min_value?: number + max_value?: number + choices?: string[] + description?: string +} + +export interface StrategySchema { + name: string + label: string + description: string + params: ParamSchema[] +} + +export interface StrategiesResponse { + strategies: StrategySchema[] + count: number +} + +// ── OHLCV 行情(GET /api/v1/bars) ──────────────────────────────────────────── + +export interface Bar { + datetime: string + open: number + high: number + low: number + close: number + vol: number + amount: number +} + +export interface DataFrameResponse { + data: Record[] + count: number +} + +// ── 回测请求(POST /api/v1/backtest/run) ───────────────────────────────────── + +export type ExecutionMode = 'next_open' | 'next_close' | 'this_close' | 'worst' | 'best' +export type Category = 'DAY' | 'WEEK' | 'MONTH' | 'MIN_5' | 'MIN_15' | 'MIN_30' | 'MIN_60' + +export interface BacktestRequest { + strategy: string + params?: Record + cash?: number + commission?: number + min_commission?: number + stamp_tax?: number + slippage?: number + execution?: ExecutionMode + ohlcv?: Bar[] + symbol?: string + category?: Category + count?: number +} + +// ── 回测结果 ────────────────────────────────────────────────────────────────── + +export interface Performance { + total_return: number + annual_return: number + max_drawdown: number + max_dd_duration: number + sharpe: number + sortino: number + calmar: number + total_trades: number + win_trades: number + lose_trades: number + rejected_trades: number + win_rate: number + profit_factor: number + avg_win: number + avg_loss: number + max_win: number + max_loss: number + avg_holding_days: number + volatility: number +} + +export interface EquityPoint { + datetime: string + cash: number + position_value: number + total: number + drawdown: number + drawdown_pct: number +} + +export interface Trade { + datetime: string + direction: 'BUY' | 'SELL' + size: number + price: number + commission: number + slippage: number + pnl: number + rejected: boolean +} + +export interface BacktestResult { + performance: Performance + equity_curve: EquityPoint[] + trades: Trade[] + positions: Record[] + config: Record +} + +// ── 后台任务(POST /api/v1/backtest/run/async + GET /tasks/{id}) ───────────── + +export interface TaskSubmitResponse { + task_id: string + status: 'pending' | 'running' +} + +export type TaskStatus = 'pending' | 'running' | 'done' | 'failed' + +export interface TaskState { + task_id: string + status: TaskStatus + result: BacktestResult | PortfolioResult | null + error: string | null + description: string + elapsed: number +} + +// ── 组合回测(Phase 3) ─────────────────────────────────────────────────────── + +export interface PortfolioBacktestRequest { + strategy: string + params?: Record + cash?: number + commission?: number + slippage?: number + execution?: ExecutionMode + stocks: string[] + category?: Category + start_date?: string + end_date?: string +} + +export interface PortfolioResult { + total_performance: { + total_return: number + annual_return: number + total_stocks: number + total_cash: number + } + individual_results: Record + equity_allocation: Record + combined_equity: EquityPoint[] +} + +// ── 错误响应(后端 ApiErrorResponse) ───────────────────────────────────────── + +export interface ApiError { + error: string + detail: string +} diff --git a/web-ui/src/views/BacktestView.vue b/web-ui/src/views/BacktestView.vue new file mode 100644 index 0000000..35c14ec --- /dev/null +++ b/web-ui/src/views/BacktestView.vue @@ -0,0 +1,216 @@ + + + + + diff --git a/web-ui/src/views/PortfolioView.vue b/web-ui/src/views/PortfolioView.vue new file mode 100644 index 0000000..18fad31 --- /dev/null +++ b/web-ui/src/views/PortfolioView.vue @@ -0,0 +1,266 @@ + + + + + diff --git a/web-ui/tsconfig.app.json b/web-ui/tsconfig.app.json new file mode 100644 index 0000000..d72aa75 --- /dev/null +++ b/web-ui/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + "allowArbitraryExtensions": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/web-ui/tsconfig.json b/web-ui/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/web-ui/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/web-ui/tsconfig.node.json b/web-ui/tsconfig.node.json new file mode 100644 index 0000000..8455dcb --- /dev/null +++ b/web-ui/tsconfig.node.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "es2023", + "lib": ["ES2023"], + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "module": "nodenext", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/web-ui/vite.config.ts b/web-ui/vite.config.ts new file mode 100644 index 0000000..b1a2fb1 --- /dev/null +++ b/web-ui/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' + +// https://vite.dev/config/ +// dev server 把 /api 请求代理到后端 FastAPI(默认 8000 端口), +// 解决开发期跨域;生产环境由 FastAPI 同源托管 dist。 +export default defineConfig({ + plugins: [vue()], + server: { + port: 5173, + proxy: { + '/api': { + target: 'http://127.0.0.1:8000', + changeOrigin: true, + }, + }, + }, +})