4、指数计算实战:用Python从零实现一个债券价格指数的计算(日频、月频)
债券指数,说白了就是一把尺子。用来量一量整个债市涨了还是跌了。很多朋友觉得这东西很神秘,其实不然。我当年刚入行时,也以为指数是交易所直接发出来的,后来自己动手算了一遍,才发现核心逻辑并不复杂。
今天我们就从零开始,手写一个债券价格指数。日频、月频都安排上。你跟着我走一遍,以后面试被问到指数编制,心里就有底了。
4.1 指数计算的核心逻辑
先搞清楚一件事:债券指数和股票指数最大的区别在哪?
股票指数通常只考虑价格变化。债券指数呢,还得考虑利息。因为债券每天都有应计利息在滚动,你今天买和明天买,全价是不一样的。
所以债券价格指数,一般用全价( Dirty Price )来算。公式其实很简单:
指数收益率 = (今日全价总值 / 昨日全价总值) - 1
当日指数 = 上日指数 × (1 + 当日收益率)
嗯,这里要注意:我们算的是加权的。不是简单平均。每只债券的权重,通常用市值来定。市值大的债券,对指数影响就大。
Index_t = Index_{t-1} × Σ( w_i × (P_{i,t} + AI_{i,t}) / (P_{i,t-1} + AI_{i,t-1}) )
其中 w_i 是第 i 只债券的市值权重,P 是净价,AI 是应计利息。
4.2 数据准备:我们需要什么?
动手写代码前,先理清楚输入数据。我个人习惯,先把数据字段列个表,免得写到一半发现缺东西。
| 字段名 | 说明 | 示例 |
|---|---|---|
| bond_id | 债券代码 | 210005.IB |
| trade_date | 交易日 | 2024-01-15 |
| clean_price | 净价 | 101.25 |
| accrued_interest | 应计利息 | 0.87 |
| dirty_price | 全价 = 净价 + 应计利息 | 102.12 |
| market_value | 市值(用于计算权重) | 50亿 |
这里有个坑,我曾经踩过:应计利息的计算方式。不同债券的计息基准不一样,有的按实际天数,有的按30/360。如果你直接从行情接口拿数据,一定要确认清楚对方给的是哪种。
4.3 日频指数计算实战
好,直接上代码。我们先写一个日频版本。逻辑很直白:每天算一次加权平均收益率,然后累乘。
import pandas as pd
import numpy as np
def calculate_daily_index(df, base_date, base_value=100):
"""
计算日频债券价格指数
df: 包含 bond_id, trade_date, dirty_price, market_value 的DataFrame
base_date: 基期日期
base_value: 基期指数值,默认100
"""
# 按日期排序
df = df.sort_values(['trade_date', 'bond_id']).copy()
# 计算每日每只债券的市值权重
df['weight'] = df.groupby('trade_date')['market_value'].transform(
lambda x: x / x.sum()
)
# 计算每日加权全价
df['weighted_price'] = df['weight'] * df['dirty_price']
# 按日期汇总
daily = df.groupby('trade_date').agg(
total_weighted_price=('weighted_price', 'sum')
).reset_index()
# 计算日收益率
daily['return'] = daily['total_weighted_price'].pct_change()
# 计算指数(基期设为100)
daily['index_value'] = base_value
for i in range(1, len(daily)):
daily.loc[daily.index[i], 'index_value'] = (
daily.loc[daily.index[i-1], 'index_value'] *
(1 + daily.loc[daily.index[i], 'return'])
)
return daily[['trade_date', 'index_value', 'return']]
# 使用示例
# result = calculate_daily_index(bond_data, '2024-01-02')
4.4 月频指数:两种思路
月频指数怎么做?有两种常见做法。
方法一:从日频聚合
把每天的指数值拿出来,取月末最后一天的值。简单粗暴,但有个问题:如果月末那天没有交易数据怎么办?
方法二:按月重算
每个月独立计算一次。用月初和月末的全价数据,算一个月的收益率。我个人更推荐这种方法,因为更符合实际投资逻辑。
来看看代码实现:
def calculate_monthly_index(df, base_date, base_value=100):
"""
计算月频债券价格指数(按月重算法)
"""
# 先按日频算
daily_index = calculate_daily_index(df, base_date, base_value)
# 提取每月最后一个交易日
daily_index['year_month'] = daily_index['trade_date'].astype(str).str[:7]
monthly = daily_index.groupby('year_month').last().reset_index()
# 重新计算月收益率
monthly['return'] = monthly['index_value'].pct_change()
# 重新计算指数(避免累积误差)
monthly['monthly_index'] = base_value
for i in range(1, len(monthly)):
monthly.loc[monthly.index[i], 'monthly_index'] = (
monthly.loc[monthly.index[i-1], 'monthly_index'] *
(1 + monthly.loc[monthly.index[i], 'return'])
)
return monthly[['year_month', 'monthly_index', 'return']]
为什么我要重新算一遍指数,而不是直接用日频的月末值?因为日频指数可能有日内波动,月末那天如果市场异常波动,直接取月末值会失真。重新按月算,相当于做了一次平滑。
4.5 避坑指南:我踩过的三个坑
做指数计算,看起来简单,实际坑不少。我把自己踩过的坑列出来,你遇到了直接绕开。
- 坑一:权重漂移。债券会到期、会付息,市值每天都在变。如果你用固定权重,时间一长指数就失真了。我建议每天或每月重新计算权重。
- 坑二:新券纳入与老券剔除。指数样本不是一成不变的。新发的债券要加进来,快到期的要踢出去。这个处理不好,指数会出现跳跃。我曾经因为忘记处理新券纳入,导致指数在某个周一突然跳空1.2%,被领导叫去喝茶...
- 坑三:节假日处理。债券市场节假日不交易,但利息还在计。如果你直接用缺失日期的数据,收益率会算错。我的做法是:用插值法把缺失日期的全价补上,或者干脆跳过非交易日。
4.6 知识体系总览
说了这么多,我们来画个图,把整个指数计算的逻辑串起来。这样你脑子里就有个框架了。
这张图把整个流程串起来了。从输入数据开始,经过全价计算和权重分配,再到日频指数,最后根据需求选择输出日频还是月频。你写代码的时候,就按这个顺序来,不会乱。
4.7 实战小贴士
最后分享几个实战中的小经验:
- 数据质量第一。指数计算最怕数据出错。我每次跑指数前,都会先做一遍数据清洗:检查净价有没有负数、应计利息有没有缺失、市值有没有异常值。
- 回测验证。写好的指数,一定要拿历史数据回测。比如拿2023年的数据算一遍,然后和市场上已有的指数(如中债指数)对比。偏差在0.1%以内算正常,超过0.5%就要查原因了。
- 性能优化。如果你要算几千只债券的日频指数,用纯Python循环会非常慢。我建议用向量化操作,或者用numpy的矩阵运算。上面给的代码已经用了向量化,但如果你债券数量超过5000只,可以考虑用Cython加速。