American Call¶
Background¶
Black Scholes Implicit Amer Call
Educational script demonstrating black scholes implicit amer call concepts.
What This Code Demonstrates¶
- Parameters ===
Code¶
```python """ Black Scholes Implicit Amer Call
Educational script demonstrating black scholes implicit amer call concepts. """
============================================================================¶
black_scholes_RUN_IMPLICIT_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("IMPLICIT FINITE DIFFERENCE ANALYSIS")
print("="*70)
print(f"American {option_type.upper()} Option Analysis")
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"Grid Points: {M+1}")
print(f"Price Range: ${S_min} - ${S_max}")
# === Instantiate Black-Scholes model using wrapper ===
bs_model = bs.BlackScholes(S0, K, T, r, sigma, q)
print(f"\nCalculating option prices...")
# === Run Implicit FDM in Original Space ===
print(f" Running Implicit FDM (Original Space)...")
S_orig, V_orig = bs_model.price_numerical(
method="implicit",
option_type=option_type,
Smin=S_min,
Smax=S_max,
NS=M+1,
early_exercise=True
)
# === Run Implicit FDM in Log-Price Space ===
print(f" Running Implicit FDM (Log-Price Space)...")
S_log, V_log = bs_model.price_numerical(
method="implicit_log",
option_type=option_type,
Smin=S_min_log,
Smax=S_max,
NX=M+1,
early_exercise=True
)
# === Analytical Black-Scholes Price (Vectorized) ===
print(f" Computing analytical benchmark...")
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 functions
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)
# === Enhanced Plot Comparison ===
print(f" Generating comparison plot...")
fig, ax = plt.subplots(figsize=(12, 6))
# Plot the numerical solutions
ax.plot(S_orig, V_orig, label='Implicit FDM (Original Space)',
linewidth=8, alpha=0.3, color='blue')
ax.plot(S_log, V_log, label='Implicit FDM (Log Space)',
linewidth=4, alpha=0.8, color='green')
# Plot analytical European solution for reference
ax.plot(S_all, V_exact_all, 'r--', label='European Analytical (BS)', 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 = ${S0}')
# Plot intrinsic value
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, linewidth=2, label='Intrinsic Value')
# Formatting
ax.set_xlabel('Stock Price ($)', fontsize=12)
ax.set_ylabel('Option Value ($)', fontsize=12)
ax.set_title(f'American {option_type.capitalize()} Option: Implicit FDM Analysis\n' +
f'Original vs Log-Space vs European Benchmark', fontsize=14)
ax.grid(True, alpha=0.3)
ax.legend(fontsize=10)
# Clean appearance
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show()
# === Error Analysis (American vs European) ===
print(f"\nAmerican vs European Analysis:")
# Get European prices 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)
# Calculate early exercise premiums
premium_orig = V_orig - V_exact_orig
premium_log = V_log - V_exact_log
# Error analysis
error_orig = np.max(np.abs(V_orig - V_exact_orig))
error_log = np.max(np.abs(V_log - V_exact_log))
print(f" Early Exercise Premium Analysis:")
print(f" Original Space:")
print(f" Max Premium: ${np.max(premium_orig):.6f}")
print(f" Mean Premium: ${np.mean(premium_orig):.6f}")
print(f" Max Error vs European: ${error_orig:.6f}")
print(f" Log Space:")
print(f" Max Premium: ${np.max(premium_log):.6f}")
print(f" Mean Premium: ${np.mean(premium_log):.6f}")
print(f" Max Error vs European: ${error_log:.6f}")
# Method comparison
method_diff = np.max(np.abs(V_orig - np.interp(S_orig, S_log, V_log)))
print(f"\n Method Agreement:")
print(f" Max difference between methods: ${method_diff:.6f}")
if method_diff < 0.001:
print(f" ✅ Excellent agreement between implicit methods")
elif method_diff < 0.01:
print(f" ✅ Good agreement between implicit methods")
else:
print(f" ⚠️ Consider higher grid resolution")
# === Detailed Price Analysis ===
print(f"\nPrice Analysis at Key Points:")
print(f"{'Stock':<8} {'European':<10} {'Amer Orig':<11} {'Amer Log':<11} {'Premium':<10}")
print("-" * 60)
key_prices = [60, 80, 100, 120, 150] if option_type == "call" else [40, 60, 80, 100, 120]
for S_test in key_prices:
# European benchmark
if option_type == "call":
euro_price = bs.bs_call_price(S_test, K, T, r, sigma, q)
else:
euro_price = bs.bs_put_price(S_test, K, T, r, sigma, q)
# American prices
idx_orig = np.argmin(np.abs(S_orig - S_test))
idx_log = np.argmin(np.abs(S_log - S_test))
amer_orig = V_orig[idx_orig]
amer_log = V_log[idx_log]
avg_premium = (amer_orig + amer_log) / 2 - euro_price
print(f"${S_test:<7.0f} ${euro_price:<9.4f} ${amer_orig:<10.4f} "
f"${amer_log:<10.4f} ${avg_premium:<9.5f}")
# === Current Stock Price Analysis ===
print(f"\nAt Current Stock Price (S = ${S0}):")
# Analytical benchmark
analytical_call, analytical_put = bs_model.price_analytical()
european_current = analytical_call if option_type == "call" else analytical_put
# American prices
idx_orig_s0 = np.argmin(np.abs(S_orig - S0))
idx_log_s0 = np.argmin(np.abs(S_log - S0))
american_orig_s0 = V_orig[idx_orig_s0]
american_log_s0 = V_log[idx_log_s0]
print(f" European Price: ${european_current:.6f}")
print(f" American (Orig): ${american_orig_s0:.6f}")
print(f" American (Log): ${american_log_s0:.6f}")
print(f" Early Ex Premium: ${american_orig_s0 - european_current:.6f}")
# === Summary ===
print(f"\n{'='*70}")
print("SUMMARY")
print("="*70)
print(f"✅ Implicit Method Results:")
print(f" • Original Space Max Premium: ${np.max(premium_orig):.6f}")
print(f" • Log Space Max Premium: ${np.max(premium_log):.6f}")
print(f" • Method Agreement: ${method_diff:.6f}")
if option_type == "call" and q == 0:
print(f"\n💡 Call Option Insights:")
print(f" • No dividends: Early exercise premium is minimal")
print(f" • American ≈ European for most practical purposes")
print(f" • Premium appears mainly for deep ITM options")
else:
print(f"\n💡 Put Option Insights:")
print(f" • Significant early exercise value expected")
print(f" • Premium increases for ITM puts")
print(f" • Log-space handles S→0 boundary better")
print(f"\n🎯 Computational Notes:")
print(f" • Implicit methods: Unconditionally stable")
print(f" • Can use larger time steps than explicit methods")
print(f" • Linear system solved at each time step")
print(f" • Early exercise via projection constraint")
print(f"\n⚡ Method Recommendations:")
if np.max(premium_log) > np.max(premium_orig):
print(f" • Log-space shows higher premiums (more accurate)")
print(f" • Prefer log-space for wide price ranges")
else:
print(f" • Both methods show similar premiums")
print(f" • Original space adequate for moderate ranges")
print(f" • Grid resolution affects early exercise accuracy")
print(f" • Consider adaptive mesh refinement")
print("="*70)
```
Exercises¶
Exercise 1. Explain why an American call on a non-dividend-paying stock (\(q = 0\)) should have zero early exercise premium. Under what dividend yield \(q\) does early exercise become optimal?
Solution to Exercise 1
For \(q = 0\), the call value satisfies \(C \geq S - K e^{-r(T-t)} > S - K\) (intrinsic value), so the option is always worth more alive. Early exercise forfeits the time value of the strike payment and downside insurance. When \(q > 0\), holding the option means forgoing dividends. Early exercise becomes optimal roughly when the dividend yield exceeds the risk-free rate, \(q > r\), or more precisely when the present value of lost dividends exceeds the remaining time value.
Exercise 2. In the implicit FDM with early exercise, the projection \(V_{i,j} = \max(V_{i,j}^{\text{implicit}}, \text{payoff}(S_i))\) is applied after solving the linear system. Explain why this preserves the tridiagonal structure.
Solution to Exercise 2
The projection is applied element-wise after solving \(A\mathbf{V}^j = \mathbf{b}^j\). The tridiagonal system is solved first without modification, then each entry is compared to the intrinsic value. This two-step approach (solve then project) preserves the matrix structure. The free boundary \(S^*(t_j)\) emerges as the stock price where the projection first activates.
Exercise 3. Compare unconditional stability (implicit) versus conditional stability (explicit). With \(M = 100\), \(N = 100\), \(T = 1\), which method gives valid results?
Solution to Exercise 3
With \(M = 100\), \(S_{\max} = 300\), \(\sigma = 0.2\): the CFL condition requires \(\Delta t \leq 0.0025\), meaning \(N \geq 400\) for the explicit method. With \(N = 100\), the explicit method violates stability and produces oscillatory solutions. The implicit method is unconditionally stable and gives a valid (though coarser) approximation with \(\Delta t = 0.01\). Only the implicit method produces meaningful results here.
Exercise 4. The Thomas algorithm solves a tridiagonal system in \(O(M)\) operations. What is the total complexity of the implicit FDM with \(N\) time steps and \(M\) spatial points? Compare with Crank-Nicolson.
Solution to Exercise 4
Total complexity: \(O(N \times M)\) for both implicit and Crank-Nicolson, since each time step requires one \(O(M)\) tridiagonal solve plus \(O(M)\) projection. The difference is accuracy: implicit is \(O(\Delta t)\) in time while Crank-Nicolson is \(O((\Delta t)^2)\). Both are \(O((\Delta S)^2)\) in space. Crank-Nicolson achieves higher accuracy for the same cost, though it may produce oscillations near the American exercise boundary.