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

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

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

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

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

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

42 KiB
Raw Permalink Blame History

高股息回测系统 · 开发计划

版本: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 —— 系统已端到端可运行,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)

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

三条硬边界

  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 要点

-- 分红明细: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 年起可端到端跑通」的数据条件,不等待历史回补。

交付

  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)

第一版完成后,输入:

最低市值: 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 全量落库,同输入必同输出。