数据获取与清洗:打好CTA策略的地基
做CTA策略这么多年,我越来越觉得一个道理:策略的收益上限,往往在数据准备阶段就已经决定了。你想想看,再牛逼的模型,喂进去的是脏数据,出来的结果能靠谱吗?
这一章,我们就来聊聊数据获取与清洗的那些事儿。我会用tushare和akshare这两个库,带你走一遍完整的流程。嗯,这里要注意,我默认你已经装好了这两个库,如果还没装,先跑个 pip install tushare akshare 再说。
核心观点: 数据质量 > 策略复杂度。一个简单的均线策略,配上干净的数据,往往比一个复杂的机器学习模型配上脏数据更赚钱。
3.1 数据获取:从哪儿拿?怎么拿?
我个人习惯用akshare,因为它免费且数据源比较全。tushare也不错,但pro版本需要积分。咱们先看akshare怎么拿期货数据。
import akshare as ak
import pandas as pd
# 获取螺纹钢主力连续合约数据
# 注意:akshare的接口有时会变,建议用前查下文档
symbol = "RB" # 螺纹钢
start_date = "2020-01-01"
end_date = "2024-12-31"
df = ak.futures_main_sina(symbol=symbol,
start_date=start_date,
end_date=end_date)
print(df.head())
这里有个坑,我踩过好几次。主力连续合约并不是真实存在的合约,它是把不同月份的主力合约拼接起来的。这就带来了一个问题:换月时的跳空。
避坑指南: 我曾经直接用主力连续合约做回测,结果发现策略在换月日附近表现异常。后来才意识到,是换月跳空导致的虚假信号。所以,拿到数据后,第一件事就是检查换月点。
3.2 数据清洗:脏数据怎么处理?
数据拿到手,别急着用。先看看有没有缺失值、异常值。我一般会按以下步骤来:
- 检查缺失值:用
df.isnull().sum()看看哪些列有空值 - 处理缺失值:对于期货数据,我倾向于用前向填充(ffill),因为价格是连续的
- 检查异常值:比如价格突然跳涨100%,那多半是数据错误
- 处理异常值:可以用中位数替换,或者直接删除
# 缺失值处理
df.fillna(method='ffill', inplace=True) # 前向填充
# 异常值检测:用3倍标准差法
from scipy import stats
z_scores = stats.zscore(df['close'])
abs_z_scores = np.abs(z_scores)
filtered_entries = (abs_z_scores < 3)
df_clean = df[filtered_entries]
你可能会问:为什么不用均值填充?说实话,对于时间序列数据,均值填充会破坏时序结构。前向填充更符合市场逻辑——今天的价格不知道,那就用昨天的。
3.3 复权处理:还原真实交易
复权处理是CTA策略里最容易忽略的一环。说白了,就是要把换月时的跳空给抹平。不然你的策略会以为价格真的涨了或跌了那么多,其实只是换了个合约。
我常用的复权方法有两种:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 前复权 | 调整历史价格,使换月点前后价格连续 | 回测时常用,能真实反映策略表现 |
| 后复权 | 调整当前价格,保持历史价格不变 | 实盘交易时更直观 |
# 简单的复权处理示例
def adjust_price(df, method='forward'):
"""
对主力连续合约进行复权处理
method: 'forward' 前复权, 'backward' 后复权
"""
df = df.copy()
# 计算每日收益率
df['return'] = df['close'].pct_change()
if method == 'forward':
# 前复权:从后往前调整
df['adj_close'] = df['close'].iloc[-1] / (1 + df['return']).cumprod()
else:
# 后复权:从前往后调整
df['adj_close'] = df['close'].iloc[0] * (1 + df['return']).cumprod()
return df
小技巧: 我建议你在复权后,画个图看看价格曲线是否平滑。如果还有明显的跳空,说明复权没做好,需要检查换月点的处理逻辑。
3.4 数据存储:HDF5 vs Parquet
数据清洗完了,得存起来。我个人比较推荐用Parquet格式,原因有三:
- 压缩率高:同样的数据,Parquet比CSV小5-10倍
- 读取快:支持列式存储,只读需要的列
- 兼容性好:Pandas、Spark、Dask都能直接读
# 存储为Parquet
df_clean.to_parquet('RB_main_contract.parquet',
compression='snappy')
# 读取Parquet
df = pd.read_parquet('RB_main_contract.parquet')
# 如果数据量特别大(比如全品种全周期),可以用HDF5
# HDF5支持按日期分块存储,读取时更灵活
import h5py
with h5py.File('futures_data.h5', 'w') as f:
f.create_dataset('RB', data=df_clean.values)
你想想看,如果你每天都要跑全品种回测,用CSV光读取就要花几分钟,换成Parquet可能几秒就搞定了。时间就是金钱啊。
3.5 知识体系总览
说了这么多,咱们用一张图来总结一下本章的核心逻辑:
这张图把整个流程串起来了。你会发现,数据获取只是第一步,真正的功夫在清洗和复权上。我见过太多人,数据一拿到就急着跑策略,结果亏了钱还不知道问题出在哪。
最后说一句: 数据准备这件事,花再多时间都不为过。你想想看,一个策略跑十年,数据质量差1%,累积下来就是巨大的误差。所以,别嫌麻烦,把这一步做扎实了。