Chapter 14 — Exotic Options
"An exotic option is just a vanilla option that your counterparty can't hedge."
After this chapter you will be able to:
- Price digital (cash-or-nothing and asset-or-nothing) options from the Black-Scholes formula and construct vanilla options as portfolios of digitals
- Price barrier options using the reflection principle closed form and identify when continuous vs discrete monitoring matters
- Apply Monte Carlo with geometric control variates to price arithmetic Asian options with variance reduction
- Understand why chooser, compound, and cliquet options are used in structured products
- Explain the economic motivation for each exotic payoff type and who the natural buyers are
The vocabulary of options pricing expanded dramatically in the late 1980s and early 1990s as banks began structuring over-the-counter derivatives tailored to the precise hedging needs of corporate clients. A commodity producer that sells output monthly needs an Asian option that averages over the monthly settlement prices, not a single European option that fixes the price on one date. An exporter who only needs currency protection if rates move beyond a certain level can buy a barrier option far more cheaply than a vanilla option — the barrier absorbs some probability and reduces the premium accordingly. A treasurer who will pay floating rates but wants the insurance of a cap only if conditions warrant it can buy a chooser option that defers the put-or-call decision. Exotic options were invented for precision, not speculation.
From a mathematical perspective, exotics are interesting because they break the elegance of the Black-Scholes framework. Many have path-dependent payoffs — they depend on not just the terminal price but on the trajectory of prices over the option's lifetime. The reflection principle of Brownian motion enables exact closed-form formulas for barrier and lookback options; the geometric average of log-normal prices is itself log-normal, giving a closed form for geometric Asian options; but the arithmetic average of log-normal prices has no closed-form distribution, requiring Monte Carlo. Each exotic pushes us to use a different mathematical tool.
This chapter implements seven families of exotic options: digitals, barriers, Asians, lookbacks, compounds, choosers, and cliquets. For each, we present the closed-form solution where one exists, and Monte Carlo with control variates where it does not.
14.1 Digital Options
Cash-or-nothing: pays $Q$ if $S_T > K$, else 0. Asset-or-nothing: pays $S_T$ if $S_T > K$, else 0.
module Digital = struct
(** Cash-or-nothing call: Q * N(d2) *)
let cash_or_nothing_call ~spot ~strike ~rate ?(div_yield = 0.0)
~vol ~tau ~cash_amount =
let d1 = (log (spot /. strike) +. (rate -. div_yield +. 0.5 *. vol *. vol) *. tau)
/. (vol *. sqrt tau) in
let d2 = d1 -. vol *. sqrt tau in
cash_amount *. exp (-. rate *. tau) *. Numerics.norm_cdf d2
let cash_or_nothing_put ~spot ~strike ~rate ?(div_yield = 0.0)
~vol ~tau ~cash_amount =
let d1 = (log (spot /. strike) +. (rate -. div_yield +. 0.5 *. vol *. vol) *. tau)
/. (vol *. sqrt tau) in
let d2 = d1 -. vol *. sqrt tau in
cash_amount *. exp (-. rate *. tau) *. Numerics.norm_cdf (-. d2)
(** Asset-or-nothing call: S * e^{-qT} * N(d1) *)
let asset_or_nothing_call ~spot ~strike ~rate ?(div_yield = 0.0) ~vol ~tau =
let d1 = (log (spot /. strike) +. (rate -. div_yield +. 0.5 *. vol *. vol) *. tau)
/. (vol *. sqrt tau) in
spot *. exp (-. div_yield *. tau) *. Numerics.norm_cdf d1
(** Decomposition: vanilla call = asset-or-nothing call - K*e^{-rT}*cash-or-nothing call *)
let call_from_digitals ~spot ~strike ~rate ~div_yield ~vol ~tau =
let aon = asset_or_nothing_call ~spot ~strike ~rate ~div_yield ~vol ~tau in
let con = cash_or_nothing_call ~spot ~strike ~rate ~div_yield ~vol ~tau
~cash_amount:strike in
aon -. con
(** Gap option: pays (S - K_pay) if S > K_trigger *)
let gap_call ~spot ~strike_trigger ~strike_pay ~rate ?(div_yield = 0.0) ~vol ~tau =
let d1 = (log (spot /. strike_trigger) +. (rate -. div_yield +. 0.5 *. vol *. vol) *. tau)
/. (vol *. sqrt tau) in
let d2 = d1 -. vol *. sqrt tau in
spot *. exp (-. div_yield *. tau) *. Numerics.norm_cdf d1
-. strike_pay *. exp (-. rate *. tau) *. Numerics.norm_cdf d2
end
14.2 Barrier Options
Why Barrier Options Exist
Barrier options are cheaper than vanilla options because part of the probability space has been removed from the payoff. A down-and-out call with barrier $H = 80$ on a stock at $S = 100$ is cheaper than a vanilla call because the option cancels if the stock ever hits 80 — exactly when a holder who bought protection cheap would most want the option to survive. Who buys them? Mostly corporate treasurers and asset managers who have a view that the stock will rise (want the call payoff) but believe the extreme downside scenario is very unlikely, and would rather save premium than insure against low-probability events. The barrier is set below the level they consider plausible.
Barrier options also arise in structured products: capital-protected notes often embed a knock-in put that only activates if the index falls more than 30–40% (the knock-in barrier). The note issuer can raise the capital protection level precisely because the put is cheaper than a vanilla put.
Discrete vs continuous monitoring. In theory, barriers are monitored continuously. In practice, exchange-traded barriers are monitored at the close of each business day. This difference matters: continuous monitoring assigns a higher knock-out probability than daily monitoring (there are more opportunities to hit the barrier intraday and recover by close). For a daily-monitored barrier, the effective barrier is higher than the nominal level by approximately $0.5826 \cdot \sigma \sqrt{\Delta t}$ (the Broadie-Glasserman-Kou correction). For $\sigma = 25%$ and daily monitoring ($\Delta t = 1/252$), the correction is about 0.8%, which is small but non-trivial for near-barrier pricing. Always clarify with the counterparty whether monitoring is daily or continuous.
Barrier options are activated or extinguished when the spot crosses a barrier $H$.
| Type | Condition | Description |
|---|---|---|
| Down-and-out call | $S_t \geq H ;\forall t$ | Standard call, but cancels if $S$ hits $H$ |
| Down-and-in call | $\min_t S_t \leq H$ | Call that only activates on touching $H$ |
| Up-and-out call | $\max_t S_t \leq H$ | Call cancelled by breaching $H$ from below |
| Up-and-in call | $\max_t S_t \geq H$ | Call activated by breaching $H$ |
module Barrier = struct
(** Reflection principle closed-form for down-and-out call (H <= K) *)
let down_and_out_call ~spot ~strike ~rate ?(div_yield = 0.0)
~vol ~tau ~barrier =
if spot <= barrier then 0.0
else begin
let b = barrier in
let mu = (rate -. div_yield) /. (vol *. vol) -. 0.5 in
let ln_sb = log (b /. spot) in
let sigma_t = vol *. sqrt tau in
let d1 x = (log (spot /. x) +. (rate -. div_yield +. 0.5 *. vol *. vol) *. tau)
/. sigma_t in
let d2 x = d1 x -. sigma_t in
let phi = Numerics.norm_cdf in
let s0 = spot *. exp (-. div_yield *. tau) in
let kdf = strike *. exp (-. rate *. tau) in
let b2s = b *. b /. spot in
let b2k = b *. b /. strike in
(* Standard BS call *)
let vanilla = s0 *. phi (d1 strike) -. kdf *. phi (d2 strike) in
(* Reflection term *)
let reflect =
let d1r = (log (b2s /. strike) +. (rate -. div_yield +. 0.5 *. vol *. vol) *. tau)
/. sigma_t in
let d2r = d1r -. sigma_t in
(b /. spot) ** (2.0 *. mu +. 1.0)
*. (b2s *. exp (-. div_yield *. tau) *. phi d1r
-. b2k *. exp (-. rate *. tau) *. phi d2r)
in
ignore ln_sb;
vanilla -. reflect
end
(** Down-and-in call: in-out parity C_di + C_do = C_vanilla *)
let down_and_in_call ~spot ~strike ~rate ?(div_yield = 0.0) ~vol ~tau ~barrier =
let vanilla = Black_scholes.call ~spot ~strike ~rate ~div_yield ~vol ~tau in
let dao = down_and_out_call ~spot ~strike ~rate ~div_yield ~vol ~tau ~barrier in
vanilla -. dao
(** Up-and-out put: closed form. Requires H >= K for standard form. *)
let up_and_out_put ~spot ~strike ~rate ?(div_yield = 0.0) ~vol ~tau ~barrier =
if spot >= barrier then 0.0
else begin
(* Uses same reflection principle with reversed sign structure *)
let mu = (rate -. div_yield) /. (vol *. vol) -. 0.5 in
let sigma_t = vol *. sqrt tau in
let phi = Numerics.norm_cdf in
let d1 x = (log (spot /. x) +. (rate -. div_yield +. 0.5 *. vol *. vol) *. tau)
/. sigma_t in
let d2 x = d1 x -. sigma_t in
let s0 = spot *. exp (-. div_yield *. tau) in
let kdf = strike *. exp (-. rate *. tau) in
let vanilla_put = kdf *. phi (-. d2 strike) -. s0 *. phi (-. d1 strike) in
let b2s = barrier *. barrier /. spot in
let b2k = barrier *. barrier /. strike in
let d1r = (log (b2s /. strike) +. (rate -. div_yield +. 0.5 *. vol *. vol) *. tau)
/. sigma_t in
let d2r = d1r -. sigma_t in
let reflect =
(barrier /. spot) ** (2.0 *. mu +. 1.0)
*. (b2k *. exp (-. rate *. tau) *. phi (-. d2r)
-. b2s *. exp (-. div_yield *. tau) *. phi (-. d1r))
in
vanilla_put -. reflect
end
(** In-out parity: P_ui + P_uo = P_vanilla *)
let up_and_in_put ~spot ~strike ~rate ?(div_yield = 0.0) ~vol ~tau ~barrier =
let vanilla = Black_scholes.put ~spot ~strike ~rate ~div_yield ~vol ~tau in
let uop = up_and_out_put ~spot ~strike ~rate ~div_yield ~vol ~tau ~barrier in
vanilla -. uop
(** Monte Carlo pricing for barriers — handles discrete monitoring *)
let mc_barrier ~spot ~strike ~rate ?(div_yield = 0.0) ~vol ~tau ~barrier
~barrier_type ~option_type ~n_steps ~n_paths () =
let dt = tau /. float_of_int n_steps in
let drift = (rate -. div_yield -. 0.5 *. vol *. vol) *. dt in
let sigdt = vol *. sqrt dt in
let df = exp (-. rate *. tau) in
let payoffs = Array.init n_paths (fun _ ->
let s = ref spot in
let barrier_hit = ref false in
for _ = 0 to n_steps - 1 do
s := !s *. exp (drift +. sigdt *. Mc.std_normal ());
(match barrier_type with
| `Down -> if !s <= barrier then barrier_hit := true
| `Up -> if !s >= barrier then barrier_hit := true)
done;
let intrinsic = match option_type with
| `Call -> Float.max 0.0 (!s -. strike)
| `Put -> Float.max 0.0 (strike -. !s)
in
match barrier_type with
| `Down -> if !barrier_hit then 0.0 else intrinsic (* knock-out *)
| `Up -> if !barrier_hit then intrinsic else 0.0 (* knock-in *)
) in
df *. Array.fold_left (+.) 0.0 payoffs /. float_of_int n_paths
end
14.3 Asian Options (Closed Form for Geometric)
The geometric Asian call has a closed-form solution under BS since the geometric mean of a log-normal process is log-normal:
$$\text{E}^Q\left[\frac{1}{n}\sum_{i=1}^n S_{t_i}\right] \approx \text{geometric mean with adjusted parameters}$$
For a geometric Asian call with continuous monitoring, adjusted parameters are:
$$\tilde{r} = \frac{r-q}{2} + \frac{\sigma^2}{6} \cdot \frac{1}{2}, \quad \tilde{\sigma} = \frac{\sigma}{\sqrt{3}}$$
module Asian = struct
(** Geometric average Asian call — closed form (continuous monitoring) *)
let geometric_call ~spot ~strike ~rate ?(div_yield = 0.0) ~vol ~tau =
let adjusted_vol = vol /. sqrt 3.0 in
let adjusted_rate = (rate -. div_yield) /. 2.0 +. vol *. vol /. 12.0 in
let adjusted_carry = adjusted_rate -. (rate -. div_yield) /. 2.0 -. vol *. vol /. 12.0 in
let d1 = (log (spot /. strike) +. (adjusted_rate +. 0.5 *. adjusted_vol *. adjusted_vol) *. tau)
/. (adjusted_vol *. sqrt tau) in
let d2 = d1 -. adjusted_vol *. sqrt tau in
let phi = Numerics.norm_cdf in
ignore adjusted_carry;
spot *. exp (-. (rate -. div_yield -. adjusted_rate) *. tau) *. phi d1
-. strike *. exp (-. rate *. tau) *. phi d2
(** Arithmetic average Asian — use MC with geometric as control variate *)
let arithmetic_call_mc ~spot ~strike ~rate ?(div_yield = 0.0) ~vol ~tau ~n_steps ~n_paths () =
let dt = tau /. float_of_int n_steps in
let drift = (rate -. div_yield -. 0.5 *. vol *. vol) *. dt in
let sigdt = vol *. sqrt dt in
let df = exp (-. rate *. tau) in
let geo_exact = geometric_call ~spot ~strike ~rate ~div_yield ~vol ~tau in
let arith_payoffs = Array.make n_paths 0.0 in
let geo_payoffs = Array.make n_paths 0.0 in
for i = 0 to n_paths - 1 do
let s = ref spot and arith_sum = ref 0.0 and geo_log_sum = ref 0.0 in
for _ = 0 to n_steps - 1 do
s := !s *. exp (drift +. sigdt *. Mc.std_normal ());
arith_sum := !arith_sum +. !s;
geo_log_sum := !geo_log_sum +. log !s
done;
let avg_a = !arith_sum /. float_of_int n_steps in
let avg_g = exp (!geo_log_sum /. float_of_int n_steps) in
arith_payoffs.(i) <- Float.max 0.0 (avg_a -. strike);
geo_payoffs.(i) <- Float.max 0.0 (avg_g -. strike)
done;
(* Control variate: use geometric average call *)
let n = float_of_int n_paths in
let fa = Array.fold_left (+.) 0.0 arith_payoffs /. n in
let fg = Array.fold_left (+.) 0.0 geo_payoffs /. n in
let cov = Array.fold_left2 (fun a ai gi ->
a +. (ai -. fa) *. (gi -. fg)) 0.0 arith_payoffs geo_payoffs
/. (n -. 1.0) in
let var_g = Array.fold_left (fun a g -> a +. (g -. fg) *. (g -. fg)) 0.0 geo_payoffs
/. (n -. 1.0) in
let beta = cov /. var_g in
let adj_fa = fa -. beta *. (fg -. geo_exact /. df) in
df *. adj_fa
end
14.4 Lookback Options
Lookbacks depend on the maximum or minimum of the path.
For a floating-strike lookback call ($S_T - \min_t S_t$), the closed-form solution under constant BS:
$$C_{\text{lookback}} = S_0 N(d_1) - S_{\min} e^{-rT} N(d_2) + S_0 e^{-qT} \frac{\sigma^2}{2(r-q)} \left[N(-d_1) - e^{(q-r)T}(S_0/S_{\min})^{-2(r-q)/\sigma^2} N(-d_3)\right]$$
module Lookback = struct
(** Floating-strike lookback call: E[S_T - S_min] using Goldman-Sosin-Gatto *)
let floating_call ~spot ~s_min ~rate ?(div_yield = 0.0) ~vol ~tau =
let b = rate -. div_yield in
let sig2 = vol *. vol in
let sqrt_t = sqrt tau in
let d1 = (log (spot /. s_min) +. (b +. 0.5 *. sig2) *. tau) /. (vol *. sqrt_t) in
let d2 = d1 -. vol *. sqrt_t in
let d3 = (log (spot /. s_min) +. (-. b +. 0.5 *. sig2) *. tau) /. (vol *. sqrt_t) in
let phi = Numerics.norm_cdf in
if Float.abs b < 1e-8 then
(* Special case r = q *)
spot *. exp (-. div_yield *. tau) *. (phi d1 -. vol *. sqrt_t *. Numerics.norm_pdf d1)
-. s_min *. exp (-. rate *. tau) *. phi d2
else begin
let eta = (spot /. s_min) ** (-. 2.0 *. b /. sig2) in
spot *. exp (-. div_yield *. tau) *. phi d1
-. s_min *. exp (-. rate *. tau) *. phi d2
+. spot *. exp (-. div_yield *. tau) *. sig2 /. (2.0 *. b)
*. (-. phi d1 +. eta *. phi (-. d3))
end
(** Fixed-strike lookback call: E[max(S_max - K, 0)] *)
let fixed_call ~spot ~strike ~s_max ~rate ?(div_yield = 0.0) ~vol ~tau =
ignore (spot, strike, s_max, rate, div_yield, vol, tau);
(* Formula: see Conze & Viswanathan (1991) *)
failwith "See exercise 14.3"
end
14.5 Compound Options
A compound option is an option on an option. Four types: call-on-call (CoC), call-on-put (CoP), put-on-call (PoC), put-on-put (PoP).
module Compound = struct
(** Call-on-call: option to buy a call C(S, K2, T2) at cost K1 at time T1.
Uses bivariate normal from Geske (1979). *)
let call_on_call ~spot ~strike_outer ~strike_inner ~rate ?(div_yield = 0.0)
~vol ~t1 ~t2 ~critical_spot =
(* critical_spot: S* such that C(S*, K2, T2-T1) = K1 *)
let phi2 = Numerics.bivar_norm_cdf in (* bivariate normal CDF *)
let rho = sqrt (t1 /. t2) in
let d1 = (log (spot /. strike_inner)
+. (rate -. div_yield +. 0.5 *. vol *. vol) *. t2)
/. (vol *. sqrt t2) in
let d2 = d1 -. vol *. sqrt t2 in
let y1 = (log (spot /. critical_spot)
+. (rate -. div_yield +. 0.5 *. vol *. vol) *. t1)
/. (vol *. sqrt t1) in
let y2 = y1 -. vol *. sqrt t1 in
let phi = Numerics.norm_cdf in
ignore phi;
spot *. exp (-. div_yield *. t2) *. phi2 (y1, d1, rho)
-. strike_inner *. exp (-. rate *. t2) *. phi2 (y2, d2, rho)
-. strike_outer *. exp (-. rate *. t1) *. Numerics.norm_cdf y2
(** Bivariate normal CDF — production: use Genz (1992) algorithm *)
(* Note this is a stub; full implementation in Appendix C *)
end
14.6 Chooser Options
A chooser option lets the holder choose, at time $T_c$, whether it becomes a call or put expiring at $T > T_c$:
$$V(T_c) = \max(C(S_{T_c}, K, T - T_c), P(S_{T_c}, K, T - T_c))$$
module Chooser = struct
(** Simple chooser (equal K and T for call and put) *)
let price ~spot ~strike ~rate ?(div_yield = 0.0) ~vol ~t_choose ~t_expiry =
(* By put-call parity at choosing time:
max(C, P) = C + max(0, K*e^{-r(T-Tc)} - S*e^{-q(T-Tc)})
= C + put_on_S with adjusted strike *)
let t_rem = t_expiry -. t_choose in
let adj_k = strike *. exp (-. rate *. t_rem) in (* PV of strike *)
let call = Black_scholes.call ~spot ~strike ~rate ~div_yield ~vol ~tau:t_expiry in
let put_adj = Black_scholes.put ~spot ~strike:adj_k ~rate ~div_yield ~vol ~tau:t_choose in
call +. put_adj
end
14.7 Cliquet Options
A cliquet (ratchet) option locks in periodic returns:
$$\text{Payoff} = \sum_{i=1}^n \max\left(\min\left(\frac{S_{t_i} - S_{t_{i-1}}}{S_{t_{i-1}}}, \text{cap}\right), \text{floor}\right)$$
module Cliquet = struct
type params = {
floor : float; (* local floor per period, e.g. -0.05 *)
cap : float; (* local cap per period, e.g. +0.10 *)
global_floor : float; (* total payoff floor *)
global_cap : float; (* total payoff cap *)
}
let mc_price ~spot:_ ~rate ~vol ~tau ~n_periods ~n_paths ~params () =
let dt = tau /. float_of_int n_periods in
let drift = (rate -. 0.5 *. vol *. vol) *. dt in
let sigdt = vol *. sqrt dt in
let df = exp (-. rate *. tau) in
let payoffs = Array.init n_paths (fun _ ->
let s = ref 1.0 in (* normalized *)
let total = ref 0.0 in
for _ = 0 to n_periods - 1 do
let z = Mc.std_normal () in
let prev = !s in
s := !s *. exp (drift +. sigdt *. z);
let ret = (!s -. prev) /. prev in
let local = Float.max params.floor (Float.min params.cap ret) in
total := !total +. local
done;
Float.max params.global_floor (Float.min params.global_cap !total)
) in
df *. Array.fold_left (+.) 0.0 payoffs /. float_of_int n_paths
end
14.8 Chapter Summary
Exotic options demonstrate that the payoff of a derivative is limited only by the imagination of the parties involved — but their pricing requires using the full toolkit of mathematical finance.
Digital options are the simplest departure from the vanilla framework: binary payoffs with exact closed-form expressions derived directly from the Black-Scholes formula for $d_1$ and $d_2$. They are also the building block for more complex structures: a call spread in the limit of zero strike width is a digital. Barrier options use the reflection principle of Brownian motion to compute the probability of the path hitting a specified level, yielding closed-form solutions. In-out parity ($C_{\text{knock-in}} + C_{\text{knock-out}} = C_{\text{vanilla}}$) provides a model-independent consistency check.
Asian options are the textbook example of path dependency. The arithmetic average of geometric Brownian motion has no closed-form distribution, but the geometric average does — it is log-normal with adjusted drift and volatility. The geometric average price therefore provides an exact control variate for Monte Carlo: simulate both averages simultaneously, compute the exact correction using the geometric formula, and apply it to the MC estimate of the arithmetic average. This variance reduction technique is the standard approach and reduces standard error by 60-80% in practice.
Lookback options, which pay the difference between the terminal price and the running minimum (or maximum), appear frequently in structured products as capital protection features. Their closed-form pricing via the Goldman-Sosin-Gatto formula relies on the distribution of the maximum of a Brownian motion with drift. Cliquets, which accumulate capped and floored periodic returns, are among the most complex structures in equity derivatives and are almost universally priced by Monte Carlo with a stochastic volatility model.
Exercises
14.1 Verify the in-out parity relation $C_{\text{di}} + C_{\text{do}} = C_{\text{vanilla}}$ numerically for 10 different barrier levels $H \in [70, 95]$ with $S=100, K=100, r=5%,\sigma=25%, T=1$.
14.2 Implement an up-and-out call with a continuous barrier and compare the closed-form formula to MC with 250 daily steps. Study the discretisation bias.
14.3 Implement the fixed-strike lookback call closed form using the Conze-Viswanathan (1991) formula. Validate against lookback MC.
14.4 Price a cliquet with floor=-5%, cap=+8%, 12 monthly periods, $r=3%$, $\sigma=20%$ using MC ($N=100000$). Study how the price varies with cap and floor levels.