从 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。
865 lines
42 KiB
Markdown
865 lines
42 KiB
Markdown
# 高股息回测系统 · 开发计划
|
||
|
||
> 版本: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` 全量落库,同输入必同输出。
|