File size: 17,323 Bytes
42f8189
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
# -*- 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("------------------------")