第三章:数据获取与清洗——从原始数据到可用样本

做利率债分析,最怕什么?

不是模型不灵,不是策略不对。而是你拿到的数据本身就是脏的。我见过太多人花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())
我的建议:如果你刚开始做利率债分析,先用聚宽跑通流程。等需要高频数据或定制化数据时,再切换到Wind。别一上来就搞两个接口,容易把自己绕晕。

3.2 日频数据清洗——基础但关键

日频数据看起来简单,但坑不少。我总结了一套「三步清洗法」:

  1. 日期对齐:确保所有时间序列的日期索引一致
  2. 缺失值处理:区分「真的缺失」和「假的缺失」
  3. 异常值检测:用统计方法找出离谱的数据点

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σ对极端值敏感,IQR对偏态分布更稳健。两个方法都认为异常,那基本可以确定是数据错误。如果只有一个方法标记,我会保留数据,但加一个「可疑」标签。

3.4 知识体系总览

下面这张图是我自己梳理的「数据获取与清洗」全流程。每次做新项目,我都会先对照这张图检查一遍。

利率债数据获取与清洗全流程 数据源层 Wind接口(wsd/wsi) | 聚宽接口(get_price) | 交易所原始数据 日频数据清洗 日期对齐 → 缺失值处理 异常值检测(3σ + IQR) 节假日处理 → 数据持久化 高频数据清洗 时间戳对齐 → 聚合降频 跳空检测 → 噪音过滤 Tick级异常标记 → 重采样 数据合并与对齐 多源数据交叉验证 → 统一交易日历 → 生成面板数据 可用数据集 收益率曲线 | 流动性指标 | 波动率特征 | 价差序列

3.5 实战中的避坑指南

最后分享几个我踩过的坑,希望能帮你省点时间。

避坑1:Wind的wsi接口获取分钟数据时,默认返回的是「收盘价」而非「最后成交价」。这两个概念在国债期货里差别很大——收盘价是加权平均,最后成交价是最后一笔。我建议用wsi时明确指定字段。
避坑2:聚宽的数据有「复权」选项。做利率债分析时,千万别用复权数据!国债期货的复权逻辑和股票完全不同,用了复权数据,你的回测结果会完全失真。

我曾经接手过一个项目,同事用聚宽的复权数据跑了三个月回测,策略表现好得离谱。后来一查,复权把移仓换月的价差全抹平了,策略实际上是在「作弊」。从那以后,我所有利率债数据都只用原始价格。

嗯,数据清洗这部分就讲到这里。记住一句话:脏数据进,脏结果出。花在清洗上的每一分钟,都会在后续分析中十倍回报给你。


公众号:蓝海资料掘金营,微信deep3321