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

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

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

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

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

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

865 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 高股息回测系统 · 开发计划
> 版本: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` 全量落库,同输入必同输出。