## Outcome **Schema 49 is already sufficient for Sprint 20B.** The minimal path is to activate and connect the existing canonical valuation, performance-scope, coverage, FX, and ingestion components. No schema migration is justified unless tracking mode must be user-configurable and durable rather than derived from account role/source. ## Existing reusable architecture - **Performance engine:** `ledger/performance.py` - `ttwror_daily_v1()` at line 205 - `xirr_v1()` at line 382 - `twr_v1()`, `annualize_return()`, attribution and quality primitives - Explicit daily-boundary and missing-FX failure behavior; no live-data fallback. - **Performance orchestration:** `services/portfolio_performance.py` - `build_performance_coverage()` at line 59 - `_load_activities()` / `load_scope_activities()` at lines 296/374 - `_load_valuations()` and account aggregation at lines 392/542 - `build_portfolio_performance()` at line 667 - **Scope and cash-flow evidence:** `services/performance_scope.py` - `set_performance_scope_classification()` at line 23 - `set_performance_cashflow_coverage()` at line 100 - Portfolio transfers are neutralized while account-level transfers become external flows; covered by `test_scope_aware_raiffeisen_akb_transfer...`. - **Canonical persistence, schema 49:** `storage/migrations.py` - `account_value_snapshots` line 1552 - `portfolio_valuation_snapshots` line 1741 - `performance_cashflow_coverage` line 1801 - Immutable `portfolio_ingestion_batches/items` lines 2015–2080 - Existing `fx_rates`, `transactions`, scope registry, market prices, audit log - Canonical valuation rows already carry scope/account, original/base currency, FX rate/direction, timestamp, capture cutoff, source/reference, version/supersession and quality reasons. - **Generic preview/confirm/import:** `services/portfolio_data.py` - `preview_ingestion()` line 941 - `_insert_valuation()` lines 1064–1080 - `confirm_ingestion()` lines 1232–1367 - Atomic `BEGIN IMMEDIATE`, preview expiry, source revision/fingerprint validation, confirmation idempotency, immutable batch/items and audit. - Existing source adapters cover PostFinance, legacy account totals, cash snapshots and canonical transactions (`_records()`, lines 913–922). - **Provider paths:** - PostFinance: `preview_postfinance_import()` / `confirm_postfinance_import()` in `services/postfinance_service.py`; generic ingestion additionally writes a partial canonical baseline. - True Wealth: preview/confirm/manual-value workflows in `services/truewealth_service.py`; confirmed values land in `account_value_snapshots` and can already be promoted through `legacy_account_values`. - Daily securities valuation: `services/portfolio_analytics.py::run_daily_market_valuation()` line 395; values confirmed positions using dated prices/FX plus cash and writes account/instrument canonical snapshots. - **APIs:** `api/routers/overview.py` - ingestion preview/confirm/history lines 80–116 - reconciliation lines 119–139 - coverage lines 142–145 - stored-data-only performance API lines 148–175 - **Consumers:** `wealth_cockpit.py::build_wealth_cockpit()` lines 978–1130 already calls the canonical performance and period-aware coverage services. ## Gaps / contract fields 1. **No generic performance `tracking_mode` exists.** - `accounts.balance_mode` is cash-specific and should not be reused. - For schema-49 Sprint 20B, derive mode from role/source: - `ledger_daily_market` for holdings + cash accounts - `provider_total_value` for True Wealth/robo/pension/total-value accounts - `baseline_only` where transaction history is incomplete - Expose the derived mode in source/coverage responses. Add persistence only if users must override it. 2. **No external daily-valuation payload contract.** - `IngestionPreviewRequest` only supports source selection plus PostFinance file fields; it cannot accept generic daily rows. - If Sprint 20B requires CSV/API daily totals, add fields such as: - `file_name`, `content_base64` or structured `valuations` - per row: `account_id`, `valuation_at`, `value_original`, `currency`, `fx_rate_to_base`, `source_reference`, `quality_status`, `reason_codes` - These map directly to the existing snapshot table; no migration needed. 3. **Cash-flow coverage is not integrated into generic confirmation.** - Add optional preview-bound evidence fields: - `cashflow_coverage_from/to` - `cashflow_coverage_status` - `cashflow_coverage_source/note` - Update coverage only after successful atomic confirmation and only when the import actually proves completeness. 4. **Coverage route omits period/account parameters.** - The service supports `from_date`/`to_date`, but `/portfolio/performance/coverage` calls it without them. Expose the same period used by the performance request. 5. **Daily market job is not universal.** - It covers canonical securities and latest cash, not provider-total-only accounts such as True Wealth. - `INSERT OR REPLACE` with stable IDs may conflict with immutable valuation triggers when retrying a prior partial run. Replace this with the existing append-only version/supersession insert pattern. ## Minimal implementation plan 1. **`services/portfolio_data.py`** - Add one `daily_account_valuations` source adapter. - Normalize rows into existing `SourceRecord` valuation records. - Reuse `_insert_valuation()`, preview hash binding, versioning, batch/items, audit and idempotency. - Include tracking mode and cash-flow-coverage impact in preview. 2. **`api/schemas/portfolio_data.py`** - Add strict daily valuation row/file fields and optional coverage evidence. - Bind all new fields into preview/confirm identity; do not allow extras. 3. **`services/portfolio_analytics.py`** - Refactor daily snapshot writes to a shared append-only canonical valuation helper. - Keep this runner for ledger-revalued accounts only. 4. **`services/truewealth_service.py`** - Prefer promoting the confirmed total directly into a canonical valuation inside the same transaction, using shared versioning/idempotency logic. - Lower-risk alternative: retain the current two-stage `account_value_snapshots → legacy_account_values` promotion, but expose it explicitly as pending activation. 5. **`services/performance_scope.py` / `portfolio_performance.py`** - Add a deterministic role-to-tracking-mode helper. - Surface derived mode and period-specific blockers in coverage; do not modify TTWROR/XIRR calculations. 6. **`api/routers/overview.py`** - Add period/account query parameters to the coverage endpoint. ### Focused tests - Extend `test_portfolio_data_ingestion_reconciliation.py`: - daily-row preview is storage-free - stale/tampered confirm rejected - duplicate confirm idempotent - corrections append versions and supersession - valuation + coverage + audit commit atomically - missing FX is blocked/partial, never estimated - Extend `test_portfolio_market_analytics_v1.py`: - partial-run retry does not hit immutable-trigger failure - rerun produces no duplicate active value. - Extend `test_true_wealth_valuation_pilot.py`: - confirmed provider total becomes exactly one canonical valuation. - Preserve `test_sprint14_performance_contract.py` and `test_portfolio_performance_foundation.py`. - Add an end-to-end Sprint 20B test: daily valuations + covered external flows yield TTWROR and XIRR; baseline-only and incomplete coverage remain explicitly unavailable. ## Verification / repository state - Confirmed branch `sprint20b/performance-activation-daily-valuations-v1` at exact SHA `7f1d76bc62e19861e55929273859e27ca6d74970`. - Ran the four focused suites with `uv run pytest`: **78 passed, 1 deprecation warning**. - **No files created or modified.** - Initial bare `pytest` command was unavailable; `uv run pytest` used the repository environment successfully.