# 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.

# Sprint 6 – Portfolio Data Ingestion & Reconciliation

## Provenienz und Bestandsaudit

- Basis ist ausschließlich Sprint-5-Commit `480d7a1b72a950e864c4bb021d3f58af8f8c15f9`; Zielbranch `sprint6/portfolio-data-ingestion-reconciliation`.
- `transactions` bleibt das kanonische Ledger. `portfolio_valuation_snapshots` bleibt der immutable Sprint-5-Bewertungsvertrag; `account_value_snapshots` ist Legacy-Totalwertquelle. `positions_snapshot`, `cash_account_snapshots`, `market_prices` und `fx_rates` werden read-only für Reconciliation verwendet.
- Bestehende Importer besitzen `import_sessions`, deterministische `row_hash`-Idempotenz und bereits bestätigte Transaktionen beziehungsweise normalisierte Snapshots. Sprint 6 liest keine Rohdatei und fügt keinen Provider hinzu.
- Preview/Confirm übernimmt die globale lokale Write-Security. App-Start, Deployment und GET-Endpunkte führen weder Import noch Backfill aus.

## Quellenmatrix und erlaubte Projektion

| Quelle | Normalisierte Eingabe | Unterstützung | Externe Identität / Zeit / Währung | Abdeckung und Lücke | Erlaubtes Ziel |
|---|---|---|---|---|---|
| `transactions` | kanonische Ledgerzeilen | Kauf, Verkauf, Dividende, Gebühr, Steuer, Transfer | `row_hash`, externe Referenz; Trade-/Eventzeit; Original-/Basiswährung | Activities; unklare Instrumente und nicht unterstützte Actions werden nur ausgewiesen | read-only Activity-Lineage, keine zweite Buchhaltung |
| `account_value_snapshots` | Account-Totalwerte | Totalwertkonten einschließlich TrueWealth-artiger Konten | Snapshot-ID intern gehasht; Bewertungsdatum; CHF-Total | keine erfundenen Holdings, keine Einzel-Cost-Basis | versionierter `portfolio_valuation_snapshot` |
| `cash_account_snapshots` | normalisierte Cash-Snapshots | Cash-Totalwert | Snapshot-ID intern gehasht; Balance-Datum; Original/CHF | kein Preis-/Holding-Ersatz | versionierter `portfolio_valuation_snapshot` |
| `positions_snapshot` | gemeldete/gespeicherte Menge und CHF-Wert | Mengen- und Positionswertabgleich | interner Snapshot; Stichtag | keine neue Activity, keine automatische Korrektur | read-only Reconciliation |
| `market_prices` | normalisierte Schlusskurse | Preisbewertung | Instrument/Datum/Provider | fehlender oder stichtagsfremder Preis bleibt Lücke | read-only Reconciliation |
| `fx_rates` | normalisierte FX-Kurse | Fremdwährungsumrechnung | Währungspaar/Datum/Provider | kein FX-1-Fallback außer CHF→CHF-Identität | read-only Reconciliation |
| `portfolio_valuation_snapshots` | kanonische, versionierte Bewertungen | Performance/Totalwertvergleich | Source-Lineage, Version, Capture-Time/Cutoff | immutable; Korrektur nur neue Version | read-only Vergleich und Performance |

Nicht unterstützt bleiben neue Bank-/Brokerquellen, Rohdatei-Upload, Scraping, externe Providerabrufe, Corporate Actions, Derivate, Shorts und automatische Korrekturbuchungen.

## Ingestion-, Idempotenz- und Auditvertrag

- Preview ist schreibfrei. Sie bindet Quelle, Portfolio-/Account-Scope, Zeitraum, eindeutigen UTC-Data-Cutoff, Ausgangsrevision, Input-Fingerprint, Preview-ID, zufällige Confirmation-ID, Ablaufzeit und Payload-Hash.
- Dispositionen: `new`, `unchanged`, `duplicate`, `ambiguous`, `blocked`, `versioned`. Nur `new` und `versioned` besitzen einen Write-Plan; blockierte oder unklare Zeilen werden nie bestätigt.
- Confirm nimmt exklusiv `BEGIN IMMEDIATE`, berechnet dieselbe Preview im Lock erneut, vergleicht Revision/Fingerprint/Preview/Payload und schreibt nur geplante kanonische Bewertungen. Identische Confirmation-ID ist idempotent; veränderte Wiederverwendung, Ablauf und Staleness werden abgelehnt.
- Snapshotkorrekturen erhalten die Historie und erzeugen `snapshot_version + 1` mit `supersedes_snapshot_id`. Bestätigte Batches, Items, ihr portfolio-spezifischer Audit-Eintrag und Sprint-5-Snapshots sind per Trigger immutable.
- API/UI/Audit enthalten nur gehashte Referenzen und aggregierte/sichere Zusammenfassungen, keine Rohpfade, Secrets, Tokens oder sensiblen Quelldateinamen.

## Scope-aware Transfers

- Sind beide Transferkonten im ausgewerteten Scope, bleibt die Gruppe interner Cashflow.
- Ist nur die empfangende Seite im Account-Scope, wird sie für diesen Scope externe Einzahlung; ist nur die sendende Seite enthalten, externe Auszahlung.
- Auf Portfolio-/Haushaltsscope bleiben gepaarte eigene Raiffeisen-/AKB-Konten intern. Der Transfer wird nicht zusätzlich als Einkommen und Ausgabe behandelt.

## Reconciliation, Toleranzen, Coverage und Quality

- Engine `portfolio_reconciliation_v1`, Toleranzvertrag `portfolio_reconciliation_tolerance_v1`; Geld/Mengen/Preise/FX und absolute/relative Grenzen werden als Decimal verarbeitet und als Strings transportiert.
- Mengenabgleich: Ledgermenge versus gespeicherter Positionssnapshot. Wertabgleich: Ledgermenge × gespeicherter Preis × expliziter FX versus gemeldeter CHF-Positionswert. Accountabgleich: gemeldete Positionswerte plus Cash versus Account-Totalwert. Totalwertkonten vergleichen kanonischen und Legacy-Totalwert ohne Holdings.
- Status `matched`, `within_tolerance`, `mismatch`, `not_comparable`, `unavailable` bleibt getrennt von Sprint-5-Quality `complete|partial|unavailable`.
- Reason-Codes: `missing_source_mapping`, `missing_position_snapshot`, `missing_account_valuation`, `missing_price`, `missing_fx`, `stale_snapshot`, `stale_price`, `stale_fx`, `cutoff_mismatch`, `duplicate_source_record`, `ambiguous_instrument`, `unsupported_activity`, `quantity_mismatch`, `value_mismatch`, `cash_gap`, `source_error`.
- Coverage wird für Accounts, Instrumente, Activities, frühere/abschließende Bewertungen, Preise, FX, Cost Basis und Reconciliation ausgegeben. Fehlende Daten sind `null`/`unavailable`, niemals Nullwert oder Match. Jeder Lauf enthält Stichtag, Cutoff, Quellen und stabilen Fingerprint.

## API und UI

- `GET /api/portfolio/data-sources`
- `POST /api/portfolio/ingestion/preview`
- `POST /api/portfolio/ingestion/confirm` (einziger neuer Ingestion-Schreibpfad)
- `GET /api/portfolio/ingestion/history` (paginiert)
- `GET /api/portfolio/reconciliation` (Detailpagination)
- Die Portfolio-Seite zeigt Quellenstatus, letzten Versuch/Confirm, Preview-Counts und Detailkarten, expliziten Confirm, immutable Historie, Coverage, Mengen-/Wert-/Cash-/FX-Differenzen und Totalwertkonten. Es existiert keine Korrektur-, Order-, Empfehlungs- oder Rebalancing-Aktion.

## Migration, Sicherheit, Verifikation, Review, Deployment und Rollback

- Additive Migration: Schema 40 → 41 mit `portfolio_ingestion_batches`, `portfolio_ingestion_items`, Indizes und Immutable-Triggern; kein automatischer Backfill.
- Test-/UAT-Schutz: `JARVIS_FINANCE_ENV=test|uat` akzeptiert DB und Runtime ausschließlich unter `/tmp`. `tests/conftest.py` setzt fail-closed isolierte Defaults und initialisiert nur diese Test-DB. CLI-Tests, die eine eigene Runtime wählen, setzen den zugehörigen DB-Pfad explizit. Ein Negativtest gegen den produktiven Pfad brach vor Collection ab; produktive Prüfsumme und Schema blieben unverändert.
- Sicherheitsvorfall während des Abschlussgates: Ein historischer API-Test ohne DB-Override öffnete die produktive Standard-DB und löste die additive Migration 41 vorzeitig aus. Die neuen Tabellen waren leer; bestehende Businessdaten waren unverändert. Nach expliziter Freigabe wurde Migration 41 einmalig kontrolliert entfernt und Schema 40 wiederhergestellt. Das konsistente Pre-Rollback-Backup bleibt unverändert erhalten und wird nicht zurückgespielt. Der Rollback wird nicht wiederholt.
- Produktiver Read-only-Nachweis nach Guard und Gates: Schema 40, `integrity_check=ok`, keine Schema-41-Restobjekte und unveränderte SHA-256 `7f1127131d4c7abb9ba4e6091d08aee9b5a6f225d7bfb417f02ee0256ef8dc52`.
- Migrationsprobe auf konsistenter Produktionskopie: Schema 40 → 41, `integrity_check=ok`, Business-Digest unverändert, alle neuen Ingestiontabellen leer.
- Backend-Abschlusslauf: **656 passed, 3 failed** ausschließlich wegen drei CLI-Tests, deren testlokaler Runtime-Pfad durch den neuen globalen Test-DB-Guard nicht mehr automatisch den DB-Pfad ersetzte. Nach explizitem DB-Override bestanden genau diese betroffenen Tests mit **3 passed**. Damit sind alle **659** Backendtests belegt; gemäß Gate-Regel wurde nach dem lokalisierten Harness-Fix nicht die gesamte Suite wiederholt.
- Frontend-Abschlusslauf: **44 Testdateien / 152 Tests**. Typecheck und Production Build bestanden; Build transformierte 418 Module, mit bestehender nicht blockierender Chunk-Warnung. Ruff bestand für alle fachlich geänderten Sprint-6-Dateien; Compileall bestand. Repository-weites Ruff bleibt wegen vorbestehender, sprintfremder Altlasten kein Gate.
- Synthetische UAT unter `/tmp`: API Health, Preview, Confirm, Historie und Reconciliation erfolgreich; Schema 41 und `integrity_check=ok`. Responsive-Artefakte wurden für Desktop `1440×1000`, iPad quer `1024×768` und Mobile `390×844` erzeugt und visuell geprüft.
- Das exakt einmal durchgeführte finale read-only Sol-Review meldete **2 High / 2 Medium**, keine Criticals. Behoben und kontrollgetestet: (1) eindeutige fortlaufende Snapshotversionen für mehrere Werte desselben Accounts/Stichtags in einem Batch; (2) stichtagsfremde Positionen, Preise und FX sind `not_comparable` und reduzieren Coverage; (3) Preview-Zeitpunkt ist kryptografisch an den Payload gebunden, während identische erfolgreiche Retries auch nach TTL idempotent bleiben; (4) unbestätigte Transaktionen sind aus Ingestion und Reconciliation ausgeschlossen. Es erfolgte kein zweites Vollreview.
- Rollback-Codeziel ist der verifizierte Sprint-5-Commit. Vor dem kontrollierten Deployment werden vorherige Revision und Runtime-DB erneut gesichert, die Migration erneut auf einer aktuellen DB-Kopie erprobt und Rollback-Bereitschaft verifiziert. Eine Wiederherstellung erfolgt nur bei tatsächlich notwendigem Datenrollback.
