修复:量价单位 / 未来函数守卫 / 实时画像闸门;行情回补到 2005;手册补全流程
本轮会话的三项正确性改造(均为「不报错、只让结果静默错」的类型):
1) 修复 stock_daily 量价单位前后不一致
- 现象:2015-2019 存 Tushare 原始单位(手/千元),2020 起存(股/元),2019 同日混合;
而流动性阈值按「元」配置 → 早年门槛实际是「日均成交额 ≥ 200 亿元」,
把 2015-2019 的股票池整体清空(实测 2016/2017/2018 各选出 0 只)。
- 修复:写入端 sync/price.py 统一换算;读取端 units.normalize_ohlcv_units
按行判定并幂等换算(price_history / avg_amount 都走它);
审计新增 UNIT-OHLCV 防回归。
- 效果:2016/2017/2018 的股票池变为 7/11/13 只。
2) 未来函数守卫(单次回测)
- 股票池自带 asof:若晚于回测起点即**拒绝执行**(原先静默冻结套用),
与 walk-forward 已有的拒绝理由一致;确需复现加 --allow-lookahead-universe,
偏差写入 unimplemented_json。
3) 新增实时(PIT)个股画像闸门
- profile/pit.py:每个决策日按当时可见数据重算过去 5 年画像,
惰性(仅买入条件已触发的标的)、面板按 asof 缓存、
规则不含财务指标时不查财报表;被剔除时产出 REJECT + 逐规则留痕。
- 指标定义复用 ProfileBuilder._profile_one(与批量画像逐值等价的回归测试)。
- profile/coverage.py:窗口覆盖率(按交易日历的真实开市天数),
策略新增 entry.profile_gate.min_window_coverage(默认 0,不改变既有行为)。
- core/metrics.py:闸门可用指标的唯一定义(配置期即校验,避免写错指标名静默失效)。
4) 行情回补到 2005(使 5/8/10 年窗口真正完整)
- stock_daily / adjust_factor / daily_basic 补到 2005-01-04;
hd_suspend / hd_limit 补到 2010-01-04。
- 5 年窗口覆盖率:2018-05-18 由 67.0% → 99.1%,2016-12-30 由 39.8% → 99.0%;
残差经逐日与 hd_suspend 交叉核实为真实停牌(16/16 命中)。
- 审计 G2/G3 与断点续传原先用固定阈值(2000 / 1500 只),
会把 2005-2009 的正常数据误判为异常 —— 改为按「当年应有上市股票数」成比例判定。
- 节流修正:daily/adj_factor/daily_basic 限频 480 → 170(实测该 token 约 196/min 即被拒)。
5) 自我声明如实化
- 原先「约束未生效」由「过滤后集合为空」判定,会把「这批股票恰好没停牌」
误报成「hd_suspend 无数据」;改为按表级判定。
- 补齐此前静默的「配置承诺但未实现」项:suspended_rule/limit_up_down_rule 的 defer、
cash_mode=reinvest/reinvest_rule、handle_rights_issue、signal_to_execution、
max_volume_pct、liquidity_limit_pct_adv —— 全部写入 unimplemented_json。
6) 手册:新增 §0「全流程操作(选股 → 画像 → 回测)」置于最前
- 逐步说明「命令做了什么、数据从哪来、落了哪些库、有哪些坑」;
含实时画像闸门 9 问 9 答、未来函数守卫表、成交与成本口径、验证 SQL。
- 修正旧 §2.4 漏传 --universe-run(选了池子却没用于回测);
修正两处声称「停牌顺延」「分红再投资」已实现的相反表述。
测试:403 项全部通过(含新增 test_units.py、test_profile_pit.py、
未实现声明诚实性测试、行序无关性回归测试)。
注意:本提交中 docs/*、README.md、src/hdiv/web/service.py 除本轮修改外,
也含此前遗留的未提交改动(无法按文件切分)。
This commit is contained in:
+6
-2
@@ -188,7 +188,8 @@ def test_price_frames_map_tushare_columns() -> None:
|
||||
assert list(d.columns) == ["symbol", "trade_date", "open", "high", "low",
|
||||
"close", "volume", "amount", "source", "adjust"]
|
||||
assert d.iloc[0]["symbol"] == "000001.SZ"
|
||||
assert d.iloc[0]["volume"] == 100, "Tushare 的 vol 列映射为 volume"
|
||||
assert d.iloc[0]["volume"] == 10000, "Tushare 的 vol(手)必须换算为股(×100)"
|
||||
assert d.iloc[0]["amount"] == 1000000, "Tushare 的 amount(千元)必须换算为元(×1000)"
|
||||
|
||||
a = adj_frame([{"ts_code": "000001.SZ", "trade_date": "20150105", "adj_factor": 1.2}])
|
||||
assert a.iloc[0]["factor"] == 1.2
|
||||
@@ -243,7 +244,10 @@ def test_per_api_limits_are_independent() -> None:
|
||||
cfg = __import__("hdiv.core.config", fromlist=["load_config"]).load_config("datasource")
|
||||
ts = cfg.tushare
|
||||
assert ts.limit_for("dividend") == 180
|
||||
assert ts.limit_for("daily") == 480
|
||||
# daily 系列的额度必须**低于实测上限**:以 ~196 次/分钟跑 daily 会被 Tushare
|
||||
# 拒绝,触发限频后要冷却 62 秒,逐日回补时远慢于平滑配速(见 datasource.yml 注释)
|
||||
assert 100 <= ts.limit_for("daily") <= 200, ts.limit_for("daily")
|
||||
assert ts.limit_for("daily") == ts.limit_for("adj_factor") == ts.limit_for("daily_basic")
|
||||
assert ts.limit_for("未知接口") == ts.rate_limit_default
|
||||
# 构造客户端需要 token;此处仅验证配置层
|
||||
assert ts.rate_limit_cooldown_sec >= 60, "冷却必须覆盖 Tushare 的 60 秒滑动窗口"
|
||||
|
||||
Reference in New Issue
Block a user