Quantitative Finance · Book 15 · Technology

Research, Data and Risk Platforms

Research, Data and Risk Platforms · Technology

17Position and P&L Services

Thirty-three minutes into the session, the order-entry session dropped for four seconds. Four executions happened while it was down, and when it came back the strategy’s own execution reports put it 500 shares less short than the venue’s drop copy did. The position service had to decide which to believe. The answer is neither, until they agree: count every execution any source reports, once, and say that the position is pending until the sources match. Every downstream number — risk (chapter 18), limits, P&L, the books that close at night — starts from positions, and positions start from executions that arrive twice, late, out of order, and sometimes are taken back.

17.1 Positions from executions

Definition 17.1 (Position service)

A position service maintains the firm’s positions and P&L by account and instrument from its executions, merged from every source that reports them, and serves them as of any time, by trade date or settlement date, with a status that says whether its sources agree.

Definition 17.2 (Execution identifier)

An execution identifier is the venue’s unique identifier of one execution (in the simulator, its match number with the order’s client identifier); it is what makes an execution reported by two sources, or twice by one, recognisably the same.

The chapter’s service (firm.posservice) stores executions keyed by that identifier and never stores a position: every position is computed by replaying the executions in time order through Book 1’s exact keeper (firm.pnl), in which quantity and cash are integers, so that cash plus quantity times the mark, less fees, holds to the ten-thousandth of a dollar. Storing the events rather than their sum makes arrival order irrelevant, makes duplicates harmless (idempotent processing, Book 13, chapter 24) and makes every past position recomputable.

    def on_execution(self, source: str, e: Execution) -> bool:
        self.seen[source].add(e.exec_id)
        if e.exec_id in self.execs:
            return False
        self.execs[e.exec_id] = e
        return True

    def on_bust(self, exec_id: str, ts: int) -> None:
        self.busted[exec_id] = ts

    def on_correction(self, exec_id: str, ts: int, qty: int | None = None,
                      price: int | None = None) -> None:
        self.corrections.append((ts, exec_id, qty, price))
Listing 17.1. Executions from any source, counted once; busts and corrections kept as events, not applied by editing the past. code/firm/posservice/firm_posservice.py
    def _effective(self, as_of=None) -> list[Execution]:
        out = {}
        for k, e in self.execs.items():
            if as_of is not None and e.ts > as_of:
                continue
            if k in self.busted and (as_of is None or self.busted[k] <= as_of):
                continue
            out[k] = e
        for ts, k, q, p in sorted(self.corrections, key=lambda c: c[0]):
            if k in out and (as_of is None or ts <= as_of):
                e = out[k]
                out[k] = replace(e, qty=e.qty if q is None else q,
                                 price=e.price if p is None else p)
        return sorted(out.values(), key=lambda e: (e.ts, e.exec_id))

    def position(self, account, symbol, as_of=None, view="trade", date=None) -> Position:
        p = Position()
        for e in self._effective(as_of):
            if e.account != account or e.symbol != symbol:
                continue
            if view == "settlement" and (date is None or e.settle_date > date):
                continue
            p.on_fill(e.side, e.qty, e.price, e.fee)
        return p
Listing 17.2. A position is a replay: the executions in force at a time (busts and corrections applied if known by then), in time order, through the exact keeper. code/firm/posservice/firm_posservice.py
The position service. Executions arrive from the session and from the drop copy, busts and corrections from the venue; the service keeps executions, not positions, and computes every view from them; the clearing statement is the check at the end of the chain.
Figure 17.1. The position service. Executions arrive from the session and from the drop copy, busts and corrections from the venue; the service keeps executions, not positions, and computes every view from them; the clearing statement is the check at the end of the chain.

17.2 Two feeds of the same truth: sessions and drop copies

The order-entry session reports the executions of the orders it sent; the drop copy (Book 13, chapter 21) sends the firm a copy of every execution of its sessions on a separate connection, for exactly the moment the first one fails. A session that drops and logs back in asking only for new messages never receives the reports generated while it was down; a drop copy that runs a little late (50 milliseconds here) has them. The service takes both, counts each execution once, and compares the two sets: when they differ, the position it serves is the union, and its status is pending.

    def status(self, account, symbol) -> str:
        mine = {k for k, e in self.execs.items()
                if e.account == account and e.symbol == symbol}
        ids = [self.seen[s] & mine for s in self.sources]
        return "agreed" if all(i == ids[0] for i in ids) else "pending"

    def missing(self, source: str) -> set:
        others = set().union(*(self.seen[s] for s in self.sources if s != source))
        return others - self.seen[source]
Listing 17.3. The status: agreed when every source has reported the same executions, pending otherwise, with the executions each source is missing. code/firm/posservice/firm_posservice.py

17.3 Busts, corrections and late fills

Definition 17.3 (Trade bust)

A trade bust is the cancellation of an execution by the venue after it happened — for example under its rules on clearly erroneous trades (Book 1, chapter 31) — so that for the clearing statement the trade never took place.

A bust arrives minutes or hours after the execution; a correction changes an execution’s price or quantity. The service applies both as events with their own times, so that the position as of any moment can be computed as it was known then (the bust not yet received) or as it is known now (the trade never happened). A late fill — an execution report that arrives after the position has been read — is only a late event: the replay puts it in its place.

17.4 Intraday P&L and end-of-day marks

Definition 17.4 (Intraday P&L, end-of-day mark)

Intraday P&L is the P&L of positions marked at current market prices during the day, for monitoring and limits. The end-of-day mark is the price at which positions are valued for the day’s official P&L and books, usually the official closing price, set by the valuation policy rather than by the last print the strategy happened to see.

The chapter marks intraday at the feed’s mid and at the close at the venue’s last trade, which stands for the official close. The P&L is exact: firm.pnl keeps cash and quantity in integers, and the realised and unrealised parts under average cost (Book 1, chapter 7) add up, with fees, to the total at the mark.

17.5 Start of day and the books that close

Definition 17.5 (Start-of-day position, trade-date position, settlement-date position)

The start-of-day position is the position a day begins with: the previous day’s end-of-day position, reconciled. The trade-date position counts every execution from the moment it trades; the settlement-date position counts it only from its settlement date (one business day later in US equities, Book 1, chapter 5), which is what custody and cash reflect.

At the end of the day the service records each position with its end-of-day mark and total; the next day’s start-of-day position is that record. The clearing firm’s statement — the executions it cleared, busts removed — is the check: the service reconciles its positions against it and lists every break before the next day starts.

17.6 Four seconds without a session

The chapter’s experiment runs a quoting strategy (500 shares a side, an inventory limit of 5 000 shares, orders left working if the session drops) for an hour on ten firm.tape sessions of Book 10’s simulator, each with a drop-copy session. At a seeded minute the order-entry session drops for four seconds; the client logs back in asking for new messages only, and operations request a replay of the missed reports a minute later. At minute 50 the venue busts the execution nearest minute 30. Three services are compared with the clearing statement: session reports only; session and drop copy merged; merged with the bust applied.

def services(d: dict) -> dict:
    """Feed the three services in arrival order; return them and the arrival log."""
    start, end = d["drop"], d["drop"] + OUTAGE
    replay = end + REPLAY_AFTER
    events = []
    for e in d["execs"]:
        lost = start <= e.ts < end
        events.append((replay if lost else e.ts, "session", e, lost))
        events.append((e.ts + DROPCOPY_LAG, "dropcopy", e, False))
    events.sort(key=lambda x: (x[0], x[1], x[2].exec_id))
    a, b, c = P.PositionService(("session",)), P.PositionService(), P.PositionService()
    for _t, src, e, replayed in events:
        if src == "session" and not replayed:
            a.on_execution(src, e)        # the session-only service never replays
        b.on_execution(src, e)
        c.on_execution(src, e)
    if d["bust"] is not None:
        c.on_bust(d["bust"].exec_id, d["open"] + BUST_NOTICE)
    return {"session": a, "merged": b, "merged+bust": c,
            "events": [(t, src, e) for t, src, e, _r in events]}
Listing 17.4. The three services fed in arrival order: the session’s reports (the lost ones only on replay), the drop copy 50 milliseconds late, and the bust notice. code/platforms/17-position-and-pnl-services/python/pl_posservice.py
The position of each service minus the clearing statement’s, through the hour of seed 3 (sampled every five seconds). The busted execution of minute 30 puts every service 100 shares off the statement; the outage of minute 33 adds 500 to the session-only service; the service with the drop copy and the bust agrees with the statement from the bust notice at minute 50. Data: fig_posservice.py.
Figure 17.2. The position of each service minus the clearing statement’s, through the hour of seed 3 (sampled every five seconds). The busted execution of minute 30 puts every service 100 shares off the statement; the outage of minute 33 adds 500 to the session-only service; the service with the drop copy and the bust agrees with the statement from the bust notice at minute 50. Data: fig_posservice.py.

Example 17.6 (Seed 3)

The session drops at minute 33 and four executions happen in the four seconds; the session-only service is 500 shares off from then on. The merged service has them 50 milliseconds after they happen, from the drop copy, and reports its position as pending for 62 seconds, until the replay brings the session into agreement. Both carry the execution of minute 30, which the venue busts at minute 50; only the service that applies the notice agrees with the clearing statement at the close. At the close the statement shows −3 300-3\,300 shares and −$3 493-\$3\,493; the session-only service is 600 shares and $252 off, the merged service 100 shares and $47 off, the merged service with the bust exact.

servicemean ∣|position error∣| (shares)mean ∣|P&L error∣| ($)
session reports only29052.4
with the drop copy22028.4
with the drop copy and the bust00.0
Table 17.1. Errors at the close against the clearing statement, averaged over ten one-hour sessions, each with a four-second outage at a seeded minute and one execution busted twenty minutes after it happened.

Over the ten sessions (Table 17.1), the outage costs little execution by execution — it catches executions in four sessions (one, four, three and one), because in four seconds a quoting strategy trades rarely — and the bust costs something every day. Without the drop copy the service ends the day 290 shares and $52 off on average; with it, 220 shares and $28 off, all of it the bust; with the bust applied, exact. In the sessions where the outage lost executions, the merged service reported pending for 60 to 63 seconds, until the replay; without a replay it would have reported pending until someone looked.

Each service’s position minus the clearing statement’s at the close, session by session. Without the drop copy the error is the lost executions plus the busted one; with it, the busted one alone; with the bust applied, nothing (the third bar is zero everywhere). Data: fig_posservice.py.
Figure 17.3. Each service’s position minus the clearing statement’s at the close, session by session. Without the drop copy the error is the lost executions plus the busted one; with it, the busted one alone; with the bust applied, nothing (the third bar is zero everywhere). Data: fig_posservice.py.

Remark 17.7 (What pending buys)

A pending status turns a silent error into a visible one. The merged service’s position was right during the outage, but only the disagreement between its sources said that something had happened to the session; the session-only service was wrong and confident. Risk (chapter 18) should treat a pending position as the union of what any source reports — the larger exposure — until it agrees.

17.7 Tutorial: one day, three sources, one statement

Goal. Build positions from two execution feeds and a bust, watch them disagree and agree, and reconcile with the clearing statement. End state: Figure 17.2 and Table 17.1.

  1. A day: pl_posservice.day(seed) runs the simulator with a drop-copy session and a scheduled outage.
  2. Services: services(d); status and missing during the outage.
  3. The statement: truth(d); reconcile(service, statement).
  4. The close: at_close() over ten seeds; end_of_day and start_of_day for the roll.

What to change next. Cancel orders on disconnect and see what the outage costs then; delay the drop copy by a second and watch the pending windows grow.

17.8 Build: the position service

Purpose. One source of positions and P&L for risk, limits, reporting and the books, correct under duplicate, late and missing reports, busts and corrections, and honest about when it is not sure.

Interface. Execution; PositionService(sources) with on_execution, on_bust, on_correction, position(account, symbol, as_of, view, date), status, missing, pnl, end_of_day, start_of_day; reconcile.

Rules. Executions are stored, positions computed; an execution identifier counts once; busts and corrections are events with times; integer quantities and cash (firm.pnl); pending whenever sources disagree.

Acceptance tests. code/firm/posservice/tests/: idempotent merge and status; exact P&L and independence from arrival order; busts and corrections as of times; trade-date and settlement-date views, the end-of-day roll, and reconciliation breaks.

Stretch. Many accounts and instruments with allocations from an average-price account; corporate actions on positions (chapter 6); the service’s events journalled and replayed (chapter 12).

Sources and further reading

  • One Quant Book 1, chapters 5, 7 and 31 (settlement, positions and P&L, erroneous trades); One Quant Book 13, chapters 21 and 24 (drop copies, idempotent processing); One Quant Book 10, chapter 26 (the simulator’s drop copy and session faults).

17.9 Exercises

Exercise 17.1 ★

The same execution arrives from the session, then twice from the drop copy. What is the position, and why?

Solution

Solution of Exercise 17.1.

The execution counted once: the service keys executions by their identifier, so the second and third reports only record that the drop copy has seen it (and the status becomes agreed).

Exercise 17.2 ★

A buy of 100 at $100.00 and a sell of 300 at $100.02, marked at $100.00. What are the position, the realised and the unrealised P&L?

Solution

Solution of Exercise 17.2.

Short 200 shares. Realised on the 100 closed: 100×$0.02=$2100 \times \$0.02 = \$2; the 200 opened short at $100.02 are marked at $100.00: unrealised +$4+\$4. Total $6, which is also cash (−$10 000+$30 006-\$10\,000 + \$30\,006) plus quantity times mark (−$20 000-\$20\,000).

Exercise 17.3 ★

Why is a busted trade removed as an event rather than deleted from the store?

Solution

Solution of Exercise 17.3.

So that the position before the notice can still be computed as it was known then — what risk and the strategy saw — and the audit shows who removed what and when; deleting it would rewrite history and make every earlier report unreproducible.

Exercise 17.4 ★★

Why did the outage lose executions in only four of the ten sessions, and what would change it?

Solution

Solution of Exercise 17.4.

In four seconds a quoting strategy’s resting orders trade only if an aggressive order arrives at their price: in six sessions none did. Longer outages, larger or more aggressive orders, or a busier market (the open, news) would lose more.

Exercise 17.5 ★★

A trade on Friday settles the next business day. What do the trade-date and settlement-date views show on Friday evening and on Monday?

Solution

Solution of Exercise 17.5.

On Friday evening the trade-date view includes it and the settlement-date view does not; it settles on Monday (the next business day), from when both include it.

Exercise 17.6 ★★

The merged service’s position was right during the outage. Why is its pending status still useful?

Solution

Solution of Exercise 17.6.

Because it is the only signal that the session lost messages: without it the session’s view is wrong and nobody knows. Pending prompts the replay, tells risk to use the union of the sources, and marks the P&L as provisional.

Exercise 17.7 ★★★

Coding. Run at_close with the strategy cancelling its orders on disconnect. What happens to the executions lost in the outage?

Solution

Solution of Exercise 17.7.

With cancel on disconnect the strategy’s orders are cancelled when the session drops, so nothing trades during the outage: no session loses an execution in any of the ten sessions, and the session-only service’s error is the bust’s alone, 220 shares on average, like the merged service’s. Cancel on disconnect removes the problem at its source, at the price of leaving the book for a few seconds.

Exercise 17.8 ★★★

Find the flaw. “Our position service adds each fill to the position as it arrives, and resets from the clearing file every morning, so it is right at least once a day.”

Solution

Solution of Exercise 17.8.

Adding fills as they arrive counts duplicates twice and misses lost ones, and the error persists all day, when risk and limits use it; the morning reset hides the error instead of finding it. Key executions by identifier, merge the drop copy, keep busts as events, reconcile, and explain every break before resetting anything.

17.10 Problem: Four Seconds Without a Session

Problem 17.1

Weekend problem — positions from two feeds and a bust

The chapter’s strategy, ten sessions, a four-second outage and a bust each.

Part I — The service.

  1. What does the service store, and how does it compute a position?
  2. What makes two reports the same execution?
  3. What does the status say, and when?
  4. How does the service handle a bust and a correction?
  5. What keeps the P&L exact?

Part II — The sources.

  1. Why does a session lose reports in an outage?
  2. What does the drop copy add, and at what cost?
  3. What is a replay, and why is it requested?
  4. What does the clearing statement represent?
  5. When does each service agree with the statement?

Part III — The day.

  1. In how many sessions did the outage lose executions, and how many?
  2. How long was the merged service pending?
  3. What were the errors at the close in seed 3?
  4. What were the mean errors over the ten sessions?
  5. Which error does the drop copy remove, and which not?

Part IV — The verdict.

  1. State the named result: the position and P&L error at the close without the drop copy, with it, and with it plus the bust, and the time until the service agrees with the clearing statement.
  2. What should risk do with a pending position?
  3. What should happen at the start of the next day?
  4. What would you monitor on the position service itself?
  5. In one sentence: what makes a position trustworthy?
Solution

Solution of Problem 17.1.

  1. Executions keyed by identifier, busts and corrections as events; a position is the replay of the executions in force at a time through the exact keeper.
  2. The execution identifier.
  3. Agreed when every source has reported the same executions; pending otherwise.
  4. As events with their own times: removed or replaced from their time on, the earlier position still computable.
  5. Integer quantity and cash in firm.pnl; the mark applied only at the end.
  6. It logs back in asking only for new messages; the reports generated while it was down are skipped unless a replay is requested.
  7. Every execution on a second path, 50 milliseconds late; one more connection to run and reconcile.
  8. A request to the venue to resend the session’s messages from a sequence number; it brings the session back into agreement.
  9. The executions the clearing firm cleared, busts removed: the day’s truth.
  10. The merged service with the bust from the bust notice on; the others never before the statement.
  11. In four of the ten: one, four, three and one executions.
  12. 60 to 63 seconds, until the replay.
  13. Statement −3 300-3\,300 shares and −$3 493-\$3\,493; session only 600 shares and $252 off; merged 100 and $47 off; merged with the bust exact.
  14. 290 shares and $52.4 without the drop copy; 220 and $28.4 with it; 0 with the bust.
  15. The lost executions; not the bust, which only the venue’s notice removes.
  16. Named result. At the close, averaged over ten sessions: 290 shares and $52.4 off the clearing statement from the session alone, 220 and $28.4 with the drop copy, none with the drop copy and the bust applied; the complete service agrees with the statement from the bust notice on, twenty minutes after the busted trade, and was pending for about a minute after each outage that lost executions.
  17. Use the union of the sources — the larger exposure — and flag it until it agrees.
  18. Reconcile with the clearing statement, explain every break, then roll the end-of-day positions into the start-of-day.
  19. Pending time per account, breaks at reconciliation, lag between sources, and executions seen by one source only.
  20. That it is computed from every execution, once, and agrees with an independent record.

17.11 Interview questions

Interview question 17.1 ★ developer

How do you make a position service robust to duplicate execution reports?

Solution

Solution of Interview question 17.1.

Key executions by the venue’s execution identifier and make applying one idempotent: a second report of a known identifier changes nothing but the record of which sources have seen it.

What the interviewer is looking for: Idempotency on a venue identifier.

Interview question 17.2 ★★ developer

Your order-entry session dropped for a few seconds. How do you know your position is right?

Solution

Solution of Interview question 17.2.

Compare the session’s executions with the drop copy’s: any execution in one and not the other is a gap; request a replay from the last sequence number received; until the sources agree, treat the position as pending and use the union.

What the interviewer is looking for: A second source and a replay.

Interview question 17.3 ★★ developer, researcher

Why store executions rather than positions?

Solution

Solution of Interview question 17.3.

Positions are a function of executions; storing executions makes arrival order and duplicates harmless, keeps busts and corrections as history, and lets any past position be recomputed and explained. A stored position can only be patched.

What the interviewer is looking for: Event sourcing.

Interview question 17.4 ★★ developer

What is the difference between trade-date and settlement-date positions, and who uses each?

Solution

Solution of Interview question 17.4.

Trade-date counts an execution when it trades (risk, P&L, limits); settlement-date when it settles (cash, custody, financing, some regulatory reports). They differ by the settlement cycle.

What the interviewer is looking for: Who uses which view.

Interview question 17.5 ★★★ developer

Design the position and P&L service of a multi-strategy trading firm.

Solution

Solution of Interview question 17.5.

Executions from every session and drop copy into an event store keyed by execution identifier, with busts, corrections and allocations as events; positions and exact P&L computed by replay, by account and instrument, trade and settlement date, intraday and end-of-day marks from the valuation policy; statuses and alerts when sources disagree; reconciliation with the clearing statement before the start-of-day roll.

What the interviewer is looking for: Events, idempotency, two sources, reconciliation.

Terms defined in this chapter

See all 2333 terms in the glossary