Spaces:
Sleeping
Sleeping
| # -*- 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("------------------------") |