Public note

Quant OS:Tushare 数据资产、Qlib 研究链与本地回测实录(2026-07-31)

·Markdown 原文

Quant OS:Tushare 数据资产、Qlib 研究链与本地回测实录(2026-07-31)

[!success] 先说结论 本机 Tushare 镜像已经不再是“只有早期几十个文件”的样例数据。它现在约 7.7 GB,A 股日线自然月分区已经覆盖 1990-12-19—2026-07-28;我已经从中 冻结出 2018—2025 的中证 500 研究输入,构建了 Qlib 0.9.7 Provider,并用 正式项目自己的 Python 环境完成了三次逐 byte 一致的本地回测。

[!warning] 这证明了什么,又没有证明什么 它证明“本地真实数据 → 可校验数据集 → Qlib 信号 → 组合回测 → 可重放证据” 已经跑通;不证明当前动量策略有投资价值,也不证明 Quant OS 已经达到 60/80 分生产标准。公开状态仍明确为 production_ready=falseinvestment_value_claim=falsegate_credit=[]

代码只在 Quant OS 独立项目中。本文统一记为:

$QUANT_OS_ROOT

原始镜像统一记为:

$TUSHARE_MIRROR_ROOT

OB 只保存本文这种知识文档,不保存 Python 源码、Parquet、Provider、回测 artifact、账号、密码或 token。

一、应该怎样理解这批数据

最容易犯的错误,是看到 7.7 GB 和一亿多行,就把它当成“已经可用于回测的 数据集”。其实现在有三个不同对象:

flowchart LR
    A["Live Tushare mirror<br/>持续变化的原始 Parquet"] --> B["Scoped snapshot<br/>冻结选择与文件 hash"]
    B --> C["Qlib Provider<br/>calendar / instruments / features"]
    C --> D["Signal & portfolio backtest"]
    D --> E["Evidence record<br/>指标、hash、边界"]
  1. Live mirror 是原材料仓。下载器可能继续增加或刷新分区,不能直接作为 一次实验的唯一版本号。
  2. Scoped snapshot 固定本次究竟选择了哪些 API、日期、指数、job 和 Parquet 文件,并记录 size、row count、query hash、SHA-256。
  3. Qlib Provider 是面向 Qlib 的派生格式。它可快速读取,但不是原始数据 备份,也不能替代 frozen source manifest。

这次新增的关键能力,不只是“会读 Parquet”,而是把这三层的身份连接起来: snapshot 和 Provider 各自可验证,再用 lineage verifier 对 221 个 source jobs 的六个字段逐一比对。最终差异为 0。

二、数据到底有什么

截至 2026-07-31 的点时盘点:

数据 当前自然文件/分区 当前行数 可利用方向
daily 428 个月 18,041,386 OHLC、收益、成交量额、回测行情
adj_factor 428 个月 18,868,457 累计复权
daily_basic 331 个月 17,335,584 市值、换手、估值、横截面特征
stk_limit 235 个月 17,878,521 逐股逐日真实涨跌停价格
suspend_d 37 年 641,379 停复牌、可交易性
stock_basic 4 个版本 5,869 代码、上市/退市信息
000905.SH index_daily 37 年 5,239 真实中证 500 benchmark
000905.SH index_weight 37 年 129,000 历史月度成分和权重

全镜像的 SQLite job ledger 有约 37.8 万条 completed 版本记录、约 1.80 亿行。 这里“版本记录”会包含同一自然分区的历史刷新,所以不能把 ledger 中所有历史 版本的行数直接当作当前去重数据量。

本次真正进入 Qlib 的范围是:

index       000905.SH
date        2018-01-01 ... 2025-12-31
core APIs   daily, adj_factor, trade_cal, stock_basic,
            index_daily, index_weight

在这一区间内,96/96 个日线月分区完整,原始 daily 共 8,813,885 行;上海、 深圳可用日线 8,544,077 行,1,942 个交易日。自然键重复、核心空值、OHLC 包络异常和交易日历缺口均为 0;可用日线的复权因子缺失为 0。

daily_basic 在同一范围只缺 13 个可用键,但它尚未进入本次 Provider。这样做 是有意把“数据链跑通”和“因子集合扩张”分开,避免在一次 release 中偷偷改变 输入定义。

三、Quant OS 已经新增了什么

1. 范围冻结器

tools/tushare_snapshot.py 会在一个 SQLite 读事务中,按 (api, partition) 选择最新 completed 版本;它拒绝范围内 running、deferred、 缺分区和文件校验失败。对选中的每个文件做两次 size、SHA、Parquet row count 验证,并在结束时确认 source job 仍是 latest。

它还会自动纳入研究起点前一年的 index_weight 锚点,否则 2018 年第一天无法 知道当时已经生效的成分集合。

2. Tushare → Qlib v2 构建器

tools/tushare_qlib.pyadapters/tushare_local.py 已支持:

  • SSE 交易日历;
  • 真实 000905.SH 指数行情;
  • 历史 index_weight 中证 500 成分;
  • 严格每期 500 只、权重和约 100、快照最大间隔检查;
  • 复权 OHLC、反向调整 share volume、保持 money 不变;
  • 构建前后再次读取 ledger 和源文件,降低 TOCTOU 风险;
  • Provider tree、manifest、converter source 和 data_version 验证;
  • 拒绝 PIT universe 与 synthetic benchmark 混用。

由于 Tushare 的 index_weight 数据没有单独验证过的 published_at,本次采用 保守规则:月末快照从下一个 Provider 交易日才生效。因此它只能称为 event-time / conservative next-session PIT 近似,不能冒充严格的 knowledge-time PIT。

复权使用每只股票在构建区间首个有效复权因子作常数锚:

multiplier = adj_factor_t / first_adj_factor
adjusted OHLC = raw OHLC × multiplier
adjusted volume = raw share volume / multiplier
money = raw money

这不会用未来公司行为重新缩放更早价格,但也意味着以不同 start 构建的两个 Provider 价格 level 可能不同,不能直接拼接。要延长历史,应统一起点重建。

3. 独立血缘验证器

tools/tushare_lineage.py 不相信“文件名看起来一样”,而是比较 snapshot 与 Provider 的每一个 source job:

id
path
row_count
byte_count
sha256
request_sha256

本次结果为 221 jobs × 6 fieldsmismatch_count=0converter_source_matches_current=true

4. 一键研究链

Makefile 已提供:

make tushare-snapshot
make tushare-build
make tushare-verify
make tushare-lineage
make tushare-backtest
make tushare-replay

tushare-replay 会再跑一次相同回测,并用 cmp 要求两个结果 JSON 逐 byte 一致。每次 build 必须换一个不存在的新 Provider 目录,工具拒绝覆盖已冻结版本。

四、已经跑出来的 Provider

正式本地位置:

$QUANT_OS_ROOT/data/qlib/tushare-csi500-2018-2025-v2/

核心身份:

证据
data version 5bf19d2da064357ad1802bca4bfa0c0505ed63fe2b24785ec6c56ecff1724963
Provider tree f173b8095fd9a62a63807324fa48bad83f0c282eea3abba871d0b0efd30b199f
Provider manifest 27fb6fedb6a4b114eae2aba44505fb6c4d32c7a9ae81e58c724dfadb8dd10ed0
文件/字节 10,011 / 71,707,194
日历 1,942 sessions,2018-01-02—2025-12-31
历史有效股票 1,111
被选日线 1,975,455
成分快照 97 observed / 96 effective
每期成分 严格 500
最大快照间隔 36 天

为什么 97 observed 只有 96 effective?因为 2025-12-31 的月末 roster 要到下一 个 Provider 交易日生效,但当前 Provider 正好在 2025-12-31 结束,所以它被 看见、校验,却不会提前污染区间内的持仓集合。

五、已经跑过的真实本地回测

这不是 Alpha 模型定稿,而是一条研究 smoke:

Qlib              0.9.7
Python            3.12.13
period            2019-01-02 ... 2025-12-30
feature start     2018-01-02
signal            20 日动量
portfolio         Top 50 / drop 5
rebalance         周频
benchmark         SH000905(真实指数日线)

在迁移环境中先连续跑了两次;随后又在正式 $QUANT_OS_ROOT/.venv-qlib312 环境中重跑一次。三次输出逐 byte 一致:

证据/指标 结果
run JSON SHA-256 803035a95ee9cf6f90af20cdf7e7024149b97e0113b585209a80f80048be87d3
signal 176,615 行
portfolio report 1,698 个交易日
策略累计收益 25.3152%
中证 500 累计收益 78.9560%
最大回撤 -60.4164%
总成本 0.035793
总换手 65.513938

这组数字首先告诉我们的不是“策略赚了 25%”,而是:

  1. 数据、日历、历史成分、真实 benchmark 和 Qlib simulator 能完成长区间运行;
  2. 同输入、同代码、同环境契约可以重放;
  3. 简单 20 日动量明显跑输中证 500,且最大回撤约 60%,不能进入候选策略;
  4. 工程链路通过与策略有效性是两件完全不同的事。

Qlib 会打印 $open contains nanMean of empty slice。这主要来自停牌、 新进入成分但没有当日可用开盘价等稀疏情况;本次运行退出码为 0、结果可重放, 所以它是需要继续分类治理的数据告警,不是一次运行失败。

六、你现在怎样自己验证

正式项目已拥有独立研究环境:

export QUANT_OS_ROOT=/path/to/quant-os
export TUSHARE_MIRROR_ROOT=/path/to/tushare-mirror
cd "$QUANT_OS_ROOT"
source .venv-qlib312/bin/activate
export PYTHONPATH=src:.

先验证已经冻结的 Provider 与血缘,不需要重建 76 MB 数据:

python tools/tushare_qlib.py verify \
  data/qlib/tushare-csi500-2018-2025-v2

python tools/tushare_lineage.py \
  --source-manifest artifacts/local-tushare-20260731/scoped-source-manifest.json \
  --provider-dir data/qlib/tushare-csi500-2018-2025-v2

预期看到:

provider ok = true
source_job_count = 221
mismatch_count = 0
converter_source_matches_current = true

重放已有研究:

make tushare-replay \
  PYTHON=.venv-qlib312/bin/python \
  TUSHARE_MIRROR_ROOT="$TUSHARE_MIRROR_ROOT"

它大约会输出较多 Qlib warning;最终应得到两个相同 SHA 的 JSON,并通过 cmp。如果要重新构建,请先把 TUSHARE_PROVIDER 指向一个尚不存在的新 目录,不要覆盖本页记录的 v2 Provider。

整库的 325 项测试已经通过,另有 1 项可选 runtime skip:

make test PYTHON=.venv-qlib312/bin/python

七、这些数据接下来能怎样利用

现在就能做

  1. Qlib 因子研究:在当前中证 500 PIT 近似 universe 上增加价格量能、 daily_basic 市值/换手/估值特征。
  2. Alpha158 + LightGBM:做严格 train/valid/test 和 walk-forward,输出 IC、Rank IC、分组收益、换手、成本敏感性,而不是只看累计收益。
  3. 研究回归测试:每次 adapter、特征或策略变更后,用冻结 data_version 重放,判断差异来自代码还是数据。
  4. 本地事件回测输入:将同一 snapshot 映射到 Quant OS canonical market state,加入逐股涨跌停、停复牌、T+1 和费用模型。
  5. 模型/目标包出口:本地训练后输出带 signal_as_ofnext_session、 model/data hash 的 TargetPackage,供聚宽或 QMT adapter 消费。

聚宽怎样利用

聚宽托管回测不能直接读取这台 Mac 的 7.7 GB 本地目录。正确做法不是硬上传 全部 Parquet,而是:

  • 在本地用冻结数据训练、验证和生成 TargetPackage;
  • 在聚宽用同一策略契约和平台原生行情做 hosted parity;
  • 对 symbol、时钟、复权、成分、订单和结果 evidence 做差异报告。

聚宽登录后才能形成真实平台 evidence;当前本地 Tushare/Qlib 成功不能替代 聚宽账号内的真实 hosted backtest。

QMT 怎样利用

QMT 适配层最终只应接收:

  • 用户在本机安全配置的账号/终端信息;
  • 已验签或 hash 固定的 TargetPackage;
  • QMT 行情、资产、持仓、订单和回报。

账号和密码不能写进 OB、Git、Provider 或 evidence。拿到账号之前,可以完成 adapter、mock、dry-run 和 fail-closed 门禁;拿到账号后还必须实测连接、 只读查询、最小下单、撤单、回报、重启恢复和 kill switch。

八、为什么它仍不等于 60/80 分

还缺的不是“再跑一个收益曲线”,而是生产语义:

  • stock_st 历史权限被拒,不能把空值当作非 ST;
  • 本次 Qlib simulator 使用统一 9.5% 涨跌停近似,尚未消费逐股逐日 stk_limit
  • 成分快照没有独立 published_at,不是严格 knowledge-time PIT;
  • 还没有把成员区间每日覆盖与 suspend_d 原因完整区分;
  • snapshot 是 hash manifest,不是 raw byte archive;源文件若被删除,仅靠 manifest 不能重建;
  • 尚无聚宽真实托管回测证据;
  • 尚无 QMT 真实券商连接、成交回报、恢复和风险演练证据;
  • 尚无漂移监控、容量评估、影子运行和持续 paper/live 观察期。

所以这次工作的正确定位是:数据与研究底座从“样例可运行”升级为“真实、 可冻结、可追溯、可重放的本地研究链”。它是走向 60/80 分的必要基础,但 不会自动获得生产门禁分数。

九、下一轮最值得做的实验

按信息增益排序:

  1. daily_basic 接入新的 frozen release,建立 price/volume + size/value/ liquidity 的特征数据集;
  2. 跑 Alpha158 + LightGBM 的滚动训练与 purged walk-forward;
  3. 建立 baseline:中证 500、等权、简单动量、LightGBM 四组同口径比较;
  4. 做交易成本从 5/10/20/50 bps、换手约束、TopK 和持有期的敏感性分析;
  5. stk_limitsuspend_d 映射进 Quant OS 事件回测;
  6. 登录聚宽后跑同窗口 hosted parity;
  7. QMT 账号开通后先做只读和 shadow,再做最小真实订单演练。

真正进入“模型研究”的起点,应该是第 2 项,而不是继续扩大原始数据文件数量。 现在数据量已经够用;下一步的瓶颈是时间语义、实验设计、交易约束和跨平台 证据,而不是缺一条能画出曲线的脚本。