## Audit outcomes ### Critical gaps - **Batch quote refresh is hard-capped at 10 and cannot paginate.** - `QuoteRefreshRequest.limit` defaults to `10`: `src/jarvis_finance/api/schemas/market.py:17-23`. - Equity SQL applies `LIMIT ?` and defaults to 10: `src/jarvis_finance/services/market_service.py:114-128`. - No offset/cursor/`has_more` exists in request or response: `api/schemas/market.py:17-34`. - Repeating the request always selects the same alphabetically first instruments. - The SQL scans all mapped instruments, not current confirmed holdings, so even raising the limit to 22 does not guarantee the 22 positions are selected. - Frontend explicitly sends 10: `frontend/src/api/equity.ts:15`, `frontend/src/pages/EquityPage.vue:197`. - Deterministic 22-instrument probe produced: `total=10`, `updated=10`, `stored=10`. - **Historical collection can persist a current quote as historical.** - `refresh_market_prices()` requests `effective_date`, but persists using that requested date without validating `quote.price_timestamp`: `market_data/prices.py:459-477`. - Finnhub, Twelve Data, Massive, and EODHD ignore `price_date` and fetch current/latest data: `market_data/prices.py:197-238`, `257-292`. - `CompositeEquityPriceProvider` can therefore fall through from historical-capable FMP to a current-only provider: `market_data/prices.py:295-308`. - Probe confirmed a quote timestamped `2026-07-26` was stored with `price_date='2026-07-01'`. - The daily orchestrator correctly rejects future position quotes at `services/portfolio_analytics.py:410-414` and benchmark quotes at `568-570`; the standalone CLI refresh path does not. - **Transient provider failures falsely mutate instruments to `suspected_inactive`.** - HTTP providers convert authentication, entitlement, rate-limit, network, and generic provider errors into `quality_status="missing"`: `market_data/prices.py:73-110`, `165-190`, `197-292`. - Every missing quote then changes durable instrument metadata: `market_data/prices.py:480-486`. - Probe with `mock_network_error` changed an active instrument to `suspected_inactive`. - A later successful quote resolves price alerts but does **not** restore `instrument_status`: `market_data/prices.py:424-428`. - No test asserts that network/auth/rate-limit failures leave instrument metadata unchanged; current missing-price tests only check alert deduplication: `tests/unit/test_fx_market_data_phase_c.py:127-137`. ### Important gaps - **Provider failure classification is inconsistent and lossy.** - `_quality_from_error()` recognizes only rate limit, missing, unsupported, and stale: `services/market_service.py:28-38`; auth, entitlement, network, timeout, and 5xx collapse to the default. - Equity batch catches thrown exceptions as generic `provider_error`, truncated to ten entries: `services/market_service.py:132-143`. - Quote-return failures usually become warnings rather than typed errors. - Daily valuation converts all fetch failures/no-data conditions to `price_missing`: `services/portfolio_analytics.py:381-405`. - `ProviderRateLimitError` and `ProviderFetchError` are declared but unused: `market/providers.py:59-64`. - **Only FMP implements historical EOD semantics.** - FMP uses `/stable/historical-price-eod/full`, a seven-calendar-day window, rejects future rows, and selects the latest eligible prior row: `market_data/prices.py:93-142`. - Covered by `tests/unit/test_sprint9_fx_fmp_history.py:81-121`. - FMP converts restricted/auth/rate-limit/network failures to undifferentiated missing quotes. - Other pricing providers expose the same `get_price(..., price_date=...)` signature but silently ignore the date. - No formal provider capability contract (`supports_historical_eod`, pacing, retry policy, markets/currencies) exists in `EquityPriceProvider`: `market_data/prices.py:47-49`. - `instrument_provider_statuses()` reports lookup/key status, not pricing/history capability: `market_data/catalog.py:572-621`. - Stooq is advertised as an available EOD fallback and Alpha Vantage as optional, but neither has a pricing implementation/factory registration: `market_data/catalog.py:580-583`; factory is only `market_data/prices.py:311-323`. - **No equity pacing or retry.** - Batch and daily valuation loops are sequential but issue requests immediately: `services/market_service.py:132-143`, `services/portfolio_analytics.py:374-451`. - Equity HTTP clients have no backoff, `Retry-After`, minimum interval, or injectable sleeper: `market_data/prices.py:66-91`, `157-179`. - Catalog lookup has throttling and one 429 retry, but that path is unrelated to valuation pricing: `market_data/catalog.py:127-208`. - CoinGecko has retries/backoff and chunk pacing, demonstrating a reusable pattern: `market/providers.py:67-111`, `224-300`. - **Partial valuation runs cannot resume.** - Both `complete` and `partial` runs are terminal/idempotent: `services/portfolio_analytics.py:349-354`. - Existing test explicitly locks this behavior in: `tests/unit/test_sprint9_fx_fmp_history.py:161-185`. - Thus a transient miss remains partial forever for the same date/fingerprint. - `market_data_runs` has aggregate counters only—no per-instrument cursor/checkpoint/attempt/error state: `storage/migrations.py:1841-1862`. - A `running` run can restart from the beginning, but there is no true cursor and no persisted retry schedule. ### Minimal implementation seams 1. Replace string-based failures with a structured category such as `no_data`, `configuration`, `auth`, `entitlement`, `rate_limited`, `network`, `timeout`, `provider_5xx`, `invalid_payload`, and `unsupported_historical`. 2. Add explicit provider capabilities and refuse current-only providers for historical requests; `auto` must filter by required capability. 3. Validate `source_date/timestamp <= requested_date` centrally before every `store_market_price()` call, and persist the actual source date rather than the requested date. 4. Remove automatic `suspected_inactive` mutation. Create a review alert only for genuine `no_data`; inactivity should require repeated evidence/manual confirmation. 5. Make batch selection holdings-aware and process all eligible confirmed positions. If bounded, add deterministic cursor/`has_more`; do not repeatedly select the first ten. 6. Add sequential pacing and bounded retry/backoff to equity providers, honoring 429/`Retry-After`. 7. For real crash/retry resume, add per-run/per-instrument checkpoint state. At minimum, stop treating `partial` as permanently idempotent and retry only failed/retryable instruments without rewriting successful provenance. ### Verification - Relevant existing suites passed: **32 passed**: - `test_market_quotes_detail_charts_v1.py` - `test_fx_market_data_phase_c.py` - `test_provider_integration_v2.py` - Tests ran against a disposable `/tmp` runtime; no secrets or production DB were accessed. - Repository remained unchanged; no files created or modified. - Initial pytest attempt was correctly blocked because inherited environment variables referenced the productive runtime; rerun used isolated `/tmp` paths.