天天看點

QuantLib 金融計算——收益率曲線之建構曲線(5)

目錄

  • QuantLib 金融計算——收益率曲線之建構曲線(5)
    • 概述
    • Nelson-Siegel 模型家族的成員
      • Nelson-Siegel 模型
      • Svensson 模型
      • 修正 Svensson 模型
      • Bjork-Christensen 模型
      • Bliss 模型
      • Diebold Li 模型
    • 實作
    • 測試
      • 測試資料
      • 正則化條件
      • 代碼
      • 拟合結果
    • 後續話題
    • 參考文獻
    • 擴充閱讀
如果未做特别說明,文中的程式都是 C++11 代碼。

本文代碼對應的 QuantLib 版本是 1.15。相關源代碼可以在 QuantLibEx 找到。

Nelson-Siegel 模型家族的成員一方面可以很好地拟合現實世界中的期限結構,另一方面能夠用相當少的參數描述期限結構的複雜形态,并具有明确的經濟學含義。目前,QuantLib 隻實作了 Nelson-Siegel 模型家族中的兩個成員,即 Nelson-Siegel 模型和 Svensson 模型,本文在 QuantLib 的架構内實作 Nelson-Siegel 模型家族中的其他常見成員,并展示如何在拟合過程中添加正則化條件以得到合理的結果。

Nelson-Siegel 模型家族的主要成員之間大同小異,均可以認為是對經典 Nelson-Siegel 模型的小修小改,共同的目标是将即期期限結構表達為幾種特定形态期限結構的線性組合(水準、斜率和曲率三種因子)。

經典 Nelson-Siegel 模型以遠期利率曲線 \(f(t)\) 為出發點,在連續複利的情境下,認為 \(f(t)\) 可以表示為拉蓋爾函數(Laguerre function,\(1,e^{-x},xe^{-x},x^2e^{-x}, \dots\))的線性組合,再根據即期與遠期之間的關系:

\[s(t) = \frac{1}{t} \int_0^t f(s)ds

\]

就可以得到即期期限結構 \(s(t)\) 的表達式,也就是幾個函數的線性組合,而這幾個函數對應着不同形态的期限結構。

最終對期限結構的拟合就順理成章地轉化為了求解幾個待定參數——一個數值優化問題。

Nelson-Siegel 模型下遠期期限結構表示為:

\[f(t) = c_0 + c_1 e^{-\kappa t} + c_2 \kappa t e^{-\kappa t}

對應的即期期限結構表示為:

\[s(t) = c_0 + c_1 \frac{1 - e^{-\kappa t}}{\kappa t} + c_2 \left(\frac{1 - e^{-\kappa t}}{\kappa t} -e^{-\kappa t} \right)

其中的三個函數分别對應三個形态的期限結構:

  • \(h(t) = 1\):對應水準的期限結構,即水準因子
  • \(h(t) = \frac{1 - e^{-\kappa t}}{\kappa t}\):對應單調傾斜的期限結構,即斜率因子
  • \(h(t) = \frac{1 - e^{-\kappa t}}{\kappa t} -e^{-\kappa t}\):對應中部隆起的期限結構,即曲率因子

參數 \(\kappa\) 則用來控制期限結構的形态。

Svensson 模型在 Nelson-Siegel 模型的基礎上增加了一項,試圖改善對期限結構中段的拟合效果,其遠期期限結構表示為:

\[f(t) = c_0 + c_1 e^{-\kappa t} + c_2 \kappa t e^{-\kappa t} + c_3 \kappa_1 t e^{-\kappa_1 t}

\[s(t) = c_0 + c_1 \frac{1 - e^{-\kappa t}}{\kappa t} + c_2 \left(\frac{1 - e^{-\kappa t}}{\kappa t} -e^{-\kappa t} \right) + c_3 \left(\frac{1 - e^{-\kappa_1 t}}{\kappa_1 t} -e^{-\kappa_1 t} \right)

其中的四個函數分别對應三個形态的期限結構:

  • \(h(t) = 1\):對應水準的期限結構
  • \(h(t) = \frac{1 - e^{-\kappa t}}{\kappa t}\):對應單調傾斜的期限結構
  • \(h(t) = \frac{1 - e^{-\kappa t}}{\kappa t} -e^{-\kappa t}\):對應中部隆起的期限結構
  • \(h(t) = \frac{1 - e^{-\kappa_1 t}}{\kappa_1 t} -e^{-\kappa_1 t}\):對應另一個中部隆起的期限結構

參數 \(\kappa\) 和 \(\kappa_1\) 則用來控制期限結構的形态。

由于 Svensson 模型存在兩個中部隆起的期限結構,形态很相似,緻使拟合計算的過程中會受到“共線性”的不良影響。為了克服共線性,有學者直接對即期期限結構動手,修改了第二個中部隆起的期限結構:

\[s(t) = c_0 + c_1 \frac{1 - e^{-\kappa t}}{\kappa t} + c_2 \left(\frac{1 - e^{-\kappa t}}{\kappa t} -e^{-\kappa t} \right) + c_3 \left(\frac{1 - e^{-\kappa_1 t}}{\kappa_1 t} -e^{-2\kappa_1 t} \right)

Bjork-Christensen 模型在 Nelson-Siegel 模型的基礎上增加了一項,試圖改善對期限結構前段的拟合效果,其遠期期限結構表示為:

\[f(t) = c_0 + c_1 e^{-\kappa t} + c_2 \kappa t e^{-\kappa t} + c_3 e^{-2\kappa t}

\[s(t) = c_0 + c_1 \frac{1 - e^{-\kappa t}}{\kappa t} + c_2 \left(\frac{1 - e^{-\kappa t}}{\kappa t} -e^{-\kappa t} \right) + c_3 \frac{1 - e^{-2\kappa t}}{2\kappa t}

Bliss 模型在 Nelson-Siegel 模型上增加了有限的自由度,試圖在 Svensson 模型和 Nelson-Siegel 模型之間取得一種折衷,其遠期期限結構表示為:

\[f(t) = c_0 + c_1 e^{-\kappa t} + c_2 \kappa_1 t e^{-\kappa_1 t}

\[s(t) = c_0 + c_1 \frac{1 - e^{-\kappa t}}{\kappa t} + c_2 \left(\frac{1 - e^{-\kappa_1 t}}{\kappa_1 t} -e^{-\kappa_1 t} \right)

當 \(\kappa = \kappa_1\) 時 Bliss 模型就是 Nelson-Siegel 模型。

Diebold Li 模型是一個特殊版本的 Nelson-Siegel 模型,用來控制期限結構形态的 \(\kappa\) 需要事先指定,無需估計,遠期期限結構表示為:

這幾個 Nelson-Siegel 模型家族成員的實作均參照現有的

NelsonSiegelFitting

類,讀者可以在 QuantLibEx 找到相關代碼。

示例所用的樣本券交易資料來自專門進行期限結構分析的 R 包——termstrc。具體來說是資料集

govbonds

中的

AUSTRIA

部分,包含 2008-01-30 這一天澳洲市場上 16 隻固息債的成交資料。讀者可以在這裡找到對應資料檔案。

在拟合期限結構時,QuantLib 支援使用者添加 \(L^2\) 正則化條件,為此使用者需要提供兩個數組,分别是

  • 猜想的參數

    guess

    :\(\beta^{guess}\)
  • 正則化權重

    l2

    :\(\lambda(>0)\)

加入正則化條件之後,期限結構的拟合可以表述為如下數值優化問題:

\[\mathop{\arg\min}_{\beta} F(\beta) := \sum_j \left(ModelPrice_j(s(t;\beta)) - MarketPrice_{j} \right)^2 + \sum_i\lambda_i(\beta_i - \beta^{guess}_i)^2

通常相鄰兩個時間點上期限結構的參數不會發生劇烈變化,是以猜想的參數 \(\beta^{guess}\) 可以設定為上一期的結果,正則化權重 \(\lambda\) 的設定就見仁見智了。

void TestNelsonSiegelClassTermStructure() {

    using namespace std;
    using namespace QuantLib;

    /*
    基礎交易資料
    */

    Size bondNum = 16;

    vector<Real> cleanPrice = {
        100.4941, 103.5572, 104.4135, 105.0056, 99.8335, 101.25, 102.3832, 97.0053,
        99.5164, 101.2435, 104.0539, 101.15, 96.1395, 91.1123, 122.0027, 92.4369};

    vector<Handle<Quote>> priceHandle(bondNum);

    for (Size i = 0; i < bondNum; ++i) {
        ext::shared_ptr<Quote> q(
            new SimpleQuote(cleanPrice[i]));
        Handle<Quote> hq(q);
        priceHandle[i] = hq;
    }

    vector<Year> issueYear = {
        1999, 1999, 2001, 2002, 2003, 1999, 2004, 2005,
        2006, 2007, 2003, 2008, 2005, 2006, 1997, 2007};

    vector<Month> issueMonth = {
        Feb, Oct, Jan, Jan, May, Jan, Jan, Apr,
        Apr, Sep, Jan, Jan, Jan, Jan, Jul, Jan};

    vector<Day> issueDay = {
        22, 22, 4, 9, 20, 15, 15, 26, 21, 17, 15, 8, 14, 11, 10, 12};

    vector<Year> maturityYear = {
        2009, 2010, 2011, 2012, 2013, 2014, 2014, 2015,
        2016, 2017, 2018, 2019, 2020, 2021, 2027, 2037};

    vector<Month> maturityMonth = {
        Jul, Jan, Jan, Jul, Oct, Jan, Jul, Jul,
        Sep, Sep, Jan, Mar, Jul, Sep, Jul, Mar};

    vector<Day> maturityDay = {
        15, 15, 4, 15, 20, 15, 15, 15,
        15, 15, 15, 15, 15, 15, 15, 15};

    vector<Date> issueDate(bondNum), maturityDate(bondNum);

    for (Size i = 0; i < bondNum; ++i) {
        Date idate(issueDay[i], issueMonth[i], issueYear[i]);
        Date mdate(maturityDay[i], maturityMonth[i], maturityYear[i]);
        issueDate[i] = idate;
        maturityDate[i] = mdate;
    }

    vector<Real> couponRate = {
        0.04, 0.055, 0.0525, 0.05, 0.038, 0.04125, 0.043, 0.035,
        0.04, 0.043, 0.0465, 0.0435, 0.039, 0.035, 0.0625, 0.0415};

    /*
    配置 helper
    */

    Frequency frequency = Annual;
    Actual365Fixed dayCounter(Actual365Fixed::Standard);
    BusinessDayConvention paymentConv = Unadjusted;
    BusinessDayConvention terminationDateConv = Unadjusted;
    BusinessDayConvention convention = Unadjusted;
    Real redemption = 100.0;
    Real faceAmount = 100.0;
    Australia calendar;

    Date today = calendar.adjust(Date(30, Jan, 2008));
    Settings::instance().evaluationDate() = today;

    Natural bondSettlementDays = 0;
    Date bondSettlementDate = calendar.advance(
        today,
        Period(bondSettlementDays, Days));

    vector<ext::shared_ptr<BondHelper>> instruments(bondNum);
    vector<Time> maturity(bondNum);

    for (Size i = 0; i < bondNum; ++i) {

        vector<Real> bondCoupon = {couponRate[i]};

        Schedule schedule(
            issueDate[i],
            maturityDate[i],
            Period(frequency),
            calendar,
            convention,
            terminationDateConv,
            DateGeneration::Backward,
            false);

        ext::shared_ptr<FixedRateBondHelper> helper(
            new FixedRateBondHelper(
                priceHandle[i],
                bondSettlementDays,
                faceAmount,
                schedule,
                bondCoupon,
                dayCounter,
                paymentConv,
                redemption));

        maturity[i] = dayCounter.yearFraction(
            bondSettlementDate, helper->maturityDate());

        instruments[i] = helper;
    }

    Real accuracy = 1.0e-6;
    Natural maxEvaluations = 5000;
    Array weights;

    /*
    正則化條件
    */

    Array l2Ns(4, 0.5);
    Array guessNs(4);
    guessNs[0] = 4 / 100.0, guessNs[1] = 0.0, guessNs[2] = 0.0, guessNs[3] = 0.5;

    Array l2Sv(6, 0.5);
    Array guessSv(6);
    guessSv[0] = 4 / 100.0, guessSv[1] = 0.0, guessSv[2] = 0.0, guessSv[3] = 0.0, guessSv[4] = 0.2, guessSv[5] = 0.15;

    Array l2Asv(6, 0.5);
    Array guessAsv(6);
    guessAsv[0] = 4 / 100.0, guessAsv[1] = 0.0, guessAsv[2] = 0.0, guessAsv[3] = 0.0, guessAsv[4] = 0.2, guessAsv[5] = 0.3;

    Array l2Bc(5, 0.5);
    Array guessBc(5);
    guessBc[0] = 4 / 100.0, guessBc[1] = 0.0, guessBc[2] = 0.0, guessBc[3] = 0.0, guessBc[4] = 0.2;

    Array l2Bl(5, 0.5);
    Array guessBl(5);
    guessBl[0] = 4 / 100.0, guessBl[1] = 0.0, guessBl[2] = 0.0, guessBl[3] = 0.5, guessBl[4] = 0.5;

    ext::shared_ptr<OptimizationMethod> optMethod(
        new LevenbergMarquardt());

    /*
    拟合方法
    */

    NelsonSiegelFitting nsf(
        weights, optMethod, l2Ns);

    SvenssonFitting svf(
        weights, optMethod, l2Sv);

    AdjustedSvenssonFitting asvf(
        weights, optMethod, l2Asv);

    DieboldLiFitting dlf(
        0.5, weights, optMethod);

    BjorkChristensenFitting bcf(
        weights, optMethod, l2Bc);

    BlissFitting blf(
        weights, optMethod, l2Bl);

    FittedBondDiscountCurve tsNelsonSiegel(
        bondSettlementDate,
        instruments,
        dayCounter,
        nsf,
        accuracy,
        maxEvaluations,
        guessNs);

    FittedBondDiscountCurve tsSvensson(
        bondSettlementDate,
        instruments,
        dayCounter,
        svf,
        accuracy,
        maxEvaluations,
        guessSv);

    FittedBondDiscountCurve tsAdjustedSvensson(
        bondSettlementDate,
        instruments,
        dayCounter,
        asvf,
        accuracy,
        maxEvaluations,
        guessAsv);

    FittedBondDiscountCurve tsDieboldLi(
        bondSettlementDate,
        instruments,
        dayCounter,
        dlf,
        accuracy,
        maxEvaluations);

    FittedBondDiscountCurve tsBjorkChristensen(
        bondSettlementDate,
        instruments,
        dayCounter,
        bcf,
        accuracy,
        maxEvaluations,
        guessBc);

    FittedBondDiscountCurve tsBliss(
        bondSettlementDate,
        instruments,
        dayCounter,
        blf,
        accuracy,
        maxEvaluations,
        guessBl);

    /*
    結果
    */

    cout << "NelsonSiegel Results: \t\t" << tsNelsonSiegel.fitResults().solution() << endl;
    cout << "Svensson Results: \t\t" << tsSvensson.fitResults().solution() << endl;
    cout << "AdjustedSvensson Results: \t" << tsAdjustedSvensson.fitResults().solution() << endl;
    cout << "DieboldLi Results: \t\t" << tsDieboldLi.fitResults().solution() << endl;
    cout << "BjorkChristensen Results: \t" << tsBjorkChristensen.fitResults().solution() << endl;
    cout << "Bliss Results: \t\t\t" << tsBliss.fitResults().solution() << endl;

    cout << endl;

    Real NSrate, SVrate, ASVrate, DLrate, BCrate, Brate;

    for (Size i = 0; i < bondNum; ++i) {

        Time t = dayCounter.yearFraction(
            bondSettlementDate, maturityDate[i]);

        NSrate = tsNelsonSiegel.zeroRate(t, Compounding::Continuous, frequency).rate() * 100.0;
        SVrate = tsSvensson.zeroRate(t, Compounding::Continuous, frequency).rate() * 100.0;
        ASVrate = tsAdjustedSvensson.zeroRate(t, Compounding::Continuous, frequency).rate() * 100.0;
        DLrate = tsDieboldLi.zeroRate(t, Compounding::Continuous, frequency).rate() * 100.0;
        BCrate = tsBjorkChristensen.zeroRate(t, Compounding::Continuous, frequency).rate() * 100.0;
        Brate = tsBliss.zeroRate(t, Compounding::Continuous, frequency).rate() * 100.0;

        cout << setprecision(3) << fixed
             << t << ",\t"
             << NSrate << ",\t"
             << SVrate << ",\t"
             << ASVrate << ",\t"
             << DLrate << ",\t"
             << BCrate << ",\t"
             << Brate << endl;
    }
}
           

NelsonSiegel Results: 		[ 0.0500803; -0.0105414; -0.0303842; 0.456529 ]
Svensson Results: 		[ 0.0431095; -0.00716036; -0.0340932; 0.0391339; 0.228995; 0.117208 ]
AdjustedSvensson Results: 	[ 0.0506269; -0.0116339; 0.0029305; -0.0135686; 0.179066; 0.267767 ]
DieboldLi Results: 		[ 0.0496643; -0.00879931; -0.0329267 ]
BjorkChristensen Results: 	[ 0.0508039; -0.0555185; 0.0115282; 0.0415581; 0.227838 ]
Bliss Results: 			[ 0.0500892; -0.0106013; -0.0315605; 0.513831; 0.456329 ]

1.458,	3.581,	3.544,	3.569,	3.592,	3.547,	3.581
1.962,	3.545,	3.550,	3.542,	3.542,	3.543,	3.545
2.932,	3.549,	3.585,	3.554,	3.537,	3.573,	3.549
4.460,	3.657,	3.682,	3.664,	3.651,	3.678,	3.657
5.726,	3.780,	3.782,	3.782,	3.780,	3.786,	3.780
5.964,	3.803,	3.802,	3.805,	3.805,	3.807,	3.803
6.460,	3.852,	3.843,	3.852,	3.856,	3.850,	3.852
7.460,	3.947,	3.928,	3.943,	3.954,	3.936,	3.947
8.633,	4.049,	4.024,	4.042,	4.057,	4.031,	4.049
9.633,	4.126,	4.102,	4.117,	4.134,	4.106,	4.126
9.967,	4.150,	4.127,	4.141,	4.157,	4.129,	4.150
11.129,	4.226,	4.210,	4.217,	4.232,	4.206,	4.226
12.466,	4.302,	4.295,	4.293,	4.305,	4.284,	4.301
13.636,	4.358,	4.360,	4.351,	4.359,	4.344,	4.358
19.468,	4.548,	4.576,	4.553,	4.538,	4.555,	4.548
29.142,	4.700,	4.677,	4.721,	4.680,	4.730,	4.701
           
QuantLib 金融計算——收益率曲線之建構曲線(5)

拟合結果大緻分為兩組,

  • 第一組,Svensson 模型和 Bjork-Christensen 模型在前段的形态互相比較接近,
  • 第二組,其他幾個模型在前段的形态互相比較接近

如果沒有正則化限制,當控制曲線形狀的參數已知時,Nelson-Siegel 模型和 Svensson 模型的拟合問題是凸的(Ferstl & Hayden, 2010),比較容易解決。但參數未知時,問題變成了非凸的。

在沒有正則化限制的條件下如何找到合理的模型參數将成為後續的話題。

  1. Ferstl.R, Hayden.J (2010). "Zero-Coupon Yield Curve Estimation with the Package

    termstrc

    ." Journal of Statistical Software, Volume 36, Issue 1.
  2. De Pooter M (2007). "Examining the Nelson-Siegel Class of Term Structure Models: In-Sample Fit versus Out-of-Sample Forecasting Performance." SSRN eLibrary. http://ssrn.com/paper=992748.

《QuantLib 金融計算》系列合集

★ 持續學習 ★ 堅持創作 ★

繼續閱讀