Chapter 29 — Systems Design for Quantitative Finance

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


After this chapter you will be able to:

  • Apply event-sourcing and immutable-snapshot patterns to build auditable, reproducible quantitative systems where every risk number is traceable to its inputs
  • Design a layered quant system architecture: market data → curve building → pricing → risk aggregation → reporting, with explicit typed interfaces between layers
  • Use OCaml module signatures as formal contracts between system components, ensuring that the compiler enforces interface compatibility
  • Implement a portfolio P&L attribution engine that decomposes daily P&L into market moves, carry, and unexplained residual
  • Write property-based tests with QCheck to verify that pricing functions satisfy no-arbitrage constraints across randomly generated market scenarios

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:

┌─────────────────────────────────────────────────────────────────────┐
│                    QUANTITATIVE SYSTEM ARCHITECTURE                 │
└─────────────────────────────────────────────────────────────────────┘

  MARKET DATA LAYER                STATIC DATA LAYER
  ┌──────────────────┐             ┌──────────────────┐
  │  Live Feed       │             │  Trade Store     │
  │  (FIX/FAST/UDP)  │             │  (event log)     │
  │  Tick normaliser │             │  Holiday cal.    │
  │  Snapshot store  │             │  Reference data  │
  └────────┬─────────┘             └────────┬─────────┘
           │                                │
           ▼                                ▼
  ┌───────────────────────────────────────────────────┐
  │               CURVE / SURFACE CACHE               │
  │  Yield curves · Vol surfaces · Credit hazard rates│
  │ (immutable snapshots, rebuilt on each market tick)│
  └────────────────────┬──────────────────────────────┘
                       │
                       ▼
  ┌───────────────────────────────────────────────────┐
  │                  PRICING ENGINE                   │
  │  BlackScholes · Binomial · Monte Carlo · CRD PDE  │
  │  Bond pricer · Swap pricer · CDS pricer · XVA     │
  │  Input: instrument + market snapshot (immutable)  │
  │  Output: PV + Greeks (pure function, no side-eff) │
  └────────────────────┬──────────────────────────────┘
                       │
                       ▼
  ┌───────────────────────────────────────────────────┐
  │               RISK AGGREGATION                    │
  │  Portfolio Greeks · VaR / ES · Stress scenarios   │
  │  P&L attribution (carry, delta, vega, residual)   │
  │  Marginal risk contributions · Limit monitoring   │
  └────────────────────┬──────────────────────────────┘
                       │
                       ▼
  ┌───────────────────────────────────────────────────┐
  │                  REPORTING                        │
  │  Risk report · Regulatory (FRTB, XVA)             │
  │  Blotter · P&L explain · Backtesting dashboard    │
  └───────────────────────────────────────────────────┘

  ───────────────────── Cross-cutting concerns ─────────────────────
  Event bus (algebraic effects)  |  Audit log  |  CI property tests

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
      (* Fixed-for-floating swap NPV = notional × (fixed_annuity × fixed_rate - float_annuity)
         Annuity = Σ_{i=1}^{n} τ_i × df(t_i)  where τ_i = payment period in years *)
      let n_periods = int_of_float (s.maturity *. 4.0) in  (* quarterly payments *)
      let dt = s.maturity /. float_of_int n_periods in
      let annuity = ref 0.0 in
      let float_leg = ref 0.0 in
      let prev_df = ref 1.0 in
      for i = 1 to n_periods do
        let t  = float_of_int i *. dt in
        let df = Yield_curve.discount_factor curve t in
        annuity   := !annuity   +. dt *. df;
        float_leg := !float_leg +. (!prev_df -. df);  (* forward rate × dt × df ≈ df_{i-1} - df_i *)
        prev_df   := df
      done;
      let fixed_leg = s.fixed_rate *. !annuity in
      let npv = s.notional *. (fixed_leg -. !float_leg) in
      (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

Quantitative systems often require the ability to "rewind time" — to see exactly what the portfolio risk looked like yesterday at 10:00 AM. Traditional database systems that only store the "current state" cannot do this without complex historical tagging. Event Sourcing solves this by storing the sequence of events as the single source of truth.

29.5.1 The Event Log as Source of Truth

Every action that modifies the system state (booking a trade, amending a strike, cancelling a position) is recorded as an immutable event. The current state is simply the projection of these events.

sequenceDiagram
    participant T as Trader / Feed
    participant L as Event Log (Source of Truth)
    participant S as Snapshot Store
    participant P as Current State (Projection)

    T->>L: Book Trade Event
    L->>P: Apply Event
    T->>L: Amend Trade Event
    L->>P: Apply Event
    Note over P: State is updated in-memory
    L->>S: Periodic Snapshot (State at T)
    Note over S: Faster recovery next time
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 ->
      let state' = { state with active_trades = Hashtbl.copy state.active_trades } in
      Hashtbl.replace state'.active_trades r.Trade.id r;
      state'
    | TradeAmended { id; new_trade; _ } ->
      let state' = { state with active_trades = Hashtbl.copy state.active_trades } in
      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; _ } ->
      let state' = { state with active_trades = Hashtbl.copy state.active_trades } in
      Hashtbl.remove state'.active_trades id;
      { state' with cancelled = id :: state.cancelled }
    | _ -> state (* simplified *)

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

end

29.5.2 Snapshots for Performance

Replaying millions of events on every startup is inefficient. We use Snapshots: periodically (e.g., at end-of-day), we store the serialized state. To reconstruct the state at any time $T$, we:

  1. Load the latest snapshot $S$ where $Timestamp(S) < T$.
  2. Replay only the events that occurred between $Timestamp(S)$ and $T$.

This architectural pattern ensures that the system is reproducible: given the same event log and the same market data snapshots, any risk calculation can be perfectly recreated years later for regulatory audit or backtesting.


29.6 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.7 Property-Based Testing Financial Invariants with QCheck

Unit tests verify one input–output pair at a time. Property-based testing with QCheck verifies that a predicate holds for thousands of randomly generated inputs — and when it fails, automatically shrinks the counterexample to the smallest failing case.

For a trading system, property-based testing is exceptionally powerful: financial invariants such as put-call parity, no-arbitrage bounds, and monotonicity conditions hold for all valid inputs, not just the ones developers thought to test. If an implementation violates them in some corner of the parameter space, QCheck will find it.

(** Install: opam install qcheck qcheck-alcotest *)
open QCheck

(** ── Generators for financial inputs ── *)
let gen_spot    = Gen.float_range 1.0 500.0
let gen_strike  = Gen.float_range 1.0 500.0
let gen_vol     = Gen.float_range 0.01 2.0
let gen_rate    = Gen.float_range 0.0 0.15
let gen_tau     = Gen.float_range 0.01 10.0

(** Combined option parameter generator *)
let gen_option_params =
  Gen.map5 (fun s k v r t -> (s, k, v, r, t))
    gen_spot gen_strike gen_vol gen_rate gen_tau

(** ── Property: put-call parity ── *)
(** C - P = S*exp(-qT) - K*exp(-rT)  for q=0 *)
let test_put_call_parity =
  Test.make ~name:"put_call_parity" ~count:1000
    (make gen_option_params)
    (fun (s, k, v, r, t) ->
      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 forward_pv = s -. k *. exp (-. r *. t) in
      abs_float (call -. put -. forward_pv) < 1e-8)

(** ── Property: call is monotone increasing in vol ── *)
let test_call_vega_positive =
  Test.make ~name:"call_vega_positive" ~count:1000
    (make gen_option_params)
    (fun (s, k, v, r, t) ->
      let bump = 0.001 in
      let c1 = Black_scholes.call ~spot:s ~strike:k ~rate:r ~vol:v ~tau:t in
      let c2 = Black_scholes.call ~spot:s ~strike:k ~rate:r ~vol:(v +. bump) ~tau:t in
      c2 >= c1)

(** ── Property: no-arbitrage lower bound for calls ── *)
(** C ≥ max(0, S - K*exp(-rT)) *)
let test_call_lower_bound =
  Test.make ~name:"call_no_arb_lower_bound" ~count:1000
    (make gen_option_params)
    (fun (s, k, v, r, t) ->
      let c = Black_scholes.call ~spot:s ~strike:k ~rate:r ~vol:v ~tau:t in
      let lb = Float.max 0.0 (s -. k *. exp (-. r *. t)) in
      c >= lb -. 1e-10)

(** ── Property: bond price monotone decreasing in yield ── *)
let test_bond_price_yield_monotone =
  Test.make ~name:"bond_price_monotone_in_yield" ~count:1000
    (make (Gen.pair (Gen.float_range 0.001 0.15) (Gen.float_range 0.001 0.15)))
    (fun (y1, y2) ->
      let price y =
        let face = 1000.0 and coupon = 50.0 and n = 10 in
        let df i = exp (-. y *. float_of_int i) in
        List.init n (fun i -> coupon *. df (i+1))
        |> List.fold_left (+.) 0.0
        |> (+.) (face *. df n)
      in
      if y1 < y2 then price y1 >= price y2
      else price y2 >= price y1)

(** ── Property: discount factor is in (0, 1] for non-negative rates ── *)
let test_discount_factor_bounds =
  Test.make ~name:"discount_factor_in_unit_interval" ~count:1000
    (make (Gen.pair gen_rate gen_tau))
    (fun (r, t) ->
      let df = exp (-. r *. t) in
      df > 0.0 && df <= 1.0)

(** Run all tests with qcheck-alcotest *)
let () =
  let open Alcotest in
  run "Financial Invariants" [
    "black_scholes", [
      QCheck_alcotest.to_alcotest test_put_call_parity;
      QCheck_alcotest.to_alcotest test_call_vega_positive;
      QCheck_alcotest.to_alcotest test_call_lower_bound;
    ];
    "fixed_income", [
      QCheck_alcotest.to_alcotest test_bond_price_yield_monotone;
      QCheck_alcotest.to_alcotest test_discount_factor_bounds;
    ];
  ]

When a property fails, QCheck reports the smallest counterexample it found after shrinking — immediately revealing the edge case (e.g., extremely deep in-the-money options, near-zero time-to-expiry) rather than a random large input. The shrinking process is automatic: generators for float shrink toward zero; generators for structured types shrink each field independently.

Where to apply property-based tests in the trading system:

PropertyFinancial invariantModule
Put-call parityC - P = F - K·e^{-rT}Black_scholes
Monotone in vol∂C/∂σ > 0 (positive vega)Black_scholes
Convexity in yield∂²P/∂y² > 0Bond_pricer
Curve monotonicityP(0,T₁) ≥ P(0,T₂) for T₁ ≤ T₂Yield_curve
Trade roundtripfrom_json (to_json t) = tTrade_store
Event replayreplay events = current_stateEvent_store
Portfolio DV01Σ DV01ᵢ = DV01(portfolio)Risk_engine

Property tests complement unit tests: unit tests fix the expected output for known inputs; property tests verify structural guarantees across all inputs the generator can produce.


29.8 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.6) 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).

Property-based testing with QCheck (§29.7) closes the correctness loop: type-system guarantees prevent structurally incorrect programs; property tests verify that correct-looking programs satisfy financial invariants across the full input space. Together they make a trading system that is difficult to break and easy to verify.

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.6 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.

29.6 ★★ Using QCheck (§29.7), write property tests for the trade store: (a) a trade is always retrievable by ID after insertion; (b) amending a trade preserves its trade ID; (c) cancelling a trade changes its lifecycle state to Cancelled and reduces the portfolio size by one. Run 500 tests and verify all pass.


Next: Chapter 30 — Capstone: A Complete Trading System