## Ergebnis Bestehende Sprint-5-Performance-Foundation am Base-Commit `9f2102ae25407dec1d8800e976186a112aea7825` vollständig read-only inventarisiert. Der Worktree ist unverändert und sauber. ### Datei-/Zeilenkarte | Bereich | Datei / Zeilen | Bestehender Vertrag | |---|---|---| | Engine und Datenobjekte | `src/jarvis_finance/ledger/performance.py:16-88` | `portfolio_performance_v1`, `fifo_v1`, `Decimal`-basierte `Activity`, `Valuation`, `Quality`, `ReturnResult` | | Activity-Bereinigung | `ledger/performance.py:111-125` | Reversals neutralisieren Original und Gegenbuchung; unsupported/voided werden ausgeschlossen | | Externe Cashflows | `ledger/performance.py:128-141` | Nur `external_deposit` und `external_withdrawal`; Betrag aus `net`, sonst `gross`; Vorzeichen wird anhand des Kinds normalisiert | | `twr_v1` | `ledger/performance.py:144-175` | Geometrische Verknüpfung; Cashflow gilt direkt **nach** Bewertungsgrenze; jede Cashflow-Zeit braucht exakte Grenzbewertung; keine Dietz-Schätzung | | `xirr_v1` | `ledger/performance.py:178-233` | Actual/365, Decimal-XNPV, feste Suchdomäne und Bisection; mehrere Vorzeichenwechsel werden als `mwr_multiple_solutions` abgelehnt | | Fingerprint/Quality | `ledger/performance.py:236-249` | SHA-256 über kanonisches JSON; schlechtester Status plus vereinheitlichte Reason-Codes | | Activity-Klassifikation | `src/jarvis_finance/services/portfolio_performance.py:27-50` | Aliase für Ein-/Auszahlung, Transfer, Kauf/Verkauf, Dividende, Zins, Fee, Tax und Reversal | | Activity-Laden/Transfergrenze | `portfolio_performance.py:59-149` | `transactions` bleibt kanonisches Ledger; vollständig innerhalb Scope gepaarte Transfers bleiben intern; nur über Scope-Grenze führende gepaarte Transfers werden extern | | Bewertungsquellen | `portfolio_performance.py:152-276` | Primär `portfolio_valuation_snapshots`; höchste Version bis Cutoff. Fallback auf immutable/aktive `account_value_snapshots`; pro Konto/Tag hat kanonischer Snapshot Vorrang | | `performance_included` | `portfolio_performance.py:279-285` | Portfolio-Scope: `performance_included=1 AND is_active=1`; expliziter Account-Scope prüft nur `performance_included=1` | | Account-Aggregation | `portfolio_performance.py:288-324` | Accountwerte werden nur aufgenommen, wenn für denselben `valuation_at` alle erwarteten Accounts vorhanden sind | | FIFO/P&L | `portfolio_performance.py:333-400` | Wiederverwendung von `fifo_v1`; Instrumentbewertungen liefern Closing-Market-Value für unrealized P&L | | Orchestrierung/Antwort | `portfolio_performance.py:403-565` | TWR/MWR, Quality, Cashflows, P&L, Fingerprint, Zeitreihe, Attribution und Quellen | | HTTP-Endpunkt | `src/jarvis_finance/api/routers/overview.py:121-148` | Read-only `GET /api/portfolio/performance`; Parameter `from`, `to`, `method`, `account_id`, `base_currency`, `data_cutoff`; `ValueError` → HTTP 400 | | API-Schema | `src/jarvis_finance/api/schemas/portfolio_performance.py:8-105` | Strikte Quality-, Summary-, Cashflow-, Attribution-, Cost-Basis- und Response-Modelle | | Additive Migration | `src/jarvis_finance/storage/migrations.py:1686-1757` | Performance-Spalten in `transactions`; immutable und nicht löschbare versionierte `portfolio_valuation_snapshots` | | Legacy-Totalwerte | `storage/migrations.py:1521-1537` | `account_value_snapshots` | | Account-Default | `src/jarvis_finance/storage/schema.py:1` | `performance_included INTEGER NOT NULL DEFAULT 1` | | CSV-Importsemantik | `src/jarvis_finance/imports/accounts_importer.py:44-79` | Fehlendes `performance_included` wird als `True` importiert | | Allgemeine Valuation-Ingestion | `src/jarvis_finance/services/portfolio_data.py:1064-1080` | Schreibt kanonische Account-Snapshots mit Source `ingestion:` | | PostFinance-Baseline | `portfolio_data.py:1208-1228` | Kanonischer partieller Account-Snapshot mit `baseline_only` und `missing_transaction_history` | | Markt-/FX-Snapshots | `src/jarvis_finance/services/portfolio_analytics.py:591-628` | Schreibt Instrument- und Account-Snapshots aus gespeicherten Preisen, FX und Cash | | TrueWealth legacy | `src/jarvis_finance/services/truewealth_service.py:393-407,486-499` | Offizielle Totalwerte werden berücksichtigt; `truewealth_manual_provisional` wird im Performance-Fallback explizit ausgeschlossen | | PostFinance legacy | `src/jarvis_finance/services/postfinance_service.py:824-835` | Offizielle Account-Totalwerte als `postfinance_official_import` | | Manuelle Totalwerte | `src/jarvis_finance/services/manual_entry_service.py:1081-1104` | Legacy-Quelle `manual_total_value` | | Ursprünglicher Vertrag | `docs/finance-manager-2.0/sprint-4-portfolio-policy-foundation.md:79-148` | Sprint-5-Performancevertrag und bekannte Grenzen | ### Aktuelle Cashflow-Semantik - **Extern:** `external_deposit`, `deposit`, `cash_deposit`; analog Withdrawals. - **Nicht extern:** interne Transfers innerhalb des ausgewählten Scopes, Käufe/Verkäufe, Dividenden, Zinsen, Gebühren und Steuern. - Dividenden/Zinsen gehen nur in `attribution.dividends_and_interest`. - Fees/Taxes werden separat summiert; ihre Wirkung auf Rendite soll bereits im gespeicherten Portfoliowert enthalten sein. - Ungepaarte Transfers bleiben bewusst intern, weil keine belastbare Gegenkonto-Evidenz besteht. - Reversals neutralisieren Original und Reversal, bleiben aber im Fingerprint. ### Attribution: aktueller Null-/Reason-Pfad `portfolio_performance.py:553-562` liefert immer: - `market_price: null` - `fx: null` - `status: "partial"` - `reason_codes: ["market_fx_attribution_requires_position_level_history"]` Dividenden/Zinsen, Fees, Taxes und externe Cashflows sind lediglich nominelle Periodensummen. Es gibt derzeit: - keine Markt-/FX-Zerlegung, - keine Attribution-Quality in `quality`, - keine Reconciliation-Identität bzw. keinen Residual, - keine Attribution auf Account-/Instrumentebene, - keinen Test, der den bestehenden Null-/Reason-Vertrag explizit absichert. ## Wesentliche Contract-Gaps für Sprint 14 1. **Fehlender FX-Kurs eines externen Cashflows kann eine numerische TWR stehen lassen.** Der Cashflow wird ausgelassen (`portfolio_performance.py:450-456`), aber seine Quality wird nicht in `twr_result.quality` bzw. `mwr_result.quality` kombiniert. Nur `overall` wird nachträglich auf `partial` gesetzt (`:500-501`). Damit kann eine materiell falsche Rendite nicht-null erscheinen. 2. **`unsupported_activity` ist nicht kennzahlenspezifisch propagiert.** Dieselbe Entkopplung besteht für ausgeschlossene Activities; außerdem wird dadurch `net_external_cashflows` pauschal `null`, selbst wenn die unsupported Activity cashflow-fremd war (`:542`). 3. **Attribution ist nur ein statischer Platzhalter.** `partial` wird selbst bei vollständig fehlender Positionshistorie verwendet; `unavailable` und Datenabdeckung sind nicht definiert. Markt/FX bleiben unabhängig von vorhandenen Instrument-Snapshots immer `null`. 4. **Keine Attribution-Rechenkonvention.** Offen sind insbesondere Reihenfolge der Preis-/FX-Effekte, Behandlung intraperiodischer Trades und Cash, Residual, Dividenden-Brutto/Netto sowie Identität zur Portfolio-P&L bzw. Rendite. 5. **`xirr_v1` wird extern ausschließlich als `mwr` bezeichnet.** Response und Quality kennen `mwr`, aber keine explizite `xirr_v1`-Methodenversion. Auch `engine_version` ist im Pydantic-Vertrag nur freier String. 6. **Scope-Semantik ist inkonsistent.** Explizit gewähltes, inaktives Konto ist berechenbar, Portfolio-Scope schließt es aus (`:279-285`). `performance_included` ist zudem opt-out mit Default `1`; es gibt keine periodisierte Gültigkeit dieses Flags. 7. **Multi-Account-Bewertungen verlangen identische Text-Timestamps.** Aggregiert wird nach dem rohen `valuation_at` (`:292-316`), obwohl `twr_v1` Zeitzonen auf UTC normalisiert. Semantisch gleiche Zeitpunkte mit unterschiedlicher ISO-Darstellung können daher als unvollständig gelten. 8. **Kanonischer Snapshot verdrängt Legacy unabhängig von Verwendbarkeit.** Bereits das Vorhandensein eines kanonischen Account-Snapshots am Tag unterdrückt den Legacy-Fallback (`:220-250`), auch wenn der kanonische Snapshot später wegen Quality/FX nicht nutzbar ist. 9. **Cutoff-Vergleiche sind textuell.** `created_at <= ?` und `captured_at <= ?` vergleichen ISO-Timestamps lexikalisch; der Performance-Builder validiert nur den Datumspräfix. Unterschiedliche Offsetdarstellungen sind dadurch nicht vollständig kanonisiert. ## Kleinste nicht-duplizierende Erweiterungspunkte - **Keine neue Route, kein neues Ledger und keine neue Bewertungstabelle:** bestehendes `build_portfolio_performance()` und `PortfolioPerformanceResponse` erweitern. - `_load_activities()` und `_load_valuations()` weiterverwenden; sie liefern bereits Lineage, Originalwährung, gespeichertes FX, Versionen und Account-/Instrument-Scope. - Eine kleine pure `attribution_v1`-Primitive neben `twr_v1`/`xirr_v1` ergänzen; die Service-Schicht sollte nur normalisierte periodische Positionseingaben aufbauen. - Vorher einen gemeinsamen **Cashflow-Quality-Wert** einführen und in TWR/MWR kombinieren, statt die Gründe nur nachträglich auf `overall` zu setzen. - Bestehendes `attribution`-Objekt erweitern, statt parallele Summary-/Analytics-Verträge anzulegen; `portfolio_analytics.py` sollte Konsument bleiben, nicht zweite Performance-Engine werden. - Für Preis/FX-Zerlegung die vorhandenen Instrument-Snapshots verwenden; fehlen Opening/Closing-Historie oder rekonstruierbare Mengen, explizit `null + unavailable/partial + reason_codes`, niemals Account-Gesamtwert künstlich auf Positionen verteilen. - Tests im bestehenden `tests/unit/test_portfolio_performance_foundation.py` ergänzen; keine neue konkurrierende Test-Fixture-Infrastruktur nötig. ## Verifikation - Fokussierter Testlauf: **22 passed**, eine Starlette/httpx-Deprecation-Warnung. - Worktree anschließend geprüft: **keine erstellten oder geänderten Dateien**, `git status` sauber, `git diff --check` ohne Befund. - Keine produktive Datenbank, echten Finanzdaten oder Secrets geöffnet.