Files
ggx/docs/implementation-status.md
T
simon fce725e13c 初始提交:高股息策略研究与回测系统
从 Point-in-Time 股票筛选到统一 Web 前端的完整链路:
筛选 → 画像 → 策略 → 回测 → Walk-forward → 绩效分析 → 报告/前端。

架构
- 数据层与策略层分离;策略代码不写 SQL,只经 data/repo.py 取数
- 所有业务阈值集中在 config/*.yml,代码零硬编码(字段写错直接报错)
- 报告只做「run_id → SQL → 渲染」,不做任何计算,数字可追溯
- 前后端分离:output/ 静态站点 + hdiv web 提供的 REST API

数据安全
- 只增不删:SQL 钩子拦截 DELETE/DROP/TRUNCATE,并有源码扫描测试守护
- qlib 原有表只读,本项目数据写入 hd_ 前缀表
- 回补使用 INSERT IGNORE,保证既有行零改动
- .env 存密钥且已 gitignore;output/、logs/、.venv/ 不入库

交付物
- 30 张 hd_* 表、7 个 YAML 配置、283 项自动化测试
- 统一 Web 前端(hash 路由 SPA)+ nginx 部署配置与 launchd 托管脚本

如实声明的限制
- 策略缺少稳定的样本外超额收益(Walk-forward 7 窗口均值 -0.95%,
  基准 +2.29%);其价值体现在回撤控制,而非超额收益
- 涨跌停/停牌约束仅覆盖 2019 年起;index_weight 尚未填充
- AI Agent 层(plan.md 第四版 P8)未实现

详见 docs/user-guide.md 与 docs/implementation-status.md。
2026-10-03 13:54:56 +08:00

382 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 实施状态报告
> 对应 `docs/development-plan.md`(计划)与 `docs/plan.md`(需求)
> 更新:2026-10-02
---
## 0. 一句话结论
**系统已端到端可运行**:从 Point-in-Time 股票池筛选 → 个股画像 → 策略定义 →
回测 → Walk-forward → 绩效分析 → 参数敏感性 → 固定格式 HTML 报告,
全链路打通并通过 262 项自动化测试。`plan.md §50` 的 14 项验收能力**全部具备**。
数据同步仍在后台补齐最后 ~15%(分红 99.6%、财报 ~87%),
但这不影响系统功能 —— 它只影响股票池的绝对规模。
---
## 1. 已交付清单
### 1.1 数据层(P0a / P0b / G1–G6)
| 缺口 | 状态 | 实测结果 |
|---|---|---|
| **G1 分红明细** | ✅ 完成 | `hd_dividend` 5,880 只 / 253,879 行(其中 56,513 条实施且有现金分红),覆盖 1990–2026 |
| **G2 日线行情** | ✅ 完成 | `stock_daily` 起点 **2015-01-05**(2015 年首个交易日),2,838 个交易日;**2019 年空洞已补齐**(+89 万行) |
| **G2b 复权因子** | ✅ 完成 | `adjust_factor` 同区间,2,840 个交易日 |
| **G3 每日指标** | ✅ 完成 | `daily_basic` 起点 2015-01-05,2,286 个交易日 |
| **G4 扩展财务** | ✅ 完成 | 财务指标 5,903 只(100%)、现金流 5,893、资产负债表/利润表 5,902;公告日齐全率 100%。缺失的 10 只均为 1990 年代退市股(Tushare 无报表) |
| **G5 基准指数** | ✅ 完成 | `hd_index_daily` 7 个指数 / 40,083 行;沪深300 覆盖 2002 起 |
| **G6 停牌/涨跌停** | ✅ 完成 | `hd_suspend` 67,765 行;`hd_limit` **669 万行** |
### 1.2 数据库(30 张 `hd_*` 表)
全部建表完成,结构迁移幂等(连续两次 `ddl apply` 均返回 0 个动作)。
**只增不删**由三层保证:SQL 安全钩子 + 源码扫描测试 + 迁移的前置条件守卫。
### 1.3 代码模块
| 模块 | 文件 | 状态 |
|---|---|---|
| 配置层 | `core/config.py`(7 类配置 + 严格校验 + config_hash) | ✅ |
| 数据安全 | `data/db.py`(SQL 钩子)、`data/ddl.py`(幂等 DDL) | ✅ |
| 单位归一化 | `data/units.py`(万元/万股/百分数 → 元/股/小数) | ✅ |
| PIT 取数 | `data/repo.py`(唯一取数出口) | ✅ |
| 同步器 | `data/sync/`(分红/财报/指数/行情/停牌涨跌停) | ✅ |
| 数据审计 | `data/audit.py`(18 项检查) | ✅ |
| 股票池 | `universe/selector.py` + 4 个 Filter | ✅ |
| 因子 | `factor/dividend_yield.py` | ✅ |
| 个股画像 | `profile/builder.py` | ✅ |
| 策略管理 | `strategy/registry.py` | ✅ |
| 回测引擎 | `backtest/engine.py` | ✅ |
| Walk-forward | `backtest/walk_forward.py` | ✅ |
| 绩效分析 | `analysis/performance.py` | ✅ |
| 敏感性 | `analysis/sensitivity.py` | ✅ |
| 报告渲染 | `report/`(7 类报告 + 离线校验) | ✅ |
---
## 2. `plan.md §50` 十四项验收
| # | 能力 | 状态 | 落库位置 |
|---|---|---|---|
| ① | 找出符合条件的股票 | ✅ | `hd_universe_member` |
| ② | 生成 PIT 股票池 | ✅ | `hd_universe_run` |
| ③ | 每股历史股息率序列 | ✅ | `hd_profile_series` |
| ④ | P10/P25/P50/P75/P90 | ✅ | `hd_profile_stat` |
| ⑤ | 生成买卖信号 | ✅ | `hd_backtest_signal` |
| ⑥ | 执行历史回测 | ✅ | `hd_backtest_run` |
| ⑦ | 正确处理分红与除权 | ✅ | `hd_dividend` + 分红台账(含红利税) |
| ⑧ | 加入交易成本 | ✅ | `hd_backtest_trade.*_cost` |
| ⑨ | K线 + 买卖点 | ✅ | `output/profile_*.html`(四联图 + P75/P25 阈值线) |
| ⑩ | 收益/回撤/Sharpe | ✅ | `hd_backtest_metric` |
| ⑪ | 基准比较 | ✅ | 沪深300 / 中证红利 / 上证指数 |
| ⑫ | Walk-forward | ✅ | `hd_walkforward_window` |
| ⑬ | 样本内/外结果 | ✅ | `hd_backtest_metric.scope` |
| ⑭ | 完整参数与版本 | ✅ | `hd_strategy` + `hd_strategy_param`(36 项) |
---
## 3. 关键正确性保障
以下是开发过程中**真实踩到**并已修复的静默错误 ——
它们共同特点是「不报错,只是给出错误结论」,因此每一条都配了回归测试。
| # | 问题 | 后果 | 修复 |
|---|---|---|---|
| 1 | Tushare `total_mv` 单位是**万元**,配置阈值是**元** | 市值过滤选中 **0 只**股票 | `data/units.py` 统一归一化 + `UNIT` 审计(恒等式 + 绝对量级双判据) |
| 2 | 用**季报累计** ROE 比「年均 8%」阈值 | 几乎所有好公司被误杀 | `annual_financial_averages()` 只用年报口径 |
| 3 | 银行负债率天然 90%+ | 整个金融板块被误杀(招商银行) | `industry_exemptions` 行业豁免,Risk/Quality 统一口径 |
| 4 | 年度分红除权间隔中位数 **366 天** > 365 | 股息率被算成 0,污染历史分位 | TTM 加 45 天宽限期 |
| 5 | 分红除权晚于 asof | 稳定分红公司被误判「连续分红 0 年」(中国神华) | 一年宽限期 |
| 6 | `NaN or 0.0` 返回 `NaN` | 10 万条 NULL 分红被当成数值参与者 | `_fnum()` NaN 感知转换 |
| 7 | 费率 side 配置小写 `sell`、代码传大写 `SELL` | **印花税永远为 0**,成本系统性低估 | 统一大写比较 |
| 8 | 净值曲线漏加现金 | 净值严重失真 | `总市值 = 现金 + 持仓` + 断言测试 |
| 9 | 建仓阶梯与减仓阶梯各自判定 | 持仓时会「因高分位被减仓」,年换手 8.9、持仓仅 30 天 | 合成唯一阶梯 + 死区(修复后:换手 0.91、持仓 345 天) |
| 10 | MySQL 唯一约束**不约束 NULL** | 静默重复行 | 显式 `dedup_key`;三张表补 `NOT NULL`;专门测试守护 |
| 11 | pandas 3.0 字符串列不再是 `object` dtype | 空串 `ts_code` 被写进库 | 改用 `is_numeric_dtype` 判定 |
| 12 | `is_buy` 在涨跌停分支前未定义 | 一旦 `hd_limit` 有数据即崩溃 | 定义提前 |
| 13 | Jinja2 autoescape 转义 `<script>` | 图表静默失效 | `| safe` + 校验器专项检测 |
| 14 | `dividend_records` 的 SELECT **漏了 `base_share`** | 支付率与 FCF 覆盖在全库范围内恒为 NULL —— `max_payout_ratio` 与 `min_fcf_dividend_cover` 两个筛选条件**从未生效** | 补上该列 + 回归测试断言 SELECT 列表 |
| 15 | 用 **FY2023 分红 ÷ 2024Q1 净利润** 算支付率 | 美的集团算出 230.9% 的荒谬支付率(真实 61.6%) | 新增 `Repo.annual_financials()`,支付率/FCF 覆盖一律**同财年**比较 |
| 16 | 画像从未计算分红质量指标 | `dividend_quality` 与 `composite` 安全边际得分恒为 NULL,`plan.md §14/§15` 形同未实现 | 画像复用 `DividendFilter` 的口径函数(单一口径来源) |
| 17 | 断点续传只看「日期是否存在」 | 原 qlib 数据在 2019 年仅 243 只/日被当作「已同步」,形成**整年数据空洞** | 判据改为「当日股票数 ≥ 1500」 |
| 19 | **Jinja2 autoescape 把嵌入 `<script>` 的 JSON 转义成 `&#34;`** | 内联 JS 语法非法 → **全部报告的图表都不显示**(页面能开、容器空白) | `_json_for_script` 返回 `Markup` 并把 `< > & '` 转成 `\uXXXX`;校验器新增 `node --check` 真实语法检查 |
| 20 | 校验器只比对「容器数 == init 次数」 | 图表全坏却判定 OK(给了假信心) | 改为「每个容器 id 必须被脚本引用」+ HTML 实体检测 + node 语法检查 |
| 21 | 索引模板用 `g.items` | Jinja2 中解析成 `dict.items` 方法而非该键 → 索引页渲染失败 | 键名改为 `reports` |
| 22 | `report.yml` 声明了无人实现的 `naming.strategy` | 配置承诺了不存在的产物 | 移除该键(策略说明报告属未实现的 P8) |
| 18 | walk-forward 的 train/test 以 `persist=False` 运行 | `hd_walkforward_window` 的 run_id 是**悬空引用**,报告无法下钻 | 一并落库,并把 `wf_id/window_index` 纳入 run_id 指纹(否则同区间会撞 id 互相覆盖) |
---
## 4. 实测回测结果(2015-01-05 ~ 2026-09-30,11.7 年)
> 策略:`HD_MR_V1` v1.0 —— 市值 ≥ 500 亿、上市 ≥ 10 年、连续分红 ≥ 5 年、
> 股息率 ≥ 3%、年均 ROE ≥ 8%(金融豁免)、买入 ≥ 历史 P75、卖出 ≤ P25、
> 单股 ≤ 10%、行业 ≤ 25%、最多 20 只。
### 4.1 与基准对比
| 指标 | **策略** | 沪深300 | 中证红利 | 上证指数 |
|---|---:|---:|---:|---:|
| 总收益 | **+114.80%** | +19.66% | +53.35% | +14.67% |
| 年化 CAGR | **+6.73%** | +1.54% | +3.71% | +1.17% |
| 最大回撤 | **−21.05%** | −46.70% | −46.51% | −52.30% |
| Sharpe | 0.31 | — | — | — |
**策略跑赢天然基准「中证红利」61 个百分点,而回撤不到其一半。**
> ⚠ **但这个数字有严重误导性,请看 §4.6 的 Walk-forward 结果。**
> 单条路径的全期回测会把「持有可能穿越熊市并在后期回本」的效应放大,
> 而逐年样本外检验给出的是一幅**完全不同的图景**。
> 该结果是在**修正了支付率与 FCF 覆盖两个筛选条件之后**取得的
> (此前这两个条件因 `base_share` 漏选而静默失效)。修正后股票池由 49 只
> 收紧到 28 只,收益反而提高 —— 说明「分红可持续性」这一安全边际条件
> 确实在筛选优质标的。
### 4.2 交易与分红
| 项目 | 数值 |
|---|---:|
| 成交笔数 | 180 |
| 累计现金分红 | 558,797 元(占期初资金 55.9%) |
| 已扣红利税 | 13,676 元 |
| 期末资金 | 2,148,010 元 |
| **资金对账残差** | **0.0000** ✅ |
### 4.3 逐年收益
| 年 | 2019 | 2020 | 2021 | 2022 | 2023 | 2024 | 2025 | 2026 |
|---|---:|---:|---:|---:|---:|---:|---:|---:|
| 策略 | +18.56% | +6.64% | +11.85% | **−3.99%** | +1.25% | +22.22% | +14.67% | +7.32% |
11.7 年中仅 2022 一年为负,且跌幅远小于同期市场。
### 4.4 预热期说明(重要)
**2015–2018 年组合保持 100% 现金、无任何成交**,这是**预期行为**而非缺陷:
滚动 5 年分位参照窗口需要历史分布,而本项目数据自 2015-01-05 起
(2015 年前无行情/指标)。在参照窗口填满之前,系统无法计算「历史分位」,
因此不产生信号 —— 这正是**不做未来函数**的代价与证明:
若为了让 2015 年就有信号而放宽窗口,就等于用不足的样本编造分位。
组合自 2019 年起建仓,2020 年起现金占比稳定在 1% 以下(基本满仓)。
### 4.5 参数敏感性(plan.md §27)
扫描买入分位 P70/75/80/85/90 的结果:
| 买入分位 | CAGR | 最大回撤 | Sharpe | 成交 |
|---|---:|---:|---:|---:|
| P70 | 3.03% | −22.76% | 0.07 | 70 |
| P75 | 5.26% | −17.03% | 0.22 | 72 |
| P80 | 2.77% | −21.37% | 0.05 | 66 |
| P85 | 7.27% | −19.75% | 0.39 | 62 |
| P90 | 3.99% | −19.85% | 0.14 | 55 |
系统判读:CAGR 跨度 4.50pp、最大跳变 4.50pp、**平滑度 0.00**、
检出 1 处尖峰(P85)→ 判定为
**「存在尖峰或跳变,疑似过拟合,请谨慎解读」**。
> 这是一个**诚实的不利结论**,也正是 plan.md §27 想要暴露的问题。
> 需注意该扫描是在**修正支付率/FCF 筛选条件之前**、且区间仅 2021–2024、
> 股票池仅数十只的条件下完成的。**应在当前口径下于 10 年区间重跑后才作数。**
### 4.6 Walk-forward 样本外验证(plan.md §23/§25)
7 个滚动窗口,每个窗口用训练段校准分位分布、测试段**冻结**该分布:
| 窗口 | 训练区间 | 测试区间 | 样本外收益 | 样本外回撤 |
|---|---|---|---:|---:|
| #0 | 2015–2019 | 2020 | **+4.01%** | −11.94% |
| #1 | 2016–2020 | 2021 | **−3.07%** | −15.79% |
| #2 | 2017–2021 | 2022 | **−9.17%** | −24.22% |
| #3 | 2018–2022 | 2023 | **−8.63%** | −18.00% |
| #4 | 2019–2023 | 2024 | **+2.61%** | −24.62% |
| #5 | 2020–2024 | 2025 | **+3.78%** | −10.50% |
| #6 | 2021–2025 | 2026(部分) | **+3.83%** | −14.88% |
**样本外汇总:**
| 指标 | 数值 |
|---|---:|
| 盈利窗口 | 4 / 7(胜率 57.14%) |
| 样本外收益 **均值** | **−0.95%** |
| 样本外收益 中位数 | +2.61% |
| 样本外 CAGR 均值 | −0.78% |
| 最差窗口回撤 | −24.62% |
| **基准收益均值** | **+2.29%** |
| **超额收益均值** | **−3.24%** |
#### 结论:单路径回测显著高估了策略
对比 §4.1 与本节:
| | 全期单路径回测 | Walk-forward 样本外均值 |
|---|---:|---:|
| 收益 | **+114.80%**(11.7 年) | **−0.95%/年** |
| 相对基准 | +95pp | **−3.24pp** |
**这个反差是 Walk-forward 存在的全部意义。** 两者的差异来自方法论而非 bug:
1. **全期回测允许仓位穿越牛熊**:2019–2021 建的仓在 2022–2023 的下跌中继续持有,
到 2024–2026 随市场回升而回本 —— 单条路径把这段「扛过去」的收益完整计入。
2. **Walk-forward 逐年冻结参照分布**:测试年必须用**训练年**校准的股息率分布,
不能自适应。当市场环境切换(如 2022–2023 的估值中枢下移),
训练期校准的阈值在测试期就失灵了。
3. 样本量小:每年只是一个观测点,7 个窗口的均值本身标准误很大
(稳定性指标 −0.16,说明窗口间差异大于均值本身)。
**因此:不要采信 §4.1 的 +114.80%。** 更接近真实的表述是
「该策略在 2020/2024/2025/2026 的样本外为正,在 2021/2022/2023 为负,
长期看与基准相比没有稳定的超额收益,且回撤更小(防御性成立、进攻性不足)」。
> 值得注意的是,策略的**回撤控制**在样本外依然稳定成立
> (最差 −24.62%,而基准同期回撤 −46% ~ −52%),
> 这与「高股息 + 安全边际」的定位一致 —— 它更像一个**降低波动的配置工具**,
> 而非超额收益来源。
## 5. 已知限制(如实声明)
1. **分红与财报已基本完成**(财务指标 5,903/5,903,现金流 5,893/5,903);
指数成分股权重 `index_weight` 仍为空(不影响基准收益计算)。
2. **涨跌停与停牌约束覆盖 2019 年起**;2015-2018 区间为近似建模,
引擎会在 `hd_backtest_run.unimplemented_json` 中如实声明。
(停牌同步器起点为 2019-01-01,如需 2015-2018 可改 `--start` 重跑。)
3. **未实现部分成交**(按信号全额成交,受资金与权重上限约束)。
4. **`index_weight` 为空**:不影响基准收益计算(用指数点位),
仅影响成分股分析。
5. **AI Agent 层(P8)未实现** —— 属 `plan.md` 第四版扩展。
6. **敏感性结论受限于扫描区间与股票池规模**,见 §4.5 说明。
7. **2015-2018 为预热期**(无历史分布可用),见 §4.4 说明。
8. **策略缺少稳定的样本外超额收益**(见 §4.6),这是最重要的结论。
9. **Walk-forward 已具备 7 个滚动窗口**(2015-2019/2020 … 2021-2025/2026),
与 `plan.md §23` 的示例完全一致,train/test 均落库可下钻;
单次完整跑完约需 25 分钟,用 `hdiv backtest --mode walkforward` 执行。
---
## 6. 如何运行
```bash
cd ~/project/高股息回测
export PYTHONPATH=src
# 1) 建表(幂等)
.venv/bin/python -m hdiv ddl apply
# 2) 数据同步(按需)
.venv/bin/python -m hdiv sync dividend --only-missing
.venv/bin/python -m hdiv sync financial --interleaved --only-missing
.venv/bin/python -m hdiv sync index
.venv/bin/python -m hdiv sync trading --start 2019-01-01
HDIV_ALLOW_BACKFILL=1 .venv/bin/python -m hdiv sync backfill # 2015-2018 回补
# 3) 数据审计(含 HTML)
.venv/bin/python -m hdiv audit
# 4) 股票池 → 画像
.venv/bin/python -m hdiv universe --asof 2024-06-28
.venv/bin/python -m hdiv profile --universe-run <run_id>
# 5) 策略登记与回测
.venv/bin/python -m hdiv strategy register
.venv/bin/python -m hdiv backtest --start 2021-01-01 --end 2024-06-28
.venv/bin/python -m hdiv backtest --mode walkforward
.venv/bin/python -m hdiv sensitivity
# 6) 校验报告离线可用性
.venv/bin/python -m hdiv report validate
# 7) 测试
.venv/bin/python -m pytest tests/ -q
```
报告输出在 `output/`,从 `output/index.html` 进入。
---
## 7. 可修改的配置
| 文件 | 控制什么 |
|---|---|
| `config/universe.yml` | **股票筛选条件**(市值/上市年限/连续分红/股息率/ROE/行业豁免…) |
| `config/profile.yml` | **个股特性**(统计窗口/分位/波动频率/安全边际权重/TTM 口径) |
| `config/strategy/high_dividend_v1.yml` | **策略定义**(买卖分位/建仓阶梯/仓位上限/风控/生命周期状态) |
| `config/cost.yml` | 佣金/印花税/过户费/滑点/红利税 |
| `config/backtest.yml` | 区间/调度/分位参照口径/Walk-forward/基准/撮合 |
| `config/report.yml` | 图表开关/输出命名/版面/资源模式 |
| `config/datasource.yml` | 数据库/只读白名单/回补许可/Tushare 限频 |
修改任一文件后 `config_hash` 变化,历史 run 仍可完整复现。
---
## 7.1 报告部署(重要)
报告通过**相对路径**引用图表库:``<script src="assets/echarts.min.js">``。
因此发布时必须**整体部署 `output/` 目录**,包括 `output/assets/`。
例如把 `output/` 映射到 `/ggx/`,则下列两者都必须可达:
```text
http://<host>:8080/ggx/index.html ← 报告
http://<host>:8080/ggx/assets/echarts.min.js ← 图表库(约 1 MB)
```
若只复制了 `*.html`,页面能打开但**所有图表都不会显示**。
若你的部署方式无法附带 `assets/` 目录,改用自包含模式:
```yaml
# config/report.yml
asset_mode: inline # 每份 HTML 内嵌图表库,体积由约 10MB 增至约 79MB
```
改完重新生成报告即可(`hdiv report index` 或对应报告命令)。
**部署前自检**(会真实执行 `node --check` 校验每份报告的内联 JS):
```bash
hdiv report validate # 或:python -m hdiv.report.validate output
```
## 7.2 Web 前端(前后端分离)
系统提供统一 Web 前端,替代原先平铺的静态报告:
| 能力 | 实现 |
|---|---|
| 统一入口 | `output/index.html`(hash 路由单页应用) |
| 股票池记录管理 | 列表 / **命名** / **归档** / **软删除** / 打开股票清单 |
| 个股画像 | 点击清单里的个股直接打开画像页(四联图 + 分位 + 雷达) |
| 股票池 ↔ 回测 | `hdiv backtest --universe-run <run_id>` 建立关联,双向可见 |
| 回测主页面 | 只显示**条件与说明**,点击进入详细结果 |
| 详细结果 | 总结果 + 净值曲线 + 绩效指标 + **逐笔成交与理由** |
| 目录归一化 | `hdiv site normalize` 归档历史、静态报告入 `reports/` |
**后端用 Python 标准库实现**(`ThreadingHTTPServer`):零新增依赖、
一条命令启动、nginx 只需反代 `/api`,无版本漂移风险。
部署:`deploy/nginx.conf.example`(两种布局)+ `deploy/serve.sh`(启停脚本)。
## 8. 测试覆盖
```
262 passed
```
| 测试文件 | 覆盖 |
|---|---|
| `test_config.py` | 配置正向加载 + 18 类非法配置必须被拒 |
| `test_safety.py` | SQL 安全钩子(含 11 类删除语句、只读白名单、前缀约束);源码扫描无删除语句、无 qlib import |
| `test_schema.py` | 30 张表结构、前缀、幂等性、**唯一键列不得可空**(NULL 绕过唯一约束) |
| `test_sync.py` | 单位转换、NaN→NULL、分红去重键、财报 PIT 丢弃、限频器 |
| `test_universe.py` | **单位换算与量级检测**、行业豁免、年报均值口径、分红宽限期、滤网索引契约 |
| `test_backtest.py` | 成本模型(含印花税)、A股整手、资金对账、目标仓位阶梯与死区、参数耦合、敏感性判读、Walk-forward 窗口、无未来函数 |
| `test_cli_contract.py` | CLI 与使用手册的接口契约(含 `web`/`site` 命令、`--universe-run`) |
| `test_web.py` | 前端↔后端接口契约、软删除可逆性、归档可见性、资源路径重写、JSON 可序列化 |