从 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。
42 KiB
高股息回测系统 · 开发计划
版本:V1.0 日期:2026-10-02 依据:
docs/plan.md(A股投资策略制定与回测方案 V1.0)+ 用户补充要求(数据源、YAML 配置、DB 优先、HTML 图表) 数据基座:~/project/qlib(本机 MariaDB127.0.0.1:3306/qlib+ Tushare Pro) 状态:关键决策 D1–D7 已于 2026-10-02 确认(见 §7)。 实施进展见docs/implementation-status.md—— 系统已端到端可运行,plan.md §50的 14 项验收能力全部具备,195 项自动化测试通过。
0. 结论摘要
- qlib 已提供可直接使用的数据基座:行情、复权因子、每日指标(含
dv_ttm股息率)、PIT 财务指标(带announce_date)、股票基本信息(含 338 只退市股)、ST 改名历史、交易日历。共 20 张表、约 2300 万行。 - 有 6 个数据缺口必须先补齐(见 §1.3),其中「无分红明细表」与「行情仅覆盖 2019 起」是硬阻塞项,P0a/P0b 阶段解决。
- Tushare 权限已实测全部可用:
dividend/fina_indicator/cashflow/balancesheet/income/daily/adj_factor/daily_basic/index_daily/index_weight/suspend_d/stk_limit均返回code=0。 - 架构口径:新建独立 Python 项目,只以 qlib 的数据库为共享数据层,不 import qlib 后端代码,qlib 项目零代码改动;所有新表统一
hd_前缀,只增不删。 - 配置外置:筛选条件、个股特性、策略、成本、回测、报告全部 YAML 化(
config/),代码零硬编码阈值。 - DB 优先:新增 30 张
hd_*表承载股票池、画像、策略、回测、交易、绩效、敏感性、报告元数据;JSON 仅用于策略归档快照与报告上下文。 - 图表固定格式:Jinja2 模板 + 内置 ECharts(离线、无 CDN),输入一律是
run_id→ DB 查询,输出固定命名 HTML。 - 实施节奏:先用现有 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)
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 目录结构
高股息回测/
├── 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 分层与边界
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
三条硬边界
- 数据层 ≠ 策略层:策略代码不允许写 SQL,只能调
data.repo。 - 策略 ≠ 回测引擎:策略只产出信号(含
reason_json),引擎只执行信号。 - 图表 ≠ 业务:
report只做「run_id→ SQL → 渲染」,不参与任何计算。
2.3 数据库策略
- 库:沿用本机 MariaDB
127.0.0.1:3306,schemaqlib。 - 只读表白名单:
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 要点
-- 分红明细: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
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 ★ 股票筛选条件
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 ★ 个股特性配置
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 ★ 策略定义
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
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
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
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 年起可端到端跑通」的数据条件,不等待历史回补。
交付
pyproject.toml+ venv(Python 3.12、pandas、numpy、SQLAlchemy 2.0、pymysql、pydantic v2、PyYAML、Jinja2、pyarrow、tqdm)core/config.py:YAML → pydantic 严格校验,生成config_hashdata/db.py:连接池 + 只读表白名单拦截 + 只写hd_前缀断言 + 无 DELETE 保证data/ddl.py:30 张hd_*表幂等建表(CREATE TABLE IF NOT EXISTS),不注册进 qlib 的 Alembicdata/sync/:与年份无关的全历史同步器(一次拉全,各 1 个文件)dividend(G1,最关键)、fina_indicator+cashflow+balancesheet+income(G4)index_daily(G5)、index_weight填充(G5)suspend_d+stk_limit(G6)
hd_data_audit+data_audit_*.htmldocs/data-dictionary.md初版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-scorefactor/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 决策带来的计划修订
- 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 验收。
- P0a · 最小可用数据集:建全部 30 张
- P1–P8 的开发不被 P0b 阻塞(因 D3),但 M3 里程碑(对齐
plan.md §50)要求 P0b 完成。 - qlib 项目零代码改动(因 D5),仅在数据层面新增历史行(因 D2);回补脚本放在本项目
src/hdiv/data/sync/内。 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)
第一版完成后,输入:
最低市值: 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. 附:本计划的四条纪律
- 只增不删:不执行任何
DELETE/DROP/TRUNCATE;重跑一律新建run_id。 - 配置零硬编码:任何阈值只允许出现在
config/*.yml,代码中出现的常量只能是无业务含义的(如 0、1)。 - 无未来函数:所有取数必经
data/repo,且以announce_date/imp_ann_date/available_date <= asof为唯一纪律;每个阶段都有对应的负例测试。 - 可复现:每次回测的
code_version + data_version + config_hash + seed全量落库,同输入必同输出。