Solution: Risk-Neutral Pricing of a European Call Under GBM
Part 1 — Monte Carlo
# Python
import numpy as np
rng = np.random.default_rng(2026)
S0, K, r, sigma, T = 100.0, 105.0, 0.04, 0.30, 0.5
N = 1_000_000
Z = rng.standard_normal(N)
S_T = S0 * np.exp((r - 0.5 * sigma**2) * T + sigma * np.sqrt(T) * Z)
discounted_payoffs = np.exp(-r * T) * np.maximum(S_T - K, 0.0)
C_hat = discounted_payoffs.mean()
se = discounted_payoffs.std(ddof=1) / np.sqrt(N)
ci_low, ci_high = C_hat - 1.96 * se, C_hat + 1.96 * se
print(f"MC price: {C_hat:.4f}")
print(f"95% CI: [{ci_low:.4f}, {ci_high:.4f}]")
# MC price: 5.6775
# 95% CI: [5.6541, 5.7010]Estimated price: $5.68 with a 95% CI of about $0.02. The standard error is small because we used ; for production pricing of vanilla options, closed-form is preferred, but the Monte Carlo infrastructure generalises to exotic payoffs.
Part 2 — Black-Scholes closed form
# Python
from scipy.stats import norm
d1 = (np.log(S0 / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
d2 = d1 - sigma * np.sqrt(T)
C_bs = S0 * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
print(f"Black-Scholes price: {C_bs:.4f}")
# Black-Scholes price: 5.6688The closed-form value is $5.6688, comfortably inside the Monte Carlo 95% confidence interval . The two approaches agree within simulation noise.
Part 3 — Exercise probability
# Python
mc_exercise_prob = (S_T > K).mean()
cf_exercise_prob = norm.cdf(d2)
print(f"MC Q(S_T > K): {mc_exercise_prob:.4f}")
print(f"Theoretical Phi(d_2): {cf_exercise_prob:.4f}")
# MC Q(S_T > K): 0.4260
# Theoretical Phi(d_2): 0.4261Match to four decimal places. The exercise probability under is — one of the two distinct "probabilities" that appear in the Black-Scholes formula. (The other, , is the delta and has a different interpretation: it is the probability of exercise under an alternative numeraire called the stock-price measure, not under .)
Part 4 — Using the real-world drift
Replacing with :
# Python
mu_real = 0.10
Z = rng.standard_normal(N)
S_T_real = S0 * np.exp((mu_real - 0.5 * sigma**2) * T + sigma * np.sqrt(T) * Z)
wrong_price = np.exp(-r * T) * np.maximum(S_T_real - K, 0.0).mean()
print(f"Wrong price (using mu=0.10 and discount r=0.04): {wrong_price:.4f}")
# Wrong price (using mu=0.10 and discount r=0.04): 7.3802The price jumps to $7.38 — an overstatement of $1.71, or 30% above the correct value. The option is not worth $7.38. The correct discounted expected payoff must be computed under the risk-neutral measure, not the real-world measure. Using the real-world drift but discounting at the risk-free rate is incoherent: it double-counts the risk premium by letting the stock drift upward at while implicitly assuming a hedger can discount at .
The Fundamental Theorem of Asset Pricing guarantees that in a no-arbitrage market there is a risk-neutral measure under which discounted asset prices are martingales. The option's fair value is the -expectation of the discounted payoff — a statement that does not depend on at all. This measure-independence is a huge practical feature: estimating from historical data is notoriously noisy, but option prices do not require it.
Takeaways
- Monte Carlo is unbiased; Black-Scholes is closed-form. For vanilla options, always use Black-Scholes. Monte Carlo is for exotic payoffs where no closed form exists, or for model extensions where the terminal distribution is not log-normal.
- Closed-form and Monte Carlo should agree within CI. Mismatch signals a simulation bug (wrong discount rate, Euler scheme error, seed reuse, biased estimator).
- is the risk-neutral exercise probability. It is not the real-world probability of expiring in the money — those two numbers differ by the same measure-change that moves to .
- Never mix real-world drift with risk-free discounting. The fair price is , not . The latter is arbitrarily wrong and scales with .