# 更新日志 本文件记录 easy-tdx 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。 ## [1.17.13] — 2026-07-04 **修复多策略组合回测「最大回撤」严重虚高** —— 用户反馈:3 个策略各自最大回撤仅 45.53%/40.16%/16.89%,组合在一起却显示 **83.76%**。根因是 `MultiStrategyEngine._build_combined_equity` 计算 `drawdown_pct` 时**分母误用初始资金(`initial`)而非逐点峰值(`peak`)**:净值大涨后峰值是初始值的好几倍(本例总收益 545%,峰值≈6.45×初始),同样的绝对回撤额除以小的初始值,百分比被等比放大。正确公式应为 `drawdown / peak`(相对当时峰值的回撤,0~1),与单标的 `PortfolioTracker.equity_curve` 的 `drawdown_pct` 定义一致。修复后最大回撤回到合理区间(≤ 各策略最大回撤的加权,不可能超过 100%)。**连带修复**:卡玛比率(`年化收益 / 最大回撤`)此前因 max_drawdown 虚高而被压低,修复后恢复正常。其余指标(总收益/年化/夏普/索提诺/波动率/交易数/胜率/盈亏比)经逐一核对**均正确**,不受此 bug 影响。 ### 修复 - **`drawdown_pct` 分母改用逐点峰值**(`src/easy_tdx/backtest/multi_strategy_engine.py` `_build_combined_equity`)—— `drawdown / initial` → `drawdown / peak`(`peak_safe = peak.where(peak != 0, 1.0)` 防除零)。这同时修复 `EquityChart` 回撤曲线显示(前端读 `drawdown_pct` 取负向下画)。 - **回归守卫**(`tests/unit/test_multi_strategy.py::test_max_drawdown_relative_to_peak_not_initial`)—— 构造「净值 1→6→4」的大涨后回撤场景,断言 `max_drawdown ≈ 33%`(旧逻辑会算成 200%,必 >1,断言 `≤1.0` 抓住回归)。 ## [1.17.12] — 2026-07-04 **修复 CI 在新版 FastAPI 上路由注册失败** —— v1.17.11 的 `DELETE /api/v1/strategies/{id}` 用 `status_code=204`,较新 FastAPI/Starlette 在路由注册阶段(`add_api_route`)就抛 `AssertionError: Status code 204 must not have a response body`,导致 CI 的 ubuntu 矩阵(py3.10/3.12/3.13)整片 ERROR(21 个 web 测试因 fixture 导入 router 而连带失败)。改为返回 `200 + {"deleted": id}` 确认体,既消除注册期断言又给前端明确反馈。 ### 修复 - **DELETE 路由不再用 204**(`src/easy_tdx/web/routers/strategies.py`)—— `status_code=204` 改为默认 200,返回 `{"deleted": strategy_id}`;同步更新测试断言(`tests/unit/test_strategy_store.py`)。 ## [1.17.11] — 2026-07-04 **Web UI 新增「策略库」与「多策略组合回测」** —— 此前回测结果存在进程内存,重启即丢,用户无法留存自己反复验证过的好策略。本次落地两层能力:(1) **策略库**——在单标的/组合回测结果区点「保存策略」,把策略 + 标的上下文 + 成绩快照(总收益/夏普/回撤/胜率)一起存进本地 SQLite 单文件(`~/.easy_tdx/strategies.db`),策略库页可载入回填、一键重跑、删除;(2) **多策略组合回测**——策略库勾选 N 个单标的策略,各拿 1/N 资金、各跑原标的(取最新行情),净值曲线按日期并集对齐求和,组合结果复用单标的的 19 项完整绩效指标(基于合并净值曲线 + 汇总成交用 `PerformanceAnalyzer` 算出),并展示各策略当前持仓。**895 单测全绿**(+24 新增),ruff format/check / mypy strict / 前端 vue-tsc 全通过。 ### 新增 - **策略库后端**(`src/easy_tdx/web/strategy_store.py`、`routers/strategies.py`)—— SQLite 单文件 CRUD(加入/列出/查看/删除),落库路径随 `EASY_TDX_CONFIG_DIR` 环境变量走(与 `config.py` 同约定),线程安全(写操作串行锁 + `check_same_thread=False`)。5 个接口:`GET/POST /api/v1/strategies`、`GET/DELETE /strategies/{id}`。保存记录含 strategy + params + context(symbol 或 stocks + 日期 + 周期)+ trade_config + snapshot(成绩快照)+ tags + notes。 - **策略库前端**(`web-ui/src/views/StrategiesView.vue` + 路由 `/strategies` + 导航)—— 卡片网格列表,展示策略名/标的/收益/夏普/回撤/标签/备注/创建时间。「载入」跳转对应回测页并自动回填(单标的剥掉市场前缀只传 6 位代码;组合新增 URL query 回填);「删除」二次确认。空态提示去回测页保存。 - **保存策略按钮**(`BacktestView.vue` / `PortfolioView.vue` 结果区)—— 弹窗填名称/标签/备注,其余(策略参数、标的上下文、成绩快照)自动从当前请求 + 结果填入。 - **多策略组合回测引擎**(`src/easy_tdx/backtest/multi_strategy_engine.py`)—— `MultiStrategyEngine`:N 个策略各拿 1/N 资金、各跑原标的,曲线按日期并集 ffill 对齐求和。输出结构同 `PortfolioResult`(`individual_results` key 形如 `"双均线交叉@SH:601088"`),前端复用组合页图表零改动。 - **多策略组合回测接口**(`web/routers/backtest.py` `POST /backtest/multi-strategy/run/async`)—— 勾选 N 个策略,逐个在 async 上下文取行情 + 构造策略实例(失败跳过),后台线程跑引擎。组合整体绩效基于合并净值曲线 + 汇总成交喂 `PerformanceAnalyzer`,得到与单标的同口径的 19 项指标。 - **策略库组合回测 UI**(`StrategiesView.vue`)—— 每张卡片加复选框(组合策略无单一 symbol 自动 disabled),顶部「组合回测(N)」按钮,结果区复用 `EquityChart` + `MetricTable`(19 项绩效)+ `PortfolioSummaryTable` + `PortfolioCompareChart` + 当前持仓表(各策略回测结束持仓快照)。 ### 变更 - **`PortfolioView.vue` 新增 URL query 回填** —— 此前组合页不读 query,策略库「载入组合策略」无法回填;新增 `onMounted` 读取 `strategy/params/stocks/startDate/endDate/category`,与单标的页回填风格一致。 - **修正多策略合并净值曲线回撤符号** —— `_build_combined_equity` 原用 `drawdown = total - peak`(负值),改为 `peak - total`(正值),与单标的 `PortfolioTracker`、`PerformanceAnalyzer`、`EquityChart`(前端取负向下画)的正值约定一致;否则最大回撤算成 0、夏普/卡玛比率失真。 ### 已知约束(非 bug) - **多策略组合回测仅支持资金分仓(并行制)** —— 每个策略各拿 1/N 资金独立回测后曲线相加;不支持信号共振(投票制,`combo.py` 已有但未暴露 Web API)。资金/成本统一一组均分,不支持每策略单独配置。 - **组合回测结果暂不回存策略库** —— 当前可保存的是单次回测的策略;多策略组合的结果暂未支持存为"策略的组合"。 ## [1.17.10] — 2026-07-04 **Web UI 一键寻优「查看」按钮跳转携带完整行情上下文** —— `/optimize` 页策略排名表的两个「查看」按钮此前跳转只带 `strategy` + `params`,丢失了股票代码、周期、起止日期,导致跳到回测页后用户得手动重选标的与日期才能复现寻优行情。本次让跳转 URL 额外携带 `symbol/startDate/endDate/category`,回测页 `onMounted` 自动回填到 `SymbolPicker` 表单(股票代码/周期/起止日期全部就位),用户只需点「开始回测」即可完整复现。**向后兼容**:老书签(只有 `strategy/params`)仍正常工作,缺失字段保持默认值。前端 `vue-tsc --noEmit` / `vite build` 通过,后端 870 单测全绿(无回归)。 ### 新增 - **SymbolPicker 表单状态双向同步**(`web-ui/src/components/SymbolPicker.vue`)—— `code/category/startDate/endDate` 从私有 `ref` 升级为 `defineModel`(带默认值),父组件可读(拼 URL)可写(回填表单)。`defineExpose({ loadBars, loading })` 保留不动,向后兼容已有调用。 ### 变更 - **「查看」跳转 URL 携带完整上下文**(`web-ui/src/views/OptimizeView.vue`)—— 抽 `buildBacktestQuery(strategyName, params)` 统一构造 query,`onViewParams`(单策略网格排名表)/`onViewAll`(全局策略排名表)两个按钮跳转时附带 `symbol/startDate/endDate/category`。 - **回测页回填标的与日期**(`web-ui/src/views/BacktestView.vue`)—— `onMounted` 新增读取 `route.query.symbol/startDate/endDate/category`,各字段独立 `if` 守卫回填到 `SymbolPicker` v-model 镜像 ref。老 URL 缺失字段保持默认值。 ### 已知约束(非 bug) - **URL query `category` 无白名单校验** —— 与既有 `strategy/params` 读取风格一致,非法值由后端 `/bars` 兜底拒绝;前端 `