数据获取与清洗:套利策略的基石
做套利回测,最怕什么?不是策略写错了,而是数据本身有问题。我见过太多人花几个月写策略,结果一跑回测,收益曲线漂亮得不行——后来发现是数据里有未来函数。嗯,这种坑,踩一次就够了。
今天我们就来聊聊数据获取与清洗。说白了,就是怎么把原始数据变成能用的、干净的、对齐的数据。这是所有套利策略的第一步,也是最容易被忽视的一步。
一、数据源的选择:Tushare vs AKShare
做量化交易,数据源是绕不开的话题。我个人习惯用两个库:Tushare 和 AKShare。它们各有千秋,我分别说说。
1.1 Tushare:老牌选手,数据质量高
Tushare 是国内最早一批开放金融数据的库。它的优点是数据质量高,字段规范。但缺点也很明显——需要积分。免费用户只能拿到基础数据,高级功能要付费。
我在项目中遇到过一个问题:Tushare 的日线数据默认是前复权的。如果你要做套利,尤其是跨品种套利,复权方式一定要统一。否则两个品种的价差曲线会失真。
adj='qfq' 或 adj='hfq',保持复权方式一致。
import tushare as ts
# 设置 token
ts.set_token('your_token_here')
pro = ts.pro_api()
# 获取平安银行日线数据
df = pro.daily(ts_code='000001.SZ', start_date='20230101', end_date='20231231')
print(df.head())
1.2 AKShare:开源免费,覆盖面广
AKShare 是后来崛起的库,完全开源免费。它的数据源来自各大财经网站,覆盖面很广——股票、期货、期权、外汇都有。
但要注意,AKShare 的数据质量参差不齐。有些接口返回的数据字段名不统一,需要自己清洗。说白了,它把脏活累活留给了你。
import akshare as ak
# 获取期货主力合约数据
df = ak.futures_main_sina(symbol="RB0")
print(df.head())
ak.__version__ 确认版本。
二、数据清洗:去重与缺失值处理
数据拿到手,第一件事不是分析,而是清洗。你想想看,如果数据里有重复行,或者某天数据缺失,回测结果能准吗?
2.1 去重:别让重复数据骗了你
数据重复的原因很多:网络重传、接口重复调用、数据源本身的问题。我曾经在回测中发现某天的收益率异常高,查了半天,原来是那天的数据重复了三次,导致资金曲线被重复计算。
去重其实很简单,用 pandas 的 drop_duplicates() 就行。但要注意,一定要指定去重的依据——通常是时间戳和代码。
import pandas as pd
# 假设 df 是原始数据
df = df.drop_duplicates(subset=['trade_date', 'ts_code'], keep='last')
print(f"去重后数据量: {len(df)}")
keep='last' 保留最后一条记录。如果数据源有更新,最后一条通常是最新的。
2.2 缺失值处理:别让空值毁了策略
缺失值在金融数据里很常见。比如节假日没有交易数据,或者某只股票停牌了。处理缺失值,我一般分三步走:
- 检查缺失情况:用
isnull().sum()看看哪些字段有缺失 - 判断缺失原因:是正常缺失(如节假日)还是异常缺失(如数据丢失)
- 选择处理方法:填充、删除、或者插值
# 检查缺失值
print(df.isnull().sum())
# 向前填充(用上一个交易日的数据填充)
df = df.fillna(method='ffill')
# 或者直接删除缺失行
df = df.dropna(subset=['close', 'open', 'high', 'low'])
三、数据对齐:时间戳对齐的艺术
做套利,最核心的就是价差。但两个品种的交易时间可能不一样。比如股指期货和ETF,期货是9:30开盘,ETF也是9:30,但期货有夜盘,ETF没有。你想想看,如果不对齐时间戳,价差计算就是错的。
3.1 为什么需要对齐?
举个例子。你想做沪深300股指期货和沪深300ETF的套利。期货的交易时间是9:30-11:30, 13:00-15:00,外加夜盘21:00-23:00。ETF只有白天交易。如果你直接把两个数据拼在一起,夜盘的数据会让价差曲线出现巨大的跳跃。
我在项目中遇到过更坑的情况:两个品种的分钟数据,一个用的是自然时间,一个用的是交易时间。自然时间包含非交易时段,交易时间只包含交易时段。如果不处理,回测结果完全失真。
3.2 对齐方法:重采样与合并
对齐的核心思路是:找到一个共同的时间频率,然后把两个数据都重采样到这个频率上。
# 假设 df1 是期货数据,df2 是ETF数据
# 先确保时间戳是 datetime 类型
df1['trade_time'] = pd.to_datetime(df1['trade_time'])
df2['trade_time'] = pd.to_datetime(df2['trade_time'])
# 设置时间戳为索引
df1 = df1.set_index('trade_time')
df2 = df2.set_index('trade_time')
# 重采样到1分钟频率,只保留交易时段
df1_aligned = df1.between_time('09:30', '15:00').resample('1T').last()
df2_aligned = df2.between_time('09:30', '15:00').resample('1T').last()
# 合并两个数据框
df_merged = pd.merge(df1_aligned, df2_aligned, left_index=True, right_index=True, suffixes=('_fut', '_etf'))
print(df_merged.head())
resample('1T') 时没注意,结果把非交易时段的数据也包含进来了。后来加了 between_time() 才解决。记住,先过滤再重采样。
3.3 对齐后的检查
对齐完成后,一定要检查一下。我一般会看两个指标:
- 数据长度是否一致:如果两个品种的数据量差太多,说明对齐有问题
- 时间戳是否连续:用
diff()检查时间间隔是否均匀
# 检查时间戳是否连续
time_diff = df_merged.index.to_series().diff().dropna()
print(f"时间间隔统计: \n{time_diff.describe()}")
# 如果最大间隔超过1分钟,说明有缺失
if time_diff.max() > pd.Timedelta('1 minute'):
print("警告:存在时间间隔超过1分钟的数据")
四、完整流程示例
说了这么多,我们来走一遍完整的流程。假设我们要获取沪深300股指期货和沪深300ETF的1分钟数据,然后清洗对齐。
import akshare as ak
import pandas as pd
# 1. 获取数据
futures_df = ak.futures_minute_sina(symbol="IF0", period="1")
etf_df = ak.fund_etf_hist_em(symbol="510300", period="1")
# 2. 清洗:去重
futures_df = futures_df.drop_duplicates(subset=['date', 'time'])
etf_df = etf_df.drop_duplicates(subset=['date', 'time'])
# 3. 清洗:缺失值处理
futures_df = futures_df.fillna(method='ffill')
etf_df = etf_df.fillna(method='ffill')
# 4. 对齐:统一时间戳
futures_df['trade_time'] = pd.to_datetime(futures_df['date'] + ' ' + futures_df['time'])
etf_df['trade_time'] = pd.to_datetime(etf_df['date'] + ' ' + etf_df['time'])
futures_df = futures_df.set_index('trade_time')
etf_df = etf_df.set_index('trade_time')
# 只保留交易时段
futures_aligned = futures_df.between_time('09:30', '15:00').resample('1T').last()
etf_aligned = etf_df.between_time('09:30', '15:00').resample('1T').last()
# 5. 合并
merged = pd.merge(futures_aligned, etf_aligned, left_index=True, right_index=True, suffixes=('_fut', '_etf'))
print(f"最终数据量: {len(merged)}")
print(merged.head())
嗯,数据这块就聊到这儿。记住,花80%的时间在数据上,20%的时间在策略上。数据干净了,策略自然就稳了。