Spaces:
Sleeping
Sleeping
import gradio as gr | |
import numpy as np | |
import pandas as pd | |
import matplotlib.pyplot as plt | |
from scipy.stats import norm, lognorm | |
import seaborn as sns | |
# Set matplotlib style for professional plots | |
plt.style.use('default') | |
sns.set_palette("husl") | |
class TVOGAnalysis: | |
def __init__(self): | |
self.reset_parameters() | |
def reset_parameters(self): | |
"""Reset to default parameters""" | |
self.scenarios = 10000 | |
self.risk_free_rate = 0.02 | |
self.volatility = 0.03 | |
self.maturity = 10 | |
self.sum_assured = 500000 | |
self.policy_count = 100 | |
def generate_random_numbers(self, scenarios, time_steps): | |
"""Generate standard normal random numbers""" | |
np.random.seed(42) # For reproducibility | |
return np.random.standard_normal((scenarios, time_steps)) | |
def simulate_account_values(self, initial_av, scenarios, time_steps): | |
"""Simulate account value paths using geometric Brownian motion""" | |
dt = 1/12 # Monthly time steps | |
rand_nums = self.generate_random_numbers(scenarios, time_steps) | |
# Initialize account value matrix | |
av_paths = np.zeros((scenarios, time_steps + 1)) | |
av_paths[:, 0] = initial_av | |
# Simulate paths | |
for t in range(time_steps): | |
drift = (self.risk_free_rate - 0.5 * self.volatility**2) * dt | |
diffusion = self.volatility * np.sqrt(dt) * rand_nums[:, t] | |
av_paths[:, t+1] = av_paths[:, t] * np.exp(drift + diffusion) | |
return av_paths | |
def calculate_gmab_payouts(self, av_paths): | |
"""Calculate GMAB payouts at maturity""" | |
final_av = av_paths[:, -1] | |
guarantee = self.sum_assured * self.policy_count | |
payouts = np.maximum(guarantee - final_av, 0) | |
# Present value of payouts | |
discount_factor = np.exp(-self.risk_free_rate * self.maturity) | |
pv_payouts = payouts * discount_factor | |
return pv_payouts, payouts | |
def black_scholes_put(self, S0, K, T, r, sigma): | |
"""Black-Scholes-Merton formula for European put option""" | |
d1 = (np.log(S0/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T)) | |
d2 = d1 - sigma*np.sqrt(T) | |
put_price = K*np.exp(-r*T)*norm.cdf(-d2) - S0*norm.cdf(-d1) | |
return put_price | |
def create_dashboard(): | |
tvog = TVOGAnalysis() | |
def update_analysis(scenarios, risk_free_rate, volatility, maturity, | |
sum_assured, policy_count, min_premium, max_premium, num_points): | |
# Update parameters | |
tvog.scenarios = int(scenarios) | |
tvog.risk_free_rate = risk_free_rate | |
tvog.volatility = volatility | |
tvog.maturity = maturity | |
tvog.sum_assured = sum_assured | |
tvog.policy_count = policy_count | |
# Create model points with varying initial account values | |
premiums = np.linspace(min_premium, max_premium, int(num_points)) | |
initial_avs = premiums * policy_count | |
monte_carlo_results = [] | |
black_scholes_results = [] | |
time_steps = int(maturity * 12) # Monthly steps | |
for initial_av in initial_avs: | |
# Monte Carlo simulation | |
av_paths = tvog.simulate_account_values(initial_av, tvog.scenarios, time_steps) | |
pv_payouts, _ = tvog.calculate_gmab_payouts(av_paths) | |
mc_tvog = np.mean(pv_payouts) | |
monte_carlo_results.append(mc_tvog) | |
# Black-Scholes-Merton | |
guarantee = sum_assured * policy_count | |
bs_tvog = tvog.black_scholes_put(initial_av, guarantee, maturity, | |
risk_free_rate, volatility) | |
black_scholes_results.append(bs_tvog) | |
# Create results DataFrame | |
results_df = pd.DataFrame({ | |
'Premium_per_Policy': premiums, | |
'Initial_Account_Value': initial_avs, | |
'Monte_Carlo_TVOG': monte_carlo_results, | |
'Black_Scholes_TVOG': black_scholes_results, | |
'Ratio_MC_BS': np.array(monte_carlo_results) / np.array(black_scholes_results), | |
'Difference': np.array(monte_carlo_results) - np.array(black_scholes_results) | |
}) | |
# Create plots | |
fig1 = create_tvog_comparison_plot(results_df) | |
fig2 = create_sample_paths_plot(tvog, initial_avs[len(initial_avs)//2], time_steps) | |
fig3 = create_distribution_plots(tvog, initial_avs[len(initial_avs)//2], time_steps) | |
fig4 = create_convergence_plot(tvog, initial_avs[len(initial_avs)//2], time_steps) | |
return results_df, fig1, fig2, fig3, fig4 | |
def create_tvog_comparison_plot(results_df): | |
"""Create TVOG comparison plot""" | |
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) | |
# TVOG Comparison | |
ax1.scatter(results_df['Initial_Account_Value'], results_df['Monte_Carlo_TVOG'], | |
s=50, alpha=0.7, label='Monte Carlo', color='blue') | |
ax1.scatter(results_df['Initial_Account_Value'], results_df['Black_Scholes_TVOG'], | |
s=50, alpha=0.7, label='Black-Scholes-Merton', color='red') | |
ax1.set_xlabel('Initial Account Value') | |
ax1.set_ylabel('TVOG Value') | |
ax1.set_title('TVOG: Monte Carlo vs Black-Scholes-Merton') | |
ax1.legend() | |
ax1.grid(True, alpha=0.3) | |
# Ratio Plot | |
ax2.plot(results_df['Initial_Account_Value'], results_df['Ratio_MC_BS'], | |
'o-', color='green', markersize=6) | |
ax2.axhline(y=1, color='red', linestyle='--', alpha=0.7) | |
ax2.set_xlabel('Initial Account Value') | |
ax2.set_ylabel('Monte Carlo / Black-Scholes Ratio') | |
ax2.set_title('Convergence Ratio (MC/BS)') | |
ax2.grid(True, alpha=0.3) | |
plt.tight_layout() | |
return fig | |
def create_sample_paths_plot(tvog, initial_av, time_steps): | |
"""Create sample simulation paths plot""" | |
sample_scenarios = min(100, tvog.scenarios) | |
av_paths = tvog.simulate_account_values(initial_av, sample_scenarios, time_steps) | |
fig, ax = plt.subplots(1, 1, figsize=(12, 6)) | |
time_axis = np.arange(time_steps + 1) / 12 # Convert to years | |
for i in range(sample_scenarios): | |
ax.plot(time_axis, av_paths[i, :], alpha=0.3, linewidth=0.8) | |
# Add mean path | |
mean_path = np.mean(av_paths, axis=0) | |
ax.plot(time_axis, mean_path, color='red', linewidth=3, label='Mean Path') | |
# Add guarantee line | |
guarantee = tvog.sum_assured * tvog.policy_count | |
ax.axhline(y=guarantee, color='black', linestyle='--', linewidth=2, | |
label=f'Guarantee Level ({guarantee:,.0f})') | |
ax.set_xlabel('Time (Years)') | |
ax.set_ylabel('Account Value') | |
ax.set_title(f'Sample Account Value Simulation Paths (n={sample_scenarios})') | |
ax.legend() | |
ax.grid(True, alpha=0.3) | |
return fig | |
def create_distribution_plots(tvog, initial_av, time_steps): | |
"""Create distribution analysis plots""" | |
av_paths = tvog.simulate_account_values(initial_av, tvog.scenarios, time_steps) | |
final_av = av_paths[:, -1] | |
# Present value of final account values | |
pv_final_av = final_av * np.exp(-tvog.risk_free_rate * tvog.maturity) | |
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) | |
# Histogram of final account values | |
ax1.hist(pv_final_av, bins=50, density=True, alpha=0.7, color='skyblue') | |
# Theoretical lognormal distribution | |
S0 = initial_av | |
sigma = tvog.volatility | |
T = tvog.maturity | |
x_range = np.linspace(pv_final_av.min(), pv_final_av.max(), 1000) | |
theoretical_pdf = lognorm.pdf(x_range, sigma * np.sqrt(T), scale=S0) | |
ax1.plot(x_range, theoretical_pdf, 'r-', linewidth=2, label='Theoretical Lognormal') | |
ax1.axvline(x=S0, color='green', linestyle='--', label=f'Initial Value: {S0:,.0f}') | |
ax1.axvline(x=np.mean(pv_final_av), color='orange', linestyle='--', | |
label=f'Simulated Mean: {np.mean(pv_final_av):,.0f}') | |
ax1.set_xlabel('Present Value of Final Account Value') | |
ax1.set_ylabel('Density') | |
ax1.set_title('Distribution of Final Account Values') | |
ax1.legend() | |
ax1.grid(True, alpha=0.3) | |
# GMAB Payouts | |
pv_payouts, _ = tvog.calculate_gmab_payouts(av_paths) | |
non_zero_payouts = pv_payouts[pv_payouts > 0] | |
ax2.hist(pv_payouts, bins=50, alpha=0.7, color='lightcoral') | |
ax2.set_xlabel('GMAB Payout (Present Value)') | |
ax2.set_ylabel('Frequency') | |
ax2.set_title(f'GMAB Payout Distribution\n({len(non_zero_payouts)} non-zero payouts)') | |
ax2.grid(True, alpha=0.3) | |
# Add statistics text | |
stats_text = f'Mean Payout: {np.mean(pv_payouts):,.0f}\n' | |
stats_text += f'Max Payout: {np.max(pv_payouts):,.0f}\n' | |
stats_text += f'Payout Probability: {len(non_zero_payouts)/len(pv_payouts):.1%}' | |
ax2.text(0.95, 0.95, stats_text, transform=ax2.transAxes, | |
verticalalignment='top', horizontalalignment='right', | |
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8)) | |
plt.tight_layout() | |
return fig | |
def create_convergence_plot(tvog, initial_av, time_steps): | |
"""Create convergence analysis plot""" | |
# Test different numbers of scenarios | |
scenario_counts = [100, 500, 1000, 2000, 5000, 10000] | |
if tvog.scenarios not in scenario_counts: | |
scenario_counts.append(tvog.scenarios) | |
scenario_counts.sort() | |
mc_results = [] | |
guarantee = tvog.sum_assured * tvog.policy_count | |
bs_result = tvog.black_scholes_put(initial_av, guarantee, tvog.maturity, | |
tvog.risk_free_rate, tvog.volatility) | |
np.random.seed(42) # For reproducible convergence | |
for n_scenarios in scenario_counts: | |
av_paths = tvog.simulate_account_values(initial_av, n_scenarios, time_steps) | |
pv_payouts, _ = tvog.calculate_gmab_payouts(av_paths) | |
mc_tvog = np.mean(pv_payouts) | |
mc_results.append(mc_tvog) | |
fig, ax = plt.subplots(1, 1, figsize=(10, 6)) | |
ax.plot(scenario_counts, mc_results, 'bo-', markersize=8, linewidth=2, | |
label='Monte Carlo Results') | |
ax.axhline(y=bs_result, color='red', linestyle='--', linewidth=2, | |
label=f'Black-Scholes Result: {bs_result:.0f}') | |
ax.set_xlabel('Number of Scenarios') | |
ax.set_ylabel('TVOG Value') | |
ax.set_title('Monte Carlo Convergence Analysis') | |
ax.legend() | |
ax.grid(True, alpha=0.3) | |
ax.set_xscale('log') | |
# Add convergence statistics | |
final_error = abs(mc_results[-1] - bs_result) / bs_result * 100 | |
ax.text(0.02, 0.98, f'Final Error: {final_error:.2f}%', | |
transform=ax.transAxes, verticalalignment='top', | |
bbox=dict(boxstyle='round', facecolor='white', alpha=0.8)) | |
return fig | |
# Create Gradio interface | |
with gr.Blocks(title="TVOG Analysis Dashboard") as app: | |
gr.Markdown(""" | |
# Time Value of Options and Guarantees (TVOG) Analysis Dashboard | |
This dashboard compares Monte Carlo simulation results with Black-Scholes-Merton analytical solutions | |
for Variable Annuity products with Guaranteed Minimum Accumulation Benefits (GMAB). | |
**Target Users:** Actuaries, Finance Professionals, Economists, and Academics | |
""") | |
with gr.Row(): | |
with gr.Column(scale=1): | |
gr.Markdown("### Model Parameters") | |
scenarios = gr.Slider( | |
minimum=1000, maximum=50000, step=1000, value=10000, | |
label="Number of Monte Carlo Scenarios" | |
) | |
risk_free_rate = gr.Slider( | |
minimum=0.001, maximum=0.1, step=0.001, value=0.02, | |
label="Risk-Free Rate (continuous)" | |
) | |
volatility = gr.Slider( | |
minimum=0.01, maximum=0.5, step=0.01, value=0.03, | |
label="Volatility (σ)" | |
) | |
maturity = gr.Slider( | |
minimum=1, maximum=30, step=1, value=10, | |
label="Maturity (years)" | |
) | |
with gr.Row(): | |
sum_assured = gr.Number( | |
value=500000, label="Sum Assured per Policy" | |
) | |
policy_count = gr.Number( | |
value=100, label="Number of Policies" | |
) | |
gr.Markdown("### Model Point Range") | |
with gr.Row(): | |
min_premium = gr.Number( | |
value=300000, label="Min Premium per Policy" | |
) | |
max_premium = gr.Number( | |
value=500000, label="Max Premium per Policy" | |
) | |
num_points = gr.Slider( | |
minimum=3, maximum=20, step=1, value=9, | |
label="Number of Model Points" | |
) | |
calculate_btn = gr.Button("Run Analysis", variant="primary") | |
with gr.Column(scale=2): | |
gr.Markdown("### Results Summary") | |
results_table = gr.Dataframe( | |
headers=["Premium per Policy", "Initial Account Value", "Monte Carlo TVOG", | |
"Black-Scholes TVOG", "MC/BS Ratio", "Difference"], | |
label="TVOG Comparison Results" | |
) | |
with gr.Tabs(): | |
with gr.Tab("TVOG Comparison"): | |
tvog_plot = gr.Plot(label="Monte Carlo vs Black-Scholes Analysis") | |
with gr.Tab("Simulation Paths"): | |
paths_plot = gr.Plot(label="Sample Account Value Trajectories") | |
with gr.Tab("Distribution Analysis"): | |
dist_plot = gr.Plot(label="Final Values & Payout Distributions") | |
with gr.Tab("Convergence Analysis"): | |
conv_plot = gr.Plot(label="Monte Carlo Convergence to Analytical Solution") | |
# Event handlers | |
calculate_btn.click( | |
fn=update_analysis, | |
inputs=[scenarios, risk_free_rate, volatility, maturity, | |
sum_assured, policy_count, min_premium, max_premium, num_points], | |
outputs=[results_table, tvog_plot, paths_plot, dist_plot, conv_plot] | |
) | |
# Initial calculation | |
app.load( | |
fn=update_analysis, | |
inputs=[scenarios, risk_free_rate, volatility, maturity, | |
sum_assured, policy_count, min_premium, max_premium, num_points], | |
outputs=[results_table, tvog_plot, paths_plot, dist_plot, conv_plot] | |
) | |
return app | |
if __name__ == "__main__": | |
app = create_dashboard() | |
app.launch() |