数据获取与清洗:用Python搞定债券数据

做债券量化,最头疼的事是什么?

我个人觉得,不是策略设计,也不是模型调参,而是——数据。债券市场的数据源分散、格式混乱、缺失值多,这几乎是每个固收量化工程师的噩梦。我刚开始做债券指数跟踪策略时,光数据清洗就花了两周时间,那会儿真想摔键盘。

今天咱们就来聊聊,怎么用Python把债券行情数据、信用评级数据、久期数据拿到手,再把这些「脏数据」洗得干干净净。

数据获取:从哪里来?

债券数据不像股票那么方便,Wind、聚宽、Tushare这些平台各有各的接口。我习惯用Wind的Python API,但考虑到大家可能没有Wind账号,咱们用公开数据源来演示。

核心数据源一览

数据类型 常用来源 获取方式
债券行情数据 中国债券信息网、上交所 API / 爬虫
信用评级数据 中债资信、联合评级 PDF解析 / 数据库
久期数据 中债估值中心 CSV下载 / API

举个例子,获取某只国债的日频行情数据,我一般这么写:

import pandas as pd
import requests
from datetime import datetime, timedelta

def get_bond_quote(bond_code, start_date, end_date):
    """
    获取债券行情数据
    bond_code: 债券代码,如 '019547'
    """
    url = f"https://api.example.com/bond/{bond_code}"
    params = {
        'start': start_date,
        'end': end_date,
        'fields': 'close,volume,open_interest'
    }
    resp = requests.get(url, params=params)
    data = resp.json()
    df = pd.DataFrame(data)
    df['trade_date'] = pd.to_datetime(df['trade_date'])
    return df.set_index('trade_date')

# 获取近30天数据
df_quote = get_bond_quote('019547', 
                          (datetime.now() - timedelta(days=30)).strftime('%Y%m%d'),
                          datetime.now().strftime('%Y%m%d'))
print(df_quote.head())

嗯,这里要注意:债券代码的编码规则和股票不一样,上交所的国债是01开头,企业债是12开头。我曾经因为代码搞错,跑了一周的回测才发现数据全是错的……

信用评级数据:从PDF里「抠」出来

信用评级数据是最让人头疼的。评级机构发布的PDF报告,格式五花八门。我试过用PDFPlumber、Camelot这些库,但效果都不太理想。

后来我换了个思路——直接爬取评级机构的网页数据。很多评级机构会在官网上公布评级结果,用BeautifulSoup就能搞定。

from bs4 import BeautifulSoup
import re

def parse_rating_page(html_content):
    """
    解析评级页面,提取债券代码和评级结果
    """
    soup = BeautifulSoup(html_content, 'html.parser')
    rows = soup.find_all('tr')
    
    ratings = []
    for row in rows:
        cols = row.find_all('td')
        if len(cols) >= 3:
            bond_code = cols[0].text.strip()
            rating = cols[1].text.strip()
            rating_date = cols[2].text.strip()
            ratings.append({
                'bond_code': bond_code,
                'rating': rating,
                'rating_date': rating_date
            })
    return pd.DataFrame(ratings)

# 示例:解析本地HTML文件
with open('rating_page.html', 'r', encoding='utf-8') as f:
    html = f.read()
df_ratings = parse_rating_page(html)
print(df_ratings.head())

避坑指南

我曾经遇到过一个坑:某只债券的评级从AA+下调到AA,但评级机构在PDF里用的是「AA+→AA」这种格式,解析时没处理好,导致数据丢失。后来我加了个正则表达式,专门处理这种带箭头的格式。

久期数据:别被「修正久期」骗了

久期数据看起来简单,但坑不少。中债估值中心提供的久期数据,有「久期」和「修正久期」两个字段。很多人直接用「久期」做分析,但实际计算债券价格变动时,应该用「修正久期」。

我一般这样获取和处理久期数据:

def get_duration_data(bond_list, date):
    """
    获取多只债券的久期数据
    bond_list: 债券代码列表
    date: 日期字符串 'YYYYMMDD'
    """
    # 模拟从数据源获取
    duration_data = []
    for bond in bond_list:
        # 实际项目中这里调用API或读取本地文件
        duration_info = {
            'bond_code': bond,
            'date': date,
            'modified_duration': 4.52,  # 示例数据
            'macaulay_duration': 4.78,
            'convexity': 0.23
        }
        duration_data.append(duration_info)
    
    df = pd.DataFrame(duration_data)
    # 计算修正久期与麦考利久期的差异
    df['duration_diff'] = df['macaulay_duration'] - df['modified_duration']
    return df

# 示例
bonds = ['019547', '019548', '019549']
df_duration = get_duration_data(bonds, '20240315')
print(df_duration[['bond_code', 'modified_duration', 'macaulay_duration']])

重要提醒

修正久期和麦考利久期的差异,在利率波动大时尤其明显。我见过有人直接用麦考利久期计算DV01,结果偏差了5%以上。记住:做风险对冲时,一定用修正久期。

数据清洗与对齐:最磨人的环节

数据拿到手了,但三个数据源的日期、代码格式都不一样。行情数据是日频,评级数据可能几个月才更新一次,久期数据又是周频。怎么对齐?

我的做法是三步走:

  1. 统一日期索引:把所有数据都转成日频,缺失值用前向填充
  2. 统一债券代码:不同数据源的代码格式可能不同,比如行情数据用6位代码,评级数据用8位。我统一转成8位,前面补0
  3. 处理异常值:比如某天行情数据突然跳涨10%,明显是数据错误,需要剔除或插值
def align_bond_data(quote_df, rating_df, duration_df):
    """
    对齐三个数据源
    """
    # 1. 统一日期索引
    all_dates = pd.date_range(start=quote_df.index.min(), 
                              end=quote_df.index.max(), freq='D')
    
    # 2. 行情数据重采样到日频
    quote_aligned = quote_df.reindex(all_dates).ffill()
    
    # 3. 评级数据前向填充
    rating_aligned = rating_df.reindex(all_dates).ffill()
    
    # 4. 久期数据插值(线性插值)
    duration_aligned = duration_df.reindex(all_dates).interpolate(method='linear')
    
    # 5. 合并所有数据
    merged = pd.concat([quote_aligned, rating_aligned, duration_aligned], axis=1)
    
    # 6. 删除全为NaN的行
    merged = merged.dropna(how='all')
    
    return merged

# 示例
merged_data = align_bond_data(df_quote, df_ratings, df_duration)
print(f"对齐后数据量: {len(merged_data)} 行")
print(merged_data.head())

核心逻辑流程图

行情数据 信用评级数据 久期数据 步骤1:统一日期索引(日频重采样) 步骤2:统一债券代码格式(6位→8位) 步骤3:缺失值处理(前向填充/线性插值) ✅ 对齐后的完整数据集

实战中的那些坑

数据清洗这件事,说起来简单,做起来全是泪。我分享几个真实踩过的坑:

  • 节假日问题:债券市场有调休日,有些交易日行情数据是空的。我一开始用简单的ffill填充,结果把非交易日的数据也填进去了。后来加了个交易日历判断。
  • 评级滞后:评级调整公告后,行情数据可能当天就反应了,但评级数据要等几天才更新。对齐时要注意时间窗口。
  • 代码重复:同一只债券在不同数据源里可能代码不一样。比如「19国债03」在行情系统里是019547,在评级系统里可能是101903。我建了个映射表来解决。

我的小技巧

数据清洗完成后,我习惯先做一次「完整性检查」:随机挑10只债券,手动核对行情、评级、久期三个字段。虽然费时间,但能发现很多自动化检查漏掉的问题。

好了,数据获取和清洗就聊到这儿。记住一句话:数据质量决定了策略的上限。花80%的时间在数据上,剩下的20%做策略,这买卖不亏。


蓝海数据掘金营,专注资料整理