Chapter 29 — Systems Design for Quantitative Finance

"A good quant system is like a good trade: clear on the upside, bounded on the downside."


Quantitative finance generates more software than most scientific disciplines — pricing models, risk engines, strategy backtesting frameworks, data pipelines, execution systems, and reporting tools. The quality of this software determines not just the correctness of calculations but the velocity of research (how quickly can a quant test a new model idea?) and the reliability of production (how confident are we that today's risk report matches yesterday's with only the changes we intended?).

The architectural patterns that make quantitative systems reliable are not exotic — they are the standard tools of functional programming applied consistently. Immutability: market data and trade records should be append-only; a pricing run should never modify its inputs. Type safety: instrument types should be sum types that force exhaustive handling in every computation that touches them. Separation of concerns: the market data layer, the pricing layer, the risk layer, and the reporting layer should be decoupled modules with explicit interfaces. Auditability: every trade and every risk number should be traceable to its inputs through an event log.

OCaml is exceptionally well-suited for this style of systems design. Its module system provides the cleanest abstraction mechanism in any industry language: a module signature specifies exactly what a component exposes, and the compiler enforces that the implementation matches the signature. Algebraic data types make illegal states unrepresentable. The Result type makes error handling explicit. And OCaml's performance means that the safety guarantees don't come at the cost of speed.


29.1 Architecture Patterns

Modern quant systems have distinct tiers:

Market Data → [Normalisation] → [Risk Engine] → [Position/P&L] → [Reporting]
                                      ↑
                               [Pricing Library]
                                      ↑
                           [Curve/Surface Cache]

Key design principles:

  • Immutability: market data and curves should be immutable snapshots
  • Versioned state: each end-of-day snapshot is a distinct record
  • Event sourcing: record all trades and market events; compute state by replay
  • Type safety: phantom types prevent misuse (currency, day count, etc.)

29.2 Market Data Management

module Market_data = struct

  (** Immutable market data snapshot at a specific timestamp *)
  type t = {
    timestamp    : int64;                          (* epoch nanoseconds *)
    ir_curves    : (string, Yield_curve.t) Hashtbl.t;
    equity_vols  : (string, Iv_surface.t) Hashtbl.t;
    fx_rates     : (string, float) Hashtbl.t;
    credit_curves: (string, Credit.credit_curve) Hashtbl.t;
  }

  let create ts = {
    timestamp     = ts;
    ir_curves     = Hashtbl.create 16;
    equity_vols   = Hashtbl.create 64;
    fx_rates      = Hashtbl.create 32;
    credit_curves = Hashtbl.create 32;
  }

  (** Bump a single curve for sensitivity calculation (returns new snapshot) *)
  let bump_curve md ~curve_id ~bump_fn =
    let md' = { md with
      ir_curves = Hashtbl.copy md.ir_curves } in
    Hashtbl.find_opt md'.ir_curves curve_id
    |> Option.iter (fun c ->
        Hashtbl.replace md'.ir_curves curve_id (bump_fn c));
    md'

  (** Parallel shift of all IR curves (+1bp) *)
  let ir_parallel_shift md ~bps =
    let md' = { md with ir_curves = Hashtbl.copy md.ir_curves } in
    Hashtbl.iter (fun k c ->
      Hashtbl.replace md'.ir_curves k (Yield_curve.shift c (bps /. 10000.0))
    ) md.ir_curves;
    md'

end

29.3 Trade Representation

module Trade = struct

  (** Sum type covering all instrument types in the system *)
  type t =
    | EuropeanOption of {
        underlying : string;
        call_put   : [`Call | `Put];
        strike     : float;
        expiry     : float;
        notional   : float;
      }
    | IrSwap of {
        fixed_rate  : float;
        maturity    : float;
        pay_receive : [`Pay | `Receive];
        notional    : float;
        currency    : string;
      }
    | CreditDefaultSwap of {
        reference  : string;
        maturity   : float;
        spread_bps : float;
        recovery   : float;
        notional   : float;
        buy_sell   : [`Buy_prot | `Sell_prot];
      }
    | Bond of Bond.t
    | FxForward of {
        ccy_pair   : string;
        rate       : float;
        notional   : float;
        maturity   : float;
        direction  : [`Buy | `Sell];
      }

  type trade_record = {
    id       : string;
    trade    : t;
    book     : string;
    trader   : string;
    cpty     : string;
    entered  : int64;
  }

  (** Dispatch pricing to correct pricer given market data *)
  let price record md =
    match record.trade with
    | EuropeanOption o ->
      let spot = Hashtbl.find md.Market_data.equity_vols o.underlying
                 |> fun vs -> vs.Iv_surface.spot in
      let ivol = 0.25 in (* lookup from surface *)
      let rate = 0.03 in
      o.notional *. (match o.call_put with
        | `Call -> Black_scholes.call ~spot ~strike:o.strike ~rate ~vol:ivol ~tau:o.expiry
        | `Put  -> Black_scholes.put  ~spot ~strike:o.strike ~rate ~vol:ivol ~tau:o.expiry)
    | IrSwap s ->
      let curve_id = "USD.OIS" in
      let _curve   = Hashtbl.find md.Market_data.ir_curves curve_id in
      let npv = s.notional *. (s.maturity *. 0.001) in (* placeholder *)
      (match s.pay_receive with `Pay -> -. npv | `Receive -> npv)
    | _ -> 0.0   (* other types: exercise *)

end

29.4 Sensitivity and Greek Reporting

module Greeks_report = struct

  type sensitivity = {
    trade_id : string;
    greek    : string;
    value    : float;
    unit     : string;
  }

  (** Bump-and-reprice for any Greek *)
  let bump_reprice ~trade_record ~md ~bump_fn ~bump_size =
    let base  = Trade.price trade_record md in
    let md_up = bump_fn md in
    let up    = Trade.price trade_record md_up in
    (up -. base) /. bump_size

  let delta_report ~trade_record ~md ~bump_bps =
    let bump = bump_bps /. 10000.0 in
    bump_reprice ~trade_record ~md
      ~bump_fn:(fun m -> Market_data.bump_curve m ~curve_id:"EQUITY"
                          ~bump_fn:(fun s -> s *. (1.0 +. bump)))
      ~bump_size:bump

  let dv01_report ~trade_record ~md =
    bump_reprice ~trade_record ~md
      ~bump_fn:(fun m -> Market_data.ir_parallel_shift m ~bps:1.0)
      ~bump_size:0.0001

end

29.5 Event Sourcing for Trade Lifecycle

module Trade_events = struct

  type event =
    | TradeBooked     of Trade.trade_record
    | TradeAmended    of { id: string; new_trade: Trade.t; reason: string }
    | TradeCancelled  of { id: string; reason: string; time: int64 }
    | TradeSettled    of { id: string; settlement_price: float; time: int64 }

  type state = {
    active_trades   : (string, Trade.trade_record) Hashtbl.t;
    cancelled       : string list;
    settled         : (string * float) list;
  }

  let empty_state () = { active_trades = Hashtbl.create 64; cancelled = []; settled = [] }

  let apply_event state event =
    match event with
    | TradeBooked r ->
      Hashtbl.add state.active_trades r.Trade.id r;
      state
    | TradeAmended { id; new_trade; _ } ->
      Hashtbl.find_opt state.active_trades id
      |> Option.iter (fun r ->
          Hashtbl.replace state.active_trades id { r with Trade.trade = new_trade });
      state
    | TradeCancelled { id; _ } ->
      Hashtbl.remove state.active_trades id;
      { state with cancelled = id :: state.cancelled }
    | TradeSettled { id; settlement_price; _ } ->
      Hashtbl.remove state.active_trades id;
      { state with settled = (id, settlement_price) :: state.settled }

  let replay events =
    List.fold_left apply_event (empty_state ()) events

end

29.6 Chapter Summary

Quantitative finance systems have distinctive requirements that make functional design patterns especially valuable. Correctness is paramount: a bug that misprices a derivative or miscalculates a risk limit can cause losses that dwarf the engineering cost of prevention. Auditability is required: regulators and risk managers must be able to reconstruct any historical calculation from its inputs. And the domain is complex enough that good abstractions — which hide irrelevant detail while exposing relevant structure — substantially reduce both bugs and development time.

Immutable market data snapshots are the correct model for pricing: each pricing run receives a consistent view of the world (a yield curve, a set of implied volatilities, a set of spot prices) and produces deterministic outputs. Mutable global state makes it impossible to run two pricing calculations in parallel or to reproduce a historical calculation without exactly reconstructing the global state at that time. The snapshot model avoids both problems.

Sum types for instruments encode the domain correctly: a Trade.t that is a variant over all supported instrument types forces every calculation that touches it to handle every case. When a new instrument type is added, the compiler immediately identifies every function that needs updating. This is the most powerful correctness tool available — far more reliable than runtime type checks or documentation.

Event sourcing models the full lifecycle of a trade as a sequence of immutable events: creation, amendment, novation, maturity, cancellation. The current state is derived by replaying this event log, which gives the system a complete audit trail and the ability to recompute historical risk numbers at any past date. The event log is the single source of truth; the current state is a cached projection of it.



29.5 Algebraic Effects for Event-Driven Trade Processing

Production trading systems are event-driven: actions (trade creation, amendment, settlement) emit events that trigger downstream processing (persistence, risk recalculation, regulatory reporting, client notification). The traditional implementation uses callbacks, observer pattern, or message queues — each coupling the business logic to the notification mechanism.

OCaml 5's algebraic effects provide a cleaner separation. Business logic performs effects; infrastructure handles them. The business logic has no dependency on the infrastructure, making it trivially testable and swappable:

(** Trade lifecycle effects *)
effect Trade_created   : trade_record -> unit
effect Trade_amended   : trade_record -> unit
effect Trade_settled   : trade_record -> unit
effect Risk_recalc     : string -> unit    (* portfolio_id *)
effect Send_confirm    : string * string -> unit  (* counterparty_id * message *)
effect Persist         : trade_record -> unit
effect Audit_log       : string -> unit

(** Business logic: performs effects, agnostic of implementation *)
let create_trade ~id ~instrument ~notional ~currency ~counterparty =
  let t = { trade_id = id; instrument; notional; currency;
             counterparty; trade_date = "2026-01-01";
             lifecycle_state = `Pending; price = None } in
  perform (Persist t);                      (* save to database *)
  perform (Audit_log (Printf.sprintf "Created trade %s" id));
  perform (Trade_created t);                (* notify downstream *)
  perform (Risk_recalc counterparty);       (* trigger risk calc *)
  perform (Send_confirm (counterparty, Printf.sprintf "Trade %s created" id));
  t

let settle_trade_event t =
  let settled = { t with lifecycle_state = `Settled } in
  perform (Persist settled);
  perform (Audit_log (Printf.sprintf "Settled trade %s" t.trade_id));
  perform (Trade_settled settled);
  perform (Risk_recalc t.counterparty);
  settled

(** Production handler: routes effects to real infrastructure *)
let run_production f =
  match f () with
  | v -> v
  | effect (Persist t)           k -> Db.save_trade t;            continue k ()
  | effect (Audit_log msg)       k -> Audit.write msg;             continue k ()
  | effect (Trade_created t)     k -> Event_bus.publish `Created t; continue k ()
  | effect (Trade_settled t)     k -> Event_bus.publish `Settled t; continue k ()
  | effect (Risk_recalc pid)     k -> Risk_engine.queue_recalc pid; continue k ()
  | effect (Send_confirm (cp,m)) k -> Messaging.send cp m;          continue k ()

(** Test handler: captures effects for assertion, no real I/O *)
let run_test f =
  let events  : string list ref = ref [] in
  let persisted : trade_record list ref = ref [] in
  let result = match f () with
    | v -> v
    | effect (Persist t)           k -> persisted := t :: !persisted; continue k ()
    | effect (Audit_log msg)       k -> events :=  ("audit:" ^ msg) :: !events; continue k ()
    | effect (Trade_created t)     k -> events := ("created:" ^ t.trade_id) :: !events; continue k ()
    | effect (Trade_settled t)     k -> events := ("settled:" ^ t.trade_id) :: !events; continue k ()
    | effect (Risk_recalc pid)     k -> events := ("risk:" ^ pid) :: !events; continue k ()
    | effect (Send_confirm (cp,_)) k -> events := ("confirm:" ^ cp) :: !events; continue k ()
  in
  (result, List.rev !persisted, List.rev !events)

(** Test: zero real I/O, full business logic coverage *)
let test_create_trade () =
  let (t, persisted, events), (), _ =
    run_test (fun () -> create_trade
      ~id:"T001" ~instrument:"AAPL" ~notional:100000.0
      ~currency:"USD" ~counterparty:"CP1")
  in
  assert (t.trade_id = "T001");
  assert (List.length persisted = 1);
  assert (List.mem "created:T001" events);
  Printf.printf "test_create_trade: PASS\n"

The business logic (create_trade, settle_trade_event) is completely decoupled from infrastructure. Switching from Db.save_trade to a different ORM, changing the event bus implementation, or disabling notifications for a batch processing run requires only swapping the handler — the business logic is unchanged. The test handler captures all effects in lists without touching any real system, enabling complete unit tests with zero mocking boilerplate.

This pattern — effects as a dependency injection mechanism — scales to the entire trading system lifecycle: trade booking, risk calculation, settlement, regulatory reporting, and client notification all become effects that different handlers route to different implementations. The handler at the top of the stack determines the deployment context (live trading, backtest, stress test, unit test) without any business logic code change.


29.6 Chapter Summary

A production derivatives pricing and risk system is not a collection of individual algorithms but an integrated architecture: a domain model that faithfully represents financial instruments, a trade store that manages the trade lifecycle, a pricing engine that computes present values and Greeks on demand, and a risk calculation layer that aggregates sensitivities to produce actionable hedging recommendations.

OCaml's type system serves a dual role in this architecture. At the domain model level, sum types and phantom types encode the invariants of the financial domain directly: a trade.t that is an exhaustive variant over all supported instrument types, a lifecycle state phantom type that prevents post-settlement operations on pre-settlement trades, and currency phantom types that prevent adding GBP to USD without explicit conversion. At the module level, signatures and functors create composable, testable components that can be assembled differently in live, backtesting, and stress-testing contexts.

Sum types for instruments encode the domain correctly: a Trade.t that is a variant over all supported instrument types forces every calculation that touches it to handle every case. When a new instrument type is added, the compiler immediately identifies every function that needs updating. This is the most powerful correctness tool available — far more reliable than runtime type checks or documentation.

Event sourcing models the full lifecycle of a trade as a sequence of immutable events: creation, amendment, novation, maturity, cancellation. The current state is derived by replaying this event log, which gives the system a complete audit trail and the ability to recompute historical risk numbers at any past date. The event log is the single source of truth; the current state is a cached projection of it.

Algebraic effects (§29.5) provide the cleanest mechanism for event-driven trade processing: business logic performs effects, infrastructure handles them, and the two are fully decoupled. The same business logic code runs in production (with real database and messaging handlers) and in tests (with capturing handlers that collect effects for assertion).

For a comprehensive treatment of how these features compose into end-to-end correctness, see Appendix F.


Exercises

29.1 Build a full trade store using a hashtable of Trade.trade_records. Add trades, amend one, cancel one, and compute total portfolio delta.

29.2 Implement parallel DV01 calculation across 100 trades using OCaml 5 Domains and compare to sequential.

29.3 Implement an event-sourced trade book that reconstructs portfolio state by replaying an event log from disk.

29.4 Add a scenario analysis module: apply a ±10bp IR shift, ±10% equity shock, and ±100bp credit spread shock to the portfolio and report total P&L impact for each scenario.

29.5 Implement a run_backtest handler for the effects in §29.5 that: (a) suppresses all messaging effects; (b) routes Persist to an in-memory Hashtbl instead of a database; (c) routes Risk_recalc to an immediate synchronous calculation instead of queuing. Verify that the same business logic (create_trade, settle_trade_event) runs unchanged under both run_production and run_backtest.


Next: Chapter 30 — Capstone: A Complete Trading System