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.