import numpy as np import matplotlib.pyplot as plt from scipy.stats import norm # ----------------------------- # Black-Scholes European Put # ----------------------------- def european_put_black_scholes(S0, K, r, sigma, T): d1 = (np.log(S0/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T)) d2 = d1 - sigma*np.sqrt(T) return K*np.exp(-r*T)*norm.cdf(-d2) - S0*norm.cdf(-d1) # ----------------------------- # GBM Simulation # ----------------------------- def simulate_gbm(S0, r, sigma, T, M, N, seed=None): if seed is not None: np.random.seed(seed) dt = T / N # Vectorized simulation for speed S = np.zeros((M, N + 1)) S[:, 0] = S0 for t in range(1, N + 1): z = np.random.standard_normal(M) S[:, t] = S[:, t-1] * np.exp((r - 0.5 * sigma**2) * dt + sigma * np.sqrt(dt) * z) return S # ----------------------------- # Longstaff-Schwartz Monte Carlo (Fixed) # ----------------------------- def lsm_american_put(S0, K, r, sigma, T, M=100_000, N=50, basis_deg=2, seed=42, return_boundary=False): S = simulate_gbm(S0, r, sigma, T, M, N, seed) dt = T / N df = np.exp(-r * dt) # Immediate exercise value (payoff) at each step payoff = np.maximum(K - S, 0) # V stores the value of the option at each path # Initialize with the payoff at maturity V = payoff[:, -1] boundary = [] # Backward induction for t in range(N - 1, 0, -1): # Identify In-The-Money paths itm = payoff[:, t] > 0 # Default: option value at t is just the discounted value from t+1 V = V * df if np.any(itm): X = S[itm, t] Y = V[itm] # These are already discounted from t+1 # Regression: Estimate Continuation Value coeffs = np.polyfit(X, Y, basis_deg) continuation_value = np.polyval(coeffs, X) # Exercise if payoff > estimated continuation value exercise = payoff[itm, t] > continuation_value # Update V for paths where we exercise # Get the indices of the ITM paths that should exercise itm_indices = np.where(itm)[0] exercise_indices = itm_indices[exercise] V[exercise_indices] = payoff[exercise_indices, t] # Boundary: The highest stock price at which we still exercise if len(exercise_indices) > 0: boundary.append((t * dt, np.max(S[exercise_indices, t]))) else: boundary.append((t * dt, np.nan)) else: boundary.append((t * dt, np.nan)) # Final price is the average of discounted values at t=1 price = np.mean(V * df) if return_boundary: return price, boundary[::-1] return price # ----------------------------- # QuantLib American Put (v1.18) # ----------------------------- def quantlib_american_put(S0, K, r, sigma, T): import QuantLib as ql today = ql.Date().todaysDate() ql.Settings.instance().evaluationDate = today maturity = today + int(T*365) payoff = ql.PlainVanillaPayoff(ql.Option.Put, K) exercise = ql.AmericanExercise(today, maturity) spot = ql.QuoteHandle(ql.SimpleQuote(S0)) dividend_curve = ql.YieldTermStructureHandle(ql.FlatForward(today, 0.0, ql.Actual365Fixed())) riskfree_curve = ql.YieldTermStructureHandle(ql.FlatForward(today, r, ql.Actual365Fixed())) vol_curve = ql.BlackVolTermStructureHandle(ql.BlackConstantVol(today, ql.NullCalendar(), sigma, ql.Actual365Fixed())) process = ql.BlackScholesMertonProcess(spot, dividend_curve, riskfree_curve, vol_curve) engine = ql.FdBlackScholesVanillaEngine(process, 200, 400) option = ql.VanillaOption(payoff, exercise) option.setPricingEngine(engine) return option.NPV() # ----------------------------- # Main script # ----------------------------- if __name__ == "__main__": # Parameters S0, K, r, sigma, T = 100, 100, 0.05, 0.2, 1.0 M, N = 200_000, 50 # European Put eur_bs = european_put_black_scholes(S0, K, r, sigma, T) print(f"European Put (BS): {eur_bs:.4f}") # LSM American Put lsm_price, boundary = lsm_american_put(S0, K, r, sigma, T, M, N, return_boundary=True) print(f"LSM American Put (M={M}): {lsm_price:.4f}") # QuantLib American Put ql_price = quantlib_american_put(S0, K, r, sigma, T) print(f"QuantLib American Put: {ql_price:.4f}") print(f"Lower bound (immediate exercise): {max(K-S0,0):.4f}") # Plot Exercise Boundary times = [b[0] for b in boundary] boundaries = [b[1] for b in boundary] plt.figure(figsize=(8,5)) plt.plot(times, boundaries, color='orange', label="LSM Exercise Boundary") plt.axhline(K, color='red', linestyle='--', label="Strike Price") plt.xlabel("Time to Maturity") plt.ylabel("Stock Price for Exercise") plt.title("American Put LSM Exercise Boundary") plt.gca().invert_xaxis() plt.legend() plt.grid(True) plt.show()