第二章:金融数据准备——获取国债收益率曲线数据

做量化分析的人都知道一句话:垃圾进,垃圾出。收益率曲线的主成分分析也不例外。数据源选错了、清洗不到位,后面跑出来的结果就是一堆数字垃圾。

我个人习惯把数据准备分成三步:获取 → 清洗 → 对齐。每一步都有坑,我踩过不少,今天一并说清楚。

2.1 数据源的选择:中债登 vs 彭博

国内做国债收益率分析,主流数据源就两个:中债登和彭博。我两个都用过,说说区别。

对比维度 中债登 彭博
数据覆盖 全市场国债,含银行间 全球国债,含中国
更新频率 每日17:00左右 实时/每日
历史长度 2002年至今 1990年代至今
数据精度 中债估值,权威性高 BVAL估值,国际通用
获取成本 免费/低 高(需终端许可)

如果你做的是纯国内市场的策略,中债登足够了。我早期做跨境套利策略时,必须用彭博,因为需要同时拉中美两边的曲线。嗯,这里要注意:两个数据源的曲线定义可能不同——中债登用的是中债到期收益率曲线,彭博用的是YCGT0023 Index(中国国债即期收益率)。

⚠️ 我曾经踩过的坑: 中债登的曲线是「中债估值」,彭博的曲线是「BVAL估值」。两者在短端(1年以内)差异不大,但在长端(10年以上)有时能差5-10个基点。做PCA时,这种系统性偏差会直接影响主成分的载荷向量。

2.2 数据获取实战:Python代码示例

我一般用Python写数据获取脚本。中债登的数据可以通过其官网下载,或者用Wind/Python接口。彭博的话,用blpapi包。

下面是一个从彭博拉取中国国债收益率曲线的示例:

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

# 连接彭博终端
session = blpapi.Session()
session.start()

# 定义要获取的期限点
tenors = ['1Y', '2Y', '3Y', '5Y', '7Y', '10Y', '15Y', '20Y', '30Y']
tickers = [f'YCGT0023 Index' for _ in tenors]  # 中国国债即期收益率

# 构建请求
ref_data_service = session.getService('//blp/refdata')
request = ref_data_service.createRequest('HistoricalDataRequest')

# 设置时间范围
end_date = datetime.now()
start_date = end_date - timedelta(days=365*5)  # 拉5年数据

request.getElement('securities').appendValue('YCGT0023 Index')
request.getElement('fields').appendValue('PX_LAST')
request.set('startDate', start_date.strftime('%Y%m%d'))
request.set('endDate', end_date.strftime('%Y%m%d'))

# 发送请求并解析
session.sendRequest(request)
data = []
while True:
    event = session.nextEvent()
    if event.eventType() == blpapi.Event.RESPONSE:
        for msg in event:
            # 解析数据...
            pass
        break

# 整理成DataFrame
df = pd.DataFrame(data, columns=['date'] + tenors)
df['date'] = pd.to_datetime(df['date'])
df.set_index('date', inplace=True)

如果你没有彭博终端,用中债登的数据也完全够用。中债登官网提供CSV下载,或者用Wind的w.wsd函数:

from WindPy import w
w.start()

# 获取中债国债到期收益率曲线
codes = ['CGB1Y.WI', 'CGB2Y.WI', 'CGB3Y.WI', 'CGB5Y.WI', 
         'CGB7Y.WI', 'CGB10Y.WI', 'CGB15Y.WI', 'CGB20Y.WI', 'CGB30Y.WI']

data = w.wsd(codes, "close", "2019-01-01", "2024-01-01", 
             "returnType=1")  # returnType=1 表示收益率

df = pd.DataFrame(data.Data, index=codes, columns=data.Times).T
df.columns = [c.replace('.WI', '') for c in codes]
💡 我的个人经验: 拉数据时,尽量多拉几个期限点。比如1年、2年、3年、5年、7年、10年、15年、20年、30年。PCA需要足够多的变量(期限点)才能提取出有意义的因子。少于5个期限点,PCA的结果基本没法用。

2.3 数据清洗:那些年我遇到的脏数据

数据拉下来,别急着跑PCA。先看看数据长什么样。我遇到过的问题包括:

  • 缺失值:某些日期没有数据(节假日、数据源故障)
  • 异常值:收益率突然跳变几百个基点(数据录入错误)
  • 重复值:同一日期出现多条记录
  • 非交易日:中债登和彭博对节假日的处理不同

清洗流程我一般这样写:

def clean_yield_curve(df):
    """
    清洗收益率曲线数据
    """
    # 1. 去重:按日期去重,保留最后一条
    df = df[~df.index.duplicated(keep='last')]
    
    # 2. 排序:按日期升序
    df.sort_index(inplace=True)
    
    # 3. 处理缺失值:用前向填充 + 线性插值
    df = df.ffill().interpolate(method='linear', limit=5)
    
    # 4. 检测异常值:用3倍标准差法
    for col in df.columns:
        mean = df[col].mean()
        std = df[col].std()
        outliers = (df[col] < mean - 3*std) | (df[col] > mean + 3*std)
        if outliers.any():
            print(f'发现异常值: {col}, 日期: {df.index[outliers].tolist()}')
            # 用前后均值替换
            df.loc[outliers, col] = np.nan
    
    # 再次插值
    df = df.interpolate(method='linear')
    
    return df

# 应用清洗
df_clean = clean_yield_curve(df)
⚠️ 我曾经踩过的坑: 有一次我直接用前向填充处理所有缺失值,结果发现2015年8月11日(汇改那天)的数据被填充成了前一天的数值。那天收益率曲线发生了结构性变化,填充等于抹掉了关键信息。后来我改成:缺失超过3天的,用插值;缺失超过5天的,直接标记为NaN,后续分析时剔除该日期

2.4 数据对齐:让不同期限的数据「说同一种语言」

数据清洗完了,下一步是对齐。对齐的核心问题是:不同期限的收益率数据,是否在同一时间点上可比?

举个例子:1年期国债收益率每天都有报价,但30年期国债可能一周才交易几次。如果你直接用原始数据,30年期那列会有大量缺失值。

对齐策略我推荐两种:

  1. 日历对齐:只保留所有期限都有数据的日期。简单粗暴,但会丢失信息。
  2. 插值对齐:对缺失的期限点进行插值,保证每个日期都有完整曲线。

我个人更倾向第二种。原因很简单——PCA需要完整的协方差矩阵。你想想看,如果某天30年期数据缺失,那天的观测值就得扔掉。扔多了,样本量就不够了。

插值对齐的代码:

def align_curves(df, method='linear'):
    """
    对齐不同期限的收益率数据
    method: 'linear' 线性插值, 'cubic' 三次样条
    """
    # 确保索引是日期类型
    df.index = pd.to_datetime(df.index)
    
    # 生成完整的日期序列(交易日历)
    all_dates = pd.date_range(start=df.index.min(), 
                              end=df.index.max(), 
                              freq='B')  # 仅交易日
    
    # 重索引到完整日期序列
    df_aligned = df.reindex(all_dates)
    
    # 插值
    if method == 'linear':
        df_aligned = df_aligned.interpolate(method='linear')
    elif method == 'cubic':
        df_aligned = df_aligned.interpolate(method='cubic')
    
    # 去掉首尾的NaN
    df_aligned = df_aligned.dropna(how='all')
    
    return df_aligned

# 对齐后的数据
df_aligned = align_curves(df_clean, method='linear')
print(f'对齐前: {df.shape[0]} 行, 对齐后: {df_aligned.shape[0]} 行')
🔑 核心要点: 数据对齐的质量,直接决定了PCA结果的可靠性。我见过有人直接用原始数据跑PCA,结果第一主成分的方差解释率只有60%,因为缺失值导致协方差矩阵估计有偏。对齐之后,方差解释率提升到了85%以上。

2.5 数据质量检查:跑PCA前的最后一道防线

数据准备好了,别急着跑PCA。先做几个快速检查:

  • 检查收益率范围:中国国债收益率一般在1%-5%之间,超出这个范围要警惕
  • 检查期限结构:正常情况下,长期收益率高于短期(正向斜率)。如果出现倒挂,要确认是否合理
  • 检查相关性矩阵:不同期限的收益率应该高度相关(相关系数>0.8)。如果某期限与其他期限相关性很低,可能是数据问题

我一般会画一个相关性热力图,一眼就能看出问题:

import seaborn as sns
import matplotlib.pyplot as plt

# 计算相关性矩阵
corr = df_aligned.corr()

# 画热力图
plt.figure(figsize=(10, 8))
sns.heatmap(corr, annot=True, fmt='.2f', cmap='coolwarm', 
            xticklabels=corr.columns, yticklabels=corr.columns)
plt.title('国债收益率期限相关性矩阵')
plt.show()

如果看到某个期限与其他期限的相关系数低于0.7,我会回去检查那个期限的原始数据。大概率是数据源的问题。

💡 我的个人经验: 数据准备阶段花的时间,通常占整个项目的60%以上。别嫌麻烦。我见过太多人花2小时写PCA代码,却只花10分钟准备数据。结果跑出来的结果根本没法解释,还得回头重做。

2.6 本章知识体系

下面这张图总结了本章的核心逻辑:

数据准备流程 数据获取 中债登 / 彭博 数据清洗 去重 / 插值 / 异常检测 数据对齐 日历/插值对齐 多期限点:1Y~30Y 至少5个期限点 清洗策略 前向填充 + 线性插值 质量检查 相关性矩阵 / 范围检查 输出:完整、对齐的收益率曲线数据 用于主成分分析

数据准备好了,下一步就是真正的PCA分析。不过在那之前,我建议你把今天讲的内容在本地跑一遍。亲手拉一次数据、做一次清洗,比看十遍文章都管用。

专注资料整理