Day 82 — Unrealized P&L: Continuous Monitoring of Open Risk
The “Just Recompute It” Trap
Here’s how a junior engineer wires up unrealized P&L monitoring after finishing the position tracker on Day 78 and the portfolio aggregator on Day 79: every time a price tick arrives, loop over the book and recompute.
def on_tick(self, symbol, price):
self.prices[symbol] = price
total = 0.0
for sym, pos in self.positions.items():
if pos.qty != 0:
total += (self.prices.get(sym, pos.avg_cost) - pos.avg_cost) * pos.qty
self.unrealized_pnl = total
It’s correct. It passes every unit test you write for a 5-symbol paper account. It ships. Then your book grows to 200 names and a Tuesday afternoon CPI print sends implied vol through the roof, and the tick rate across your subscribed symbols jumps from a lazy 10/sec to 600+/sec. Your risk dashboard — the one thing you actually need to be responsive during a spike — starts updating once every two or three seconds instead of continuously. You are flying blind through the exact five minutes you needed the instrument panel most.
That’s the trap: the naive approach isn’t wrong, it’s wrong at scale, and the scale it breaks at is correlated with the moments you can least afford it to break.
The Failure Mode
Walk through the mechanics. A single-threaded asyncio event loop is reading ticks off a WebSocket. Each on_tick call is synchronous and blocks the loop for its duration. If recomputing unrealized P&L is O(n) in the number of open positions, and n sits at, say, 200 during an active trading week, then every tick costs you 200 subtraction-multiply-add operations plus a dict lookup per position — call it a few microseconds in pure Python, more once you account for attribute access overhead on non-__slots__ objects.
A few microseconds times 10 ticks/second is nothing. A few hundred microseconds times 600 ticks/second is 200+ milliseconds of blocked event loop per second of wall-clock time, on top of whatever else is competing for that same GIL-bound loop — order management, other market data callbacks, your checkpoint writer. The read loop falls behind its socket buffer. Ticks queue up. Now you’re not just slow, you’re stale: the price you’re computing P&L against might be several seconds old, and you have no signal telling you that’s happening, because the naive loop doesn’t distinguish “fresh mark” from “the last number I happened to have lying around.”
There’s a second, quieter failure baked into most first-draft implementations: using float for cost basis and mark price. A float cost basis of 142.37 isn’t exactly representable in binary floating point. Do enough VWAP updates and mark-to-market computations against it and you accumulate drift — not the kind of drift that blows up a demo, the kind that shows up as a two-cent discrepancy against your broker’s statement three weeks later that takes an afternoon to root-cause.



