Quantitative Finance · Book 15 · Technology

Research, Data and Risk Platforms

Research, Data and Risk Platforms · Technology

21Trade Capture and Booking

A swap of $100 million was amended to $150 million on Monday, novated to another dealer on Wednesday and partially terminated on Friday morning. On Friday afternoon the risk system held version 3 ($150 million, the new dealer), the confirmation system version 2 ($150 million, the old dealer) and the settlement system version 4 ($90 million, the new dealer). Each had copied the trade when something it cared about happened, and none of them had followed what happened to it. This chapter treats a trade as the result of its events: captured once in a canonical model, booked by rule, allocated by a stated rule, and changed only by events that every downstream system reads.

21.1 What a trade is, as data

Definition 21.1 (Canonical trade model)

A canonical trade model is the firm’s single representation of a trade shared by every system that handles it: a header (identifiers, parties, book, dates, status, version) and the economics (the instrument, quantity and price), with rules that every valid trade satisfies.

Definition 21.2 (Unique transaction identifier)

A unique transaction identifier (UTI) identifies one transaction across all the parties and repositories that report it: under the CPMI–IOSCO technical guidance, the legal entity identifier (Book 2, chapter 28) of the entity that generates it followed by a value unique for that entity, at most 52 characters, upper-case letters and digits only.

The chapter’s model (firm.tradecap) keeps the instrument in the form Book 5’s pricing library serialises it, so that a captured trade is priced, risk-managed and reported from the same record; the validator checks the identifiers’ formats, that buyer and seller differ, that settlement does not precede the trade, and that the quantity is positive. The industry’s own models — FpML for messages, ISDA’s Common Domain Model for events — follow the same idea at a much larger scale.

21.2 Capture: from execution to booked trade

Definition 21.3 (Trade capture, booking)

Trade capture turns an execution — an exchange execution report, an OTC ticket, a voice trade — into a trade in the canonical model, once and as early as possible. Booking assigns the captured trade to a book, the unit that owns its risk and P&L.

Definition 21.4 (Booking model)

A booking model is the set of rules that decides where a trade is booked — by instrument type, desk, legal entity and strategy — so that the book is a consequence of the trade’s attributes, not a field typed by hand.

The chapter’s exchange side is a block: 9 900 shares bought on Book 10’s simulator by an execution algorithm sending an order that takes the offer every 20 seconds, filled in 57 executions over three minutes at an average of $100.0783. Each execution report becomes a trade with its own UTI, bought by the firm from the clearing house. The OTC side is 200 swaps from six dealers, captured from tickets on Monday morning.

The 57 executions of the 9 900-share block on the simulator, by time and price (marker area by size): ten child orders taking the offer every 20 seconds, each filled in several executions as it walks the book. Every execution is captured as a trade; every fund is allocated at the average. Data: fig_tradecap.py.
Figure 21.1. The 57 executions of the 9 900-share block on the simulator, by time and price (marker area by size): ten child orders taking the offer every 20 seconds, each filled in several executions as it walks the book. Every execution is captured as a trade; every fund is allocated at the average. Data: fig_tradecap.py.

21.3 The lifecycle as events

Definition 21.5 (Trade lifecycle event, trade amendment)

A trade lifecycle event is anything that happens to a trade after it is executed: an amendment, a novation (Book 1, chapter 5), a partial termination, an exercise, an allocation, a cancellation. A trade amendment changes a term of the trade (notional, rate, dates) by agreement; it is recorded as a new version, never by overwriting the old one.

ISDA’s Common Domain Model describes lifecycle events in the same terms: “a lifecycle event describes a state transition,” with a before and an after state, composed from a few primitive instructions — execution, quantity change, terms change, party change, exercise, transfer and a handful of others. firm.tradecap keeps the events and computes the trade: a store applies events in time order and rebuilds any version as of any time (Listing 21.1). The event sourcing of chapter 24 is the general pattern; here it is the only way to answer, on Friday, what the trade was on Wednesday.

class TradeStore:
    """Event-sourced trades: the log is the truth, states are replays."""

    def __init__(self):
        self.log: list[Event] = []

    def apply(self, e: Event) -> None:
        if e.kind not in KINDS:
            raise ValueError(f"unknown event {e.kind}")
        if e.kind != "new" and self.as_of(e.trade_id, e.ts) is None:
            raise ValueError(f"{e.kind} on a trade that does not exist: {e.trade_id}")
        self.log.append(e)

    def as_of(self, trade_id: str, ts: float | None = None) -> dict | None:
        state = None
        mine = sorted((x for x in self.log if x.trade_id == trade_id), key=lambda x: x.ts)
        for e in mine:
            if ts is not None and e.ts > ts:
                break
            state = _step(state, e)
        return state

    def versions(self, trade_id: str) -> list[tuple]:
        out, state = [], None
        mine = sorted((x for x in self.log if x.trade_id == trade_id), key=lambda x: x.ts)
        for e in mine:
            state = _step(state, e)
            out.append((e.ts, state["version"]))
        return out
Listing 21.1. The trade store: events appended, never edited; a trade as of any time is the replay of its events up to then. code/firm/tradecap/firm_tradecap.py
def _step(state, e: Event) -> dict:
    if e.kind == "new":
        s = copy.deepcopy(e.data)
        s.update(version=1, status="live")
        return s
    s = copy.deepcopy(state)
    s["version"] += 1
    if e.kind == "amend":
        s.update(e.data)
    elif e.kind == "novate":                         # a party steps out, another steps in
        s[e.data["side"]] = e.data["new"]
    elif e.kind == "terminate":                  # partial termination: the quantity falls
        s["quantity"] -= e.data["quantity"]
        if s["quantity"] <= 0:
            s["status"] = "terminated"
    elif e.kind == "exercise":
        s["status"] = "exercised"
        s["instrument"] = e.data["into"]
    elif e.kind == "cancel":
        s["status"] = "cancelled"
    elif e.kind == "allocate":
        s["status"] = "allocated"
        s["allocations"] = e.data["allocations"]
    return s
Listing 21.2. Each event kind as a transition from one version of the trade to the next. code/firm/tradecap/firm_tradecap.py
The hook’s swap through the week and what three copy-based systems held on Friday at 15:00. Each copied the trade on the events it cares about, or on its own schedule. Settlement holds version 4 only because the last event, a termination, is one it copies on; only a system that follows every event holds the current version whatever the last event was.
Figure 21.2. The hook’s swap through the week and what three copy-based systems held on Friday at 15:00. Each copied the trade on the events it cares about, or on its own schedule. Settlement holds version 4 only because the last event, a termination, is one it copies on; only a system that follows every event holds the current version whatever the last event was.

21.4 Three versions on Friday

The week’s lifecycle is seeded: 69 amendments, 26 novations and 41 partial terminations among the 200 swaps between Monday 10:00 and Friday 14:00, the hook’s swap among them. Three downstream systems keep copies: risk copies every trade at 18:00 each evening; confirmation receives a copy on the events it confirms (new trades and amendments); settlement on the events that move cash (new trades and terminations). On Friday at 15:00 their copies are compared with the event-sourced trade.

POLICIES = {                  # what each copy-based system copies, and when
    "risk": ("nightly", None),                          # every trade at 18:00, every day
    "confirmation": ("on", {"new", "amend"}),           # a copy on the events it confirms
    "settlement": ("on", {"new", "terminate"}),         # a copy on the events that move cash
}


def downstream(store: T.TradeStore, events: list, at: float = 4 * DAY + 15 * HOUR,
               hours=(18,)) -> dict:
    ids = sorted({e.trade_id for e in events})
    nights = [d * DAY + h * HOUR for d in range(5) for h in hours if d * DAY + h * HOUR <= at]
    views = {}
    for name, (how, kinds) in POLICIES.items():
        v = {}
        for tid in ids:
            if how == "nightly":
                v[tid] = store.as_of(tid, max(nights))
            else:
                copies = [e.ts for e in events
                          if e.trade_id == tid and e.kind in kinds and e.ts <= at]
                v[tid] = store.as_of(tid, max(copies)) if copies else None
        views[name] = v
    golden = {tid: store.as_of(tid, at) for tid in ids}
    wrong = {name: sum(views[name][t] != golden[t] for t in ids) for name in views}
    disagree = sum(len({repr(views[n][t]) for n in views}) > 1 for t in ids)
    return {"wrong": wrong, "disagree": disagree, "golden": golden, "views": views,
            "n": len(ids)}
Listing 21.3. The three copy policies and the comparison with the event-sourced trade on Friday afternoon. code/platforms/21-trade-capture-and-booking/python/pl_tradecap.py
systemwrong, copyingwrong, following events
risk (nightly copy)250
confirmation (copy on new and amend)600
settlement (copy on new and terminate)710
trades on which the three disagree109 of 2000
Table 21.1. Swaps whose downstream copy differs from the event-sourced trade on Friday at 15:00, after a week of lifecycle events.
Downstream systems against the event-sourced swaps on Friday afternoon, after a week of amendments, novations and partial terminations: copies are wrong in 25 to 71 trades and disagree on 109; systems that follow the events are never wrong. Data: fig_tradecap.py.
Figure 21.3. Downstream systems against the event-sourced swaps on Friday afternoon, after a week of amendments, novations and partial terminations: copies are wrong in 25 to 71 trades and disagree on 109; systems that follow the events are never wrong. Data: fig_tradecap.py.

Copying fails in a different way in each system (Table 21.1): risk is wrong only about the day’s events (25 trades), confirmation about every novation and termination it was never sent (60), settlement about every amendment and novation (71), and on 109 of the 200 swaps at least two systems disagree. Following the events, all three hold the same trade, the one the store rebuilds.

21.5 Allocations and straight-through processing

Definition 21.6 (Block allocation)

A block allocation splits a trade executed for several accounts at once — a block for several funds — among them after execution, at the block’s average price, by a rule fixed before the trade that also decides where the units that do not divide evenly go.

def allocate(fills, weights: dict, rule: str = "largest-remainder", lot: int = 1):
    """Split a block among funds at its average price, in whole lots; the rule decides where
    the lots that do not divide go. Returns the allocations and the residual."""
    total = sum(q for q, _ in fills)
    avg = sum(q * p for q, p in fills) / total
    lots = total // lot
    wsum = sum(weights.values())
    exact = {f: lots * w / wsum for f, w in weights.items()}
    if rule == "round-each":
        n = {f: int(math.floor(x + 0.5)) for f, x in exact.items()}
    else:
        n = {f: int(math.floor(x)) for f, x in exact.items()}
        left = lots - sum(n.values())
        if rule == "largest-remainder":
            order = sorted(exact, key=lambda f: (-(exact[f] - n[f]), f))
        elif rule == "floor-then-largest":
            order = sorted(exact, key=lambda f: (-weights[f], f))
        else:
            raise ValueError(rule)
        for f in order[:left]:
            n[f] += 1
    alloc = {f: (k * lot, avg) for f, k in n.items()}
    return alloc, total - sum(q for q, _ in alloc.values())
Listing 21.4. Allocation in whole lots at the average price, with three rules for the lots that do not divide. code/firm/tradecap/firm_tradecap.py

The block of 9 900 shares (99 lots of 100) is allocated to three funds with targets of 50%, 30% and 20%, whose exact shares are 4 950, 2 970 and 1 980. The largest-remainder rule gives 4 900, 3 000 and 2 000: all 99 lots allocated, no fund more than 50 shares from its target. Giving the odd lots to the largest funds gives 5 000, 3 000 and 1 900, 80 shares from target for the smallest. Rounding each fund on its own gives 5 000, 3 000 and 2 000 — 10 000 shares for a block of 9 900: an over-allocation of one lot that someone must buy or take back. (Table 21.2). Every fund pays the block’s average price, $100.0783, whatever executions made it.

rulefund Afund Bfund Cresiduallargest deviation
largest remainder4 9003 0002 000050
odd lots to the largest funds5 0003 0001 900080
each fund rounded5 0003 0002 000−100-10050
Table 21.2. A block of 9 900 shares allocated to three funds (targets 50%, 30%, 20%) in lots of 100 under three rules: shares per fund, the block minus the allocation, and the largest deviation from a fund’s exact share.

Definition 21.7 (Straight-through processing)

Straight-through processing carries a trade from execution to settlement — capture, booking, allocation, confirmation, clearing, settlement — without manual re-entry or intervention, each step reading the output of the previous one in the canonical model.

Straight-through processing is what the canonical model and the event stream buy: each manual step is a copy, and each copy is a version that can drift. The rate of trades that pass through without a human touch is the operational measure of a platform’s trade capture (chapter 22 measures the breaks when they do not).

As of September 2026 — Identifiers and event models

CPMI and IOSCO published their technical guidance on the harmonisation of the UTI on 28 February 2017: the generating entity’s LEI concatenated with a value unique for that entity, no check digit, at most 52 characters from A–Z and 0–9. ISDA’s Common Domain Model, maintained as open source, models lifecycle events as state transitions composed from primitive instructions (execution, quantity change, terms change, party change, exercise and others).

21.6 Tutorial: one block, two hundred swaps, one week

Goal. Capture executions and tickets into one model, allocate a block, run a week of lifecycle events, and compare copying with following events. End state: Tables 21.1 and 21.2.

  1. Capture: pl_tradecap.block_fills() and capture_block; validate every trade.
  2. Allocate: allocations(fills) under the three rules.
  3. Lifecycle: week(); store.versions and store.as_of.
  4. Downstream: downstream(store, events).

What to change next. Add an exercise event that turns a swaption into a swap and follow it downstream; allocate by the funds’ cash instead of fixed targets.

21.7 Build: trade capture

Purpose. One trade record for every system, changed only by events, from capture to settlement.

Interface. uti, validate, Event, TradeStore (apply, as_of, versions), from_execution, from_ticket, allocate, BookingModel.

Rules. Trades are never edited, only evented; every trade validates; identifiers follow the UTI and LEI formats; allocation rules are fixed before the trade and allocate the whole block; books come from rules.

Acceptance tests. code/firm/tradecap/tests/: UTI format and validation errors; a lifecycle’s versions and states as of any time, including termination to zero and an event on an unknown trade; exercise and capture from an execution report; the three allocation rules; the booking model.

Stretch. Messages in FpML or events in the CDM’s JSON; allocations by cash with fractional shares where allowed; a straight-through rate report.

Sources and further reading

  • CPMI and IOSCO, Harmonisation of the Unique Transaction Identifier: Technical Guidance, February 2017.
  • ISDA Common Domain Model documentation, “Event Model”.
  • One Quant Book 1, chapter 5, and Book 2, chapter 28 (novation, LEIs); One Quant Book 10, chapter 26 (execution reports).

21.8 Exercises

Exercise 21.1 ★

A block of 1 000 shares is split among three funds with equal targets in lots of 100. What does each rule give?

Solution

Solution of Exercise 21.1.

Ten lots, exact shares of 3133\tfrac13 lots each. Largest remainder: 400, 300 and 300 (the tie broken by the funds’ names). Odd lots to the largest funds: the same, since the weights are equal. Each fund rounded: 300 each, 900 shares, leaving 100 unallocated.

Exercise 21.2 ★

Why is the risk system wrong about fewer trades than the other two?

Solution

Solution of Exercise 21.2.

It copies every trade every evening, so it misses only what happened since Thursday at 18:00; the other two copy on some kinds of event only, and miss every event of the other kinds, whenever it happened in the week.

Exercise 21.3 ★

What does a UTI generated by the firm for its 7th trade look like?

Solution

Solution of Exercise 21.3.

The firm’s LEI followed by a value unique for the firm: 5493001KJTIIGC8Y1R12 and 000…0007 padded to 52 characters, upper-case letters and digits only.

Exercise 21.4 ★★

Why must the allocation rule be fixed before the trade, and what goes wrong if a trader chooses it after seeing the fills?

Solution

Solution of Exercise 21.4.

Because otherwise the allocation can favour a fund after the fact — the better fills, the odd lot in a rising market — which is unfair to the other clients and, for a manager, a conflict that regulators look for. A rule fixed in advance and applied mechanically removes the choice.

Exercise 21.5 ★★

The confirmation system is wrong about 60 trades. Which events did it miss, and what would it have had to subscribe to?

Solution

Solution of Exercise 21.5.

Every novation and every partial termination: it was sent copies on new trades and amendments only. It needs every event that changes what it confirms — in practice, the whole stream, filtered by itself rather than by the sender.

Exercise 21.6 ★★

A trade is booked in the wrong book and moved the next day. How should the move be recorded, and why?

Solution

Solution of Exercise 21.6.

As an event (a book transfer) with its time and reason, not by editing the book field: the risk and P&L of the previous day were reported in the old book and must remain reproducible, and the transfer itself is information for product control.

Exercise 21.7 ★★★

Coding. Rerun downstream with the risk system copying at 12:00 as well as at 18:00. How many trades is it wrong about?

Solution

Solution of Exercise 21.7.

Two trades: copying at noon as well catches the morning’s events, leaving only the events between noon and 15:00. More frequent copies shrink the window; they never close it.

Exercise 21.8 ★★★

Find the flaw. “Every system has the trade, so they must agree: each one reads it from the trading system when it needs it.”

Solution

Solution of Exercise 21.8.

Reading on demand from the trading system avoids stale copies only if the trading system holds every event and every system reads the same version at the same moment; in practice each reads at a different time, keeps what it read, and misses what the trading system does not own (novations, terminations processed elsewhere). Agreement needs one event stream and versions as of a stated time.

21.9 Problem: Three Versions on Friday

Problem 21.1

Weekend problem — copies against events

The block, the 200 swaps and the week of the chapter.

Part I — The model.

  1. What does the canonical model hold, and why the instrument as the pricing library serialises it?
  2. How is a UTI built?
  3. What does the validator check?
  4. What does a booking model decide?
  5. Why is a trade never edited?

Part II — The week.

  1. What events happened to the swaps?
  2. What did the hook’s swap go through, and what did each system hold on Friday?
  3. What does each copy policy miss?
  4. How many trades was each system wrong about?
  5. On how many trades did the systems disagree, and how many under events?

Part III — The block.

  1. How was the block executed, and at what average price?
  2. What are the exact shares of the three funds?
  3. What does each rule allocate, and with what residual?
  4. What is the largest deviation from target under each rule?
  5. What price does each fund pay?

Part IV — The verdict.

  1. State the named result: the number of downstream disagreements after a week of lifecycle events under copy-based and event-based distribution, and the allocation residuals of a block split among three funds under three rounding rules.
  2. Which allocation rule would you adopt, and why?
  3. What would you require of a new downstream system?
  4. What does straight-through processing measure?
  5. In one sentence: what is a trade?
Solution

Solution of Problem 21.1.

  1. Identifiers, parties, book, dates, status, version, and the instrument, quantity and price; the library’s form lets every system price and risk-manage the trade from the same record.
  2. The generating entity’s LEI followed by a value unique for it, at most 52 characters, A–Z and 0–9.
  3. Identifier formats, distinct parties, settlement not before trade date, a positive quantity, an instrument type.
  4. Where a trade is booked, from its instrument type and desk.
  5. So that every version stays reproducible and every system can rebuild the same one.
  6. 69 amendments, 26 novations and 41 partial terminations among 200 swaps.
  7. Amended Monday, novated Wednesday, cut by 40% Friday; risk held version 3, confirmation version 2, settlement version 4.
  8. Risk the events since the last evening; confirmation novations and terminations; settlement amendments and novations.
  9. 25, 60 and 71 trades.
  10. 109 of 200; none.
  11. Ten child orders taking the offer every 20 seconds, 57 executions, average $100.0783.
  12. 4 950, 2 970 and 1 980 shares.
  13. Largest remainder 4 900/3 000/2 000, residual 0; odd lots to the largest 5 000/3 000/1 900, residual 0; each rounded 5 000/3 000/2 000, residual −100-100.
  14. 50, 80 and 50 shares.
  15. The block’s average price, $100.0783.
  16. Named result. After a week of lifecycle events, copy-based distribution leaves 25, 60 and 71 wrong trades in risk, confirmation and settlement and 109 of 200 swaps on which they disagree; event-based distribution, none. The block of 9 900 shares splits with residuals of 0, 0 and −100-100 shares under largest-remainder, odd-lots-to-the-largest and round-each rules, and deviations from target of at most 50, 80 and 50.
  17. Largest remainder: it allocates the whole block and keeps every fund closest to its target.
  18. That it consumes the event stream and can say which version it holds as of when.
  19. The share of trades that go from execution to settlement without manual intervention.
  20. The result of its events.

21.10 Interview questions

Interview question 21.1 ★ developer

Three systems show three versions of the same trade. How do you find out which is right, and how do you stop it happening?

Solution

Solution of Interview question 21.1.

Rebuild the trade from its events as of now and compare each system’s version with it; then replace copies by subscriptions to the event stream, with each system reporting the version it holds.

What the interviewer is looking for: Event sourcing and versions.

Interview question 21.2 ★★ developer

How would you represent a trade so that pricing, risk, confirmation and settlement can all use it?

Solution

Solution of Interview question 21.2.

A canonical model: a header of identifiers, parties, book, dates, status and version, and the economics as the pricing library’s instrument, with validation; systems keep their own derived views but never their own copy of the terms.

What the interviewer is looking for: One model, validated, shared.

Interview question 21.3 ★★ developer

A block is filled at twenty prices for five funds. How do you allocate it?

Solution

Solution of Interview question 21.3.

At the block’s average price, by a rule fixed before the trade (pro rata by target, in whole lots, remainders by largest fractional part), the whole block allocated, with the rule and result recorded.

What the interviewer is looking for: Average price, pre-set rule, no residual.

Interview question 21.4 ★★ developer

What is a novation, and what does it change in the systems that hold the trade?

Solution

Solution of Interview question 21.4.

The replacement of one party by another, with the other party’s consent; the trade continues with a new counterparty, so credit exposure, confirmations, settlement instructions and reporting change while the economics may not.

What the interviewer is looking for: Party change as an event with downstream effects.

Interview question 21.5 ★★★ developer

Design the trade capture and booking layer of a multi-asset trading firm.

Solution

Solution of Interview question 21.5.

Adapters from every execution source into a canonical model; UTIs and validation at capture; booking by rules; allocations by pre-set rules; lifecycle events in an event-sourced store; every downstream system on the event stream with versions as of time; straight-through rate and breaks monitored.

What the interviewer is looking for: Capture once, events everywhere.

Terms defined in this chapter

See all 2333 terms in the glossary