bond_simulation / bond_simulation_tutorial.py
jeff7522553
新增CIR
42f8189
# -*- coding: utf-8 -*-
"""
債券利率蒙地卡羅模擬教學腳本 (Bond Simulation Tutorial)
本腳本旨在教學演示如何使用 Python 進行債券利率的蒙地卡羅模擬。
程式碼中的變數名稱、公式與註解都對標 'equation.md' 文件,以利於學習與對照。
包含四種利率模型:
1. Vasicek 模型:用於模擬無風險利率 (drf),具有均值回歸特性。
2. Cox-Ingersoll-Ross (CIR) 模型:Vasicek 的變體,確保利率為正。
3. 幾何布朗運動 (GBM) 模型:一個常用於股價的簡化利率模型。
4. 有風險利率模型:在無風險利率上疊加一個隨機的信用利差。
並根據模擬出的利率路徑,使用「修正存續期間」與「價格曲度」來近似計算債券價格的變化。
"""
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
# --- 1. 利率模擬函數 ---
def simulate_vasicek(r_0, k, theta, sigma, T, dt, n_simulations, dW_t):
"""
使用 Vasicek 模型模擬利率路徑。
根據隨機微分方程 (SDE):
dr_t = k(θ - r_t)dt + σdW_t
參數說明 (對標 equation.md):
:param r_0: float, 初始利率 (r_t 在 t=0 的值)。
:param k: float, 均值回歸速度 (kappa),代表利率回復到長期均值的速度。
:param theta: float, 長期平均利率或均衡水平 (θ)。
:param sigma: float, 波動率 (σ),代表利率隨機波動的幅度。
:param T: float, 總模擬時長(年)。
:param dt: float, 每個時間步長(年)。
:param n_simulations: int, 模擬路徑的數量。
:param dW_t: np.ndarray, 預先生成的維納過程增量 (dW_t),代表隨機衝擊。
其維度應為 (n_simulations, num_steps)。
:return: np.ndarray, 模擬的利率路徑,形狀為 (n_simulations, num_steps + 1)。
"""
num_steps = int(T / dt)
# 初始化利率路徑數組,第一欄設為初始利率 r_0
r_paths = np.zeros((n_simulations, num_steps + 1))
r_paths[:, 0] = r_0
# 迭代計算每一步的利率
for t in range(1, num_steps + 1):
# 根據 Vasicek 公式的離散化形式進行計算
# r(t) = r(t-1) + k * (theta - r(t-1)) * dt + sigma * dW_t
# dW_t 是一個服從 N(0, sqrt(dt)) 的隨機變數,我們在傳入前已處理好
r_paths[:, t] = r_paths[:, t-1] + k * (theta - r_paths[:, t-1]) * dt + sigma * dW_t[:, t-1]
# 確保利率不會變成不切實際的負值
r_paths[:, t] = np.maximum(0.0001, r_paths[:, t])
return r_paths
def simulate_cir(r_0, k, theta, sigma, T, dt, n_simulations, dW_t):
"""
使用 Cox-Ingersoll-Ross (CIR) 模型模擬利率路徑。
根據隨機微分方程 (SDE):
dr_t = k(θ - r_t)dt + σ√r_t dW_t
此模型確保利率恆為正值(在 2kθ > σ² 的條件下)。
參數說明 (對標 equation.md):
:param r_0: float, 初始利率 (r_t 在 t=0 的值)。
:param k: float, 均值回歸速度 (kappa_cir)。
:param theta: float, 長期平均利率 (theta_cir)。
:param sigma: float, 波動率 (sigma_cir)。
:param T: float, 總模擬時長(年)。
:param dt: float, 每個時間步長(年)。
:param n_simulations: int, 模擬路徑的數量。
:param dW_t: np.ndarray, 預先生成的維納過程增量 (dW_t)。
:return: np.ndarray, 模擬的利率路徑。
"""
num_steps = int(T / dt)
r_paths = np.zeros((n_simulations, num_steps + 1))
r_paths[:, 0] = r_0
for t in range(1, num_steps + 1):
# 為了避免對負數開根號,先取 r(t-1) 和 0 之間的最大值
sqrt_r = np.sqrt(np.maximum(0, r_paths[:, t-1]))
# 根據 CIR 公式的離散化形式進行計算
# r(t) = r(t-1) + k * (theta - r(t-1)) * dt + sigma * sqrt(r(t-1)) * dW_t
r_paths[:, t] = r_paths[:, t-1] + k * (theta - r_paths[:, t-1]) * dt + sigma * sqrt_r * dW_t[:, t-1]
# 再次確保利率不會變為負數
r_paths[:, t] = np.maximum(0.0001, r_paths[:, t])
return r_paths
def simulate_gbm(r_0, mu, sigma, T, dt, n_simulations, dW_t):
"""
使用幾何布朗運動 (GBM) 模型模擬利率路徑。
根據隨機微分方程 (SDE):
dr_t = μr_t dt + σr_t dW_t
其離散化形式為 r_t = r_{t-1} * exp((μ - 0.5σ²)Δt + σdW_t)。
參數說明 (對標 equation.md):
:param r_0: float, 初始利率 (r_t 在 t=0 的值)。
:param mu: float, 長期漂移趨勢 (μ, mu_r)。
:param sigma: float, 年化波動率 (σ, sigma_r)。
:param T: float, 總模擬時長(年)。
:param dt: float, 每個時間步長(年)。
:param n_simulations: int, 模擬路徑的數量。
:param dW_t: np.ndarray, 預先生成的維納過程增量 (dW_t)。
:return: np.ndarray, 模擬的利率路徑。
"""
num_steps = int(T / dt)
r_paths = np.zeros((n_simulations, num_steps + 1))
r_paths[:, 0] = r_0
for t in range(1, num_steps + 1):
# 根據 GBM 公式的離散化形式進行計算
drift = (mu - 0.5 * sigma**2) * dt
diffusion = sigma * dW_t[:, t-1]
r_paths[:, t] = r_paths[:, t-1] * np.exp(drift + diffusion)
# 確保利率不會變成不切實際的負值
r_paths[:, t] = np.maximum(0.0001, r_paths[:, t])
return r_paths
def simulate_risky_rate(drf_paths, dW_f, initial_spread, sigma_s, rho, T, dt, n_simulations):
"""
模擬有風險利率 (drs),即在無風險利率(drf)上疊加信用利差(spread)。
公式:
drs_t = drf_t + spread_t
d(spread_t) = σ_s dW_s
dW_s = ρ * dW_f + √(1 - ρ²) * dZ_t (其中 dZ_t 是獨立的維納過程)
參數說明 (對標 equation.md):
:param drf_paths: np.ndarray, 已模擬好的無風險利率路徑 (drf_t)。
:param dW_f: np.ndarray, 生成 drf_paths 所使用的隨機衝擊 (dW_f)。
:param initial_spread: float, 初始信用利差。
:param sigma_s: float, 信用利差的波動率 (σ_s, sigma_spread)。
:param rho: float, drf 與 spread 變動之間的相關係數 (ρ, correlation)。
:param T: float, 總模擬時長(年)。
:param dt: float, 每個時間步長(年)。
:param n_simulations: int, 模擬路徑的數量。
:return: tuple (np.ndarray, np.ndarray), 分別為模擬的有風險利率路徑(drs)和信用利差路徑(spread)。
"""
num_steps = int(T / dt)
# 1. 生成一個獨立的隨機衝擊 dZ_t
dZ_t = np.random.normal(0, 1, (n_simulations, num_steps)) * np.sqrt(dt)
# 2. 根據相關係數 rho,合成信用利差的隨機衝擊 dW_s
dW_s = rho * dW_f + np.sqrt(1 - rho**2) * dZ_t
# 3. 模擬信用利差的路徑
spread_paths = np.zeros((n_simulations, num_steps + 1))
spread_paths[:, 0] = initial_spread
for t in range(1, num_steps + 1):
# 根據 d(spread_t) = σ_s dW_s 進行計算
spread_paths[:, t] = spread_paths[:, t-1] + sigma_s * dW_s[:, t-1]
# 確保利差為正
spread_paths[:, t] = np.maximum(0.0001, spread_paths[:, t])
# 4. 有風險利率 = 無風險利率 + 信用利差
drs_paths = drf_paths + spread_paths
return drs_paths, spread_paths
# --- 2. 債券價格計算函數 ---
def calculate_price_paths(r_paths, D_mod, convT, initial_price=100.0):
"""
使用修正存續期間(D_mod)和價格曲度(convT)來近似計算債券價格路徑。
近似公式:
ΔP/P ≈ -D_mod * Δy + (C_mod / 2) * (Δy)²
其中 y 是殖利率(yield),此處用模擬的利率 r_paths 作為替代。
參數說明:
:param r_paths: np.ndarray, 模擬的利率路徑 (y)。
:param D_mod: float, 債券的修正存續期間 (Modified Duration)。
:param convT: float, 來自資料庫的價格曲度值。
在我們的系統中,convT 的定義是 (P * C_mod / 2),
其中 P 是初始價格,C_mod 是標準的曲度定義。
:param initial_price: float, 債券的初始價格,預設為100。
:return: np.ndarray, 模擬的債券價格路徑。
"""
num_steps = r_paths.shape[1] - 1
price_paths = np.zeros_like(r_paths)
price_paths[:, 0] = initial_price
# 從 convT 反解出公式中需要的 (C_mod / 2) 項。
# 根據定義 convT = initial_price * C_mod / 2
# 因此 C_mod / 2 = convT / initial_price
# 這個因子在整個模擬中被視為常數。
convexity_factor = convT / initial_price
for t in range(1, num_steps + 1):
# 計算兩期之間的利率變化 Δy
delta_y = r_paths[:, t] - r_paths[:, t-1]
# 抓取前一期的價格 P(t-1)
prev_price = price_paths[:, t-1]
# 1. 計算價格變動百分比 (ΔP/P)
# ΔP/P = -D_mod * Δy + (C_mod / 2) * (Δy)²
price_change_percentage = -D_mod * delta_y + convexity_factor * (delta_y ** 2)
# 2. 計算新價格 P(t) = P(t-1) * (1 + ΔP/P)
price_paths[:, t] = prev_price * (1 + price_change_percentage)
return price_paths
# --- 3. 主執行區塊 ---
if __name__ == "__main__":
# --- A. 設定模擬參數 ---
# 債券基本資料 (此處為範例,實際應用中從資料庫讀取)
bond_data = {
'name': 'CGB10Y',
'avgYld': 2.2, # 平均殖利率 (%)
'mdurT': 8.9, # 修正存續期間 (D_mod)
'convT': 45.5, # 價格曲度 (P * C_mod / 2)
'maturity': datetime(2034, 5, 15)
}
# 模擬通用參數
n_simulations = 100 # 模擬次數
dt = 1/252 # 時間步長 (年),假設一年252個交易日
# 計算剩餘到期年限,作為模擬總時長 T
time_to_maturity = (bond_data['maturity'] - datetime.now()).days / 365.25
T = max(time_to_maturity, 0.1) # 確保至少模擬一小段時間
num_steps = int(T / dt)
time_points = np.linspace(0, T, num_steps + 1) # 模擬的時間點數列
# 初始利率 (將百分比轉為小數)
r_0 = bond_data['avgYld'] / 100.0
# --- B. 設定各模型參數 (將百分比轉為小數) ---
# 1. Vasicek & CIR 模型參數
k_vasicek = 0.3
theta_vasicek = 2.2 / 100.0
sigma_vasicek = 0.5 / 100.0
k_cir = 0.2
theta_cir = 2.5 / 100.0
sigma_cir = 0.4 / 100.0
# 2. GBM 模型參數
mu_r = 0.05 / 100.0
sigma_r = 0.6 / 100.0
# 3. 有風險利率模型參數
initial_spread = 0.8 / 100.0
sigma_s = 0.3 / 100.0
rho = 0.6
# --- C. 生成隨機衝擊 ---
# 為每個需要獨立隨機性的模型預先生成維納過程增量 dW_t
# dW_t ~ N(0, dt) 等價於 sqrt(dt) * N(0, 1)
np.random.seed(42) # 設定隨機種子以確保結果可重現
dW_vasicek = np.random.normal(0, 1, (n_simulations, num_steps)) * np.sqrt(dt)
dW_cir = np.random.normal(0, 1, (n_simulations, num_steps)) * np.sqrt(dt)
dW_gbm = np.random.normal(0, 1, (n_simulations, num_steps)) * np.sqrt(dt)
# --- D. 執行模擬 ---
print("開始執行利率模擬...")
# 1. 模擬無風險利率 drf (Vasicek)
drf_paths = simulate_vasicek(r_0, k_vasicek, theta_vasicek, sigma_vasicek, T, dt, n_simulations, dW_vasicek)
# 2. 模擬有風險利率 drs (在 drf 基礎上增加信用利差)
drs_paths, spread_paths = simulate_risky_rate(drf_paths, dW_vasicek, initial_spread, sigma_s, rho, T, dt, n_simulations)
# 3. 模擬綜合利率 dr (GBM)
dr_paths = simulate_gbm(r_0, mu_r, sigma_r, T, dt, n_simulations, dW_gbm)
# 4. 模擬 CIR 利率 dr_cir
dr_cir_paths = simulate_cir(r_0, k_cir, theta_cir, sigma_cir, T, dt, n_simulations, dW_cir)
print("利率模擬完成。")
print("開始計算債券價格路徑...")
# 5. 計算對應的債券價格路徑
price_drf_paths = calculate_price_paths(drf_paths, bond_data['mdurT'], bond_data['convT'])
price_drs_paths = calculate_price_paths(drs_paths, bond_data['mdurT'], bond_data['convT'])
price_dr_paths = calculate_price_paths(dr_paths, bond_data['mdurT'], bond_data['convT'])
price_dr_cir_paths = calculate_price_paths(dr_cir_paths, bond_data['mdurT'], bond_data['convT'])
print("價格計算完成。")
# --- E. 結果可視化 ---
print("正在生成圖表...")
# 設定 Matplotlib 以正確顯示中文和負號
try:
plt.rcParams['font.sans-serif'] = ['Microsoft JhengHei'] # For Windows
plt.rcParams['axes.unicode_minus'] = False
except:
print("未找到 'Microsoft JhengHei' 字體,圖表中的中文可能無法正常顯示。")
print("您可以嘗試安裝 'Microsoft JhengHei' 或替換為系統中已有的中文字體,例如 'SimHei' (黑體) 或 'KaiTi' (楷體)。")
# 繪製利率模擬路徑圖 (只畫前50條以保持清晰)
fig_rates, axes_rates = plt.subplots(2, 2, figsize=(14, 10), sharex=True)
fig_rates.suptitle(f'四種利率模型的模擬路徑 (前 {min(50, n_simulations)} 條)', fontsize=16)
axes_rates[0, 0].plot(time_points, drf_paths[:50, :].T * 100, lw=0.5)
axes_rates[0, 0].set_title('1. 無風險利率 (drf) - Vasicek')
axes_rates[0, 0].set_ylabel('利率 (%)')
axes_rates[0, 0].grid(True, linestyle='--', alpha=0.6)
axes_rates[0, 1].plot(time_points, drs_paths[:50, :].T * 100, lw=0.5)
axes_rates[0, 1].set_title('2. 有風險利率 (drs) - Vasicek + Spread')
axes_rates[0, 1].grid(True, linestyle='--', alpha=0.6)
axes_rates[1, 0].plot(time_points, dr_paths[:50, :].T * 100, lw=0.5)
axes_rates[1, 0].set_title('3. 綜合利率 (dr) - GBM')
axes_rates[1, 0].set_xlabel('時間 (年)')
axes_rates[1, 0].set_ylabel('利率 (%)')
axes_rates[1, 0].grid(True, linestyle='--', alpha=0.6)
axes_rates[1, 1].plot(time_points, dr_cir_paths[:50, :].T * 100, lw=0.5)
axes_rates[1, 1].set_title('4. CIR 利率 (dr_cir)')
axes_rates[1, 1].set_xlabel('時間 (年)')
axes_rates[1, 1].grid(True, linestyle='--', alpha=0.6)
fig_rates.tight_layout(rect=[0, 0, 1, 0.96])
plt.savefig("tutorial_rate_paths.png")
# 繪製價格模擬路徑圖
fig_prices, axes_prices = plt.subplots(2, 2, figsize=(14, 10), sharex=True, sharey=True)
fig_prices.suptitle(f'對應的債券價格模擬路徑 (前 {min(50, n_simulations)} 條)', fontsize=16)
axes_prices[0, 0].plot(time_points, price_drf_paths[:50, :].T, lw=0.5)
axes_prices[0, 0].set_title('基於 drf (Vasicek) 的價格')
axes_prices[0, 0].set_ylabel('債券價格')
axes_prices[0, 0].grid(True, linestyle='--', alpha=0.6)
axes_prices[0, 1].plot(time_points, price_drs_paths[:50, :].T, lw=0.5)
axes_prices[0, 1].set_title('基於 drs (Risky) 的價格')
axes_prices[0, 1].grid(True, linestyle='--', alpha=0.6)
axes_prices[1, 0].plot(time_points, price_dr_paths[:50, :].T, lw=0.5)
axes_prices[1, 0].set_title('基於 dr (GBM) 的價格')
axes_prices[1, 0].set_xlabel('時間 (年)')
axes_prices[1, 0].set_ylabel('債券價格')
axes_prices[1, 0].grid(True, linestyle='--', alpha=0.6)
axes_prices[1, 1].plot(time_points, price_dr_cir_paths[:50, :].T, lw=0.5)
axes_prices[1, 1].set_title('基於 dr_cir (CIR) 的價格')
axes_prices[1, 1].set_xlabel('時間 (年)')
axes_prices[1, 1].grid(True, linestyle='--', alpha=0.6)
fig_prices.tight_layout(rect=[0, 0, 1, 0.96])
plt.savefig("tutorial_price_paths.png")
# 顯示最終價格分佈
plt.figure(figsize=(10, 6))
plt.hist(price_drf_paths[:, -1], bins=50, alpha=0.7, label='基於 drf (Vasicek)', density=True)
plt.hist(price_drs_paths[:, -1], bins=50, alpha=0.7, label='基於 drs (Risky)', density=True)
plt.hist(price_dr_paths[:, -1], bins=50, alpha=0.7, label='基於 dr (GBM)', density=True)
plt.hist(price_dr_cir_paths[:, -1], bins=50, alpha=0.7, label='基於 dr_cir (CIR)', density=True)
plt.title('模擬結束時的債券價格分佈')
plt.xlabel('最終價格')
plt.ylabel('機率密度')
plt.legend()
plt.grid(True, linestyle='--', alpha=0.6)
plt.savefig("tutorial_final_price_dist.png")
print("圖表已儲存為 tutorial_rate_paths.png, tutorial_price_paths.png, 和 tutorial_final_price_dist.png")
# 顯示繪圖
plt.show()
# --- F. 簡單統計分析 ---
final_prices_drf = price_drf_paths[:, -1]
final_prices_drs = price_drs_paths[:, -1]
print("\n--- 最終價格統計分析 ---")
print(f"模型: 基於 drf (Vasicek)")
print(f" 平均最終價格: {np.mean(final_prices_drf):.4f}")
print(f" 價格標準差: {np.std(final_prices_drf):.4f}")
print(f" 5% 分位數價格: {np.percentile(final_prices_drf, 5):.4f}")
print(f" 95% VaR (從初始價100計算的潛在最大損失): {100 - np.percentile(final_prices_drf, 5):.4f}")
print(f"\n模型: 基於 drs (Risky)")
print(f" 平均最終價格: {np.mean(final_prices_drs):.4f}")
print(f" 價格標準差: {np.std(final_prices_drs):.4f}")
print(f" 5% 分位數價格: {np.percentile(final_prices_drs, 5):.4f}")
print(f" 95% VaR (從初始價100計算的潛在最大損失): {100 - np.percentile(final_prices_drs, 5):.4f}")
print("------------------------")