第二章:数据准备——认识Qlib的数据格式、获取股票日线数据、数据预处理与清洗、数据存储与加载

做量化选股,数据就是你的弹药库。弹药不行,枪法再好也白搭。

我个人习惯,在开始任何模型构建之前,先花至少半天时间把数据底子打牢。Qlib 这套框架对数据格式有自己的一套规矩,说白了就是「你得按我的格式来,我才给你好好干活」。今天我们就来把这套规矩摸透。

2.1 Qlib 的数据格式长什么样?

Qlib 的数据格式,核心就两个字:bin。它不是我们常见的 CSV 或者 Excel,而是一种二进制格式。为什么?因为快。你想想看,回测时可能要加载几千只股票、几十年的日线数据,CSV 读一遍得等到猴年马月。

Qlib 的数据目录结构大概是这样的:

qlib_data/
├── cn_data/
│   ├── calendars/
│   │   └── day.txt          # 交易日历
│   ├── features/
│   │   ├── sh600000/
│   │   │   ├── open.bin
│   │   │   ├── high.bin
│   │   │   ├── low.bin
│   │   │   ├── close.bin
│   │   │   └── volume.bin
│   │   └── sh600001/
│   │       └── ...
│   └── instruments/
│       └── all.txt           # 股票列表

每个股票一个文件夹,里面每个字段一个 bin 文件。这种设计,我在项目中遇到过——当你需要只读取某只股票的收盘价时,不用把整个 CSV 都塞进内存,直接定位到 close.bin 文件,按偏移量读取就行。性能差距,十倍起步。

核心要点:Qlib 的数据是「宽表转长表」的思路。传统 DataFrame 是一行一个日期、一列一个字段。Qlib 是一个字段一个文件,一行一个日期。这种设计对时间序列的切片操作极其友好。

2.2 获取股票日线数据

数据从哪来?我一般用两个渠道:一是从聚宽、Tushare 这类平台下载,二是直接用 Qlib 自带的下载脚本。这里我推荐后者,省事。

Qlib 提供了一个命令行工具,一键下载中国 A 股日线数据:

# 安装 Qlib 后,运行以下命令
python scripts/get_data.py qlib_data --target_dir ~/.qlib/qlib_data/cn_data --region cn

这个脚本会帮你把 2000 年至今的日线数据全部拉下来,包括开盘价、收盘价、最高价、最低价、成交量、成交额、涨跌幅、换手率等基础字段。我第一次跑的时候,大概花了 40 分钟,数据量在 2GB 左右。

小技巧:如果你只想下载最近几年的数据,可以加个 --start_date 2020-01-01 参数。我平时做策略验证,用近 5 年的数据就足够了,太老的数据市场结构变化太大,参考意义有限。

当然,你也可以用自己的数据源。比如从 Wind 或者 Bloomberg 导出的 CSV,只要格式对,Qlib 也能吃进去。格式要求如下:

字段 说明 示例
date 交易日期 2024-01-15
symbol 股票代码 sh600000
open 开盘价 10.25
high 最高价 10.58
low 最低价 10.12
close 收盘价 10.45
volume 成交量(股) 12345678

2.3 数据预处理与清洗

数据拿到手,别急着用。真实市场的数据,脏得你想象不到。我曾经遇到过某只股票某天收盘价是负数——对,你没看错,负数。后来查了半天,是数据源那边的一个 bug。

所以,清洗是必须的。我一般做这几步:

  1. 去重:同一只股票同一天出现多条记录,保留第一条或者最后一条。
  2. 缺失值处理:停牌的日子,数据是空的。直接填充为前一天的数值,或者用插值法。
  3. 异常值过滤:涨跌幅超过 ±20% 的,大概率是数据错误,直接剔除。
  4. 复权处理:这个很重要。股票分红送股后,价格会跳空。如果不复权,你的回测结果会失真。Qlib 默认使用后复权。

代码实现大概是这样的:

import pandas as pd
import numpy as np

def clean_data(df):
    # 去重
    df = df.drop_duplicates(subset=['date', 'symbol'])
    
    # 缺失值填充:用前一天的数值
    df = df.sort_values(['symbol', 'date'])
    df = df.groupby('symbol').fillna(method='ffill')
    
    # 异常值过滤:涨跌幅超过 ±20% 的剔除
    df = df[(df['change'] > -0.2) & (df['change'] < 0.2)]
    
    return df

注意:复权处理一定要在计算技术指标之前做。我见过有人先算 MA5,再复权,结果 MA5 全算错了。顺序搞反,后面全白干。

2.4 数据存储与加载

清洗完的数据,怎么存进 Qlib?Qlib 提供了一个 dump_bin 工具,可以把 CSV 转成 bin 格式。

from qlib.data import D
from qlib.contrib.data.handler import Alpha158

# 从 CSV 转成 Qlib 格式
python scripts/dump_bin.py dump_all --csv_path ./my_data.csv --qlib_dir ~/.qlib/qlib_data/my_data --freq day

存好之后,加载就简单了:

# 加载数据
data = D.features(
    instruments=['sh600000', 'sh600001'],
    fields=['$open', '$close', '$volume', 'Ref($close, -1)'],
    start_time='2020-01-01',
    end_time='2024-12-31'
)

这里 Ref($close, -1) 表示取前一天的收盘价。Qlib 的表达式系统很强大,你可以直接在字段里写公式,不用先算好再存。我个人觉得这个设计非常聪明——数据存储和计算逻辑解耦了,换策略时不用重新存数据。

加载性能对比:同样 1000 只股票、5 年数据,CSV 加载需要 12 秒,Qlib bin 格式只需要 0.8 秒。这就是为什么量化框架都爱用二进制格式。

2.5 本章知识体系

下面这张图,帮你把数据准备的整个流程串起来:

数据准备流程 数据获取 Qlib下载 / 外部数据源 数据清洗 去重 / 缺失值 / 异常值 复权处理 后复权 / 前复权 格式转换 CSV → Qlib bin格式 数据存储 按股票/字段分文件存储 数据加载 D.features() ✅ 可用于模型训练的标准化数据 支持表达式查询、高性能读取、多股票并行

嗯,到这里,数据准备这块就讲完了。你可能会问:为什么花这么大篇幅讲数据?因为我在项目里吃过太多亏了。数据不对,模型再花哨也是垃圾。记住一句话:垃圾进,垃圾出

我的习惯:每次开始新策略之前,我都会先跑一遍数据完整性检查——看看有没有缺失日期、有没有异常跳空、复权是否一致。这一步花 10 分钟,能省后面 10 小时的 debug 时间。


公众号:蓝海数据掘金营,微信deep3321