Implementation design · 004-portfolio-weights

Portfolio weight calculation

How we will build it.

Status Draft Spec 004-portfolio-weights Owner Brian Corbin · @briancorbinxyz Branch 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.

PrincipleComplianceNotes
P-2 · Small, reversible changesOKOne derived read-view over positions; O(N)-on-read first, incremental maintainer deferred (D-003).
P-3 · Explicit over implicitOKThe denominator basis is an explicit WeightBasis enum, never inferred (D-001, FR-002).
P-6 · Tests accompany behaviorOKTDD per story: tests written and failing before implementation (T-100/T-200/T-300).
P-7 · Done means verifiedOKSum-to-one invariant, staleness flag, and reweight latency are all directly checked (§8 risks, SLOs).
P-8 · Ship dark by defaultOKportfolio.analytics.enabled defaults false; the slice is off until switched on (T-901).
P-9 · Decisions are groundedOKD-001/D-002 verified against the spec; D-003 rests on a spike accepted as a §8 risk.
P-15 · Structured observabilityOKMicrometer reweight timer on /q/metrics; OpenAPI at /q/openapi (T-901).
P-16 · Service level objectivesOKReweight, 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 TradeLedger log 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-9 on 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). double appears in exactly one place — the final display ratio mtm/denominator; the market values it divides are exact integers. Ship dark: portfolio.analytics.enabled default 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 onSourceStatusFinding / 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 verified 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 verified 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 verified 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 spike 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. assumed 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.

POSITION SNAPSHOT 003 · exact-int MTM PORTFOLIOWEIGHTS fold → WeightBook WEIGHTS + ROLLUPS ratios, Herfindahl
Exact-integer market values flow from 003 into the weights fold, which emits floating-point ratios and concentration rollups. The one division is the only floating-point step.

5 · Alternatives considered

OptionWell-defined with shortsSum-to-oneLatency-fitTotal
Net ΣMV denominator2147
Long-only denominator (drop shorts)1337
Gross Σ|MV| denominator — chosen default55414

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 ΣMV denominator 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 WeightBasis enum: GROSS = Σ|MV| (default; weights in [0,1] summing to 1) and NET = Σ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-9 under gross is asserted on every recompute.
Consequences
+ A single, contained use of double on the money path — everything upstream stays exact. − Requires the 1e-9 tolerance contract and care that double never 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

RiskLikelihoodImpactMitigation
O(N)-on-read misses p99 < 500 µs at N=5000 (accepts the D-003 spike).MMBenchmark 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.MHNear-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.LHdouble 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.LMThe 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

  1. Initial design. Gross/net denominator (D-001), contained double display 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.