初始提交:高股息策略研究与回测系统

从 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。
This commit is contained in:
2026-10-03 13:54:56 +08:00
commit fce725e13c
127 changed files with 28001 additions and 0 deletions
+864
View File
@@ -0,0 +1,864 @@
# 高股息回测系统 · 开发计划
> 版本:V1.0
> 日期:2026-10-02
> 依据:`docs/plan.md`(A股投资策略制定与回测方案 V1.0)+ 用户补充要求(数据源、YAML 配置、DB 优先、HTML 图表)
> 数据基座:`~/project/qlib`(本机 MariaDB `127.0.0.1:3306/qlib` + Tushare Pro)
> **状态:关键决策 D1–D7 已于 2026-10-02 确认(见 §7)。**
> **实施进展见 [`docs/implementation-status.md`](implementation-status.md)** ——
> 系统已端到端可运行,`plan.md §50` 的 14 项验收能力全部具备,195 项自动化测试通过。
---
# 0. 结论摘要
1. **qlib 已提供可直接使用的数据基座**:行情、复权因子、每日指标(含 `dv_ttm` 股息率)、PIT 财务指标(带 `announce_date`)、股票基本信息(含 338 只退市股)、ST 改名历史、交易日历。共 20 张表、约 2300 万行。
2. **有 6 个数据缺口必须先补齐**(见 §1.3),其中「**无分红明细表**」与「**行情仅覆盖 2019 起**」是硬阻塞项,P0a/P0b 阶段解决。
3. **Tushare 权限已实测全部可用**:`dividend` / `fina_indicator` / `cashflow` / `balancesheet` / `income` / `daily` / `adj_factor` / `daily_basic` / `index_daily` / `index_weight` / `suspend_d` / `stk_limit` 均返回 `code=0`。
4. **架构口径**:新建独立 Python 项目,**只以 qlib 的数据库为共享数据层**,不 import qlib 后端代码,qlib 项目**零代码改动**;所有新表统一 `hd_` 前缀,**只增不删**。
5. **配置外置**:筛选条件、个股特性、策略、成本、回测、报告全部 YAML 化(`config/`),代码零硬编码阈值。
6. **DB 优先**:新增 30 张 `hd_*` 表承载股票池、画像、策略、回测、交易、绩效、敏感性、报告元数据;JSON 仅用于策略归档快照与报告上下文。
7. **图表固定格式**:Jinja2 模板 + 内置 ECharts(离线、无 CDN),输入一律是 `run_id` → DB 查询,输出固定命名 HTML。
8. **实施节奏**:先用**现有 2020 起数据**跑通端到端(P0a),历史回补 2015-2018(P0b)后台长跑不阻塞开发。
---
# 1. 数据资产勘察结论
## 1.1 qlib 现有表盘点(实测)
| 表 | 行数 | 时间范围 | 关键结论 |
|---|---:|---|---|
| `stock` | 6,041 | 上市 1990-12 ~ 2026-09 | 5,903 只,**含 338 只已退市**,有 industry/area/market/exchange |
| `stock_daily` | 7,688,126 | **2019-01-02** ~ 2026-09-04 | 5,786 只;仅 2019 起 |
| `adjust_factor` | 7,922,475 | **2019-01-02** ~ 2026-09-04 | 复权因子,与行情同区间 |
| `daily_basic` | 7,717,819 | **2020-01-02** ~ 2026-09-18 | pe/pe_ttm/pb/ps/ps_ttm/**dv_ratio/dv_ttm**/total_mv/circ_mv |
| `financial_indicator` | 327,138 | 1990-06 ~ 2026-06 | **仅 6 个指标**(eps/roe/total_revenue/net_profit/gross_margin),但带 `announce_date` → PIT 可用 |
| `stock_name_history` | 13,962 | 1990 ~ | 含 **2,831 条 ST/\*ST 记录**,可按日期还原历史 ST 状态 |
| `trading_calendar` | 10,227 | 2000-01 ~ 2027-12 | 6,787 个交易日;2015-2018 有 975 个交易日 |
| `index_weight` | **0** | — | **空表**(成分股权重未同步) |
| `sync_log` | 19,849 | — | qlib 自身同步台账 |
| `strategy` / `experiment` / `selection_result` / `signal_event` / `job` / `global_config` | 少量 | — | qlib 自身研究平台表,**不复用、不写入** |
## 1.2 可复用资产
- **Point-in-Time 财务**:`financial_indicator.announce_date` 即公告日,可直接做 `announce_date <= asof` 纪律。
- **生存者偏差**:`stock.delist_date` + `stock_name_history` + 全量退市股,可还原任意历史交易日的真实股票池。
- **ST 过滤**:`stock_name_history` 的 `name` 含 `ST/*ST` 且带 `start_date/end_date`,支持历史时点判定(而非仅看当前名)。
- **估值面板**:`daily_basic` 已是宽表因子面板,PE/PB/PS/股息率/市值可直接取用。
- **交易日历**:覆盖 2015-2027,回测排期无需自建。
- **数据源**:`.env` 中 Tushare token 有效,且**用户已授权全权使用**。
## 1.3 数据缺口(P0 阻塞项)
| 编号 | 缺口 | 影响 | 解决方式 |
|---|---|---|---|
| **G1** | **无分红明细表**(现金分红/送转/除权除息/股权登记日/派息日) | plan §14/§30 分红质量、分红再投资、正确复权全部无法实现 | 新建 `hd_dividend`,Tushare `dividend` 全量回补 |
| **G2** | 行情 `stock_daily`/`adjust_factor` 仅 2019 起 | 无法做 plan §22「10 年」与 §23「train 5~8 年」的 Walk-forward | 回补 2015-01-01 ~ 2018-12-31(约 975 交易日 × ~3,500 只 ≈ 340 万行) |
| **G3** | `daily_basic` 仅 2020 起 | 2020 前无 PE/PB/股息率/市值 | 回补 2015-01-01 ~ 2019-12-31 |
| **G4** | 财务指标仅 6 项 | 缺 ROIC / FCF / 资产负债率 / 经营现金流 / 分红支付率 / 利息保障 | 新建 `hd_fina_indicator`、`hd_cashflow`、`hd_balancesheet`、`hd_income`,全历史回补 |
| **G5** | 无指数行情,`index_weight` 空 | plan §28.4 基准比较(沪深300/中证红利/上证)不可用 | 新建 `hd_index_daily`,回补 `000300.SH`/`000922.CSI`/`000001.SH` 等;写入 `index_weight` 成分股 |
| **G6** | 无停牌 / 涨跌停表 | 交易可行性约束只能近似(plan §29 成本与成交现实性) | 新建 `hd_suspend`、`hd_limit`,回补 `suspend_d` / `stk_limit` |
## 1.4 一次实测的接口可用性(全部 `code=0`)
```text
dividend(600036.SH) 75 条 ├─ 含 div_proc/ann_date/ex_date/pay_date/cash_div_tax
fina_indicator 8 条 ├─ roe/roic/grossprofit_margin/debt_to_assets/current_ratio
cashflow / balancesheet / income ok
daily(2015) ok ├─ 可回补 G2
index_daily(2015) ok ├─ 可回补 G5
index_basic ok ├─ 中证红利 = 000922.CSI(2008-05-26 发布)
suspend_d / stk_limit ok ├─ 可回补 G6
```
---
# 2. 总体架构
## 2.1 目录结构
```text
高股息回测/
├── docs/
│ ├── plan.md # 已有(需求原文)
│ ├── development-plan.md # 本文件
│ ├── data-dictionary.md # 表结构 + 字段口径(随开发更新)
│ ├── methodology.md # 因子/信号/成本口径说明
│ └── decisions/ADR-*.md # 关键决策记录
├── config/ # ★ 全部可调参数(用户可直接改)
│ ├── datasource.yml # 数据库 + Tushare
│ ├── universe.yml # ★ 股票筛选条件
│ ├── profile.yml # ★ 个股特性配置
│ ├── cost.yml # 交易成本模型
│ ├── backtest.yml # 回测区间 / Walk-forward / 基准
│ ├── report.yml # 图表与输出命名
│ └── strategy/
│ ├── _schema.yml # 策略字段校验口径(文档用)
│ └── high_dividend_v1.yml # ★ 策略定义(版本化)
├── src/hdiv/
│ ├── core/ # 配置加载 + pydantic 校验 + 日志 + 版本指纹
│ ├── data/
│ │ ├── db.py # 连接管理;只读表白名单 + 只写 hd_ 前缀双重校验
│ │ ├── models.py # hd_* SQLAlchemy 模型
│ │ ├── ddl.py # 建表 / 增量加列(幂等,无 DROP)
│ │ ├── repo.py # Point-in-Time 查询层(唯一取数出口)
│ │ └── sync/ # Tushare 同步器(按 API 一文件)
│ ├── universe/ # filters/(market/risk/dividend/quality)+ selector
│ ├── factor/ # dividend_yield / valuation / quality / 面板工具
│ ├── profile/ # dividend / valuation / quality / risk / score
│ ├── strategy/ # base / registry / dsl / rules / high_dividend
│ ├── backtest/ # engine / portfolio / execution / cost / dividend / benchmark / walk_forward
│ ├── analysis/ # performance / risk / trade / sensitivity / attribution
│ ├── report/ # renderer(Jinja2)+ 各报告装配器
│ └── cli.py # 统一命令行入口
├── templates/ # HTML 模板(固定格式)
│ ├── base.html partials/ charts/ reports/
├── assets/ # 内置 echarts.min.js(离线)
├── output/ # 生成的 HTML 报告(固定命名)
├── sql/ # 建表 SQL(可读副本,便于人工审查)
├── tests/
├── .env # MYSQL_PASSWORD / TUSHARE_TOKEN(不提交)
├── .env.example
└── pyproject.toml
```
## 2.2 分层与边界
```text
config/*.yml ──► core.config(Schema 校验)──► 全模块只读引用
│
Tushare ──► data.sync ──► MariaDB(qlib) ◄─────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
data.repo(PIT) hd_* 结果表 hd_report 元数据
(唯一取数出口) ▲ │
│ │ │
universe ─► factor ─► profile ─► strategy ─► backtest ─► analysis
│
report.renderer
▼
output/*.html
```
**三条硬边界**
1. **数据层 ≠ 策略层**:策略代码不允许写 SQL,只能调 `data.repo`。
2. **策略 ≠ 回测引擎**:策略只产出信号(含 `reason_json`),引擎只执行信号。
3. **图表 ≠ 业务**:`report` 只做「`run_id` → SQL → 渲染」,不参与任何计算。
## 2.3 数据库策略
- 库:沿用本机 MariaDB `127.0.0.1:3306`,schema `qlib`。
- **只读表白名单**:`stock` / `stock_daily` / `adjust_factor` / `daily_basic` / `financial_indicator` / `stock_name_history` / `trading_calendar` / `index_weight`。
`db.py` 在 SQLAlchemy 事件层对白名单表**拦截 UPDATE/DELETE**(防误写)。
- **只写前缀**:项目自身只能创建/写入 `hd_` 前缀表;`ddl.py` 拒绝任何 `DROP`/`TRUNCATE`/无 `hd_` 前缀的 DDL。
- **禁止删除**:全区无 `DELETE` 语句(架构扫描 + 单测断言);同步采用 `INSERT ... ON DUPLICATE KEY UPDATE` 幂等 upsert。
- **例外(需确认,见 §7-D2)**:G2/G3 回补写入 qlib 现有 `stock_daily` / `daily_basic` / `adjust_factor`(**纯新增历史区间,不覆盖既有行**),`index_weight` 空表填充。
---
# 3. 数据库设计(新增 30 张 `hd_*` 表)
## 3.1 表清单
**A. 数据同步层(P0)**
| 表 | 用途 | 主键/唯一键 |
|---|---|---|
| `hd_dividend` | 分红送转全明细(G1) | `(symbol, end_date, div_proc, ann_date)` |
| `hd_fina_indicator` | 扩展财务指标(G4) | `(symbol, end_date, ann_date)` |
| `hd_cashflow` | 经营/投资/筹资现金流、FCF(G4) | `(symbol, end_date, ann_date)` |
| `hd_balancesheet` | 资产/负债/权益(G4) | `(symbol, end_date, ann_date)` |
| `hd_income` | 营收/净利润/归母净利润(G4) | `(symbol, end_date, ann_date)` |
| `hd_index_daily` | 基准指数行情(G5) | `(index_code, trade_date)` |
| `hd_suspend` | 停牌记录(G6) | `(symbol, trade_date)` |
| `hd_limit` | 涨跌停价(G6) | `(symbol, trade_date)` |
| `hd_sync_log` | 本项目同步台账 | `id` |
| `hd_data_audit` | 数据完整性问题清单 | `id` |
**B. 股票池层(P1)**
| 表 | 用途 |
|---|---|
| `hd_universe_run` | 一次筛选运行:配置快照 + hash + asof + 统计 |
| `hd_universe_member` | 逐股逐滤网的通过位 + 失败原因 + 命中值(长表,可回溯「为什么没选上」) |
**C. 因子与画像层(P2)**
| 表 | 用途 |
|---|---|
| `hd_factor_snapshot` | **仅在决策时点**落因子值(避免 1.5 亿行日频面板,见 §3.2) |
| `hd_profile_run` | 画像运行:窗口配置 + hash + asof |
| `hd_profile_stat` | 逐股逐指标的 min/max/mean/median/std/**P10/P25/P50/P75/P90** + 当前值 + 当前分位 |
| `hd_profile_series` | 逐股逐指标的时间序列(供 HTML 画图,仅筛选后股票,体积可控) |
| `hd_profile_score` | 安全边际分项得分(股息率/估值/财务/资产负债/分红质量) |
**D. 策略层(P3)**
| 表 | 用途 |
|---|---|
| `hd_strategy` | 策略身份:id/name/version/status(YAML 生命周期枚举)/yaml_path/config_json/config_hash/parent_id |
| `hd_strategy_param` | 参数扁平化(path → value),便于跨版本 diff 与敏感性扫描 |
**E. 回测层(P4/P5)**
| 表 | 用途 |
|---|---|
| `hd_backtest_run` | 运行头:strategy_id/version、config_hash、**data_version**、区间、mode、cost_json、code_version |
| `hd_backtest_equity` | 逐日净值曲线:nav/cash/持仓市值/日收益/回撤/基准点位/基准净值 |
| `hd_backtest_position` | 逐日持仓:数量/成本/市值/权重/持有天数 |
| `hd_backtest_trade` | 逐笔成交:**signal_date / execution_date / side / price / quantity / commission / stamp_tax / transfer_fee / slippage_cost / realized_pnl / holding_days / reason_json** |
| `hd_backtest_signal` | 逐信号:类型/目标权重/触发值/分位/`reason_json`/**executed + skip_reason** |
| `hd_backtest_metric` | 指标长表:`scope`(all/train/test/window_k) × `metric_code` × `benchmark_code` |
| `hd_walkforward_run` | WF 运行头 |
| `hd_walkforward_window` | 每个窗口:train/test 区间 + 冻结参数 + 对应 train/test run_id |
**F. 分析层(P6)**
| 表 | 用途 |
|---|---|
| `hd_sensitivity_run` | 敏感性扫描头:基准策略 + 扫描网格 |
| `hd_sensitivity_point` | 每点:param_path/param_value/run_id + CAGR/MDD/Sharpe/Calmar/Turnover |
**G. 报告层(P7)**
| 表 | 用途 |
|---|---|
| `hd_report` | 报告元数据:type/html_path/title/context_json/**sql_provenance_json**/source_run_ids/generated_at |
## 3.2 关键设计决策:为什么不做全量日频因子面板
若建 `hd_factor_daily(symbol, trade_date, factor_code, value)`:
`5,874 × 2,700 交易日 × 12 因子 ≈ 1.9 亿行`,成本与收益不成比例——因为 `daily_basic` **本身已是宽表因子面板**。
**方案**:
- 估值类因子(PE/PB/PS/股息率/市值)→ **不落库**,由 `data.repo` 从 `daily_basic` 直接取。
- 需要新算的因子(PIT-TTM 每股分红、修正股息率、分位、z-score)→ **只在决策时点计算**,落 `hd_factor_snapshot`(asof_date 粒度)。
- 需要在图上展示的长序列 → 只对**已筛选入池的股票**(数百只)落 `hd_profile_series`。
估算:`hd_factor_snapshot` 若按月 × 200 只 × 15 因子 × 10 年 ≈ 36 万行;`hd_profile_series` ≈ 200 × 2,500 × 6 ≈ 300 万行。均可接受。
## 3.3 关键表 DDL 要点
```sql
-- 分红明细:PIT 语义核心表
CREATE TABLE hd_dividend (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
symbol VARCHAR(12) NOT NULL,
end_date DATE NOT NULL COMMENT '分红对应报告期',
ann_date DATE NULL COMMENT '公告日(预案)',
imp_ann_date DATE NULL COMMENT '实施公告日 ← PIT 关键',
div_proc VARCHAR(16) NOT NULL COMMENT '预案/股东大会通过/实施/取消',
stk_div DECIMAL(16,6) NULL COMMENT '每股送转股',
cash_div DECIMAL(16,6) NULL COMMENT '每股税后现金分红',
cash_div_tax DECIMAL(16,6) NULL COMMENT '每股税前现金分红',
record_date DATE NULL, ex_date DATE NULL, pay_date DATE NULL, div_listdate DATE NULL,
base_share DECIMAL(24,4) NULL,
source VARCHAR(16) NOT NULL DEFAULT 'tushare',
created_at DATETIME NOT NULL,
UNIQUE KEY uq_hd_div (symbol, end_date, div_proc, ann_date),
KEY ix_hd_div_ex (symbol, ex_date),
KEY ix_hd_div_pit (symbol, imp_ann_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 成交明细:reason_json 为 AI 可解释性的关键(plan §32)
CREATE TABLE hd_backtest_trade (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
run_id VARCHAR(32) NOT NULL, trade_id VARCHAR(32) NOT NULL,
symbol VARCHAR(12) NOT NULL,
signal_date DATE NOT NULL, execution_date DATE NOT NULL,
side VARCHAR(4) NOT NULL,
price DECIMAL(16,4) NOT NULL, quantity DECIMAL(20,4) NOT NULL, amount DECIMAL(24,4) NOT NULL,
commission DECIMAL(16,4) NOT NULL DEFAULT 0,
stamp_tax DECIMAL(16,4) NOT NULL DEFAULT 0,
transfer_fee DECIMAL(16,4) NOT NULL DEFAULT 0,
slippage_cost DECIMAL(16,4) NOT NULL DEFAULT 0,
realized_pnl DECIMAL(24,4) NULL, holding_days INT NULL,
reason_json TEXT NULL,
created_at DATETIME NOT NULL,
UNIQUE KEY uq_hd_trade (run_id, trade_id),
KEY ix_hd_trade_run (run_id, symbol, execution_date)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
## 3.4 写入纪律(禁止删除)
| 机制 | 实现 |
|---|---|
| 无 DELETE | 全代码库禁止 `DELETE FROM`、`session.delete`、`DROP`、`TRUNCATE`;CI 单测正则扫描源码断言 |
| 幂等重复运行 | 结果表以 `run_id` 为分区键;**重跑生成新 run_id**,旧 run 保留 |
| 同步更新 | `INSERT ... ON DUPLICATE KEY UPDATE`(仅更新数据列,不动主键) |
| DDL | `ddl.py` 只做 `CREATE TABLE IF NOT EXISTS` 与 `ALTER TABLE ... ADD COLUMN`(幂等、可回滚) |
| 回补保护 | 回补前先 `SELECT COUNT(*)` 记录基线,回补后写 `hd_sync_log`(前后行数差可审计) |
---
# 4. 配置文件设计(全部可被你直接修改)
> 约定:`config/*.yml` 不含任何密钥;密钥经 `*_env` 字段引用根目录 `.env`。
> 所有配置在加载时经 pydantic 严格校验,**字段名写错直接报错**,不静默取默认值。
## 4.1 `config/datasource.yml`
```yaml
version: 1
database:
host: 127.0.0.1
port: 3306
db: qlib
user: qlib
password_env: MYSQL_PASSWORD # 密钥只在 .env
charset: utf8mb4
pool_size: 8
read_only_tables: # 白名单:拦截 UPDATE/DELETE
- stock
- stock_daily
- adjust_factor
- daily_basic
- financial_indicator
- stock_name_history
- trading_calendar
- index_weight
write_prefix: hd_
forbid_delete: true
tushare:
token_env: TUSHARE_TOKEN
api_url: http://api.tushare.pro
rate_limit_per_min: 400
retry: 3
retry_backoff_sec: 2
timeout_sec: 60
```
## 4.2 `config/universe.yml` ★ 股票筛选条件
```yaml
version: 1
name: high_dividend_universe
evaluation:
mode: point_in_time # point_in_time | latest
asof: latest # mode=latest 时生效
market: # MarketFilter
exchanges: [SZSE, SSE] # 需要北交所时加 BSE
markets: [主板, 创业板, 科创板]
min_listing_years: 10
min_market_cap: 50000000000 # 元(500 亿)
min_float_market_cap: null
min_avg_amount_20d: 20000000 # 元,流动性
require_trading_on_asof: true
risk: # RiskFilter
exclude_st: true
st_lookback_from_history: true # 用 stock_name_history 判历史 ST
exclude_delisting: true
exclude_suspended: true
exclude_negative_equity: true
max_debt_to_assets: 0.80
max_pledge_ratio: null
exclude_major_litigation: false # 需额外数据源,暂关
dividend: # DividendFilter
yield_source: computed # computed(PIT 自算) | dv_ttm(daily_basic)
min_dividend_yield: 0.03
min_continuous_years: 5
continuous_rule: cash_div_tax_gt_0 # 以税前现金分红>0 计一次
window_years: 6
min_dividend_years_in_window: 5
max_payout_ratio: 1.00
min_payout_ratio: null
min_dps_cagr_5y: null
require_positive_fcf: true
min_fcf_dividend_cover: 1.0
quality: # FinancialQualityFilter
min_roe_5y_avg: 0.08
min_roic_5y_avg: null
min_gross_margin: null
min_net_margin: null
min_ocf_to_profit: 0.60
max_debt_to_assets: null # 覆盖 risk 段设置(null=不覆盖)
pit_rule: announce_date_le_asof
output:
min_members: 10 # 少于则 WARN
max_members: 200
warn_on_empty: true
```
## 4.3 `config/profile.yml` ★ 个股特性配置
```yaml
version: 1
name: default_profile
windows_years: [5, 8, 10]
min_obs_days: 500
metrics:
valuation: [dv_yield, pe_ttm, pb, ps_ttm]
financial: [roe, roic, revenue, net_profit, ocf, fcf,
debt_to_assets, gross_margin, net_margin, interest_cover]
dividend: [dps, payout_ratio, fcf_dividend_cover,
dividend_continuity_years, dps_cagr, dps_volatility]
risk: [vol_60d, vol_250d, max_drawdown_3y, beta_300, avg_amount_20d]
return: [ret_1y, ret_3y, ret_5y, cagr_5y]
percentiles: [10, 25, 50, 75, 90]
dividend_yield_volatility:
daily_window: 250
monthly_window: 60
quarterly_window: 20
annual_window: 10
safety_margin:
mode: separate # separate(分项展示,plan §15 第一版) | composite
weights:
dividend_yield: 0.30
valuation: 0.25
financial_quality: 0.25
balance_sheet: 0.10
dividend_quality: 0.10
score_bounds: [0, 100]
```
## 4.4 `config/strategy/high_dividend_v1.yml` ★ 策略定义
```yaml
strategy:
id: HD_MR_V1
name: high_dividend_mean_reversion
version: "1.0"
status: DRAFT # DRAFT|RESEARCH|BACKTEST|VALIDATED|PAPER|LIVE|ARCHIVED
description: 高股息 + 安全边际 + 估值均值回归
universe:
ref: config/universe.yml
override: {} # 就地覆盖筛选条件,如 {dividend: {min_dividend_yield: 0.035}}
entry:
yield_percentile: 75
require_universe_pass: true
require_risk_pass: true
scale_in: # 分批建仓;留空 => 一次性建满
- { percentile: 75, weight: 0.25 }
- { percentile: 80, weight: 0.50 }
- { percentile: 85, weight: 0.75 }
- { percentile: 90, weight: 1.00 }
exit:
yield_percentile: 25
scale_out:
- { percentile: 50, weight: 0.50 }
- { percentile: 25, weight: 0.00 }
stop_loss_pct: null # 第一版不加复杂止损(plan §21)
max_holding_days: null
position:
max_position: 0.10
min_position: 0.01
sector_max_position: 0.25
max_holdings: 20
weight_scheme: equal # equal | score | inverse_vol
risk:
max_portfolio_drawdown: 0.20
max_single_drawdown: 0.25
liquidity_limit_pct_adv: 0.05
execution:
signal_to_execution: next_open # next_open | next_close | same_close
limit_up_down_rule: skip
suspended_rule: defer
cost:
ref: config/cost.yml
```
## 4.5 `config/cost.yml`
```yaml
version: 1
model: a_share_default
commission: { rate: 0.00025, min: 5.0, side: both }
stamp_duty: { rate: 0.0005, side: sell }
transfer_fee: { rate: 0.00001, side: both }
slippage:
mode: bps # bps | fixed | tick
value: 10 # 10 bps
dividend_tax:
enabled: true
holding_based: true # 持股 > 1 年免税
rates: { "1m": 0.20, "1y": 0.10, "gt1y": 0.00 }
```
## 4.6 `config/backtest.yml`
```yaml
version: 1
capital: { initial: 1000000, currency: CNY }
period: { start: 2015-01-01, end: latest }
walk_forward:
enabled: true
scheme: rolling # rolling | expanding
train_years: 5
test_years: 1
step_months: 12
min_train_years: 5
dividend:
cash_mode: reinvest # reinvest | hold | cash_out
reinvest_rule: same_stock_next_open
apply_dividend_tax: true
benchmark:
- { code: 000300.SH, name: 沪深300 }
- { code: 000922.CSI, name: 中证红利 }
- { code: 000001.SH, name: 上证指数 }
fill:
price: next_open
partial_fill: false
max_volume_pct: 0.05
reproducibility:
record_code_version: true
record_data_version: true
seed: 42
```
## 4.7 `config/report.yml`
```yaml
version: 1
templates_dir: templates
output_dir: output
theme: light # light | dark | auto
offline_assets: true # 内置 ECharts,禁止 CDN
include_sql_provenance: true # HTML 内嵌数据来源 SQL + run_id
charts:
kline_signals: true
equity_curve: true
drawdown: true
yield_percentile: true
yearly_returns: true
monthly_heatmap: true
rolling_metrics: true
sensitivity_heatmap: true
sector_exposure: true
naming:
index: "index.html"
backtest: "backtest_{run_id}.html"
walkforward: "walkforward_{wf_id}.html"
sensitivity: "sensitivity_{sens_id}.html"
profile: "profile_{symbol}_{asof}.html"
universe: "universe_{asof}.html"
audit: "data_audit_{date}.html"
```
---
# 5. HTML 图表固定输出规范
## 5.1 固定约定
| 项 | 规范 |
|---|---|
| 渲染 | Jinja2 模板 + 内置 `assets/echarts.min.js`,**单文件自包含**,断网可开 |
| 输入 | 只接收 `run_id` / `symbol` / `asof` 等标识,内部走 `data.repo` 查询 |
| 数据 | 页面内嵌 `window.__DATA__ = {...}` JSON,由服务端从 DB 查询注入 |
| 命名 | 严格按 `report.yml:naming`,可预测路径 |
| 溯源 | 页脚展示 `run_id / config_hash / data_version / code_version / 生成时间 / 数据截止日` |
| 导航 | `output/index.html` 汇总所有报告卡片 |
| 禁止 | 模板内不做任何指标计算(计算全在 `analysis/`,已落库) |
## 5.2 报告清单与图表
| 报告 | 布局 | 图表 |
|---|---|---|
| **数据审计** `data_audit_*.html` | 缺口矩阵 + 覆盖率表 | 覆盖率柱图、缺口时间轴 |
| **股票池** `universe_*.html` | 成员表 + 逐股滤网命中 | 行业分布、市值分布、股息率分布、逐股滤网瀑布 |
| **个股画像** `profile_*_*.html` | 头部指标卡 + 分位带 | **K线+股息率+PE+PB+回撤四联图**、股息率历史分布直方图、分位带、财务趋势 |
| **回测** `backtest_*.html` | KPI 卡 + 净值 + 交易表 | 净值曲线(含基准)、回撤、持仓权重、月度热力图、**个股 K线+买卖点**、行业暴露 |
| **Walk-forward** `walkforward_*.html` | 窗口表 | 样本内/外指标对比、窗口拼接净值、参数稳定性 |
| **敏感性** `sensitivity_*.html` | 扫描表 | 参数热力图、CAGR/Sharpe 对参数曲线 |
> **个股画像的「K线 + 买卖点」** 直接对齐 `plan.md §33` 的排版:K线 → 股息率 → PE → PB → 回撤,四联共享 X 轴。
---
# 6. 开发阶段计划
> 与 `plan.md §49` 的 P0–P11 对齐;难度/依赖并列。
## P0a · 基础设施 + 最小可用数据集
> 决策 D3:先建立「2020 年起可端到端跑通」的数据条件,不等待历史回补。
**交付**
1. `pyproject.toml` + venv(Python 3.12、pandas、numpy、SQLAlchemy 2.0、pymysql、pydantic v2、PyYAML、Jinja2、pyarrow、tqdm)
2. `core/config.py`:YAML → pydantic 严格校验,生成 `config_hash`
3. `data/db.py`:连接池 + **只读表白名单拦截** + **只写 `hd_` 前缀断言** + 无 DELETE 保证
4. `data/ddl.py`:30 张 `hd_*` 表幂等建表(`CREATE TABLE IF NOT EXISTS`),**不注册进 qlib 的 Alembic**
5. `data/sync/`:与年份无关的全历史同步器(一次拉全,各 1 个文件)
- `dividend`(G1,**最关键**)、`fina_indicator` + `cashflow` + `balancesheet` + `income`(G4)
- `index_daily`(G5)、`index_weight` 填充(G5)
- `suspend_d` + `stk_limit`(G6)
6. `hd_data_audit` + `data_audit_*.html`
7. `docs/data-dictionary.md` 初版
8. `config/backtest.yml: period.start = 2020-01-01`(P0b 完成后切 2015-01-01)
**验收**
- `hd_dividend` ≥ 5,000 只、覆盖 2010 至今;`div_proc='实施'` 且含 `ex_date` 的记录 ≥ 8 万条
- 三个基准指数(`000300.SH`/`000922.CSI`/`000001.SH`)2015 至今无缺口
- `hd_fina_indicator`/`hd_cashflow`/`hd_balancesheet`/`hd_income` 各覆盖 ≥ 5,000 只、2010 至今,且 `announce_date` 非空率 ≥ 99%
- `data_audit_*.html` 中 G1/G4/G5/G6 全部 ✅(G2/G3 标注为「P0b 待回补」)
- 源码扫描:0 处 `DELETE`/`DROP`/`TRUNCATE`;0 处 import qlib 代码
## P0b · 历史回补(后台长跑,不阻塞 P1–P8)
**交付**
- `daily` + `adj_factor` 回补 **2015-01-01 ~ 2018-12-31**(约 975 交易日 × ~3,500 只 ≈ 340 万行)
- `daily_basic` 回补 **2015-01-01 ~ 2019-12-31**
- 分年分片 + `hd_sync_log` 断点续传;回补前后基线快照对比
- 完成后:`config/backtest.yml: period.start` 切至 `2015-01-01`,重跑 P4 复权一致性验收
**验收**
- `stock_daily` 最小 `trade_date` ≤ 2015-01-02;2015-2018 每个交易日股票数 ≥ 2,700
- **既有数据零改动**:2019-01-02 之后的所有 (symbol, trade_date) 行内容与回补前逐行一致
- `data_audit_*.html` 中 **G1–G6 全部 ✅**(即 `plan.md §22` 的 10 年窗口可用)
## P1 · Point-in-Time 股票池
**交付**
- `data/repo.py`:PIT 查询原语(`asof` 参数贯穿;财务 `announce_date <= asof`;ST 按 `stock_name_history` 区间判定;退市股按 `delist_date` 参与历史)
- `universe/filters/`:`MarketFilter` / `RiskFilter` / `DividendFilter` / `FinancialQualityFilter`,各自从 YAML 读参
- `universe/selector.py`:组合执行 + 逐股逐滤网留痕
- 落库 `hd_universe_run` / `hd_universe_member`
- CLI:`hdiv universe run -c config/universe.yml --asof 2018-06-29`
- `universe_*.html`
**验收**
- **生存者偏差测试**:对 2018-06-29 快照,能筛出**此后已退市**的成员股(用例固化)
- **未来函数测试**:把某股 2024 年报数据人为置入后,`asof=2018-06-29` 的筛选结果不变
- 同一 `asof` 两次运行结果完全一致(可复现)
## P2 · 股息率因子 + 个股画像
**交付**
- `factor/dividend_yield.py`:PIT-TTM 每股分红(按 `imp_ann_date`/`ex_date` 归属)→ 股息率序列 → 历史分位 / z-score
- `factor/valuation.py`、`factor/quality.py`:PE/PB/PS 分位、ROE/ROIC/FCF/负债率、分红质量(连续性/DPS CAGR/DPS 波动/支付率/FCF 覆盖)
- `profile/`:五类画像 + 安全边际分项
- 落库 `hd_factor_snapshot` / `hd_profile_run` / `hd_profile_stat` / `hd_profile_series` / `hd_profile_score`
- `profile_*.html`(含 `plan.md §35` 的贵州茅台式展示)
**验收**
- 抽 3 只股票(如 600036.SH、601088.SH、000895.SZ)的 P10/P25/P50/P75/P90 与独立手工核对一致(误差 < 1e-6)
- 分位单调性断言:`P10 ≤ P25 ≤ P50 ≤ P75 ≤ P90`
- 画像页可离线打开且四联图 X 轴对齐
## P3 · 策略 DSL + 版本管理
**交付**
- `strategy/dsl.py`:YAML → pydantic 严格校验(非法 percentile、`entry ≤ exit`、权重和 ≠ 1 直接报错)
- `strategy/registry.py`:加载 / 注册 / 版本指纹(`config_hash`)
- `strategy/base.py` + `high_dividend.py`:只产信号,不碰执行
- 落库 `hd_strategy` / `hd_strategy_param`;生命周期状态机
- CLI:`hdiv strategy validate|register|list|diff`
**验收**
- 篡改 YAML 任一阈值 → `config_hash` 变化且策略签名变化
- 非法配置 100% 被拒(负例测试集 ≥ 10 条)
- `plan.md §26` 要求的 `Strategy Version / Parameter Version / Training Period / Test Period` 四元组可完整落库还原
## P4 · 回测引擎
**交付**
- `backtest/engine.py`:事件驱动逐日循环;`signal → execution` 分离
- `backtest/execution.py`:`next_open` 成交、涨跌停跳过、停牌顺延、成交量占比上限
- `backtest/cost.py`:佣金(含最低 5 元)/ 印花税 / 过户费 / 滑点
- `backtest/dividend.py`:现金分红 + 送转 + 配股 → 持仓数量与现金调整;分红再投资两种模式(`plan.md §31` 模式 A/B)
- `backtest/portfolio.py`:单股上限 / 行业上限 / 权重方案
- `backtest/benchmark.py`:基准对齐
- 落库 `hd_backtest_run` / `_equity` / `_position` / `_trade` / `_signal` / `_metric`
- CLI:`hdiv backtest run -s config/strategy/high_dividend_v1.yml`
**验收**
- **复权一致性**:单只股票「买入并持有」回测收益 vs 由 `adjust_factor` 直接推导的收益,误差 < 0.5%
- **成本守恒**:期初资金 = 期末资金 − Σ(买入金额) + Σ(卖出金额) + Σ(分红) − Σ(费用)(逐日对账通过)
- **信号分离**:`hd_backtest_signal` 中 `executed=false` 的记录均有 `skip_reason`(涨跌停/停牌/资金不足)
- 每笔交易的 `reason_json` 含 `plan.md §32` 要求的全部字段
## P5 · Walk-forward
**交付**
- `backtest/walk_forward.py`:rolling/expanding 窗口切片;train 段定阈值 → test 段**冻结**
- 落库 `hd_walkforward_run` / `hd_walkforward_window`
- `walkforward_*.html`
**验收**
- test 段代码路径**无法**读取 train 之外的数据(接口层断言:test 期间传入的 `params` 为冻结对象)
- 三窗口(如 2015-2019/2020、2016-2020/2021、2017-2021/2022)样本外指标独立落库
- train/test 的 `hd_backtest_metric.scope` 可分别聚合
## P6 · 绩效与风险分析
**交付**
- `analysis/performance.py`:`plan.md §28.1` 收益指标(Initial/Final/Total Return/CAGR/Annual)
- `analysis/risk.py`:§28.2(MDD/Vol/Downside Vol/Sharpe/Sortino/Calmar)
- `analysis/trade.py`:§28.3(Trades/Win Rate/Avg Holding/Avg Profit/Avg Loss/Profit Factor/Turnover)
- `analysis/attribution.py`:§28.4 基准比较
- `analysis/sensitivity.py`:参数扫描(买入 P70/75/80/85/90 × 卖出 P20/25/30)
- 落库 `hd_backtest_metric` / `hd_sensitivity_run` / `hd_sensitivity_point`
**验收**
- 指标清单与 `plan.md §28` **逐项对齐**(自动化清单比对测试)
- 敏感性结果能回答 `plan.md §27` 的判断:CAGR 对参数是否平滑(输出平滑度指标)
- 单一指标(如 Sharpe)计算与独立实现(`numpy` 手算)一致
## P7 · HTML 固定格式输出
**交付**
- `report/renderer.py` + `templates/`(base / partials / charts / reports)
- 8 类报告页 + `output/index.html` 导航
- `hd_report` 元数据落库(含 SQL 溯源)
- CLI:`hdiv report build --run-id <id>` / `--all`
**验收**
- **断网打开**全部正常(无外部请求;用静态资源引用检查断言)
- 页面所有数值均可由 `hd_report.sql_provenance_json` 中的 SQL 复现
- 文件路径与 `report.yml:naming` 完全一致
## P8 · 策略解释与 AI Agent(`plan.md §36–37`)
**交付**
- `report/strategy_explanation.py`:自动生成「为什么买/卖/持有 + 历史表现 + 策略弱点」
- `ai/`:Data/Factor/Strategy/Backtest/Analysis Agent 骨架;Strategy Agent 输出必须过 Schema 校验才可运行;Backtest Agent 只调引擎不自算
- 落库:解释文本进 `hd_report.context_json`
**验收**
- 解释中的每个数字都能追溯到 `hd_backtest_*` 表
- 非法自然语言策略 100% 被 Schema 拦截
---
## 6.1 优先级与工作量估算
| 阶段 | 关键交付 | 相对工作量 | 依赖 |
|---|---|---:|---|
| P0a | 建表 + 7 个全历史同步器 + 数据审计 | 中高 | — |
| P0b | 2015-2018 行情回补(后台长跑) | 低(机器时间为主) | P0a |
| P1 | PIT 股票池 | 中高 | P0a |
| P2 | 股息率因子 + 画像 | 高 | P0a/P1 |
| P3 | 策略 DSL | 中 | P1 |
| P4 | 回测引擎 | **最高** | P0a–P3 |
| P5 | Walk-forward | 中 | P4 |
| P6 | 绩效与风险 | 中 | P4 |
| P7 | HTML 输出 | 中高 | P4/P6 |
| P8 | 解释 + Agent | 中 | P7 |
**建议里程碑**
- **M0(数据就绪)** = P0a → 具备 2020 起全链路数据条件,且分红/财务/基准/停牌涨跌停全部到位
- **M1(可用)** = P0a + P1 + P2 → 能回答「现在哪些股票符合高股息 + 安全边际」
- **M2(可回测)** = + P3 + P4 + P6 → 能跑出 CAGR/MDD/Sharpe 与交易明细(2020 起)
- **M3(可研究)** = + P0b + P5 + P7 → 满足 `plan.md §50` 全部 14 项,回测窗口 2015 起 10 年
- **M4(可扩展)** = + P8 → 自然语言策略生成与自动研究报告
> **关键路径**:P0a → P1 → P2 → P4 → P7。P0b 与 P5 可并行后台推进,不占关键路径。
---
# 7. 关键决策(已确认)
> 2026-10-02 已与用户确认,以下为**生效决策**,全部实现须遵守。
> 后续如需变更,须在 `docs/decisions/` 下新增 ADR 并同步修订本表。
| 编号 | 决策点 | **生效结论** | 影响 |
|---|---|---|---|
| **D1** | 新表放哪 | ✅ **`qlib` 库内加 `hd_*` 前缀表** | 可跨表 JOIN `stock`/`daily_basic`;30 张表**不注册进 qlib 的 Alembic 版本链**,qlib 升级不受影响 |
| **D2** | 历史回补写到哪 | ✅ **追加进 qlib 现有 `stock_daily`/`daily_basic`/`adjust_factor`** | 纯新增历史区间,**不覆盖任何既有行**;回补前后行数写入 `hd_sync_log` 可审计;`index_weight` 空表填充成分股 |
| **D3** | 回测起始年份 | ✅ **先按 2020 跑通端到端,再补 2015 数据** | P0 拆为 **P0a(最小可用数据集)** 与 **P0b(历史回补)**,见 §6 修订;P0b 可在后台长跑,不阻塞 P1–P8 |
| **D4** | 图表技术栈 | ✅ **Python + Jinja2 + 内置 ECharts 静态 HTML** | 单文件自包含、断网可开、零运维;CSS/JS 全部内联或本地引用 |
| **D5** | 是否复用 qlib 代码 | ✅ **完全独立实现,仅共享数据库** | 不 import `qlib/backend/app/**`;QLib 的口径由我阅读后在新代码中重新实现并加测试;qlib 项目代码零改动 |
| **D6** | 分红数据口径 | ✅ **以税前现金分红 `cash_div_tax > 0` 计一次分红**,且只认 `div_proc = '实施'` 且存在 `ex_date` 或 `pay_date` | 与 Tushare `dv_ttm` 口径一致,避免「股东提议/预案/取消」污染连续性判定 |
| **D7** | 股息率因子口径 | ✅ **自算 PIT-TTM 为回测主口径**(按 `imp_ann_date` 生效);`daily_basic.dv_ttm` 仅作交叉校验列落库 | 彻底规避未来函数;两口径差异 > 5% 时在 `hd_data_audit` 记 WARN |
## 7.1 决策带来的计划修订
1. **P0 拆分为两段**(因 D3):
- **P0a · 最小可用数据集**:建全部 30 张 `hd_*` 表;同步 `dividend`/`fina_indicator`/`cashflow`/`balancesheet`/`income`/`index_daily`/`suspend_d`/`stk_limit`(这些与年份无关,一次拉全历史);`index_weight` 填充。**目标:立刻具备 2020 年起端到端可跑的数据条件。**
- **P0b · 历史回补(后台长跑)**:`daily` + `adj_factor` 回补 2015-01-01 ~ 2018-12-31;`daily_basic` 回补 2015-01-01 ~ 2019-12-31。完成后 `backtest.yml: period.start` 从 `2020-01-01` 切到 `2015-01-01`,并重跑 P4 验收。
2. **P1–P8 的开发不被 P0b 阻塞**(因 D3),但 **M3 里程碑(对齐 `plan.md §50`)要求 P0b 完成**。
3. **qlib 项目零代码改动**(因 D5),仅在数据层面新增历史行(因 D2);回补脚本放在本项目 `src/hdiv/data/sync/` 内。
4. **`index_weight` 是唯一例外**:它是 qlib 的表但为空,按 D2 填充成分股权重(纯新增,无删除)。
## 7.2 由决策派生的硬约束(写入单测)
| 约束 | 断言方式 |
|---|---|
| 30 张表全部 `hd_` 前缀 | 扫描 `information_schema.tables`,本项目创建的表 100% 匹配 `^hd_` |
| 不注册进 qlib Alembic | 断言 `alembic_version` 表内容不被本项目修改 |
| 回补只增不删 | 回补前后对 `stock_daily`/`daily_basic`/`adjust_factor` 做行数 + 日期区间 + 校验和快照;断言「回补后行数 ≥ 回补前」且「既有 (symbol, trade_date) 的 close 未被修改」 |
| 不 import qlib 代码 | 源码扫描:无 `from app.`/`import app.`/`qlib.backend` 引用 |
| qlib 源码零改动 | 记录 `~/project/qlib` 的 git 工作区状态为 clean |
---
# 8. 风险与对策
| 风险 | 影响 | 对策 |
|---|---|---|
| Tushare 积分/频率限制导致回补慢 | P0 延期 | 分年分片 + `hd_sync_log` 断点续传 + `rate_limit_per_min` 可调;回补可后台长跑 |
| 2015-2018 部分股票 `daily_basic` 缺 `dv_ttm` | 早期股息率不可用 | 主口径为自算 PIT-TTM(D7),`dv_ttm` 仅交叉校验 |
| 分红数据 `div_proc` 存在「取消」「股东提议」等非实施记录 | 分红连续性误判 | 只认 `div_proc = '实施'` 且存在 `ex_date`/`pay_date`(D6) |
| 银行/保险等金融股 FCF 口径失真 | `require_positive_fcf` 误杀 | YAML 中按行业豁免(`quality.exempt_industries`),P2 阶段加入 |
| 复权与分红重复计算 | 收益虚高 | 强制:用**不复权价 + 独立分红现金流**建仓,复权价仅用于口径校验;P4 设「复权一致性」验收 |
| 回测过拟合 | 结论不可信 | 硬性要求 Walk-forward + 参数敏感性(`plan.md §26/§27`),并把 `config_hash`/`data_version` 写进每一次 run |
| qlib 侧若升级 Alembic | 表冲突 | 只写 `hd_` 前缀,不注册进 qlib 的 Alembic 版本链(D1) |
| 回补追加进 qlib 现有表可能影响 qlib 平台行为 | 静默回归 | 回补仅新增 2015-2018 区间、绝不触碰 2019+ 既有行;回补前后基线快照 + 逐行一致性断言(D2) |
---
# 9. 验收标准(对齐 `plan.md §50`)
第一版完成后,输入:
```yaml
最低市值: 500亿 上市年限: 10年 连续分红: 5年
最低股息率: 3% 买入: 历史P75 卖出: 历史P25
```
系统应自动完成 14 项:
| # | 能力 | 落地阶段 | 落库位置 |
|---|---|---|---|
| ① | 找出符合条件的股票 | P1 | `hd_universe_member` |
| ② | 生成股票池(PIT) | P1 | `hd_universe_run` |
| ③ | 每股历史股息率序列 | P2 | `hd_profile_series` |
| ④ | 计算 P10/P25/P50/P75/P90 | P2 | `hd_profile_stat` |
| ⑤ | 生成买卖信号 | P3 | `hd_backtest_signal` |
| ⑥ | 执行历史回测 | P4 | `hd_backtest_run` |
| ⑦ | 正确处理分红与除权 | P4 | `hd_dividend` + `hd_backtest_trade` |
| ⑧ | 加入交易成本 | P4 | `hd_backtest_trade.*_cost` |
| ⑨ | 输出 K线 + 买卖点 | P7 | `backtest_*.html` |
| ⑩ | 收益/回撤/Sharpe 指标 | P6 | `hd_backtest_metric` |
| ⑪ | 与 Benchmark 比较 | P6 | `hd_backtest_metric.benchmark_code` |
| ⑫ | 执行 Walk-forward | P5 | `hd_walkforward_window` |
| ⑬ | 样本内/外结果 | P5 | `hd_backtest_metric.scope` |
| ⑭ | 保存完整参数与版本 | P3 | `hd_strategy` + `hd_strategy_param` |
---
# 10. 附:本计划的四条纪律
1. **只增不删**:不执行任何 `DELETE`/`DROP`/`TRUNCATE`;重跑一律新建 `run_id`。
2. **配置零硬编码**:任何阈值只允许出现在 `config/*.yml`,代码中出现的常量只能是无业务含义的(如 0、1)。
3. **无未来函数**:所有取数必经 `data/repo`,且以 `announce_date`/`imp_ann_date`/`available_date <= asof` 为唯一纪律;每个阶段都有对应的负例测试。
4. **可复现**:每次回测的 `code_version + data_version + config_hash + seed` 全量落库,同输入必同输出。
+381
View File
@@ -0,0 +1,381 @@
# 实施状态报告
> 对应 `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 可序列化 |
+1995
View File
File diff suppressed because it is too large Load Diff
+1509
View File
File diff suppressed because it is too large Load Diff