Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -6,13 +6,13 @@ from scipy.stats import norm, lognorm
|
|
6 |
import seaborn as sns
|
7 |
|
8 |
# Set matplotlib style for professional plots
|
9 |
-
plt.style.use('default')
|
10 |
sns.set_palette("husl")
|
11 |
|
12 |
class TVOGAnalysis:
|
13 |
def __init__(self):
|
14 |
self.reset_parameters()
|
15 |
-
|
16 |
def reset_parameters(self):
|
17 |
"""Reset to default parameters"""
|
18 |
self.scenarios = 10000
|
@@ -21,61 +21,55 @@ class TVOGAnalysis:
|
|
21 |
self.maturity = 10
|
22 |
self.sum_assured = 500000
|
23 |
self.policy_count = 100
|
24 |
-
|
25 |
def generate_random_numbers(self, scenarios, time_steps):
|
26 |
"""Generate standard normal random numbers"""
|
27 |
np.random.seed(42) # For reproducibility
|
28 |
return np.random.standard_normal((scenarios, time_steps))
|
29 |
-
|
30 |
def simulate_account_values(self, initial_av, scenarios, time_steps):
|
31 |
"""Simulate account value paths using geometric Brownian motion"""
|
32 |
dt = 1/12 # Monthly time steps
|
33 |
rand_nums = self.generate_random_numbers(scenarios, time_steps)
|
34 |
-
|
35 |
# Initialize account value matrix
|
36 |
av_paths = np.zeros((scenarios, time_steps + 1))
|
37 |
av_paths[:, 0] = initial_av
|
38 |
-
|
39 |
# Simulate paths
|
40 |
for t in range(time_steps):
|
41 |
drift = (self.risk_free_rate - 0.5 * self.volatility**2) * dt
|
42 |
diffusion = self.volatility * np.sqrt(dt) * rand_nums[:, t]
|
43 |
av_paths[:, t+1] = av_paths[:, t] * np.exp(drift + diffusion)
|
44 |
-
|
45 |
return av_paths
|
46 |
-
|
47 |
def calculate_gmab_payouts(self, av_paths):
|
48 |
"""Calculate GMAB payouts at maturity"""
|
49 |
final_av = av_paths[:, -1]
|
50 |
guarantee = self.sum_assured * self.policy_count
|
51 |
payouts = np.maximum(guarantee - final_av, 0)
|
52 |
-
|
53 |
# Present value of payouts
|
54 |
discount_factor = np.exp(-self.risk_free_rate * self.maturity)
|
55 |
pv_payouts = payouts * discount_factor
|
56 |
-
|
57 |
return pv_payouts, payouts
|
58 |
-
|
59 |
def black_scholes_put(self, S0, K, T, r, sigma):
|
60 |
"""Black-Scholes-Merton formula for European put option"""
|
61 |
-
if sigma <= 0 or T <= 0: # Avoid division by zero or log of non-positive
|
62 |
-
if K > S0 : # Intrinsic value if option is in the money at expiry (simplified)
|
63 |
-
return K * np.exp(-r * T) - S0 # Simplified, could be just K - S0 for payout
|
64 |
-
else:
|
65 |
-
return 0.0
|
66 |
-
|
67 |
d1 = (np.log(S0/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
|
68 |
d2 = d1 - sigma*np.sqrt(T)
|
69 |
-
|
70 |
put_price = K*np.exp(-r*T)*norm.cdf(-d2) - S0*norm.cdf(-d1)
|
71 |
return put_price
|
72 |
|
73 |
def create_dashboard():
|
74 |
tvog = TVOGAnalysis()
|
75 |
-
|
76 |
-
def update_analysis(scenarios, risk_free_rate, volatility, maturity,
|
77 |
-
|
78 |
-
|
79 |
# Update parameters
|
80 |
tvog.scenarios = int(scenarios)
|
81 |
tvog.risk_free_rate = risk_free_rate
|
@@ -83,327 +77,297 @@ def create_dashboard():
|
|
83 |
tvog.maturity = maturity
|
84 |
tvog.sum_assured = sum_assured
|
85 |
tvog.policy_count = policy_count
|
86 |
-
|
87 |
# Create model points with varying initial account values
|
88 |
premiums = np.linspace(min_premium, max_premium, int(num_points))
|
89 |
-
initial_avs = premiums * policy_count
|
90 |
-
|
91 |
monte_carlo_results = []
|
92 |
black_scholes_results = []
|
93 |
-
|
94 |
time_steps = int(maturity * 12) # Monthly steps
|
95 |
-
|
96 |
-
for
|
97 |
# Monte Carlo simulation
|
98 |
-
av_paths = tvog.simulate_account_values(
|
99 |
pv_payouts, _ = tvog.calculate_gmab_payouts(av_paths)
|
100 |
mc_tvog = np.mean(pv_payouts)
|
101 |
monte_carlo_results.append(mc_tvog)
|
102 |
-
|
103 |
# Black-Scholes-Merton
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
total_guarantee = sum_assured * policy_count
|
108 |
-
|
109 |
-
bs_tvog = tvog.black_scholes_put(total_initial_av, total_guarantee, maturity,
|
110 |
-
risk_free_rate, volatility)
|
111 |
black_scholes_results.append(bs_tvog)
|
112 |
-
|
113 |
# Create results DataFrame
|
114 |
-
# Ensure no division by zero for ratio
|
115 |
-
bs_array = np.array(black_scholes_results)
|
116 |
-
mc_array = np.array(monte_carlo_results)
|
117 |
-
ratio_mc_bs = np.divide(mc_array, bs_array, out=np.zeros_like(mc_array, dtype=float), where=bs_array!=0)
|
118 |
-
|
119 |
-
|
120 |
results_df = pd.DataFrame({
|
121 |
'Premium_per_Policy': premiums,
|
122 |
-
'
|
123 |
-
'
|
124 |
-
'
|
125 |
-
'Ratio_MC_BS':
|
126 |
-
'
|
127 |
})
|
128 |
-
|
129 |
-
# Select a representative initial AV for detailed plots (e.g., middle one)
|
130 |
-
representative_idx = len(initial_avs) // 2
|
131 |
-
representative_initial_av = initial_avs[representative_idx]
|
132 |
-
|
133 |
-
|
134 |
# Create plots
|
135 |
fig1 = create_tvog_comparison_plot(results_df)
|
136 |
-
fig2 = create_sample_paths_plot(tvog,
|
137 |
-
fig3 = create_distribution_plots(tvog,
|
138 |
-
fig4 = create_convergence_plot(tvog,
|
139 |
-
|
140 |
return results_df, fig1, fig2, fig3, fig4
|
141 |
-
|
142 |
def create_tvog_comparison_plot(results_df):
|
143 |
"""Create TVOG comparison plot"""
|
144 |
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
|
145 |
-
|
146 |
# TVOG Comparison
|
147 |
-
ax1.scatter(results_df['
|
148 |
-
|
149 |
-
ax1.scatter(results_df['
|
150 |
-
|
151 |
-
ax1.set_xlabel('Initial Account Value
|
152 |
-
ax1.set_ylabel('TVOG Value
|
153 |
ax1.set_title('TVOG: Monte Carlo vs Black-Scholes-Merton')
|
154 |
ax1.legend()
|
155 |
ax1.grid(True, alpha=0.3)
|
156 |
-
|
157 |
# Ratio Plot
|
158 |
-
ax2.plot(results_df['
|
159 |
-
|
160 |
ax2.axhline(y=1, color='red', linestyle='--', alpha=0.7)
|
161 |
-
ax2.set_xlabel('Initial Account Value
|
162 |
ax2.set_ylabel('Monte Carlo / Black-Scholes Ratio')
|
163 |
ax2.set_title('Convergence Ratio (MC/BS)')
|
164 |
ax2.grid(True, alpha=0.3)
|
165 |
-
|
166 |
plt.tight_layout()
|
167 |
return fig
|
168 |
-
|
169 |
-
def create_sample_paths_plot(tvog,
|
170 |
-
"""Create sample simulation paths plot
|
171 |
sample_scenarios = min(100, tvog.scenarios)
|
172 |
-
|
173 |
-
|
174 |
-
|
175 |
fig, ax = plt.subplots(1, 1, figsize=(12, 6))
|
|
|
176 |
time_axis = np.arange(time_steps + 1) / 12 # Convert to years
|
177 |
-
|
178 |
for i in range(sample_scenarios):
|
179 |
ax.plot(time_axis, av_paths[i, :], alpha=0.3, linewidth=0.8)
|
180 |
-
|
|
|
181 |
mean_path = np.mean(av_paths, axis=0)
|
182 |
ax.plot(time_axis, mean_path, color='red', linewidth=3, label='Mean Path')
|
183 |
-
|
184 |
-
|
185 |
-
|
186 |
-
|
187 |
-
|
|
|
188 |
ax.set_xlabel('Time (Years)')
|
189 |
-
ax.set_ylabel('
|
190 |
-
ax.set_title(f'Sample
|
191 |
ax.legend()
|
192 |
ax.grid(True, alpha=0.3)
|
193 |
-
|
194 |
return fig
|
195 |
-
|
196 |
-
def create_distribution_plots(tvog,
|
197 |
-
"""Create distribution analysis plots
|
198 |
-
|
199 |
-
|
200 |
-
|
201 |
-
|
202 |
-
|
203 |
-
|
204 |
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
|
205 |
-
|
206 |
-
# Histogram of final
|
207 |
-
ax1.hist(
|
208 |
-
|
209 |
-
|
|
|
210 |
sigma = tvog.volatility
|
211 |
T = tvog.maturity
|
212 |
-
|
213 |
-
|
214 |
-
|
215 |
-
# Mean and variance for log(ST)
|
216 |
-
log_mean = np.log(S0_total) + (r - 0.5 * sigma**2) * T
|
217 |
-
log_std = sigma * np.sqrt(T)
|
218 |
-
|
219 |
-
x_range = np.linspace(pv_final_av_total.min(), pv_final_av_total.max(), 1000)
|
220 |
-
theoretical_pdf = lognorm.pdf(x_range, s=log_std, scale=np.exp(log_mean)) # s is shape, loc is 0 by default
|
221 |
ax1.plot(x_range, theoretical_pdf, 'r-', linewidth=2, label='Theoretical Lognormal')
|
222 |
-
|
223 |
-
ax1.axvline(x=
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
ax1.set_xlabel('Present Value of Final Total Account Value')
|
228 |
ax1.set_ylabel('Density')
|
229 |
-
ax1.set_title('Distribution of Final
|
230 |
ax1.legend()
|
231 |
ax1.grid(True, alpha=0.3)
|
232 |
-
|
233 |
-
# GMAB Payouts
|
234 |
-
|
235 |
-
non_zero_payouts =
|
236 |
-
|
237 |
-
ax2.hist(
|
238 |
-
|
239 |
-
ax2.hist(non_zero_payouts, bins=50, alpha=0.8, color='red', label="Non-Zero Payouts")
|
240 |
-
|
241 |
-
ax2.set_xlabel('Total GMAB Payout (Present Value)')
|
242 |
ax2.set_ylabel('Frequency')
|
243 |
-
ax2.set_title(f'
|
244 |
ax2.grid(True, alpha=0.3)
|
245 |
-
|
246 |
-
|
247 |
-
stats_text = f'Mean
|
248 |
-
stats_text += f'Max
|
249 |
-
stats_text += f'Payout Probability: {len(non_zero_payouts)/len(
|
250 |
-
ax2.text(0.95, 0.95, stats_text, transform=ax2.transAxes,
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
plt.tight_layout()
|
255 |
return fig
|
256 |
-
|
257 |
-
def create_convergence_plot(tvog,
|
258 |
-
"""Create convergence analysis plot
|
259 |
-
|
260 |
-
|
261 |
-
if
|
262 |
-
scenario_counts.append(
|
263 |
scenario_counts.sort()
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
|
269 |
-
|
270 |
-
|
271 |
-
|
|
|
272 |
for n_scenarios in scenario_counts:
|
273 |
-
|
274 |
-
# Temporarily set tvog.scenarios for this iteration for payout calc if it uses it
|
275 |
-
# However, simulate_account_values takes n_scenarios directly
|
276 |
-
av_paths = tvog.simulate_account_values(initial_av_total, n_scenarios, time_steps)
|
277 |
-
# calculate_gmab_payouts uses tvog.sum_assured and tvog.policy_count, which are fine
|
278 |
pv_payouts, _ = tvog.calculate_gmab_payouts(av_paths)
|
279 |
mc_tvog = np.mean(pv_payouts)
|
280 |
-
|
281 |
-
|
282 |
-
np.random.set_state(original_seed_state) # Restore global random state
|
283 |
-
|
284 |
fig, ax = plt.subplots(1, 1, figsize=(10, 6))
|
285 |
-
|
286 |
-
|
287 |
-
|
288 |
-
|
289 |
-
|
|
|
290 |
ax.set_xlabel('Number of Scenarios')
|
291 |
-
ax.set_ylabel('TVOG Value
|
292 |
-
ax.set_title('Monte Carlo Convergence Analysis
|
293 |
ax.legend()
|
294 |
ax.grid(True, alpha=0.3)
|
295 |
ax.set_xscale('log')
|
296 |
-
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
302 |
-
|
303 |
-
ax.text(0.02, 0.98, f'BS Result near zero',
|
304 |
-
transform=ax.transAxes, verticalalignment='top',
|
305 |
-
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
|
306 |
-
|
307 |
-
plt.tight_layout()
|
308 |
return fig
|
309 |
-
|
310 |
# Create Gradio interface
|
311 |
-
with gr.Blocks(title="TVOG Analysis Dashboard") as app:
|
312 |
gr.Markdown("""
|
313 |
# Time Value of Options and Guarantees (TVOG) Analysis Dashboard
|
314 |
-
|
315 |
-
This dashboard compares Monte Carlo simulation results with Black-Scholes-Merton analytical solutions
|
316 |
for Variable Annuity products with Guaranteed Minimum Accumulation Benefits (GMAB).
|
317 |
-
|
318 |
**Target Users:** Actuaries, Finance Professionals, Economists, and Academics
|
319 |
""")
|
320 |
-
|
321 |
with gr.Row():
|
322 |
with gr.Column(scale=1):
|
323 |
gr.Markdown("### Model Parameters")
|
324 |
-
|
|
|
325 |
minimum=1000, maximum=50000, step=1000, value=10000,
|
326 |
label="Number of Monte Carlo Scenarios"
|
327 |
)
|
328 |
-
|
|
|
329 |
minimum=0.001, maximum=0.1, step=0.001, value=0.02,
|
330 |
label="Risk-Free Rate (continuous)"
|
331 |
)
|
332 |
-
|
|
|
333 |
minimum=0.01, maximum=0.5, step=0.01, value=0.03,
|
334 |
label="Volatility (σ)"
|
335 |
)
|
336 |
-
|
|
|
337 |
minimum=1, maximum=30, step=1, value=10,
|
338 |
label="Maturity (years)"
|
339 |
)
|
|
|
340 |
with gr.Row():
|
341 |
-
|
342 |
value=500000, label="Sum Assured per Policy"
|
343 |
)
|
344 |
-
|
345 |
-
value=100, label="Number of Policies"
|
346 |
)
|
347 |
-
|
348 |
-
gr.Markdown("### Model Point Range
|
|
|
349 |
with gr.Row():
|
350 |
-
|
351 |
-
value=
|
352 |
)
|
353 |
-
|
354 |
-
value=
|
355 |
)
|
356 |
-
|
|
|
357 |
minimum=3, maximum=20, step=1, value=9,
|
358 |
label="Number of Model Points"
|
359 |
)
|
|
|
360 |
calculate_btn = gr.Button("Run Analysis", variant="primary")
|
361 |
-
|
362 |
with gr.Column(scale=2):
|
363 |
gr.Markdown("### Results Summary")
|
364 |
-
|
365 |
-
headers=["Premium per Policy", "Initial Account Value
|
366 |
-
|
367 |
-
|
368 |
-
label="TVOG Comparison Results",
|
369 |
-
wrap=True
|
370 |
)
|
371 |
-
|
372 |
-
with gr.
|
373 |
-
|
374 |
-
|
375 |
-
|
376 |
-
|
377 |
-
|
378 |
-
|
379 |
-
|
380 |
-
|
381 |
-
|
|
|
|
|
382 |
# Event handlers
|
383 |
-
inputs_list = [scenarios_input, risk_free_rate_input, volatility_input, maturity_input,
|
384 |
-
sum_assured_input, policy_count_input, min_premium_input, max_premium_input,
|
385 |
-
num_points_input]
|
386 |
-
outputs_list = [results_table_display, tvog_plot_display, paths_plot_display,
|
387 |
-
dist_plot_display, conv_plot_display]
|
388 |
-
|
389 |
calculate_btn.click(
|
390 |
fn=update_analysis,
|
391 |
-
inputs=
|
392 |
-
|
|
|
393 |
)
|
394 |
-
|
395 |
-
# Initial calculation
|
396 |
app.load(
|
397 |
fn=update_analysis,
|
398 |
-
inputs=
|
399 |
-
|
|
|
400 |
)
|
401 |
-
|
|
|
402 |
|
403 |
if __name__ == "__main__":
|
404 |
-
# Ensure that matplotlib does not try to use a GUI backend in environments where it's not available
|
405 |
-
import matplotlib
|
406 |
-
matplotlib.use('Agg') # Use a non-interactive backend like Agg
|
407 |
-
|
408 |
app = create_dashboard()
|
409 |
app.launch()
|
|
|
6 |
import seaborn as sns
|
7 |
|
8 |
# Set matplotlib style for professional plots
|
9 |
+
plt.style.use('default')
|
10 |
sns.set_palette("husl")
|
11 |
|
12 |
class TVOGAnalysis:
|
13 |
def __init__(self):
|
14 |
self.reset_parameters()
|
15 |
+
|
16 |
def reset_parameters(self):
|
17 |
"""Reset to default parameters"""
|
18 |
self.scenarios = 10000
|
|
|
21 |
self.maturity = 10
|
22 |
self.sum_assured = 500000
|
23 |
self.policy_count = 100
|
24 |
+
|
25 |
def generate_random_numbers(self, scenarios, time_steps):
|
26 |
"""Generate standard normal random numbers"""
|
27 |
np.random.seed(42) # For reproducibility
|
28 |
return np.random.standard_normal((scenarios, time_steps))
|
29 |
+
|
30 |
def simulate_account_values(self, initial_av, scenarios, time_steps):
|
31 |
"""Simulate account value paths using geometric Brownian motion"""
|
32 |
dt = 1/12 # Monthly time steps
|
33 |
rand_nums = self.generate_random_numbers(scenarios, time_steps)
|
34 |
+
|
35 |
# Initialize account value matrix
|
36 |
av_paths = np.zeros((scenarios, time_steps + 1))
|
37 |
av_paths[:, 0] = initial_av
|
38 |
+
|
39 |
# Simulate paths
|
40 |
for t in range(time_steps):
|
41 |
drift = (self.risk_free_rate - 0.5 * self.volatility**2) * dt
|
42 |
diffusion = self.volatility * np.sqrt(dt) * rand_nums[:, t]
|
43 |
av_paths[:, t+1] = av_paths[:, t] * np.exp(drift + diffusion)
|
44 |
+
|
45 |
return av_paths
|
46 |
+
|
47 |
def calculate_gmab_payouts(self, av_paths):
|
48 |
"""Calculate GMAB payouts at maturity"""
|
49 |
final_av = av_paths[:, -1]
|
50 |
guarantee = self.sum_assured * self.policy_count
|
51 |
payouts = np.maximum(guarantee - final_av, 0)
|
52 |
+
|
53 |
# Present value of payouts
|
54 |
discount_factor = np.exp(-self.risk_free_rate * self.maturity)
|
55 |
pv_payouts = payouts * discount_factor
|
56 |
+
|
57 |
return pv_payouts, payouts
|
58 |
+
|
59 |
def black_scholes_put(self, S0, K, T, r, sigma):
|
60 |
"""Black-Scholes-Merton formula for European put option"""
|
|
|
|
|
|
|
|
|
|
|
|
|
61 |
d1 = (np.log(S0/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
|
62 |
d2 = d1 - sigma*np.sqrt(T)
|
63 |
+
|
64 |
put_price = K*np.exp(-r*T)*norm.cdf(-d2) - S0*norm.cdf(-d1)
|
65 |
return put_price
|
66 |
|
67 |
def create_dashboard():
|
68 |
tvog = TVOGAnalysis()
|
69 |
+
|
70 |
+
def update_analysis(scenarios, risk_free_rate, volatility, maturity,
|
71 |
+
sum_assured, policy_count, min_premium, max_premium, num_points):
|
72 |
+
|
73 |
# Update parameters
|
74 |
tvog.scenarios = int(scenarios)
|
75 |
tvog.risk_free_rate = risk_free_rate
|
|
|
77 |
tvog.maturity = maturity
|
78 |
tvog.sum_assured = sum_assured
|
79 |
tvog.policy_count = policy_count
|
80 |
+
|
81 |
# Create model points with varying initial account values
|
82 |
premiums = np.linspace(min_premium, max_premium, int(num_points))
|
83 |
+
initial_avs = premiums * policy_count
|
84 |
+
|
85 |
monte_carlo_results = []
|
86 |
black_scholes_results = []
|
87 |
+
|
88 |
time_steps = int(maturity * 12) # Monthly steps
|
89 |
+
|
90 |
+
for initial_av in initial_avs:
|
91 |
# Monte Carlo simulation
|
92 |
+
av_paths = tvog.simulate_account_values(initial_av, tvog.scenarios, time_steps)
|
93 |
pv_payouts, _ = tvog.calculate_gmab_payouts(av_paths)
|
94 |
mc_tvog = np.mean(pv_payouts)
|
95 |
monte_carlo_results.append(mc_tvog)
|
96 |
+
|
97 |
# Black-Scholes-Merton
|
98 |
+
guarantee = sum_assured * policy_count
|
99 |
+
bs_tvog = tvog.black_scholes_put(initial_av, guarantee, maturity,
|
100 |
+
risk_free_rate, volatility)
|
|
|
|
|
|
|
|
|
101 |
black_scholes_results.append(bs_tvog)
|
102 |
+
|
103 |
# Create results DataFrame
|
|
|
|
|
|
|
|
|
|
|
|
|
104 |
results_df = pd.DataFrame({
|
105 |
'Premium_per_Policy': premiums,
|
106 |
+
'Initial_Account_Value': initial_avs,
|
107 |
+
'Monte_Carlo_TVOG': monte_carlo_results,
|
108 |
+
'Black_Scholes_TVOG': black_scholes_results,
|
109 |
+
'Ratio_MC_BS': np.array(monte_carlo_results) / np.array(black_scholes_results),
|
110 |
+
'Difference': np.array(monte_carlo_results) - np.array(black_scholes_results)
|
111 |
})
|
112 |
+
|
|
|
|
|
|
|
|
|
|
|
113 |
# Create plots
|
114 |
fig1 = create_tvog_comparison_plot(results_df)
|
115 |
+
fig2 = create_sample_paths_plot(tvog, initial_avs[len(initial_avs)//2], time_steps)
|
116 |
+
fig3 = create_distribution_plots(tvog, initial_avs[len(initial_avs)//2], time_steps)
|
117 |
+
fig4 = create_convergence_plot(tvog, initial_avs[len(initial_avs)//2], time_steps)
|
118 |
+
|
119 |
return results_df, fig1, fig2, fig3, fig4
|
120 |
+
|
121 |
def create_tvog_comparison_plot(results_df):
|
122 |
"""Create TVOG comparison plot"""
|
123 |
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
|
124 |
+
|
125 |
# TVOG Comparison
|
126 |
+
ax1.scatter(results_df['Initial_Account_Value'], results_df['Monte_Carlo_TVOG'],
|
127 |
+
s=50, alpha=0.7, label='Monte Carlo', color='blue')
|
128 |
+
ax1.scatter(results_df['Initial_Account_Value'], results_df['Black_Scholes_TVOG'],
|
129 |
+
s=50, alpha=0.7, label='Black-Scholes-Merton', color='red')
|
130 |
+
ax1.set_xlabel('Initial Account Value')
|
131 |
+
ax1.set_ylabel('TVOG Value')
|
132 |
ax1.set_title('TVOG: Monte Carlo vs Black-Scholes-Merton')
|
133 |
ax1.legend()
|
134 |
ax1.grid(True, alpha=0.3)
|
135 |
+
|
136 |
# Ratio Plot
|
137 |
+
ax2.plot(results_df['Initial_Account_Value'], results_df['Ratio_MC_BS'],
|
138 |
+
'o-', color='green', markersize=6)
|
139 |
ax2.axhline(y=1, color='red', linestyle='--', alpha=0.7)
|
140 |
+
ax2.set_xlabel('Initial Account Value')
|
141 |
ax2.set_ylabel('Monte Carlo / Black-Scholes Ratio')
|
142 |
ax2.set_title('Convergence Ratio (MC/BS)')
|
143 |
ax2.grid(True, alpha=0.3)
|
144 |
+
|
145 |
plt.tight_layout()
|
146 |
return fig
|
147 |
+
|
148 |
+
def create_sample_paths_plot(tvog, initial_av, time_steps):
|
149 |
+
"""Create sample simulation paths plot"""
|
150 |
sample_scenarios = min(100, tvog.scenarios)
|
151 |
+
av_paths = tvog.simulate_account_values(initial_av, sample_scenarios, time_steps)
|
152 |
+
|
|
|
153 |
fig, ax = plt.subplots(1, 1, figsize=(12, 6))
|
154 |
+
|
155 |
time_axis = np.arange(time_steps + 1) / 12 # Convert to years
|
156 |
+
|
157 |
for i in range(sample_scenarios):
|
158 |
ax.plot(time_axis, av_paths[i, :], alpha=0.3, linewidth=0.8)
|
159 |
+
|
160 |
+
# Add mean path
|
161 |
mean_path = np.mean(av_paths, axis=0)
|
162 |
ax.plot(time_axis, mean_path, color='red', linewidth=3, label='Mean Path')
|
163 |
+
|
164 |
+
# Add guarantee line
|
165 |
+
guarantee = tvog.sum_assured * tvog.policy_count
|
166 |
+
ax.axhline(y=guarantee, color='black', linestyle='--', linewidth=2,
|
167 |
+
label=f'Guarantee Level ({guarantee:,.0f})')
|
168 |
+
|
169 |
ax.set_xlabel('Time (Years)')
|
170 |
+
ax.set_ylabel('Account Value')
|
171 |
+
ax.set_title(f'Sample Account Value Simulation Paths (n={sample_scenarios})')
|
172 |
ax.legend()
|
173 |
ax.grid(True, alpha=0.3)
|
174 |
+
|
175 |
return fig
|
176 |
+
|
177 |
+
def create_distribution_plots(tvog, initial_av, time_steps):
|
178 |
+
"""Create distribution analysis plots"""
|
179 |
+
av_paths = tvog.simulate_account_values(initial_av, tvog.scenarios, time_steps)
|
180 |
+
final_av = av_paths[:, -1]
|
181 |
+
|
182 |
+
# Present value of final account values
|
183 |
+
pv_final_av = final_av * np.exp(-tvog.risk_free_rate * tvog.maturity)
|
184 |
+
|
185 |
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
|
186 |
+
|
187 |
+
# Histogram of final account values
|
188 |
+
ax1.hist(pv_final_av, bins=50, density=True, alpha=0.7, color='skyblue')
|
189 |
+
|
190 |
+
# Theoretical lognormal distribution
|
191 |
+
S0 = initial_av
|
192 |
sigma = tvog.volatility
|
193 |
T = tvog.maturity
|
194 |
+
|
195 |
+
x_range = np.linspace(pv_final_av.min(), pv_final_av.max(), 1000)
|
196 |
+
theoretical_pdf = lognorm.pdf(x_range, sigma * np.sqrt(T), scale=S0)
|
|
|
|
|
|
|
|
|
|
|
|
|
197 |
ax1.plot(x_range, theoretical_pdf, 'r-', linewidth=2, label='Theoretical Lognormal')
|
198 |
+
ax1.axvline(x=S0, color='green', linestyle='--', label=f'Initial Value: {S0:,.0f}')
|
199 |
+
ax1.axvline(x=np.mean(pv_final_av), color='orange', linestyle='--',
|
200 |
+
label=f'Simulated Mean: {np.mean(pv_final_av):,.0f}')
|
201 |
+
|
202 |
+
ax1.set_xlabel('Present Value of Final Account Value')
|
|
|
203 |
ax1.set_ylabel('Density')
|
204 |
+
ax1.set_title('Distribution of Final Account Values')
|
205 |
ax1.legend()
|
206 |
ax1.grid(True, alpha=0.3)
|
207 |
+
|
208 |
+
# GMAB Payouts
|
209 |
+
pv_payouts, _ = tvog.calculate_gmab_payouts(av_paths)
|
210 |
+
non_zero_payouts = pv_payouts[pv_payouts > 0]
|
211 |
+
|
212 |
+
ax2.hist(pv_payouts, bins=50, alpha=0.7, color='lightcoral')
|
213 |
+
ax2.set_xlabel('GMAB Payout (Present Value)')
|
|
|
|
|
|
|
214 |
ax2.set_ylabel('Frequency')
|
215 |
+
ax2.set_title(f'GMAB Payout Distribution\n({len(non_zero_payouts)} non-zero payouts)')
|
216 |
ax2.grid(True, alpha=0.3)
|
217 |
+
|
218 |
+
# Add statistics text
|
219 |
+
stats_text = f'Mean Payout: {np.mean(pv_payouts):,.0f}\n'
|
220 |
+
stats_text += f'Max Payout: {np.max(pv_payouts):,.0f}\n'
|
221 |
+
stats_text += f'Payout Probability: {len(non_zero_payouts)/len(pv_payouts):.1%}'
|
222 |
+
ax2.text(0.95, 0.95, stats_text, transform=ax2.transAxes,
|
223 |
+
verticalalignment='top', horizontalalignment='right',
|
224 |
+
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
|
225 |
+
|
226 |
plt.tight_layout()
|
227 |
return fig
|
228 |
+
|
229 |
+
def create_convergence_plot(tvog, initial_av, time_steps):
|
230 |
+
"""Create convergence analysis plot"""
|
231 |
+
# Test different numbers of scenarios
|
232 |
+
scenario_counts = [100, 500, 1000, 2000, 5000, 10000]
|
233 |
+
if tvog.scenarios not in scenario_counts:
|
234 |
+
scenario_counts.append(tvog.scenarios)
|
235 |
scenario_counts.sort()
|
236 |
+
|
237 |
+
mc_results = []
|
238 |
+
|
239 |
+
guarantee = tvog.sum_assured * tvog.policy_count
|
240 |
+
bs_result = tvog.black_scholes_put(initial_av, guarantee, tvog.maturity,
|
241 |
+
tvog.risk_free_rate, tvog.volatility)
|
242 |
+
|
243 |
+
np.random.seed(42) # For reproducible convergence
|
244 |
+
|
245 |
for n_scenarios in scenario_counts:
|
246 |
+
av_paths = tvog.simulate_account_values(initial_av, n_scenarios, time_steps)
|
|
|
|
|
|
|
|
|
247 |
pv_payouts, _ = tvog.calculate_gmab_payouts(av_paths)
|
248 |
mc_tvog = np.mean(pv_payouts)
|
249 |
+
mc_results.append(mc_tvog)
|
250 |
+
|
|
|
|
|
251 |
fig, ax = plt.subplots(1, 1, figsize=(10, 6))
|
252 |
+
|
253 |
+
ax.plot(scenario_counts, mc_results, 'bo-', markersize=8, linewidth=2,
|
254 |
+
label='Monte Carlo Results')
|
255 |
+
ax.axhline(y=bs_result, color='red', linestyle='--', linewidth=2,
|
256 |
+
label=f'Black-Scholes Result: {bs_result:.0f}')
|
257 |
+
|
258 |
ax.set_xlabel('Number of Scenarios')
|
259 |
+
ax.set_ylabel('TVOG Value')
|
260 |
+
ax.set_title('Monte Carlo Convergence Analysis')
|
261 |
ax.legend()
|
262 |
ax.grid(True, alpha=0.3)
|
263 |
ax.set_xscale('log')
|
264 |
+
|
265 |
+
# Add convergence statistics
|
266 |
+
final_error = abs(mc_results[-1] - bs_result) / bs_result * 100
|
267 |
+
ax.text(0.02, 0.98, f'Final Error: {final_error:.2f}%',
|
268 |
+
transform=ax.transAxes, verticalalignment='top',
|
269 |
+
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
|
270 |
+
|
|
|
|
|
|
|
|
|
|
|
271 |
return fig
|
272 |
+
|
273 |
# Create Gradio interface
|
274 |
+
with gr.Blocks(title="TVOG Analysis Dashboard") as app:
|
275 |
gr.Markdown("""
|
276 |
# Time Value of Options and Guarantees (TVOG) Analysis Dashboard
|
277 |
+
|
278 |
+
This dashboard compares Monte Carlo simulation results with Black-Scholes-Merton analytical solutions
|
279 |
for Variable Annuity products with Guaranteed Minimum Accumulation Benefits (GMAB).
|
280 |
+
|
281 |
**Target Users:** Actuaries, Finance Professionals, Economists, and Academics
|
282 |
""")
|
283 |
+
|
284 |
with gr.Row():
|
285 |
with gr.Column(scale=1):
|
286 |
gr.Markdown("### Model Parameters")
|
287 |
+
|
288 |
+
scenarios = gr.Slider(
|
289 |
minimum=1000, maximum=50000, step=1000, value=10000,
|
290 |
label="Number of Monte Carlo Scenarios"
|
291 |
)
|
292 |
+
|
293 |
+
risk_free_rate = gr.Slider(
|
294 |
minimum=0.001, maximum=0.1, step=0.001, value=0.02,
|
295 |
label="Risk-Free Rate (continuous)"
|
296 |
)
|
297 |
+
|
298 |
+
volatility = gr.Slider(
|
299 |
minimum=0.01, maximum=0.5, step=0.01, value=0.03,
|
300 |
label="Volatility (σ)"
|
301 |
)
|
302 |
+
|
303 |
+
maturity = gr.Slider(
|
304 |
minimum=1, maximum=30, step=1, value=10,
|
305 |
label="Maturity (years)"
|
306 |
)
|
307 |
+
|
308 |
with gr.Row():
|
309 |
+
sum_assured = gr.Number(
|
310 |
value=500000, label="Sum Assured per Policy"
|
311 |
)
|
312 |
+
policy_count = gr.Number(
|
313 |
+
value=100, label="Number of Policies"
|
314 |
)
|
315 |
+
|
316 |
+
gr.Markdown("### Model Point Range")
|
317 |
+
|
318 |
with gr.Row():
|
319 |
+
min_premium = gr.Number(
|
320 |
+
value=300000, label="Min Premium per Policy"
|
321 |
)
|
322 |
+
max_premium = gr.Number(
|
323 |
+
value=500000, label="Max Premium per Policy"
|
324 |
)
|
325 |
+
|
326 |
+
num_points = gr.Slider(
|
327 |
minimum=3, maximum=20, step=1, value=9,
|
328 |
label="Number of Model Points"
|
329 |
)
|
330 |
+
|
331 |
calculate_btn = gr.Button("Run Analysis", variant="primary")
|
332 |
+
|
333 |
with gr.Column(scale=2):
|
334 |
gr.Markdown("### Results Summary")
|
335 |
+
results_table = gr.Dataframe(
|
336 |
+
headers=["Premium per Policy", "Initial Account Value", "Monte Carlo TVOG",
|
337 |
+
"Black-Scholes TVOG", "MC/BS Ratio", "Difference"],
|
338 |
+
label="TVOG Comparison Results"
|
|
|
|
|
339 |
)
|
340 |
+
|
341 |
+
with gr.Row():
|
342 |
+
tvog_plot = gr.Plot(label="TVOG Comparison Analysis")
|
343 |
+
|
344 |
+
with gr.Row():
|
345 |
+
paths_plot = gr.Plot(label="Sample Simulation Paths")
|
346 |
+
|
347 |
+
with gr.Row():
|
348 |
+
dist_plot = gr.Plot(label="Distribution Analysis")
|
349 |
+
|
350 |
+
with gr.Row():
|
351 |
+
conv_plot = gr.Plot(label="Monte Carlo Convergence")
|
352 |
+
|
353 |
# Event handlers
|
|
|
|
|
|
|
|
|
|
|
|
|
354 |
calculate_btn.click(
|
355 |
fn=update_analysis,
|
356 |
+
inputs=[scenarios, risk_free_rate, volatility, maturity,
|
357 |
+
sum_assured, policy_count, min_premium, max_premium, num_points],
|
358 |
+
outputs=[results_table, tvog_plot, paths_plot, dist_plot, conv_plot]
|
359 |
)
|
360 |
+
|
361 |
+
# Initial calculation
|
362 |
app.load(
|
363 |
fn=update_analysis,
|
364 |
+
inputs=[scenarios, risk_free_rate, volatility, maturity,
|
365 |
+
sum_assured, policy_count, min_premium, max_premium, num_points],
|
366 |
+
outputs=[results_table, tvog_plot, paths_plot, dist_plot, conv_plot]
|
367 |
)
|
368 |
+
|
369 |
+
return app
|
370 |
|
371 |
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
372 |
app = create_dashboard()
|
373 |
app.launch()
|