## Ergebnis - Repository geprüft: `/home/agent/.hermes/worktrees/FinanceManager-main-deploy` - Commit stimmt: `2bede53eb921d7a752d9ce745b2b5a9971b4c99c` - Working Tree unverändert/sauber. - Keine produktiven Datenbanken, Finanzwerte oder Secrets geöffnet. - **Hauptfazit:** Die Bausteine für Quotes, FX, CHF-Bewertung und TWR/MWR existieren bereits. Sprint 8 sollte diese erweitern und **keine zweite Market-/Performance-Engine** aufbauen. Es fehlen insbesondere Daily-Job-Orchestrierung/Locking, ein kanonischer täglicher Portfolio-Snapshot, Benchmark-Zeitreihen/-Vergleich und Risk v1. ## 1. Market-Quote-Provider ### Equity/ETF **Datei:** `src/jarvis_finance/market_data/prices.py` Vorhandene Klassen/Funktionen: - `EquityPriceProvider` – Provider-Protokoll - `EquityPriceQuote` - `MockEquityPriceProvider` - `FmpEquityPriceProvider` - `TwelveDataEquityPriceProvider` - `FinnhubEquityPriceProvider` - `MassiveEquityPriceProvider` - `EodhdEquityPriceProvider` - `CompositeEquityPriceProvider` - Reihenfolge: FMP → Twelve Data → Finnhub → Massive - EODHD ist bewusst nicht im Standard-Composite. - `equity_price_provider_by_name()` - `store_market_price()` - `refresh_market_prices()` Persistenz erfolgt in `market_prices`, eindeutig über: ```text (instrument_id, price_date, provider) ``` **Zusätzlicher Chart-Provider:** `src/jarvis_finance/services/market_service.py` - `_yfinance_history()` - `get_equity_candles()` `yfinance` ist aktuell nur für explizit abgerufene/cached Candles vorgesehen, nicht als kanonischer EOD-Quote-Provider. ### Crypto **Dateien:** - `src/jarvis_finance/market/providers.py` - `MarketDataProvider` - `BatchMarketDataProvider` - `CoinGeckoClient` - `store_crypto_price()` - `refresh_crypto_prices()` - `src/jarvis_finance/services/market_service.py` - `BinanceClient` - `refresh_crypto_quote()` - `refresh_crypto_quotes_batch()` - `get_crypto_quote()` ### Cache-/Zeitreihen-Infrastruktur **Datei:** `src/jarvis_finance/market_data/cache.py` - `upsert_equity_price_point()` - `upsert_crypto_price_point()` - `upsert_equity_intraday_candles()` - `backfill_equity_points_from_market_prices()` - `backfill_crypto_points_from_crypto_prices()` - `get_equity_chart_points()` - `get_crypto_chart_points()` ### Wiederverwendung - Für tägliche Equity/ETF-Schlusskurse direkt `refresh_market_prices()` + `store_market_price()` verwenden. - Für Crypto `refresh_crypto_prices()` bzw. bestehende CoinGecko-Schnittstelle verwenden. - `market_prices` als kanonische tägliche Instrument-Quote-Tabelle behalten. - `equity_price_points`/`crypto_price_points` nur als Chart-/Cache-Projektion betrachten, nicht als zweite Snapshot-Quelle. - Provider-Fehler- und Quality-Semantik (`fresh`, `missing`, `stale`, Alerts) übernehmen. ## 2. FX, insbesondere Frankfurter ### Provider **Datei:** `src/jarvis_finance/fx/providers.py` - `HistoricalFxUnavailable` - `MockFxProvider` - `FrankfurterFxProvider` - `name = "frankfurter"` - `get_rate()` - unterstützt `latest` und historisches Datum - behandelt Rate Limit und Netzwerk-/Providerfehler - `TwelveDataFxProvider` - `fx_provider_by_name()` ### Auflösung und Persistenz **Datei:** `src/jarvis_finance/fx/rates.py` - `FxProvider` - `FxResolutionResult` - `upsert_fx_rate()` - `upsert_fx_unavailable()` - `latest_fx_rate()` - `get_fx_rate_to_chf()` - `resolve_fx_rate_to_chf()` - Reihenfolge: **Frankfurter → lokaler Cache → Fallback-Provider** - CHF wird deterministisch als `1`/`not_needed` behandelt. - `update_fx_rates()` - `recheck_transaction_fx_status()` **Manuelle Ausnahme:** `src/jarvis_finance/fx/overrides.py` - `set_manual_fx_override()` ### CHF-Bewertung **Datei:** `src/jarvis_finance/ledger/positions.py` - `_latest_market_price()` - `calculate_positions()` - liest lokale `market_prices` - verwendet `latest_fx_rate(..., quote_currency="CHF")` - berechnet `market_value_chf` - fail-closed bei fehlendem FX/Preis - `save_position_snapshots()` **Wichtige Einschränkung:** `calculate_positions()` ist nicht rein read-only: Es erzeugt Alerts und ruft `commit()` auf. Für einen deterministischen Daily-Snapshot-Job sollte die Bewertungsprojektion entweder von diesen Side Effects getrennt oder bewusst in einem kontrollierten Job-Kontext ausgeführt werden. ### Wiederverwendung - Frankfurter unverändert als Primärquelle verwenden. - `update_fx_rates()` für tägliche CHF-Paare verwenden. - Keine neue FX-Tabelle oder neue FX-Auflösungslogik schaffen. - Bestehende `latest_fx_rate()`-As-of-Semantik für CHF-Bewertungen wiederverwenden. ## 3. Snapshot-Tabellen und Migrationen Das Projekt verwendet kein ORM-Modell; Modelle sind SQLite-DDL, Dataclasses und Pydantic-Schemas. ### Bereits vorhandene Tabellen **Initialschema:** `src/jarvis_finance/storage/schema.py` - `market_prices` - `fx_rates` - `crypto_prices` - `positions_snapshot` - `cash_balances` **Kompatibilitätsmigrationen:** `src/jarvis_finance/storage/migrations.py` Aktueller Marker: - `MIGRATION_VERSION = 41` - `MIGRATION_NAME = "041_portfolio_data_ingestion_reconciliation_v1"` Relevante Migration Functions: - `_create_market_quote_chart_tables()` - `equity_price_points` - `equity_intraday_candles` - `crypto_price_points` - `_create_account_value_snapshot_tables()` - `account_value_snapshots` - `_create_cash_account_snapshot_tables()` - `cash_account_snapshots` - `_create_portfolio_performance_tables()` - `portfolio_valuation_snapshots` - immutable Update-/Delete-Trigger - versioniert über `snapshot_version` und `supersedes_snapshot_id` ### Snapshot-Writers **Datei:** `src/jarvis_finance/services/portfolio_data.py` - `_insert_valuation()` - `_insert_postfinance_record()` - schreibt abhängig vom Record in: - `portfolio_valuation_snapshots` - `positions_snapshot` - `cash_account_snapshots` - `account_value_snapshots` **Datei:** `src/jarvis_finance/ledger/positions.py` - `save_position_snapshots()` - schreibt `positions_snapshot` per `INSERT OR REPLACE` - auditiert die Speicherung ### Bewertung der Tabellen für Sprint 8 - `market_prices`: geeignet als Daily Instrument Quote Store. - `fx_rates`: geeignet als Daily FX Store. - `portfolio_valuation_snapshots`: beste Basis für reproduzierbare Portfolio-/Account-Performance; immutable und versioniert. - `positions_snapshot`: enthält detaillierte CHF-Positionswerte, ist aber **nicht immutable** und wird per `INSERT OR REPLACE` überschrieben. - `account_value_snapshots`: Legacy/manuelle Totalwerte; nicht als neuer Sprint-8-Kern ausbauen. - `crypto_prices`: append-orientiert, aber ohne eindeutige tägliche `(asset, currency, date, provider)`-Identität. ### Lücke Es gibt keinen kanonischen Daily-Snapshot-Run, der: 1. Quotes und FX mit einem gemeinsamen `as_of` erfasst, 2. alle Positionen in CHF bewertet, 3. Account-/Portfolio-Totale nach `portfolio_valuation_snapshots` schreibt, 4. Coverage/Quality und Input-Fingerprint festhält, 5. Wiederholungen desselben Tages idempotent/versioniert behandelt. ## 4. Scheduler, Jobs und Locking ### Vorhanden **CLI:** `src/jarvis_finance/cli/main.py` Kommandos: - `update-crypto-prices` - `update-fx-rates` - `update-market-prices` - `update-equity-prices` - `update-equity-quotes` - `update-crypto-live-stats` - `update-market-charts` Die CLI ist der sinnvollste Einstiegspunkt für einen Sprint-8-Daily-Job. ### Nicht vorhanden Es wurden keine produktiven Implementierungen gefunden für: - APScheduler/Celery/cron wrapper - systemd `.service`/`.timer` - Job-Run-Tabelle - Job-Leases oder Lock-Owner - `flock`/Lockfile - Heartbeat/Retry-State - Daily-Run-Status oder Coverage-Manifest `README.md` bezeichnet Provider-Aufrufe zwar als Backend/CLI/Scheduler-Verantwortung, implementiert aber keinen Scheduler. ### Bestehende Transaktionssperren - `src/jarvis_finance/services/portfolio_policy.py::confirm_policy()` nutzt `BEGIN IMMEDIATE`. - `src/jarvis_finance/services/portfolio_data.py::confirm_ingestion()` nutzt `BEGIN IMMEDIATE`. - `src/jarvis_finance/services/transfer_pairing.py` nutzt ebenfalls `BEGIN IMMEDIATE`. Diese schützen einzelne Schreib-Workflows, sind aber kein Job-Lock. **DB-Verbindung:** `src/jarvis_finance/storage/database.py::connect()` - `check_same_thread=False` - Foreign Keys an - kein explizites `busy_timeout` - kein WAL-/Job-Lock-Setup ### Empfehlung Einen kleinen Orchestrator über den bestehenden CLI-/Service-Funktionen bauen, nicht einen zweiten Provider-Pipeline-Stack. Er benötigt mindestens: - single-writer Lock oder DB-Lease, - eindeutigen Job-Key pro Business Date, - Run-Status/Start/End/Quality, - idempotente Retry-Semantik, - atomare Veröffentlichung des fertigen Portfolio-Snapshots, - keine Provider-Aufrufe in GET- oder UI-Render-Pfaden. ## 5. TWR/MWR und Performance ### Pure Engine **Datei:** `src/jarvis_finance/ledger/performance.py` - `ENGINE_VERSION = "portfolio_performance_v1"` - `COST_BASIS_VERSION = "fifo_v1"` - `Activity` - `Valuation` - `Quality` - `effective_activities()` - `external_cashflow()` - `twr_v1()` - `xirr_v1()` - `stable_input_fingerprint()` - `combined_quality()` Eigenschaften: - Decimal-basiert - keine Live-Aufrufe - keine erfundenen Preise/FX - TWR benötigt Cashflow-Bewertung an exakten Subperiodengrenzen - MWR nutzt XIRR und lehnt Mehrfachlösungen fail-closed ab ### Service **Datei:** `src/jarvis_finance/services/portfolio_performance.py` - `_load_activities()` - `_load_valuations()` - `_aggregate_account_valuations()` - `_lot_summary()` - `build_portfolio_performance()` Liest primär versionierte `portfolio_valuation_snapshots`, mit read-only Legacy-Fallback auf `account_value_snapshots`. ### API - `src/jarvis_finance/api/routers/overview.py::portfolio_performance()` - GET `/api/portfolio/performance` - `src/jarvis_finance/api/schemas/portfolio_performance.py::PortfolioPerformanceResponse` ### UI - `frontend/src/api/portfolio.ts::getPortfolioPerformance()` - `frontend/src/components/performance/PortfolioPerformancePanel.vue` - eingebunden in `frontend/src/pages/PortfolioPage.vue` ### Wiederverwendung Risk v1 und Benchmarkvergleich sollten dieselbe gespeicherte Portfolio-Zeitreihe und denselben `data_cutoff`/`input_fingerprint` verwenden. Keine separate Performance-Pipeline aufbauen. ## 6. Versionierte Policy und Benchmark ### Persistenz und Versionierung **Datei:** `src/jarvis_finance/storage/migrations.py::_create_portfolio_policy_tables()` `portfolio_policies` enthält: - `version` - `is_active` - `effective_from` - `previous_policy_id` - `base_currency` - `benchmarks_json` - `request_fingerprint` - `confirmation_id` - `payload_hash` - `audit_id` Immutable Content-/No-Delete-Trigger sind vorhanden. ### Service und Schema **Dateien:** - `src/jarvis_finance/services/portfolio_policy.py` - `_canonical_payload()` - `_validate()` - `preview_policy()` - `confirm_policy()` - `active_policy()` - `policy_detail()` - `policy_history()` - `evaluate_policy()` - `src/jarvis_finance/api/schemas/portfolio_policy.py` - `PolicyPreviewRequest.benchmarks` - `PortfolioPolicyResponse.benchmarks` Der aktuelle Benchmark-Vertrag akzeptiert Objekte mit ausschließlich: ```json {"reference": "..."} ``` `weight` wird ausdrücklich abgelehnt. ### UI - `frontend/src/api/portfolio.ts::PortfolioPolicyDraft` - `frontend/src/components/policy/PortfolioPolicyManager.vue` - historische Details zeigen Benchmarks an - der aktuelle Editor besitzt aber **kein sichtbares Benchmark-Eingabefeld** - eingebunden in `frontend/src/pages/PortfolioPage.vue` ### Lücken für „one policy benchmark“ - `benchmarks` ist eine unbeschränkte Liste; es gibt kein `max_length=1`. - Keine explizite Benchmark-ID, Quote-Währung, Provider-Symbol oder Mapping-Version. - Keine Verbindung zwischen Policy-`reference` und `market_prices`. - Kein Benchmark-Return, Relative Return oder Quality Contract. - Benchmark fehlt in `PolicyEvaluationResponse`. - UI kann vorhandene Benchmarks anzeigen/kopieren, aber nicht regulär erfassen. - Instrumentfeld `instruments.benchmark` ist ein separates, nicht policy-versioniertes Metadatum und sollte nicht als Policy-Benchmark-Quelle missbraucht werden. ## 7. Deterministic Risk v1 Es wurde keine quantitative Risk Engine gefunden: - kein `risk_v1` - keine Volatilität - kein Drawdown - kein Sharpe/Sortino - kein VaR - kein Tracking Error `src/jarvis_finance/services/portfolio_advisor.py` enthält lediglich regelbasierte Risk-Faktoren/Signale; das ist kein reproduzierbarer Portfolio-Risk-Contract. ### Empfehlung Risk v1 als reine Berechnung auf der vorhandenen `PortfolioPerformance.time_series` aufbauen, vorzugsweise nahe: - `src/jarvis_finance/ledger/performance.py` - `src/jarvis_finance/services/portfolio_performance.py` Mit expliziter Version, Quality-/Reason-Codes, identischem `data_cutoff` und identischem Snapshot-Input. Benchmarkrisiko nur berechnen, wenn Benchmark-Zeitreihe und Datumsausrichtung vollständig sind; sonst `null/unavailable`, niemals `0`. ## 8. Wahrscheinliche fokussierte Tests ### Bestehende Backend-Tests zum Erweitern - `tests/unit/test_fx_frankfurter_primary_hotfix.py` - `tests/unit/test_fx_market_data_phase_c.py` - `tests/unit/test_provider_integration_v2.py` - `tests/unit/test_market_quotes_detail_charts_v1.py` - `tests/unit/test_crypto_price_update.py` - `tests/unit/test_coingecko_provider.py` - `tests/unit/test_portfolio_performance_foundation.py` - `tests/unit/test_portfolio_policy_foundation.py` - `tests/unit/test_portfolio_data_ingestion_reconciliation.py` - `tests/unit/test_reconciliation_snapshot_foundation.py` - `tests/unit/test_schema.py` - `tests/unit/test_postfinance_portfolio_source.py` ### Bestehende Frontend-Tests - `frontend/src/components/performance/PortfolioPerformancePanel.test.ts` - `frontend/src/components/policy/PortfolioPolicyManager.test.ts` - `frontend/src/pages/PortfolioPage.test.ts` - `frontend/src/components/portfolio-data/DataIngestionReconciliationPanel.test.ts` ### Sinnvolle neue fokussierte Tests - `tests/unit/test_daily_market_fx_snapshot_v1.py` - idempotenter Tageslauf - Frankfurter-first - Retry/Lock - unvollständige Quote-/FX-Coverage - `tests/unit/test_portfolio_daily_chf_valuation_v1.py` - Preis × Menge × gespeicherter FX - CHF identity - fehlender Preis/FX bleibt `null` - keine Future Quotes - `tests/unit/test_policy_benchmark_v1.py` - exakt null oder ein Benchmark - Policy-Version bindet Benchmark unveränderlich - Mapping-/Quality-Fälle - `tests/unit/test_portfolio_risk_v1.py` - synthetische konstante/fallende/volatile Reihen - Drawdown-/Volatilitätskontrollen - deterministischer Fingerprint - fehlende/zu kurze Serie fail-closed - `frontend/src/components/performance/PortfolioPerformancePanel.test.ts` - Benchmark/Risk-Anzeige und unavailable-Zustände - `frontend/src/components/policy/PortfolioPolicyManager.test.ts` - genau ein Benchmark-Eingabefeld und versionierte Detailanzeige ## Dateien/Probleme - **Erstellt oder geändert:** keine. - **Tests ausgeführt:** keine; Auftrag war reine Code-Inventur. - **Issues:** keine Zugriffsprobleme. Wesentliche technische Lücke ist die fehlende Scheduler-/Lock-/Run-Infrastruktur und nicht die Provider- oder Performance-Basis.