# Portfolio/equity completeness and data-honesty audit

Use this reference for read-only reviews of an equity holdings page and its portfolio-summary APIs, especially when acceptance requires honest partial totals, responsive detail UX, local-only rendering, and explicit search/filter/sort contracts.

## Trace values end to end

Inspect and cite every seam:

1. ledger/position calculation;
2. service mapping;
3. response schema/OpenAPI model;
4. frontend API type;
5. page computed values and formatters;
6. focused backend and frontend tests.

Do not approve a total merely because it is a decimal string or existing tests are green. Check what the number means.

## Data-honesty invariants

- Never collapse `unknown` into numeric zero.
- Preserve legitimate zero as `0.00`; never use numeric truthiness to infer snapshot/value presence.
- Missing provider snapshots, prices, or FX rates stay nullable or explicitly unavailable.
- A subtotal of known positions is `partial` whenever any in-scope position is unvalued.
- Portfolio totals should expose: `complete | partial | unavailable`, known or nullable value, valued/total counts, missing reason codes, valuation `as_of`, and freshness.
- Separate valuation completeness from cost-basis completeness. Missing cost basis does not make market value unknown; zero market value does not prove missing cost basis.
- Verify current holdings are valued from quantities × current stored prices × stored FX, not by summing historical transaction gross amounts. Probe buys, sells, voids, dividends, fees, and transfers.
- A browser request timestamp is not a valuation timestamp.

## Contract sufficiency

An equity-row contract normally needs more than a free-text `status`:

- stable position, account, and instrument IDs;
- separate platform and account labels;
- ticker and ISIN;
- quantity and price currency;
- nullable price/value;
- explicit price, FX, valuation, and cost-basis statuses;
- reason codes and freshness/as-of metadata.

Prefer a typed collection envelope when page-level coverage/status is required. Ensure detail routes declare response models rather than returning untyped dictionaries.

## UX acceptance checks

### Completeness status

Require an API-backed statement such as “8 of 10 positions valued · known subtotal CHF … · 2 excluded · as of …”. Backend/runtime badges are not a substitute.

### Search/filter/sort

Verify behavior, not just control presence:

- search includes name, ticker, ISIN, account, and platform;
- dedicated filters cover account/platform, asset type, and valuation status;
- columns sort deterministically, including null placement and tie-breakers;
- result count and clear/reset state are visible;
- controls have accessible labels and URL state when acceptance requires it.

### Responsive detail surface

Inspect actual breakpoint classes and rendered placement. A component named `Drawer` is not proof of drawer behavior.

- desktop: right-side drawer/panel when required;
- tablet/iPad: deliberate sheet or side panel, not accidental inline content;
- mobile: bottom sheet with backdrop;
- all modes: dialog semantics, accessible name, Escape close, focus trap/restore, scroll containment, and honest loading/error state.

Treat CSS that changes `fixed` to `static` at an early breakpoint as a high-risk seam: it often turns the intended drawer into ordinary content on tablet and desktop.

## Provider-call boundary

Trace every on-mount and row-selection request to backend implementation. Prove render/detail GETs are cache/DB-only. Provider access belongs only behind explicit user action, with tests that monkeypatch provider functions to fail if render paths touch them.

A hard-coded “provider calls: no” label is not proof. Conversely, distinguish explicit refresh/chart clicks from automatic render calls.

## Deterministic counterexample probes

Cover at least:

1. one valued plus one unvalued position: known subtotal is partial;
2. legitimate zero remains `0.00`, not null;
3. missing managed-account snapshot is unavailable, not zero;
4. current market price differs from transaction cost: overview uses current valuation;
5. buy then sell: historical gross amounts are not accumulated into current holdings;
6. missing FX with present local-currency price;
7. missing cost basis with complete market valuation;
8. duplicate account names on different platforms;
9. rapid row selection/detail failure does not display stale details;
10. mobile, tablet/iPad, and desktop detail geometry plus keyboard behavior;
11. no provider calls on initial load or detail open.

## Historical cutoff, fallback, and cache acceptance

Apply one acceptance predicate consistently to fresh provider responses, fallback responses, and resume/cache hits:

- source date is at or before the requested cutoff;
- source age is within the accepted business-day window;
- provider supports historical-as-of;
- quote is positive and finite;
- provider symbol is exact/approved;
- currency matches the confirmed mapping;
- exchange matches the confirmed market or an explicit alias;
- quality/error state is acceptable.

Do not let a broad cache query (`fresh` + non-empty close) bypass stricter provider validation. Batch `complete` must reflect the audited valuation result, not only the broad preflight/cache count.

Persist observed provider provenance separately from expected mapping metadata. Filling a missing observed exchange with the expected exchange and then presenting it as provider provenance is misleading.

Check benchmarks independently: benchmark paths often reject future dates but omit currency, exchange, and maximum-age checks before storing `quality_status='fresh'`.

Explicit retry actions in a historical view must propagate the displayed cutoff. A per-position retry that sends only `provider` silently refreshes today rather than repairing the historical run.

## Stale values versus complete totals

Numeric presence is not valuation completeness. A valuation engine may deliberately preserve stale values for visibility while correctly marking the run partial. The API and frontend must not then infer `complete`, non-null total, `fresh`, or “valued” solely from `market_value != null`.

Use a deterministic probe: seed one confirmed position, return a positive quote older than the allowed business-day window, run valuation, and compare run status with summary completeness, total nullability, row freshness/status, and frontend-derived state. A `partial/stale` run exposed as `complete/fresh` is blocking.

Preserve null through adjacent surfaces. Audit allocation/chart helpers and platform aggregations for `null ?? 0`, `Number(null)`, minimum-width bars, and sums that turn unknown equity into `0.00 CHF`.

## Eligibility guardrails at every entry point

Use the same eligibility predicate for batch refresh, direct single-instrument refresh, daily valuation, cached reuse, and benchmark mapping:

- account is active;
- instrument active flag is true;
- instrument status is not inactive, delisted, suspended, or merged;
- valuation policy permits automatic refresh/valuation;
- mapping status and uniqueness are acceptable.

Guarding only the batch route leaves direct-refresh and scheduled-valuation bypasses.

## Exact-tree and privacy review

- Review merge-base-to-working-tree, not only `base...HEAD`; a feature branch may contain only uncommitted work.
- Include untracked files in scope.
- Treat holdings inventories, ISINs, account/provider associations, and price-availability matrices as productive financial data even without quantities or balances. A document labeled “sanitized” can still reconstruct portfolio composition and violate repository privacy rules.

## Reporting shape

Lead with **“Blocking findings exist”** or **“No blocking findings.”** Report release-relevant findings by severity. For each, include the violated invariant, exact current file/line references, a concrete failure path or deterministic counterexample, why existing tests miss it, and the smallest contract or UX boundary that must change. Separate verified non-findings (for example, render GETs stayed provider-free) from defects. State which tests/probes actually ran and whether the tree remained unchanged. Do not promote missing local dependencies into a durable product finding.