Chapter 30 — Capstone: A Complete Trading System

"Real systems are built incrementally, tested relentlessly, and deployed cautiously."


This final chapter assembles the mathematical models, numerical algorithms, and engineering patterns from the previous 29 chapters into a single coherent trading system. The goal is not production-ready code — that requires years of engineering and operational hardening well beyond the scope of a book — but to demonstrate that the individual components we have built are composable: they can be wired together into a system that captures the essential structure and behaviour of a professional quantitative trading operation.

The system we build has eight layers: market data ingestion, reference data and instrument description, curve building (yield curves, dividend curves, vol surfaces), instrument pricing, Greeks calculation, portfolio risk aggregation, pre-trade and post-trade risk checks, and P&L attribution. Each layer corresponds to one or more chapters in this book. The yield curve bootstrapper from Chapter 7 feeds the interest rate pricer from Chapter 8. The implied volatility surface from Chapter 13 feeds the Black-Scholes pricer from Chapter 10 and the Monte Carlo engine from Chapter 12. The Greeks from Chapter 19 feed the risk limits in Chapter 18.

What makes this integration interesting — and difficult — is the data flow between layers. Curve building must happen before pricing; pricing must happen before risk calculation; risk calculations must complete before pre-trade checks. OCaml's module system and type signatures make these dependencies explicit: a function that requires a yield curve receives a Yield_curve.t parameter that can only be created by the curve-building module, making it impossible to call the pricer with stale or inconsistent market data.


30.1 System Overview

This capstone integrates every major module introduced throughout the book into a single cohesive trading system. The architecture follows the pipeline:

[Market Data Feed]
        ↓
[Normalisation & Curve Building]
        ↓
[Signal Generation]
        ↓
[Option Pricing & Greeks]
        ↓
[Risk Limits Check]
        ↓
[Execution Engine]
        ↓
[P&L Attribution]
        ↓
[End-of-Day Reporting]

30.2 System Configuration

module Config = struct

  type t = {
    risk_limits     : Risk_limits.t;
    pricing_params  : Pricing_params.t;
    execution_params: Execution_params.t;
    data_sources    : string list;
    output_dir      : string;
  }

  and risk_limits = {
    max_portfolio_delta  : float;
    max_portfolio_vega   : float;
    max_dv01            : float;
    max_single_trade_pnl : float;
    max_drawdown_pct     : float;
  }

  and pricing_params = {
    ir_curve_id     : string;
    vol_surface_id  : string;
    num_mc_paths    : int;
    num_fd_steps    : int;
  }

  and execution_params = {
    default_algo    : [`TWAP | `VWAP | `IS];
    max_participation_rate : float;
    venue_priority  : string list;
  }

  let default = {
    risk_limits = {
      max_portfolio_delta   = 100.0;
      max_portfolio_vega    = 50_000.0;
      max_dv01              = 10_000.0;
      max_single_trade_pnl  = 500_000.0;
      max_drawdown_pct      = 5.0;
    };
    pricing_params = {
      ir_curve_id    = "USD.OIS";
      vol_surface_id = "SPX.VOLS";
      num_mc_paths   = 50_000;
      num_fd_steps   = 200;
    };
    execution_params = {
      default_algo           = `VWAP;
      max_participation_rate = 0.20;
      venue_priority         = ["NYSE"; "NASDAQ"; "CBOE"];
    };
    output_dir = "/var/log/trading";
  }

end

30.3 Signal Generation Layer

module Signal_engine = struct

  type signal = {
    instrument  : string;
    direction   : [`Long | `Short | `Flat];
    conviction  : float;   (* 0.0 – 1.0 *)
    strategy    : string;
    reason      : string;
  }

  (** Momentum signal: compare 20d and 60d moving average *)
  let momentum_signal ~prices ~window_short ~window_long =
    let n = Array.length prices in
    if n < window_long then None
    else begin
      let avg arr i w =
        let s = ref 0.0 in
        for j = i - w + 1 to i do s := !s +. arr.(j) done;
        !s /. float_of_int w
      in
      let ma_s = avg prices (n-1) window_short in
      let ma_l = avg prices (n-1) window_long in
      let direction = if ma_s > ma_l then `Long else if ma_s < ma_l then `Short else `Flat in
      let conviction = abs_float (ma_s -. ma_l) /. ma_l in
      Some { instrument = ""; direction; conviction; strategy = "momentum"; reason = "" }
    end

  (** Mean-reversion signal based on z-score *)
  let mean_reversion_signal ~prices ~lookback ~entry_z ~exit_z =
    let n = Array.length prices in
    if n < lookback then None
    else begin
      let mean = Array.sub prices (n - lookback) lookback
                 |> Array.fold_left (+.) 0.0 |> fun s -> s /. float_of_int lookback in
      let std  = Array.sub prices (n - lookback) lookback
                 |> Array.map (fun x -> (x -. mean) ** 2.0)
                 |> Array.fold_left (+.) 0.0
                 |> fun s -> sqrt (s /. float_of_int lookback) in
      let z    = (prices.(n-1) -. mean) /. (std +. 1e-12) in
      if abs_float z < exit_z then
        Some { instrument = ""; direction = `Flat;  conviction = 0.0; strategy = "mean_rev"; reason = "exit" }
      else if z >  entry_z then
        Some { instrument = ""; direction = `Short; conviction = min 1.0 (abs_float z /. entry_z); strategy = "mean_rev"; reason = "sell" }
      else if z < -. entry_z then
        Some { instrument = ""; direction = `Long;  conviction = min 1.0 (abs_float z /. entry_z); strategy = "mean_rev"; reason = "buy" }
      else
        None
    end

end

30.4 Risk Check Layer

module Risk_check = struct

  type result =
    | Approved
    | Rejected of string list   (* list of breach messages *)

  let check_limits ~config ~portfolio_greeks =
    let open Config in
    let l = config.risk_limits in
    let g = portfolio_greeks in
    let breaches = ref [] in
    let check label value limit =
      if abs_float value > limit then
        breaches := Printf.sprintf "%s breach: %.2f > %.2f" label value limit :: !breaches
    in
    check "Delta"    g.Greeks_report.delta l.max_portfolio_delta;
    check "Vega"     g.Greeks_report.vega  l.max_portfolio_vega;
    check "DV01"     g.Greeks_report.dv01  l.max_dv01;
    if !breaches = [] then Approved
    else Rejected !breaches

  (** Pre-trade check: would adding this trade breach limits? *)
  let pre_trade_check ~config ~portfolio_greeks ~incremental_greeks =
    let combined = Greeks_report.{
      delta   = portfolio_greeks.delta +. incremental_greeks.delta;
      gamma   = portfolio_greeks.gamma +. incremental_greeks.gamma;
      vega    = portfolio_greeks.vega  +. incremental_greeks.vega;
      theta   = portfolio_greeks.theta +. incremental_greeks.theta;
      rho     = portfolio_greeks.rho   +. incremental_greeks.rho;
      dv01    = portfolio_greeks.dv01  +. incremental_greeks.dv01;
    } in
    check_limits ~config ~portfolio_greeks:combined

end

30.5 Execution Pipeline

module Execution_pipeline = struct

  type execution_request = {
    instrument  : string;
    direction   : [`Buy | `Sell];
    quantity    : float;
    algo        : [`TWAP | `VWAP | `IS | `Immediate];
    urgency     : float;  (* 0 = patient, 1 = urgent *)
  }

  type execution_result = {
    avg_price : float;
    quantity  : float;
    slippage  : float;
    venue     : string;
    fill_time : int64;
  }

  (** Simulate VWAP execution with square-root market impact *)
  let execute_vwap ~request ~mid_price ~adv ~risk_aversion ~horizon =
    let eta   = 0.1 in                  (* temporary impact coefficient *)
    let sigma = 0.02 in                 (* daily vol *)
    let n     = request.quantity /. adv in  (* participation rate *)
    let impact = eta *. sigma *. sqrt n in
    let direction_sign = match request.direction with `Buy -> 1.0 | `Sell -> -1.0 in
    let avg_price = mid_price *. (1.0 +. direction_sign *. impact
                                  +. 0.5 *. risk_aversion *. sigma ** 2.0 *. horizon) in
    let slippage  = direction_sign *. (avg_price -. mid_price) in
    { avg_price; quantity = request.quantity; slippage;
      venue = "NASDAQ"; fill_time = Int64.of_int 0 }

end

30.6 P&L Attribution Engine

module Pnl_attribution = struct

  type component = {
    delta_pnl   : float;
    gamma_pnl   : float;
    vega_pnl    : float;
    theta_pnl   : float;
    unexplained : float;
    total       : float;
  }

  (** One day P&L attribution for a single position *)
  let attribute ~delta ~gamma ~vega ~theta
                ~ds    (* spot return *)
                ~dvol  (* vol change *)
                ~spot  (* beginning-of-day spot *)
                ~dt    =
    let delta_pnl = delta *. ds *. spot in
    let gamma_pnl = 0.5 *. gamma *. (ds *. spot) ** 2.0 in
    let vega_pnl  = vega  *. dvol in
    let theta_pnl = theta *. dt in
    let first_order = delta_pnl +. gamma_pnl +. vega_pnl +. theta_pnl in
    (* In a real system, total would come from repricing *)
    let total       = first_order in
    let unexplained = total -. first_order in
    { delta_pnl; gamma_pnl; vega_pnl; theta_pnl; unexplained; total }

  let print_report ch pnl =
    Printf.fprintf ch "=== P&L Attribution ===\n";
    Printf.fprintf ch "  Delta:       %+.2f\n" pnl.delta_pnl;
    Printf.fprintf ch "  Gamma:       %+.2f\n" pnl.gamma_pnl;
    Printf.fprintf ch "  Vega:        %+.2f\n" pnl.vega_pnl;
    Printf.fprintf ch "  Theta:       %+.2f\n" pnl.theta_pnl;
    Printf.fprintf ch "  Unexplained: %+.2f\n" pnl.unexplained;
    Printf.fprintf ch "  Total:       %+.2f\n" pnl.total

end

30.7 End-of-Day Report

module Eod_report = struct

  type t = {
    date           : string;
    num_trades     : int;
    portfolio_npv  : float;
    daily_pnl      : float;
    cumulative_pnl : float;
    var_95         : float;
    max_drawdown   : float;
    greeks         : Greeks_report.portfolio_greeks;
    pnl_attr       : Pnl_attribution.component;
    risk_breaches  : string list;
  }

  let print_report ch r =
    Printf.fprintf ch "==========================================================\n";
    Printf.fprintf ch " END OF DAY RISK REPORT — %s\n" r.date;
    Printf.fprintf ch "==========================================================\n";
    Printf.fprintf ch " Trades today:    %d\n" r.num_trades;
    Printf.fprintf ch " Portfolio NPV:   %+.2f\n" r.portfolio_npv;
    Printf.fprintf ch " Daily P&L:       %+.2f\n" r.daily_pnl;
    Printf.fprintf ch " Cumulative P&L:  %+.2f\n" r.cumulative_pnl;
    Printf.fprintf ch " VaR (95%%):       %.2f\n" r.var_95;
    Printf.fprintf ch " Max Drawdown:    %.2f%%\n" (r.max_drawdown *. 100.0);
    Printf.fprintf ch "\n Greeks:\n";
    Printf.fprintf ch "   Delta:  %+.4f\n" r.greeks.delta;
    Printf.fprintf ch "   Gamma:  %+.6f\n" r.greeks.gamma;
    Printf.fprintf ch "   Vega:   %+.2f\n" r.greeks.vega;
    Printf.fprintf ch "   Theta:  %+.2f\n" r.greeks.theta;
    Printf.fprintf ch "   DV01:   %+.2f\n" r.greeks.dv01;
    if r.risk_breaches <> [] then begin
      Printf.fprintf ch "\n *** RISK BREACHES ***\n";
      List.iter (Printf.fprintf ch "   ! %s\n") r.risk_breaches
    end;
    Pnl_attribution.print_report ch r.pnl_attr;
    Printf.fprintf ch "==========================================================\n"

end

30.8 Putting It All Together

(** Main trading loop — simplified event-driven version *)
let run_trading_day ~config ~event_log ~prices_today ~prices_yesterday =

  (* 1. Replay trade events to get current portfolio *)
  let state       = Trade_events.replay event_log in
  let trade_list  = Hashtbl.fold (fun _ v acc -> v :: acc) state.active_trades [] in

  (* 2. Build market data snapshot *)
  let md          = Market_data.create (Int64.of_int 0) in

  (* 3. Price portfolio *)
  let npv = List.fold_left (fun acc r -> acc +. Trade.price r md) 0.0 trade_list in

  (* 4. Compute portfolio Greeks *)
  let greeks = List.fold_left (fun acc r ->
    let dv01 = Greeks_report.dv01_report ~trade_record:r ~md in
    { acc with Greeks_report.dv01 = acc.dv01 +. dv01 }
  ) Greeks_report.zero trade_list in

  (* 5. Risk check *)
  let risk_result = Risk_check.check_limits ~config ~portfolio_greeks:greeks in
  let breaches = match risk_result with
    | Risk_check.Approved    -> []
    | Risk_check.Rejected bs -> bs
  in

  (* 6. Signal generation *)
  let _signals = Signal_engine.momentum_signal
    ~prices:prices_today ~window_short:20 ~window_long:60 in

  (* 7. P&L attribution *)
  let ds   = (prices_today.(Array.length prices_today - 1) -.
              prices_yesterday.(Array.length prices_yesterday - 1))
             /. prices_yesterday.(Array.length prices_yesterday - 1) in
  let spot = prices_yesterday.(Array.length prices_yesterday - 1) in
  let pnl_attr = Pnl_attribution.attribute
    ~delta:greeks.delta ~gamma:greeks.gamma ~vega:greeks.vega ~theta:greeks.theta
    ~ds ~dvol:0.001 ~spot ~dt:(1.0 /. 252.0) in

  (* 8. Build and print EOD report *)
  let report = Eod_report.{
    date           = "2025-01-01";
    num_trades     = List.length trade_list;
    portfolio_npv  = npv;
    daily_pnl      = pnl_attr.total;
    cumulative_pnl = pnl_attr.total;  (* from a real P&L history *)
    var_95         = abs_float npv *. 0.05;
    max_drawdown   = 0.02;
    greeks;
    pnl_attr;
    risk_breaches  = breaches;
  } in
  Eod_report.print_report stdout report

30.9 Testing Strategy

Good financial systems require multiple test layers:

module Tests = struct

  (** Unit test: Black-Scholes call-put parity *)
  let test_put_call_parity () =
    let s = 100.0 and k = 100.0 and r = 0.05 and v = 0.2 and t = 1.0 in
    let call = Black_scholes.call ~spot:s ~strike:k ~rate:r ~vol:v ~tau:t in
    let put  = Black_scholes.put  ~spot:s ~strike:k ~rate:r ~vol:v ~tau:t in
    let parity = call -. put -. s +. k *. exp (-. r *. t) in
    assert (abs_float parity < 1e-10);
    Printf.printf "Put-call parity: PASS\n"

  (** Integration test: bond repricing after curve shift *)
  let test_dv01_consistency () =
    let bond = Bond.{ face = 100.0; coupon = 0.05; maturity = 5.0; frequency = 2 } in
    let curve_flat rate = Yield_curve.flat rate in
    let price r = Bond.price bond (curve_flat r) in
    let p0   = price 0.04 in
    let p_up = price 0.041 in
    let approx_dv01 = (p0 -. p_up) /. 10.0 in  (* per bp *)
    let calc_dv01   = Bond.dv01 bond (curve_flat 0.04) in
    assert (abs_float (approx_dv01 -. calc_dv01) < 1e-4);
    Printf.printf "DV01 consistency: PASS\n"

  (** Regression test: VaR should not exceed portfolio notional *)
  let test_var_bound () =
    let returns = Array.init 250 (fun i ->
      0.01 *. sin (float_of_int i)) in
    let notional = 1_000_000.0 in
    let var = Market_risk.historical_var returns 0.95 *. notional in
    assert (abs_float var < notional);
    Printf.printf "VaR bound:        PASS\n"

  let run_all () =
    test_put_call_parity ();
    test_dv01_consistency ();
    test_var_bound ()

end

30.10 Chapter Summary

This capstone chapter demonstrates that the 29 chapters of this book form a genuine system, not merely a collection of isolated models. The mathematical finance (stochastic calculus, risk-neutral pricing, term structure models) provides the theoretical foundation. The numerical methods (finite differences, Monte Carlo, tree methods) make those theories computable. The OCaml engineering patterns (sum types, modules, event sourcing, functional design) make the computations correct and maintainable.

A complete quantitative trading system has three broad layers. The market data layer ingests and validates prices, rates, and volatilities from external sources, constructs derived objects (yield curves, discount factors, implied vol surfaces), and provides a consistent snapshot to downstream consumers. The analytics layer uses these market inputs to price instruments, compute Greeks, run scenario analyses, and aggregate risk across the portfolio. The operations layer implements pre-trade checks (position limits, notional limits, Greek limits), execution (order submission and management), post-trade processing (confirmations, reconciliation), and reporting (P&L, risk reports, regulatory submissions).

P&L attribution is the validation mechanism for the analytics layer: it decomposes each day's actual P&L into Greek components (delta P&L = $\Delta \cdot \delta S$, gamma P&L = $\frac{1}{2}\Gamma (\delta S)^2$, vega P&L = $V \cdot \delta\sigma$, theta P&L = $\Theta \cdot \delta t$), and the residual measures unexplained P&L. A well-calibrated model should have small and unbiased daily residuals. Large residuals indicate model error, coding errors, or corporate actions that were not properly captured.

Layered testing — unit tests for individual functions, integration tests for module interactions, regression tests comparing output to known-good values — is not a bureaucratic overhead but a necessary investment. Financial computations compound: an error in discount factor computation propagates to every NPV, every CVA, every risk number in the system. The only way to know the system is correct is to test it at every level.


Exercises

30.1 Extend the system to handle a 50-trade portfolio of mixed instruments (equities, options, swaps). Run the full EOD report pipeline for a hypothetical trading day.

30.2 Add a CVA overlay (from Chapter 20) to the portfolio NPV. For each OTC trade, add the CVA to the pricing calculation and include it in the EOD report.

30.3 Implement a real-time P&L monitor using OCaml Domains: one domain prices the portfolio continuously as spot moves, another domain monitors VaR in parallel.

30.4 Add persistence: serialise each Trade.trade_record to JSON using Yojson and reload the portfolio from disk on startup.


Next: Appendix A — OCaml Setup and Tooling