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({ 'Model_Point': range(1, len(premiums) + 1), '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=["Model Point", "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()