## Outcome Inspected the deployed Sprint 21 baseline at `572ef44fc330911673ce190b7abfbbf210bff77c`, schema 51, read-only. The smallest Sprint 22 design is a **new read-only wealth-series projection over existing snapshots**, kept completely separate from the canonical XIRR/TTWROR input path. ## Canonical valuation architecture ### PostFinance market valuation - `src/jarvis_finance/services/portfolio_analytics.py` - `confirmed_canonical_positions()`: - Reads only confirmed, non-voided transactions. - Applies official PostFinance position anchors from `latest_official_postfinance_positions()`. - Replays only post-snapshot confirmed quantity deltas. - `_latest_cash_entries()` / `_latest_cash_by_account()`: - Enforce `balance_date <= as_of`. - Rank cash by account/currency and date. - Replace legacy depot cash with reconciled official settlement-cash components. - `store_valuation_snapshot()`: - Existing append-only valuation primitive. - Uses `snapshot_version` and `supersedes_snapshot_id`. - Rejects conflicting replay of the same source observation. - `run_daily_market_valuation()`: - Converts requested weekends to the preceding business day. - Uses cutoff-safe historical quotes only. - Rejects future prices and incomplete account inputs. - Materializes instrument and account snapshots into `portfolio_valuation_snapshots`. - **Important:** this worker still exists, but the currently deployed timer CLI does not invoke it. - `src/jarvis_finance/services/portfolio_aggregation.py` - `postfinance_account_roles()`: explicit role mapping, no name inference. - `latest_official_postfinance_cash()`: selects the latest official snapshot at or before the cutoff and reconciles rounded currency components to the official cash subtotal. - `latest_official_postfinance_positions()`: authoritative official quantity/value baseline. - `post_snapshot_postfinance_quantity_deltas()`: replays confirmed trades after that baseline. ### True Wealth - `src/jarvis_finance/services/truewealth_valuation.py` - `_official_anchors()`: - Reads immutable active `account_value_snapshots`. - Restricts to `truewealth_official_import` and `manual_total_value`. - Selects one latest observation per valuation date. - `_latest_position_anchor()` / `_anchor_positions()`: - Require a complete, reconciled True Wealth position snapshot. - `_exact_price()` / `_exact_fx()`: - Require exact-date persisted prices and FX. - `build_truewealth_model_preview()`: - Models positions from the confirmed anchor. - Derives anchor cash once, adds only confirmed external cash flows. - Blocks on missing exact-date price/FX or CHF cash-flow amount. - `store_truewealth_modelled_daily_valuation()`: - Activation-gated, append-only, fingerprinted and idempotent. - Stores source `truewealth_modelled_daily` with `quality_status='partial'` and reasons `modelled_valuation` / `ttwror_provisional`. - `run_truewealth_market_one_shot()`: - Uses one coherent market date across all positions. - Permits the existing 0–2 business-day market carry window, but persists the **actual quote date** and then invokes the exact-date model writer. - `build_truewealth_performance_view()`: - Existing mixed official/modelled reader. - Latest model version wins per day. - Official anchor suppresses the same-day modelled point. - Separates requested and available periods. - `build_truewealth_anchor_reconciliation()`: - Compares a later official anchor with the preceding model. - Explicitly leaves historical model rows unchanged. ### Crypto There are two distinct contracts that Sprint 22 must not conflate: 1. `src/jarvis_finance/services/daily_valuations.py` - `run_daily_crypto_valuation()`: - Canonical performance-account valuation. - Requires an included `crypto_portfolio` role and complete cash-flow coverage. - Reconstructs quantities from a verified start snapshot or confirmed transactions. - `_exact_prices()` requires an exact calendar-day stored CHF price. - Historical runs never substitute today’s quote. - Corrections append a higher `snapshot_version` with `supersedes_snapshot_id`. 2. `src/jarvis_finance/services/crypto_market_recovery.py` - `run_crypto_market_one_shot()`: - Current-day-only recovery/current-wealth writer. - Explicitly rejects backdating: `"current crypto market one-shot cannot backdate recorded holdings"`. - Writes source `daily_crypto_current_valuation_v1`. - Uses virtual scope `crypto-recorded-holdings`, with `account_id=NULL`; it is not automatically a performance account. - This is the crypto worker currently called by the daily CLI. The unified reader must prefer the classified performance-account series where available and must never add the virtual aggregate and classified crypto account together. ### Cash/current readers - `src/jarvis_finance/services/cash_service.py` - `_latest_snapshot()`, `_calculated_balance()`, `_post_snapshot_cash_deltas()`, `list_cash_positions()`, `get_cash_summary()`. - Current display priority includes official PostFinance components, manual balances, CSV anchors and confirmed post-anchor movements. - `src/jarvis_finance/services/equity_service.py` - `_latest_analysis()`, `_analysis_value_is_eligible()`, `list_equity_positions()`, `get_equity_summary()`. - Current equity values use audited market-analysis values when eligible. - `src/jarvis_finance/services/crypto_service.py` - `_latest_crypto_price()` and `list_crypto_positions()` use the latest current price. - These functions are valid for the current-value card but **must not be called by a historical Sprint 22 series builder**. ## Effective daily snapshot selection and performance gates - `src/jarvis_finance/services/portfolio_performance.py` - `_load_valuations()`: - Ranks `portfolio_valuation_snapshots` by account/scope/day/source and latest version/capture. - Re-canonicalizes competing variants to one scope/account/day. - Materializes official PostFinance components read-only. - Requires a complete two-account PostFinance pair. - Same-day PostFinance source priority is daily market, then official component anchor, then other. - Loads legacy/official account totals from `account_value_snapshots`. - Official True Wealth replaces only the same-day model candidate without rewriting it. - `_aggregate_account_valuations()`: - Emits a portfolio point only if every selected performance account exists on the same date. - `_endpoint_valuation_quality()`: - XIRR endpoints must be complete; provisional/modelled endpoint metadata prevents a complete result. - `build_portfolio_performance()`: - Enforces audited scope membership. - Requires complete `performance_cashflow_coverage`. - Blocks TTWROR/XIRR for unsupported or unclassified period activities. - Requires exact opening, closing and cash-flow boundary valuations. - Preserves stored FX per valuation. - Global portfolio performance is unavailable when required crypto scope/history is absent. These gates should remain unchanged. ## Existing wealth cockpit - `src/jarvis_finance/services/wealth_cockpit.py` - `period_bounds()` already provides `ytd`, `previous_year`, `12m`, and `all`. - `_current_values()` composes current cash, equity, True Wealth, crypto and unassigned values. - `_household_history()` currently: - Uses only exact dates shared by every required account. - Explicitly performs no carry or interpolation. - Selects source-specific same-day winners. - `build_wealth_cockpit()` keeps household history/current wealth separate from `build_portfolio_performance()`. - `src/jarvis_finance/api/routers/overview.py` - `GET /portfolio/wealth-cockpit`. - `src/jarvis_finance/api/schemas/wealth_cockpit.py` - Existing period and source contracts. - `history` is currently an untyped `dict[str, Any]`. ## Timer and CLI - `deploy/systemd/finance-manager-market-valuation.timer` - Daily at `23:30 Europe/Zurich`, persistent. - `deploy/systemd/finance-manager-market-valuation.service` - Calls `run-daily-market-valuation`. - Deployed fail-closed with `JARVIS_FINANCE_DAILY_VALUATION_ENABLED=0`. - `src/jarvis_finance/cli/main.py` - `_daily_source_exit_code()` treats activated blocked/partial/failed sources as failures. - `run-daily-market-valuation` checks schema 51 without applying migrations. - Runs sources in isolated connections through `run_isolated_daily_sources()`. - Current source list is: - `run_crypto_market_one_shot()` - activation-gated `run_truewealth_market_one_shot()` - It currently does **not** call `portfolio_analytics.run_daily_market_valuation()` for PostFinance. ## Reuse-first Sprint 22 backend design Add a pure read model, preferably in a small new module such as: - `src/jarvis_finance/services/modelled_wealth_series.py` - Typed response additions in `src/jarvis_finance/api/schemas/wealth_cockpit.py` - Expose through the existing `GET /portfolio/wealth-cockpit` history payload, preserving existing fields for compatibility. ### Algorithm 1. Reuse `wealth_cockpit.period_bounds()` and the existing active-account/role classification. 2. Load source observations only from: - `portfolio_valuation_snapshots` - `account_value_snapshots` - `cash_account_snapshots` - read-only official PostFinance component projection from `portfolio_aggregation` 3. Apply existing winner rules: - Highest valuation version/capture per day. - Official True Wealth beats same-day model. - Complete coherent PostFinance pair only. - Cash source priority already encoded by `_household_history()`. 4. Build an event-driven date set from: - all observation dates in the selected period, - requested period boundaries, - requested `as_of`. 5. For each source and point date, select only an observation with `observation_date <= point_date`. 6. Assign component quality: - `confirmed`: exact-date official/manual confirmed account or cash anchor. - `modelled`: exact-date persisted daily market, True Wealth model or crypto valuation. - `carried`: prior eligible confirmed/modelled observation reused without repricing. - `unknown`: no eligible observation on or before the point date. 7. Aggregate point quality conservatively: - `unknown` if any required component is unknown. - else `carried` if any component is carried. - else `modelled` if any component is modelled. - else `confirmed`. 8. Return both: - `value_chf` only when all required components are known. - `known_value_chf` plus missing component keys for partial diagnostics. 9. Preserve mixed-date provenance: - Per component: `observation_date`, `quality`, safe source kind. - Point: `mixed_dates`, oldest/newest observation date. - Do not expose account IDs or source row identifiers. 10. Add correction metadata: - `explicit_supersession` when `supersedes_snapshot_id` exists or `snapshot_version > 1`. - `replaced_same_day_observation` when a source-specific same-day winner displaced another immutable candidate. - Otherwise `none`. ### Hard boundary against historical backfill The historical builder must not call: - `get_equity_summary()` - `list_crypto_positions()` - `_latest_crypto_price()` - any provider - any valuation writer It must never multiply historical quantities by the latest/current quote. Missing historical evidence becomes `carried` from an older stored valuation or `unknown`; it never becomes a newly modelled historical number. The resulting wealth series is for charting and household wealth change only. It must **not** be passed into `_load_valuations()` or `build_portfolio_performance()`. XIRR/TTWROR continue to consume only their canonical exact stored valuation series and existing quality/coverage gates. ## Schema decision **No database schema migration is necessary for the minimal Sprint 22 design.** Schema 51 already provides: - values and business dates, - source and source reference, - captured timestamp/data cutoff, - quality/reason metadata, - append-only versions and explicit supersession, - official account-value anchors, - cash snapshots, - performance scope and cash-flow coverage, - run fingerprints and audits. Only API/Pydantic contract expansion is needed. If audit-grade explicit correction lineage were later required for every `cash_account_snapshots` or `account_value_snapshots` replacement, rather than a derived same-day marker, that would justify a future migration. It is not required for the reuse-first Sprint 22 projection. ## Tests to add Extend `tests/unit/test_wealth_cockpit_v1.py` with deterministic cases for: - mixed source observation dates; - confirmed/modelled/carried/unknown precedence; - missing-as-unknown, never zero; - official True Wealth same-day precedence; - latest snapshot version and correction marker; - coherent PostFinance pair selection; - crypto virtual/classified aggregate deduplication; - period presets and boundary points; - no post-period/future observation use; - no network/provider/current-price reader calls; - unchanged `build_portfolio_performance()` XIRR/TTWROR outputs and quality gates. Existing relevant regression coverage includes: - `tests/unit/test_sprint20b_performance_activation_daily_valuations.py` - `tests/unit/test_sprint20c_performance_activation_hardening.py` - `tests/unit/test_sprint20d_truewealth_valuation.py` - `tests/unit/test_sprint14_performance_contract.py` - `tests/unit/test_portfolio_performance_foundation.py` - `tests/unit/test_wealth_cockpit_v1.py` ## Workspace status - No files, database rows, services or timers were modified. - One local inspection issue: the deployment worktree had no `.venv/bin/python`; I used system Python against an ephemeral in-memory migrated database to verify the exact schema-51 table definitions.