## Architektur- und Gap-Map Sprint 20E **Basis verifiziert:** `9667b08a14a04feccc2d8089b374af7eb4046d2b`, Branch `sprint20e/market-data-recovery-crypto-activation-v1`, Worktree sauber. **Analyse:** ausschließlich Repository-Code; keine produktiven Daten geöffnet, keine Handelsaktionen, keine Änderungen. ### 1. Bestehender Laufpfad ```text systemd timer deploy/systemd/finance-manager-market-valuation.timer täglich 23:30 Europe/Zurich ↓ systemd oneshot finance-manager-market-valuation.service JARVIS_FINANCE_DAILY_VALUATION_ENABLED=0 (fail-closed) ↓ CLI src/jarvis_finance/cli/main.py run-daily-market-valuation ↓ globales Gate JARVIS_FINANCE_DAILY_VALUATION_ENABLED == "1" ↓ Quellen sequenziell, jeweils neue DB-Verbindung run_isolated_daily_sources(...) ├─ postfinance → run_daily_market_valuation() ├─ crypto → run_daily_crypto_valuation() ├─ truewealth_bank_cashflows └─ truewealth_modelled_valuation ``` Quellengate: `run_activated_daily_source()` in `src/jarvis_finance/services/truewealth_valuation.py:691-704`. Es prüft einen früheren Audit-Eintrag `performance_source_activation_confirmed`. Damit gilt aktuell: Sobald das globale Gate aktiviert wird, laufen **alle früher aktivierten Quellen**. Es gibt kein Scheduler-Allowlisting nur für Krypto. ### 2. Krypto-Pfad `src/jarvis_finance/services/daily_valuations.py` 1. Exklusiver `flock` pro Kryptojob. 2. Ermittelt genau einen aktiven Account mit Rolle `crypto_portfolio`. 3. Verlangt vollständige `performance_cashflow_coverage`. 4. Rekonstruiert Mengen aus: - verifizierten `crypto_holdings` als Startbestand oder - ausschließlich bestätigten `crypto_transactions`. 5. Bei aktuellem Kalendertag: CoinGecko-Refresh nach CHF. 6. Für die Bewertung werden nur Preise verwendet, deren Provider-/Fetch-Zeit exakt auf den fachlichen Tag fällt. 7. Fingerprint bindet Account, Coverage, Mengen und ausgewählte Preis-IDs/-Metadaten. 8. Schreibt einen Account-Snapshot nach `portfolio_valuation_snapshots`. 9. Schließt `market_data_runs` und `audit_log` ab. Historische Läufe holen bewusst keine aktuellen Quotes, sondern benötigen bereits persistierte exakte Tagespreise. ### 3. Relevante Tabellen | Tabelle | Rolle | |---|---| | `crypto_assets` | Asset, `coingecko_id`, Providerpräferenzen, Aktivstatus | | `crypto_holdings` | bestätigter/verifizierter Startbestand | | `crypto_transactions` | bestätigte Kryptoaktivitäten bis Stichtag | | `crypto_prices` | Preis, Währung, Provider, Provider-Zeit, Fetch-Zeit, Qualität | | `performance_scope_classifications` | Aktivierung des `crypto_portfolio`-Accounts | | `performance_cashflow_coverage` | Trackingmodus und bestätigte Coverage | | `market_data_runs` | Run-ID, Source-Key, Datum, Fingerprint, Status und Zähler | | `portfolio_valuation_snapshots` | append-only, versionierte Tagesbewertungen | | `market_prices` | Equity-/ETF-Preise inklusive Run-Provenienz | | `fx_rates` | FX-Kurse inklusive Run-Provenienz | | `portfolio_analysis_snapshots` | Markt-/FX-Analyse | | `benchmark_snapshots` | Benchmarkwerte | | `audit_log` / `alerts` | Laufabschluss, Korrekturen und Qualitätswarnungen | ### 4. Provider - **Krypto:** `CoinGeckoClient`, `/simple/price`, direkt in CHF. - **Equity:** FMP, Yahoo/yfinance, Twelve Data, Finnhub, Massive; EODHD explizit auswählbar. - **FX:** Frankfurter oder Twelve Data; CHF→CHF als `identity`. ## Wesentliche Gaps ### Kritisch 1. **Keine source-spezifische Scheduler-Aktivierung** - Das globale Env-Gate aktiviert nicht nur Krypto. - PostFinance und beide True-Wealth-Worker bleiben im CLI-Lauf enthalten. - Frühere Aktivierungs-Audits genügen, damit diese Worker Provider-/Write-Pfade erreichen. - Erfüllt „nur Krypto aktiv; PostFinance und True Wealth deaktiviert“ nicht. 2. **Kein Dry-run für den Tagesjob** - `run-daily-market-valuation` besitzt kein `--dry-run`. - `run_daily_crypto_valuation()` ruft `refresh_crypto_prices(..., dry_run=False)` fest auf. - Ein Preview des kompletten Auswahl-, Preis-, Fingerprint- und Bewertungsplans ohne Writes fehlt. - Der separate `update-crypto-prices --dry-run` deckt die Tagesbewertung nicht ab. 3. **Keine atomare Transaktion** - `store_crypto_price()` committet pro Preis. - Danach committet der Kryptojob die `running`-Runzeile separat. - Snapshot, Audit und finaler Runstatus folgen erst später. - Fehler können daher persistierte Preise oder einen dauerhaft `running` bleibenden Run hinterlassen. - `run_isolated_daily_sources()` fängt Fehler, finalisiert aber keinen bereits angelegten Run als `failed`; Rollback kann interne Commits nicht zurücknehmen. 4. **Provider-ID nicht kanonisch und nicht validiert** - Standardwert/Quote verwendet `"CoinGecko"` statt eines stabilen kanonischen Keys wie `"coingecko"`. - `crypto_prices.provider` hat keine kanonische Validierung. - `_exact_prices()` akzeptiert jeden frischen Preis für `asset_id` und Datum; es validiert weder Provider noch `crypto_prices.coingecko_id` gegen `crypto_assets.coingecko_id`. - Dadurch kann ein falscher oder anders benannter Providerdatensatz in die kanonische Bewertung gelangen. ### Wichtig 5. **Preis-/FX-Provenienz im Tageswert nur indirekt** - Der Fingerprint enthält Preis-ID, Provider und Zeitstempel, ist aber opak. - Der Audit speichert nur Anzahl der Preisinputs, nicht die ausgewählten Preis-IDs/Provider-IDs. - Der Snapshot referenziert `run_id:fingerprint`, nicht die konkrete Inputliste. - Bei direktem CHF-Preis ist `fx_rate_to_base=1`, aber „kein FX / identity“ wird nicht ausdrücklich als Provenienz dokumentiert. - `crypto_prices` besitzt anders als `market_prices`/`fx_rates` kein `run_id`. 6. **„Ein Tageswert“ ist nicht durchgängig kanonisch** - Der Writer erzeugt bei Inputkorrektur bewusst eine neue `snapshot_version`; physisch können mehrere Werte pro Account/Tag bestehen. - Der Performance-Loader rankt korrekt nach höchster Version. - Aber `performance_hardening.py:823-830` summiert **alle** Account-Versionen je Tag. Nach einer Korrektur kann die Einrichtungsansicht den Tageswert doppelt zählen. - Es fehlt ein gemeinsam wiederverwendeter kanonischer „aktive Version pro Account und fachlichem Tag“-Reader. 7. **Idempotenz nur für vollständig abgeschlossene Runs** - `complete` wird korrekt wiederverwendet. - Ein identischer `partial`-Run wird erneut bearbeitet; die Snapshot-ID verhindert teilweise Duplikate, aber kein vollständiger Resume-/Finalisierungsvertrag existiert. - Providerquotes werden vor Erstellung des Run-Fingerprints persistiert. Damit ist der eigentliche externe Input nicht sauber an einen vorab definierten Run gebunden. 8. **Locking ist pro Quelle statt Orchestrator** - Kryptojob und Marktjob haben getrennte Lockdateien. - Das schützt parallele Aufrufe derselben Quelle, verhindert aber nicht zwei komplette CLI-Orchestratoren, die verschiedene Quellen gleichzeitig bearbeiten. - Für 20E reicht ein Crypto-Lock nur dann, wenn der Scheduler hart auf Krypto begrenzt wird. ## Minimaler Änderungsvorschlag ### `src/jarvis_finance/cli/main.py` - `run-daily-market-valuation` ergänzen um: - `--source crypto` mit fail-closed Allowlist; - `--dry-run`; - optional explizites Env-Gate `JARVIS_FINANCE_DAILY_CRYPTO_ENABLED=1`. - Scheduled-Modus für 20E ausschließlich mit dem Krypto-Worker aufbauen. - PostFinance und True Wealth nicht nur als „not activated“ melden, sondern gar nicht dispatchen. - Exitcode für `partial`/`failed` beibehalten. ### `deploy/systemd/finance-manager-market-valuation.service` - Expliziter Aufruf: `run-daily-market-valuation --source crypto`. - Krypto-Gate standardmäßig `0`; Globalaktivierung darf keine anderen Quellen implizit einschalten. - Keine Timeraktivierung im Sprint. ### `src/jarvis_finance/services/daily_valuations.py` - `dry_run: bool = False` ergänzen. - Read-only Planphase erstellen: - Scope/Coverage; - bestätigte Mengen; - kanonische Provider-ID und Provider-Asset-ID; - geplante Quote-/Bewertungswrites; - Fingerprint und Reason-Codes. - Dry-run darf keine Preise, Alerts, Runs, Audits oder Snapshots schreiben. - Apply innerhalb einer expliziten Transaktion: - Quotes zunächst in Memory; - Inputs validieren; - dann Preiszeilen, Run, Snapshot, Audit und finalen Status atomar schreiben. - Fehlerpfad muss entweder vollständig rollbacken oder einen bewusst separat und sicher finalisierten `failed`-Run erzeugen; niemals `running` zurücklassen. - Einen existierenden vollständigen Account-Tageswert idempotent wiederverwenden; Korrekturen weiterhin append-only, aber genau eine aktive Version bestimmen. ### `src/jarvis_finance/market/providers.py` - Kanonische Konstante, z. B. `COINGECKO_PROVIDER_ID = "coingecko"`. - `CoinGeckoClient.name`. - Quotes und Speicherung auf kanonischen Provider-Key normalisieren. - Vor Speicherung validieren: - angefragte ID; - zurückgelieferte `coingecko_id`; - Assetmapping; - CHF; - positiver Preis; - Provider-Zeit nicht in der Zukunft und am Zieltagesdatum. - Commit aus `store_crypto_price()` entfernen; Transaktionshoheit beim Orchestrator. - Optional `run_id` für `crypto_prices` nur dann als Migration ergänzen, wenn konkrete Provenienz nicht ausreichend im Audit/Run-Payload gespeichert werden kann. ### `src/jarvis_finance/services/portfolio_performance.py` und `performance_hardening.py` - Gemeinsame kanonische Abfrage/View-Helper: höchste `snapshot_version` je Account und fachlichem Kalendertag. - Den summierenden Krypto-Reader in `performance_hardening.py:823-830` auf diesen Helper umstellen. - Source-Precedence weiterhin explizit halten; keine Addition konkurrierender Account-Gesamtwerte. ### Schemaentscheidung Ein Schema-Upgrade ist **nicht zwingend**, wenn: - konkrete Preisinput-Provenienz strukturiert im bestehenden Audit gespeichert wird; - `source_reference` weiterhin Run/Fingerprint bindet; - aktive Tageswerte konsequent durch einen kanonischen Reader bestimmt werden. Eine kleine Migration wäre nur für ein echtes FK-artiges `crypto_prices.run_id` oder eine DB-seitig erzwungene kanonische Provider-ID nötig. ## Fokussierter Testplan 1. CLI ohne Krypto-Gate: Exit 0, `writes=0`, kein DB-Connect vor Gate. 2. `--source crypto --dry-run`: Provider darf aufgerufen werden, `total_changes == 0`; keine Alerts/Runs/Audits/Snapshots. 3. Globale Aktivierung plus frühere PostFinance-/True-Wealth-Audits: trotzdem ausschließlich Krypto-Worker. 4. Provider-ID `"CoinGecko"` wird kanonisch `"coingecko"`; unbekannte IDs fail-closed. 5. Quote mit abweichender CoinGecko-ID wird abgelehnt. 6. Audit enthält ausgewählte Preis-ID, Provider-ID, Provider-Zeit, Währung und explizite FX-Identity-Provenienz. 7. Fehler nach erstem Preisinput: keine Teilwrites und kein `running`-Run. 8. Wiederholung desselben vollständigen Laufs: gleiche Run-ID, null zusätzliche Writes. 9. Identischer partieller Lauf: deterministischer Resume-/Failed-Vertrag, keine Snapshotduplikate. 10. Zwei konkurrierende Prozesse: zweiter Lauf erhält `market_job_already_running`. 11. Preiskorrektur erzeugt append-only Version 2, aber alle Consumer liefern genau einen Tageswert. 12. Wochenende: Krypto nutzt den Kalendertag, PostFinance wird nicht aufgerufen. 13. Providerfehler: kein neuer bestätigter Tageswert; letzter bestätigter Wert bleibt unverändert. 14. Read-only Jobstatus/API erzeugen null Writes. ## Verifikation / Einschränkungen - Worktree blieb unverändert und sauber. - Fokustests konnten nicht ausgeführt werden: Im Worktree existiert keine `.venv`, und der aktive Python-Interpreter enthält kein `pytest`. - **Dateien erstellt oder geändert:** keine.