Spaces:
Sleeping
Sleeping
jeff7522553 commited on
Commit ·
2dacaa6
1
Parent(s): 5b339f4
初始化
Browse files- app.py +175 -0
- bond.py +303 -0
- bond_db.csv +0 -0
- requirements.txt +5 -0
app.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
# 從 bond.py 導入重構後的函數和數據
|
| 6 |
+
from bond import create_bond_database, run_simulation
|
| 7 |
+
|
| 8 |
+
# 1. 數據準備
|
| 9 |
+
BOND_DATABASE = create_bond_database()
|
| 10 |
+
BOND_CHOICES = list(BOND_DATABASE.keys())
|
| 11 |
+
|
| 12 |
+
# --- Gradio 介面輔助函數 ---
|
| 13 |
+
|
| 14 |
+
def get_bond_info_html(bond_name):
|
| 15 |
+
"""
|
| 16 |
+
根據下拉選單選擇的債券名稱,生成用於顯示其詳細資訊的 HTML 字串。
|
| 17 |
+
|
| 18 |
+
:param bond_name: str, 使用者從下拉選單選擇的債券名稱。
|
| 19 |
+
:return: str, 一個 HTML 格式的字串,用於在 Gradio 介面中顯示。
|
| 20 |
+
"""
|
| 21 |
+
if not bond_name:
|
| 22 |
+
return "<p>請從上方選擇一個債券</p>"
|
| 23 |
+
|
| 24 |
+
bond = BOND_DATABASE[bond_name]
|
| 25 |
+
issue_date_str = bond['issueDate'].strftime('%Y-%m-%d')
|
| 26 |
+
maturity_date_str = bond['maturity'].strftime('%Y-%m-%d')
|
| 27 |
+
|
| 28 |
+
# **重要**: 根據資料庫中的百分比格式,調整顯示方式
|
| 29 |
+
html = f"""
|
| 30 |
+
<div style="border: 1px solid #e0e0e0; padding: 15px; border-radius: 5px;">
|
| 31 |
+
<h4 style="margin-top:0;">債券: {bond_name}</h4>
|
| 32 |
+
<table style="width:100%;">
|
| 33 |
+
<tr><td style="width:50%;"><strong>平均殖利率 (avgYld):</strong> {bond['avgYld']:.3f}%</td><td style="width:50%;"><strong>票面利率 (coupon):</strong> {bond['coupon']:.3f}%</td></tr>
|
| 34 |
+
<tr><td><strong>發行日 (issueDate):</strong> {issue_date_str}</td><td><strong>到期日 (maturity):</strong> {maturity_date_str}</td></tr>
|
| 35 |
+
<tr><td><strong>每年付息 (payPerYr):</strong> {bond['payPerYr']} 次</td><td><strong>每年複利 (compPerYr):</strong> {bond['compPerYr']} 次</td></tr>
|
| 36 |
+
<tr><td><strong>存續期間 (durT):</strong> {bond['durT']}</td><td><strong>修正存續期間 (mdurT):</strong> {bond['mdurT']}</td></tr>
|
| 37 |
+
<tr><td><strong>價格曲度 (convT):</strong> {bond['convT']}</td><td></td></tr>
|
| 38 |
+
</table>
|
| 39 |
+
</div>
|
| 40 |
+
"""
|
| 41 |
+
return html
|
| 42 |
+
|
| 43 |
+
def run_monte_carlo(
|
| 44 |
+
bond_name, n_simulations, dt,
|
| 45 |
+
kappa_rf, theta_rf, sigma_rf,
|
| 46 |
+
initial_spread, sigma_spread, correlation,
|
| 47 |
+
mu_r, sigma_r
|
| 48 |
+
):
|
| 49 |
+
"""
|
| 50 |
+
Gradio 的主要執行函數。
|
| 51 |
+
它負責收集所有來自介面的輸入參數,將百分比單位轉換為小數,
|
| 52 |
+
調用後端的 `run_simulation` 核心函式,並將計算結果回傳給介面的各個輸出元件。
|
| 53 |
+
|
| 54 |
+
:param bond_name: str, 選擇的債券名稱。
|
| 55 |
+
:param n_simulations: int, 模擬次數。
|
| 56 |
+
:param dt: float, 時間步長。
|
| 57 |
+
:param ...: 所有來自UI的模擬參數 (此處為百分比單位)。
|
| 58 |
+
:return: tuple, 包含所有結果的元組,用於更新 Gradio 介面的各個輸出元件。
|
| 59 |
+
"""
|
| 60 |
+
if not bond_name:
|
| 61 |
+
raise gr.Error("請先選擇一個債券!")
|
| 62 |
+
|
| 63 |
+
bond_data = BOND_DATABASE[bond_name]
|
| 64 |
+
|
| 65 |
+
# **重要**: 將來自UI的百分比參數轉換為小數以進行計算
|
| 66 |
+
params = {
|
| 67 |
+
'kappa_rf': kappa_rf,
|
| 68 |
+
'theta_rf': theta_rf / 100.0,
|
| 69 |
+
'sigma_rf': sigma_rf / 100.0,
|
| 70 |
+
'initial_spread': initial_spread / 100.0,
|
| 71 |
+
'sigma_spread': sigma_spread / 100.0,
|
| 72 |
+
'correlation': correlation,
|
| 73 |
+
'mu_r': mu_r / 100.0,
|
| 74 |
+
'sigma_r': sigma_r / 100.0,
|
| 75 |
+
"n_simulations":int(n_simulations),
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
rate_stats_df, price_stats_df, fig_rate_paths, fig_rate_dist, fig_price_paths, fig_price_dist, report_filepath = run_simulation(
|
| 79 |
+
bond_data=bond_data, n_simulations=int(n_simulations), dt=dt, params=params
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
return (
|
| 83 |
+
rate_stats_df, price_stats_df,
|
| 84 |
+
fig_rate_paths, fig_rate_dist,
|
| 85 |
+
fig_price_paths, fig_price_dist,
|
| 86 |
+
gr.update(value=report_filepath, visible=True)
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
# --- 建立 Gradio 介面 ---
|
| 90 |
+
|
| 91 |
+
with gr.Blocks(theme=gr.themes.Soft(), title="債券利率蒙地卡羅模擬") as app:
|
| 92 |
+
gr.Markdown("# 債券利率與價格蒙地卡羅模擬")
|
| 93 |
+
|
| 94 |
+
# --- 步驟 1 & 2: 參數設定 ---
|
| 95 |
+
with gr.Row():
|
| 96 |
+
with gr.Column(scale=2):
|
| 97 |
+
gr.Markdown("## 步驟 1: 選擇債券")
|
| 98 |
+
bond_selector = gr.Dropdown(BOND_CHOICES, label="選擇債券", info="從模擬的資料庫中選擇一個債券以載入其初始參數。", value=BOND_CHOICES[0])
|
| 99 |
+
bond_info_display = gr.HTML(get_bond_info_html(BOND_CHOICES[0]))
|
| 100 |
+
|
| 101 |
+
with gr.Column(scale=3):
|
| 102 |
+
gr.Markdown("## 步驟 2: 設定模擬參數")
|
| 103 |
+
with gr.Tabs():
|
| 104 |
+
with gr.Tab("無風險利率 (drf)"):
|
| 105 |
+
kappa_rf = gr.Slider(0.01, 1.0, value=0.3, step=0.01, label="均值回歸速度 (kappa_rf)")
|
| 106 |
+
theta_rf = gr.Slider(1.0, 5.0, value=2.2, step=0.1, label="長期平均利率 (%)")
|
| 107 |
+
sigma_rf = gr.Slider(0.1, 2.0, value=0.5, step=0.1, label="波動率 (%)")
|
| 108 |
+
with gr.Tab("有風險利率 (drs)"):
|
| 109 |
+
initial_spread = gr.Slider(0.1, 3.0, value=0.8, step=0.1, label="初始信用利差 (%)")
|
| 110 |
+
sigma_spread = gr.Slider(0.1, 1.0, value=0.3, step=0.05, label="利差波動率 (%)")
|
| 111 |
+
correlation = gr.Slider(-1.0, 1.0, value=0.6, step=0.05, label="drf與利差的相關係數")
|
| 112 |
+
with gr.Tab("綜合利率 (dr)"):
|
| 113 |
+
mu_r = gr.Slider(0.0, 0.2, value=0.05, step=0.01, label="長期漂移趨勢 (%)")
|
| 114 |
+
sigma_r = gr.Slider(0.1, 2.0, value=0.6, step=0.1, label="年化波動率 (%)")
|
| 115 |
+
n_simulations = gr.Slider(50, 100000, value=100, step=50, label="模擬次數 (n_simulations)")
|
| 116 |
+
dt = gr.Number(value=1/252, label="時間步長 (dt)", info="以年為單位,預設為一個交易日(1/252年)。")
|
| 117 |
+
|
| 118 |
+
# --- 步驟 3: 執行 ---
|
| 119 |
+
run_button = gr.Button("開始模擬", variant="primary")
|
| 120 |
+
|
| 121 |
+
# --- 步驟 4: 結果呈現 ---
|
| 122 |
+
with gr.Tabs():
|
| 123 |
+
with gr.Tab("利率分析"):
|
| 124 |
+
gr.Markdown("## 利率模擬結果")
|
| 125 |
+
with gr.Row():
|
| 126 |
+
with gr.Column(scale=1):
|
| 127 |
+
gr.Markdown("#### 利率統計數據")
|
| 128 |
+
rate_results_table = gr.DataFrame(headers=["指標", "數值"], datatype=["str", "str"])
|
| 129 |
+
with gr.Column(scale=2):
|
| 130 |
+
gr.Markdown("#### 利率路徑與相關性圖")
|
| 131 |
+
plot_rate_paths = gr.Plot()
|
| 132 |
+
gr.Markdown("#### 利率變化分佈圖")
|
| 133 |
+
plot_rate_distribution = gr.Plot()
|
| 134 |
+
|
| 135 |
+
with gr.Tab("債券價格分析"):
|
| 136 |
+
gr.Markdown("## 債券價格模擬結果")
|
| 137 |
+
gr.Markdown("使用修正存續期間與曲度來估算價格變化。初始價格假設為 100。")
|
| 138 |
+
with gr.Row():
|
| 139 |
+
with gr.Column(scale=1):
|
| 140 |
+
gr.Markdown("#### 價格統計數據 (含 VaR)")
|
| 141 |
+
price_results_table = gr.DataFrame()
|
| 142 |
+
with gr.Column(scale=2):
|
| 143 |
+
gr.Markdown("#### 價格模擬路徑")
|
| 144 |
+
plot_price_paths = gr.Plot()
|
| 145 |
+
gr.Markdown("#### 最終價格分佈")
|
| 146 |
+
plot_price_distribution = gr.Plot()
|
| 147 |
+
|
| 148 |
+
# --- 步驟 5: 下載資料 ---
|
| 149 |
+
with gr.Column():
|
| 150 |
+
gr.Markdown("--- \n ## 下載完整報告,(若模擬次數超過1000,則不會包含模擬路徑與價格資料,以避免檔案過大)")
|
| 151 |
+
report_file = gr.File(label="下載完整報告 (Excel)", visible=False)
|
| 152 |
+
|
| 153 |
+
# --- 事件監聽 ---
|
| 154 |
+
bond_selector.change(fn=get_bond_info_html, inputs=bond_selector, outputs=bond_info_display)
|
| 155 |
+
|
| 156 |
+
all_inputs = [
|
| 157 |
+
bond_selector, n_simulations, dt,
|
| 158 |
+
kappa_rf, theta_rf, sigma_rf,
|
| 159 |
+
initial_spread, sigma_spread, correlation,
|
| 160 |
+
mu_r, sigma_r
|
| 161 |
+
]
|
| 162 |
+
|
| 163 |
+
run_button.click(
|
| 164 |
+
fn=run_monte_carlo,
|
| 165 |
+
inputs=all_inputs,
|
| 166 |
+
outputs=[
|
| 167 |
+
rate_results_table, price_results_table,
|
| 168 |
+
plot_rate_paths, plot_rate_distribution,
|
| 169 |
+
plot_price_paths, plot_price_distribution,
|
| 170 |
+
report_file
|
| 171 |
+
]
|
| 172 |
+
)
|
| 173 |
+
|
| 174 |
+
if __name__ == "__main__":
|
| 175 |
+
app.launch()
|
bond.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
import tempfile
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def create_bond_database():
|
| 12 |
+
"""
|
| 13 |
+
創建一個包含多個債券樣本的字典,作為模擬資料庫。
|
| 14 |
+
注意:此處的 avgYld 和 coupon 以「百分比」形式儲存 (例如 2.2 代表 2.2%)。
|
| 15 |
+
|
| 16 |
+
:return: dict,鍵為債券名稱,值為包含該債券詳細參數的字典。
|
| 17 |
+
|
| 18 |
+
例如:
|
| 19 |
+
平均殖利率 (avgYld): 1.553%
|
| 20 |
+
票面利率 (coupon): 2.125%
|
| 21 |
+
發行日 (issueDate): 2011-01-13
|
| 22 |
+
到期日 (maturity): 2031-01-13
|
| 23 |
+
每年付息 (payPerYr): 1 次
|
| 24 |
+
每年複利 (compPerYr): 1 次
|
| 25 |
+
存續期間 (durT): 5.3548
|
| 26 |
+
修正存續期間 (mdurT): 5.273
|
| 27 |
+
價格曲度 (convT): 1762.7325
|
| 28 |
+
"""
|
| 29 |
+
bond_table = pd.read_csv(os.path.join(THIS_DIR, "bond_db.csv"))
|
| 30 |
+
bond_table["issueDate"] = bond_table["issueDate"].apply(lambda x: pd.to_datetime(str(x)))
|
| 31 |
+
bond_table["maturity"] = bond_table["maturity"].apply(lambda x: pd.to_datetime(str(x)))
|
| 32 |
+
db = {
|
| 33 |
+
row["name"]: row
|
| 34 |
+
for row in bond_table.to_dict("records")
|
| 35 |
+
}
|
| 36 |
+
return db
|
| 37 |
+
|
| 38 |
+
# --- 模擬函數 ---
|
| 39 |
+
def simulate_vasicek(r0, kappa, theta, sigma, T, dt, n_simulations, dW_shocks):
|
| 40 |
+
"""
|
| 41 |
+
使用 Vasicek 模型模擬無風險利率(drf)的路徑。
|
| 42 |
+
此模型包含均值回歸特性。
|
| 43 |
+
|
| 44 |
+
:param r0: float, 初始利率 (小數形式)。
|
| 45 |
+
:param kappa: float, 均值回歸速度。
|
| 46 |
+
:param theta: float, 長期平均利率。
|
| 47 |
+
:param sigma: float, 波動率。
|
| 48 |
+
:param T: float, 總模擬時長(年)。
|
| 49 |
+
:param dt: float, 每個時間步長(年)。
|
| 50 |
+
:param n_simulations: int, 模擬路徑的數量。
|
| 51 |
+
:param dW_shocks: np.ndarray, 預先生成的隨機衝擊。
|
| 52 |
+
:return: np.ndarray, 模擬的利率路徑,形狀為 (n_simulations, num_steps + 1)。
|
| 53 |
+
"""
|
| 54 |
+
num_steps = int(T / dt)
|
| 55 |
+
rates = np.zeros((n_simulations, num_steps + 1)); rates[:, 0] = r0
|
| 56 |
+
for t in range(1, num_steps + 1):
|
| 57 |
+
rates[:, t] = np.maximum(0.001, rates[:, t-1] + kappa * (theta - rates[:, t-1]) * dt + sigma * dW_shocks[:, t-1])
|
| 58 |
+
return rates
|
| 59 |
+
|
| 60 |
+
def simulate_risky_rate(rf_paths, dW_rf, initial_spread, sigma_spread, correlation, T, dt, n_simulations, dW_independent):
|
| 61 |
+
"""
|
| 62 |
+
在無風險利率的基礎上,模擬有風險利率(drs)的路徑。
|
| 63 |
+
主要模擬與無風險利率相關的信用利差(credit spread)的變化。
|
| 64 |
+
|
| 65 |
+
:param rf_paths: np.ndarray, 已模擬好的無風險利率路徑。
|
| 66 |
+
:param dW_rf: np.ndarray, 生成無風險利率路徑所使用的隨機衝擊。
|
| 67 |
+
:param initial_spread: float, 初始信用利差。
|
| 68 |
+
:param sigma_spread: float, 信用利差的波動率。
|
| 69 |
+
:param correlation: float, 無風險利率與信用利差變動的相關係數。
|
| 70 |
+
:param T: float, 總模擬時長(年)。
|
| 71 |
+
:param dt: float, 每個時間步長(年)。
|
| 72 |
+
:param n_simulations: int, 模擬路徑的數量。
|
| 73 |
+
:param dW_independent: np.ndarray, 獨立的隨機衝擊,用於生成利差的非系統性風險部分。
|
| 74 |
+
:return: tuple (np.ndarray, np.ndarray), 分別為模擬的有風險利率路徑和信用利差路徑。
|
| 75 |
+
"""
|
| 76 |
+
num_steps = int(T / dt); spread_paths = np.zeros((n_simulations, num_steps + 1)); spread_paths[:, 0] = initial_spread
|
| 77 |
+
dW_spread = correlation * dW_rf + np.sqrt(1 - correlation**2) * dW_independent
|
| 78 |
+
for t in range(1, num_steps + 1):
|
| 79 |
+
spread_paths[:, t] = np.maximum(0.001, spread_paths[:, t-1] + sigma_spread * dW_spread[:, t-1])
|
| 80 |
+
return rf_paths + spread_paths, spread_paths
|
| 81 |
+
|
| 82 |
+
def simulate_gbm(r0, mu, sigma, T, dt, n_simulations, dW_shocks):
|
| 83 |
+
"""
|
| 84 |
+
使用幾何布朗運動(Geometric Brownian Motion)模型模擬綜合利率(dr)的路徑。
|
| 85 |
+
此模型常用於模擬股價,這裡用來作為一個簡化的利率模型。
|
| 86 |
+
|
| 87 |
+
:param r0: float, 初始利率 (小數形式)。
|
| 88 |
+
:param mu: float, 長期漂移率。
|
| 89 |
+
:param sigma: float, 波動率。
|
| 90 |
+
:param T: float, 總模擬時長(年)。
|
| 91 |
+
:param dt: float, 每個時間步長(年)。
|
| 92 |
+
:param n_simulations: int, 模擬路徑的數量。
|
| 93 |
+
:param dW_shocks: np.ndarray, 預先生成的隨機衝擊。
|
| 94 |
+
:return: np.ndarray, 模擬的利率路徑。
|
| 95 |
+
"""
|
| 96 |
+
num_steps = int(T / dt); rates = np.zeros((n_simulations, num_steps + 1)); rates[:, 0] = r0
|
| 97 |
+
for t in range(1, num_steps + 1):
|
| 98 |
+
rates[:, t] = np.maximum(0.001, rates[:, t-1] * np.exp((mu - 0.5 * sigma**2) * dt + sigma * dW_shocks[:, t-1]))
|
| 99 |
+
return rates
|
| 100 |
+
|
| 101 |
+
def calculate_price_paths(rate_paths, mdurT, convT, initial_price=100.0):
|
| 102 |
+
"""
|
| 103 |
+
使用修正存續期間(mdurT)和價格曲度(convT)近似法,根據利率路徑計算債券價格路徑。
|
| 104 |
+
|
| 105 |
+
重要:
|
| 106 |
+
此處的 convT 參數假定為您資料庫中的 "1/2價格曲度" (如 288.3137),
|
| 107 |
+
其金融定義為 (P * C_Mod / 2),且 P 為 initial_price (通常為 100)。
|
| 108 |
+
|
| 109 |
+
:param rate_paths: np.ndarray, 模��的利率路徑,刻度為小數點形式。
|
| 110 |
+
:param mdurT: float, 債券的修正存續期間 (D_Mod)。
|
| 111 |
+
:param convT: float, 債券的 "1/2 價格曲度" (P * C_Mod / 2)。
|
| 112 |
+
:param initial_price: float, 債券的初始價格,預設為100。
|
| 113 |
+
:return: np.ndarray, 模擬的債券價格路徑。
|
| 114 |
+
"""
|
| 115 |
+
n_simulations, num_steps = rate_paths.shape[0], rate_paths.shape[1] - 1
|
| 116 |
+
price_paths = np.zeros_like(rate_paths)
|
| 117 |
+
price_paths[:, 0] = initial_price
|
| 118 |
+
|
| 119 |
+
# --- 邏輯修正 ---
|
| 120 |
+
# 根據定義,convT = (initial_price * C_Mod / 2)
|
| 121 |
+
# 我們需要 C_Mod / 2 這一項
|
| 122 |
+
# 因此,C_Mod / 2 = convT / initial_price
|
| 123 |
+
#
|
| 124 |
+
# 這個值在近似法中被假定為常數,不應隨 P(t-1) 變動。
|
| 125 |
+
# 【錯誤的邏輯】: convexity_factor = convT / prev_price
|
| 126 |
+
# 【修正後的邏輯】:
|
| 127 |
+
convexity_factor = convT / initial_price
|
| 128 |
+
# --- 修正結束 ---
|
| 129 |
+
|
| 130 |
+
for t in range(1, num_steps + 1):
|
| 131 |
+
delta_y = rate_paths[:, t] - rate_paths[:, t-1]
|
| 132 |
+
|
| 133 |
+
# 抓取前一期的價格 P(t-1)
|
| 134 |
+
prev_price = price_paths[:, t-1]
|
| 135 |
+
|
| 136 |
+
# 1. 計算價格變動百分比 (dP/P)
|
| 137 |
+
# dP/P = -mdurT * dy + (C_Mod / 2) * (dy^2)
|
| 138 |
+
#
|
| 139 |
+
# 【注意】: 這裡的 convexity_factor 是我們在迴圈外
|
| 140 |
+
# 就已經計算好的常數 (convT / initial_price)
|
| 141 |
+
price_change_factor = -mdurT * delta_y + convexity_factor * (delta_y ** 2)
|
| 142 |
+
|
| 143 |
+
# 2. 計算新價格 P(t) = P(t-1) * (1 + dP/P)
|
| 144 |
+
price_paths[:, t] = prev_price * (1 + price_change_factor)
|
| 145 |
+
|
| 146 |
+
return price_paths
|
| 147 |
+
|
| 148 |
+
# --- 主執行函數 ---
|
| 149 |
+
def run_simulation(bond_data, n_simulations, dt, params):
|
| 150 |
+
"""
|
| 151 |
+
程式核心執行函數。
|
| 152 |
+
整合所有模擬、計算、繪圖與報告生成步驟。
|
| 153 |
+
|
| 154 |
+
:param bond_data: dict, 選定債券的詳細資料。
|
| 155 |
+
:param n_simulations: int, 模擬總次數。
|
| 156 |
+
:param dt: float, 每個時間步長(年)。
|
| 157 |
+
:param params: dict, 所有模擬模型的參數。
|
| 158 |
+
:return: tuple, 包含所有結果的元組。
|
| 159 |
+
"""
|
| 160 |
+
np.random.seed(42)
|
| 161 |
+
|
| 162 |
+
# **重要**: 將來自資料庫的百分比格式轉換為小數格式以進行計算
|
| 163 |
+
initial_yield = bond_data['avgYld'] / 100.0
|
| 164 |
+
|
| 165 |
+
time_to_maturity = (bond_data['maturity'] - datetime.now()).days / 365.25
|
| 166 |
+
T = max(time_to_maturity, 0.1)
|
| 167 |
+
num_steps = int(T / dt)
|
| 168 |
+
time_points = np.linspace(0, T, num_steps + 1)
|
| 169 |
+
|
| 170 |
+
dW_rf = np.random.normal(0, 1, (n_simulations, num_steps)) * np.sqrt(dt)
|
| 171 |
+
dW_spread_independent = np.random.normal(0, 1, (n_simulations, num_steps)) * np.sqrt(dt)
|
| 172 |
+
dW_dr = np.random.normal(0, 1, (n_simulations, num_steps)) * np.sqrt(dt)
|
| 173 |
+
|
| 174 |
+
# --- 利率模擬 ---
|
| 175 |
+
rf_paths = simulate_vasicek(initial_yield, params['kappa_rf'], params['theta_rf'], params['sigma_rf'], T, dt, n_simulations, dW_rf)
|
| 176 |
+
drs_paths, _ = simulate_risky_rate(rf_paths, dW_rf, params['initial_spread'], params['sigma_spread'], params['correlation'], T, dt, n_simulations, dW_spread_independent)
|
| 177 |
+
dr_paths = simulate_gbm(initial_yield, params['mu_r'], params['sigma_r'], T, dt, n_simulations, dW_dr)
|
| 178 |
+
|
| 179 |
+
# --- 價格模擬 ---
|
| 180 |
+
price_rf_paths = calculate_price_paths(rf_paths, bond_data['mdurT'], bond_data['convT'])
|
| 181 |
+
price_drs_paths = calculate_price_paths(drs_paths, bond_data['mdurT'], bond_data['convT'])
|
| 182 |
+
price_dr_paths = calculate_price_paths(dr_paths, bond_data['mdurT'], bond_data['convT'])
|
| 183 |
+
|
| 184 |
+
# --- 統計數據 ---
|
| 185 |
+
drf_changes = np.diff(rf_paths, axis=1).flatten(); drs_changes = np.diff(drs_paths, axis=1).flatten(); dr_changes = np.diff(dr_paths, axis=1).flatten()
|
| 186 |
+
cov_matrix = np.cov(drf_changes, drs_changes); corr_matrix = np.corrcoef(drf_changes, drs_changes)
|
| 187 |
+
rate_stats_df = pd.DataFrame({
|
| 188 |
+
"指標": ["drf 變化量平均", "drf 變化量標準差", "drs 變化量平均", "drs 變化量標準差", "dr 變化量平均", "dr 變化量標準差", "drf-drs 共變異數", "drf-drs 相關係數"],
|
| 189 |
+
"數值": [f"{np.mean(drf_changes):.8f}", f"{np.std(drf_changes):.8f}", f"{np.mean(drs_changes):.8f}", f"{np.std(drs_changes):.8f}", f"{np.mean(dr_changes):.8f}", f"{np.std(dr_changes):.8f}", f"{cov_matrix[0, 1]:.8f}", f"{corr_matrix[0, 1]:.6f}"]
|
| 190 |
+
})
|
| 191 |
+
|
| 192 |
+
final_prices_rf = price_rf_paths[:, -1]; final_prices_drs = price_drs_paths[:, -1]; final_prices_dr = price_dr_paths[:, -1]
|
| 193 |
+
price_stats_df = pd.DataFrame({
|
| 194 |
+
"統計量": ["平均價格", "價格標準差", "最大價格", "最小價格", "95%分位數 (Q95)", "5%分位數 (Q5)", "95% VaR (價值損失)"],
|
| 195 |
+
"基於 drf": [f"{np.mean(final_prices_rf):.4f}", f"{np.std(final_prices_rf):.4f}", f"{np.max(final_prices_rf):.4f}", f"{np.min(final_prices_rf):.4f}", f"{np.percentile(final_prices_rf, 95):.4f}", f"{np.percentile(final_prices_rf, 5):.4f}", f"{100 - np.percentile(final_prices_rf, 5):.4f}"],
|
| 196 |
+
"基於 drs": [f"{np.mean(final_prices_drs):.4f}", f"{np.std(final_prices_drs):.4f}", f"{np.max(final_prices_drs):.4f}", f"{np.min(final_prices_drs):.4f}", f"{np.percentile(final_prices_drs, 95):.4f}", f"{np.percentile(final_prices_drs, 5):.4f}", f"{100 - np.percentile(final_prices_drs, 5):.4f}"],
|
| 197 |
+
"基於 dr": [f"{np.mean(final_prices_dr):.4f}", f"{np.std(final_prices_dr):.4f}", f"{np.max(final_prices_dr):.4f}", f"{np.min(final_prices_dr):.4f}", f"{np.percentile(final_prices_dr, 95):.4f}", f"{np.percentile(final_prices_dr, 5):.4f}", f"{100 - np.percentile(final_prices_dr, 5):.4f}"]
|
| 198 |
+
})
|
| 199 |
+
|
| 200 |
+
# --- Excel 報告 ---
|
| 201 |
+
fd, report_filepath = tempfile.mkstemp(suffix=".xlsx")
|
| 202 |
+
os.close(fd)
|
| 203 |
+
with pd.ExcelWriter(report_filepath) as writer:
|
| 204 |
+
pd.DataFrame(list(bond_data.items()), columns=['項目', '數值']).applymap(lambda x: x.strftime('%Y-%m-%d') if isinstance(x, datetime) else x).to_excel(writer, sheet_name='bond_info', index=False)
|
| 205 |
+
pd.DataFrame(list(params.items()), columns=['參數', '設定值']).to_excel(writer, sheet_name='simulation_parameters', index=False)
|
| 206 |
+
rate_stats_df.to_excel(writer, sheet_name='rate_statistics', index=False)
|
| 207 |
+
price_stats_df.to_excel(writer, sheet_name='price_statistics', index=False)
|
| 208 |
+
if n_simulations <= 1000: # 避免檔案過大
|
| 209 |
+
pd.DataFrame(rf_paths.T, index=time_points).to_excel(writer, sheet_name='drf_rate_paths')
|
| 210 |
+
pd.DataFrame(drs_paths.T, index=time_points).to_excel(writer, sheet_name='drs_rate_paths')
|
| 211 |
+
pd.DataFrame(dr_paths.T, index=time_points).to_excel(writer, sheet_name='dr_rate_paths')
|
| 212 |
+
pd.DataFrame(price_rf_paths.T, index=time_points).to_excel(writer, sheet_name='drf_price_paths')
|
| 213 |
+
pd.DataFrame(price_drs_paths.T, index=time_points).to_excel(writer, sheet_name='drs_price_paths')
|
| 214 |
+
pd.DataFrame(price_dr_paths.T, index=time_points).to_excel(writer, sheet_name='dr_price_paths')
|
| 215 |
+
|
| 216 |
+
# --- 繪圖 ---
|
| 217 |
+
plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei']; plt.rcParams['axes.unicode_minus'] = False
|
| 218 |
+
fig_rate_paths = plt.figure(figsize=(12, 10))
|
| 219 |
+
ax1 = fig_rate_paths.add_subplot(2, 2, 1)
|
| 220 |
+
ax1.plot(time_points, rf_paths[:10].T, alpha=0.7)
|
| 221 |
+
ax1.set_title('drf - Risk-free interest rate paths')
|
| 222 |
+
ax1.set_ylabel('Interest rate')
|
| 223 |
+
ax1.grid(True)
|
| 224 |
+
|
| 225 |
+
ax2 = fig_rate_paths.add_subplot(2, 2, 2)
|
| 226 |
+
ax2.plot(time_points, drs_paths[:10].T, alpha=0.7)
|
| 227 |
+
ax2.set_title('drs - Risky interest rate paths')
|
| 228 |
+
ax2.grid(True)
|
| 229 |
+
|
| 230 |
+
ax3 = fig_rate_paths.add_subplot(2, 2, 3)
|
| 231 |
+
ax3.plot(time_points, dr_paths[:10].T, alpha=0.7)
|
| 232 |
+
ax3.set_title('dr - Combined interest rate paths')
|
| 233 |
+
ax3.set_xlabel('Time (years)')
|
| 234 |
+
ax3.set_ylabel('Interest rate')
|
| 235 |
+
ax3.grid(True)
|
| 236 |
+
|
| 237 |
+
ax4 = fig_rate_paths.add_subplot(2, 2, 4)
|
| 238 |
+
ax4.scatter(drf_changes[::10], drs_changes[::10], alpha=0.3, s=5)
|
| 239 |
+
ax4.set_title(f'drf vs drs changes (correlation: {corr_matrix[0, 1]:.4f})')
|
| 240 |
+
ax4.set_xlabel('drf changes')
|
| 241 |
+
ax4.set_ylabel('drs changes')
|
| 242 |
+
ax4.grid(True)
|
| 243 |
+
|
| 244 |
+
fig_rate_paths.tight_layout()
|
| 245 |
+
|
| 246 |
+
fig_rate_dist = plt.figure(figsize=(12, 4))
|
| 247 |
+
ax_dist1 = fig_rate_dist.add_subplot(1, 3, 1)
|
| 248 |
+
ax_dist1.hist(drf_changes, bins=50, density=True, alpha=0.7, color='blue')
|
| 249 |
+
ax_dist1.set_title('Distribution of drf changes')
|
| 250 |
+
ax_dist1.set_xlabel('Change')
|
| 251 |
+
|
| 252 |
+
ax_dist2 = fig_rate_dist.add_subplot(1, 3, 2)
|
| 253 |
+
ax_dist2.hist(drs_changes, bins=50, density=True, alpha=0.7, color='red')
|
| 254 |
+
ax_dist2.set_title('Distribution of drs changes')
|
| 255 |
+
ax_dist2.set_xlabel('Change')
|
| 256 |
+
|
| 257 |
+
ax_dist3 = fig_rate_dist.add_subplot(1, 3, 3)
|
| 258 |
+
ax_dist3.hist(dr_changes, bins=50, density=True, alpha=0.7, color='green')
|
| 259 |
+
ax_dist3.set_title('Distribution of dr changes')
|
| 260 |
+
ax_dist3.set_xlabel('Change')
|
| 261 |
+
|
| 262 |
+
fig_rate_dist.tight_layout()
|
| 263 |
+
|
| 264 |
+
fig_price_paths = plt.figure(figsize=(12, 5))
|
| 265 |
+
ax_p1 = fig_price_paths.add_subplot(1, 3, 1)
|
| 266 |
+
ax_p1.plot(time_points, price_rf_paths[:10].T, alpha=0.7)
|
| 267 |
+
ax_p1.set_title('Price paths (based on drf)')
|
| 268 |
+
ax_p1.set_ylabel('Price')
|
| 269 |
+
ax_p1.grid(True)
|
| 270 |
+
|
| 271 |
+
ax_p2 = fig_price_paths.add_subplot(1, 3, 2)
|
| 272 |
+
ax_p2.plot(time_points, price_drs_paths[:10].T, alpha=0.7)
|
| 273 |
+
ax_p2.set_title('Price paths (based on drs)')
|
| 274 |
+
ax_p2.set_xlabel('Time (years)')
|
| 275 |
+
ax_p2.grid(True)
|
| 276 |
+
|
| 277 |
+
ax_p3 = fig_price_paths.add_subplot(1, 3, 3)
|
| 278 |
+
ax_p3.plot(time_points, price_dr_paths[:10].T, alpha=0.7)
|
| 279 |
+
ax_p3.set_title('Price paths (based on dr)')
|
| 280 |
+
ax_p3.grid(True)
|
| 281 |
+
|
| 282 |
+
fig_price_paths.tight_layout()
|
| 283 |
+
|
| 284 |
+
fig_price_dist = plt.figure(figsize=(12, 4))
|
| 285 |
+
ax_pd1 = fig_price_dist.add_subplot(1, 3, 1)
|
| 286 |
+
ax_pd1.hist(final_prices_rf, bins=50, alpha=0.7, color='blue')
|
| 287 |
+
ax_pd1.set_title('Distribution of final prices (drf)')
|
| 288 |
+
ax_pd1.set_xlabel('Price')
|
| 289 |
+
|
| 290 |
+
ax_pd2 = fig_price_dist.add_subplot(1, 3, 2)
|
| 291 |
+
ax_pd2.hist(final_prices_drs, bins=50, alpha=0.7, color='red')
|
| 292 |
+
ax_pd2.set_title('Distribution of final prices (drs)')
|
| 293 |
+
ax_pd2.set_xlabel('Price')
|
| 294 |
+
|
| 295 |
+
ax_pd3 = fig_price_dist.add_subplot(1, 3, 3)
|
| 296 |
+
ax_pd3.hist(final_prices_dr, bins=50, alpha=0.7, color='green')
|
| 297 |
+
ax_pd3.set_title('Distribution of final prices (dr)')
|
| 298 |
+
ax_pd3.set_xlabel('Price')
|
| 299 |
+
|
| 300 |
+
fig_price_dist.tight_layout()
|
| 301 |
+
|
| 302 |
+
|
| 303 |
+
return rate_stats_df, price_stats_df, fig_rate_paths, fig_rate_dist, fig_price_paths, fig_price_dist, report_filepath
|
bond_db.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio
|
| 2 |
+
pandas
|
| 3 |
+
numpy
|
| 4 |
+
matplotlib
|
| 5 |
+
openpyxl
|