3、数据准备与清洗:债券行情数据获取、财务数据对齐、异常值处理、复权与价格调整

做量化选债,有个残酷的现实我得先告诉你:80%的时间花在数据上,20%的时间才用来建模。这不是夸张,是我踩过无数坑之后的血泪总结。

债券数据有多脏?比股票数据脏得多。股票有统一的交易所、标准化的行情接口,债券呢?银行间、交易所、场外市场,数据源五花八门。我刚开始做债券量化时,天真地以为直接拉个Wind就能跑,结果回测曲线漂亮得不像话——后来发现是数据里掺了未来信息。

嗯,这一章我们就来聊聊,怎么把那些乱七八糟的原始数据,收拾成能用的样子。

3.1 债券行情数据的获取渠道与格式

先说说数据从哪来。我个人习惯把数据源分成三类:

数据源 覆盖范围 更新频率 成本
Wind/Choice/聚宽 全市场债券 T+1或实时 高(几万到十几万/年)
中债登/上清所 银行间债券 日频 中(部分免费)
交易所公开数据 交易所债券 实时 低(爬虫即可)

这里有个坑:不同数据源对同一只债券的收盘价可能差好几个BP。为什么?因为债券交易是OTC的,没有统一的撮合价格。Wind取的可能是做市商报价的中位数,中债登用的是估值模型算出来的理论价,交易所则是实际成交价。

我曾经在回测中发现一个策略年化收益差了3%,查了两天才发现是数据源不一致导致的。从那以后,我定了个规矩:回测和实盘必须用同一个数据源,至少要保持价格生成逻辑一致。

3.2 财务数据的对齐:时间戳的艺术

债券的财务数据对齐,比股票麻烦得多。股票一年出四次季报,时间相对固定。债券呢?信用债的发行人可能是非上市公司,年报拖到4月底才出是常事,有的甚至直接延期。

你想想看,如果你用2023年年报的财务数据去预测2023年1月的违约风险,这不就是典型的前瞻性偏差吗?

我个人习惯的做法是:

  1. 建立财务数据的时间戳映射表:每份财报对应一个「可用日期」,通常是报告期后推2-3个月(年报4月底、中报8月底、季报1个月后)
  2. 用「可用日期」对齐行情数据:比如2023年4月30日之后,才能使用2022年年报的数据
  3. 处理延迟披露:如果某只债券的发行人延期披露,那就用上一期的数据填充,直到新数据可用

核心原则:宁可数据旧一点,也不能用未来的数据。回测中多赚的每一分钱,实盘里都会加倍还回去。

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%的数据问题。

债券数据准备与清洗流程 数据获取 Wind / 中债登 / 交易所 数据对齐 财务数据时间戳映射 数据清洗 异常值 / 复权调整 获取要点 • 多数据源交叉验证 • 区分净价/全价/估值价 • 注意OTC交易特殊性 • 记录数据源版本号 对齐要点 • 建立可用日期映射表 • 处理延迟披露情况 • 避免前瞻性偏差 • 保持数据颗粒度一致 清洗要点 • 价格合理性检验 • 波动率异常过滤 • 全价复权处理 • 人工复核可疑数据 核心原则:宁可数据旧,不可数据错

这张图看起来简单,但每个环节展开都是一门学问。数据获取阶段,你要搞清楚不同数据源的价格定义;数据对齐阶段,你要跟时间赛跑,确保每一笔数据都在正确的时间点被使用;数据清洗阶段,你要像侦探一样,找出那些看似合理实则离谱的异常值。

说实话,我见过太多人在这上面栽跟头了。有的团队花了几百万买数据,结果因为没做复权处理,回测结果全是假的。有的研究员辛辛苦苦跑出来的因子,一上实盘就失效,查了半天是财务数据对齐出了问题。

所以我的建议是:在数据准备阶段多花点时间,后面建模的时候你会感谢自己的

最后说一句:数据清洗没有一劳永逸的方法。市场在变,数据源在变,你的清洗规则也要跟着变。保持警惕,保持怀疑,这是量化研究员最基本的素养。


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