# Portfolio market completeness and UX release pattern

Use this reference when completing a portfolio valuation universe and releasing a user-facing holdings page. It complements the architecture and sprint-execution references.

## 1. Diagnose before changing data

Build one sanitized row per active confirmed position:

- canonical name, ISIN and source ticker;
- mapped provider symbol, venue and expected quote currency;
- most recent accepted quote and date;
- controlled historical request outcome;
- normalized error class (`rate_limited`, `auth_error`, `plan_restricted`, `network_error`, `symbol_mismatch`, `currency_mismatch`, `exchange_mismatch`, `price_missing`);
- eligible cache hit or miss;
- smallest safe remediation.

Do not infer rate limiting from a sequence that merely stops producing prices. Record HTTP/provider class separately from an empty payload. Never persist complete provider responses or secrets.

## 2. Make provider capability explicit

Each provider should declare at least:

- `supports_latest`;
- `supports_historical_as_of`;
- `supports_exchange_suffix`;
- `rate_limit_policy`.

A provider without reliable historical-as-of support must not participate in historical auto-fallback. A fallback quote is eligible only when all of these hold:

1. returned provider symbol equals the confirmed mapping;
2. quote currency equals the confirmed trading currency;
3. returned venue matches an explicit alias set for the confirmed venue;
4. quote date is on or before the cutoff;
5. quote age is inside the accepted business-day window;
6. actual provider, symbol, venue, quote date and fetched-at timestamp are stored;
7. canonical ISIN remains unchanged.

A provider-created row whose close is `NaN` is not a price. Select the latest non-empty close on or before the cutoff; never substitute a current quote silently.

## 3. Validate cache eligibility, not merely cache presence

A cache row must not count toward coverage just because it is `fresh` and has a numeric close. Revalidate it against the current mapping:

- provider symbol;
- quote currency;
- primary provider or an approved historical fallback;
- venue for fallback rows;
- cutoff and business-day age;
- no error marker.

Use the same eligibility predicate for all three decisions:

- skip an already covered position;
- compute batch coverage;
- build the audited valuation snapshot.

This prevents a wrong-currency, stale, mismapped or unrelated provider row from making a batch look complete.

## 4. Controlled full-universe refresh

- Select the full active mapped universe; do not leave a UI/default `limit=10` bottleneck.
- Order missing positions before eligible cached positions.
- Process sequentially with fixed pacing.
- Retry only bounded transient classes such as rate limits and network errors.
- Do not retry plan/auth/symbol/currency/venue failures blindly.
- Resume by skipping eligible cached positions.
- Never mark an instrument inactive from a single provider, network, auth or plan failure.
- Return per-position status, attempts, provider, symbol, currency and price date.

An individual “retry quote” action must carry the valuation snapshot's `as_of` date. After it succeeds, run or resume the full batch/valuation so the audited read model actually updates.

## 5. Honest read-model semantics

Treat valuation quality independently from the presence of a numeric value:

- `null` means unknown and must remain `null`;
- stale values may remain in audit payloads but must not become user-facing fresh values;
- only quality=`fresh` values count as valued coverage;
- a portfolio/equity total is publishable only when the audited snapshot itself is complete and every expected account/instrument key has an eligible value;
- during partial coverage, expose a labelled valued partial subtotal, not a full total;
- suppress percentages when the denominator is incomplete;
- derive the unvalued list from the full position universe, never from a top-N view.

Apply the gate at both layers:

- backend: nullable totals and no investment signals when valuation is incomplete;
- frontend: hide/lock global asset and platform allocations and advisor signals rather than coercing `null` to zero.

Watch for chart/allocation helpers that coerce `null` with `Number(value ?? 0)` or draw a minimum bar; these can visually turn unknown exposure into zero.

## 6. UX contract

A professional holdings page should show immediately:

- complete total or clearly labelled valued partial subtotal;
- `valued/total` coverage;
- cutoff date;
- last successful valuation run;
- missing-position filter when needed.

Table expectations:

- default market-value descending;
- sortable columns;
- depot, asset-type and data-status filters;
- name/ticker/ISIN search;
- right-aligned Swiss-formatted money;
- sticky header;
- an explicit status/reason for missing values;
- human depot labels without rewriting original account IDs.

Responsive detail pattern:

- desktop: fixed right drawer around 460–520 px;
- tablet/mobile: bounded bottom sheet with internal scrolling;
- preserve background/table position;
- close by button, Escape and backdrop;
- focus close button on open, trap Tab, restore prior focus;
- keep technical provenance inside a collapsed section;
- hide unfinished trade actions; only render actions that execute a real workflow.

Rendering the page and opening cached details must not call external providers. Charts and quote refreshes are explicit actions.

## 7. Verification and release order

1. Run the complete workflow on a SQLite backup/copy, including fallback retrieval and idempotent replay.
2. Assert complete position count, actual provider/date provenance, protected quantities and policy state.
3. Test stale, wrong-currency, future-date, symbol/venue mismatch, rate-limit retry, resume, null totals and signal/allocation suppression.
4. Run full backend, full frontend, typecheck, production build, compileall, lint, migration tests, repository/secret safety and `git diff --check`.
5. Perform the final diff review **after** the last test-driven edits. If review finds issues, fix them and rerun affected full gates; a review before the final edits is not final evidence.
6. Capture pre-deployment responsive screenshots while the old release is still live.
7. Commit, push, open PR, await CI, merge normally, and verify remote main SHA.
8. Before production data work: backup, SHA-256, integrity check, restore/open test, business digest and timer state.
9. Deploy the exact merged main commit, run the controlled valuation, restore timer state, then capture post-deployment screenshots and automated UAT evidence.

Keep financial screenshots outside Git when they contain real values; report their runtime paths instead.
