Skip to content

Convergence (Space Grid)

Background

Black Scholes Cn Convergence Space

Educational script demonstrating black scholes cn convergence space concepts.


What This Code Demonstrates

  • Parameters ===

Code

```python """ Black Scholes Cn Convergence Space

Educational script demonstrating black scholes cn convergence space concepts. """

============================================================================

black_scholes_RUN_CONVERGENCE_ANALYSIS_ON_SPACE_GRID_SIZE_OF_CN_SCHEME_FOR_EUROPEAN_CALL.py

============================================================================

import black_scholes as bs import matplotlib.pyplot as plt import numpy as np

=== Parameters ===

if name == "main": S = 100 K = 100 T = 1.0 r = 0.05 sigma = 0.2 q = 0 S_min = 0.1 S_max = 300 # S_max should be bigger than your S_max of interest if use log space option_type = "call"

print(f"\n{'='*70}")
print("GRID CONVERGENCE ANALYSIS")
print("="*70)
print(f"Analyzing {option_type.upper()} option convergence")
print(f"Parameters:")
print(f"  Stock Price (S): ${S}")
print(f"  Strike Price (K): ${K}")
print(f"  Time to Maturity: {T} year")
print(f"  Risk-free Rate: {r:.1%}")
print(f"  Volatility: {sigma:.1%}")
print(f"  Price Range: ${S_min} - ${S_max}")

# Create Black-Scholes model
bs_model = bs.BlackScholes(S, K, T, r, sigma, q)

# Get analytical benchmark
if option_type == "call":
    analytical_price, _ = bs_model.price_analytical()
else:
    _, analytical_price = bs_model.price_analytical()

print(f"  Analytical Price: ${analytical_price:.6f}")

print(f"\nRunning convergence analysis...")

errors = []
grid_sizes = []
num_points = []

for M in range(15, 100, 5):  # Grid points
    print(f"  Testing grid size: {M+1} points...")

    # Run Crank-Nicolson method
    S_cn, V_cn = bs_model.price_numerical(
        method="cn", 
        option_type=option_type, 
        Smin=S_min, 
        Smax=S_max, 
        NS=M+1
    )

    # Get analytical prices at grid points
    if option_type == "call":
        V_exact = bs.bs_call_price(S_cn, K, T, r, sigma, q)
    else:
        V_exact = bs.bs_put_price(S_cn, K, T, r, sigma, q)

    # Calculate maximum error
    max_error = np.max(np.abs(V_cn - V_exact))
    errors.append(max_error)
    grid_sizes.append(1 / M)  # Grid spacing
    num_points.append(M + 1)

# Convert to numpy arrays for analysis
errors = np.array(errors)
grid_sizes = np.array(grid_sizes)
num_points = np.array(num_points)

print(f"Convergence analysis completed!")

# === Enhanced Plotting ===
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))

# Plot 1: Log-log convergence plot
ax1.loglog(grid_sizes, errors, 'bo-', linewidth=2, markersize=6, label='CN FDM Errors')

# Fit theoretical convergence lines
log_h = np.log(grid_sizes)
log_err = np.log(errors)

# Fit linear regression to estimate convergence order
coeffs = np.polyfit(log_h, log_err, 1)
convergence_order = coeffs[0]

# Plot theoretical convergence lines
h_theory = np.logspace(np.log10(grid_sizes.min()), np.log10(grid_sizes.max()), 100)
C1 = errors[-1] / (grid_sizes[-1]**1)  # Adjust constant
C2 = errors[-1] / (grid_sizes[-1]**2)  # Adjust constant

ax1.loglog(h_theory, C1 * h_theory**1, 'r--', alpha=0.7, label='O(h) - First Order')
ax1.loglog(h_theory, C2 * h_theory**2, 'g--', alpha=0.7, label='O(h²) - Second Order')

ax1.set_xlabel('Grid Spacing (h)', fontsize=12)
ax1.set_ylabel('Max Absolute Error', fontsize=12)
ax1.set_title(f'Grid Convergence: {option_type.capitalize()} Option\n' +
             f'Estimated Convergence Order: {convergence_order:.2f}', fontsize=14)
ax1.grid(True, alpha=0.3)
ax1.legend()

# Plot 2: Semi-log plot (as in original)
ax2.plot(np.log(grid_sizes), errors, 'bo-', linewidth=2, markersize=6, label='CN FDM Errors')
ax2.set_xlabel('log(Grid Spacing)', fontsize=12)
ax2.set_ylabel('Max Absolute Error', fontsize=12)
ax2.set_title(f'Semi-log Convergence Plot\n{option_type.capitalize()} Option', fontsize=14)
ax2.grid(True, alpha=0.3)
ax2.legend()

plt.tight_layout()
plt.show()

# === Detailed Analysis ===
print(f"\n{'='*70}")
print("CONVERGENCE ANALYSIS RESULTS")
print("="*70)

print(f"Grid Convergence Summary:")
print(f"{'Grid Points':<12} {'Grid Spacing':<15} {'Max Error':<15} {'Error Reduction':<15}")
print("-" * 65)

for i, (n_pts, h, err) in enumerate(zip(num_points, grid_sizes, errors)):
    if i == 0:
        reduction = "—"
    else:
        reduction = f"{errors[i-1]/err:.2f}x"

    print(f"{n_pts:<12} {h:<15.6f} {err:<15.2e} {reduction:<15}")

# Convergence order analysis
print(f"\nConvergence Order Analysis:")
print(f"  Estimated Order: {convergence_order:.3f}")

if abs(convergence_order + 2) < 0.3:
    print(f"  ✅ Excellent! Close to theoretical O(h²) for CN method")
elif abs(convergence_order + 1.5) < 0.5:
    print(f"  ✅ Good! Between first and second order convergence")
elif abs(convergence_order + 1) < 0.3:
    print(f"  ⚠️  First order convergence - check implementation")
else:
    print(f"  ⚠️  Unusual convergence pattern - investigate further")

# Error reduction analysis
error_reductions = [errors[i]/errors[i+1] for i in range(len(errors)-1)]
avg_reduction = np.mean(error_reductions)

print(f"\nError Reduction Analysis:")
print(f"  Average error reduction per refinement: {avg_reduction:.2f}x")
print(f"  Theoretical expectation for O(h²): ~4x")

if avg_reduction > 3.5:
    print(f"  ✅ Excellent convergence behavior")
elif avg_reduction > 2.5:
    print(f"  ✅ Good convergence behavior")
else:
    print(f"  ⚠️  Slower than expected convergence")

# Practical recommendations
print(f"\nPractical Recommendations:")
best_efficiency_idx = np.argmax(1/errors / num_points)  # Error per computational cost
best_grid = num_points[best_efficiency_idx]

print(f"  Most efficient grid size: {best_grid} points")
print(f"  Error at this size: {errors[best_efficiency_idx]:.2e}")

# Find grid size for specific accuracy targets
target_errors = [1e-3, 1e-4, 1e-5]
for target in target_errors:
    if errors.min() <= target:
        idx = np.where(errors <= target)[0][0]
        required_points = num_points[idx]
        print(f"  For {target:.0e} accuracy: {required_points} points minimum")

# Method characteristics
print(f"\nCrank-Nicolson Method Characteristics:")
print(f"  ✓ Unconditionally stable")
print(f"  ✓ Second-order accurate in time and space (theoretical)")
print(f"  ✓ Good for smooth solutions like European options")
print(f"  ⚠ Requires solving linear systems")

# Comparison insight
if option_type == "call":
    print(f"\nCall Option Specific Notes:")
    print(f"  • Smooth payoff function aids convergence")
    print(f"  • No boundary singularities")
    print(f"  • Uniform grid typically adequate")
else:
    print(f"\nPut Option Specific Notes:")
    print(f"  • Steeper gradients near S=0 may affect convergence")
    print(f"  • Consider log-space methods for better accuracy")
    print(f"  • Boundary condition at S=0 more challenging")

print(f"\nComputational Efficiency:")
finest_grid = num_points[-1]
coarsest_grid = num_points[0]
comp_ratio = (finest_grid)**2 / (coarsest_grid)**2  # Roughly proportional to computational cost

print(f"  Computational cost ratio (finest/coarsest): ~{comp_ratio:.1f}x")
print(f"  Error improvement ratio: {errors[0]/errors[-1]:.1f}x")
print(f"  Efficiency gain: {(errors[0]/errors[-1])/comp_ratio:.2f}x error reduction per unit cost")

print("="*70)

```

Exercises

Exercise 1. Describe a spatial convergence study for the CN scheme. What should you plot, and what slope indicates second-order convergence?

Solution to Exercise 1

Fix \(N_t\) large enough that time error is negligible. Vary \(N_S\) and compute the error \(e(N_S) = |V_{\text{num}} - V_{\text{exact}}|\) at a reference point (e.g., \(S = S_0\)). Plot \(\log(e)\) vs \(\log(\Delta S)\). A slope of 2 indicates \(O(\Delta S^2)\) convergence, confirming second-order spatial accuracy.


Exercise 2. Why must the time step be sufficiently small when testing spatial convergence?

Solution to Exercise 2

If \(\Delta t\) is too large, the time discretization error dominates the spatial error, and the observed convergence rate reflects \(O(\Delta t^2)\) rather than \(O(\Delta S^2)\). To isolate spatial convergence, use \(\Delta t \ll \Delta S^2\) so that the time error is negligible.


Exercise 3. What convergence rate do you expect near the strike price \(S = K\) where the payoff has a kink?

Solution to Exercise 3

Near \(S = K\), the payoff is non-smooth, and the solution gradient changes rapidly. The local convergence rate may degrade to first order or worse. Away from \(S = K\), the full second-order rate is observed. Rannacher smoothing or adaptive grids can restore second-order convergence globally.


Exercise 4. If doubling \(N_S\) from 100 to 200 reduces the error from \(4 \times 10^{-4}\) to \(1 \times 10^{-4}\), what is the observed convergence order?

Solution to Exercise 4

\(p = \log(e_1/e_2)/\log(h_1/h_2) = \log(4)/\log(2) = 2\). This confirms second-order spatial convergence, consistent with the CN scheme.