第二章:金融数据准备——获取国债收益率曲线数据
做量化分析的人都知道一句话:垃圾进,垃圾出。收益率曲线的主成分分析也不例外。数据源选错了、清洗不到位,后面跑出来的结果就是一堆数字垃圾。
我个人习惯把数据准备分成三步:获取 → 清洗 → 对齐。每一步都有坑,我踩过不少,今天一并说清楚。
2.1 数据源的选择:中债登 vs 彭博
国内做国债收益率分析,主流数据源就两个:中债登和彭博。我两个都用过,说说区别。
| 对比维度 | 中债登 | 彭博 |
|---|---|---|
| 数据覆盖 | 全市场国债,含银行间 | 全球国债,含中国 |
| 更新频率 | 每日17:00左右 | 实时/每日 |
| 历史长度 | 2002年至今 | 1990年代至今 |
| 数据精度 | 中债估值,权威性高 | BVAL估值,国际通用 |
| 获取成本 | 免费/低 | 高(需终端许可) |
如果你做的是纯国内市场的策略,中债登足够了。我早期做跨境套利策略时,必须用彭博,因为需要同时拉中美两边的曲线。嗯,这里要注意:两个数据源的曲线定义可能不同——中债登用的是中债到期收益率曲线,彭博用的是YCGT0023 Index(中国国债即期收益率)。
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]
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)
2.4 数据对齐:让不同期限的数据「说同一种语言」
数据清洗完了,下一步是对齐。对齐的核心问题是:不同期限的收益率数据,是否在同一时间点上可比?
举个例子:1年期国债收益率每天都有报价,但30年期国债可能一周才交易几次。如果你直接用原始数据,30年期那列会有大量缺失值。
对齐策略我推荐两种:
- 日历对齐:只保留所有期限都有数据的日期。简单粗暴,但会丢失信息。
- 插值对齐:对缺失的期限点进行插值,保证每个日期都有完整曲线。
我个人更倾向第二种。原因很简单——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]} 行')
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,我会回去检查那个期限的原始数据。大概率是数据源的问题。
2.6 本章知识体系
下面这张图总结了本章的核心逻辑:
数据准备好了,下一步就是真正的PCA分析。不过在那之前,我建议你把今天讲的内容在本地跑一遍。亲手拉一次数据、做一次清洗,比看十遍文章都管用。