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

20 KiB
Raw Blame History

实施状态报告

对应 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> 图表静默失效 `
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. 如何运行

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/,则下列两者都必须可达:

http://<host>:8080/ggx/index.html                  ← 报告
http://<host>:8080/ggx/assets/echarts.min.js       ← 图表库(约 1 MB)

若只复制了 *.html,页面能打开但所有图表都不会显示。

若你的部署方式无法附带 assets/ 目录,改用自包含模式:

# config/report.yml
asset_mode: inline     # 每份 HTML 内嵌图表库,体积由约 10MB 增至约 79MB

改完重新生成报告即可(hdiv report index 或对应报告命令)。

部署前自检(会真实执行 node --check 校验每份报告的内联 JS):

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 可序列化