Implementation design · 004-portfolio-weights
Portfolio weight calculation
How we will build it.
004-portfolio-weights
Created
Read time
Compute each holding's weight as its market value over the book total, reading exact-integer
mark-to-market values from the position engine (003)
and dividing only once — the final ratio is the one place double appears on the money path.
The denominator is an explicit, selectable basis (gross Σ|MV| default, net ΣMV
guarded near zero) so weights stay well-defined with shorts. The single biggest risk is latency:
whether O(N)-on-read meets the reweight SLO (p99 < 500 µs at N=5000) is a spike, with a
running-denominator incremental maintainer as the ready fallback.
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.
| Principle | Compliance | Notes |
|---|---|---|
| P-2 · Small, reversible changes | One derived read-view over positions; O(N)-on-read first, incremental maintainer deferred (D-003). | |
| P-3 · Explicit over implicit | The denominator basis is an explicit WeightBasis enum, never inferred (D-001, FR-002). | |
| P-6 · Tests accompany behavior | TDD per story: tests written and failing before implementation (T-100/T-200/T-300). | |
| P-7 · Done means verified | Sum-to-one invariant, staleness flag, and reweight latency are all directly checked (§8 risks, SLOs). | |
| P-8 · Ship dark by default | portfolio.analytics.enabled defaults false; the slice is off until switched on (T-901). | |
| P-9 · Decisions are grounded | D-001/D-002 verified against the spec; D-003 rests on a spike accepted as a §8 risk. | |
| P-15 · Structured observability | Micrometer reweight timer on /q/metrics; OpenAPI at /q/openapi (T-901). | |
| P-16 · Service level objectives | Reweight, normalization, and freshness SLOs are inherited from the spec and tracked here (§2). |
These are the load-bearing principles this slice turns on. The full
P-1…P-20 register applies unchanged; platform-wide conformance is
tracked in the umbrella design (001).
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; test quarkus-junit5, rest-assured.
- Architecture
- Vertical-slice module
io.spectastic.portfolio.weights— a derived read view over the event-sourced position snapshot (fold TradeEvent + PriceTick) owned by 003; this slice owns weights only. - Storage
- None of its own. Reads the in-memory position snapshot from 003 at a ledger sequence; the
TradeLedgerlog is owned upstream. - Testing
- JUnit5 unit tests plus one
@QuarkusTest(rest-assured over/q/openapi); jacoco. - Coverage
- Diff-aware: a patch target on changed lines, and a change never lowers coverage of what it touches. No universal project percentage.
- Perf targets
- Reweight p99 < 500 µs full-book at N=5000 (SLO-reweight);
|Σweights − 1| ≤ 1e-9on every gross recompute (SLO-normalization); freshness p99 < 2 ms from originating tick (SLO-freshness). - Constraints
- Exact integer fixed-point on the money path (
PRICE_SCALE=10_000, whole-long quantities).doubleappears in exactly one place — the final display ratiomtm/denominator; the market values it divides are exact integers. Ship dark:portfolio.analytics.enableddefault false.
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 |
|---|---|---|---|
| 003 exposes a read-consistent market-value snapshot at a ledger sequence; weights read it and never recompute MTM. | specs/003-position-engine/spec.html |
Position engine owns mark-to-market; this slice consumes it (spec §6 assumption, out-of-scope defer-to 003). | |
| The denominator must be an explicit, selectable gross/net basis, gross summing to one within tolerance. | specs/004-portfolio-weights/spec.html#FR-002 |
FR-002 mandates gross Σ|MV| (default) and net ΣMV with documented sign semantics. | |
| The reweight envelope is p99 < 500 µs full-book at N=5000. | specs/004-portfolio-weights/spec.html#SLO-reweight |
SLO-reweight, 28-day window, occurrence budgeting; drives the compute-strategy choice. | |
| O(N)-on-read reweight meets SLO-reweight at N=5000, or a running-denominator maintainer is required. | Benchmark the fold at N=5000 | Pending — O(N)-on-read is the spike baseline; the incremental maintainer (D-003) is the fallback if it misses. | |
| All market values are in a single base currency for this slice. | — | Multi-currency translation is deferred to TBD-multi-currency-base (spec §6); not worth verifying now. |
4 · Approach
The slice is a single derived view. On read it folds the current 003 position snapshot into a
WeightBook: it accumulates the denominator under the selected WeightBasis
(gross Σ|MV| or net ΣMV), then emits each instrument's weight as the exact
integer market value divided by that denominator. The division is the sole floating-point step; every
value feeding it stays an exact integer, and the gross invariant |Σweights − 1| ≤ 1e-9 is
asserted on each recompute. Empty books yield empty weights (no divide-by-zero), and the net basis
carries a near-zero-denominator guard that flags instability instead of emitting exploding weights.
For the spike we compute O(N) on read, which is trivially reconcilable against a full replay; the
running-denominator incremental maintainer that lands the p99 SLO at N=5000 is noted and deferred
(§9). Concentration rollups — top-N holdings and a Herfindahl index Σ weight² — derive
from the same weights. A weight whose underlying price is older than the freshness bound is marked
stale rather than silently served.
5 · Alternatives considered
| Option | Well-defined with shorts | Sum-to-one | Latency-fit | Total |
|---|---|---|---|---|
| Net ΣMV denominator | 2 | 1 | 4 | 7 |
| Long-only denominator (drop shorts) | 1 | 3 | 3 | 7 |
| Gross Σ|MV| denominator — chosen default | 5 | 5 | 4 | 14 |
Gross Σ|MV| is the default because it is the only basis that is well-defined with shorts
and sums to one; net ΣMV is still offered (guarded near zero) for the desk that
wants signed exposure. Orthogonally, on compute strategy we weighed O(N)-on-read against a
running-denominator maintainer on the same latency-fit criterion: O(N)-on-read is chosen for the spike
for its simplicity and reversibility (P-2), and the running-denominator maintainer is the noted
optimization that lands p99 < 500 µs at N=5000, deferred within the slice (D-003, §9).
6 · Decisions
D-001 · Explicit, selectable denominator — gross default, net guarded
- Status
Accepted - Context
- A naive
ΣMVdenominator breaks with shorts — weights exceed 100% or blow up near a zero net (spec §1). FR-002 requires the denominator to be explicit and selectable (specs/004-portfolio-weights/spec.html#FR-002). - Decision
- A
WeightBasisenum:GROSS = Σ|MV|(default; weights in[0,1]summing to 1) andNET = ΣMV(offered, with documented sign semantics and a near-zero-denominator guard). - Consequences
- + Unambiguous concentration with shorts; + the sum-to-one invariant is directly testable. − Two code paths plus a guard to maintain.
D-002 · Weights are double ratios over exact-integer market values
- Status
Accepted - Context
- The money path is exact integer fixed-point (
PRICE_SCALE=10_000, whole-long quantities); a fraction of the book cannot be represented exactly as an integer. NFR-002 bounds the normalization error (specs/004-portfolio-weights/spec.html#SLO-normalization). - Decision
- Weight =
(double) mtm / denominator. Market values stay exact integers; only the final ratio is floating. The invariant|Σweights − 1| ≤ 1e-9under gross is asserted on every recompute. - Consequences
- + A single, contained use of
doubleon the money path — everything upstream stays exact. − Requires the 1e-9 tolerance contract and care thatdoublenever leaks back into market-value arithmetic.
D-003 · O(N)-on-read for the spike; running-denominator maintainer deferred
- Status
Accepted - Context
- SLO-reweight is p99 < 500 µs at N=5000 (
specs/004-portfolio-weights/spec.html#SLO-reweight); whether O(N)-on-read meets it at that N is unmeasured — the §3 spike. Accepted as a §8 risk. - Decision
- Compute weights O(N) on read from the current 003 position snapshot for the spike. Note the running-denominator incremental maintainer as the optimization that lands the SLO, deferred within the slice (§9).
- Consequences
- + Simplest, most reversible first cut (P-2), correctness before speed, and trivially reconcilable against a full replay. − May miss p99 at N=5000 until the incremental maintainer lands.
7 · Project structure
Only the new or changed paths, grouped by the vertical-slice boundary the §6 decisions draw.
src/
main/java/io/spectastic/portfolio/weights/
PortfolioWeights.java # fold snapshot → WeightBook; ratios, guard, staleness, Herfindahl
WeightBasis.java # GROSS (default) | NET enum
test/java/io/spectastic/portfolio/
PortfolioWeightsTest.java # sum-to-one, reweight-on-tick, shorts + guard + staleness
8 · Risks & mitigations
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| O(N)-on-read misses p99 < 500 µs at N=5000 (accepts the D-003 spike). | M | M | Benchmark the fold at N=5000 (the §3 spike); the running-denominator incremental maintainer (D-003) is the ready fallback that meets the SLO. |
| Net-basis denominator nears zero on a balanced long/short book. | M | H | Near-zero guard flags instability instead of emitting exploding weights (US3, FR-002); gross is the default. |
The double ratio leaks back into exact market-value arithmetic. | L | H | double confined to the final mtm/denominator ratio; market values stay integer; |Σweights − 1| ≤ 1e-9 asserted every recompute (NFR-002). |
| A stale price is served without a flag. | L | M | The freshness bound marks weights stale past the bound (FR-003, SLO-freshness); covered by SC-002. |
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 running-denominator incremental maintainer is deferred within the slice. For the spike, weights are computed O(N) on read from the current position snapshot — the simplest correct cut and trivially reconcilable against a full replay (P-2). The incremental maintainer is the optimization that lands p99 < 500 µs at N=5000, but it adds mutable running state and tick-ordering care that correctness and the sum-to-one invariant should precede. It is tracked as the §3 spike and the §8 latency risk, and will land only if the N=5000 benchmark shows O(N)-on-read misses SLO-reweight.
10 · Open questions
None outstanding — the denominator choice (offer both, gross default, net guarded near zero), the
single contained use of double, and the O(N)-on-read-for-now compute strategy were all
settled against the spec and the shared design contract.
11 · Change log
- Initial design. Gross/net denominator (D-001), contained
doubledisplay ratio over exact-integer market values (D-002), O(N)-on-read for the spike with running-denominator maintainer deferred (D-003). Consumes the 003 position snapshot; SLOs inherited from the spec.