Files
ggx/config/backtest.yml
T
simon cf6d4d2c56 功能:每日动态股票池回测(--mode daily)+ 每日增量同步 + PIT 批量取数层
说明:本提交是工作区中此前的未提交工作(在 14ec0c6 之后产生),**非本次会话所写**,
按用户要求整理并推送。已做安全检查(无明文凭据、无大文件、.env/logs/output 仍被忽略),
并完成可执行范围内的测试验证(见「测试」一节)。

## 新增能力

1) `hdiv backtest --mode daily --start <日期>`
   - src/hdiv/backtest/daily.py:两趟式(先逐日选股,再复用既有引擎模拟)
   - 每个交易日按当日可见数据重建股票池(PIT),每个交易日判断买卖点
   - `pool_exit_action`:hold(只减不加、不因掉出池子而清仓)/ sell(掉出即清仓)
   - `profile_on_trade`:买卖决策发生时计算并留痕个股画像,**不区分是否在当日池内**
     (卖出/减仓同样留痕,否则「为什么卖」缺证据)
   - 与 walkforward 的分工:daily 是一条连续路径的推演,不是过拟合检验;
     因此不使用训练段、不冻结分布,阈值口径一律 rolling
   - 拒绝 `--universe-run`(daily 的定义就是逐日重筛,冻结池与之矛盾)

2) PIT 批量取数层 src/hdiv/universe/pit.py
   - PitRepo 继承 Repo,**只重写取数**(按区块批量预载 + 逐日内存切片),
     派生逻辑(最新一期财报合并、单位归一化、支付率口径等)一行不重写
     —— 以保证与逐日单点查询**结果等价**
   - 候选集预剪枝:用「不可能通过」的边界条件提前排除,文档论证为精确等价而非近似
   - src/hdiv/universe/daily.py:每日动态筛选器(仍然调用既有 selector 与四个 Filter)

3) 每日增量同步 `hdiv sync daily`
   - src/hdiv/data/sync/daily.py:只抓「库里还没有的那几天」,
     按「当日股票数 ≥ 当年规模阈值」判定缺口,不重拉历史、不覆盖既有行;
     支持 `--dry-run` 先看待抓清单
   - deploy/daily-sync.sh、deploy/install-sync-schedule.sh、
     deploy/com.hddiv.sync.plist.example(launchd 每天 17:00)
   - 新表 hd_daily_universe(逐日入选成员留痕)+ sql/hd_daily_universe.sql + schema.py
     (该表已存在于库中,`ddl plan` 返回 0 个待执行动作)

4) Web 与文档
   - 前端支持 daily 模式记录下钻(web/app.js、web/app.css、web/index.html、
     web/favicon.svg)
   - README / docs/user-guide.md / docs/implementation-status.md 同步更新:
     三种回测模式的取舍、daily 的成本说明(6.7 年约 1.5 小时)与调优手段

## 测试

tests/ 共 500 项(新增 tests/test_daily.py 43 项、tests/test_sync_daily.py 36 项)。

已验证通过:
- 排除上述两个新文件的 **421 项:全部通过(pytest 退出码 0)**
- 两个新文件的**非 DB 单元测试 60 项:全部通过**

未能在合理时间内跑完:
- 两个新文件中 **19 项 DB 标记的重型测试**。实测瓶颈是一条**无界全表扫描**:
  `SELECT ... FROM hd_cashflow WHERE ann_date <= :asof ORDER BY symbol, end_date, ann_date`
  (31 万行,无 symbol/报告期下限)。全量套件跑到 161 项时已耗时 20 分钟、
  0 失败,按该速率预计需 3 小时以上,因此改为分档验证。
- 旁证:库中存在 3 次成功的 daily 端到端运行(2026-10-05 10:05 / 10:32 / 11:03,
  区间 2024-03-01~03-15),说明该路径可正常完成。

## 已知待改进

- 上述 `hd_cashflow`(及同类「按 ann_date 上界取全历史」)的查询缺
  symbol / 报告期下限,是 daily 模式的主要性能瓶颈,建议下一轮优化。
2026-10-05 11:57:13 +08:00

136 lines
6.4 KiB
YAML
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.
# ============================================================
# 回测配置(★ 可直接修改)
# 对应 plan.md §22–§27(Walk-forward / 过拟合控制)与 §29–§31
# ============================================================
version: 1
capital:
initial: 1000000 # 元
currency: CNY
period:
# P0b 已完成 2015-2018 行情回补,行情/每日指标均覆盖 2015-01-05 起,
# 因此按 plan.md §22「历史数据 10 年」的要求使用完整窗口。
start: 2015-01-01
end: latest
# ------------------------------------------------------------
# 调度:多久评估一次信号、多久重建一次股票池
# ------------------------------------------------------------
schedule:
# 信号评估频率(月):1 = 每月末评估一次买卖信号
signal_frequency_months: 1
# 股票池重建频率(月):高股息池成员(大市值/长上市/连续分红)非常稳定,
# 每年重建一次即可;调小更严格但显著更慢。
universe_refresh_months: 12
# 信号与成交的时间关系由 fill.price 决定(next_open = 次日开盘成交)
# ------------------------------------------------------------
# 分位阈值口径
# ------------------------------------------------------------
percentile_reference:
# rolling —— 用截至评估日的滚动窗口分布(PIT 自适应,单次回测默认)
# frozen —— 冻结训练窗口分布(walk-forward 测试段必须用这个,plan.md §25)
mode: rolling
# rolling 模式下的回看年数
lookback_years: 5
# ------------------------------------------------------------
# Walk-forward(plan.md §23/§24/§25)
# ------------------------------------------------------------
walk_forward:
enabled: true
# rolling —— 固定长度训练窗(plan.md §23 的示例)
# expanding —— 训练窗递增
scheme: rolling
train_years: 5 # plan.md §43: Training = 5~8 年
test_years: 1 # plan.md §43: Test = 1~2 年
step_months: 12 # 窗口步进
min_train_years: 5
# 测试期禁止重新调参:引擎层硬约束,train 段产出的参数对象冻结后传入
freeze_params_in_test: true
# ------------------------------------------------------------
# 每日动态股票池(backtest --mode daily)
#
# 与上面的 walk_forward 是**两种不同的检验**,不要混用:
# walk_forward —— 切多个 (train, test) 窗口检验过拟合(样本外能否复现);
# daily —— 从 --start 起跑**一条连续路径**,每个交易日重新选股、
# 每个交易日判断买卖,回答「动态股票池下实际会怎样」。
# daily 模式不使用训练段,也就没有「冻结分布」——阈值口径一律 rolling(PIT)。
# ------------------------------------------------------------
daily:
# 股票池重建频率(交易日):1 = 每个交易日按当日可见数据重新筛选
universe_refresh_days: 1
# 信号评估频率(交易日):1 = 每个交易日评估买卖点
signal_frequency_days: 1
# 持仓掉出当日股票池后怎么办:
# hold —— 只减不加(默认):不再买入/加仓,但不因「掉出池子」而清仓,
# 仍按股息率分位规则决定减仓/卖出。池子回答「能买什么」,
# 不回答「必须卖什么」。
# sell —— 掉出即视为卖出信号,次日开盘清仓。
pool_exit_action: hold
# 买卖决策发生时计算并留痕个股画像(**不区分是否在当日池内**)。
# 卖出/减仓同样触发画像 —— 否则「为什么卖」缺证据。
profile_on_trade: true
# 是否把每日入选成员写入 hd_daily_universe(供事后查「某天为什么是这些股票」)
persist_daily_universe: true
# 批量预载的分块年数(内存控制):行情/每日指标按年分块载入,用完即弃
chunk_years: 1
# 逐日选股的进度打印间隔(交易日)
progress_every_days: 20
# ------------------------------------------------------------
# 分红处理(plan.md §30/§31)
# ------------------------------------------------------------
dividend:
# reinvest —— 现金分红**回落到可投资现金池**:与初始资金同一个 cash 变量,
# 下次调仓时按目标权重再配置(默认;plan.md §31 模式 B 的权重口径)
# hold —— 分红永久留存、不参与后续买入(未实现,会写入 unimplemented)
# cash_out —— 分红移出组合(未实现,会写入 unimplemented)
cash_mode: reinvest
# portfolio_rebalance —— 下次调仓时按目标权重再配置(已实现,默认)
# same_stock_next_open —— 按同一只股票次日开盘再投资(未实现,会写入 unimplemented)
reinvest_rule: portfolio_rebalance
# 红利税**总闸**;分档税率在 config/cost.yml 的 dividend_tax
# (两者必须同时为真才计税)
apply_dividend_tax: true
# 送股/转增:按 stk_div 调整股数、总成本不变(plan.md §30)
# false 时股数不调整,而不复权价照常除权下跌 → 会写入 unimplemented
handle_stock_dividend: true
# 配股:**未实现**(无配股价/比例数据)。true = 配置声称要处理,
# 每次 run 都会在 unimplemented_json 里声明这个缺口
handle_rights_issue: true
# ------------------------------------------------------------
# 基准(plan.md §28.4)
# ------------------------------------------------------------
benchmark:
- { code: 000300.SH, name: 沪深300 }
- { code: 000922.CSI, name: 中证红利 }
- { code: 000001.SH, name: 上证指数 }
# 无风险利率(年化),用于 Sharpe / Sortino
risk_free_rate: 0.02
# ------------------------------------------------------------
# 成交撮合
# ------------------------------------------------------------
fill:
# next_open —— 信号次日开盘成交(默认,最贴近实盘)
# next_close / same_close
price: next_open
partial_fill: false
# 单笔成交不超过当日成交量的比例(流动性约束)
max_volume_pct: 0.05
# 涨跌停处理:skip(跳过当日)/ defer(顺延到下一可成交日)
limit_up_down_rule: skip
suspended_rule: defer
# ------------------------------------------------------------
# 可复现性(plan.md §47 原则 6)
# ------------------------------------------------------------
reproducibility:
record_code_version: true
record_data_version: true
seed: 42