Implementation design · 003-position-engine
Real-time position engine
How we will build it.
003-position-engine
Created
Read time
Fold the ledger into an aggregate Position per (account, instrument) — a signed
netQty (long) and a signed costBasisTotal (long, scale 1e-4) — updated in O(1) per fill, with
mark-to-market recomputed in O(1) per held instrument on each price tick. All money-path arithmetic is exact
integer fixed-point, so the incremental result bit-matches a from-zero replay. The single biggest risk is
average-cost drift at a sign-flip: it is retired by settling the removal-by-proportion and crossing-zero-reset
rules as exact integer arithmetic (D-002), tested against replay before any impl lands.
1 · Principles check
Verify this design respects every principle in v0.1.0. Any violation requires either a principles amendment or a deliberate exception logged below.
Focused on the principles most load-bearing for this slice; platform-wide principle conformance is tracked in the umbrella design (001).
| Principle | Compliance | Notes |
|---|---|---|
| P-2 · Small, reversible changes | One fold plus a revaluation step; three functional requirements in scope, weights and lots deferred to siblings. | |
| P-3 · Explicit over implicit | State is an explicit aggregate (netQty, costBasisTotal); sign-flip and crossing-zero rules are written as exact integer expressions (D-002), not left to averaging. | |
| P-5 · Record the why | Aggregate-vs-per-lot, the removal-by-proportion rule, and O(1) revaluation are grounded decisions D-001..D-003. | |
| P-6 · Tests accompany behavior | TDD per story — tests written and failing before impl (T-100/T-110, T-200/T-210, T-300/T-310) plus a reconcile-to-replay test. | |
| P-7 · Done means verified | Reconcile-to-replay to the cent (SC-001) and jacoco patch coverage gate the slice as done. | |
| P-9 · Decisions are grounded | Every must-tier decision here is grounding="verified" against spec FR/NFR ids; no assumed facts on the money path. | |
| P-15 · Structured observability | Micrometer timers for the fold and the revalue path, exported on Prometheus /q/metrics. | |
| P-16 · Service level objectives | Update p99 < 25 µs, revalue p99 < 15 µs, reconcile 99.999% — cited from the spec SLOs. | |
| P-8 · Ship dark by default | n/a | The umbrella flag portfolio.analytics.enabled (default false) is owned by 001; this slice adds no new toggle. |
2 · Technical context
- Language(s)
- Java 21 (Temurin 21.0.9)
- Frameworks
- Quarkus 3.37.3, Maven; extensions quarkus-rest, quarkus-rest-jackson, quarkus-arc, quarkus-micrometer-registry-prometheus, quarkus-smallrye-openapi
- Architecture
- Event-sourced, modular-monolith: the engine is a stateful in-memory projection folding
TradeEvent+PriceTick, wired intoAnalyticsPlatform(@ApplicationScoped) alongside the otherio.spectastic.portfoliomodules. - Storage
- None of its own — reads the append-only
TradeLedger(002); position state is in-memory, rebuildable by replay. Snapshot = deep copy; restore-then-replay-tail == full replay. - Testing
- Fast JUnit5 unit tests plus one
@QuarkusTestREST smoke; rest-assured for the smoke. - Coverage
- jacoco, diff-aware: patch ≥ 90% on touched domain, and a change never lowers coverage of what it touches. No single universal %.
- Perf targets
- Update p99 < 25 µs, revalue p99 < 15 µs, reconcile-to-replay 99.999% bit-match.
- Constraints
- No floating point on the money path: exact integer fixed-point at scale 1e-4 (
PRICE_SCALE = 10_000), whole-longquantities. Incremental result MUST bit-match a from-zero replay.
3 · Grounding & evidence
Every design-bearing fact this design rests on, and how it was confirmed — read this before the
approach: it separates what the design knows from what it assumes (per
REQ-LIFECYCLE-006). Status is verified (opened the source
this turn), spike (decidable only by a time-boxed investigation — run it, record the
finding), or assumed (taken as true without verification). A must-tier decision
may not rest on an assumed/unresolved-spike fact unless accepted as a
<spec-risk> (see §8).
| Claim the design rests on | Source | Status | Finding / note |
|---|---|---|---|
| Net quantity is a signed incremental fold that must equal the signed sum of fills. | spec.html#FR-001 |
FR-001 fixes signed net per (account, instrument), updated per fill, equal to a folded replay. | |
| Cost basis is VWAP of open exposure and resets to the crossing fill on a sign change. | spec.html#FR-002 |
FR-002 mandates the reset; grounds the D-002 sign-flip arithmetic. | |
| A price tick revalues held instruments in O(1) and never touches quantity. | spec.html#FR-003 |
FR-003 scopes MTM/unrealized P&L to a per-tick O(1) revaluation. | |
| Latency envelope: update p99 < 25 µs, revalue p99 < 15 µs. | SLO-update · SLO-revalue |
Rules out recompute-from-replay per read; requires an incremental fold and O(1) revaluation. | |
| Exact integer fixed-point at scale 1e-4 makes incremental state bit-match replay. | Shared design contract (money-path); reconcile-to-cent NFR-003 |
Whole-long quantities and long basis totals; no floating point, so the fold is deterministic and replay-exact. | |
| Per-lot decomposition belongs to 005 and only has to agree with this aggregate. | spec.html §4 note · 005 |
§4 note states lot-level basis is a different decomposition maintained separately; grounds D-001's deferral. |
4 · Approach
The engine keeps one aggregate row per open position: a signed netQty (whole long) and a signed
costBasisTotal (long, scale 1e-4). A fill folds in O(1). Adding exposure in the current direction
accumulates quantity and basis; reducing exposure removes basis in proportion to the quantity removed by exact integer
arithmetic removed = costBasisTotal * |f| / |net| (D-002); a fill that crosses zero closes the old side and
resets basis to newNet * price for the residual. Average cost (costBasisTotal / netQty) is
derived on read for display only — the authoritative per-lot split is 005's job (D-001). Because every operation is
integer-exact, the incremental fold bit-matches a from-zero replay.
Mark-to-market is a separate, cheap path: on each PriceTick the engine recomputes mtmValue and
unrealizedPnl for the held instrument in O(1) and never mutates quantity (D-003); a tick for an un-held
instrument is a no-op. A consistent snapshot returns every position as of a single ledger sequence — a deep copy taken
at sequence N so no consumer reads a torn mix of pre- and post-event state. New code lives under
io.spectastic.portfolio.position as PositionEngine and Position, reading the shared
model owned by 001 and wired through AnalyticsPlatform.
5 · Alternatives considered
| Option | latency-fit | no-duplication-with-005 | replay-exactness | Total |
|---|---|---|---|---|
| Per-lot basis inside the position engine | 3 | 1 | 5 | 9 |
| Recompute-from-replay each read | 1 | 5 | 5 | 11 |
| Aggregate average-cost — chosen | 5 | 5 | 5 | 15 |
Per-lot basis inside the engine duplicates the decomposition that 005-tax-lots owns and drifts from it; recompute-from-replay on every read is trivially exact but violates the update/revalue latency SLOs. The aggregate average-cost fold fits the latency envelope, keeps a single lot authority in 005, and stays replay-exact through integer arithmetic.
6 · Decisions
D-001 · Aggregate state per position, per-lot deferred to 005
- Status
Accepted - Context
- FR-001/FR-002 (§3 grounding) require signed net quantity and a VWAP cost basis per
(account, instrument); the §4 note assigns lot-level decomposition to 005. - Decision
- Keep aggregate state per
(account, instrument): signednetQty(long) and signedcostBasisTotal(long, scale 1e-4). Average cost =costBasisTotal / netQtyis display-only; the authoritative per-lot decomposition is deferred to 005 and must agree in aggregate. - Consequences
- + O(1) fold, single lot authority, replay-exact. − Realized-gain relief cannot be read here; it waits on 005.
D-002 · Sign-flip rule by exact integer arithmetic
- Status
Accepted - Context
- FR-002 (§3 grounding) mandates that basis reset to the crossing fill on a sign change; average-cost drift at the flip is the slice's biggest risk.
- Decision
- Reducing exposure removes basis proportionally by exact integer arithmetic
removed = costBasisTotal * |f| / |net|(deterministic → replay-exact); a fill crossing zero resets basis tonewNet * pricefor the residual, so the old side's basis does not bleed into the new one. - Consequences
- + Deterministic, replay-exact, no special-case mutate path. − Integer truncation must be applied consistently in both the fold and the replay oracle so they bit-match.
D-003 · Mark-to-market on tick is O(1) and never changes quantity
- Status
Accepted - Context
- FR-003 and SLO-revalue (§3 grounding) require per-tick MTM/unrealized in O(1) per held instrument within p99 < 15 µs.
- Decision
- On each
PriceTickrecomputemtmValueandunrealizedPnlfor the held instrument in O(1); ticks never mutatenetQty, and a tick for an un-held instrument is a no-op. - Consequences
- + Revaluation stays inside the latency envelope and cannot corrupt quantity. − MTM freshness is bounded by tick arrival; stale marks are a data-feed concern, not the engine's.
7 · Project structure
Only the new or changed paths, grouped by the boundaries the §6 architecture-pattern decision draws.
src/main/java/io/spectastic/portfolio/
position/
PositionEngine.java — fold TradeEvent + PriceTick, snapshot at sequence
Position.java — aggregate state: netQty, costBasisTotal, mtmValue, unrealizedPnl, asOfSeq
src/test/java/io/spectastic/portfolio/
PositionEngineTest.java — net qty + VWAP basis, sign-flip, MTM, snapshot, reconcile-to-replay
8 · Risks & mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Average-cost drift across a sign-flip diverges from replay. | M | H | D-002 exact integer removal/reset rule; a reconcile-to-replay test spanning at least one sign-flip and one correction gates the slice (SC-001). |
| Integer truncation applied inconsistently between fold and replay oracle. | L | H | Share one arithmetic helper between fold and oracle; property test asserts bit-match over generated fill streams. |
| Fold or revalue path misses the latency SLO under load. | L | M | Micrometer update/revalue timers on Prometheus /q/metrics; O(1) per event by construction; no per-read recompute. |
| Torn snapshot reads a mix of pre- and post-event state across instruments. | L | M | Snapshot is a deep copy taken at a single sequence N (FR-004); US3 test asserts every instrument is at exactly N. |
9 · Complexity tracking
Anywhere this design introduces complexity not strictly required by the spec, justify it here. A reviewer should be able to point at any non-trivial choice and find its rationale in this section.
The only non-obvious mechanism is the exact-integer sign-flip arithmetic (D-002). It is not gold-plating: FR-002 requires the basis reset, and floating-point averaging would violate the reconcile-to-cent NFR. It is contained to one helper shared by the fold and the replay oracle, keeping the added complexity minimal and testable.
10 · Open questions
None outstanding — the sign-flip basis-reset rule (D-002) and the snapshot-at-sequence read-consistency contract (FR-004) were settled in the interview, and lot-level decomposition is explicitly deferred to 005, not open.
11 · Change log
- Initial design. Aggregate position state (D-001), exact-integer sign-flip basis rule (D-002), O(1) mark-to-market on tick (D-003); TDD per story with a reconcile-to-replay gate.