From Binomial Trees to Monte Carlo for Exotic Options & Portfolio Risk
Paris Dauphine University-PSL
Under risk‑neutral measure \(\mathbb{Q}\) for a dividend yield \(q\) and risk‑free rate \(r\):
\[\frac{dS_t}{S_t} = (r - q)\,dt + \sigma \, dW_t\]
Discretization (Euler / exact for GBM) over \(\Delta t\):
\[S_{t+\Delta t} = S_t \exp\Big( (r - q - \tfrac{1}{2}\sigma^2)\,\Delta t + \sigma \sqrt{\Delta t} Z \Big), \quad Z \sim \mathcal{N}(0,1)\]
In VBA we approximate Normal via WorksheetFunction.Norm_S_Inv(Rnd) or a Box–Muller.
Let payoff be function of whole path: \(f(S_0, S_1, \dots, S_N)\). Risk‑neutral price:
\[V_0 = e^{-rT} \, \mathbb{E}^Q \big[ f(\text{path}) \big] \approx e^{-rT} \, \frac{1}{M} \sum_{m=1}^M f^{(m)}\]
Monte Carlo Error (Std Err):
\[\text{SE} = e^{-rT} \; \frac{\hat{\sigma}_f}{\sqrt{M}}\]
where \(\hat{\sigma}_f\) is sample std of discounted payoffs.
Confidence Interval (95%): \(\hat{V} \pm 1.96\, \text{SE}\).
We pre‑allocate a 2D array \(\text{Paths}(0..N, 1..M)\) or simulate on the fly (memory ↔︎ flexibility trade‑off).
Pseudo:
Avoid using Cells in inner loops (slow). Use arrays + write out final results.
Public Function MCEuroCall(S0 As Double, K As Double, r As Double, q As Double, _
sigma As Double, T As Double, N As Long, M As Long) As Double
Dim dt As Double: dt = T / N
Dim disc As Double: disc = Exp(-r * T)
Dim drift As Double: drift = (r - q - 0.5 * sigma ^ 2) * dt
Dim volStep As Double: volStep = sigma * Sqr(dt)
Dim i As Long, j As Long
Dim S As Double, Z As Double
Dim payoffSum As Double, payoffSq As Double, payoff As Double
For i = 1 To M
S = S0
For j = 1 To N
Z = StdNormal()
S = S * Exp(drift + volStep * Z)
Next j
payoff = Application.Max(S - K, 0#)
payoffSum = payoffSum + payoff
payoffSq = payoffSq + payoff * payoff
Next i
Dim meanPayoff As Double
meanPayoff = payoffSum / M
MCEuroCall = disc * meanPayoff
End FunctionUse to validate vs closed‑form Black–Scholes for quality control.
Option Explicit
'========================
' Black–Scholes (Call)
'========================
Public Function BlackScholesCall( _
ByVal S0 As Double, _
ByVal K As Double, _
ByVal r As Double, _
ByVal q As Double, _
ByVal sigma As Double, _
ByVal T As Double) As Double
Dim discR As Double, discQ As Double
Dim d1 As Double, d2 As Double
' Edge cases
If T <= 0# Then
BlackScholesCall = Application.Max(S0 - K, 0#)
Exit Function
End If
If sigma <= 0# Then
' Deterministic forward with zero vol
discR = Exp(-r * T)
discQ = Exp(-q * T)
BlackScholesCall = Application.Max(S0 * discQ - K * discR, 0#)
Exit Function
End If
discR = Exp(-r * T)
discQ = Exp(-q * T)
d1 = (Log(S0 / K) + (r - q + 0.5# * sigma * sigma) * T) / (sigma * Sqr(T))
d2 = d1 - sigma * Sqr(T)
BlackScholesCall = S0 * discQ * CND(d1) - K * discR * CND(d2)
End Function
'========================
' Standard normal CDF
' (Abramowitz–Stegun 7.1.26 approximation)
' Max abs error ~ 7.5e-8
'========================
Private Function CND(ByVal x As Double) As Double
Const p As Double = 0.2316419
Const b1 As Double = 0.319381530
Const b2 As Double = -0.356563782
Const b3 As Double = 1.781477937
Const b4 As Double = -1.821255978
Const b5 As Double = 1.330274429
Dim t As Double, pdf As Double, k As Double, c As Double
Dim ax As Double
ax = Abs(x)
t = 1# / (1# + p * ax)
pdf = Exp(-0.5# * ax * ax) / Sqr(2# * 3.1415926535897931#)
k = (((((b5 * t + b4) * t) + b3) * t + b2) * t + b1) * t
c = 1# - pdf * k
If x >= 0# Then
CND = c
Else
CND = 1# - c
End If
End FunctionPayoff: \(\max(A-K,0)\) with arithmetic average \(A = \tfrac{1}{N} \sum_{j=1}^N S_j\).
Efficient: accumulate running sum instead of storing full path.
Public Function MCAsianCall(S0 As Double, K As Double, r As Double, q As Double, _
sigma As Double, T As Double, N As Long, M As Long) As Double
Dim dt As Double: dt = T / N
Dim disc As Double: disc = Exp(-r * T)
Dim drift As Double: drift = (r - q - 0.5 * sigma ^ 2) * dt
Dim volStep As Double: volStep = sigma * Sqr(dt)
Dim i As Long, j As Long
Dim S As Double, Z As Double
Dim sumS As Double, payoff As Double
Dim acc As Double
For i = 1 To M
S = S0: sumS = 0#
For j = 1 To N
Z = StdNormal()
S = S * Exp(drift + volStep * Z)
sumS = sumS + S
Next j
payoff = Application.Max(sumS / N - K, 0#)
acc = acc + payoff
Next i
MCAsianCall = disc * (acc / M)
End FunctionPayoff: \(\max(S_T - K,0)\) if path never crosses barrier \(B\) (\(S_j < B\; \forall j\)). Else \(0\).
Public Function MCUpAndOutCall(S0 As Double, K As Double, B As Double, r As Double, q As Double, _
sigma As Double, T As Double, N As Long, M As Long) As Double
Dim dt As Double
dt = T / N
Dim disc As Double
disc = Exp(-r * T)
Dim drift As Double
drift = (r - q - 0.5 * sigma ^ 2) * dt
Dim volStep As Double
volStep = sigma * Sqr(dt)
Dim i As Long, j As Long, knocked As Boolean
Dim S As Double, Z As Double, payoff As Double, acc As Double
For i = 1 To M
S = S0: knocked = False
For j = 1 To N
Z = StdNormal()
S = S * Exp(drift + volStep * Z)
If S >= B Then knocked = True: Exit For
Next j
If Not knocked Then
payoff = Application.Max(S - K, 0#)
acc = acc + payoff
End If
Next i
MCUpAndOutCall = disc * acc / M
End FunctionNote: Discrete monitoring; true continuous monitoring requires correction.
Payoff: \(\max( M_T - K, 0 )\) with \(M_T = \max_j S_j\).
Public Function MCLookbackCall(S0 As Double, K As Double, r As Double, q As Double, _
sigma As Double, T As Double, N As Long, M As Long) As Double
Dim dt As Double
dt = T / N
Dim disc As Double
disc = Exp(-r * T)
Dim drift As Double
drift = (r - q - 0.5 * sigma ^ 2) * dt
Dim volStep As Double: volStep = sigma * Sqr(dt)
Dim i As Long, j As Long
Dim S As Double, Z As Double, maxS As Double, acc As Double
For i = 1 To M
S = S0: maxS = S0
For j = 1 To N
Z = StdNormal()
S = S * Exp(drift + volStep * Z)
If S > maxS Then maxS = S
Next j
acc = acc + Application.Max(maxS - K, 0#)
Next i
MCLookbackCall = disc * acc / M
End FunctionFor \(\Delta\) (Delta):
\[\Delta \approx \frac{V(S_0 + h) - V(S_0 - h)}{2h}\]
VBA pattern (reuse pricing function):
Public Function MCDeltaEuroCall(S0 As Double, K As Double, r As Double, q As Double, _
sigma As Double, T As Double, N As Long, M As Long, h As Double) As Double
Dim up As Double, dn As Double
up = MCEuroCall(S0 + h, K, r, q, sigma, T, N, M)
dn = MCEuroCall(S0 - h, K, r, q, sigma, T, N, M)
MCDeltaEuroCall = (up - dn) / (2# * h)
End FunctionIdea: For each \(Z\), also use \(-Z\); average payoffs → reduce variance (symmetry).
Public Function MCEuroCallAntithetic(S0 As Double, K As Double, r As Double, q As Double, _
sigma As Double, T As Double, N As Long, M As Long) As Double
Dim dt As Double: dt = T / N, disc As Double: disc = Exp(-r * T)
Dim drift As Double: drift = (r - q - 0.5 * sigma ^ 2) * dt
Dim volStep As Double: volStep = sigma * Sqr(dt)
Dim i As Long, j As Long
Dim S1 As Double, S2 As Double, Z As Double
Dim payoff As Double, acc As Double
For i = 1 To M \ 2 ' assume M even
S1 = S0: S2 = S0
For j = 1 To N
Z = StdNormal()
S1 = S1 * Exp(drift + volStep * Z)
S2 = S2 * Exp(drift - volStep * Z)
Next j
payoff = 0.5 * (Application.Max(S1 - K, 0#) + Application.Max(S2 - K, 0#))
acc = acc + payoff
Next i
MCEuroCallAntithetic = disc * (2# * acc / M)
End Function