alidenewade commited on
Commit
03cdfd2
·
verified ·
1 Parent(s): 43223e7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +200 -236
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') # Using 'default' as specified
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
- sum_assured, policy_count, min_premium, max_premium, num_points):
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 # Assuming premium is total, not per policy for AV calc
90
-
91
  monte_carlo_results = []
92
  black_scholes_results = []
93
-
94
  time_steps = int(maturity * 12) # Monthly steps
95
-
96
- for initial_av_point in initial_avs: # Changed initial_av to initial_av_point
97
  # Monte Carlo simulation
98
- av_paths = tvog.simulate_account_values(initial_av_point, tvog.scenarios, time_steps)
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
- # S0 for BSM is the total initial account value for the block of policies
105
- # K is the total guarantee for the block of policies
106
- total_initial_av = initial_av_point # This is already total for the policies based on premium * policy_count
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
- 'Initial_Account_Value_Total': initial_avs,
123
- 'Monte_Carlo_TVOG_Total': monte_carlo_results,
124
- 'Black_Scholes_TVOG_Total': black_scholes_results,
125
- 'Ratio_MC_BS': ratio_mc_bs,
126
- 'Difference_MC_BS': mc_array - bs_array
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, representative_initial_av, time_steps)
137
- fig3 = create_distribution_plots(tvog, representative_initial_av, time_steps)
138
- fig4 = create_convergence_plot(tvog, representative_initial_av, time_steps)
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['Initial_Account_Value_Total'], results_df['Monte_Carlo_TVOG_Total'],
148
- s=50, alpha=0.7, label='Monte Carlo', color='blue')
149
- ax1.scatter(results_df['Initial_Account_Value_Total'], results_df['Black_Scholes_TVOG_Total'],
150
- s=50, alpha=0.7, label='Black-Scholes-Merton', color='red')
151
- ax1.set_xlabel('Initial Account Value (Total)')
152
- ax1.set_ylabel('TVOG Value (Total)')
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['Initial_Account_Value_Total'], results_df['Ratio_MC_BS'],
159
- 'o-', color='green', markersize=6)
160
  ax2.axhline(y=1, color='red', linestyle='--', alpha=0.7)
161
- ax2.set_xlabel('Initial Account Value (Total)')
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, initial_av_total, time_steps):
170
- """Create sample simulation paths plot using total initial AV"""
171
  sample_scenarios = min(100, tvog.scenarios)
172
- # Simulate paths for the total block of policies
173
- av_paths = tvog.simulate_account_values(initial_av_total, sample_scenarios, time_steps)
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
- total_guarantee = tvog.sum_assured * tvog.policy_count
185
- ax.axhline(y=total_guarantee, color='black', linestyle='--', linewidth=2,
186
- label=f'Total Guarantee Level ({total_guarantee:,.0f})')
187
-
 
188
  ax.set_xlabel('Time (Years)')
189
- ax.set_ylabel('Total Account Value')
190
- ax.set_title(f'Sample Total Account Value Simulation Paths (n={sample_scenarios})')
191
  ax.legend()
192
  ax.grid(True, alpha=0.3)
193
- plt.tight_layout()
194
  return fig
195
-
196
- def create_distribution_plots(tvog, initial_av_total, time_steps):
197
- """Create distribution analysis plots using total initial AV"""
198
- # Simulate paths for the total block of policies
199
- av_paths = tvog.simulate_account_values(initial_av_total, tvog.scenarios, time_steps)
200
- final_av_total = av_paths[:, -1]
201
-
202
- pv_final_av_total = final_av_total * np.exp(-tvog.risk_free_rate * tvog.maturity)
203
-
204
  fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
205
-
206
- # Histogram of final total account values
207
- ax1.hist(pv_final_av_total, bins=50, density=True, alpha=0.7, color='skyblue')
208
-
209
- S0_total = initial_av_total
 
210
  sigma = tvog.volatility
211
  T = tvog.maturity
212
- r = tvog.risk_free_rate
213
-
214
- # Theoretical lognormal distribution for total AV
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=S0_total, color='green', linestyle='--', label=f'Initial Total Value: {S0_total:,.0f}')
224
- ax1.axvline(x=np.mean(pv_final_av_total), color='orange', linestyle='--',
225
- label=f'Simulated Mean PV: {np.mean(pv_final_av_total):,.0f}')
226
-
227
- ax1.set_xlabel('Present Value of Final Total Account Value')
228
  ax1.set_ylabel('Density')
229
- ax1.set_title('Distribution of Final Total Account Values (PV)')
230
  ax1.legend()
231
  ax1.grid(True, alpha=0.3)
232
-
233
- # GMAB Payouts for the total block
234
- pv_payouts_total, _ = tvog.calculate_gmab_payouts(av_paths) # Uses total guarantee internally
235
- non_zero_payouts = pv_payouts_total[pv_payouts_total > 1e-6] # Use a small epsilon for float comparison
236
-
237
- ax2.hist(pv_payouts_total, bins=50, alpha=0.7, color='lightcoral', label="All Scenarios")
238
- if len(non_zero_payouts) > 0:
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'Total GMAB Payout Distribution\n({len(non_zero_payouts)} non-zero payouts)')
244
  ax2.grid(True, alpha=0.3)
245
- ax2.legend()
246
-
247
- stats_text = f'Mean Total Payout: {np.mean(pv_payouts_total):,.0f}\n'
248
- stats_text += f'Max Total Payout: {np.max(pv_payouts_total):,.0f}\n'
249
- stats_text += f'Payout Probability: {len(non_zero_payouts)/len(pv_payouts_total):.1%}'
250
- ax2.text(0.95, 0.95, stats_text, transform=ax2.transAxes,
251
- verticalalignment='top', horizontalalignment='right',
252
- bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
253
-
254
  plt.tight_layout()
255
  return fig
256
-
257
- def create_convergence_plot(tvog, initial_av_total, time_steps):
258
- """Create convergence analysis plot using total initial AV"""
259
- scenario_counts = [100, 500, 1000, 2000, 5000, 10000, 20000] # Expanded
260
- current_sim_scenarios = tvog.scenarios # Store original
261
- if current_sim_scenarios not in scenario_counts:
262
- scenario_counts.append(current_sim_scenarios)
263
  scenario_counts.sort()
264
-
265
- mc_results_total = []
266
- total_guarantee = tvog.sum_assured * tvog.policy_count
267
- bs_result_total = tvog.black_scholes_put(initial_av_total, total_guarantee, tvog.maturity,
268
- tvog.risk_free_rate, tvog.volatility)
269
-
270
- original_seed_state = np.random.get_state() # Save global random state
271
-
 
272
  for n_scenarios in scenario_counts:
273
- np.random.set_state(original_seed_state) # Reset seed for each n_scenarios run
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
- mc_results_total.append(mc_tvog)
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
- ax.plot(scenario_counts, mc_results_total, 'bo-', markersize=8, linewidth=2,
286
- label='Monte Carlo Results (Total)')
287
- ax.axhline(y=bs_result_total, color='red', linestyle='--', linewidth=2,
288
- label=f'Black-Scholes Result (Total): {bs_result_total:,.0f}')
289
-
 
290
  ax.set_xlabel('Number of Scenarios')
291
- ax.set_ylabel('TVOG Value (Total)')
292
- ax.set_title('Monte Carlo Convergence Analysis (Total)')
293
  ax.legend()
294
  ax.grid(True, alpha=0.3)
295
  ax.set_xscale('log')
296
-
297
- if bs_result_total > 1e-6: # Avoid division by zero
298
- final_error = abs(mc_results_total[-1] - bs_result_total) / bs_result_total * 100
299
- ax.text(0.02, 0.98, f'Final Error: {final_error:.2f}%',
300
- transform=ax.transAxes, verticalalignment='top',
301
- bbox=dict(boxstyle='round', facecolor='white', alpha=0.8))
302
- else:
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: # No theme specified to use default
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
- scenarios_input = gr.Slider(
 
325
  minimum=1000, maximum=50000, step=1000, value=10000,
326
  label="Number of Monte Carlo Scenarios"
327
  )
328
- risk_free_rate_input = gr.Slider(
 
329
  minimum=0.001, maximum=0.1, step=0.001, value=0.02,
330
  label="Risk-Free Rate (continuous)"
331
  )
332
- volatility_input = gr.Slider(
 
333
  minimum=0.01, maximum=0.5, step=0.01, value=0.03,
334
  label="Volatility (σ)"
335
  )
336
- maturity_input = gr.Slider(
 
337
  minimum=1, maximum=30, step=1, value=10,
338
  label="Maturity (years)"
339
  )
 
340
  with gr.Row():
341
- sum_assured_input = gr.Number(
342
  value=500000, label="Sum Assured per Policy"
343
  )
344
- policy_count_input = gr.Number(
345
- value=100, label="Number of Policies", precision=0
346
  )
347
-
348
- gr.Markdown("### Model Point Range (Based on Premium per Policy)")
 
349
  with gr.Row():
350
- min_premium_input = gr.Number(
351
- value=3000, label="Min Premium per Policy" # Adjusted default
352
  )
353
- max_premium_input = gr.Number(
354
- value=7000, label="Max Premium per Policy" # Adjusted default
355
  )
356
- num_points_input = gr.Slider(
 
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
- results_table_display = gr.Dataframe(
365
- headers=["Premium per Policy", "Initial Account Value (Total)",
366
- "Monte Carlo TVOG (Total)", "Black-Scholes TVOG (Total)",
367
- "MC/BS Ratio", "Difference (MC-BS)"],
368
- label="TVOG Comparison Results",
369
- wrap=True
370
  )
371
- # Tabs for plots, placed after the row with inputs and summary table
372
- with gr.Tabs():
373
- with gr.TabItem("📈 TVOG Comparison"):
374
- tvog_plot_display = gr.Plot(label="TVOG Comparison Analysis")
375
- with gr.TabItem("📊 Sample Paths"):
376
- paths_plot_display = gr.Plot(label="Sample Simulation Paths")
377
- with gr.TabItem("📉 Distribution Analysis"):
378
- dist_plot_display = gr.Plot(label="Distribution Analysis")
379
- with gr.TabItem("⚙️ Monte Carlo Convergence"):
380
- conv_plot_display = gr.Plot(label="Monte Carlo Convergence")
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=inputs_list,
392
- outputs=outputs_list
 
393
  )
394
-
395
- # Initial calculation on app load
396
  app.load(
397
  fn=update_analysis,
398
- inputs=inputs_list, # Use the same input components
399
- outputs=outputs_list
 
400
  )
401
- return app
 
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()