Day 80 — Market Value: Calculating Real-Time AUM
The “Sum It Every Time” Trap
Here’s how a junior engineer builds AUM on day one of a portfolio manager: a function that takes cash plus the current positions, loops over every symbol, calls the broker for the latest quote, multiplies quantity by price, and sums it up.
def naive_aum(cash_cents, positions, broker_client):
total = cash_cents
for symbol, qty in positions.items():
quote = broker_client.get_latest_quote(symbol) # network call
total += round(qty * quote.price_cents)
return total
It’s correct. It’s obvious. It’s also the reason a portfolio manager falls over during exactly the ten minutes a day it matters most.
Trace what happens on a real trading floor. You’re running a WebSocket subscription to trade prints across your held symbols. Under normal conditions AAPL prints maybe twice a second — call naive_aum() on every tick, no problem, plenty of headroom. Now CPI comes in hot, the tape accelerates, and your 40-symbol book is printing 200 ticks a second across all names combined. Each tick triggers a full rescan: 40 quote calls, 40 multiplications, one summation. You’ve gone from calling the broker 40 times a minute to 8,000 times a minute. Alpaca’s paper-trading REST tier caps you at roughly 200 requests a minute. You blow through the budget in under two seconds and start eating 429s — right as your risk engine is trying to answer “are we still inside our leverage limit” during the exact window where the answer might be no.
The Failure Mode, Precisely
Two things break, and they break for different reasons:
Rate-limit exhaustion. naive_aum() is O(N) broker calls per invocation, where N is your position count. Call it once per tick and total API pressure is O(N × ticks-per-second). Portfolio size and market volatility multiply against each other. Nothing about this scales — it’s quadratic pain hiding behind a five-line function.
Blocking the event loop. Even if you swap REST polling for a local price cache (no network calls), a full O(N) rescan on every single tick still means the coroutine handling that tick doesn’t yield until it’s walked every position. With an asyncio event loop and one task fielding the WebSocket, that’s N multiplications of latency injected between every other coroutine’s chance to run — order submission, the reconciliation timer, the dashboard refresh. At 40 positions this is invisible. At 4,000 (a multi-strategy book, or a basket product) it’s a stutter you can see.
Neither failure mode is about “Python being slow.” It’s an algorithmic mistake: recomputing a sum from scratch when only one term of that sum changed.
The AutoQuant-Alpha Architecture
AUM decomposes cleanly:
AUM = cash + Σ(qty_i × price_i) for every held position i
A price tick for symbol i changes exactly one term in that sum. Every other term is untouched. So instead of re-summing, maintain a running total and apply a delta:
new_total = old_total - (qty_i × old_price_i) + (qty_i × new_price_i)
This is AUMEngine.update_price() in src/aum_engine.py. It looks up one PositionState by symbol (a dict lookup, O(1)), computes the new market value for that position, adjusts the running _equity_total_cents by the difference, and returns. No loop. No scan. The cost of a tick doesn’t care whether the book holds 10 symbols or 5,000 — tests/stress_test.py proves this directly by timing the same operation across four portfolio sizes and asserting the ratio stays flat.
The broker is touched exactly twice in this design: once at bootstrap (a single snapshot to seed starting positions and cash), and periodically during reconciliation — never on the hot tick path.
Implementation Deep Dive
Integer cents, one parse boundary. Every dollar-denominated value enters the system through parse_dollars_to_cents() in src/money.py and never touches floating point again. This isn’t paranoia — 0.1 + 0.2 != 0.3 in IEEE 754, and a portfolio manager processing thousands of P&L updates a day will accumulate that error into real, visible cent drift by end of session. Quantities (fractional shares are legal on Alpaca) stay Decimal throughout, because a share count isn’t currency — it only re-enters cent-space at the one multiplication site, round_product_to_cents(), which applies banker’s rounding (ROUND_HALF_EVEN) so rounding bias doesn’t compound in one direction over thousands of ticks.
Self-healing reconciliation. An O(1) incremental accumulator has one real risk: if a single delta gets dropped or double-applied (a bug, a race, a malformed tick), the running total silently drifts from truth forever, because nothing ever re-derives it from scratch. reconcile() answers this by periodically computing the O(n) brute-force sum (brute_force_aum_cents()), diffing it against the incremental total, and — if they disagree — resetting the running total to the ground truth. The system self-heals instead of accumulating unbounded drift. Reconciliation is deliberately rare (every 50 ticks by default): frequent enough to catch drift quickly, rare enough that its O(n) cost never dominates.
Crash-safe checkpointing. persistence.py writes checkpoints via serialize → write to a temp file in the same directory → os.replace(). os.replace() is atomic on POSIX and Windows: any process reading the checkpoint path, or a process that crashes mid-write, only ever observes the fully-old file or the fully-new file — never a truncated, half-written JSON blob that corrupts the next bootstrap. tests/test_persistence.py verifies this directly by injecting a failure mid-write and asserting the prior checkpoint survives untouched.
Offline-first design. price_feed.py defines a BasePriceFeed interface with two implementations: AlpacaPriceFeed (a thin wrapper around alpaca-py‘s StockDataStream) and SimulatedPriceFeed — a seeded geometric random walk that implements the identical async iterator protocol. AUMEngine and the dashboard consume Tick objects and cannot tell which one they’re talking to. This means the lesson, the test suite, and the stress test all run deterministically without live credentials, while the exact same harness works unmodified against Alpaca’s paper sandbox when ALPACA_API_KEY_ID is set.
Production Readiness: What to Watch
Per-tick latency. Should stay in the low single-digit microseconds regardless of portfolio size. If it starts climbing with position count, an O(n) path has crept back into the hot loop —
stress_test.pyis your regression guard for this.Reconciliation drift (
last_drift_cents). Should read 0 after every reconciliation pass. Persistent non-zero drift means deltas are being lost upstream — check for dropped WebSocket messages or a race between fill events and price ticks.Checkpoint staleness. Age of
data/aum_checkpoint.jsonrelative to now. A stale checkpoint after a crash means a larger tick-replay gap on restart.API call budget. Broker calls per minute should be flat and small (bootstrap + reconciliation only) — if this number tracks tick volume, something is bypassing the incremental path.
Step-by-Step Guide
Github Link:
https://github.com/sysdr/quantpython-p/tree/main/day80/autoquant_alpha_day80
Prerequisites
pip install -r requirements.txtLive mode additionally requires an Alpaca paper-trading account with ALPACA_API_KEY_ID and ALPACA_API_SECRET_KEY exported. Offline mode requires nothing beyond the pip install.
Execution
# 300-tick offline demo, live Rich dashboard, no credentials needed
bash scripts/demo.sh
# Full verification: unit tests + stress test + checkpoint round-trip
bash scripts/verify.sh
# Against live Alpaca paper-trading quotes
bash scripts/start.sh
Verification
scripts/verify.sh runs the 16-test unit suite, then stress_test.py (which asserts per-tick latency stays flat from 10 to 5,000 symbols and that the incremental total matches the brute-force baseline on every single tick of a 2,000-tick run), then a headless 400-tick session that writes a real checkpoint, then validates that checkpoint’s structure.
Success Criteria
You may advance to Day 81 only after all of the following pass.
1. Full test suite is green
python -m pytest tests/ -v
Pass condition: all 16 tests pass, including:
test_reconcile_detects_and_heals_drift— proves the engine detects an injected drift and self-heals to the brute-force ground truth.test_old_file_survives_a_failed_write— proves a checkpoint write that crashes mid-way never corrupts the previous good checkpoint.
2. O(1) latency claim is proven, not asserted
python -m tests.stress_test
Pass condition: the script must print PASS: per-tick latency is effectively constant across portfolio size — meaning per-tick latency at 5,000 symbols is under 5x the latency at 10 symbols. It must also confirm the incremental AUM total matches the O(n) brute-force baseline exactly at every checked portfolio size, with zero cent divergence.
3. A real checkpoint round-trips correctly
rm -f data/aum_checkpoint.json
python -m src.harness --ticks 400 --offline --headless --tick-delay 0.0
Pass condition: data/aum_checkpoint.json exists after the run, contains a cash_cents integer field and a non-empty positions object, and can be reloaded via AUMEngine.from_snapshot() to produce an aum_cents value identical to the engine’s value at the moment the checkpoint was written.
4. One command, zero manual fixes
bash scripts/verify.sh
Pass condition: this single script — unit tests, stress test, headless session, checkpoint sanity check — exits with status 0 and prints ALL VERIFICATION CHECKS PASSED, on a machine with no Alpaca credentials set.
5. Measurable target for the day
Your AUMEngine.update_price() implementation must sustain an average per-tick latency under 5 microseconds at a portfolio size of 1,000 positions, as measured by stress_test.py, with zero cents of drift between the incremental total and the brute-force baseline across a 2,000-tick run.
If you hit all five, you’re clear for Day 81.
Homework: Production Challenge
The current reconcile() cadence is a fixed tick count (every 50 ticks), which means reconciliation frequency scales with tick volume, not wall clock time — during a quiet market you might go minutes between checks; during a volatility spike you might reconcile every few hundred milliseconds, right when the O(n) cost is least welcome.
Replace the tick-count trigger with a time-based reconciliation schedule (e.g., every 2 seconds of wall-clock time, independent of tick volume) using asyncio‘s event loop timer facilities rather than blocking sleep calls — and make sure a burst of ticks arriving faster than your reconciliation interval doesn’t cause reconciliation calls to queue up and run back-to-back. Extend tests/test_aum_engine.py with a test that simulates a tick burst and asserts reconciliation still runs at a bounded rate.




