---
title: "Quant OS：Tushare 数据资产、Qlib 研究链与本地回测实录（2026-07-31）"
canonical: "https://gomars.fun/pages/8/"
updated: "2026-07-30T17:42:21Z"
---

# 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=false`、
> `investment_value_claim=false`、`gate_credit=[]`。

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

```text
$QUANT_OS_ROOT
```

原始镜像统一记为：

```text
$TUSHARE_MIRROR_ROOT
```

- 公开状态页：[Quant OS Status](https://gomars.fun/quant-os/status/)
- 代码仓库：[boat/quant-os](https://git.gomars.fun/boat/quant-os)
- 本次提交：[6275370](https://git.gomars.fun/boat/quant-os/commit/6275370afc14ac5dec3b834647e8166c3e9724e3)
- 权威技术手册：[TUSHARE_LOCAL_DATA.md](https://git.gomars.fun/boat/quant-os/src/branch/master/docs/TUSHARE_LOCAL_DATA.md)
- 本地运行手册：[TUSHARE_QLIB_LOCAL_RUN.md](https://git.gomars.fun/boat/quant-os/src/branch/master/docs/runbooks/TUSHARE_QLIB_LOCAL_RUN.md)

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

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

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

```mermaid
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 的范围是：

```text
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.py` 和 `adapters/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。

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

```text
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：

```text
id
path
row_count
byte_count
sha256
request_sha256
```

本次结果为 `221 jobs × 6 fields`，`mismatch_count=0`，
`converter_source_matches_current=true`。

### 4. 一键研究链

Makefile 已提供：

```text
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

正式本地位置：

```text
$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：

```text
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 nan` 和 `Mean of empty slice`。这主要来自停牌、
新进入成分但没有当日可用开盘价等稀疏情况；本次运行退出码为 0、结果可重放，
所以它是需要继续分类治理的数据告警，不是一次运行失败。

## 六、你现在怎样自己验证

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

```bash
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 数据：

```bash
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
```

预期看到：

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

重放已有研究：

```bash
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：

```bash
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_of`、`next_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_limit`、`suspend_d` 映射进 Quant OS 事件回测；
6. 登录聚宽后跑同窗口 hosted parity；
7. QMT 账号开通后先做只读和 shadow，再做最小真实订单演练。

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