Implementation design · 003-position-engine

Real-time position engine

How we will build it.

Status Draft Spec 003-position-engine Owner Brian Corbin · @briancorbinxyz Branch 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).

PrincipleComplianceNotes
P-2 · Small, reversible changesOKOne fold plus a revaluation step; three functional requirements in scope, weights and lots deferred to siblings.
P-3 · Explicit over implicitOKState 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 whyOKAggregate-vs-per-lot, the removal-by-proportion rule, and O(1) revaluation are grounded decisions D-001..D-003.
P-6 · Tests accompany behaviorOKTDD 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 verifiedOKReconcile-to-replay to the cent (SC-001) and jacoco patch coverage gate the slice as done.
P-9 · Decisions are groundedOKEvery must-tier decision here is grounding="verified" against spec FR/NFR ids; no assumed facts on the money path.
P-15 · Structured observabilityOKMicrometer timers for the fold and the revalue path, exported on Prometheus /q/metrics.
P-16 · Service level objectivesOKUpdate p99 < 25 µs, revalue p99 < 15 µs, reconcile 99.999% — cited from the spec SLOs.
P-8 · Ship dark by defaultn/aThe 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 into AnalyticsPlatform (@ApplicationScoped) alongside the other io.spectastic.portfolio modules.
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 @QuarkusTest REST 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-long quantities. 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 onSourceStatusFinding / note
Net quantity is a signed incremental fold that must equal the signed sum of fills. spec.html#FR-001 verified 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 verified 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 verified 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 verified 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 verified 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 verified §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.

TradeLedger append-only log PositionEngine fold + revalue Snapshot as-of sequence N
TradeEvents and PriceTicks fold into per-instrument aggregate state; a deep-copy snapshot exposes all positions consistently at one ledger sequence.

5 · Alternatives considered

Optionlatency-fitno-duplication-with-005replay-exactnessTotal
Per-lot basis inside the position engine3159
Recompute-from-replay each read15511
Aggregate average-cost — chosen55515

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): signed netQty (long) and signed costBasisTotal (long, scale 1e-4). Average cost = costBasisTotal / netQty is 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 to newNet * price for 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 PriceTick recompute mtmValue and unrealizedPnl for the held instrument in O(1); ticks never mutate netQty, 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

RiskLikelihoodImpactMitigation
Average-cost drift across a sign-flip diverges from replay.MHD-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.LHShare 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.LMMicrometer 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.LMSnapshot 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

  1. 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.