【QuantLib-Python】固定利付商品の評価と検証

# 0. はじめに

## 0-1. QuantLib-Python
QuantLib-PythonとはQuantLibというC++で書かれた「金融商品の時価評価ライブラリ」をPythonから呼び出すためのラッパーである．QuantLibではスワップやオプションといった金融商品の時価を計算することができ，それを手軽にPythonから呼び出せるQuantLib-Python（以下これを単にQuantLibと呼ぶ）は大変便利なものである．詳細は[公式HP](https://www.quantlib.org/)へ．

## 0-2. この記事を読めば

- わかる
    - デュレーション評価（QuantLib）
        - 日本国債（半年単利）
        - 定期預金（半年複利）
    - 評価の数値検証
- わからない
    - 時価やデュレーションの概念
    - QauntLibの基本的な使い方

## 0-3. 誰のための記事か

マーケット部門の従事者で1年目の方を読者として想定している．キャッシュ・フロー，割引率やデュレーションなどの概念，日本国債，定期預金の商品性については既知とする．

# 0-4. 計算環境

- Windows 11 (24H2)
- Python 3.12
- pandas 2.2.2
- pyxirr 0.10.6
- QuantLib 1.34

# 1. 日本国債と定期預金の共通点

日本国債と定期預金の共通点は固定金利の債務・債権契約である点だ．銀行が日本国債を資産として，定期預金を負債として持つことを考えると銀行は債権者であり債務者でもあるといえる．

:::note 
銀行は債権者かつ債務者であるため，特有のバランスシート管理（Asset Liability Management; ALM）が必要となる．
:::

ALMについては今後の記事で触れていきたい．

||債権者|債務者|
|---|:---:|:---:|
|日本国債|**銀行**|政府|
|定期預金|預金者|**銀行**|

ただし日本国債と定期預金では利息計算において違いがある．前者は**半年単利**で半年ごとに利払があるのに対して，後者は一般的な邦銀において**半年複利**となっておりさらに利払が終了日のみ行われる．細かなところで商品性が異なる2つについて個別に見ていく．

:::note warn
日本国債と定期預金は細かなところで商品性が異なる．
:::

# 2. 日本国債の評価と検証
## 2-1. 商品性

具体的には以下のような日本国債を考える．

||日本国債|
|:---:|---:|
|年限|5|
|額面|100|
|利息|0.5%|
|利息計算|半年単利|
|日数計算|Act/365|
|開始日|2025/1/15|
|終了日|2030/1/15|

ここでは半年ごとに利払が行われるため次の図のような受け渡しとなっている．

```mermaid
sequenceDiagram
    actor d as 債務者（政府）
    actor c as 債権者（銀行）
    c ->> d: 元本 (期初)
    loop 毎半期
        d ->> c: 利息
    end
    d ->> c: 元本 (期末)
```

## 2-2. ソースコード

QuantLibを使った評価をExcelで検証していくため各種モジュールをインポート，スケジュールを定義する．

```python
import QuantLib as ql
from pyxirr import xirr

import pandas as pd

eval_date = ql.Date(15, 1, 2025)
ql.Settings.instance().evaluationDate = eval_date

tenor = ql.Period(ql.Semiannual)  # 利払間隔
calendar = ql.Japan()             # 休祝日カレンダー
convention = ql.ModifiedFollowing # 営業日調整
day_count = ql.Actual365Fixed()   # 日数計算
rule = ql.DateGeneration.Backward # 日付生成のルール
end_of_month = False              # 月末日ロール

effective_date = eval_date
termination_date = eval_date + ql.Period(5, ql.Years)

schedule = ql.Schedule(effective_date, termination_date,
                      tenor, calendar,
                      convention, convention,
                      rule, end_of_month)
display(
    pd.DataFrame([_ for _ in schedule], columns=['date'])
    )
```

日本の休祝日カレンダーに則っていることが確認できる．

||date|
|---|---:|
|0|January 15th, 2025|
|1|July 15th, 2025|
|2|January 15th, 2026|
|3|July 15th, 2026|
|4|January 15th, 2027|
|5|July 15th, 2027|
|6|January 17th, 2028|
|7|July 18th, 2028|
|8|January 15th, 2029|
|9|July 17th, 2029|
|10|January 15th, 2030|



QuantLibで決済日に注意して日本国債を定義する．
```python
nominal = 100.
jgb_coupon = .5    # %単位
settle_days_tn = 1 # 国債の決済ラグ (Tomorrow Next) T+1

jgb = ql.FixedRateBond(settle_days_tn,
                       nominal, schedule,
                       [jgb_coupon/100],
                       day_count
                       )

# JGBの決済日はJanuary 16th, 2025
print(f"JGBの決済日は{jgb.settlementDate()}")
```

フラット・イールドで割引率を計算し，キャッシュ・フローと合わせて確認する．

```python
settle_days_on = 0

# 割引金利を連続複利で計算
curve = ql.FlatForward(settle_days_on,
                       calendar,
                       ql.QuoteHandle(ql.SimpleQuote(.5/100)),
                       day_count
                       )

# 視覚化
pd.options.display.precision = 8
display(
    pd.DataFrame([(cf.date(), cf.amount(), curve.discount(cf.date()))
                   for cf in jgb.cashflows()
                   ],
                 columns=['date', 'amount', 'disconut'])
    )
```
||date|amount|disconut|
|---|---:|---:|---:|
|0|July 15th, 2025|0.24794521|0.99752362|
|1|January 15th, 2026|0.25205479|0.99501248|
|2|July 15th, 2026|0.24794521|0.99254845|
|3|January 15th, 2027|0.25205479|0.99004983|
|4|July 15th, 2027|0.24794521|0.98759809|
|5|January 17th, 2028|0.25479452|0.98508495|
|6|July 18th, 2028|0.25068493|0.98261858|
|7|January 15th, 2029|0.24794521|0.98018525|
|8|July 17th, 2029|0.25068493|0.97773115|
|9|January 15th, 2030|0.24931507|0.97529655|
|10|January 15th, 2030|100.00000000|0.97529655|


日本国債の時価評価を行う．Excelでの出力値は少し下の画像のように求めた．

```python
handle = ql.YieldTermStructureHandle(curve) # 定期預金の評価でも用いる
engine = ql.DiscountingBondEngine(handle)
jgb.setPricingEngine(engine)

# JGBの時価は99.99691143
print(f"JGBの時価は{jgb.NPV():.8f}")

# Excel -> 99.99691143
# 誤差(QL-Excel)は-0.00000000
print(f"誤差(QL-Excel)は{jgb.NPV() - 99.99691143:.8f}")
```

内部収益率を求め，デュレーションについても評価した．

```python
# 内部収益率を計算
jgb_yield = jgb.bondYield(day_count, ql.Compounded, ql.Annual)

# デュレーションを計算
jgb_dur = ql.BondFunctions.duration(
              jgb,
              jgb_yield,
              day_count,
              ql.Compounded,
              ql.Annual,
              ql.Duration.Simple)
# Simple, Modified, Macaulay から選べる

# JGBのデュレーションは4.94420034年
print(f"JGBのデュレーションは{jgb_dur:.8f}年")

# Excel -> 4.94420034年
# 誤差(QL-Excel)は-0.00000000年
print(f"誤差(QL-Excel)は{jgb_dur - 4.94420034:.8f}年")
```
## 2-3. Excelによる計算

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3700838/5c867e7f-afcc-70cc-d263-fbb1ab79765b.png)

誤差は非常に小さい（小数点8桁以下）とわかる．

# 3. 定期預金の評価と検証

## 3-1. 商品性

次に以下のような定期預金を考える．

||定期預金|
|:---:|---:|
|年限|5|
|額面|100|
|利息|0.25%|
|利息計算|半年複利|
|日数計算|Act/365|
|開始日（預入日）|2025/1/15|
|終了日（満期日）|2030/1/15|
|||
|市場金利|0.5% フラット|

市場金利は預金の時価を評価するためのイールドである．また以下，預金という商品性に合わせて開始日を預入日，終了日を満期日と表現する．

:::note
円貨預金の評価イールドは市場金利（TONA Swap）を用いることが多い．（実際には現場による．）
::: 

## 3-2. ソースコード

評価にはQuantLibを用いるのでソースコードを記していく．

```python
import QuantLib as ql
from pyxirr import xirr # 内部収益率 計算用

import math             # 指数 計算用
import pandas as pd
```

まずカレンダーを作る．

```Python
eval_date = ql.Date(15, 1, 2025)
ql.Settings.instance().evaluationDate = eval_date

tenor = ql.Period(ql.Semiannual)  # 利払間隔
calendar = ql.Japan()             # 休祝日カレンダー
convention = ql.ModifiedFollowing # 営業日調整
day_count = ql.Actual365Fixed()   # 日数計算
rule = ql.DateGeneration.Backward # 日付生成のルール
end_of_month = False              # 月末日ロール

effective_date = eval_date
# termination_date = eval_date + ql.Period(5, ql.Years)
termination_date = calendar.advance(eval_date, ql.Period(5, ql.Years))

schedule = ql.Schedule(effective_date, termination_date,
                       tenor, calendar,
                       convention, convention,
                       rule, end_of_month)
```
以下，定期預金のキャッシュ・フローから求めていく．この時，定期預金の特徴として半年複利で計算したキャッシュ・フローが期中には支払われず，**満期日にまとめて支払われる**という点がある．よって求めるものは満期日のキャッシュ・フローのみとなる．これは**預金レートの割引率**から計算される．

::: note
満期日のキャッシュローは貯金のDFカーブから計算される．
:::

考えているのは以下のような，満期日に確定したキャッシュ・フローが立つような商品である．

```mermaid
sequenceDiagram
    actor d as 債務者（銀行）
    actor c as 債権者（預金者）
    c ->> d: 元本 (預入日)
    d ->> c: 元本+利息 (満期日)
```

銀行から見たPVを数式で表してみると以下のようになっている．$ \text{DF}^\text{D}_{満期日} $は満期日における預金レートのDFを指す．

```math
    -\text{元本CF}+(\text{元本CF+利払CF})×\text{DF}^\text{D}_{満期日}
```

これが預入日では0となっているので，元利CFで元本CFと利払CFの和を表現すると

```math
    \text{元利CF} = \text{元本CF÷}\text{DF}^\text{D}_{満期日}
```

となる．すなわち先述のように**預金レートの割引率**があれば求まるのだが，これは固定利息0.25%から計算されるはずである．具体的には

```math
    \text{DF}^\text{D}_{満期日} = \Pi_\text{付利回数} (1+\text{利息}*\text{DCF(付利期間))}^{-1}
```
と計算される．ここで付利回数は10回，DCFはおおむね半年（0.5）となっているはずである．この$\text{DF}^\text{D}_{満期日}$で元本CF100を除せば元利CFが求まる．

さらに元利CFをマーケットレートで評価すれば預金の時価PVが求まる．具体的には

```math
    \begin{align*}
        \text{PV} & = \text{元利CF×} \text{DF}^\text{Market}_{満期日} \\
                  & = \text{元本CF×}\frac{\text{DF}^\text{M}_{満期日}}{\text{DF}^\text{D}_{満期日}} \\
                  & = \text{元本CF×} \frac{\exp{(-R^M}_{満期日}\text{×DCF}(\text{年限}))}{\text{DF}^\text{D}_{満期日}}
    \end{align*}
```

実際にこの方法で時価を求めたソースが以下である．QusntLibとは別に最下部のキャプチャにあるようにExcelでも計算しており，それとの差分は元本100に対して0.00000048程となっている．

```Python
nominal     = 100.
depo_rate   = .25 # %単位
market_rate = .5
maturity    = 5

rate_handle = ql.QuoteHandle(
                  ql.SimpleQuote(depo_rate/100) # 実数単位
              )
settle_days_on = 0
term_structure = ql.FlatForward(
                     settle_days_on,
                     calendar,
                     rate_handle,
                     day_count,
                     ql.Compounded,
                     ql.Semiannual
                     )
depo_df = term_structure.discount(termination_date)

# 満期日における預金レートのDFは0.98757875
print(f"満期日における預金レートのDFは{depo_df:.8f}")

ganri_cf = nominal / depo_df

# 満期日の元利CFは101.25774785
print(f"満期日の元利CFは{ganri_cf:.8f}")

market_df = math.exp(-market_rate/100 * day_count.yearFraction(
                                            effective_date,
                                            termination_date)
                                        # 5.00273972
                        )

# 満期日における市場レートのDFは0.97529655
print(f"満期日における市場レートのDFは{market_df:.8f}")

depo_pv = ganri_cf * market_df

# 預金PVは98.75633231
print(f"預金PVは{depo_pv:.8f}")

# Excel -> 98.75633183
# 誤差（QL-Excel）は0.00000048
print(f"誤差（QL-Excel）は{depo_pv - 98.75633183:.8f}")
```

これで時価が出たわけだがデュレーションについても計算していく．しかし今回，満期日にのみキャッシュ・フローが立っていることを考えると，デュレーションの定義に立ち返ればそれは年限そのものとなる．すなわち
```math
    \text{Dur} = \text{DCF}_{預入日，満期日} \approx 5
```
となる．

:::note
預金のキャッシュ・フローが満期日にのみ立つことを考えると，デュレーションは年限と一致している．
:::

最後にQuantLibを用いるも別の方法で時価とデュレーションを求めてみる．そのためにQuantLibのLegオブジェクトを用いる．

まずは時価について以下のように求めることができる．Excelとの誤差，上の方法との誤差についてそれぞれ小さく収まっていると解釈できる．誤差は各利息機関における割引率が小数点8桁のレベルで異なっていることに起因しており，現状は要因が把握できていない．

```python
# 割引金利を0.5%の連続複利で計算
market_curve = ql.FlatForward(settle_days_on,
                       calendar,
                       ql.QuoteHandle(
                           ql.SimpleQuote(market_rate/100)
                           ),
                       day_count
                       )
ytm_handle = ql.YieldTermStructureHandle(market_curve)

# CFはtermination dateのみに立つ
leg = ql.Leg([ql.SimpleCashFlow(ganri_cf, schedule[-1])])

# depoのlegをmarketで割り引く
depo_npv = ql.CashFlows.npv(leg, ytm_handle, False)

# 預金PVをLegオブジェクトで計算すると98.75633231
print(f"預金PVをLegオブジェクトでを使って算すると{depo_npv:.8f}")

# Excel -> 98.75633183
# 誤差（QL-Excel）は0.00000048
print(f"誤差（QL-Excel）は{depo_npv - 98.75633183:.8f}")

# 割引率を使った計算値 -> 98.75633231
# QuantLibによる2つの計算方法の誤差は0.00000000
print(f"QuantLibによる2つの計算方法の誤差は{depo_npv - 98.75633231:.8f}")
```

ここからデュレーションを求めていく．預金の内部収益率を求めるのにpyxirrライブラリのxirr関数を用いる．

```python
dates = [effective_date.to_date(), termination_date.to_date()]
amounts = [-nominal] + [ganri_cf]

# 取組日（開始日）と満期日（終了日）の支払・受取データから内部収益率を計算
depo_yield = xirr(dates, amounts)

# デュレーションを計算
depo_dur = ql.CashFlows.duration(
               leg,
               depo_yield,
               day_count,
               ql.Compounded, 
               ql.Annual,
               ql.Duration.Simple,
               False
               )

# 定期預金のデュレーションは5.00273973年
print(f"定期預金のデュレーションは{depo_dur:.8f}年")

# Excel -> 5.00273973
誤差(QL-Excel)は-0.00000000年
print(f"誤差(QL-Excel)は{depo_dur - 5.00273973:.8f}年")
```

デュレーションについても誤差は小さいと解釈できる．

## 3-3. Excelによる計算

最後にExcelでの計算結果についてキャプチャのみとなるが示しておく．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3700838/bb44e468-7117-e990-75be-3b3371a61323.png)

# -1. 終わりに

今回は固定利付商品でも，細かいところで商品性が異なる2つについて整理してみた．BSの観点から見ると大きな差はないが，利払のタイミングという意味では明らかに異なっており，簿価残高が大きくなるほどその影響は無視できないと個人的には感じている．

本記事に続いてALMの分野で

**「定期預金（負債）とアモチ債券（資産）のデュレーションギャップをスワップで調整する」**

という記事にチャレンジしてみたいと思っている．その他

**「TONAスワップ，Basis Swapを評価しスワップレート（フェアレート）からDFカーブを引いていく」**

についてもまとめたいと考えている．本記事もこれらの記事においても自分の学習の整理がメインではあるが，初学者の助けにもなれたら幸いである．