第三章:数据获取与清洗——从原始数据到可用样本
做利率债分析,最怕什么?
不是模型不灵,不是策略不对。而是你拿到的数据本身就是脏的。我见过太多人花80%的时间调模型,最后发现是数据源出了问题。所以这一章,咱们把数据获取和清洗这件事彻底讲透。
3.1 数据接口调用:Wind vs 聚宽
国内做固收,Wind是标配。但聚宽在回测和量化场景下也有独特优势。我个人习惯是:生产环境用Wind,研究阶段用聚宽。
3.1.1 Wind Python接口
Wind的Python接口叫WindPy,安装后需要先启动Wind客户端。嗯,这里有个坑——Wind客户端必须开着,否则接口连不上。
from WindPy import w
w.start()
# 获取国债收益率曲线数据
# 代码:yield_cb_国债,日期范围
data = w.wsd("yield_cb_10y", "close", "2024-01-01", "2024-12-31")
print(data.Data[0][:5]) # 打印前5个数据点
我在项目中遇到过一个问题:Wind的wsd函数在节假日会返回空值。你以为数据丢了,其实只是那天没交易。所以后面一定要做日期对齐。
3.1.2 聚宽数据接口
聚宽的优势在于数据标准化做得好。它的get_price函数直接返回DataFrame,省去了很多格式转换的麻烦。
import jqdatasdk as jq
jq.auth('your_account', 'your_password')
# 获取国债期货主力合约数据
df = jq.get_price('T9999.CCFX', start_date='2024-01-01',
end_date='2024-12-31', frequency='daily',
fields=['open', 'close', 'high', 'low', 'volume'])
print(df.head())
3.2 日频数据清洗——基础但关键
日频数据看起来简单,但坑不少。我总结了一套「三步清洗法」:
- 日期对齐:确保所有时间序列的日期索引一致
- 缺失值处理:区分「真的缺失」和「假的缺失」
- 异常值检测:用统计方法找出离谱的数据点
3.2.1 日期对齐
不同数据源的交易日历可能不一样。Wind默认用上交所日历,聚宽用中金所日历。国债期货和国债现货的交易日也不完全重合。
import pandas as pd
# 假设有两个DataFrame:df_bond(国债收益率)和 df_future(国债期货)
# 对齐到共同的交易日
common_dates = df_bond.index.intersection(df_future.index)
df_bond_aligned = df_bond.loc[common_dates]
df_future_aligned = df_future.loc[common_dates]
# 或者用reindex填充缺失日期
all_dates = pd.date_range('2024-01-01', '2024-12-31', freq='B')
df_bond_aligned = df_bond.reindex(all_dates)
merge按日期合并!我曾经这么干过,结果发现两个数据源的日期格式不一样——一个用YYYY-MM-DD,一个用YYYY/MM/DD。合并后全是NaN,排查了整整一下午。
3.2.2 缺失值填充策略
缺失值怎么填?要看场景。
| 缺失类型 | 推荐方法 | 适用场景 |
|---|---|---|
| 单个缺失 | 前向填充(ffill) | 收益率曲线、估值数据 |
| 连续缺失(≤3天) | 线性插值 | 国债期货价格 |
| 连续缺失(>3天) | 标记为NaN,后续处理 | 流动性指标、成交数据 |
| 节假日缺失 | 不填充,保持原样 | 所有日频数据 |
# 前向填充
df['yield_10y'] = df['yield_10y'].ffill()
# 线性插值
df['future_price'] = df['future_price'].interpolate(method='linear')
# 标记连续缺失超过3天的数据
df['missing_flag'] = df['yield_10y'].isna().astype(int)
df['missing_group'] = df['missing_flag'].diff().ne(0).cumsum()
df['missing_count'] = df.groupby('missing_group')['missing_flag'].transform('sum')
df.loc[df['missing_count'] > 3, 'yield_10y'] = None
3.3 高频数据清洗——更细的颗粒度,更多的坑
高频数据(分钟级、Tick级)的清洗逻辑和日频完全不同。说白了,高频数据更「脏」,因为市场噪音、数据延迟、撮合机制都会引入异常。
3.3.1 时间戳对齐
交易所的Tick数据时间戳精确到毫秒,但不同数据源的时间戳格式可能不一样。我习惯统一转为datetime64[ms]格式。
# 假设原始时间戳是字符串 '2024-01-15 09:30:05.123'
df['timestamp'] = pd.to_datetime(df['timestamp'], format='%Y-%m-%d %H:%M:%S.%f')
# 对齐到整秒
df['timestamp_second'] = df['timestamp'].dt.floor('S')
# 按秒聚合
df_second = df.groupby('timestamp_second').agg({
'price': 'last',
'volume': 'sum'
}).reset_index()
3.3.2 异常值检测——3σ法则与IQR
高频数据里经常出现「跳空」——比如前一秒价格是100.02,下一秒变成100.50。这可能是数据错误,也可能是真实的市场异动。
我的做法是:先用3σ法则筛一遍,再用IQR(四分位距)做二次确认。
import numpy as np
# 3σ法则
mean = df['price'].mean()
std = df['price'].std()
df['outlier_3sigma'] = np.abs(df['price'] - mean) > 3 * std
# IQR方法
Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1
df['outlier_iqr'] = (df['price'] < Q1 - 1.5 * IQR) | (df['price'] > Q3 + 1.5 * IQR)
# 取交集:两个方法都标记为异常才剔除
df['final_outlier'] = df['outlier_3sigma'] & df['outlier_iqr']
df_clean = df[~df['final_outlier']]
3.4 知识体系总览
下面这张图是我自己梳理的「数据获取与清洗」全流程。每次做新项目,我都会先对照这张图检查一遍。
3.5 实战中的避坑指南
最后分享几个我踩过的坑,希望能帮你省点时间。
wsi接口获取分钟数据时,默认返回的是「收盘价」而非「最后成交价」。这两个概念在国债期货里差别很大——收盘价是加权平均,最后成交价是最后一笔。我建议用wsi时明确指定字段。
我曾经接手过一个项目,同事用聚宽的复权数据跑了三个月回测,策略表现好得离谱。后来一查,复权把移仓换月的价差全抹平了,策略实际上是在「作弊」。从那以后,我所有利率债数据都只用原始价格。
嗯,数据清洗这部分就讲到这里。记住一句话:脏数据进,脏结果出。花在清洗上的每一分钟,都会在后续分析中十倍回报给你。
公众号:蓝海资料掘金营,微信deep3321