# Phase 3 – Sprint 4 / 4.1 / 5: Policy- und Portfolio-Performance-Foundation

## Fachlicher Zweck

Sprint 4 führt eine versionierte Portfolio-Policy als kontrollierte, lokale Entscheidungsgrundlage ein. Sprint 4.1 härtet den Vertrag für request-basierte Idempotenz, unveränderliche historische Versionen, Decimal-Validierung, minimale Audits und eine read-only Detailansicht. Die Policy löst keine Orders, Rebalancings, Anlageempfehlungen oder Performanceberechnungen aus.

## Daten- und API-Vertrag

- Preview ist vollständig read-only und liefert einen inhaltsgebundenen `preview_id` sowie einen neuen serverseitigen `confirmation_id`.
- Confirm verlangt exakt den dargestellten Policy-Payload, `preview_id`, `confirmation_id` und `confirm=true`.
- Gleiche `confirmation_id` plus gleicher kanonischer Payload ist idempotent und liefert dieselbe Policy-Version.
- Gleiche `confirmation_id` plus abweichender Preview oder Payload wird fail-closed abgelehnt.
- Eine neue `confirmation_id` erzeugt auch bei früher bereits bestätigtem Inhalt eine neue Version; A → B → A ergibt Version 1 → 2 → 3 mit korrekter Vorgängerkette.
- Es ist genau eine Version aktiv. Bestätigte Policy-Inhalte und Allokationen bleiben unveränderlich.
- `GET /api/portfolio/policy/{policy_id}` liefert aktive und historische Versionen mit ihren eigenen Allokationen. Unbekannte IDs ergeben 404 ohne ID-Leak. Der GET-Pfad verändert keine Policy-, Allokations-, Audit- oder Zeitstempeldaten.
- Das Confirm-Audit enthält als Detail-Allowlist ausschließlich `version` und `previous_policy_id`; `policy_id` bleibt `entity_id`.
- Geld- und Prozentwerte werden Decimal-basiert geprüft. Binäre Floats, negative Werte, nicht-endliche Werte, Prozentwerte über 100, ungültige Bandgrenzen, Allokationssummen ungleich 100, Crypto-Limitverletzungen und nicht unterstützte Benchmark-Gewichte werden fail-closed abgelehnt.

## Frontend-Vertrag

- Das Frontend übernimmt beide Kennungen unverändert aus der Preview und bestätigt eine unveränderliche Kopie des dargestellten Payloads.
- Eine fachliche Änderung nach Preview invalidiert Preview und Bestätigungskennung; vor Confirm ist eine neue Preview erforderlich.
- Während Confirm sind Mehrfachauslösung und Doppelklick gesperrt. Ein technisch wiederholter identischer Request bleibt serverseitig idempotent.
- Aktive und historische Versionen sind über dieselbe read-only Detailansicht erreichbar.
- Die Detailansicht zeigt vollständige Policy- und Allokationswerte und ist eindeutig als `Nur lesen · Schreibgeschützt` markiert. Sie enthält keine Speicher-, Aktivierungs-, Bearbeitungs-, Kopier-, Order- oder Rebalancing-Aktion.
- Konflikt-, Validierungs-, 404- und allgemeine Ladefehler werden auf Deutsch dargestellt; Kennungen, Hashes und Audit-IDs erscheinen nicht in Benutzermeldungen oder Browserlogs.

## Migration

Die additive Kompatibilitätsmigration ergänzt `portfolio_policies` um nullable `confirmation_id` und `payload_hash`, einen partiellen Unique-Index auf vorhandenen Bestätigungskennungen und einen Trigger gegen nachträgliche Änderungen der Request-Identität. Bestehende Sprint-4-Zeilen bleiben ohne Backfill lesbar. Die Migration ist sowohl auf einer leeren Datenbank als auch auf einem synthetischen Sprint-4-Bestand getestet.

## Sicherheitsgrenzen

- Runtime-Daten und UAT-Daten liegen außerhalb des Repositories; Tests und UAT verwenden ausschließlich synthetische Daten.
- Die bestehende lokale GET-Konvention und die vorhandene Write-Security bleiben unverändert. Im deaktivierten Write-Modus werden entfernte Schreibzugriffe mit 403 abgelehnt; Sprint 4.1 führt keine zweite Auth-/API-Konvention ein.
- Preview persistiert nichts. Confirm bleibt Preview → explizites Confirm → Audit.
- Historische Details sind read-only. Es existiert kein Reaktivierungs- oder Schreibpfad.
- Policy-Evaluation bleibt `not_assessable`, solange aktuelle Werte nicht vollständig klassifiziert und vergleichbar sind. Keine FX-, Performance- oder Empfehlungsaussage.

## Validation und Abschlussgates

- Vollständige Python-Suite: **618 passed**.
- Vollständige Frontend-Suite: **42 Testdateien / 145 Tests passed**.
- Nach dem UAT-Fix fokussierte Policy-/Portfolio-Frontendtests: **4 Testdateien / 17 Tests passed**.
- Frontend-Typecheck: grün.
- Production Build: grün, 414 Module transformiert; bestehende nicht blockierende Chunk-Warnung >500 kB.
- Ruff, Python Compile und `git diff --check`: grün.
- Migration-, OpenAPI-, Detail-GET-, Idempotenz-, Audit-, Auth-/Write-Security- und Git-Safety-Tests: grün.

## Responsive-UAT

UAT erfolgte am 2026-07-23 gegen eine isolierte synthetische SQLite-Runtime mit lokalem FastAPI-/Vue-Stack:

- Desktop: Preview → Änderung → erzwungene neue Preview → Confirm; synchrone Doppelbestätigung erzeugte nur eine Version und einen Audit-Eintrag.
- Anschließend wurde eine zweite Version bestätigt; Version 2 war aktiv, Version 1 archiviert, genau eine Version aktiv.
- Aktive und historische Details wurden geöffnet. Beide zeigten vollständige versionsrichtige Werte; die historische Ansicht war eindeutig schreibgeschützt und enthielt als einzige Aktion `Ansicht schliessen`.
- iPad quer, 1024×768: keine horizontale Seitenüberbreite, keine Überlappung; History und Detailbuttons erreichbar.
- Mobile, 390×844: keine horizontale Seitenüberbreite; History und Detailansicht bedienbar. Ein im Abschlussreview erkannter zu enger Allokationstabellenkopf wurde durch mobile Allokationskarten ersetzt und erneut visuell geprüft.
- Deutsche Fehlerzustände wurden für veraltete Preview, Confirm-Konflikt, Detail-404 und allgemeine Ladefehler geprüft.

Synthetischer UAT-Endstand: 2 Policy-Versionen, 1 aktive Version, 2 Policy-Audits und 8 versionsgebundene Allokationszeilen.

## Abschließender Sol-Review

Genau ein read-only Abschlussreview des gesamten Sprint-Diffs wurde nach den vollständigen Suites durchgeführt. Ergebnis:

- keine Critical- oder High-Findings;
- ein berechtigtes responsives Finding: die vier Spalten der read-only Allokationstabelle waren bei 390 Pixeln zwar scrollbar, aber visuell zu eng;
- Korrektur innerhalb des Sprint-Scopes: mobile Allokationskarten, Desktop/iPad behalten die Tabelle;
- fokussierte Frontendtests, Typecheck, Production Build und Mobile-UAT danach erneut grün;
- größere Folgearbeiten wurden nicht in Sprint 4.1 gezogen.

## Rollback-Hinweis

Code-Rollback erfolgt durch Revert des Sprint-4.1-Commits und erneutes Ausrollen des vorherigen Builds. Die additiven Spalten, der Index und die Trigger sollen nicht automatisch gedroppt werden; bestehende historische Policies bleiben absichtlich erhalten. Vor einem Schema-Rückbau ist ein Runtime-Backup erforderlich. Der Sprint-4-Code kann nullable Zusatzspalten ignorieren, besitzt aber nicht den gehärteten request-basierten Idempotenzvertrag; deshalb sind während eines Rollbacks Policy-Schreibzugriffe vorzugsweise zu deaktivieren. Ein fachlicher Rollback einer bestätigten Policy erfolgt nicht durch Mutation alter Zeilen, sondern ausschließlich durch eine neue bestätigte Version.

---

# Sprint 5 – Portfolio Performance Foundation

## Ergebnis und Scope

Sprint 5 ergänzt eine ausschließlich lesende, reproduzierbare Portfolio-Performance-Sicht. Sie berechnet keine Livewerte, Schätzungen, Benchmarks, Empfehlungen, Rebalancings oder Orders. Berechnungsgrundlage sind die bestehenden normalisierten `transactions` sowie additive, unveränderliche Bewertungssnapshots. Damit entsteht keine zweite Transaktionsbuchhaltung.

## Kanonischer Activity-Vertrag

- `transactions` bleibt das kanonische Ledger. Additive Felder klassifizieren `external_deposit`, `external_withdrawal`, `internal_transfer`, `buy`, `sell`, `dividend`, `fee`, `tax` und `reversal` und erhalten Buchungs-/Ereigniszeitpunkt, Basiswährung, Transfergruppe, Gegenbuchungsreferenz und Source-Referenz.
- Alte normalisierte Typen werden deterministisch auf diesen Vertrag abgebildet. Die vorhandene Import-Idempotenz über externe Referenz beziehungsweise Row-Hash bleibt bestehen; neue Provider-Adapter sind nicht Teil dieses Sprints.
- Geld, Menge, Preis, Gebühr, Steuer, FX und Rendite werden im Backend als `Decimal` verarbeitet. Binäre Floats werden von den Performance-Primitiven fail-closed abgelehnt.
- Nur Ein- und Auszahlungen über die Portfoliogrenze sind externe Cashflows. Interne Transfers, Trades, Dividenden, Gebühren und Steuern sind keine externen Einzahlungen.
- Eine Gegenbuchung neutralisiert Original und Reversal für die Berechnung, beide bleiben jedoch Bestandteil des reproduzierbaren Input-Fingerprints. Nicht unterstützte beziehungsweise alte still voided Activities werden ausgeschlossen und mit `unsupported_activity` ausgewiesen.

## Cost Basis und Lots

- Version 1 ist `fifo_v1`. Die vorhandene WAC-Projektion bleibt für bestehende Legacy-Positionsansichten kompatibel; Sprint 5 berechnet seine Cost-Basis-Projektion aus demselben Transaktionsledger und erzeugt kein zweites Ledger.
- Kauf-Cost-Basis = Bruttokaufwert + direkt zurechenbare Kaufgebühren + Steuern.
- Nettoverkaufserlös = Bruttoerlös − Verkaufsgebühren − Steuern. Verkaufsmengen verbrauchen FIFO-Lots; Lot- und Result-Lineage verweist auf die Ursprungs-Activities.
- Kontrollfall: 10 × CHF 100 + CHF 5 Gebühr = CHF 1’005 Cost Basis; Verkauf von 4 × CHF 120 − CHF 2 Gebühr − CHF 3 Steuer = CHF 475 Nettoerlös; zugeordnet CHF 402; realisiert CHF 73; verbleibend CHF 603.
- Verkäufe über mehrere Lots sind kontrollgerechnet. Short-Positionen, Derivate, Optionen und komplexe Corporate Actions ergeben `unsupported_activity` statt improvisierter Werte.
- Totalwertkonten können TWR/MWR aus Kontowert-Snapshots und externen Cashflows liefern. Ohne Einzelpositionen entstehen keine erfundenen Lots; Cost Basis und P&L bleiben dann `null` mit `missing_cost_basis`.

## Bewertungs-, FX- und Reproduzierbarkeitsvertrag

- Migration 40 führt `portfolio_valuation_snapshots` mit Account-/Instrument-Scope, Originalwert, Währung, Basiswährung, expliziter Richtung `original_to_base`, FX-Kurs, Bewertungs- und Erfassungszeitpunkt, Quelle, Version, Supersedes-Referenz, Quality und Reason-Codes ein.
- Snapshotzeilen sind durch Trigger unveränderlich und nicht löschbar. Korrekturen sind neue Versionen; die Engine wählt bis zum expliziten `data_cutoff` die höchste verfügbare Version.
- Bestehende, nicht nachträglich mutierte `account_value_snapshots` werden read-only als versionierte Legacy-Totalwertquelle angebunden; kanonische Sprint-5-Snapshots haben für denselben Konto-/Bewertungstag Vorrang. Legacy-Zeilen mit `updated_at` werden nicht als historisch reproduzierbar verwendet.
- Basiswährungsgleiche Werte verwenden die mathematische Identität; Fremdwährungen benötigen einen gespeicherten FX-Kurs. Es gibt keinen Ersatzkurs 1.0 und keinen Live-Fetch.
- Engine-Version `portfolio_performance_v1`, Cost-Basis-Version `fifo_v1`, Parameter, alle verwendeten Snapshotversionen, Quellen, Quality-Metadaten sowie ursprüngliche und neutralisierte Activities fließen in einen kanonischen SHA-256-Input-Fingerprint ein.
- Gleicher Zeitraum, Scope, Data-Cutoff, Engine-Version und Input liefern deterministisch denselben Fingerprint und dasselbe Ergebnis. Historische Wiederholung mit festem Cutoff ignoriert später erfasste Snapshotversionen.

## TWR- und MWR-Vertrag

- `twr_v1` trennt externe Cashflows, verlangt eine Bewertung an jedem Cashflow-Teilperiodenrand und verknüpft die Teilperioden geometrisch. Ein Cashflow ohne passenden Bewertungsrand erzeugt `missing_cashflow_valuation`; es wird keine Modified-Dietz-Schätzung verwendet.
- Kontrollfall: CHF 10’000 → +10 %, danach CHF 5’000 externe Einzahlung → +10 %, Endwert CHF 17’600 ergibt exakt TWR `0.21` beziehungsweise 21 %.
- `xirr_v1` nutzt Investor-Vorzeichen: Anfangswert und externe Einzahlungen negativ, Auszahlungen und Endbewertung positiv. Tage werden mit Actual/365 annualisiert.
- Der deterministische Decimal-Bisection-Solver verwendet eine feste Suchdomäne, maximal 256 Iterationen und Toleranz `1E-12`. Ungeeignete Vorzeichenstruktur, fehlende Konvergenz und mehr als ein möglicher Vorzeichenwechsel liefern kein MWR-Ergebnis.
- Gebühren und Steuern werden separat summiert und wirken über die gespeicherten Netto-Portfoliowerte auf die Performance; sie werden nie als externe Cashflows umklassifiziert.

## Quality-Vertrag

- Status: `complete`, `partial`, `unavailable`. Jede Kennzahl besitzt eine eigene Quality für TWR, MWR, Cost Basis und P&L; `overall` bezieht sich auf die angeforderte Renditemethode.
- Reason-Codes: `missing_opening_valuation`, `missing_closing_valuation`, `missing_cashflow_valuation`, `missing_price`, `missing_fx`, `missing_cost_basis`, `stale_valuation`, `invalid_valuation`, `invalid_valuation_metadata`, `unsupported_activity`, `insufficient_cashflows`, `mwr_not_converged`, `mwr_multiple_solutions`, `invalid_opening_valuation`, `invalid_subperiod_capital` und `method_not_requested`.
- Nicht berechenbare Werte sind JSON `null`, niemals 0. Eine fehlende Cost Basis sperrt eine anderweitig korrekt berechenbare TWR/MWR nicht. Stale, aber gespeicherte Werte können eine `partial`-Kennzahl liefern; fehlende Bewertung oder FX führt bei betroffenen Renditen zu `unavailable`.
- Benutzermeldungen zeigen deutsche Ursachen, Datenabdeckung und Stichtag, aber keine Activity-, Account-, Snapshot- oder Audit-IDs.

## API und Frontend

- `GET /api/portfolio/performance` akzeptiert `from`, `to`, `method=twr|mwr|both`, optional `account_id`, `base_currency` und `data_cutoff`. Ungültige Perioden, Methoden und Basiswährungen werden fail-closed mit deutschen 400-Fehlern abgelehnt.
- Die Antwort enthält Zeitraum, Portfolio-/Account-Scope, Basiswährung, Bewertungsstichtag, Data-Cutoff, Quellen, Engine-/Cost-Basis-Version, Fingerprint, getrennte Quality, TWR/MWR, Anfang/Ende, externe Cashflows, Gebühren, Steuern, Zeitreihe und belastbare Cost-Basis-/P&L-Aggregate.
- Der Endpunkt ist nur GET und führt keine Datenänderung, Order, Empfehlung oder Rebalancing-Aktion aus. Vorhandene Auth-/Write-Security bleibt unverändert.
- Die Portfolio-Seite bietet Zeitraum-Presets, benutzerdefinierte Daten und TWR/MWR-Auswahl; zeigt Renditen, Werte, Cashflows, Kosten, P&L, Linie, Quellen, Stichtage und Reproduzierbarkeitsmetadaten. Empty-/Fehlerzustände enthalten keine Demowerte.

## Migration, Tests und UAT

- Additive Migration auf leerer DB: Version 40, grün.
- Upgrade eines synthetischen Sprint-4.1-Bestands von Version 39 auf 40: Plattform-, Konto- und Transaktionszeilen unverändert erhalten; grün.
- Vollständige Backend-Suite nach Anpassung der kanonischen Schema-Versionsassertions: **636 passed**.
- Vollständige Frontend-Suite: **43 Testdateien / 149 Tests passed**.
- Fokussierte Performance-/Security-/Importer-/Legacy-Positionsprüfungen: grün; Performance-Foundation vor Review **18 passed**, nach Review-Erweiterung **22 Tests** innerhalb eines betroffenen Gates mit insgesamt **31 passed**.
- Typecheck, Production Build, Ruff und Python Compile: grün. Build: 416 Module; bestehende nicht blockierende Chunk-Warnung >500 kB.
- Responsive-UAT mit isolierter synthetischer SQLite-Runtime: Kontrollportfolio zeigte TWR 21 %, externe Einzahlung CHF 5’000, Gebühren CHF 7, Steuern CHF 3, realisiert CHF 73, nicht realisiert CHF 117 und verbleibende Cost Basis CHF 603. Fremdwährungsfall zeigte 22,22 % aus EUR-Werten und gespeicherten FX-Snapshots. Totalwertkonto zeigte 10 % ohne erfundene Lots. Fehlender FX lieferte `Nicht berechenbar` und keine Renditezahl.
- Desktop, iPad quer und Mobile: Bedienelemente lesbar und nutzbar. Ein Mobile-Finding mit seitenweiter Überbreite durch bestehende Positionstabellen wurde durch `min-w-0`-Grid-Items und lokal horizontal scrollbare Tabellen behoben; fokussierte 21 Frontendtests, Typecheck und Build danach grün.

## Sicherheit, Rollback und bekannte Grenzen

- Tests, Gates und UAT verwenden ausschließlich synthetische Datenbanken unter `/tmp`; keine produktive DB, Importdatei, echte Position, Wallet oder Secret wurde geöffnet oder ausgegeben.
- Rollback: Code per Revert des Sprint-5-Commits. Additive Spalten und Snapshottabelle nicht automatisch droppen; sie sind für den Vorgänger lesbar ignorierbar. Vor einem bewussten Schema-Rückbau Runtime-Backup erstellen. Immutable Snapshots niemals mutierend „zurückrollen“.
- Bekannte Grenzen: keine neuen Provider-Adapter; bestehende Provider müssen normalisierte Activities und für nicht bereits angebundene Quellen versionierte Snapshots künftig explizit befüllen. Bestehende unveränderte Account-Totalwerte sind angebunden. Keine Short-/Derivate-/Corporate-Action-Berechnung, Benchmarks, Prognosen, Empfehlungen, Rebalancings, Orders oder Schweizer Steuerdeklaration. MWR lehnt mehrdeutige Cashflowstrukturen konservativ ab. Historische WAC-Positionsprojektionen bleiben kompatibel, während Performance-Cost-Basis explizit als `fifo_v1` versioniert ist.

## Einmaliges Sol-Abschlussreview

Das exakt einmal durchgeführte read-only Sol-Review über den vollständigen Sprint-Diff fand keine Critical-Findings. Berechtigte sprintrelevante Findings und Behandlung:

1. Realisierter FIFO-Gewinn war zunächst kumulativ statt periodenbezogen: auf Periodendifferenz bei weiterhin stichtagsbezogener Rest-Cost-Basis korrigiert und kontrollgetestet.
2. Bestehende reproduzierbare Account-Totalwertsnapshots waren noch nicht angebunden: read-only Legacy-Adapter mit Data-Cutoff, Source-Lineage und kanonischem Snapshot-Vorrang ergänzt.
3. Zeitzonenoffsets an TWR-Grenzen wurden entfernt statt nach UTC normalisiert: UTC-Normalisierung und Offset-Kontrolltest ergänzt.
4. OpenAPI beschrieb den Zeitraum zunächst nur als generisches Dictionary: striktes `PerformancePeriod`-Schema mit den Properties `from` und `to` ergänzt.
5. Snapshot-Constraints und Defensive Parsing für negative Werte, nicht positive FX-Kurse und ungültige Quality-JSON waren unvollständig: additive CREATE-Constraints, fail-closed Quality und Tests ergänzt.
6. Cost-Basis-/P&L-Quality-Ursachen waren in der UI nur indirekt sichtbar: getrennte Badges und deutsche Reason-Erklärungen ergänzt.
7. Ein lokaler, nicht versionierbarer `node_modules`-Symlink wurde aus dem Worktree entfernt.

Post-Review wurden ausschließlich die betroffenen Gates wiederholt: Backend **31 passed**, Ruff und Python Compile grün; Frontend **5 Testdateien / 21 Tests**, Typecheck und Production Build grün. Nice-to-have bleibt die spätere direkte Provider-Befüllung der kanonischen Snapshottabelle; dies erweitert keine fachliche Berechnung und ist nicht Teil von Sprint 5.
