## Review verdict **Blocking findings exist.** I reviewed the complete base-to-working-tree change from `73a2ae293de831ae72dcaa01004656437bd6dcbb`, including untracked Sprint 11 files. ### High — Blocking 1. **Stale valuations are exposed as fresh and can produce a supposedly complete total.** `run_daily_market_valuation()` deliberately retains stale quotes in `values`, while excluding them from current coverage (`portfolio_analytics.py:451-476`, `523-532`, `656-670`). The read model then determines completeness only by whether every position has a numeric value, labels every numeric value `fresh`, and returns a non-null total (`equity_service.py:109-145`, `152-170`). The frontend repeats the same presence-only logic (`EquityPage.vue:162-167`). A runtime counterexample produced a `partial` run with `stale_price`, but `/equity/summary` reported `coverage_complete=true`, `status=complete`, and the position reported `data_status=fresh`. This defeats historical-cutoff freshness and null/partial-total guarantees. 2. **Inactive and valuation-policy exclusions are not enforced consistently.** Canonical valuation includes instruments based only on `i.is_active=1`; it does not reject `instrument_status IN ('inactive','delisted','suspended','merged')` or `valuation_policy='exclude_from_auto_price_update'` (`portfolio_analytics.py:200-217`). Direct quote refresh has no status or policy guard before contacting a provider and persisting a quote (`market_service.py:173-217`). Batch refresh excludes inactive statuses but still ignores valuation policy (`market_service.py:233-245`). Benchmark mapping similarly checks only `i.is_active` (`portfolio_analytics.py:287-301`). Therefore an excluded/delisted instrument can still be provider-fetched and included in valuation through direct or daily-run paths. 3. **Benchmark quotes bypass the currency, exchange, and staleness validation applied to portfolio quotes.** The benchmark path rejects only future or missing prices (`portfolio_analytics.py:590-600`). It never verifies the quote currency against `benchmark_mapping["currency"]`, never checks `provider_market`, and never enforces the two-business-day age limit. Nevertheless it stores the snapshot as `quality_status="fresh"` and counts benchmark coverage as complete (`portfolio_analytics.py:631-650`, `663-668`). A stale or wrong-listing benchmark can therefore make policy analytics appear complete. 4. **The resume/cache coverage check trusts any “fresh” row without validating provider provenance, symbol, currency, or exchange.** `_has_fresh_price_for_target()` checks only instrument, cutoff, non-empty close, and freshness (`market_service.py:124-134`). Batch processing uses that result to skip provider calls and calculate `valued`, `complete`, and whether valuation should run (`market_service.py:246-317`). A fresh row from the wrong listing/provider or with the wrong currency can be treated as valid coverage and never repaired by the default `only_missing=true` workflow. This is weaker than `_cached_exact_date_quote()`’s provenance checks and can produce a false `complete=true` batch response. 5. **The new tracked candidate document discloses a real portfolio inventory contrary to repository policy.** `docs/sprint11-price-diagnosis.md:5` identifies the scope as 22 PostFinance positions, and lines `19-42` enumerate instrument names, ISINs, symbols, providers, exchanges, currencies, and price availability. Calling the document “sanitized” does not anonymize the portfolio composition. This conflicts with `AGENTS.md` requiring productive financial data to remain outside Git. ### Medium 6. **The per-position retry action loses the displayed historical cutoff.** The missing-price drawer’s “Erneut versuchen” action calls `updateSelectedQuote()` (`EquityPage.vue:92`, `173`), but the API helper sends only `{provider}` (`frontend/src/api/equity.ts:15`). The backend consequently defaults to the current business date (`market_service.py:185-187`). From a historical 2026-07-24 snapshot, this button fetches a current quote rather than repairing the missing 2026-07-24 input, and the historical analysis remains incomplete. 7. **Null equity values are rendered as zero in portfolio allocation surfaces.** `PortfolioPage.vue:44` forwards the newly nullable equity value into `AllocationBars`; `AllocationBars.vue:26-29` converts null to zero and even draws a minimum 2% bar. `PortfolioPage.vue:49` likewise sums missing equity values as zero into platform allocations. Thus the KPI may correctly show an unavailable value while adjacent allocation charts misleadingly show `0.00 CHF`. ### Low — Accessibility 8. **Mobile position cards are keyboard-focusable but lack interactive semantics and Space activation.** `EquityPage.vue:43-48` uses a focusable `
` with click and Enter handlers. Screen readers will not identify it as a button/link, and standard Space activation is absent. Use a real button/link or add correct role, keyboard handling, and accessible naming. ## Additional notes - The reviewed equity position/summary GET paths did not introduce provider calls or database writes; the added GET test covers provider-call absence. - No hardcoded credentials, SQL injection, command injection, or direct XSS issue was found in the reviewed changes. - **Files modified by this review:** none. - **Issue encountered:** the worktree-local `.venv/bin/python` path was absent; the focused read-only runtime counterexample was executed with the available environment Python instead.