Skip to content

American Call

Background

Black Scholes Cn Amer Call

Educational script demonstrating black scholes cn amer call concepts.


What This Code Demonstrates

  • Parameters ===

Code

```python """ Black Scholes Cn Amer Call

Educational script demonstrating black scholes cn amer call concepts. """

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

black_scholes_RUN_CN_SCHEME_FOR_AMERICAN_CALL.py

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

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

=== Parameters ===

if name == "main": S0 = 100 K = 100 T = 1.0 r = 0.05 sigma = 0.2 q = 0 S_min = 0 S_min_log = 1e-3 # For log-space FD S_max = 300 # S_max should be bigger than your S_max of interest if use log space M = 100 # Grid points → NS = NX = M + 1 option_type = "call"

print(f"\n{'='*70}")
print("NUMERICAL METHODS COMPARISON")
print("="*70)
print(f"Option Type: {option_type.upper()}")
print(f"Stock Price (S0): ${S0}")
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"Dividend Yield: {q:.1%}")
print(f"Grid Points: {M+1}")
print(f"Stock Price Range: ${S_min} - ${S_max}")
print(f"Log-space Min: ${S_min_log}")

# === Instantiate Black-Scholes model using wrapper ===
bs_model = bs.BlackScholes(S0, K, T, r, sigma, q)

print(f"\nCalculating option prices using different methods...")

# === Run Crank-Nicolson FDM in Original Space ===
print("  Running CN FDM in original space...")
S_orig, V_orig = bs_model.price_numerical(
    method="cn", 
    option_type=option_type, 
    Smin=S_min, 
    Smax=S_max, 
    NS=M+1, 
    early_exercise=True
)

# === Run Crank-Nicolson FDM in Log-Price Space ===
print("  Running CN FDM in log-price space...")
S_log, V_log = bs_model.price_numerical(
    method="cn_log", 
    option_type=option_type, 
    Smin=S_min_log, 
    Smax=S_max, 
    NX=M+1, 
    early_exercise=True
)

# === Analytical Black-Scholes Price (Vectorized) ===
print("  Calculating analytical Black-Scholes prices...")
S_all = np.union1d(S_orig, S_log)
S_all.sort()
S_all_safe = np.maximum(S_all, 1e-10)  # Avoid log(0)

# Use vectorized utility function for efficiency
if option_type == "call":
    V_exact_all = bs.bs_call_price(S_all_safe, K, T, r, sigma, q)
else:
    V_exact_all = bs.bs_put_price(S_all_safe, K, T, r, sigma, q)

# === Plot Comparison ===
print("  Generating comparison plot...")
fig, ax = plt.subplots(figsize=(12, 6))

ax.plot(S_orig, V_orig, label='CN FDM (Original Space)', linewidth=8, alpha=0.3, color='blue')
ax.plot(S_log, V_log, label='CN FDM (Log Space)', linewidth=4, alpha=0.8, color='green')
ax.plot(S_all, V_exact_all, 'r--', label='Black-Scholes Analytical', linewidth=2)

# Add reference lines
ax.axvline(x=K, color='gray', linestyle=':', alpha=0.7, label=f'Strike = ${K}')
ax.axvline(x=S0, color='orange', linestyle=':', alpha=0.7, label=f'Current Price = ${S0}')

# Calculate intrinsic value for reference
if option_type == "call":
    intrinsic = np.maximum(S_all - K, 0)
else:
    intrinsic = np.maximum(K - S_all, 0)
ax.plot(S_all, intrinsic, 'k:', alpha=0.5, label='Intrinsic Value')

ax.set_xlabel('Stock Price ($)', fontsize=12)
ax.set_ylabel('Option Value ($)', fontsize=12)
ax.set_title(f'American {option_type.capitalize()} Option: CN FDM Comparison\n' +
            f'Original Space vs Log-Space vs Analytical', fontsize=14)
ax.grid(True, alpha=0.3)
ax.legend()

# Clean up plot appearance
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

plt.tight_layout()
plt.show()

# === Compute Max Errors ===
print(f"\nError Analysis:")

# Get exact values at grid points
if option_type == "call":
    V_exact_orig = bs.bs_call_price(np.maximum(S_orig, 1e-10), K, T, r, sigma, q)
    V_exact_log = bs.bs_call_price(np.maximum(S_log, 1e-10), K, T, r, sigma, q)
else:
    V_exact_orig = bs.bs_put_price(np.maximum(S_orig, 1e-10), K, T, r, sigma, q)
    V_exact_log = bs.bs_put_price(np.maximum(S_log, 1e-10), K, T, r, sigma, q)

error_orig = np.max(np.abs(V_orig - V_exact_orig))
error_log = np.max(np.abs(V_log - V_exact_log))

print(f"  Max absolute error (original space): ${error_orig:.6f}")
print(f"  Max absolute error (log space):      ${error_log:.6f}")

# Relative errors
rel_error_orig = error_orig / np.mean(V_exact_orig) * 100
rel_error_log = error_log / np.mean(V_exact_log) * 100

print(f"  Max relative error (original space): {rel_error_orig:.4f}%")
print(f"  Max relative error (log space):      {rel_error_log:.4f}%")

# Determine which method is more accurate
if error_log < error_orig:
    print(f"  ✓ Log-space method is more accurate by a factor of {error_orig/error_log:.2f}")
else:
    print(f"  ✓ Original-space method is more accurate by a factor of {error_log/error_orig:.2f}")

# === Price at Current Stock Price ===
print(f"\nPrices at Current Stock Price (S = ${S0}):")

# Analytical price
analytical_call, analytical_put = bs_model.price_analytical()
analytical_price = analytical_call if option_type == "call" else analytical_put

# Find closest grid points
idx_orig = np.argmin(np.abs(S_orig - S0))
idx_log = np.argmin(np.abs(S_log - S0))

price_orig = V_orig[idx_orig]
price_log = V_log[idx_log]

print(f"  Analytical Price:     ${analytical_price:.6f}")
print(f"  CN Original Space:    ${price_orig:.6f} (error: ${abs(price_orig - analytical_price):.6f})")
print(f"  CN Log Space:         ${price_log:.6f} (error: ${abs(price_log - analytical_price):.6f})")

# === Demonstrate other wrapper features ===
print(f"\n{'='*70}")
print("ADDITIONAL WRAPPER FEATURES")
print("="*70)

# Monte Carlo comparison
print(f"Comparing with Monte Carlo simulation...")
mc_results = bs_model.price_monte_carlo(num_paths=100000, plot_histogram=False)
mc_price = mc_results[0] if option_type == "call" else mc_results[1]
mc_std = mc_results[2] if option_type == "call" else mc_results[3]

print(f"  Monte Carlo Price:    ${mc_price:.6f} ± ${mc_std/np.sqrt(100000):.6f}")

# Method comparison using wrapper
comparison = bs_model.compare_methods(option_type=option_type, numerical_method='cn')
print(f"\nComprehensive Method Comparison:")
print(f"  Analytical:           ${comparison['analytical']:.6f}")
print(f"  Monte Carlo:          ${comparison['monte_carlo']['price']:.6f}")
print(f"  Numerical (CN):       ${comparison['numerical']:.6f}")

print(f"\nMethod Differences:")
print(f"  MC vs Analytical:     ${comparison['differences']['mc_vs_analytical']:.6f}")
print(f"  Numerical vs Analytical: ${comparison['differences']['numerical_vs_analytical']:.6f}")

# Greeks for context
greeks = bs_model.calculate_greeks()
print(f"\nOption Greeks (for reference):")
if option_type == "call":
    print(f"  Delta:                {greeks['delta_call']:.4f}")
else:
    print(f"  Delta:                {greeks['delta_put']:.4f}")
print(f"  Gamma:                {greeks['gamma']:.6f}")
print(f"  Vega:                 {greeks['vega']:.4f}")

print(f"\n{'='*70}")
print("SUMMARY")
print("="*70)
print(f"✅ Both numerical methods successfully price the American {option_type}")
print(f"✅ Log-space typically handles extreme stock prices better")
print(f"✅ Original-space may be more intuitive for grid setup")
print(f"✅ Analytical price provides excellent benchmark")
print(f"✅ Monte Carlo provides independent verification")
print("="*70)

```

Exercises

Exercise 1. Write the Crank-Nicolson scheme for the Black-Scholes PDE applied to an American call. How is the early exercise constraint enforced?

Solution to Exercise 1

The CN scheme solves \(A\mathbf{V}^j = B\mathbf{V}^{j+1}\) at each time step (backward in time). For an American call, after solving the linear system, enforce \(V_i^j = \max(V_i^j, S_i - K)\) at every grid point. This projection ensures the option value never falls below the intrinsic value.


Exercise 2. For a non-dividend-paying stock, is early exercise of an American call ever optimal? How does this affect the CN solver?

Solution to Exercise 2

No, early exercise is never optimal for a non-dividend-paying American call. The call value always exceeds \(S - K\) because \(C \ge S - Ke^{-r(T-t)} > S - K\) for \(r > 0\). The CN solver produces the European call price; the early exercise constraint is never binding.


Exercise 3. Describe the boundary conditions used for the American call in the finite difference grid.

Solution to Exercise 3

At \(S = 0\): \(V(0,t) = 0\) (worthless). At \(S = S_{\max}\): \(V(S_{\max}, t) = S_{\max} - Ke^{-r(T-t)}\) (deep ITM approximation). At \(t = T\): \(V(S,T) = \max(S - K, 0)\) (payoff). These Dirichlet conditions close the system.


Exercise 4. How does the Crank-Nicolson scheme handle the non-smooth payoff at \(S = K\)? What artifact can arise?

Solution to Exercise 4

The payoff \(\max(S-K, 0)\) has a kink at \(S = K\). CN can produce spurious oscillations near this kink due to its second-order accuracy attempting to resolve a non-smooth function. Rannacher time stepping (using 2--4 fully implicit steps at the start) smooths these oscillations before switching to CN for the remaining steps.