文章

QuantLib 金融计算——案例之主成分久期(PCD)

介绍主成分久期的概念,并用中国市场的数据作为案例。

由于版本问题,代码可能与最新版不兼容。

QuantLib 金融计算——案例之主成分久期(PCD)

概述

关键期限上利率的变动通常有较强的相关性,所以,使用 KRD 天然的存在着“共线性”的隐患。

处理共线性的常见手段是主成分分析(PCA),这也就引出了主成分久期(PCD)的概念——债券价格关于主成分的敏感性。

主成分久期

关于主成分久期的高级内容请见《Interest Rate Risk Modeling》阅读笔记——第十章

主成分是原始变量的线性组合,可以证明 PCD 也是 KRD 的线性组合,因此计算 PCD 需要分三步:

  1. 算出关键利率的 KRD;
  2. 对关键利率做 PCA;
  3. 根据 KRD 和 PCA 的载荷矩阵(loading)计算 PCD

计算案例

沿用《案例之 KRD、Fisher-Weil 久期及久期的解释能力》的数据,KRD 的计算略过,直接使用最终结果。

利率变化的主成分分析

需要注意的是,这里不需要对数据做标准化(standardize),否则最终计算 KRD 时要做一次尺度变换。但遵照《Interest Rate Risk Modeling》的做法,最终的主成分因子做了归一化(normalize)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import pandas as pd
import statsmodels.api as sm
import seaborn as sns

ratesChg = pd.read_csv(
    'ratesChg.csv', parse_dates=True, index_col='date')
returns = pd.read_csv(
    'returns.csv', parse_dates=True, index_col='date')

pca = sm.PCA(
    ratesChg,
    standardize=False,
    demean=True,
    normalize=True)

print(pca.loadings)

'''
      comp_00   comp_01   comp_02  ...   comp_11   comp_12   comp_13
1D   0.471356  0.877932 -0.060239  ...  0.015957  0.024632  0.006903
6M   0.047785  0.023852  0.897648  ... -0.001783  0.012518 -0.018913
1Y   0.298379 -0.136045  0.227319  ... -0.132121 -0.011556 -0.044387
2Y   0.310444 -0.149192  0.123286  ...  0.645939 -0.030575  0.140874
3Y   0.342030 -0.157388  0.033696  ... -0.385047 -0.121084  0.023274
4Y   0.342225 -0.187096  0.013315  ...  0.023240  0.393166 -0.008804
5Y   0.303104 -0.174121  0.034038  ...  0.066626 -0.031194 -0.094945
6Y   0.266243 -0.131857 -0.053863  ... -0.499595 -0.316239 -0.183736
7Y   0.224635 -0.120262 -0.054711  ...  0.102571 -0.247733  0.106077
8Y   0.218911 -0.138575 -0.168238  ... -0.150574  0.655571 -0.091769
9Y   0.217848 -0.115892 -0.188426  ...  0.132909 -0.470151  0.134931
10Y  0.136793 -0.117408 -0.208336  ...  0.180370  0.058729  0.088273
15Y  0.135273 -0.106458 -0.087883  ...  0.259897 -0.024268 -0.592554
20Y  0.102095 -0.090580 -0.020611  ... -0.106187  0.108275  0.733208
'''

下面计算一下每个成分因子对回报率的解释能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
R2 = pd.DataFrame(
    data=[sm.OLS(endog=returns, exog=pca.factors.iloc[:, i]).fit().rsquared for i in range(pca._ncomp)],
    index=pca.loadings.columns)

print(R2)

'''
comp_00  0.442932
comp_01  0.098024
comp_02  0.039253
comp_03  0.129587
comp_04  0.035543
comp_05  0.036428
comp_06  0.055197
comp_07  0.000370
comp_08  0.045802
comp_09  0.000013
comp_10  0.004617
comp_11  0.000724
comp_12  0.018448
comp_13  0.000376
'''

上一篇中构造的水平因子的解释能力大约是 50%,而前四个主成分因子的解释能力大约能达到 70%(0.7 = 0.442932 + 0.098024 + 0.039253 + 0.129587)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
pcReg = sm.OLS(
    endog=returns, exog=pca.factors).fit()

# print(pcReg.summary())

idx = [0, 1, 2, 3, 4, 5, 6, 8, 10, 12]

pcReg = sm.OLS(
    endog=returns, exog=pca.factors.iloc[:, idx]).fit()

print(pcReg.summary())

'''
                                 OLS Regression Results                                
=======================================================================================
Dep. Variable:                 return   R-squared (uncentered):                   0.906
Model:                            OLS   Adj. R-squared (uncentered):              0.902
Method:                 Least Squares   F-statistic:                              228.9
Date:                Wed, 18 Nov 2020   Prob (F-statistic):                   4.81e-116
Time:                        15:22:48   Log-Likelihood:                          1424.0
No. Observations:                 248   AIC:                                     -2828.
Df Residuals:                     238   BIC:                                     -2793.
Df Model:                          10                                                  
Covariance Type:            nonrobust                                                  
==============================================================================
                 coef    std err          t      P>|t|      [0.025      0.975]
------------------------------------------------------------------------------
comp_00       -0.0265      0.001    -33.458      0.000      -0.028      -0.025
comp_01        0.0125      0.001     15.740      0.000       0.011       0.014
comp_02        0.0079      0.001      9.960      0.000       0.006       0.009
comp_03       -0.0143      0.001    -18.097      0.000      -0.016      -0.013
comp_04       -0.0075      0.001     -9.478      0.000      -0.009      -0.006
comp_05        0.0076      0.001      9.595      0.000       0.006       0.009
comp_06        0.0094      0.001     11.811      0.000       0.008       0.011
comp_08       -0.0085      0.001    -10.759      0.000      -0.010      -0.007
comp_10       -0.0027      0.001     -3.416      0.001      -0.004      -0.001
comp_12        0.0054      0.001      6.828      0.000       0.004       0.007
==============================================================================
Omnibus:                      130.528   Durbin-Watson:                   1.924
Prob(Omnibus):                  0.000   Jarque-Bera (JB):             7723.672
Skew:                          -1.214   Prob(JB):                         0.00
'''

从所有主成分因子中筛选掉少数不显著的因子,总的解释能力已达到了 90%。

计算 PCD

根据这里的推导,PCD 向量等于 KRD 向量与载荷矩阵的乘积。

计算 PCD 时有一个的问题需要注意,即载荷向量(载荷矩阵的某列)元素的符号。单纯从 PCA 的角度来看,载荷向量元素的符号不构成一个问题,如果 $l$ 是载荷向量,$-l$ 也能充当载荷向量。但是元素的符号为载荷向量赋予经济含义,具体到利率期限结构的分析来说,通常约定

  • 水平因子(h)对应的向量所有元素都必须为正;
  • 斜率因子(s)对应的向量在短期限一端为负,在长期限一端为正;
  • 曲率因子(c)对应的向量在中间期限为正,在两端为负。

因此,计算 PCD 之前需要调整载荷向量的符号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
krd = pd.DataFrame(
    data=[
        -0.00273973, 0.01761345, 0.02607589, 0.06751827, 0.09790298,
        0.12608806, 0.15196510, 0.17540198, 0.19649419, 3.12947679,
        3.35232738, 0.00000000, 0.00000000, 0.00000000],
    index=pca.loadings.index,
    columns=['KRD'])

hsc = pca.loadings[['comp_00','comp_01','comp_03']]
hsc.columns = ['h','s','c']

# print(hsc)

hsc.loc[:,'s'] = -hsc.loc[:,'s']
hsc.loc[:,'c'] = -hsc.loc[:,'c']

print(hsc)

hsc.plot(marker='o')

'''
            h         s         c
1D   0.471356 -0.877932 -0.027072
6M   0.047785 -0.023852 -0.426987
1Y   0.298379  0.136045  0.416754
2Y   0.310444  0.149192  0.261607
3Y   0.342030  0.157388  0.222109
4Y   0.342225  0.187096  0.086126
5Y   0.303104  0.174121  0.047247
6Y   0.266243  0.131857 -0.176602
7Y   0.224635  0.120262 -0.063345
8Y   0.218911  0.138575 -0.288706
9Y   0.217848  0.115892 -0.352268
10Y  0.136793  0.117408 -0.506713
15Y  0.135273  0.106458 -0.108318
20Y  0.102095  0.090580 -0.068624
'''

因子的选择其实见仁见智,综合考虑曲线的形状和解释能力,第四个主成分因子更适合做曲率因子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pcd = (hsc * krd.values).sum()

print(pcd)

pcdNormalize = pcd * np.sqrt(pca.eigenvals[0:3]).values

print(pcdNormalize)

'''
h    1.657201
s    0.950000
c   -2.066973

h    0.026187
s    0.012167
c   -0.013483
'''

pcd 是债券关于主成分因子的敏感性,pcdNormalize 是债券关于归一化后主成分的敏感性(和书中的约定一样),可以看出 pcdNormalize 的值和回归系数大体保持一致(注意到符号的转换)。

第一主成分和上一篇的水平因子经济含义相似,但当前得到的 pcd 的值和上一篇中的 Fisher-Weil 久期相去甚远,这主要是因为主成分因子做了尺度缩放。对于水平因子来说,消除尺度缩放

1
2
pcd['h'] * pca.loadings['comp_00'].sum()
# 5.662857296706285

这样第一主成分就变成了利率变动的加权平均,结果与 Fisher-Weil 久期的值接近,且数量级保持一致。

本文由作者按照 CC BY 4.0 进行授权