【2025年1月31日】
この記事は執筆中である.諸事情により未完成な版を公開したことを許していただきたい.本題である金融工学理論の実装が一部未完成でるが,ポエムとしては完成している.
0. はじめに
0-1. この記事を読めば
-
わかる
-
Overnight Index Swap
- 商品性
- 時価評価ロジック
- 金利データ取得(Power Query)
- 割引カーブ構築(pandas, scipy, xlwings)
- 検証(QuantLib)
-
ベストプラクティス
- 入力・収集・計算・出力の各ステップにおける規約
-
-
共有したい
- EUC現場の現状と理想
0-2. 誰のための記事か
金融機関でEnd User Computing(以下,EUC)をしているすべての開発者(3-5年目程度)に捧げたい.持続可能かつ教育的な開発体制を一緒に考えさせていただきたいと思っている.またそういった開発者の上席の方々も目を通していただけると幸いである.具体的には0-3-1. 私にとってベストプラクティスとはだけでもご一読いただき現状と理想を共有したい.
割引カーブ構築の読み物としてはマーケット部門の担当者で2年目程度の方を読者として想定している.場合によっては2. OISの時価評価と3-2. 割引カーブ構築(Bootstrapping)から読んでほしい.
時間が限られている方は0-4. 忙しい皆様へだけでも読んでいただけるとありがたい.こちらに同意いただけるならこの記事を読む必要はないと思われる.もう少しだけ時間のある方は3-1. 金利データ取得(JSCC)でPower Queryの運用について見てほしい.
0-3. タイトルについて
0-3-1. 私にとってベストプラクティスとは
EUCのベストプラクティスとは何か.ベストプラクティスそれ自体の意味に立ち返ると
最も効果的、効率的な実践の方法。または最優良の事例のこと。1
という説明が見つかった.私にとって「最優良」とは,開発者(エンドユーザー)が開発したツールをその人とは異なる概して年次の浅い使用者(真のエンドユーザ―)が活用できるような開発手法のことを指す.
さらに言うと活用とは秘伝のタレみたいな古のツールによる「知らんけどボタンぽちーw」ではなく,使用者がシステム要件を把握し,適切なバージョンのツールを用いることを定義とする.これらの概念を押さえてもらうだけで効果的,効率的な体制に近づくことができると思う.
そうなると真のエンドユーザーに対するIT教育は避けることができず,その意味でEUCと教育(特に体系的な新人教育)は不可分だといえる.理想論ではあるがこのように広くEUCをとらえたとき,あなたの現場ではベストプラクティスが実現できているだろうか.
あなたが会社のお金(が支払われている時間)で開発した "あなた"さんツール , 既存ツール"あなた"さんVer は会社の資産となっている.もしそのツールをあなただけが使っていて,退職後に誰も使わないとしたらそのツールに資産価値はあるだろうか……
もちろんエンドユーザー全員がコーディングできる必要はない.なまじVBAなど書けるほうが秘伝を極めていく危険性が高いともいえる.現代におけるVBAの利活用については@k_maki氏の記事が勉強になる.「金融工学」タグに寄せられた皆様にも読んでもらえたら本望である.
氏は常々,運用・保守コストが小さく,修正コストが重くならないような開発体制を提唱している.そのための道具がテーブル,Power Query,Power Pivot,Power BI,Pythonである.また次のような「Legacy Excel」のアンチパターンにも氏の記事は触れている.
アンチパターン1 前回作業への継ぎ足し
アンチパターン2 タイミングの悪いRPA
物理的リソースの問題などもあり,これらに該当してしまっている現場も多いのではないだろうか.こういった現状を多少なりとも解決できると考えるのが3. 割引カーブ構築以降で紹介する私なりのベストプラクティスである.
0-3-2. Overnight Index Swapとは
ベストプラクティスの前にこの記事は金融工学のそれでもあるためOvernight Index Swapについて説明する.
Overnight Index Swap(以下,OIS)は金利デリバティブの1つである.固定金利と変動金利を交換するSwapの中で,変動金利として翌日物金利を参照するものを指す.
この記事では翌日物金利としてTONA(Tokyo Over Night Average rate)を採用する.TONAは日本の金融機関が1日間(Over Night)無担保で資金を貸し借りする際の金利(の加重平均値)のことである.LIBOR廃止以降は日本の無リスク金利として使用されている.
1. OISの商品性で再度触れる.
0-4. 忙しい皆様へ
この記事全体で紹介するベストプラクティスな規約を簡単に列挙する.理由については各章を見ていただきたい.1つ2つでも同意が得られたら嬉しい.
大前提としてツールによる処理は入力・収集・計算・出力という4ステップに分割して考える.それぞれに適したツールが存在している.入出力ツールは特にその現場に依ってくるが,一例として入力シート,計算シート,出力シートからなるExcelファイルを考える.
-
入力(Input)
- 入力シートを使用
- もしくは収集用Excelファイルで直接パラメータクエリとして登録
-
収集(Aggregation)
- 収集用ExcelファイルのPower Queryでデータを取得
- レコード内であればM言語で加工,不可能ならば計算ステップで対応
- 行のフィルターは最初,列の削除は最後
- 加工したデータをテーブルとして読み込み
-
計算(Calculation)
- 加工済みテーブルをxlwingsで取得,Pythonで計算
- 重い計算はPyhton -> pybind11 -> C++で対応
-
計算シートに(複数の)
F_"ファクト"
テーブルとして書き出し - (この段階では計算結果の集約を行わない)
-
出力(Output)
-
D_"ディメンション"
,C_"カレンダー"
などのテーブルも合わせてデータモデルに格納 - Power Pivotを用いて出力シートで計算結果を集約・可視化
- (定時業務ではPower BIを活用.毎月Power Pivotをメールで送付したりしない)
-
- その他
- Excel:関数は最低限,VBAはマクロボタンなどに限定
- Python:PEP8を順守,docstringを記載
- 体制:社内のコーディング規約を定義,Gitでバージョンを管理
0-5. 計算環境
-
Windows 11 24H2
-
Excel 2024
-
Python 3.13.1
- pandas 2.2.3
- scipy 1.15.1
- QuantLib 1.37
- xlwings 0.33.6
-
Visual Studio Code 1.96.4
1. OISの商品性
【執筆中】
2. OISの時価評価
【執筆中】
3. 割引カーブ構築
3-1. 金利データ取得(JSCC)
日本証券クリアリング機構(Japan Securities Clearing Corporation; JSCC)のhome > 各種情報 > 統計情報 > 金利スワップに「金利スワップ取引に関する清算値段【日次】」の統計データがあり,Swap金利をPDF形式で取得できる.これをPower Queryで読み込んでいく.
Power QueryはPDFの表を読み込める.(読み込めないものもあるが……)
Webには前営業日のデータしかないが,ここでは当該ファイルをローカルに蓄積している前提で評価日をパラメータクエリとして登録する.すなわち評価日を自由に設定できるようにする.この記事では1月24日に設定している.
入力シートから評価日を設定してもよい.
#date(2025, 1, 24) meta [IsParameterQuery=true, Type="Date", IsParameterQueryRequired=true]
また利払スケジュールを計算するために用いるSpot日もパラメータクエリとして登録する.クエリの中で簡単な関数GetSpotDate
を定義している.その中のDayOfWeek
は,ここでは日曜日からの日数を返す関数である(日 -> 0, 月 -> 1, ... , 土 -> 6).評価日は営業日となる前提の上で簡易的に場合分けをしている.
注意としてここでは祝日を考慮していない.考慮するためにはカレンダーテーブルが必要となってくる.
let
GetSpotDate = (eval as date) as date =>
let
spot = if Date.DayOfWeek(eval, Day.Sunday) <= 3 // 月火水
then Date.AddDays(eval, 2) // -> 水木金
else Date.AddDays(eval, 4) // 木金 -> 月火
in
spot,
Spot_on = GetSpotDate(Eval_on)
in
Spot_on
前述のとおりWebサイトにデータがない場合はローカルを参照するため,ファイルを蓄積しているパスもパラメータクエリとして登録しておく.パスは適宜読み替えていただきたい.
"C:\Users\htki\workDirectory\"
meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true]
パラメータクエリの登録は以上である.これらを使ってSwap金利のデータを取得する.評価日からURLとファイルパスを生成し前者からソースを取得,もし叶わなければ後者から取得する.
実際に用いたクエリ(クリックで開きます)
let
Date2URL = (_ as date) as text =>
// 引数: 評価日 -> 戻り値: JSCCのURL
let
yyyyMMdd = Date.ToText(_, "yyyyMMdd"),
URL = "https://www.jpx.co.jp/jscc/cimhll0000000umu-att/SettlementRates_"
& yyyyMMdd & ".pdf"
in
URL,
Date2FilePath = (_ as date) as text =>
// 引数: 評価日 -> 戻り値: ソースファイルのパス
let
yyyyMMdd = Date.ToText(_, "yyyyMMdd"),
Path = Folder_Path & "SettlementRates_" & yyyyMMdd & ".pdf"
in
Path,
// URL経由の金利データ取得を試行
#"Generate URL" = Date2URL(Eval_on),
#"Try URL" = try Pdf.Tables(Web.Contents(#"Generate URL"), [Implementation="1.3"]),
// URL経由で取得できなかった場合の処理
#"Generate File Path" = Date2FilePath(Eval_on),
#"Get Source" = if #"Try URL"[HasError]
then Pdf.Tables(File.Contents(#"Generate File Path"), [Implementation="1.3"])
else #"Try URL"[Value],
#"Expand Yield Table" = #"Get Source"{[Id="Table001"]}[Data]
in
#"Expand Yield Table"
テキスト型の日付はXX_date
ではなくXX_yyyyMMdd
やXX_strfdate
とする.
取得した金利テーブルからOISに関する3列(column1-3)を取得し加工する.最終的には次のようなT_OIS
テーブルを目指す.
Eval_on | Maturity | Rate |
---|---|---|
2025/01/24 | ON | 0.23% |
2025/01/24 | 1W | 0.48% |
2025/01/24 | 2W | 0.48% |
... | ... | ... |
2025/01/24 | 1Y | 0.60% |
2025/01/24 | 15M | 0.64% |
2025/01/24 | 18M | 0.68% |
2025/01/24 | 2Y | 0.74% |
... | ... | ... |
2025/01/24 | 40Y | 1.98% |
そのために以下のステップでクエリを適用した.
ステップ名 |
---|
Keep Only OIS columns |
Promote header |
Add Eval Date column |
Rename columns |
Transforme type |
Convert Unit |
Replace value: 1D -> ON |
Keep Only Necessary columns |
可能な限りステップ名で内容を表現する.また詳細エディタからコメントを入力する.
詳細エディターで// コメント
を追加しておけば画面右端のステップ名の横にinformationアイコンが表示される.カーソルを当てればコメントが確認できる.
ステップ名は英語が好ましいがコメントは日本語でもよいと思う.この辺りは一般のコーディングと同様である.
また最後のKeep Only Necessary columns
ステップでは選択した列を削除する「列の削除」ではなく,選択した列を残す「他の列を削除」を使う.どちらも選択した列名が詳細エディタに残るのだが,削除した列名より残した列名に意味がある.
列は最後に「他の列を削除」で削除する.このとき保持列を選択した順番で並び替えが行われる.
実際に用いたクエリ(クリックで開きます)
let
#"Keep Only OIS columns" = Table.SelectColumns(GetYield,{"Column1",
"Column2",
"Column3"}),
#"Promote header" =
Table.PromoteHeaders(#"Kepp Only OIS columns", [PromoteAllScalars=true]),
// パラメータクエリEval_onを参照
#"Add Evel Date column" =
Table.AddColumn(#"Promote header", "Eval_on", each Eval_on, type date),
#"Rename columns" =
Table.RenameColumns(#"Add Evel Date column",{{"Column1", "Maturity"},
{"JPY OIS", "Rate"},
{"Column3", "Unit"}}),
#"Transforme type" =
Table.TransformColumnTypes(#"Rename columns",{{"Rate", type number}}),
// %単位 -> 実数単位に変換
#"Convert Unit" =
Table.TransformColumns(#"Transforme type", {{"Rate", each _ / 100,
Percentage.Type}}),
#"Replace value: 1D -> ON" =
Table.ReplaceValue(#"Convert Unit","1D","ON",Replacer.ReplaceText,{"Maturity"}),
// 不要列を削除するとともに並び替え
#"Keep Only Necessary columns" =
Table.SelectColumns(#"Replace value: 1D -> ON", {"Eval_on", "Maturity", "Rate"})
in
#"Keep Only Necessary columns"
また可能であれば列の追加と型の定義を同時に行う.これは詳細エディターから
Table.AddColumn(ソース, "カスタム", each 0, type number)
// アンチパターン
// 追加されたカスタム = Table.AddColumn(ソース, "カスタム", each 0),
// 変更された型 = Table.TransformColumnTypes(追加されたカスタム,{{"カスタム", type number}})
とすれば1行にまとめられる.単純にステップ数が半分になる.
追加されたカスタム
-> 変換された型
-> 追加された...
はまとめられる.
次に利払スケジュールを計算していく.この後得られるT_Payment
をPythonで読み込み,カーブを計算していくことになる.
まず上のT_OIS
にSpot日のレコードを追加する.この時OISのSpot金利は市場に存在しないため,Rate列の値が埋まらないことに注意する.しかしカーブ構築のためには何かしらの方法でSpot金利を計算する必要がある.詳細は3-2. 割引カーブ構築(Bootstrapping)で述べる.
その後(金利)付利開始日,(金利)付利終了日,応当日,利払日の順で列を追加していく.ソートすると次のようなT_Payment
テーブルが得られる.
データモデルに格納しないテーブルのプレフィックスはT_
(これは諸説あり).
Eval_on | Maturity | Rate | Anniversary | Pay on | Interest Begin on | Interest End on |
---|---|---|---|---|---|---|
2025/1/24 | ON | 0.00227 | 2025/1/27 | 2025/1/29 | 2025/1/24 | 2025/1/27 |
2025/1/24 | Spot | 2025/1/28 | 2025/1/30 | 2025/1/24 | 2025/1/28 | |
2025/1/24 | 1W | 0.00475 | 2025/2/4 | 2025/2/6 | 2025/1/28 | 2025/2/4 |
2025/1/24 | 2W | 0.00475 | 2025/2/11 | 2025/2/13 | 2025/1/28 | 2025/2/11 |
... | ... | ... | ... | ... | ... | ... |
2025/1/24 | 1Y | 0.0060031 | 2026/1/28 | 2026/1/30 | 2025/1/28 | 2026/1/28 |
2025/1/24 | 15M | 0.0064135 | 2026/4/28 | 2026/4/30 | 2025/1/28 | 2026/4/28 |
2025/1/24 | 18M | 0.0068125 | 2026/7/28 | 2026/7/30 | 2025/1/28 | 2026/7/28 |
2025/1/24 | 2Y | 0.0074017 | 2027/1/28 | 2027/2/1 | 2025/1/28 | 2027/1/28 |
... | ... | ... | ... | ... | ... | ... |
2025/1/24 | 40Y | 0.0198333 | 2065/1/28 | 2065/1/30 | 2025/1/28 | 2065/1/28 |
そのために以下のステップでクエリを適用した.ここでもステップ名に気を払う.
ステップ名 |
---|
Insert Spot Date row |
Add Day Grid column |
Add Week Grid column |
Add Month Grid column |
Add Year Grid column |
Add Begin Date column |
Define 3 Functions |
Add End Date column |
Add Anniversary Date column |
Add Payment Date column |
Keep Only Necessary columns |
実際に用いたクエリ(クリックで開きます)
let
#"Insert Spot Date row" =
Table.InsertRows(T_OIS, 1,{[Eval_on = Eval_on, Maturity = "Spot", Rate = null]}),
// 日,週,月,年単位のGridを挿入(4step)
// 例: ON -> 1(日),2Y -> 2(年)
#"Add Day Grid column" = Table.AddColumn(#"Insert Spot Date row", "Grid (days)",
each if [Maturity] = "ON" then 1
else if [Maturity] = "Spot" then 2
else null, Int64.Type),
#"Add Week Grid column" = Table.AddColumn(#"Add Day Grid column", "Grid (weeks)",
each if Text.EndsWith([Maturity], "W")
then Number.FromText(
Text.Select([Maturity],
{"0".."9"})
)
else null, Int64.Type),
#"Add Month Grid column" = Table.AddColumn(#"Add Week Grid column", "Grid (months)",
each if Text.EndsWith([Maturity], "M")
then Number.FromText(
Text.Select([Maturity],
{"0".."9"})
)
else null, Int64.Type),
#"Add Year Grid column" = Table.AddColumn(#"Add Month Grid column", "Grid (years)",
each if Text.EndsWith([Maturity], "Y")
then Number.FromText(
Text.Select([Maturity],
{"0".."9"}
))
else null, Int64.Type),
// 付利開始日の追加
// ON,Spot -> 評価日,その他 -> Spot日
#"Add Begin Date column"= Table.AddColumn(#"Add Year Grid column", "Interest Begin on",
each if [Maturity] = "ON"
or [Maturity] = "Spot"
then Eval_on
else Spot_on, type date),
#"Define 3 Functions" = #"Add Begin Date column",
GetNextBusinessDate = (begin as date) as date =>
// 引数: 基準日 -> 戻り値: 翌営業日
let
next = if Date.DayOfWeek(begin, Day.Sunday) <= 4
then Date.AddDays(begin, 1)
else Date.AddDays(begin, 3)
in
next,
GetAfterNextBusinessDate = (begin as date) as date =>
// 引数: 基準日 -> 戻り値: 翌々営業日
let
after_next = if Date.DayOfWeek(begin, Day.Sunday) <= 3
then Date.AddDays(begin, 2)
else Date.AddDays(begin, 4)
in
after_next,
GetFollowingBusinessDate = (begin as date) as date =>
// 引数: 基準日 -> 戻り値: 基準日が営業日なら基準日,そうでなければ翌営業日
let
following = if Date.DayOfWeek(begin, Day.Monday) <= 5
then begin
else GetNextBusinessDate(begin)
in
following,
// 付利終了日の追加
// 付利開始日から場合分けで年限分だけ進める
#"Add End Date column" =
Table.AddColumn(#"Add Begin Date column", "Interest End on",
each if [Maturity] = "ON" then GetNextBusinessDate(
[Interest Begin on]
) else
if [Maturity] = "Spot" then GetAfterNextBusinessDate(
[Interest Begin on]
) else
if Text.End([Maturity],1) = "W"
then GetFollowingBusinessDate(
Date.AddDays([Interest Begin on],
7 * [#"Grid (weeks)"])
) else
if Text.End([Maturity],1) = "M"
then GetFollowingBusinessDate(
Date.AddMonths([Interest Begin on],
[#"Grid (months)"])
)
else GetFollowingBusinessDate(Date.AddYears(
[Interest Begin on],
[#"Grid (years)"])
)
, type date),
// 応当日(=今回は付利終了日)の追加
#"Add Anniversary Date column" = Table.AddColumn(#"Add End Date column", "Anniversary",
each [Interest End on], type date),
// 支払日(=応当日+2営業日)の追加
#"Add Payment Date column" = Table.AddColumn(#"Add Anniversary Date column", "Pay on",
each GetAfterNextBusinessDate(
[Anniversary]
), type date),
#"Keep Only Necessary columns" = Table.SelectColumns(#"Add Payment Date column",
{"Eval_on", "Maturity", "Rate",
"Anniversary", "Pay on", "Interest Begin on", "Interest End on"})
in
#"Keep Only Necessary columns"
以降はT_Payment
をxlwingsでPythonに読み込み,Bootstrappingにより割引カーブを構築をしていくことになる.2. OISの時価評価で述べたようにSpot日の割引率を基準とする以上,計算は各レコード内に留まらないため,Python(pandas)など他のツールを用いる.
他のレコードを参照する計算にはPower Queryではなくpandasを用いる.
3-2. 割引カーブ構築(Bootstrapping)
【執筆中】
3-3. 割引カーブ検証
【執筆中】