## Outcome Analyzed PR3/Sprint 17B.2 quote-refresh behavior at exact commit `785e18b20219b4fe734576a8126d23c453050ce9`. The current implementation is a useful base, but does **not** yet satisfy the requested refresh modes, response truthfulness, or production dry-run architecture. ### Current implementation findings - **Page load is provider-safe:** `EquityPage.vue:onMounted → loadPositions()` performs cached/local GETs only. - **View reload is also provider-safe:** `LoadActionBar @load="loadPositions(true)"` only bypasses frontend cache. - **Current batch action is always “missing only”:** - `frontend/src/api/equity.ts:updateEquityQuotes()` hardcodes `only_missing: true`. - **Refresh target is incorrectly bound to old data:** - `EquityPage.vue:updateEquityQuotesExplicit()` passes `summary.as_of`. - `updateSelectedQuote()` also passes `summary.as_of`. - The individual action then unnecessarily starts a second full batch. - **No true “refresh all”:** there is no frontend call with `price_date: null, only_missing: false`. - **Backend response is incomplete:** - Has `target_date` and per-instrument `price_date`, but not separately named requested/effective/actual dates. - No final common data date. - Results are untyped dictionaries and expose only the final provider, not provider-attempt errors. - **Coverage is potentially false:** - `refresh_equity_quotes_batch()` inventories mapped instruments with nonempty symbols—not actual held positions. - Missing mappings/symbols are excluded entirely, so they never appear in totals/results/errors. - Multiple positions sharing one instrument are not represented per position. - **Calendar handling is insufficient:** - `_effective_market_date()` handles weekends only. - It loses the originally requested weekend day. - No exchange holiday, timezone, or market-open/completed-session policy. - **`updated=0` is not highlighted:** the UI can report an ordinary partial result without an explicit no-update warning. - **Dry run partly exists but is not production-UAT ready:** - `QuoteRefreshRequest.dry_run` exists. - `refresh_equity_quote()` really calls the provider and skips `market_prices`, chart-cache, commit, and valuation writes. - However, the POST is blocked in production’s default `write_mode="disabled"`. - Dry-run `updated` counts successful provider responses even though nothing was updated. - `valued`/`complete` are recomputed from the unchanged DB, making the response internally confusing. - No dedicated route or test proving all relevant DB/audit/cache tables remain unchanged. ## Sequential implementation plan ### 1. Contract and date semantics **Files** - `src/jarvis_finance/api/schemas/market.py` - `frontend/src/api/types.ts` **Changes** - Replace dictionary results with typed response models, including: - `position_id`, `instrument_id`, `ticker` - mapped provider and symbol - `requested_target_date` - `effective_target_date` per exchange/session - `actual_provider_date` - `status`, attempts, sanitized errors - cached/persisted/dry-run outcome - Extend batch response with: - `dry_run` - `positions_total`, `instruments_total`, `provider_calls` - `updated`, `would_update`, `cached`, `missing`, `failed` - `final_common_data_date` - explicit overall status: `complete | partial | failed | noop | dry_run` - Define `final_common_data_date` as the minimum eligible actual date across **all required positions**, and `null` when complete common coverage is unavailable. ### 2. Trading-session resolution **Files** - New: `src/jarvis_finance/market_data/calendar.py` - Potentially `pyproject.toml` if adopting a maintained exchange-calendar dependency - `src/jarvis_finance/services/market_service.py` **Functions** - Replace `_effective_market_date()` with a resolver preserving: 1. requested calendar day, 2. exchange-specific latest completed session, 3. actual provider day. - Do not store an in-progress session as a final close. - Fail conservatively for unsupported exchange/calendar mappings. ### 3. Correct batch inventory and orchestration **File** - `src/jarvis_finance/services/market_service.py` **Functions** - Refactor `_has_fresh_price_for_target()` to return cache provenance and actual date, not only `bool`. - Refactor `refresh_equity_quote()` to return explicit requested/effective/actual dates. - Rewrite `refresh_equity_quotes_batch()` to: - inventory active held equity positions, including unmapped/missing-symbol positions; - deduplicate provider calls by instrument/mapping; - project one result per position; - implement `only_missing=true` and `only_missing=false` distinctly; - keep `price_date=null` server-resolved; - make limit truncation explicit; - return a warning whenever a non-dry-run action persists zero quotes; - skip portfolio valuation during dry run; - compute common date only from eligible complete coverage. ### 4. Provider-attempt visibility **File** - `src/jarvis_finance/market_data/prices.py` **Functions/types** - Extend `EquityPriceQuote` or introduce a typed provider-attempt trace. - Update `CompositeEquityPriceProvider.get_price()` so fallback attempts retain sanitized provider/status/error information rather than exposing only the last provider. ### 5. Separate frontend actions **Files** - `frontend/src/api/equity.ts` - `frontend/src/pages/EquityPage.vue` **Changes** - Keep **Ansicht neu laden** as GET-only `loadPositions(true)`. - Add explicit: - **Alle Kurse aktualisieren** → `{price_date: null, only_missing: false}` - **Fehlende Kurse laden** → `{price_date: null, only_missing: true}` - Individual refresh: - send `price_date: null`; - refresh only that instrument; - remove the subsequent full-batch call. - Render requested day, actual/common data day, counts, partial errors, and a prominent `updated=0` warning. - Preserve the current no-provider-on-open behavior. ### 6. Production UAT dry-run boundary **Files** - `src/jarvis_finance/api/routers/market.py` - `src/jarvis_finance/api/main.py` - `src/jarvis_finance/api/security.py` - New UAT documentation under `docs/uat/` **Architecture** - Add a dedicated endpoint such as: - `POST /api/market/equity/update-quotes/dry-run` - Router must force `dry_run=True`; do not trust a client flag to select persistence behavior. - Permit this endpoint in disabled production mode **only from loopback**, avoiding a general remote write-bypass. - Dry-run response should use `would_update`; `updated` must remain zero. - UAT procedure should fingerprint/count `market_prices`, `equity_price_points`, analysis/run, alert, and audit tables before and after, while asserting provider call count is nonzero. ## Exact test plan ### Backend **New file:** `tests/unit/test_equity_quote_refresh_sprint17b2.py` Tests: 1. View GET endpoints never instantiate/call providers. 2. `price_date=null, only_missing=false` refreshes all eligible held instruments. 3. `price_date=null, only_missing=true` skips valid cached coverage. 4. Individual refresh is independent of old summary `as_of`. 5. Weekend retains requested Sunday and reports actual Friday session. 6. Exchange holiday reports requested holiday and prior actual session. 7. Market-open request targets the latest completed session. 8. Rate limit retries and reports provider attempts without mutating instrument status. 9. Missing symbol remains in per-position coverage/results with zero provider calls. 10. Multiple positions sharing one instrument cause one provider call but produce multiple position results. 11. Partial provider coverage produces honest counts, errors, and `final_common_data_date=null`. 12. Non-dry-run `updated=0` includes an explicit warning. 13. Dry run calls providers, returns `would_update`, keeps `updated=0`, and leaves all relevant tables unchanged. 14. Complete mixed-session run computes the minimum actual date as the final common date. 15. Limit truncation is explicitly reported and cannot claim complete coverage. **Update existing** - `tests/unit/test_sprint11_portfolio_completeness.py` - Preserve retry/cache/quality tests. - Remove assumptions that mapped instruments without positions define coverage. - `tests/unit/test_market_quotes_detail_charts_v1.py` - Update typed response contract and dry-run endpoint coverage. - `tests/unit/test_api_write_security.py` - Verify dry-run route is loopback-only in disabled mode. - Verify normal quote-refresh POST remains blocked. ### Frontend **Update:** `frontend/src/pages/EquitySprint11.test.ts` Replace the current “keeps backend valuation cutoff” test with: 1. Reload performs GET refresh only and no quote API. 2. Refresh all sends `priceDate=null, onlyMissing=false`. 3. Missing-only sends `priceDate=null, onlyMissing=true`. 4. Individual action sends `priceDate=null` and does not invoke batch refresh. 5. `updated=0` warning is visible. 6. Partial counts/provider errors/common date render correctly. 7. No provider mutation call occurs on page load or drawer open. **Update** - `frontend/src/pages/EquityPageHotfix.test.ts` - Add API request-shape tests near `frontend/src/api/client.test.ts` or a new `frontend/src/api/equity.test.ts`. ## Principal risks - Exchange-calendar dependency and MIC/exchange alias mapping need an explicit architectural decision. - Current one-commit-per-quote behavior allows partially persisted runs; concurrency/run locking should be considered. - Auto-provider fallback may use a symbol valid for one provider but not another. - Dry runs consume real provider quotas despite not persisting. - Per-position results can reveal portfolio metadata; the UAT endpoint should remain loopback-only and sanitize errors. - Changing coverage from “mapped instruments” to “held positions” will intentionally alter existing totals/tests. - Existing `limit=100` can silently prevent complete refreshes if the portfolio grows. ## Verification - Backend focused suite: **19 passed**. - Frontend equity suites: **7 passed**. - `git diff --check`: passed. - Repository remained unchanged; **no files created or modified**. - Minor issue: this worktree had no `.venv`; backend tests were run with the already-available compatible FinanceManager virtual environment and `PYTHONPATH=src`. [NOTE: subagent modified files the parent previously read — re-read before editing: /home/agent/.hermes/worktrees/FinanceManager-sprint17b-review-corrections-responsive/frontend/src/api/household.ts, /home/agent/.hermes/worktrees/FinanceManager-sprint17b-review-corrections-responsive/frontend/src/components/household/TransactionDetailDrawer.vue, /home/agent/.hermes/worktrees/FinanceManager-sprint17b-review-corrections-responsive/frontend/src/pages/HouseholdReviewPage.vue, /home/agent/.hermes/worktrees/FinanceManager-sprint17b-review-corrections-responsive/frontend/src/pages/HouseholdTransactionsPage.vue, /home/agent/.hermes/worktrees/FinanceManager-sprint17b-review-corrections-responsive/src/jarvis_finance/api/schemas/household.py, /home/agent/.hermes/worktrees/FinanceManager-sprint17b-review-corrections-responsive/src/jarvis_finance/services/household_review_corrections.py]