☰
tushare ETF数据导入Qlib实战:字段映射与日期对齐指南
2026/10/21 5:40:18 网站建设 项目流程

1. 为什么要把ETF数据从tushare搬进Qlib

做量化研究的朋友大概率都经历过这个阶段:策略想法在脑子里转了很久,数据却迟迟凑不齐。tushare是国内做A股和ETF数据绕不开的一个数据源,接口稳定、字段齐全、积分门槛也不算高;而Qlib是微软开源的一套量化投研框架,从数据存储、因子计算到模型训练、回测,整条链路都给你搭好了。问题在于,这两个东西各说各话——tushare给你的是DataFrame,Qlib要的是它自己那套二进制格式。中间这层“翻译”工作,就是这篇要聊的核心。

我自己最开始做ETF轮动策略的时候,图省事直接把tushare拉下来的数据存成CSV,然后用pandas硬算。策略逻辑一复杂,回测速度就崩了,而且每次改一个参数就要重新跑一遍全量计算,效率低得让人抓狂。后来转到Qlib,最大的感受是它的数据层设计确实为量化场景做了很多优化:列式存储、表达式引擎、缓存机制,这些在频繁调参和滚动回测的时候优势非常明显。但前提是,你得先把数据正确地灌进去。

这篇内容适合两类人:一类是刚接触Qlib、手里已经有tushare数据想快速上手的;另一类是用过Qlib但卡在数据导入环节、被各种字段报错折磨过的。我会把整个流程拆开讲清楚,包括为什么要这么设计、每一步背后的逻辑是什么、以及我在实操中踩过的那些坑。整个流程不复杂,但细节不少,尤其是字段对齐和日期处理这两块,稍不注意就会埋下隐患。

2. 动手之前先把环境和数据源理清楚

2.1 tushare的接口权限与ETF数据范围

tushare的ETF数据主要通过fund_daily接口获取,这个接口返回的是场内ETF的日线行情,包含开盘价、最高价、最低价、收盘价、成交量、成交额等字段。需要注意的是,这个接口需要一定的积分权限,具体门槛以官方文档为准,我这里不展开。如果你积分不够,也可以先用fund_basic拿到ETF列表,再逐个用其他接口拼凑,但效率会低很多。

ETF和股票在数据结构上有一个关键区别:ETF没有复权概念(或者说复权处理方式不同),所以你在tushare拿到的价格就是实际交易价格。这一点在导入Qlib的时候要特别注意,因为Qlib默认的股票数据处理逻辑里会涉及复权因子,如果你直接套用股票的处理方式,算出来的收益率会出问题。

另外,tushare返回的字段名是英文的,比如ts_code、trade_date、open、high、low、close、vol、amount。这些字段名和Qlib要求的字段名不完全一致,中间需要做一层映射。我建议在写导入脚本之前,先把tushare返回的DataFrame打印出来看一眼,确认字段名和数据类型,避免后面反复调试。

2.2 Qlib的数据存储结构长什么样

Qlib的数据存储采用的是自定义的二进制格式,底层基于列式存储的思想。它把每个标的的数据按字段分开存储,比如所有ETF的收盘价存在一起,所有ETF的开盘价存在一起。这种设计的好处是,当你只需要某几个字段的时候,不需要把整张表读进来,读取效率很高。

Qlib的数据目录结构大致是这样的:根目录下有一个calendars文件夹存放交易日历,一个instruments文件夹存放标的列表,然后是features文件夹存放具体的特征数据。每个文件夹下面又按频率(比如日线)分子目录。理解这个结构很重要,因为你在导入数据的时候,实际上是在往这几个文件夹里写东西。

Qlib提供了一个dump_bin.py脚本用来把CSV数据转成它的二进制格式,但这个脚本对CSV的格式有比较严格的要求。很多人卡住就是因为CSV的列名或者索引不对,导致脚本报错或者导入后数据为空。我后面会详细讲怎么绕过这些坑。

2.3 版本兼容性:一个容易被忽略的坑

Qlib的版本迭代比较快,不同版本之间API有变化。我遇到过的情况是,在某个版本上跑通的导入脚本,升级Qlib之后直接报错。所以建议你在开始之前,先确认自己用的Qlib版本,然后对照该版本的文档来操作。如果你用的是比较新的版本,dump_bin.py的参数可能和老教程里写的不一样。

tushare这边也有类似问题,它的接口返回字段偶尔会调整。我的做法是在脚本里加一层字段校验,如果发现预期字段缺失,直接抛异常并打印实际字段列表,这样能快速定位问题,而不是等到数据导入Qlib之后才发现字段对不上。

3. 从tushare拉取ETF数据的完整操作

3.1 初始化与ETF列表获取

第一步是初始化tushare的接口。你需要自己的token,这个在tushare官网注册后就能拿到。初始化代码很简单:

import tushare as ts ts.set_token('你的token') pro = ts.pro_api()

接下来获取ETF列表。我一般用fund_basic接口,指定market='E'来筛选场内ETF:

etf_list = pro.fund_basic(market='E')

返回的DataFrame里包含ts_code、name、list_date等字段。这里有个细节:list_date是上市日期,你在后续拉行情的时候,起始日期不能早于这个日期,否则会返回空数据。我建议把list_date存下来,后面循环拉数据的时候用它作为起始日期的参考。

3.2 批量拉取日线行情与字段映射

拿到ETF列表之后,就可以循环拉取每只ETF的日线数据了。这里我不建议一次性拉全量,因为tushare对单次返回的数据量有限制,而且全量拉取容易超时。我的做法是按ETF逐个拉,每个ETF拉完之后立刻做字段重命名和类型转换,然后追加到一个总表里。

字段映射关系如下:

tushare字段Qlib字段说明
ts_codeinstrument标的代码,需要转换格式
trade_datedatetime交易日期,需要转成datetime类型
openopen开盘价
highhigh最高价
lowlow最低价
closeclose收盘价
volvolume成交量
amountamount成交额

注意ts_code的格式,tushare返回的是类似510300.SH这样的格式,而Qlib通常要求SH510300或者510300.SH这种格式,具体取决于你的配置。我一般统一转成SH510300这种前缀格式,因为Qlib的默认配置对这个格式支持比较好。

代码大概长这样:

import pandas as pd all_data = [] for code in etf_list['ts_code']: df = pro.fund_daily(ts_code=code, start_date='20150101', end_date='20241231') if df.empty: continue df = df.rename(columns={ 'trade_date': 'datetime', 'vol': 'volume' }) df['instrument'] = df['ts_code'].apply( lambda x: x.split('.')[1] + x.split('.')[0] ) df['datetime'] = pd.to_datetime(df['datetime'], format='%Y%m%d') all_data.append(df[['instrument', 'datetime', 'open', 'high', 'low', 'close', 'volume', 'amount']]) final_df = pd.concat(all_data, ignore_index=True)

3.3 数据清洗:停牌、缺失值与异常值处理

拉下来的数据不能直接用,必须做清洗。ETF虽然不像个股那样频繁停牌,但仍有几种情况需要处理:

第一种是停牌导致的缺失。如果某只ETF在某天没有交易,tushare可能直接不返回那天的记录,也可能返回一条成交量为0的记录。我的处理方式是,先按ETF和日期做重索引,把缺失的日期补上,然后用前向填充处理价格字段,成交量补0。但要注意,前向填充不能无限期使用,如果停牌时间过长,应该考虑把这段数据标记为无效。

第二种是异常值。ETF的价格波动一般不会太剧烈,如果某天的涨跌幅超过某个阈值(比如20%),大概率是数据错误。我一般会检查close字段的日收益率,把超过阈值的记录标记出来,人工确认后再决定是否剔除。

第三种是上市初期数据。有些ETF上市前几天的成交量极小,价格也不稳定,这些数据对策略训练可能是噪声。我的做法是剔除上市后前20个交易日的数据,等流动性稳定后再纳入。

3.4 日期对齐与交易日历的坑

Qlib有自己的交易日历,你在导入数据的时候,数据的日期必须和Qlib的交易日历对齐。如果tushare返回的日期和Qlib的日历不一致,导入后会出现数据错位。

我的做法是,先从Qlib的日历文件里读出交易日列表,然后把tushare的数据按这个列表做重索引。Qlib的日历文件在calendars目录下,是一个文本文件,每行一个日期。你可以直接读进来:

with open('qlib_data/calendars/day.txt', 'r') as f: trade_dates = [line.strip() for line in f.readlines()]

然后把你拉下来的数据按datetime字段和这个列表做对齐。不在列表里的日期直接丢弃,列表里有但数据里没有的日期补NaN。这一步做完之后,数据的日期维度就和Qlib完全一致了。

4. 把数据灌进Qlib的几种方式与选型建议

4.1 官方dump_bin.py脚本的使用与限制

Qlib官方提供了一个dump_bin.py脚本,位置在scripts目录下。这个脚本的基本用法是:

python dump_bin.py dump_all --csv_path ./csv_data --qlib_dir ./qlib_data --include_fields open,high,low,close,volume,amount

它要求CSV文件的格式是:第一列是日期,第一行是标的代码,每个单元格是对应的值。也就是说,你需要把数据转成“宽表”格式,而不是“长表”格式。这一点很多人一开始会搞错,因为tushare返回的是长表(每行一条记录),而dump_bin.py要的是宽表(每行一个日期,每列一个标的)。

这个脚本的优点是官方维护、兼容性好,缺点是灵活性差。比如你想在导入的时候做一些自定义的字段计算,它就做不了。另外,它对数据格式的要求比较严格,如果CSV里有空值或者格式不对,脚本可能直接报错退出,错误信息也不够友好。

4.2 自己写脚本直接写二进制文件

如果你对Qlib的数据格式比较熟悉,也可以自己写脚本直接生成二进制文件。Qlib的二进制格式其实不复杂,本质上就是把每个字段的数据按标的和日期组织成numpy数组,然后序列化。你可以参考Qlib源码里qlib/data/storage目录下的实现。

自己写脚本的好处是灵活,你可以在写入之前做任何自定义处理,比如动态计算复权因子、添加自定义字段等。缺点是容易出错,尤其是日期索引和标的索引的对齐,一旦搞错,数据就全乱了。我建议如果你不是特别熟悉Qlib的内部结构,还是先用官方脚本跑通流程,等有特殊需求再考虑自己写。

4.3 两种方式的对比与选择建议

对比维度dump_bin.py自写脚本
上手难度低高
灵活性低高
兼容性好取决于实现
调试难度中高
适合场景标准数据导入自定义字段、特殊处理

我的建议是:如果你是第一次把数据导入Qlib,先用dump_bin.py跑通全流程,确认数据能正常读取和回测。等你对Qlib的数据结构有了直观理解之后,再考虑自己写脚本做定制化处理。不要一上来就自己写,那样很容易在细节上卡住,浪费大量时间。

5. 导入后的验证:怎么确认数据真的可用

5.1 用Qlib的API读取数据做交叉验证

数据导入之后,第一件事是验证。Qlib提供了D接口来读取数据:

from qlib.data import D data = D.features( instruments=['SH510300'], fields=['$close', '$volume'], start_time='2020-01-01', end_time='2020-12-31' ) print(data.head())

把读出来的数据和tushare原始数据做对比,重点检查几个方面:日期范围是否一致、价格数值是否一致、成交量单位是否一致。我遇到过成交量单位不一致的情况,tushare返回的是手,而Qlib默认可能是股,如果不注意,回测出来的换手率会差100倍。

5.2 检查交易日历与标的列表是否完整

除了数据本身,还要检查Qlib的日历和标的列表。日历文件里应该包含你导入数据的所有日期,标的列表里应该包含你导入的所有ETF代码。如果发现缺失,说明导入过程中有问题,需要回头检查。

标的列表文件在instruments目录下,格式是每行一个标的代码,后面跟起始日期和结束日期。你可以用pandas读进来,和你预期的ETF列表做对比。

5.3 跑一个最小回测验证数据链路

最后一步,跑一个最简单的回测来验证整条链路。比如用双均线策略,只交易一只ETF,看看能不能正常出信号、正常计算收益。这一步不是为了验证策略好坏,而是为了确认数据从存储到读取到计算的全链路是通的。

如果回测报错,大概率是数据问题。常见的错误包括:字段名不对、日期格式不对、标的代码格式不对。根据报错信息逐个排查,一般都能解决。

6. 实操中踩过的坑与应对经验

6.1 字段类型不一致导致的静默错误

tushare返回的成交量字段是浮点数,但有些ETF的成交量是整数。如果你在导入的时候没有统一类型,Qlib读取的时候可能会出问题。我的做法是在导入前把所有数值字段统一转成float64,避免类型混乱。

另一个坑是日期字段。tushare返回的日期是字符串格式(如20200101),如果你忘记转成datetime,Qlib读取的时候会报错或者返回空数据。这个错误很隐蔽,因为不会直接报错,而是数据为空,让你以为是导入失败。

6.2 复权处理在ETF场景下的特殊性

前面提到过,ETF没有股票那种标准的复权因子。但ETF会有分红,分红会导致价格跳空。如果你不做任何处理,回测的时候会把分红当成亏损,影响策略评估。

我的处理方式是,从tushare获取ETF的分红记录,然后手动计算复权因子,在导入Qlib之前把价格调整好。tushare的分红接口是fund_div,返回每份分红金额和除权日期。你可以根据这些信息计算后复权价格。

具体计算逻辑是:对于每个除权日,复权因子等于除权日前一天收盘价除以(除权日前一天收盘价减去每份分红)。然后把这个因子累乘到后续所有价格上。这样处理之后,价格序列就是连续的了。

6.3 大数据量下的内存与速度优化

如果你要导入的ETF数量很多(比如几百只),或者时间跨度很长(比如十年以上),内存和速度会成为问题。我的优化经验是:

第一,不要一次性把所有数据读进内存。按ETF逐个处理,处理完一个写一个,最后再合并。这样可以大幅降低内存峰值。

第二,使用更高效的数据类型。比如价格字段用float32而不是float64,日期用int32存储而不是字符串。这些细节在数据量大的时候效果很明显。

第三,如果导入速度太慢,可以考虑用多进程并行处理。但要注意tushare的接口调用频率限制,不要因为并行导致被封禁。

6.4 增量更新与全量重建的取舍

数据导入不是一次性的工作,你需要定期更新。这时候有两个选择:增量更新和全量重建。

增量更新的好处是快,只处理新增的数据。缺点是容易出错,尤其是当历史数据有修正的时候,增量更新不会覆盖旧数据,导致数据不一致。

全量重建的好处是数据一致性好,每次都是从头来一遍。缺点是慢,尤其是数据量大的时候。

我的做法是:日常更新用增量,每个月做一次全量重建。这样既保证了日常效率,又定期清理了数据不一致的问题。增量更新的时候,我会记录上次更新的最后日期,然后从那天开始拉新数据,追加到现有数据后面。

7. 一些让流程更顺手的个人习惯

我在多次导入数据的过程中,逐渐形成了一些自己的操作习惯,这里分享出来供参考。

第一个习惯是,每次导入之前先备份现有的Qlib数据目录。导入过程中如果出错,可以直接回滚,不用重新拉数据。备份很简单,直接复制整个目录就行,虽然占点磁盘空间,但省心。

第二个习惯是,把导入脚本参数化。比如起始日期、结束日期、ETF列表、字段列表都做成命令行参数或者配置文件,这样每次调整不用改代码,直接改参数就行。我一般用argparse或者yaml配置文件来实现。

第三个习惯是,导入完成后自动生成一份数据质量报告。报告内容包括:导入的ETF数量、日期范围、每个字段的非空率、异常值数量等。这份报告能帮你快速判断数据是否可用,不用每次都手动检查。

第四个习惯是,把tushare的token和Qlib的数据路径放在环境变量里,不要硬编码在脚本中。这样脚本可以安全地分享给其他人,也方便在不同环境之间切换。

这些习惯看起来都是小事,但在长期的数据维护中,能帮你省下大量重复劳动和排查时间。尤其是数据质量报告这一条,我强烈建议你加上,它能在早期发现很多潜在问题,避免等到回测结果异常了才回头查数据。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询