Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import gradio as gr
|
2 |
+
import numpy as np
|
3 |
+
import pandas as pd
|
4 |
+
import matplotlib.pyplot as plt
|
5 |
+
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
|
19 |
+
self.risk_free_rate = 0.02
|
20 |
+
self.volatility = 0.03
|
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
|
76 |
+
tvog.volatility = volatility
|
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()
|