## Outcome - Existing automation is **one CLI orchestrator behind one user-systemd timer**—there is no worker framework, in-process scheduler, or FinanceManager cron job. - No files were created or modified; repository remained clean on `sprint20b/performance-activation-daily-valuations-v1` at `7f1d76b`. ## Architecture and execution flow - Timer: `deploy/systemd/finance-manager-market-valuation.timer:4-9` - `Mon..Fri` at `23:30 Europe/Zurich` - `Persistent=true`, five-minute accuracy, up to two-minute random delay. - Service: `deploy/systemd/finance-manager-market-valuation.service:6-19` - One-shot process in `/home/agent/.hermes/worktrees/FinanceManager-main-deploy` - Executes: ```bash python -m jarvis_finance.cli.main run-daily-market-valuation --fx-provider frankfurter ``` - Writes are confined to the external runtime directory. - CLI dispatch: `src/jarvis_finance/cli/main.py:120-122,146-168` - Loads settings, opens runtime SQLite DB, runs migrations, selects Frankfurter/Twelve Data FX, and calls `run_daily_market_valuation()`. - Core orchestrator: `src/jarvis_finance/services/portfolio_analytics.py:395-760` 1. Converts weekends to the prior business date. 2. Acquires an `fcntl` lock (`:369-380`). 3. Loads confirmed canonical stock/equity/ETF positions (`:206-285`). 4. Resolves exactly one confirmed provider mapping per instrument (`:288-307`). 5. Uses mapped FMP/Twelve Data/Finnhub/Massive/EODHD/YFinance provider; failed configured providers get a capability-checked YFinance historical fallback (`:437-526`; providers in `market_data/prices.py:75-396`). 6. Fetches CHF FX via Frankfurter by default, looking back up to eight calendar days (`portfolio_analytics.py:116-129,528-558`; `fx/providers.py:31-80`). 7. Computes instrument/account valuations and optional benchmark. 8. Stores run status, immutable analysis, and one audit event. A user-triggered equity batch refresh can also invoke the same valuation function after complete quote coverage: `services/market_service.py:333-347`, exposed by `api/routers/market.py:31-41`. This is not a separate scheduler. ## Tables written and operational evidence The job writes: - `market_prices` - `fx_rates` - `portfolio_valuation_snapshots`—instrument and account scopes - `benchmark_snapshots` - `portfolio_analysis_snapshots` - `market_data_runs` - `audit_log` - Potential market-data alerts/correction audits through `store_market_price()` and `upsert_fx_rate()`. Definitions and immutability controls are in `storage/migrations.py:1741-1784,2085-2178`. Installed user units exactly match the repository unit checksums. No user crontab or matching `/etc/cron*` entries exist. Runtime history showed: - Successful complete runs on July 27 and 28. - July 28: 22/22 price writes, 2/2 FX writes, 23 valuations. - July 29–31 failed before valuation inside `apply_migrations()` with a budget Phase-18 foreign-key error. - Current unit state remains `failed`; timer is enabled/waiting. Because no production write/run was authorized, I did not test whether the current FK-safe code fixes the next invocation. ## Idempotency, audit, and status gaps Existing controls: - Lock prevents concurrent local runs. - Complete-run replay key is `(source_key, effective date, input fingerprint)`; deterministic run ID (`portfolio_analytics.py:357-366,412-428`). - Market prices and FX use natural-key upserts. - Successful completion records coverage, missing instruments, reason codes, and a system audit event (`:713-759`). - Complete replay is tested at `tests/unit/test_portfolio_market_analytics_v1.py:172-217`. Material gaps: 1. **Not a canonical household valuation job.** It refreshes only stock/equity/ETF positions. It does not refresh or create canonical True Wealth totals, crypto portfolio valuations, or bank balances. 2. **Cash is carried from the latest snapshot**, regardless of requested valuation date, via `_latest_cash_entries()` (`portfolio_analytics.py:763-788`). It lacks `balance_date <= as_of` and confirmation/source filtering, creating backfill/future-leakage risk. 3. **Fingerprint omits cash and other valuation inputs.** A same-day complete run remains idempotent after a cash snapshot or provider-mapping correction, potentially preserving a stale account total. 4. **Partial retries are weak.** They reuse the run ID but are not idempotent (`test_sprint9_fx_fmp_history.py:161-186`). A partially materialized run may collide with immutable `INSERT OR REPLACE` analysis/valuation rows. 5. **Unhandled failures leave no application-level failed status/audit.** Exceptions can leave `market_data_runs.status='running'`; only systemd/journal reports the failure. 6. **CLI treats `partial` as success** (`cli/main.py:166`), so systemd cannot distinguish full coverage from degraded output. 7. `unavailable` analysis is persisted as run status `partial` (`portfolio_analytics.py:725-731,755`). 8. Weekday-only scheduling cannot provide 24/7 crypto valuation semantics. The canonical cockpit intentionally keeps source contracts separate: cash snapshots, PostFinance performance valuations, True Wealth official totals, and crypto prices (`wealth_cockpit.py:159-172,197-313,316-365`). Sprint 20B’s source gaps are documented in `docs/finance-manager-2.0/sprint20-performance-data-activation.md:34-56`. ## Smallest Sprint 20B extension Extend the **existing** `run-daily-market-valuation` orchestrator and systemd timer rather than introducing another scheduler: - Add source-specific valuation stages for: - confirmed bank cash snapshots, - PostFinance equity/cash account valuations, - official True Wealth totals, - crypto portfolio totals. - Materialize one canonical account snapshot per source/date into the existing `portfolio_valuation_snapshots`. - Include every selected source snapshot/version plus mapping/policy inputs in the run fingerprint. - Enforce source-date cutoffs and exact-date/no-carry-forward semantics. - Add explicit `failed` finalization/audit and safe resumability for partial runs. - Keep manual/import activation on the established Preview → Confirm → Audit path in `services/portfolio_data.py:1232-1344`; automation should consume confirmed records only. - If daily crypto is required, change the same timer to daily and let each source stage apply its own market-calendar semantics—still one orchestrator and one scheduler.