3、数据获取与清洗:Wind/QB数据接口、债券行情数据、信用评级数据、数据清洗与对齐
做固收量化,第一步就是跟数据死磕。
我见过太多人,策略逻辑写得天花乱坠,结果一跑回测,全栽在数据上。说白了,数据不干净,后面全是白费功夫。今天咱们就把这块硬骨头啃下来。
3.1 数据接口:Wind vs QB
国内做固收,绕不开两个数据源:Wind 和 QB(聚源)。我个人习惯是两者都备着,互相校验。
Wind 接口:老牌劲旅,覆盖面广。但有个坑——它的 Python 接口(WindPy)在并发请求时容易卡死。我曾经在批量拉取 500 只信用债的日行情时,直接让 Wind 客户端崩溃了三次。后来学乖了,每次请求间隔 0.5 秒。
Wind 数据获取示例
from WindPy import w
w.start()
# 获取债券行情
data = w.wsd("240019.IB", "close,volume", "2024-01-01", "2024-12-31")
# 获取信用评级
rating = w.wsd("240019.IB", "rating_issuer", "2024-01-01", "2024-12-31")
QB 接口:相对轻量,对债券市场的细节数据支持更好。比如中债估值、经纪商报价这些,QB 的数据质量反而更高。
我的建议:用 Wind 拉宏观和基础行情,用 QB 补信用债和衍生品数据。两条腿走路,稳当。
3.2 债券行情数据:不只是价格
债券行情跟股票不一样。股票你盯着收盘价就行,债券不行。你想想看,债券市场是 OTC 市场,同一只债一天可能只有几笔成交,甚至零成交。
所以我们要拿的数据包括:
- 中债估值净价:这是估值,不是成交价。但它是市场公认的基准。
- 中债估值收益率:做固收策略的核心指标,比价格更重要。
- 经纪商报价:买卖双边报价,反映真实流动性。
- 成交数据:交易所和银行间的成交明细。
注意:不同数据源的估值可能差 10-20bp。我遇到过一只城投债,Wind 给的是 4.5%,QB 给的是 4.7%。这时候怎么办?我的做法是取均值,或者以中债登官方数据为准。
3.3 信用评级数据:别只看评级符号
信用评级是固收策略的灵魂。但这里有个大坑——评级迁移。
一只债今天还是 AAA,明天可能就被下调到 AA+。你的策略如果没考虑到这个,回测结果会严重失真。
我一般会拉取以下评级数据:
| 数据字段 | 说明 | 数据来源 |
|---|---|---|
| 主体评级 | 发行人的信用等级 | Wind/QB |
| 债项评级 | 具体债券的信用等级 | Wind/QB |
| 评级展望 | 正面/稳定/负面 | Wind |
| 评级调整日期 | 评级变动的时间点 | Wind |
避坑指南:我曾经在回测中直接用最新评级,结果策略表现特别好。后来发现,因为评级下调往往滞后于市场反应。正确的做法是用历史评级,也就是「当时」的评级,而不是「现在」的评级。
3.4 数据清洗:脏数据是策略杀手
数据清洗,说白了就是给数据「洗澡」。我总结了几类常见问题:
- 缺失值:债券停牌、节假日、数据源断供。
- 异常值:收益率突然跳到 100%,明显是数据录入错误。
- 重复值:同一只债在同一天出现多条记录。
- 单位不一致:有的数据源用百分比,有的用小数。
我的清洗流程是这样的:
# 伪代码示例
def clean_bond_data(df):
# 1. 去重
df = df.drop_duplicates(subset=['bond_code', 'date'])
# 2. 处理缺失值
df['yield'] = df.groupby('bond_code')['yield'].ffill()
# 3. 剔除异常值
df = df[(df['yield'] > 0) & (df['yield'] < 0.5)] # 收益率在0-50%之间
# 4. 统一单位
df['yield'] = df['yield'] / 100 # 转为小数
return df
小技巧:对于缺失的估值数据,别直接用前一天的值填充。债券市场变化快,我一般用线性插值,或者用同期限、同评级的债券收益率做回归填充。
3.5 数据对齐:让不同频率的数据「对齐」
这是最容易被忽视的一步。你的行情数据可能是日频,评级数据可能是月频,宏观数据可能是季频。怎么对齐?
我的原则是:以最细粒度为准,向上聚合。
- 日频数据:保留每一天
- 月频数据:取月末值,填充到当月每一天
- 季频数据:取季末值,填充到当季每一天
举个例子:
# 对齐示例
# 行情数据:每日
# 评级数据:每月1日更新
# 对齐后:每日都有评级数据(使用最近一次评级)
def align_data(price_df, rating_df):
# 将评级数据向前填充到每一天
rating_df = rating_df.set_index('date')
rating_df = rating_df.reindex(price_df['date'], method='ffill')
return pd.concat([price_df, rating_df], axis=1)
注意:向前填充(ffill)和向后填充(bfill)差别很大。评级数据只能用向前填充,因为未来的评级不能用于过去的决策。这个逻辑搞反了,回测结果就是错的。
3.6 知识体系总览
下面这张图,是我做数据获取与清洗的完整流程。你可以把它当成一个检查清单:
嗯,到这里,数据获取与清洗的核心内容就讲完了。记住一句话:数据质量决定策略上限。花 80% 的时间在数据上,剩下 20% 的时间写策略,这是固收量化的铁律。
公众号:蓝海资料掘金营,微信deep3321