1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

金融機関におけるEUCのベストプラクティス ~Overnight Index Swapとともに~

Last updated at Posted at 2025-01-30

【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ファイルを考える.

  1. 入力(Input)

    1. 入力シートを使用
    2. もしくは収集用Excelファイルで直接パラメータクエリとして登録
  2. 収集(Aggregation)

    1. 収集用ExcelファイルのPower Queryでデータを取得
    2. レコード内であればM言語で加工,不可能ならば計算ステップで対応
    3. 行のフィルターは最初,列の削除は最後
    4. 加工したデータをテーブルとして読み込み
  3. 計算(Calculation)

    1. 加工済みテーブルをxlwingsで取得,Pythonで計算
    2. 重い計算はPyhton -> pybind11 -> C++で対応
    3. 計算シートに(複数の)F_"ファクト"テーブルとして書き出し
    4. (この段階では計算結果の集約を行わない)
  4. 出力(Output)

    1. D_"ディメンション"C_"カレンダー"などのテーブルも合わせてデータモデルに格納
    2. Power Pivotを用いて出力シートで計算結果を集約・可視化
    3. (定時業務では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日に設定している.

入力シートから評価日を設定してもよい.

Eval_on
#date(2025, 1, 24) meta [IsParameterQuery=true, Type="Date", IsParameterQueryRequired=true]

また利払スケジュールを計算するために用いるSpot日もパラメータクエリとして登録する.クエリの中で簡単な関数GetSpotDateを定義している.その中のDayOfWeekは,ここでは日曜日からの日数を返す関数である(日 -> 0, 月 -> 1, ... , 土 -> 6).評価日は営業日となる前提の上で簡易的に場合分けをしている.

注意としてここでは祝日を考慮していない.考慮するためにはカレンダーテーブルが必要となってくる.

Spot_on
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サイトにデータがない場合はローカルを参照するため,ファイルを蓄積しているパスもパラメータクエリとして登録しておく.パスは適宜読み替えていただきたい.

Folder_Path
"C:\Users\htki\workDirectory\"
meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true]

パラメータクエリの登録は以上である.これらを使ってSwap金利のデータを取得する.評価日からURLとファイルパスを生成し前者からソースを取得,もし叶わなければ後者から取得する.

実際に用いたクエリ(クリックで開きます)
GetYield
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_yyyyMMddXX_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ステップでは選択した列を削除する「列の削除」ではなく,選択した列を残す「他の列を削除」を使う.どちらも選択した列名が詳細エディタに残るのだが,削除した列名より残した列名に意味がある.

列は最後に「他の列を削除」で削除する.このとき保持列を選択した順番で並び替えが行われる.

実際に用いたクエリ(クリックで開きます)
T_OIS
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
実際に用いたクエリ(クリックで開きます)
T_Payment
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. 割引カーブ検証

【執筆中】

-1. おわりに

  1. https://www.itmedia.co.jp/im/articles/0308/26/news001.html

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?