【QuantLib-Python】スワップションの評価

【修正】2025/5/25
コメントでカーブ生成においてTONAレートとTIBORを混同しているというご指摘をいただき修正いたしました．

TONA割引カーブ生成においてはTONAレートとTONA Swapレートでブートストラップを行うように，TIBORフォワードカーブ作成においては3M DTIBORでブートストラップを行うように修正いたしました．

取り急ぎ全体を修正したので誤りを広めている状態は脱せたと思っています．ご迷惑をおかけいしました．

# 0. はじめに

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

## 0-2. このページで解説すること

QuantLibを用いた
1. ブートストラップ
1. スワップの時価評価
1. スワップション（バミューダン）の時価評価

## 0-3. 前提

- 計算環境
    - Windows 11
    - Python 3.13
    - QuantLib 1.37
- 数理ファイナンスの知識
    - スワップ，スワップションの商品性
    - QuantLibについて基本的な文法

# 1. ソースコード

次の条件のスワップを考える．
-  DTIBOR 3Mスワップ
    -  受け
    -  スポット・スタート
    -  想定元本：100
    -  満期：5年
    -  固定レグ：~~6ヶ月ごとに0.5%~~ 3ヶ月ごとに1.0%
    -  変動レグ：3ヶ月ごとに日本円TIBOR 3M，最初の利払いは0.77364% 
    - （スプレッド：なし）

これを原資産とするスワップションを考える．
- スワップション
    - コール
    - 行使日：バミューダン
    - ストライク：~~0.35%~~ 0.9%

これらのプライシングを記述したの次のソースコードである．
## 1.0.5 修正版

```python
import QuantLib as ql
```

```python
eval_on = ql.Date(2,5,2025)
ql.Settings.instance().evaluationDate = eval_on

settle_days_on = 0                    # 決済日
settle_days_sn = 2                    # 決済日
calendar       = ql.Japan()
convention     = ql.ModifiedFollowing # 営業日調整
end_of_month   = False                # 月末日ロール
currency       = ql.JPYCurrency()
day_counter    = ql.Actual365Fixed()  # DTIBOR, OIS共通
```

```python
instruments = [
    # 商品    年限   レート
    ('depo', '1D',  0.47700), # 預金
    ('ois',  '1W',  0.47713), # スワップ
    ('ois',  '2W',  0.47750),
    ('ois',  '3W',  0.47750),
    ('ois',  '1M',  0.47729),
    ('ois',  '2M',  0.47854),
    ('ois',  '3M',  0.48000),
    ('ois',  '4M',  0.48543),
    ('ois',  '5M',  0.49281),
    ('ois',  '6M',  0.49892),
    ('ois',  '7M',  0.50854),
    ('ois',  '8M',  0.51479),
    ('ois',  '9M',  0.52000),
    ('ois',  '10M', 0.52625),
    ('ois',  '11M', 0.53148),
    ('ois',  '1Y',  0.53750),
    ('ois',  '15M', 0.55750), #フロントスタブ
    ('ois',  '18M', 0.57469), #フロントスタブ
    ('ois',  '2Y',  0.60500),
    ('ois',  '3Y',  0.65375),
    ('ois',  '4Y',  0.69500),
    ('ois',  '5Y',  0.73750),
]

tona = ql.OvernightIndex('TONA',         # インデックスの名前
                         settle_days_on,
                         currency,
                         calendar,
                         day_counter
                         )

helpers = ql.RateHelperVector()
for instrument, tenor, rate in instruments:    
    if instrument == 'depo':
        helpers.append(
            ql.DepositRateHelper(
                rate/100.0,
                ql.Period(tenor),
                settle_days_on,
                calendar,
                convention,
                end_of_month,
                day_counter
                )
            )
    if instrument == 'ois':
        helpers.append(
            ql.OISRateHelper(
                settle_days_sn,
                ql.Period(tenor),
                ql.QuoteHandle(ql.SimpleQuote(rate/100.0)),
                tona,
                paymentLag = 2,                              # Payment Delay
                paymentConvention = convention,
                paymentCalendar = calendar
                    )
            )

discount_curve = ql.PiecewiseLogLinearDiscount(
                     eval_on,
                     helpers,
                     day_counter
                     )
discount_handle = ql.YieldTermStructureHandle(discount_curve)

tona = ql.OvernightIndex(
    'TONA',
    settle_days_on,
    currency,
    calendar,
    day_counter,
    discount_handle         # 構築したカーブを登録する
    )
```

```python
tibor = ql.Tibor(ql.Period('3M'))

instruments = [
    # 商品    年限 レート
    ('depo', '3M',  0.77378),   # 預金 (%)
    ('fra',  '4M',  0.79373),   # FRA (%)
    ('fra',  '5M',  0.81378),
    ('fra',  '6M',  0.83250),
    ('fra',  '7M',  0.85000),
    ('fra',  '8M',  0.88000),
    ('fra',  '9M',  0.91373),
    ('fra',  '10M', 0.92123),
    ('fra',  '11M', 0.93063),
    ('swap', '1Y',  32.50000),  # Swap (bp: spread)
    ('swap', '18M', 34.50000),
    ('swap', '2Y',  36.00000),
    ('swap', '3Y',  37.50000),
    ('swap', '4Y',  38.50000),
    ('swap', '5Y',  39.12500),
]

helpers = ql.RateHelperVector()
for instrument, tenor, quote in instruments:    
    if instrument == 'depo':
        helpers.append(
            ql.DepositRateHelper(
                quote/100.,
                ql.Period(tenor),
                settle_days_sn,
                calendar,
                convention,
                end_of_month,
                day_counter
                )
            )
    if instrument == 'fra':
        helpers.append(
            ql.FraRateHelper(
                quote/100.,
                int(tenor.removesuffix('M'))-3,
                tibor
                )
            )
    if instrument == 'swap':
        helpers.append(
            ql.OvernightIborBasisSwapRateHelper(
                ql.QuoteHandle(ql.SimpleQuote(quote/10000.)),
                ql.Period(tenor),
                settle_days_sn,
                calendar,
                convention,
                end_of_month,
                tona,
                tibor,
                discount_handle
                )
            )
        
fwd_curve = ql.PiecewiseFlatForward(
                eval_on,
                helpers,
                day_counter
                )
fwd_handle = ql.YieldTermStructureHandle(fwd_curve)

tibor = ql.Tibor(ql.Period('3M'), fwd_handle)    # プロジェクションカーブ
tibor.clearFixings
tibor.addFixing(ql.Date(11,4,2025), 0.81091/100) # 2025年4月11日（15日の2営業日前）のフィキシング
```

```python
effect_on = ql.Date(15,1,2025)               # 開始日
terminate_on = calendar.advance(effect_on,   # 終了日（5年満期）
                                5, ql.Years
                                )
tenor = ql.Period(ql.Quarterly)              # 利払い間隔
date_generation = ql.DateGeneration.Backward # 日付生成

tenor_fixed = ql.Period(ql.Quarterly)
schedule_fixed = ql.Schedule(effect_on, terminate_on, 
                             tenor_fixed, calendar,
                             convention, convention,
                             date_generation, end_of_month)

tenor_float = ql.Period(ql.Quarterly)
schedule_float = ql.Schedule (effect_on, terminate_on, 
                              tenor_float, calendar,
                              convention, convention,
                              date_generation, end_of_month)

fixed_rate = 1.
spread = .0     # フラット

swap = ql.VanillaSwap(ql.VanillaSwap.Receiver, # 払いor受け
                              100,             # 元本
                              schedule_fixed,  # 固定のスケジュール
                              fixed_rate/100,  # 固定金利
                              day_counter, 
                              schedule_float,
                              tibor,
                              spread,
                              day_counter
                              )

swap_engine = ql.DiscountingSwapEngine(discount_handle)
swap.setPricingEngine(swap_engine)
```

```python
bermudan_dates = [_ for _ in schedule_fixed][:-1]
bermudan_exercise = ql.BermudanExercise(bermudan_dates)
swaption = ql.Swaption(swap, bermudan_exercise)

a = 0.01
sigma = 0.01
hw_model = ql.HullWhite(discount_handle, a, sigma)
time_steps = 50

swaption.setPricingEngine(ql.TreeSwaptionEngine(hw_model, time_steps))
print(f'スワップションのプライスは{swaption.NPV():.3f}')
```


<details><summary>以下，修正前の初版</summary>

import QuantLib as ql

eval_date = ql.Date(31,3,2023)
ql.Settings.instance().evaluationDate = eval_date

settle_days_over_night = 0                      # 決済日
settle_days_spot_next = 2                       # 決済日
calendar = ql.Japan()                           # 祝休日
business_day_convention = ql.ModifiedFollowing  # 営業日調整
end_of_month = False                            # 月末日ロール

instruments = [
    # 商品    年限 レート
    ('depo', '1M', 0.1), # 預金
    ('depo', '3M', 0.1),
    ('depo', '6M', 0.2),
    ('fra',  '6M', 0.2), # 金先
    ('ois',  '1Y', 0.2), # スワップ
    ('ois',  '2Y', 0.3),
    ('ois',  '3Y', 0.4),
    ('ois',  '5Y', 0.5)
]

overnight_index_tona = ql.OvernightIndex('TONA',                 # インデックスの名前
                                         settle_days_over_night, # 決済日
                                         ql.JPYCurrency(),       # 通貨
                                         ql.Japan(),             # 休祝日カレンダー
                                         ql.Actual365Fixed()     #利息付利ルール
                                         )

helpers = ql.RateHelperVector()
for instrument, tenor, rate in instruments:    
    if instrument == 'depo':
        helpers.append(
            ql.DepositRateHelper(
                rate/100.0,
                ql.Period(tenor),
                settle_days_spot_next,
                calendar,
                business_day_convention,
                end_of_month,
                ql.Actual360()
                )
            )
    if instrument == 'fra':
        months_to_start = ql.Period(tenor).length()
        ibor_index = ql.Tibor(ql.Period('3M'))
        # ibor_index = ql.Euribor6M(yts)
        helpers.append(
            ql.FraRateHelper(
                rate/100.0,
                months_to_start,
                ibor_index
                )
            )
    if instrument == 'ois':
        helpers.append(
            ql.OISRateHelper(
                settle_days_spot_next,
                ql.Period(tenor),
                ql.QuoteHandle(ql.SimpleQuote(rate/100.0)),
                overnight_index_tona,                        # iborIndex
                paymentLag = 2,                              # 複利後決めのPayment Delay
                paymentConvention = business_day_convention,
                paymentCalendar = ql.Japan()
                    )
            )

log_cubic_discount = ql.PiecewiseLogCubicDiscount(settle_days_spot_next,
                                                  ql.Japan(),
                                                  helpers,
                                                  ql.Actual365Fixed()
                                                  )

effective_date = ql.Date(1,4,2023)                # 開始日
termination_date = calendar.advance(effective_date,
                                    5, ql.Years
                                    )             # 終了日
tenor = ql.Period(ql.Semiannual)                  # 間隔
termination_day_convention = ql.ModifiedFollowing # 終了日調整
date_generation = ql.DateGeneration.Forward       # 日付生成

tenor_fixed = ql.Period(ql.Semiannual)
schedule_fixed = ql.Schedule(effective_date, termination_date, 
                             tenor_fixed, calendar,
                             business_day_convention, termination_day_convention,
                             date_generation, end_of_month)

tenor_float = ql.Period(ql.Quarterly)
schedule_float = ql.Schedule (effective_date, termination_date, 
                              tenor_float, calendar,
                              business_day_convention, termination_day_convention,
                              date_generation, end_of_month)

fixed_rate = 0.5
day_counter = ql.Actual365Fixed() #日本円TIBOR
spread = 0

yts = ql.YieldTermStructureHandle(log_cubic_discount)
ibor_index = ql.Tibor(ql.Period(3, ql.Months), yts) #プロジェクションカーブ
ibor_index.addFixing(ql.Date(30, 3, 2023), 0.4/100) # 2023年3月30日のフィキシング

swap = ql.VanillaSwap(ql.VanillaSwap.Receiver, # 払いor受け
                              100,             # 元本
                              schedule_fixed,  # 固定のスケジュール
                              fixed_rate/100,  # 固定金利
                              day_counter, 
                              schedule_float,
                              ibor_index,
                              spread,
                              ibor_index.dayCounter()
                              )

swap_engine = ql.DiscountingSwapEngine(yts)
swap.setPricingEngine(swap_engine)

bermudan_dates = [ _ for _ in schedule_fixed][:-1]
bermudan_exercise = ql.BermudanExercise(bermudan_dates)

black_engine = ql.BlackSwaptionEngine(yts,
                                      ql.QuoteHandle(ql.SimpleQuote(0.55)),
                                      day_counter
                                      )
swaption = ql.Swaption(swap, bermudan_exercise)
swaption.setPricingEngine(black_engine)

</details>

以上のソースコードをいくつかに分解してそれぞれ解説していく．

## 1-1. ライブラリのインポート

途中式を出力するためにpandasを合わせてインポートする．
```Python
deleted
```

## 1-2. 評価日の設定

```Python
deleted
```

## 1-3. 決済周りの設定

決済日を2パターン設定し，前者はOISについて，後者はTIBORについて用いる．
祝休日として日本カレンダーを設定する．
日本カレンダーにおいて利払日が非営業日となる場合，次の営業日に利払日を変更させる．ただし次の営業日が月をまたぐ場合は前の営業日に利払日を変更させるものとする．これらのルールは利払日が月末であっても例外なく適用する．

```Python
deleted
```

## 1-4. ブートストラップ

預金と金利先渡とスワップのパーレートからDFカーブを引く．

```Python
deleted                                       )
```

引いたDFカーブをpandasのDataFrame形式で確認．

```Python
deleted
```

## 1-5. 原資産のスワップの定義

固定レグと変動レグのそれぞれについて利払スケジュールを定める．

```Python
deleted
```

固定レグの利払日を確認．

```Python
deleted
```

変動レグの利払日を確認．

```Python
deleted
```

変動レグの参照金利を定義．
```Python
deleted
```

固定レグの利息を確認．
```Python
deleted
```
変動レグの利息を確認．
```Python
deleted
```

1-3.で定義したスポットカーブを用いて時価評価する．
```Python
deleted
```

プライスと無裁定レートを確認．

```Python
deleted
```

## 1-6. スワップションを定義

固定金利の利払日（6ヶ月ごと）を行使日とする．ただし満期日は除く．

```Python
deleted
```

行使日を確認．

```Python
deleted
```

スワップションをブラックモデルで時価評価．

```Python
deleted
```

プライスを確認．

```Python
deleted
```

# 2. おまけ

スワップは期中評価に対応しているがスワップションは対応していないようだ．
```Python
new_eval_on = ql.Date(31,3,2024)
ql.Settings.instance().evaluationDate = new_eval_on
ibor_index.addFixing(ql.Date(28,3,2024), 0.6/100) # 3月29日のフィキシング

print(f'スワップのプライスは{swap.NPV():.3f}')
print(f'無裁定レートは{swap.fairRate():.2%}')
```

```Python
swaption.setPricingEngine(black_engine)
print(f'スワップションのプライスは{swaption.NPV():.3f}')
# ERROR: date (April 3rd, 2023) before reference date (March 31st, 2024)
```

# -1. おわりに

以上で見たようにQuantLibは簡単なコードでスワップションのような複雑な商品の時価評価ができる便利なものである．それにも関わらずWeb上の文献数は少なく，これから普及していく段階にあるように思える．拙文ながら本記事がQuantLib普及の一助となれば幸いである．