3、数据准备与清洗:债券行情数据获取、财务数据对齐、异常值处理、复权与价格调整
做量化选债,有个残酷的现实我得先告诉你:80%的时间花在数据上,20%的时间才用来建模。这不是夸张,是我踩过无数坑之后的血泪总结。
债券数据有多脏?比股票数据脏得多。股票有统一的交易所、标准化的行情接口,债券呢?银行间、交易所、场外市场,数据源五花八门。我刚开始做债券量化时,天真地以为直接拉个Wind就能跑,结果回测曲线漂亮得不像话——后来发现是数据里掺了未来信息。
嗯,这一章我们就来聊聊,怎么把那些乱七八糟的原始数据,收拾成能用的样子。
3.1 债券行情数据的获取渠道与格式
先说说数据从哪来。我个人习惯把数据源分成三类:
| 数据源 | 覆盖范围 | 更新频率 | 成本 |
|---|---|---|---|
| Wind/Choice/聚宽 | 全市场债券 | T+1或实时 | 高(几万到十几万/年) |
| 中债登/上清所 | 银行间债券 | 日频 | 中(部分免费) |
| 交易所公开数据 | 交易所债券 | 实时 | 低(爬虫即可) |
这里有个坑:不同数据源对同一只债券的收盘价可能差好几个BP。为什么?因为债券交易是OTC的,没有统一的撮合价格。Wind取的可能是做市商报价的中位数,中债登用的是估值模型算出来的理论价,交易所则是实际成交价。
3.2 财务数据的对齐:时间戳的艺术
债券的财务数据对齐,比股票麻烦得多。股票一年出四次季报,时间相对固定。债券呢?信用债的发行人可能是非上市公司,年报拖到4月底才出是常事,有的甚至直接延期。
你想想看,如果你用2023年年报的财务数据去预测2023年1月的违约风险,这不就是典型的前瞻性偏差吗?
我个人习惯的做法是:
- 建立财务数据的时间戳映射表:每份财报对应一个「可用日期」,通常是报告期后推2-3个月(年报4月底、中报8月底、季报1个月后)
- 用「可用日期」对齐行情数据:比如2023年4月30日之后,才能使用2022年年报的数据
- 处理延迟披露:如果某只债券的发行人延期披露,那就用上一期的数据填充,直到新数据可用
核心原则:宁可数据旧一点,也不能用未来的数据。回测中多赚的每一分钱,实盘里都会加倍还回去。
3.3 异常值处理:那些离谱的价格是怎么来的
债券市场的异常值,比股票市场多得多。股票如果出现-10%的涨跌幅,大概率是乌龙指。债券呢?可能只是正常的OTC交易。
我遇到过最离谱的一次:某只城投债的收盘价突然从100块跌到60块,吓得我赶紧检查持仓。后来发现是有一笔大宗交易,面值1亿的债券以60块成交了1000万——这明显是机构之间的利益输送,根本不是市场公允价。
怎么处理?我的经验是分三步:
- 第一步:价格合理性检验。债券价格应该在[0, 200]之间(净价),超出这个范围的直接标记为缺失。为什么是200?因为有些深度违约的债券可能跌到十几块,但超过200的绝对是数据错误。
- 第二步:波动率过滤。计算每个债券的日收益率,如果某天的收益率超过过去60天标准差的5倍,就标记为可疑。注意,这里不能用绝对阈值,因为不同债券的波动率差异很大。
- 第三步:人工复核。对于标记为可疑的数据,我会去查当天的成交明细。如果是大宗交易或者协议转让,就剔除;如果是正常交易,就保留。
一个小技巧:对于异常值,我倾向于「剔除」而不是「填充」。因为债券市场的流动性差,异常值往往伴随着信息冲击,填充反而会引入噪声。当然,如果你做的是高频策略,那就另当别论了。
3.4 复权与价格调整:别让分红搞乱你的回测
债券也有复权问题?很多人觉得债券只有利息,没有分红,不需要复权。其实不然。
债券的「分红」就是付息。付息之后,债券的净价会下降(因为利息被剥离了),但全价不变。如果你直接用净价做回测,就会在付息日看到一个「假摔」——价格突然掉下去,然后又慢慢涨回来。
我个人习惯用全价做回测,因为全价包含了应计利息,能真实反映持有期的收益。但全价也有问题:不同债券的计息基准不同(ACT/365、30/360等),计算应计利息时容易出错。
这里给出一段我常用的复权处理代码:
# 债券复权处理示例(Python伪代码)
def bond_adjust_price(df):
"""
df包含字段:净价、票面利率、付息频率、计息基准
返回:调整后的全价序列
"""
# 计算应计利息
df['accrued_interest'] = df.apply(
lambda x: calc_accrued_interest(
x['coupon_rate'],
x['last_coupon_date'],
x['settlement_date'],
x['day_count_basis']
),
axis=1
)
# 全价 = 净价 + 应计利息
df['full_price'] = df['clean_price'] + df['accrued_interest']
# 处理付息日:付息后净价跳降,但全价连续
# 这里用前向填充法处理缺失的应计利息
df['full_price'] = df['full_price'].fillna(method='ffill')
return df
嗯,这里要注意:不同债券的付息频率不一样。国债一年付息一次,信用债可能一年两次,还有一些永续债是每季度付息。写代码的时候一定要把这个参数传进去,否则算出来的应计利息全是错的。
3.5 本章知识体系总览
说了这么多,我画了一张图帮你理清思路。数据准备与清洗,说白了就是三个核心环节:获取 → 对齐 → 清洗。每个环节都有各自的坑,但只要你按照这个流程走一遍,基本能过滤掉90%的数据问题。
这张图看起来简单,但每个环节展开都是一门学问。数据获取阶段,你要搞清楚不同数据源的价格定义;数据对齐阶段,你要跟时间赛跑,确保每一笔数据都在正确的时间点被使用;数据清洗阶段,你要像侦探一样,找出那些看似合理实则离谱的异常值。
说实话,我见过太多人在这上面栽跟头了。有的团队花了几百万买数据,结果因为没做复权处理,回测结果全是假的。有的研究员辛辛苦苦跑出来的因子,一上实盘就失效,查了半天是财务数据对齐出了问题。
所以我的建议是:在数据准备阶段多花点时间,后面建模的时候你会感谢自己的。
最后说一句:数据清洗没有一劳永逸的方法。市场在变,数据源在变,你的清洗规则也要跟着变。保持警惕,保持怀疑,这是量化研究员最基本的素养。
公众号:蓝海资料掘金营,微信deep3321