Chapter 2 — OCaml Essentials for Finance
"Make illegal states unrepresentable." — Yaron Minsky, Jane Street
Yaron Minsky, who built Jane Street's OCaml infrastructure, coined the phrase that best describes OCaml's design philosophy: use the type system to make the wrong states of the program simply inexpressible. In financial software, there are many such states. A price that is negative. A probability outside $[0, 1]$. An interest rate for a currency that doesn't match the instrument's currency. A discount factor computed with an incorrect day count convention. OCaml's type system cannot prevent all of these by itself, but it enables programmers to build the abstractions that do prevent them.
This chapter introduces OCaml from the perspective of a quantitative developer. We assume some programming experience but not familiarity with functional languages or the ML family. The focus throughout is on the features most directly useful for financial programming: the type system and how to use it to model financial domain knowledge; pattern matching as a tool for exhaustive case analysis over instrument types; the module system as a mechanism for building composable, testable components; and the Result and Option types for making error handling explicit rather than relying on exceptions.
By the end of this chapter, you will have the OCaml vocabulary needed to read and write the code in the rest of this book. Advanced OCaml features — functors, GADTs, first-class modules — appear in context in later chapters when they solve specific financial modelling problems.
2.1 Types and Values
OCaml is a strongly and statically typed language. Every expression has a type determined at compile time, and types are never coerced implicitly.
2.1.1 Primitive Types
(* Integers — exact, no rounding *)
let notional_usd : int = 10_000_000 (* underscore separators for readability *)
let days_in_year : int = 365
(* Floats — IEEE 754 double precision *)
let spot_price : float = 142.37
let volatility : float = 0.2312 (* 23.12% expressed as decimal *)
let risk_free : float = 0.0525 (* 5.25% *)
(* Float arithmetic uses .+, .*, etc. to prevent accidental int/float mix *)
let forward = spot_price *. Float.exp (risk_free *. 0.5)
(* Booleans *)
let is_call : bool = true
let in_the_money = spot_price > 140.0
(* Strings *)
let ticker : string = "AAPL"
let currency : string = "USD"
(* Unit — the type with only one value; used for side effects *)
let () = Printf.printf "Spot: %.2f\n" spot_price
2.1.2 Type Inference
OCaml infers types from usage; you rarely need to annotate:
(* Types inferred: x : float, y : float, result : float *)
let discount_factor rate t = Float.exp (-. rate *. t)
(* Functions are values with types *)
(* discount_factor : float -> float -> float *)
2.1.3 Immutability by Default
Bindings in OCaml are immutable by default. This matters for correctness: a risk calculation that takes a snapshot of market data should not see that data change mid-computation.
let rate = 0.05 (* immutable *)
(* rate = 0.06 ← Type error: this is comparison, not assignment *)
(* To have mutable state, use ref *)
let counter = ref 0
counter := !counter + 1 (* := assigns, ! dereferences *)
2.2 Functions
2.2.1 Defining Functions
(* Simple function *)
let square x = x *. x
(* Multi-argument function (curried by default) *)
let black_scholes_d1 ~spot ~strike ~rate ~vol ~tau =
let open Float in
(log (spot /. strike) +. (rate +. 0.5 *. square vol) *. tau)
/. (vol *. sqrt tau)
(* Calling with named arguments *)
let d1 = black_scholes_d1 ~spot:100.0 ~strike:105.0
~rate:0.05 ~vol:0.20 ~tau:0.5
2.2.2 Currying and Partial Application
Every multi-argument OCaml function is actually a chain of single-argument functions. Partial application creates specialised functions efficiently:
(* A general discounting function *)
let discount_factor ~rate ~tau = Float.exp (-. rate *. tau)
(* Specialise for a particular rate — creates a new function *)
let discount_5pct = discount_factor ~rate:0.05
(* Now discount_5pct : tau:float -> float *)
let pv_6m = discount_5pct ~tau:0.5 (* 0.9753... *)
let pv_1y = discount_5pct ~tau:1.0 (* 0.9512... *)
This is extremely useful in financial code: specialise a pricing function with fixed market data, then map it over a book of instruments.
2.2.3 Higher-Order Functions
Functions that take functions as arguments are natural in finance:
(** Apply a payoff function to a list of simulated prices *)
let price_by_simulation ~payoff ~paths ~rate ~tau =
let n = List.length paths in
let total_payoff = List.fold_left
(fun acc s -> acc +. payoff s)
0.0 paths
in
(total_payoff /. float_of_int n) *. Float.exp (-. rate *. tau)
(* European call payoff *)
let call_payoff ~strike s = Float.max 0.0 (s -. strike)
(* European put payoff *)
let put_payoff ~strike s = Float.max 0.0 (strike -. s)
(* Usage *)
let call_price = price_by_simulation
~payoff:(call_payoff ~strike:100.0)
~paths:[95.0; 102.0; 108.0; 97.0; 112.0]
~rate:0.05 ~tau:1.0
2.2.4 Recursive Functions
Recursive functions are declared with let rec:
(** Present value of an annuity via recursion *)
let rec annuity_pv ~payment ~rate ~remaining =
if remaining = 0 then 0.0
else
payment /. (1.0 +. rate) +.
annuity_pv ~payment ~rate ~remaining:(remaining - 1)
(** More efficient tail-recursive version *)
let annuity_pv_tr ~payment ~rate ~periods =
let rec loop acc remaining =
if remaining = 0 then acc
else
let df = 1.0 /. (1.0 +. rate) ** float_of_int (periods - remaining + 1) in
loop (acc +. payment *. df) (remaining - 1)
in
loop 0.0 periods
2.3 Pattern Matching
Pattern matching is one of OCaml's most powerful features. It is like a switch statement that:
- Deconstructs data structures
- Is exhaustive (compiler warns on missing cases)
- Executes in O(log n) time via decision trees
2.3.1 Matching on Variants
type option_type = Call | Put
type option_exercise =
| European
| American
| Bermudan of { dates : int list }
let max_exercise_value ~intrinsic ~continuation ~exercise =
match exercise with
| European -> continuation (* can only exercise at expiry *)
| American -> Float.max intrinsic continuation
| Bermudan { dates } ->
if List.mem dates (current_time ()) ~equal:Int.equal
then Float.max intrinsic continuation
else continuation
2.3.2 Matching on Tuples and Records
type market_data = {
spot : float;
vol : float;
rate : float;
div_yield: float;
}
let print_market_summary { spot; vol; rate; div_yield } =
Printf.printf "S=%.2f vol=%.1f%% r=%.2f%% q=%.2f%%\n"
spot (vol *. 100.0) (rate *. 100.0) (div_yield *. 100.0)
(* Matching on tuples *)
let describe_moneyness (spot, strike) =
match Float.compare spot strike with
| c when c < 0.0 -> "out-of-the-money"
| c when c = 0.0 -> "at-the-money"
| _ -> "in-the-money"
2.3.3 Nested Patterns
type position = Long | Short
type instrument =
| Stock of { ticker : string }
| Option of { option_type : option_type; strike : float; expiry : float }
| Future of { underlying : string; expiry : float }
type trade = {
position : position;
instrument : instrument;
quantity : float;
}
let describe_trade { position; instrument; quantity } =
let dir = match position with Long -> "long" | Short -> "short" in
let inst = match instrument with
| Stock { ticker } -> Printf.sprintf "%s stock" ticker
| Option { option_type = Call; strike; _ } ->
Printf.sprintf "call option (K=%.0f)" strike
| Option { option_type = Put; strike; _ } ->
Printf.sprintf "put option (K=%.0f)" strike
| Future { underlying; expiry } ->
Printf.sprintf "%s future exp=%.2f" underlying expiry
in
Printf.sprintf "%s %.0f %s" dir quantity inst
2.4 Records and Variants for Financial Instruments
2.4.1 Defining Domain Models
(** Day count conventions *)
type day_count =
| Act360
| Act365
| Thirty360
| ActAct
(** Business day conventions *)
type bdc =
| Following
| ModifiedFollowing
| Preceding
| Unadjusted
(** A complete bond specification *)
type bond = {
isin : string;
issuer : string;
currency : string;
face_value : float;
coupon_rate: float;
day_count : day_count;
bdc : bdc;
issue_date : string; (* ISO 8601 *)
maturity : string;
frequency : int; (* coupons per year *)
}
(** A complete vanilla option *)
type vanilla_option = {
underlying : string;
option_type : option_type;
strike : float;
expiry : float; (* years to expiry *)
notional : float;
exercise : option_exercise;
currency : string;
}
(** Risk-free rate instrument *)
type rate_instrument =
| ZeroCouponBond of { maturity : float; price : float }
| IRS of {
fixed_rate : float;
floating_spread : float;
tenor : float;
frequency : int;
}
| OvernightIndexSwap of {
fixed_rate : float;
tenor : float;
}
2.4.2 Using ppx_deriving for Boilerplate
(* With ppx_deriving, the compiler generates show, compare, equal automatically *)
type credit_rating =
| AAA | AA_plus | AA | AA_minus
| A_plus | A | A_minus
| BBB_plus | BBB | BBB_minus
| BB_plus | BB | BB_minus
| B_plus | B | B_minus
| CCC | CC | C | D
[@@deriving show, compare, equal]
(* Now we can: *)
let _ = show_credit_rating AAA (* "AAA" *)
let _ = compare_credit_rating AAA BBB (* -1 *)
let _ = equal_credit_rating A A (* true *)
2.5 Modules and Functors
OCaml's module system is one of the most sophisticated in any mainstream language. It is the principal mechanism for code organisation and abstraction in financial libraries.
2.5.1 Basic Modules
module Black_scholes = struct
(** Cumulative standard normal CDF via approximation *)
let norm_cdf x =
let a1 = 0.254829592 in
let a2 = -0.284496736 in
let a3 = 1.421413741 in
let a4 = -1.453152027 in
let a5 = 1.061405429 in
let p = 0.3275911 in
let sign = if x >= 0.0 then 1.0 else -1.0 in
let x = Float.abs x in
let t = 1.0 /. (1.0 +. p *. x) in
let y = 1.0 -. (((((a5 *. t +. a4) *. t) +. a3) *. t +. a2) *. t +. a1)
*. t *. Float.exp (-. x *. x) in
0.5 *. (1.0 +. sign *. y)
let d1 ~spot ~strike ~rate ~vol ~tau =
(Float.log (spot /. strike) +. (rate +. 0.5 *. vol *. vol) *. tau)
/. (vol *. Float.sqrt tau)
let d2 ~spot ~strike ~rate ~vol ~tau =
d1 ~spot ~strike ~rate ~vol ~tau -. vol *. Float.sqrt tau
let call_price ~spot ~strike ~rate ~vol ~tau =
let d1v = d1 ~spot ~strike ~rate ~vol ~tau in
let d2v = d2 ~spot ~strike ~rate ~vol ~tau in
spot *. norm_cdf d1v -. strike *. Float.exp (-. rate *. tau) *. norm_cdf d2v
let put_price ~spot ~strike ~rate ~vol ~tau =
let d1v = d1 ~spot ~strike ~rate ~vol ~tau in
let d2v = d2 ~spot ~strike ~rate ~vol ~tau in
strike *. Float.exp (-. rate *. tau) *. norm_cdf (-. d2v) -.
spot *. norm_cdf (-. d1v)
let delta ~option_type ~spot ~strike ~rate ~vol ~tau =
let d1v = d1 ~spot ~strike ~rate ~vol ~tau in
match option_type with
| Call -> norm_cdf d1v
| Put -> norm_cdf d1v -. 1.0
end
(* Usage *)
let price = Black_scholes.call_price ~spot:100.0 ~strike:100.0
~rate:0.05 ~vol:0.20 ~tau:1.0 (* 10.45 *)
2.5.2 Module Signatures (Interfaces)
(** The public interface of a pricer — hides implementation details *)
module type PRICER = sig
type instrument
type market_data
type price = float
(** Price an instrument given market data *)
val price : instrument -> market_data -> price
(** Compute all first-order sensitivities *)
val greeks : instrument -> market_data -> (string * float) list
(** Model name *)
val name : string
end
2.5.3 Functors — Parameterised Modules
Functors are the OCaml mechanism for writing generic, reusable components:
(** A generic pricer that works with any model conforming to PRICER *)
module Make_pricer
(Model : PRICER)
(Curve : sig val discount : float -> float end) = struct
(** Price and report *)
let price_with_pv instrument market_data =
let px = Model.price instrument market_data in
let tau = 1.0 in (* simplified *)
let pv = px *. Curve.discount tau in
Printf.printf "[%s] Price: %.4f PV: %.4f\n" Model.name px pv;
pv
(** Risk report *)
let risk_report instrument market_data =
let greeks = Model.greeks instrument market_data in
List.iter (fun (name, value) ->
Printf.printf " %s: %.6f\n" name value
) greeks
end
2.6 Error Handling
2.6.1 Option for Nullable Values
(** Return None if inputs are invalid *)
let safe_log x =
if x <= 0.0 then None
else Some (Float.log x)
let implied_vol_step ~option_price ~spot ~strike ~rate ~tau =
match safe_log (spot /. strike) with
| None -> Error "spot/strike must be positive"
| Some log_sk ->
(* ... Newton step ... *)
Ok 0.20 (* placeholder *)
2.6.2 Result for Typed Errors
type pricing_error =
| InvalidInput of string
| ModelDivergence of { iterations : int; residual : float }
| MarketDataMissing of { instrument : string }
let price_option params =
if params.vol <= 0.0 then Error (InvalidInput "volatility must be positive")
else if params.tau < 0.0 then Error (InvalidInput "time to expiry must be non-negative")
else if params.tau = 0.0 then
(* At expiry, return intrinsic value *)
let payoff = match params.option_type with
| Call -> Float.max 0.0 (params.spot -. params.strike)
| Put -> Float.max 0.0 (params.strike -. params.spot)
in
Ok payoff
else
Ok (Black_scholes.call_price
~spot:params.spot ~strike:params.strike
~rate:params.rate ~vol:params.vol ~tau:params.tau)
(** Chain results with monadic bind *)
let value_trade trade market_data =
let open Result in
price_option (build_params trade market_data)
>>= fun px -> apply_notional trade.notional px
>>= fun pv -> apply_fx_conversion trade.currency market_data pv
2.6.3 Exception Handling
Exceptions should be reserved for truly exceptional conditions — not expected error paths:
exception Market_data_unavailable of string
exception Pricing_timeout of { elapsed_ms : int }
let get_spot ticker =
match Hashtbl.find market_data_cache ticker with
| Some s -> s
| None -> raise (Market_data_unavailable ticker)
let safe_price ticker strike tau =
try
let spot = get_spot ticker in
Some (Black_scholes.call_price ~spot ~strike ~rate:0.05 ~vol:0.20 ~tau)
with
| Market_data_unavailable t ->
Printf.eprintf "No market data for %s\n" t;
None
| Division_by_zero ->
Printf.eprintf "Division by zero in pricing\n";
None
2.7 Lists, Arrays, and Sequences
2.7.1 Lists for Cash Flows
OCaml lists are immutable linked lists — ideal for cash flow schedules:
(** Generate coupon cash flows *)
let generate_coupons ~face ~coupon_rate ~frequency ~periods =
let coupon = face *. coupon_rate /. float_of_int frequency in
List.init periods (fun i ->
let t = float_of_int (i + 1) /. float_of_int frequency in
(t, coupon) (* (time, amount) pairs *)
)
(** Present value of a cash flow list *)
let pv_cash_flows ~rate flows =
List.fold_left
(fun acc (t, cf) -> acc +. cf *. Float.exp (-. rate *. t))
0.0 flows
(** Usage *)
let coupons = generate_coupons ~face:1000.0 ~coupon_rate:0.05
~frequency:2 ~periods:10 (* 5-year semi-annual 5% bond *)
let face_at_maturity = (5.0, 1000.0)
let all_flows = coupons @ [face_at_maturity]
let bond_price = pv_cash_flows ~rate:0.05 all_flows (* ~1000.0 at par *)
2.7.2 Arrays for Numerical Work
Mutable arrays are essential for numerical algorithms where performance matters:
(** Monte Carlo path generation — in-place for efficiency *)
let simulate_path ~s0 ~rate ~vol ~dt ~steps rng =
let path = Array.make (steps + 1) s0 in
for i = 1 to steps do
let z = Rng.standard_normal rng in
path.(i) <- path.(i - 1) *. Float.exp
((rate -. 0.5 *. vol *. vol) *. dt +. vol *. Float.sqrt dt *. z)
done;
path
(** Arithmetic average of a path *)
let path_average path =
let n = Array.length path in
Array.fold_left (+.) 0.0 path /. float_of_int n
2.7.3 Sequences for Lazy Evaluation
Seq provides lazy sequences — useful for potentially infinite data streams:
(** Infinite sequence of trading days *)
let rec trading_days_from start =
Seq.cons start (fun () -> trading_days_from (next_business_day start))
(** Take the first n elements *)
let next_n_days n start =
trading_days_from start |> Seq.take n |> List.of_seq
2.8 Working with Core, Base, and Jane Street Libraries
Jane Street's Core library provides a richer, more consistent standard library. It is strongly recommended for financial applications.
2.8.1 Dates with Core
open Core
(** Settlement date T+2 *)
let settlement_date trade_date =
Date.add_business_days_rounding_forwards trade_date 2
(** Days between two dates under Act/365 *)
let year_fraction_act365 d1 d2 =
let days = Date.diff d2 d1 in
float_of_int days /. 365.0
(** Build a coupon schedule *)
let coupon_schedule ~start ~maturity ~frequency_months =
let rec loop current acc =
let next = Date.add_months current frequency_months in
if Date.( > ) next maturity then
List.rev ((maturity, true) :: acc) (* true = final coupon *)
else
loop next ((next, false) :: acc)
in
loop start []
2.8.2 Maps and Sets for Market Data
(** Ticker-keyed market data using polymorphic Map *)
module Ticker = String
module Market_data_map = Map.Make(Ticker)
type market_snapshot = {
spot : float;
vol : float;
div_yield: float;
}
let empty_snapshot : market_snapshot Market_data_map.t =
Market_data_map.empty
let update_snapshot map ticker data =
Map.set map ~key:ticker ~data
let get_spot map ticker =
match Map.find map ticker with
| Some { spot; _ } -> Ok spot
| None -> Error (Printf.sprintf "No market data for %s" ticker)
2.9 A Complete Mini-Library: Fixed Income Utilities
Putting the concepts together, here is a small but complete fixed income utility library:
(**
fixed_income.ml — A foundational fixed income library
Demonstrates: records, modules, pattern matching, Result, List processing
*)
(** Day count conventions supported *)
type day_count = Act365 | Act360 | Thirty360 | ActAct
(** Compute year fraction between two float timestamps (simplified) *)
let year_fraction day_count ~start_t ~end_t =
let days = end_t -. start_t in
match day_count with
| Act365 -> days /. 365.0
| Act360 -> days /. 360.0
| Thirty360 -> days /. 360.0 (* approximate *)
| ActAct -> days /. 365.25
(** A cash flow: amount payable at time t *)
type cash_flow = {
time : float; (* years from valuation date *)
amount : float;
}
(** Discount a single cash flow *)
let discount_cash_flow ~rate { time; amount } =
amount *. Float.exp (-. rate *. time)
(** Discount a list of cash flows (yield curve is flat) *)
let pv_flat_curve ~rate flows =
List.fold_left
(fun acc cf -> acc +. discount_cash_flow ~rate cf)
0.0 flows
(**
Compute bond price given flat yield
Bond price = sum of discounted cash flows
*)
let bond_price ~face ~coupon_rate ~frequency ~maturity ~yield =
let n_periods = int_of_float (maturity *. float_of_int frequency) in
let coupon = face *. coupon_rate /. float_of_int frequency in
let coupons = List.init n_periods (fun i ->
let t = float_of_int (i + 1) /. float_of_int frequency in
{ time = t; amount = coupon }
) in
let principal = { time = maturity; amount = face } in
pv_flat_curve ~rate:yield (principal :: coupons)
(**
Modified duration — sensitivity of price to yield
D_mod = -(1/P) * dP/dy ≈ -(1/P) * (P(y+dy) - P(y-dy)) / (2*dy)
*)
let modified_duration ~face ~coupon_rate ~frequency ~maturity ~yield =
let dy = 0.0001 in
let p_up = bond_price ~face ~coupon_rate ~frequency ~maturity ~yield:(yield +. dy) in
let p_down = bond_price ~face ~coupon_rate ~frequency ~maturity ~yield:(yield -. dy) in
let p = bond_price ~face ~coupon_rate ~frequency ~maturity ~yield in
-. (p_up -. p_down) /. (2.0 *. dy *. p)
(**
Dollar value of a basis point (DV01 or PVBP)
DV01 = -dP/dy * 0.0001
*)
let dv01 ~face ~coupon_rate ~frequency ~maturity ~yield =
let dy = 0.0001 in
let p_up = bond_price ~face ~coupon_rate ~frequency ~maturity ~yield:(yield +. dy) in
let p_down = bond_price ~face ~coupon_rate ~frequency ~maturity ~yield:(yield -. dy) in
-. (p_up -. p_down) /. 2.0 (* in units of face value *)
(** Yield to maturity via Newton-Raphson iteration *)
let yield_to_maturity ~face ~coupon_rate ~frequency ~maturity ~market_price =
let max_iter = 200 in
let tol = 1e-10 in
let rec newton y iter =
if iter >= max_iter then
Error (Printf.sprintf "YTM did not converge after %d iterations" max_iter)
else
let p = bond_price ~face ~coupon_rate ~frequency ~maturity ~yield:y in
let residual = p -. market_price in
if Float.abs residual < tol then Ok y
else
let dy_num = 0.0001 in
let p_up = bond_price ~face ~coupon_rate ~frequency ~maturity ~yield:(y +. dy_num) in
let dp_dy = (p_up -. p) /. dy_num in
let y_next = y -. residual /. dp_dy in
newton y_next (iter + 1)
in
let initial_guess = coupon_rate in
newton initial_guess 0
2.10 GADTs — Generalised Algebraic Data Types
Ordinary variant types are homogeneous: every constructor of type foo has type foo. GADTs (Generalised Algebraic Data Types) break this restriction — each constructor can carry a different type parameter, and the compiler uses the constructor to narrow the type inside a pattern match. No casts, no instanceof, no runtime type tags. This is impossible in Python, Java, or C++; it requires the ML-family type inference that OCaml inherits.
For quantitative finance, GADTs enable type-indexed computations: a single price function that provably returns a float for a vanilla option, a float * float pair for a CDS (price + annuity), and a float array for a swaption (a cube of scenario prices). The return type depends on the constructor, and the compiler checks it.
2.10.1 Typed Instrument Hierarchy
(** GADT: the type parameter 'a encodes the pricing result type *)
type 'a instrument =
| Vanilla_option : {
underlying : string;
strike : float;
expiry : float;
kind : [`Call | `Put];
} -> float instrument (* prices return a single float *)
| Credit_default_swap : {
reference : string;
spread_bps : float;
maturity : float;
notional : float;
} -> (float * float) instrument (* (pv, rp01) pair *)
| Yield_curve_swaption : {
expiry : float;
tenor : float;
notional : float;
} -> float array instrument (* scenario cube *)
(** A single pricer function: return type is determined by the constructor *)
let rec price : type a. a instrument -> a = function
| Vanilla_option { strike; expiry; kind; _ } ->
let s = 100.0 and r = 0.05 and v = 0.20 in
(match kind with
| `Call -> Black_scholes.call ~spot:s ~strike ~rate:r ~vol:v ~tau:expiry
| `Put -> Black_scholes.put ~spot:s ~strike ~rate:r ~vol:v ~tau:expiry)
| Credit_default_swap { spread_bps; maturity; notional; _ } ->
let s = spread_bps /. 10000.0 in
(notional *. s *. maturity, notional *. s /. 100.0)
| Yield_curve_swaption { expiry; tenor; notional } ->
Array.init 10 (fun i ->
let bump = float_of_int (i - 5) *. 0.01 in
Swaption.price ~expiry ~tenor ~notional ~rate_bump:bump)
The type a. syntax is the GADT type annotation — it tells the compiler that a is locally constrained at each match arm. The compiler verifies exhaustiveness as usual, and also verifies that the return value at each arm has the type promised by the constructor (float, float * float, or float array). Adding a new constructor without handling it is a compile-time error.
2.10.2 Type-Indexed Greeks
GADTs can also ensure that Greeks are only computed for products where they are defined:
(** Phantom product class *)
type equity (* equity options *)
type rates (* interest rate products *)
type credit (* credit products *)
(** A greek tagged with the product class it applies to *)
type 'cls greek =
| Delta : equity greek
| Gamma : equity greek
| Vega : equity greek
| DV01 : rates greek (* rate sensitivity — only for IR products *)
| CS01 : credit greek (* credit spread 01 — only for credit *)
(** Type-safe greek computation: only valid greeks for each class *)
let compute_greek : type c. c greek -> c instrument -> float =
fun greek instrument ->
match greek, instrument with
| Delta, Vanilla_option { strike; expiry; _ } ->
let d1 = (log (100.0 /. strike) +. (0.05 +. 0.02) *. expiry)
/. (0.20 *. sqrt expiry) in
Numerics.norm_cdf d1
| Gamma, Vanilla_option { strike; expiry; _ } ->
let d1 = (log (100.0 /. strike) +. (0.05 +. 0.02) *. expiry)
/. (0.20 *. sqrt expiry) in
Numerics.norm_pdf d1 /. (100.0 *. 0.20 *. sqrt expiry)
| Vega, Vanilla_option { strike; expiry; _ } ->
let d1 = (log (100.0 /. strike) +. (0.05 +. 0.02) *. expiry)
/. (0.20 *. sqrt expiry) in
100.0 *. sqrt expiry *. Numerics.norm_pdf d1
Asking for DV01 of a Vanilla_option is a compile-time type error — not a runtime exception.
2.11 Algebraic Effects — Financial Dependency Injection
OCaml 5 introduced algebraic effects: a mechanism for delimitedly resumable computations. For quantitative finance, effects solve the dependency injection problem cleanly: pricing code can request market data (spot, vol, rates) via effects, and the caller supplies a handler that determines where the data comes from — live feed, historical database, or stress-test scenario. The pricing code itself is unchanged.
This is dependency injection without interfaces, abstract classes, or functor parameters. It is the architecture Jane Street uses in their production risk systems.
(** Declare effects: requests that pricing code can make *)
effect Get_spot : string -> float
effect Get_vol : string -> float
effect Get_rate : string -> float
(** Pricing code uses effects — agnostic about the data source *)
let price_call ~ticker ~strike ~tau =
let spot = perform (Get_spot ticker) in
let vol = perform (Get_vol ticker) in
let rate = perform (Get_rate "USD") in
Black_scholes.call ~spot ~strike ~rate ~vol ~tau
(** Handler 1: live market data *)
let run_live f =
match f () with
| v -> v
| effect (Get_spot t) k -> continue k (Live_feed.spot t)
| effect (Get_vol t) k -> continue k (Live_feed.implied_vol t)
| effect (Get_rate c) k -> continue k (Live_feed.rate c)
(** Handler 2: historical backtest — same pricing code, different data *)
let run_backtest ~date f =
match f () with
| v -> v
| effect (Get_spot t) k -> continue k (Historical_db.spot t date)
| effect (Get_vol t) k -> continue k (Historical_db.implied_vol t date)
| effect (Get_rate c) k -> continue k (Historical_db.rate c date)
(** Handler 3: stress test — bump vol by a given amount *)
let run_stressed_vol ~bump f =
match f () with
| v -> v
| effect (Get_spot t) k -> continue k (Live_feed.spot t)
| effect (Get_vol t) k -> continue k (Live_feed.implied_vol t +. bump)
| effect (Get_rate c) k -> continue k (Live_feed.rate c)
(** Usage: run the same pricing function in three contexts *)
let live_price = run_live (fun () -> price_call ~ticker:"AAPL" ~strike:150.0 ~tau:0.5)
let hist_price = run_backtest ~date:"2024-01-15"
(fun () -> price_call ~ticker:"AAPL" ~strike:150.0 ~tau:0.5)
let stressed_px = run_stressed_vol ~bump:0.05
(fun () -> price_call ~ticker:"AAPL" ~strike:150.0 ~tau:0.5)
The key advantage over traditional dependency injection (passing market-data objects as function parameters) is that effects are transparent — intermediate functions in the call chain do not need to be aware of or forward the market data object. The handler at the top of the call stack intercepts all Get_spot effects from any function in its dynamic scope. This eliminates the "threading" problem of having to pass a market_data parameter through every level of a deep call hierarchy.
Effects also compose: you can wrap handlers around each other to layer concerns (logging, caching, stress testing) without modifying the pricing code.
2.12 First-Class Modules — Runtime Model Selection
In §2.5 we saw functors: compile-time module-to-module transformations. OCaml also supports first-class modules: packaging a module as a runtime value, storing it in a data structure, and unpacking it where needed. This enables plugin architectures where models are selected at runtime but verified at compile time.
2.12.1 Advanced Functors — Generic Yield Curve
(** Interface: any interpolation scheme *)
module type INTERPOLATION = sig
type t
val create : (float * float) array -> t
val interpolate : t -> float -> float
end
(** Interface: any discount curve *)
module type YIELD_CURVE = sig
val discount : float -> float
val zero_rate : float -> float
val forward_rate : float -> float -> float
end
(** Functor: build a complete yield curve from any interpolation scheme *)
module Make_yield_curve (I : INTERPOLATION) = struct
let knots : (float * float) array ref = ref [||]
let grid : I.t option ref = ref None
let calibrate pairs =
knots := pairs;
grid := Some (I.create pairs)
let zero_rate t =
match !grid with
| None -> failwith "curve not calibrated"
| Some g -> I.interpolate g t
let discount t = exp (-. zero_rate t *. t)
let forward_rate t1 t2 =
-. (log (discount t2) -. log (discount t1)) /. (t2 -. t1)
end
(** Three curve implementations: same bootstrap, different interpolation *)
module Linear_curve = Make_yield_curve(Linear_interpolation)
module Spline_curve = Make_yield_curve(Cubic_spline)
module NS_curve = Make_yield_curve(Nelson_siegel)
The entire bond pricing, swap valuation, and Greeks machinery can be written once against YIELD_CURVE — and any of the three curve implementations can be substituted without changing a line of pricing code.
2.12.2 First-Class Modules for Plugin Model Registry
(** Package a yield curve as a runtime value *)
type curve = (module YIELD_CURVE)
(** Registry: model name -> runtime module *)
let curve_registry : (string, curve) Hashtbl.t = Hashtbl.create 8
let register name m = Hashtbl.set curve_registry ~key:name ~data:m
let () =
register "linear" (module Linear_curve);
register "spline" (module Spline_curve);
register "ns" (module NS_curve)
(** Price a bond using the curve specified in config *)
let price_bond_with_curve ~curve_name ~face ~coupon_rate ~maturity =
match Hashtbl.find curve_registry curve_name with
| None -> Error (Printf.sprintf "Unknown curve model: %s" curve_name)
| Some m ->
let module C = (val m : YIELD_CURVE) in
let n = int_of_float maturity * 2 in
let coupon = face *. coupon_rate /. 2.0 in
let cf_pv = List.init n (fun i ->
let t = float_of_int (i + 1) /. 2.0 in
coupon *. C.discount t
) |> List.fold_left ( +. ) 0.0 in
Ok (cf_pv +. face *. C.discount maturity)
The curve model is selected at runtime (from a config file, a function argument, a user's UI choice), but once unpacked with (val m : YIELD_CURVE), the compiler treats it as a fully typed module. You get the flexibility of runtime polymorphism with the safety of compile-time interface checking. Python achieves the flexibility (duck typing) but not the safety. Java achieves the safety (interfaces) but with considerably more boilerplate.
2.13 PPX Metaprogramming — Automating Financial Boilerplate
OCaml's PPX (preprocessor extension) system allows compile-time code generation driven by type declarations. Unlike C++ macros (text substitution), Java annotation processors (reflection at runtime), or Python decorators (runtime wrapping), PPX operates on the typed AST: it reads a fully type-checked data structure and generates correct, type-checked code from it.
The most widely used PPX in financial OCaml is ppx_deriving, which reads a type definition and generates boilerplate functions.
(** Bond type: PPX derives show, compare, equal, and JSON serialisation *)
type bond = {
isin : string;
issuer : string;
currency : string;
face_value : float;
coupon_rate : float;
maturity : float;
seniority : [`Senior | `Subordinated | `Junior];
} [@@deriving show, compare, equal, yojson]
(** The above automatically generates: *)
(** show_bond : bond -> string (human-readable) *)
(** compare_bond : bond -> bond -> int (for sorting risk reports) *)
(** equal_bond : bond -> bond -> bool (for equality checks) *)
(** bond_to_yojson : bond -> Yojson.t (serialise to JSON) *)
(** bond_of_yojson : Yojson.t -> bond result (deserialise from JSON) *)
(** Credit rating: derived comparison enables Map and Set *)
type credit_rating =
| AAA | AA | A | BBB | BB | B | CCC | D
[@@deriving show, compare, equal]
(** Now usable as a Map key — compare_credit_rating is generated *)
module Rating_map = Map.Make(struct
type t = credit_rating
let compare = compare_credit_rating
end)
For high-performance serialisation, ppx_bin_prot (Jane Street) generates binary encoders/decoders from type definitions — used in Jane Street's internal messaging systems for serialising order records at microsecond latency. For FIX protocol parsing, custom PPX extensions can generate type-safe tag parsers from annotated record fields:
(** FIX ExecutionReport: PPX generates the parser from tag annotations *)
type execution_report = {
cl_ord_id : string; [@fix.tag 11]
exec_id : string; [@fix.tag 17]
exec_type : char; [@fix.tag 150]
ord_status : char; [@fix.tag 39]
symbol : string; [@fix.tag 55]
last_qty : float; [@fix.tag 32]
last_px : float; [@fix.tag 31]
} [@@deriving fix_message]
(* Generates: parse_execution_report : string -> execution_report result *)
(* And: serialise_execution_report : execution_report -> string *)
The generated parser is statically typed: accessing msg.last_px returns a float, not a string that must be parsed manually. Field-tag mismatches are caught at code generation time, not at runtime when a message arrives in production.
2.14 Persistent Data Structures — Immutable Market Data
OCaml's standard library and Core provide persistent (immutable) data structures: maps, sets, and sequences where "updating" produces a new version that shares structure with the old one. The old version is unchanged. This is structurally impossible in Python's dict or C++'s std::map without explicit copying.
For quantitative finance, persistent maps enable:
- Safe market data snapshots: any function receives a snapshot it can read without worrying that another thread or function is mutating it
- Zero-copy scenario branching: each stress scenario is a persistent update of a base snapshot, sharing all unchanged data
- Audit trail: every historical state is preserved and addressable
open Core
(** A market snapshot: immutable by construction *)
type snapshot = {
spots : float String.Map.t;
vols : float String.Map.t;
rates : float String.Map.t;
date : Date.t;
}
(** "Updating" produces a new snapshot; the original is completely unchanged *)
let bump_vol snapshot ticker delta =
let new_vols =
Map.update snapshot.vols ticker ~f:(function
| None -> delta
| Some v -> v +. delta)
in
{ snapshot with vols = new_vols } (* other fields shared, not copied *)
let set_rate snapshot currency new_rate =
{ snapshot with rates = Map.set snapshot.rates ~key:currency ~data:new_rate }
(** Generate N stress scenarios with zero data copying *)
let stress_scenarios base =
[ base; (* base case *)
bump_vol base "AAPL" 0.05; (* +5% vol *)
bump_vol base "AAPL" (-0.05); (* -5% vol *)
set_rate base "USD" (Map.find_exn base.rates "USD" +. 0.01); (* +100bp *)
set_rate base "USD" (Map.find_exn base.rates "USD" -. 0.01) ] (* -100bp *)
(** These 5 snapshots share all unchanged map nodes in memory —
structural sharing makes this O(log n) per scenario, not O(n) *)
The internal representation is a balanced binary tree. Map.update creates at most $O(\log n)$ new nodes (the path from root to the updated key), sharing all other nodes with the original map. For a market data snapshot with 5,000 liquid instruments, bumping a single vol creates at most ~13 new nodes — not 5,000 copies. This makes scenario analysis both memory-efficient and thread-safe: scenarios can be priced in parallel with no risk of data races.
2.15 Polymorphic Variants — Extensible Instrument Taxonomies
OCaml provides two kinds of variant types. The closed variants we have seen (e.g., type option_type = Call | Put) require all constructors to be declared in one place. Polymorphic variants, prefixed with a backtick, are open: they can be used across module boundaries without sharing a single type definition, and functions can operate on subsets of constructors.
This is particularly useful in financial systems where different desks extend a shared instrument taxonomy:
(** Core product types: defined in shared library *)
type core_instrument = [
| `Spot of { ticker : string }
| `Forward of { ticker : string; expiry : float }
| `Option of { ticker : string; strike : float; expiry : float }
]
(** Equity desk adds warrants without touching the shared library *)
type equity_instrument = [
| core_instrument
| `Warrant of { ticker : string; strike : float; expiry : float; ratio : float }
| `Convertible of { ticker : string; conversion_ratio : float }
]
(** A pricer that handles only core_instrument: still compiles on equity_instrument *)
let price_core : [< core_instrument] -> float = function
| `Spot { ticker = _ } -> 100.0 (* simplified: fetch from market data *)
| `Forward { ticker = _; expiry } -> 100.0 *. exp (0.05 *. expiry)
| `Option { strike; expiry; _ } -> Black_scholes.call
~spot:100.0 ~strike ~rate:0.05 ~vol:0.20 ~tau:expiry
(** The equity desk pricer handles the extended set *)
let price_equity : equity_instrument -> float = function
| #core_instrument as ci -> price_core ci (* delegate to core pricer *)
| `Warrant { strike; expiry; ratio } ->
ratio *. Black_scholes.call ~spot:100.0 ~strike ~rate:0.05 ~vol:0.20 ~tau:expiry
| `Convertible { conversion_ratio } ->
conversion_ratio *. 100.0
The type constraint [< core_instrument] means "a subset of core_instrument constructors" — the function accepts any polymorphic variant that is one of the core types, including specialised supersets. The #core_instrument pattern in price_equity matches any constructor from core_instrument, delegating cleanly to the shared pricer. The type checker verifies that the equity pricer handles all constructors in equity_instrument.
2.16 Labelled Arguments as Financial API Design
OCaml's labelled arguments (~name:value) make financial function calls self-documenting at the call site and allow arguments to be supplied in any order. Optional arguments with defaults (?day_count) reduce boilerplate for common cases. Together, these transform a financial API from an opaque sequence of positional parameters into something that reads like a term sheet.
(** The labelled call site reads like a term sheet *)
let price =
Black_scholes.call
~spot:142.50
~strike:145.0
~rate:0.053
~vol:0.2175
~tau:(90.0 /. 365.0)
~dividend_yield:0.014
(** Positional equivalent in C++ — unreadable at the call site:
bs_call(142.50, 145.0, 0.053, 0.2175, 0.246, 0.014) *)
(** Optional arguments with defaults for common conventions *)
let price_bond
~face
~coupon_rate
~maturity
~yield
?(day_count = `Act365) (* optional: default to Act/365 *)
?(frequency = 2) (* optional: default to semi-annual *)
?(settlement_lag = 2) () = (* optional: default to T+2 *)
let yf = match day_count with
| `Act365 -> maturity
| `Act360 -> maturity *. (365.0 /. 360.0)
| `Thirty360 -> maturity
in
let n = int_of_float (yf *. float_of_int frequency) in
let coupon = face *. coupon_rate /. float_of_int frequency in
let df t = exp (-. yield *. t) in
let coupon_pv = List.init n (fun i ->
let t = float_of_int (i + 1) /. float_of_int frequency in
coupon *. df t
) |> List.fold_left ( +. ) 0.0 in
ignore settlement_lag; (* in real code: adjust dates by settlement_lag *)
coupon_pv +. face *. df maturity
(** Minimal call: only required parameters *)
let p1 = price_bond ~face:1000.0 ~coupon_rate:0.05 ~maturity:5.0 ~yield:0.048 ()
(** Override day count and frequency — no positional confusion *)
let p2 = price_bond ~face:1000.0 ~coupon_rate:0.05 ~maturity:5.0 ~yield:0.048
~day_count:`Act360 ~frequency:4 ()
Labels prevent transposition errors: price_bond ~face:1000.0 ~coupon_rate:0.05 cannot accidentally swap face value and coupon rate, because the labels are checked by the compiler. Python keyword arguments achieve a similar ergonomic benefit but without compile-time checking of argument names (a misspelled keyword silently becomes an unexpected keyword error at runtime). C++ and Java have no native equivalent.
2.17 Chapter Summary
OCaml's type system is the central tool for writing correct financial software. This chapter has introduced its core capabilities — type inference, algebraic data types, pattern matching, modules, error handling — alongside the more advanced features that distinguish OCaml from other statically typed languages.
Phantom types (§1.2.1, examples in Chapter 1) make incorrect units and invalid state transitions structurally impossible at zero runtime cost. GADTs (§2.10) enable type-indexed computations where return types vary by constructor, eliminating casts and enforcing model-product compatibility. Algebraic effects (§2.11) provide clean dependency injection for pricing code: the same pricer runs in live, backtest, and stress-test contexts simply by changing the handler. First-class modules (§2.12) allow runtime model selection (yield curve, vol model, optimizer) with compile-time interface verification. PPX (§2.13) automates domain boilerplate — serialisation, comparison, FIX parsing — from type definitions. Persistent data structures (§2.14) enable zero-copy scenario branching and thread-safe market data sharing. Polymorphic variants (§2.15) allow desk-specific instrument extensions without modifying shared libraries. Labelled arguments (§2.16) produce self-documenting financial APIs that read like term sheets.
No single one of these features is individually decisive. Together, they form a language where financial domain knowledge can be encoded directly into the type system — making correct programs easy to write and incorrect programs hard to compile.
Exercises
2.1 Define a complete type hierarchy for a derivatives book: asset_class, product_type, instrument, and trade. Use variants with record payloads where appropriate.
2.2 Implement a Yield_curve module with signature { val discount : float -> float; val forward_rate : float -> float -> float }. Provide two implementations: one for a flat curve, one for a piecewise-linear log-discount curve.
2.3 Write a function solve_newton of type (float -> float) -> (float -> float) -> float -> float that takes f, f' (derivative), and an initial guess, and returns the root. Use it to implement YTM calculation without finite differences.
2.4 Using Result monad-style, write a pipeline that: reads a bond spec from a CSV string, validates all fields, prices the bond, and computes its duration. Each step should return Result.
2.5 Using the phantom type pattern from §2 (and Chapter 1), define a notional type tagged by currency (USD, EUR, GBP). Write add_notionals that only compiles when both operands share the same currency tag.
2.6 Implement the Make_yield_curve functor from §2.12.1. Provide a concrete Linear_interpolation module and construct a Linear_curve. Verify that forward_rate between two knot points equals the expected yield.
2.7 Using the effects pattern from §2.11, write a price_bond_portfolio function that calls perform (Get_rate currency) to fetch discount rates. Implement two handlers: one returning a flat 5% rate, one reading from an (string * float) list passed as a parameter.
2.8 Using ppx_deriving, add [@@deriving show, compare, yojson] to a vanilla_option record type. Write a function that serialises a list of options to a JSON array and deserialises it back, verifying round-trip equality.