【修正】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へ.
0-2. このページで解説すること
QuantLibを用いた
- ブートストラップ
- スワップの時価評価
- スワップション(バミューダン)の時価評価
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 修正版
import QuantLib as ql
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共通
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 # 構築したカーブを登録する
)
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営業日前)のフィキシング
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)
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}')
以下,修正前の初版
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)
以上のソースコードをいくつかに分解してそれぞれ解説していく.
1-1. ライブラリのインポート
途中式を出力するためにpandasを合わせてインポートする.
deleted
1-2. 評価日の設定
deleted
1-3. 決済周りの設定
決済日を2パターン設定し,前者はOISについて,後者はTIBORについて用いる.
祝休日として日本カレンダーを設定する.
日本カレンダーにおいて利払日が非営業日となる場合,次の営業日に利払日を変更させる.ただし次の営業日が月をまたぐ場合は前の営業日に利払日を変更させるものとする.これらのルールは利払日が月末であっても例外なく適用する.
deleted
1-4. ブートストラップ
預金と金利先渡とスワップのパーレートからDFカーブを引く.
deleted )
引いたDFカーブをpandasのDataFrame形式で確認.
deleted
1-5. 原資産のスワップの定義
固定レグと変動レグのそれぞれについて利払スケジュールを定める.
deleted
固定レグの利払日を確認.
deleted
変動レグの利払日を確認.
deleted
変動レグの参照金利を定義.
deleted
固定レグの利息を確認.
deleted
変動レグの利息を確認.
deleted
1-3.で定義したスポットカーブを用いて時価評価する.
deleted
プライスと無裁定レートを確認.
deleted
1-6. スワップションを定義
固定金利の利払日(6ヶ月ごと)を行使日とする.ただし満期日は除く.
deleted
行使日を確認.
deleted
スワップションをブラックモデルで時価評価.
deleted
プライスを確認.
deleted
2. おまけ
スワップは期中評価に対応しているがスワップションは対応していないようだ.
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%}')
swaption.setPricingEngine(black_engine)
print(f'スワップションのプライスは{swaption.NPV():.3f}')
# ERROR: date (April 3rd, 2023) before reference date (March 31st, 2024)
-1. おわりに
以上で見たようにQuantLibは簡単なコードでスワップションのような複雑な商品の時価評価ができる便利なものである.それにも関わらずWeb上の文献数は少なく,これから普及していく段階にあるように思える.拙文ながら本記事がQuantLib普及の一助となれば幸いである.