# Review guidelines:

You are acting as a reviewer for a proposed code change made by another engineer.

Below are some default guidelines for determining whether the original author would appreciate the issue being flagged.

These are not the final word in determining whether an issue is a bug. In many cases, you will encounter other, more specific guidelines. These may be present elsewhere in a developer message, a user message, a file, or even elsewhere in this system message.
Those guidelines should be considered to override these general instructions.

Here are the general guidelines for determining whether something is a bug and should be flagged.

1. It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
2. The bug is discrete and actionable (i.e. not a general issue with the codebase or a combination of multiple issues).
3. Fixing the bug does not demand a level of rigor that is not present in the rest of the codebase (e.g. one doesn't need very detailed comments and input validation in a repository of one-off scripts in personal projects)
4. The bug was introduced in the commit (pre-existing bugs should not be flagged).
5. The author of the original PR would likely fix the issue if they were made aware of it.
6. The bug does not rely on unstated assumptions about the codebase or author's intent.
7. It is not enough to speculate that a change may disrupt another part of the codebase, to be considered a bug, one must identify the other parts of the code that are provably affected.
8. The bug is clearly not just an intentional change by the original author.

When flagging a bug, you will also provide an accompanying comment. Once again, these guidelines are not the final word on how to construct a comment -- defer to any subsequent guidelines that you encounter.

1. The comment should be clear about why the issue is a bug.
2. The comment should appropriately communicate the severity of the issue. It should not claim that an issue is more severe than it actually is.
3. The comment should be brief. The body should be at most 1 paragraph. It should not introduce line breaks within the natural language flow unless it is necessary for the code fragment.
4. The comment should not include any chunks of code longer than 3 lines. Any code chunks should be wrapped in markdown inline code tags or a code block.
5. The comment should clearly and explicitly communicate the scenarios, environments, or inputs that are necessary for the bug to arise. The comment should immediately indicate that the issue's severity depends on these factors.
6. The comment's tone should be matter-of-fact and not accusatory or overly positive. It should read as a helpful AI assistant suggestion without sounding too much like a human reviewer.
7. The comment should be written such that the original author can immediately grasp the idea without close reading.
8. The comment should avoid excessive flattery and comments that are not helpful to the original author. The comment should avoid phrasing like "Great job ...", "Thanks for ...".

Below are some more detailed guidelines that you should apply to this specific review.

HOW MANY FINDINGS TO RETURN:

Output all findings that the original author would fix if they knew about it. If there is no finding that a person would definitely love to see and fix, prefer outputting no findings. Do not stop at the first qualifying finding. Continue until you've listed every qualifying finding.

GUIDELINES:

- Ignore trivial style unless it obscures meaning or violates documented standards.
- Use one comment per distinct issue (or a multi-line range if necessary).
- Use ```suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block).
- In every ```suggestion block, preserve the exact leading whitespace of the replaced lines (spaces vs tabs, number of spaces).
- Do NOT introduce or remove outer indentation levels unless that is the actual fix.

The comments will be presented in the code review as inline comments. You should avoid providing unnecessary location details in the comment body. Always keep the line range as short as possible for interpreting the issue. Avoid ranges longer than 5–10 lines; instead, choose the most suitable subrange that pinpoints the problem.

At the beginning of the finding title, tag the bug with priority level. For example "[P1] Un-padding slices along wrong tensor dimensions". [P0] – Drop everything to fix.  Blocking release, operations, or major usage. Only use for universal issues that do not depend on any assumptions about the inputs. · [P1] – Urgent. Should be addressed in the next cycle · [P2] – Normal. To be fixed eventually · [P3] – Low. Nice to have.

Additionally, include a numeric priority field in the JSON output for each finding: set "priority" to 0 for P0, 1 for P1, 2 for P2, or 3 for P3. If a priority cannot be determined, omit the field or use null.

At the end of your findings, output an "overall correctness" verdict of whether or not the patch should be considered "correct".
Correct implies that existing code and tests will not break, and the patch is free of bugs and other blocking issues.
Ignore non-blocking issues such as style, formatting, typos, documentation, and other nits.

FORMATTING GUIDELINES:
The finding description should be one paragraph.

OUTPUT FORMAT:

## Output schema  — MUST MATCH *exactly*

```json
{
  "findings": [
    {
      "title": "<≤ 80 chars, imperative>",
      "body": "<valid Markdown explaining *why* this is a problem; cite files/lines/functions>",
      "confidence_score": <float 0.0-1.0>,
      "priority": <int 0-3, optional>,
      "code_location": {
        "absolute_file_path": "<file path>",
        "line_range": {"start": <int>, "end": <int>}
      }
    }
  ],
  "overall_correctness": "patch is correct" | "patch is incorrect",
  "overall_explanation": "<1-3 sentence explanation justifying the overall_correctness verdict>",
  "overall_confidence_score": <float 0.0-1.0>
}
```

* **Do not** wrap the JSON in markdown fences or extra prose.
* The code_location field is required and must include absolute_file_path and line_range.
* Line ranges must be as short as possible for interpreting the issue (avoid ranges over 5–10 lines; pick the most suitable subrange).
* The code_location should overlap with the diff.
* Do not generate a PR fix.


<permissions instructions>
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is restricted.
Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected.
 The writable roots are `/home/agent/.hermes/worktrees/FinanceManager-hotfix-sprint13-global-portfolio-aggregation`, `/tmp`.
</permissions instructions>

<apps_instructions>
## Apps (Connectors)
Apps (Connectors) can be explicitly triggered in user messages in the format `[$app-name](app://{connector_id})`. Apps can also be implicitly triggered as long as the context suggests usage of available apps.
An app is equivalent to a set of MCP tools within the `codex_apps` MCP.
An installed app's MCP tools are either provided to you already, or can be lazy-loaded through the `tool_search` tool. If `tool_search` is available, the apps that are searchable by `tools_search` will be listed by it.
Do not additionally call list_mcp_resources or list_mcp_resource_templates for apps.
</apps_instructions>

<skills_instructions>
## Skills
A skill is a set of local instructions to follow that is stored in a `SKILL.md` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.
### Available skills
- imagegen: Generate or edit raster images when the task benefits from AI-created bitmap visuals such as photos, illustrations, textures, sprites, mockups, or transparent-background cutouts. Use when Codex should create a brand-new image, transform an existing image, or derive visual variants from references, and the output should be a bitmap asset rather than repo-native code or vector. Do not use when the task is better handled by editing existing SVG/vector/code-native assets, extending an established icon or logo system, or building the visual directly in HTML/CSS/canvas. (file: /home/agent/.codex/skills/.system/imagegen/SKILL.md)
- openai-docs: Use when the user asks how to build with OpenAI products or APIs, asks about Codex itself or choosing Codex surfaces, needs up-to-date official documentation with citations, help choosing the latest model for a use case, or model upgrade and prompt-upgrade guidance; use OpenAI docs MCP tools for non-Codex docs questions, use the Codex manual helper first for broad Codex self-knowledge, and restrict fallback browsing to official OpenAI domains. (file: /home/agent/.codex/skills/.system/openai-docs/SKILL.md)
- plugin-creator: Create and scaffold plugin directories for Codex with a required `.codex-plugin/plugin.json`, optional plugin folders/files, valid manifest defaults, and personal-marketplace entries by default. Use when Codex needs to create a new personal plugin, add optional plugin structure, generate or update marketplace entries for plugin ordering and availability metadata, or update an existing local plugin during development with the CLI-driven cachebuster and reinstall flow. (file: /home/agent/.codex/skills/.system/plugin-creator/SKILL.md)
- skill-creator: Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Codex's capabilities with specialized knowledge, workflows, or tool integrations. (file: /home/agent/.codex/skills/.system/skill-creator/SKILL.md)
- skill-installer: Install Codex skills into $CODEX_HOME/skills from a curated list or a GitHub repo path. Use when a user asks to list installable skills, install a curated skill, or install a skill from another repo (including private repos). (file: /home/agent/.codex/skills/.system/skill-installer/SKILL.md)
### How to use skills
- Discovery: The list above is the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
- Trigger rules: If the user names a skill (with `$SkillName` or plain text) OR the task clearly matches a skill's description shown above, you must use that skill for that turn. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
- How to use a skill (progressive disclosure):
  1) After deciding to use a skill, the main agent must open and read its `SKILL.md` completely before taking task actions. If a read is truncated or paginated, continue until EOF.
  2) When `SKILL.md` references relative paths (e.g., `scripts/foo.py`), resolve them relative to the skill directory listed above first, and only consider other paths if needed.
  3) If `SKILL.md` points to extra folders such as `references/`, use its routing instructions to identify the files required for the task. The main agent must read each required instruction or reference file itself before acting on it. Do not delegate reading, summarizing, or interpreting skill instructions to a subagent. Subagents may still perform task work when the selected skill allows it.
  4) If `scripts/` exist, prefer running or patching them instead of retyping large code blocks.
  5) If `assets/` or templates exist, reuse them instead of recreating from scratch.
- Coordination and sequencing:
  - If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
  - Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
- Context hygiene:
  - Progressive disclosure applies to selecting relevant files, not partially reading a selected instruction file. Do not load unrelated references, scripts, or assets.
  - Avoid deep reference-chasing: prefer opening only files directly linked from `SKILL.md` unless you're blocked.
  - When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.
</skills_instructions>

<plugins_instructions>
## Plugins
A plugin is a local bundle of skills, MCP servers, and apps. Below is the list of plugins that are enabled and available in this session.
### Available plugins
- `Data Analytics`: Answer product and business questions with data
- `GitHub`: Inspect repositories, triage pull requests and issues, debug CI, and publish changes through a hybrid GitHub connector and CLI workflow.
- `Gmail`: Work with Gmail using the configured Gmail app connector.
- `Google Calendar`: Connect Google Calendar for scheduling, availability, daily briefs, and event management.
- `Google Drive`: Use Google Drive as the single entrypoint for Drive, Docs, Sheets, and Slides work.
- `Investment Banking`: Investment banking workflows for M&A, coverage, sponsors, capital markets, LevFin, restructuring, valuation, diligence, pitch materials, and deal execution.
- `Lovable`: Build apps and websites
- `Default templates`: OpenAI default document, presentation, and spreadsheet templates.
- `Public Equity Investing`: Public equity investing workflows for listed-company research, earnings analysis, valuation, model updates, long/short pitches, catalysts, thesis tracking, ETF/index and constituent diligence, sell-side research notes, risk sizing, hedging, dashboards, and investment memos.
### How to use plugins
- Discovery: The list above is the plugins available in this session.
- Skill naming: If a plugin contributes skills, those skill entries are prefixed with `plugin_name:` in the Skills list.
- Trigger rules: If the user explicitly names a plugin, prefer capabilities associated with that plugin for that turn.
- Relationship to capabilities: Plugins are not invoked directly. Use their underlying skills, MCP tools, and app tools to help solve the task.
- Preference: When a relevant plugin is available, prefer using capabilities associated with that plugin over standalone capabilities that provide similar functionality.
- Missing/blocked: If the user requests a plugin that is not listed above, or the plugin does not have relevant callable capabilities for the task, say so briefly and continue with the best fallback.
</plugins_instructions>


# AGENTS.md instructions for /home/agent/.hermes/worktrees/FinanceManager-hotfix-sprint13-global-portfolio-aggregation

<INSTRUCTIONS>
# AGENTS.md

## FinanceManager-2.0-Regeln

- Arbeite in kleinen vertikalen PRs; keine Big-Bang-Rewrites.
- Produktive Finanzdaten, Rohimporte, Datenbanken, Reports und Secrets bleiben außerhalb Git.
- Schreibrouten bleiben Preview → Confirm → Audit und HTTP-seitig fail-closed. Keine Orderausführung.
- Sol entscheidet über Architektur, Security, Auth, Datenmodell, Migrationen, Formeln und Advisor-/IPS-Semantik.
- Spark nur sequenziell für ein kleines Ergebnis (ca. drei Dateien), nie für Secrets, produktive Daten, Auth, Migrationen oder Finanzberechnungen; Sol prüft jeden Diff.
- Kein Push, Merge, Deployment und keine Secret-Rotation ohne ausdrückliche Freigabe.
- Vor Abschluss: `make PYTHON=.venv/bin/python verify` und `git diff --check` tatsächlich ausführen.
- Jeder PR erfüllt die Felder der PR-Vorlage und aktualisiert bei Entscheidungsänderungen die Baseline/ADR.

Details: `docs/finance-manager-2.0/sprint-0-baseline.md`.

</INSTRUCTIONS>

<environment_context>
  <cwd>/home/agent/.hermes/worktrees/FinanceManager-hotfix-sprint13-global-portfolio-aggregation</cwd>
  <shell>bash</shell>
  <current_date>2026-07-27</current_date>
  <timezone>Europe/Zurich</timezone>
  <filesystem><workspace_roots><root>/home/agent/.hermes/worktrees/FinanceManager-hotfix-sprint13-global-portfolio-aggregation</root></workspace_roots><permission_profile type="managed"><file_system type="restricted"><entry access="read"><special>:root</special></entry><entry access="write"><path>/home/agent/.hermes/worktrees/FinanceManager-hotfix-sprint13-global-portfolio-aggregation</path></entry><entry access="write"><special>:slash_tmp</special></entry><entry access="write"><special>:tmpdir</special></entry><entry access="read"><path>/home/agent/.hermes/worktrees/FinanceManager-hotfix-sprint13-global-portfolio-aggregation/.git</path></entry><entry access="read"><path>/home/agent/.hermes/worktrees/FinanceManager-hotfix-sprint13-global-portfolio-aggregation/.agents</path></entry><entry access="read"><path>/home/agent/.hermes/worktrees/FinanceManager-hotfix-sprint13-global-portfolio-aggregation/.codex</path></entry></file_system></permission_profile></filesystem>
</environment_context>


Summary: auto


Review the current code changes (staged, unstaged, and untracked files) and provide prioritized findings.


git status --short && git diff --stat && git diff --cached --stat


Output:
Chunk ID: bb0cae
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 295
Output:
 M src/jarvis_finance/dashboard/data.py
 M src/jarvis_finance/imports/postfinance_documents.py
 M src/jarvis_finance/ledger/cash.py
 M src/jarvis_finance/services/cash_service.py
 M src/jarvis_finance/services/equity_service.py
 M src/jarvis_finance/services/portfolio_analytics.py
 M src/jarvis_finance/services/portfolio_data.py
 M src/jarvis_finance/services/postfinance_service.py
 M tests/test_postfinance_sprint13.py
?? docs/sprint13-global-portfolio-aggregation-hotfix.md
?? src/jarvis_finance/services/portfolio_aggregation.py
 src/jarvis_finance/dashboard/data.py               | 122 +++++--
 .../imports/postfinance_documents.py               |  36 ++-
 src/jarvis_finance/ledger/cash.py                  |  28 +-
 src/jarvis_finance/services/cash_service.py        | 221 +++++++++++--
 src/jarvis_finance/services/equity_service.py      |  79 ++++-
 src/jarvis_finance/services/portfolio_analytics.py |  42 +++
 src/jarvis_finance/services/portfolio_data.py      |  60 +++-
 src/jarvis_finance/services/postfinance_service.py |  32 +-
 tests/test_postfinance_sprint13.py                 | 357 +++++++++++++++++++++
 9 files changed, 856 insertions(+), 121 deletions(-)


git diff -- src/jarvis_finance/services/portfolio_aggregation.py docs/sprint13-global-portfolio-aggregation-hotfix.md; git diff -- . ':(exclude)src/jarvis_finance/services/portfolio_aggregation.py' ':(exclude)docs/sprint13-global-portfolio-aggregation-hotfix.md'


Output:
Chunk ID: d173e3
Wall time: 0.0001 seconds
Process exited with code 0
Original token count: 17397
Output:
Total output lines: 1357

diff --git a/src/jarvis_finance/dashboard/data.py b/src/jarvis_finance/dashboard/data.py
index d619e72..b0d6491 100644
--- a/src/jarvis_finance/dashboard/data.py
+++ b/src/jarvis_finance/dashboard/data.py
@@ -1,15 +1,20 @@
 from __future__ import annotations

 import json
-from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
+from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
 from pathlib import Path
 from sqlite3 import Connection

 from jarvis_finance.config.settings import Settings
 from jarvis_finance.crypto.holdings import calculate_crypto_holdings
-from jarvis_finance.ledger.cash import calculate_cash_balances
-from jarvis_finance.ledger.positions import calculate_positions
+from jarvis_finance.ledger.positions import Position, calculate_positions
 from jarvis_finance.market.providers import is_price_stale
+from jarvis_finance.services.portfolio_aggregation import (
+    latest_audited_market_values,
+    latest_official_postfinance_positions,
+    post_snapshot_postfinance_quantity_deltas,
+    postfinance_account_roles,
+)

 MONEY = Decimal("0.01")

@@ -533,27 +538,41 @@ def get_platform_overview(conn: Connection) -> list[dict[str, str]]:

 def get_cash_overview(conn: Connection) -> list[dict[str, str]]:
-    result = calculate_cash_balances(conn)
-    rows: list[dict[str, str]] = []
-    for (account_id, currency), bal in sorted(result.balances.items()):
-        row = conn.execute(
-            "SELECT a.account_name, p.name AS platform FROM accounts a JOIN platforms p ON p.platform_id=a.platform_id WHERE a.account_id=?",
-            (account_id,),
-        ).fetchone()
-        rows.append({
-            "account_id": account_id,
-            "platform": row["platform"] if row else "",
-            "account_name": row["account_name"] if row else account_id,
-            "currency": currency,
-            "amount_original": dec_text(bal.amount_original, "0.01"),
-            "amount_chf": dec_text(bal.amount_chf, "0.01") if bal.amount_chf is not None else "",
-            "quality_status": bal.quality_status,
-        })
-    return rows
+    # One shared cash read model for the API, Command Center, allocation and reports.
+    # The local import avoids coupling the write-oriented cash service into module init.
+    from jarvis_finance.services.cash_service import list_cash_positions
+
+    return [
+        {
+            "account_id": position.account_id,
+            "platform": position.platform,
+            "account_name": position.account_label,
+            "currency": position.currency,
+            "amount_original": position.amount,
+            "amount_chf": position.used_value_chf,
+            "quality_status": "ok" if position.status != "Abgleich offen" else "partial",
+        }
+        for position in list_cash_positions(conn)
+    ]

 def get_positions(conn: Connection) -> list[dict[str, str]]:
     result = calculate_positions(conn, persist_alerts=False)
+    official = latest_official_postfinance_positions(conn)
+    post_snapshot_deltas = post_snapshot_postfinance_quantity_deltas(conn, official)
+    audited = latest_audited_market_values(conn)
+    roles = postfinance_account_roles(conn)
+    trading_cash_id = roles.get("etrading_cash")
+    official_instruments = {key[1] for key in official}
+    for key in list(result.positions):
+        if key[0] == trading_cash_id and key[1] in official_instruments:
+            # Instrument-bearing settlement events live on trading cash, while the
+            # authoritative holding belongs to the distinct depot role.
+            result.positions.pop(key)
+    for key, override in official.items():
+        result.positions.setdefault(
+            key, Position(account_id=override.account_id, instrument_id=override.instrument_id)
+        )
     rows: list[dict[str, str]] = []
     for (account_id, instrument_id), pos in sorted(result.positions.items()):
         row = conn.execute(
@@ -573,6 +592,51 @@ def get_positions(conn: Connection) -> list[dict[str, str]]:
             """,
             (instrument_id, account_id),
         ).fetchone()
+        override = official.get((account_id, instrument_id))
+        effective_quantity = (
+            override.quantity + post_snapshot_deltas.get(instrument_id, Decimal("0"))
+            if override
+            else pos.quantity
+        )
+        if override and effective_quantity <= 0:
+            continue
+        market_value = audited.get((account_id, instrument_id))
+        use_market_run = bool(
+            market_value
+            and (not override or market_value.as_of > override.snapshot_date)
+        )
+        if override and not use_market_run:
+            quantity = effective_quantity
+            market_price = override.market_price_original
+            market_value_chf = (
+                override.market_value_chf * effective_quantity / override.quantity
+                if override.quantity
+                else Decimal("0")
+            )
+            price_currency = override.price_currency
+            price_date = override.valuation_at
+            price_provider = "PostFinance official snapshot"
+            valuation_source = "postfinance_official_import"
+        elif use_market_run and market_value:
+            quantity = effective_quantity
+            market_price = market_value.close
+            market_value_chf = (
+                market_value.value_chf * effective_quantity / market_value.quantity
+                if market_value.quantity
+                else market_value.value_chf
+            )
+            price_currency = market_value.currency
+            price_date = market_value.as_of
+            price_provider = market_value.provider
+            valuation_source = "audited_market_run"
+        else:
+            quantity = pos.quantity
+            market_price = pos.market_price_original
+            market_value_chf = pos.market_value_chf
+            price_currency = row["latest_price_currency"] if row else ""
+            price_date = date_text(row["latest_price_date"] if row else "")
+            price_provider = row["latest_price_provider"] if row else ""
+            valuation_source = "canonical_ledger_market"
         rows.append({
             "account_id": account_id,
             "instrument_id": instrument_id,
@@ -582,20 +646,22 @@ def get_positions(conn: Connection) -> list[dict[str, str]]:
             "name": row["name"] if row else instrument_id,
             "ticker": row["ticker"] if row else "",
             "isin": row["isin"] if row else "",
-            "quantity": dec_text(pos.quantity),
+            "quantity": dec_text(quantity),
             "cost_basis_chf": dec_text(pos.cost_basis_chf, "0.01"),
-            "market_price_original": dec_text(pos.market_price_original, "0.01") if pos.market_price_original is not None else "",
-            "price_currency": row["latest_price_currency"] if row else "",
-            "price_date": date_text(row["latest_price_date"] if row else ""),
-            "price_provider": row["latest_price_provider"] if row else "",
-            "market_value_chf": dec_text(pos.market_value_chf, "0.01") if pos.market_value_chf is not None else "",
-            "price_status": "ok" if pos.market_price_original is not None else "missing_market_price",
+            "market_price_original": dec_text(market_price, "0.01") if market_price is not None else "",
+            "price_currency": price_currency,
+            "price_date": price_date,
+            "price_provider": price_provider,
+            "market_value_chf": dec_text(market_value_chf, "0.01") if market_value_chf is not None else "",
+            "price_status": "ok" if market_price is not None else "missing_market_price",
+            "valuation_source": valuation_source,
+            "valuation_as_of": price_date,
             "provider_symbol_status": "mapped" if conn.execute("SELECT 1 FROM instrument_price_mappings WHERE instrument_id=? AND mapping_status IN ('mapped','confirmed','user_confirmed') AND provider_symbol IS NOT NULL LIMIT 1", (instrument_id,)).fetchone() else "missing_provider_symbol",
             "hedge_status": row["hedge_status"] if row else pos.hedge_status,
             "instrument_status": row["instrument_status"] if row else pos.instrument_status,
             "valuation_policy": row["valuation_policy"] if row else pos.valuation_policy,
             "fx_status": "ok" if "missing_fx" not in pos.quality_warnings else "missing_fx",
-            "market_price_status": "ok" if pos.market_price_original is not None else "missing_market_price",
+            "market_price_status": "ok" if market_price is not None else "missing_market_price",
             "valuation_timestamp_status": pos.valuation_timestamp_status,
             "corporate_action_status": row["corporate_action_status"] if row else pos.corporate_action_status,
             "valuation_status": pos.valuation_status,
diff --git a/src/jarvis_finance/imports/postfinance_documents.py b/src/jarvis_finance/imports/postfinance_documents.py
index b6e6703..8cfa6d0 100644
--- a/src/jarvis_finance/imports/postfinance_documents.py
+++ b/src/jarvis_finance/imports/postfinance_documents.py
@@ -7,14 +7,14 @@ without trusting extracted temporary files.

 from __future__ import annotations

-from dataclasses import dataclass
+import io
+import re
+import stat
+from dataclasses import dataclass, replace
 from datetime import datetime
 from decimal import Decimal, InvalidOperation
 from hashlib import sha256
-import io
 from pathlib import PurePosixPath
-import re
-import stat
 from zipfile import BadZipFile, ZipFile

 from pypdf import PdfReader
@@ -118,6 +118,33 @@ class PFBundle:
     reason_codes: tuple[str, ...]

+def _reconcile_cash_components(
+    cash: list[PFSnapshotCash], cash_total_chf: Decimal
+) -> list[PFSnapshotCash]:
+    """Allocate the official cash subtotal across rows whose displayed FX is rounded.
+
+    The final CHF-looking column in the source row is the whole portfolio's currency
+    exposure, not the currency-cash value. It must never be projected as cash.
+    """
+
+    raw = [item.amount_original * item.fx_rate_to_chf for item in cash]
+    raw_total = sum(raw, Decimal("0"))
+    if not raw or raw_total <= 0:
+        raise ValueError("PostFinance cash components cannot be reconciled")
+    money = Decimal("0.01")
+    allocated = [(value * cash_total_chf / raw_total).quantize(money) for value in raw]
+    residual = cash_total_chf.quantize(money) - sum(allocated, Decimal("0"))
+    if residual:
+        largest = max(range(len(raw)), key=raw.__getitem__)
+        allocated[largest] += residual
+    if sum(allocated, Decimal("0")) != cash_total_chf.quantize(money):
+        raise ValueError("PostFinance allocated cash components do not reconcile")
+    return [
+        replace(item, source_value_chf=value)
+        for item, value in zip(cash, allocated, strict=True)
+    ]
+
+
 def _hash(*parts: object) -> str:
     return sha256("|".join(str(p) for p in parts).encode()).hexdigest()

@@ -650,6 +677,7 @@ def _parse_snapshot(raw: bytes) -> PFSnapshot:
         raise ValueError("PostFinance account total does not reconcile")
     if abs(stock_total + etf_total - securities_total) > Decimal("0.05"):
         raise ValueError("PostFinance asset-class totals do not reconcile")
+    cash = _reconcile_cash_components(cash, cash_total)
     return PFSnapshot(
         sha256(raw).hexdigest(),
         page_count,
diff --git a/src/jarvis_finance/ledger/cash.py b/src/jarvis_finance/ledger/cash.py
index eb9f0c7..e4a894e 100644
--- a/src/jarvis_finance/ledger/cash.py
+++ b/src/jarvis_finance/ledger/cash.py
@@ -62,7 +62,9 @@ def _signed_cash_delta(row) -> tuple[Decimal, Decimal | None]:
     return original, chf

-def calculate_cash_balances(conn: Connection, *, as_of_date: str | None = None) -> CashCalculationResult:
+def calculate_cash_balances(
+    conn: Connection, *, as_of_date: str | None = None, persist_alerts: bool = True
+) -> CashCalculationResult:
     result = CashCalculationResult()
     params: tuple[object, ...] = ()
     where = "WHERE COALESCE(is_voided, 0) = 0"
@@ -93,17 +95,19 @@ def calculate_cash_balances(conn: Connection, *, as_of_date: str | None = None)
         if balance.amount_original < 0:
             msg = f"Negative cash balance for account={balance.account_id} currency={balance.currency}: {balance.amount_original}"
             result.negative_cash_warnings.append(msg)
-            create_alert(
-                conn,
-                priority="warnung",
-                category="cash",
-                entity_type="account",
-                entity_id=balance.account_id,
-                rule_id="negative_cash",
-                message=msg,
-                evidence={"currency": balance.currency, "amount_original": str(balance.amount_original)},
-            )
-    conn.commit()
+            if persist_alerts:
+                create_alert(
+                    conn,
+                    priority="warnung",
+                    category="cash",
+                    entity_type="account",
+                    entity_id=balance.account_id,
+                    rule_id="negative_cash",
+                    message=msg,
+                    evidence={"currency": balance.currency, "amount_original": str(balance.amount_original)},
+                )
+    if persist_alerts:
+        conn.commit()
     return result

diff --git a/src/jarvis_finance/services/cash_service.py b/src/jarvis_finance/services/cash_service.py
index 58edbd4..56b2b81 100644
--- a/src/jarvis_finance/services/cash_service.py
+++ b/src/jarvis_finance/services/cash_service.py
@@ -1,8 +1,8 @@
 from __future__ import annotations

+import json
 from datetime import UTC, datetime
 from decimal import Decimal, InvalidOperation
-import json
 from sqlite3 import Connection
 from uuid import uuid4

@@ -19,7 +19,9 @@ from jarvis_finance.api.schemas.positions import (
     ConfirmResponse,
     PreviewResponse,
 )
+from jarvis_finance.ledger.cash import _signed_cash_delta, calculate_cash_balances
 from jarvis_finance.services.api_helpers import decimal_text
+from jarvis_finance.services.portfolio_aggregation import latest_official_postfinance_cash

 CANONICAL_CASH_ACCOUNTS = [
     {"platform": "Raiffeisen", "account_name": "Raiffeisen", "balance_mode": "csv_calculated"},
@@ -147,11 +149,24 @@ def _calculated_balance(conn: Connection, account_id: str) -> Decimal:
     anchor = _latest_snapshot(conn, account_id, "csv_anchor_balance")
     if anchor:
         return Decimal(str(anchor["amount_chf"])) + _cash_movements_after_anchor(conn, account_id, anchor["balance_date"])
-    row = conn.execute(
-        "SELECT COALESCE(SUM(CAST(amount_chf AS TEXT)), '0') AS amount FROM cash_balances WHERE account_id=?",
+    rows = conn.execute(
+        "SELECT amount_chf FROM cash_balances WHERE account_id=?",
         (account_id,),
-    ).fetchone()
-    return Decimal(str(row["amount"] or "0"))
+    ).fetchall()
+    if rows:
+        return sum((Decimal(str(row["amount_chf"])) for row in rows), Decimal("0"))
+    # Legacy/demo datasets can have a transaction ledger without materialized
+    # cash_balances. Keep the shared read model compatible without overriding an
+    # explicit snapshot projection.
+    ledger = calculate_cash_balances(conn, persist_alerts=False)
+    return sum(
+        (
+            balance.amount_chf
+            for (ledger_account_id, _currency), balance in ledger.balances.items()
+            if ledger_account_id == account_id and balance.amount_chf is not None
+        ),
+        Decimal("0"),
+    )

 def _last_imported_booking(conn: Connection, account_id: str) -> str | None:
@@ -159,49 +174,185 @@ def _last_imported_booking(conn: Connection, account_id: str) -> str | None:
     return row["last_date"] if row else None

+def _post_snapshot_cash_deltas(
+    conn: Connection,
+    account_id: str,
+    snapshot_date: str,
+) -> dict[str, tuple[Decimal, Decimal | None]]:
+    rows = conn.execute(
+        """SELECT * FROM transactions
+           WHERE account_id=? AND trade_date>?
+             AND COALESCE(is_confirmed,0)=1 AND COALESCE(is_voided,0)=0
+           ORDER BY trade_date,created_at,transaction_id""",
+        (account_id, snapshot_date),
+    ).fetchall()
+    deltas: dict[str, tuple[Decimal, Decimal | None]] = {}
+    for row in rows:
+        original, amount_chf = _signed_cash_delta(row)
+        if original == 0 and amount_chf in {None, Decimal("0")}:
+            continue
+        currency = str(row["currency_original"])
+        previous_original, previous_chf = deltas.get(
+            currency,
+            (Decimal("0"), Decimal("0")),
+        )
+        deltas[currency] = (
+            previous_original + original,
+            None
+            if previous_chf is None or amount_chf is None
+            else previous_chf + amount_chf,
+        )
+    return deltas
+
+
 def list_cash_positions(conn: Connection) -> list[CashPosition]:
+    ledger = calculate_cash_balances(conn, persist_alerts=False)
     rows = conn.execute(
         """
         SELECT a.account_id, a.account_name, a.account_type, a.currency, COALESCE(a.balance_mode,'manual') AS balance_mode, p.name AS platform
         FROM accounts a JOIN platforms p ON p.platform_id=a.platform_id
-        WHERE a.is_active=1 AND a.account_type='cash'
+        WHERE a.is_active=1
+          AND (
+              a.account_type='cash'
+              OR a.account_id IN (
+                  SELECT account_id FROM postfinance_account_roles
+                  WHERE role='etrading_cash'
+              )
+          )
         ORDER BY p.name, a.account_name
         """
     ).fetchall()
     positions: list[CashPosition] = []
+    official_components = latest_official_postfinance_cash(conn)
+    official_by_account: dict[str, list] = {}
+    for component in official_components:
+        official_by_account.setdefault(component.account_id, []).append(component)
     for row in rows:
         account_id = row["account_id"]
+        if account_id in official_by_account:
+            components = {
+                component.currency: component
+                for component in official_by_account[account_id]
+            }
+            snapshot_date = next(iter(components.values())).snapshot_date
+            deltas = _post_snapshot_cash_deltas(conn, account_id, snapshot_date)
+            for currency in sorted(set(components) | set(deltas)):
+                component = components.get(currency)
+                delta_original, delta_chf = deltas.get(
+                    currency,
+                    (Decimal("0"), Decimal("0")),
+                )
+                amount_original = (
+                    component.amount_original if component else Decimal("0")
+                ) + delta_original
+                baseline_chf = component.amount_chf if component else Decimal("0")
+                amount_chf = (
+                    baseline_chf + delta_chf if delta_chf is not None else baseline_chf
+                )
+                status = (
+                    "Abgleich offen"
+                    if delta_chf is None
+                    else "Offiziell abgeglichen + Bewegungen"
+                    if delta_original or delta_chf
+                    else "Offiziell abgeglichen"
+                )
+                positions.append(
+                    CashPosition(
+                        id=f"{account_id}:{currency}",
+                        account_id=account_id,
+                        platform=row["platform"],
+                        account_label=row["account_name"],
+                        account_type=row["account_type"],
+                        currency=currency,
+                        balance_mode="official_components",
+                        amount=decimal_text(amount_original, 2),
+                    …7397 tokens truncated…currency": component.currency,
+            "amount_original": _text(component.amount_original),
+            "fx_rate_to_chf": _text(component.source_fx_rate_to_chf),
+            "value_chf": _money(component.amount_chf),
         }
-        for row in conn.execute(
-            """SELECT currency,amount_original,fx_rate_to_chf,value_chf
-               FROM postfinance_snapshot_cash WHERE snapshot_id=? ORDER BY currency""",
-            (latest["snapshot_id"],),
-        ).fetchall()
+        for component in latest_official_postfinance_cash(conn)
     ]
     events = {
         str(row["event_type"]): int(row["c"])
diff --git a/tests/test_postfinance_sprint13.py b/tests/test_postfinance_sprint13.py
index ecf37e7..10413b8 100644
--- a/tests/test_postfinance_sprint13.py
+++ b/tests/test_postfinance_sprint13.py
@@ -1,12 +1,18 @@
 from __future__ import annotations

 import base64
+import json
 from dataclasses import replace
 from decimal import Decimal
 from hashlib import sha256

 import pytest

+from jarvis_finance.dashboard.data import (
+    get_cash_overview,
+    get_command_center_summary,
+    get_positions,
+)
 from jarvis_finance.imports.postfinance_documents import (
     PFBundle,
     PFDocument,
@@ -16,6 +22,15 @@ from jarvis_finance.imports.postfinance_documents import (
     PFSnapshotPosition,
 )
 from jarvis_finance.services import postfinance_service as service
+from jarvis_finance.services.cash_service import get_cash_position_detail, get_cash_summary
+from jarvis_finance.services.equity_service import get_equity_summary
+from jarvis_finance.services.portfolio_aggregation import (
+    latest_official_postfinance_cash,
+    latest_official_postfinance_positions,
+)
+from jarvis_finance.services.portfolio_analytics import confirmed_canonical_positions
+from jarvis_finance.services.portfolio_data import build_portfolio_reconciliation
+from jarvis_finance.services.portfolio_service import get_overview
 from jarvis_finance.storage.database import connect_memory
 from jarvis_finance.storage.migrations import apply_migrations

@@ -461,3 +476,345 @@ def test_immutable_postfinance_source_and_snapshot_rows(monkeypatch, tmp_path):
             conn.execute(f"UPDATE {table} SET created_at=created_at WHERE {predicate}")
         with pytest.raises(Exception, match="cannot be deleted"):
             conn.execute(f"DELETE FROM {table} WHERE {predicate}")
+
+
+def test_global_read_model_counts_official_postfinance_components_once(monkeypatch, tmp_path):
+    """Reproduce the production placeholder/exposure/settlement-position collision."""
+    conn = make_conn()
+    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
+    original = bundle()
+    stock_total = Decimal("255791.97")
+    etf_total = Decimal("37673.19")
+
+    def split_total(total: Decimal) -> list[Decimal]:
+        values = [(total / Decimal("11")).quantize(Decimal("0.01")) for _ in range(11)]
+        values[-1] += total - sum(values, Decimal("0"))
+        return values
+
+    source_values = split_total(stock_total) + split_total(etf_total)
+    positions = tuple(
+        replace(item, quantity=Decimal("1"), market_price_original=value, market_value_chf=value)
+        for item, value in zip(original.snapshot.positions, source_values, strict=True)
+    )
+    realistic = replace(
+        original,
+        snapshot=replace(
+            original.snapshot,
+            positions=positions,
+            cash=(
+                PFSnapshotCash("EUR", Decimal("21693.27"), Decimal("0.9293"), Decimal("71623.09")),
+                PFSnapshotCash("USD", Decimal("9235.55"), Decimal("0.8146"), Decimal("249526.14")),
+            ),
+            securities_total_chf=Decimal("293465.16"),
+            cash_total_chf=Decimal("27684.07"),
+            total_value_chf=Decimal("321149.23"),
+            stock_total_chf=stock_total,
+            etf_total_chf=etf_total,
+        ),
+    )
+    monkeypatch.setattr(service, "parse_postfinance_bundle", lambda _zip, _overview: realistic)
+    conn.execute(
+        """INSERT INTO cash_account_snapshots(snapshot_id,account_id,snapshot_type,balance_date,
+               amount_original,currency,amount_chf,source,note,created_at,created_by)
+           VALUES('legacy-total',?,'manual_balance','2026-07-26','321149.23','CHF',
+                  '321149.23','manual','historical depot total placeholder',
+                  '2026-07-26T00:00:00Z','synthetic')""",
+        (CASH,),
+    )
+    conn.execute(
+        "INSERT INTO platforms(platform_id,name,platform_type,country,default_currency,is_active,created_at) VALUES('tw','True Wealth','wealth','CH','CHF',1,'2026-01-01')"
+    )
+    conn.execute(
+        """INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,
+               performance_included,is_active,created_at,portfolio_bucket)
+           VALUES('account_tw','tw','True Wealth','brokerage','CHF',1,1,'2026-01-01','truewealth')"""
+    )
+    conn.execute(
+        """INSERT INTO account_value_snapshots(snapshot_id,account_id,valuation_date,
+               total_value_chf,currency,source_type,quality_status,created_at,is_active)
+           VALUES('tw-value','account_tw','2026-07-27','152845.00','CHF',
+                  'synthetic_truewealth','complete','2026-07-27T00:00:00Z',1)"""
+    )
+    conn.commit()
+
+    payload = request()
+    preview = service.preview_postfinance_import(conn, payload)
+    service.confirm_postfinance_import(
+        conn,
+        {**payload, "preview_id": preview["preview_id"],
+         "confirmation_id": preview["confirmation_id"], "confirm": True},
+    )
+    before_gets = conn.total_changes
+    cash = get_cash_summary(conn)
+    equity = get_equity_summary(conn)
+    overview = get_overview(conn)
+    command = get_command_center_summary(conn)
+    reconciliation = build_portfolio_reconciliation(conn, as_of="2026-07-27")
+    rows = get_positions(conn)
+
+    assert conn.total_changes == before_gets
+    assert cash.cash_chf == "27684.07"
+    assert sum(Decimal(item.used_value_chf) for item in cash.positions if item.account_id == CASH) == Decimal("27684.07")
+    usd_position = next(item for item in cash.positions if item.account_id == CASH and item.currency == "USD")
+    assert get_cash_position_detail(conn, usd_position.id).currency == "USD"
+    assert equity.equity_value_chf == "293465.16"
+    assert equity.as_of == "2026-07-27T09:14:30+02:00"
+    assert sum(Decimal(row["market_value_chf"]) for row in rows if row["account_id"] == DEPOT) == Decimal("293465.16")
+    assert not [row for row in rows if row["account_id"] == CASH and row["asset_class"] in {"stock", "equity", "etf"}]
+    assert Decimal(cash.cash_chf) + Decimal(equity.equity_value_chf) == Decimal("321149.23")
+    assert overview.truewealth_value_chf == "152845.00"
+    assert command["cash_total_chf"] == "27684.07"
+    pf_reconciliation = next(
+        item for item in reconciliation["account_totals"]
+        if item["reported_total"] == "321149.23"
+    )
+    assert pf_reconciliation["derived_total"] == "321149.23"
+    assert pf_reconciliation["status"] == "matched"
+    assert conn.execute("SELECT amount_chf FROM cash_account_snapshots WHERE snapshot_id='legacy-total'").fetchone()[0] == "321149.23"
+    assert conn.execute("SELECT COUNT(*) FROM transactions WHERE internal_transfer_group_id IS NOT NULL").fetchone()[0] == 2
+
+    market_positions = []
+    for row, value in zip(
+        [row for row in rows if row["account_id"] == DEPOT],
+        split_total(Decimal("160000")) + split_total(Decimal("140000")),
+        strict=True,
+    ):
+        market_positions.append({
+            "account_id": DEPOT, "instrument_id": row["instrument_id"],
+            "quality_status": "fresh", "value_chf": str(value), "close": str(value),
+            "currency": "CHF", "provider": "synthetic-market",
+        })
+    conn.execute(
+        """INSERT INTO market_data_runs(run_id,source_key,as_of,input_fingerprint,status,
+               started_at,completed_at,price_total,price_stored,fx_total,fx_stored,
+               benchmark_total,benchmark_stored,valuation_stored,missing_instruments_json,
+               reason_codes_json)
+           VALUES('newer-run','synthetic','2026-07-28','fp','complete','2026-07-28T00:00:00Z',
+                  '2026-07-28T00:01:00Z',22,22,0,0,0,0,22,'[]','[]')"""
+    )
+    conn.execute(
+        """INSERT INTO portfolio_analysis_snapshots(analysis_snapshot_id,run_id,as_of,
+               base_currency,total_value_chf,price_coverage_pct,fx_coverage_pct,
+               benchmark_coverage_pct,quality_status,reason_codes_json,summary_json,created_at)
+           VALUES('newer-analysis','newer-run','2026-07-28','CHF','300000','100','100','0',
+                  'complete','[]',?,'2026-07-28T00:01:00Z')""",
+        (json.dumps({"positions": market_positions}),),
+    )
+    conn.commit()
+    newer = get_equity_summary(conn)
+    assert newer.equity_value_chf == "300000.00"
+    assert newer.as_of == "2026-07-28"
+    assert get_cash_summary(conn).cash_chf == "27684.07"
+    assert service.get_postfinance_summary(conn)["latest_snapshot"]["total_chf"] == "321149.23"
+
+    duplicate = service.preview_postfinance_import(conn, payload)
+    second = service.confirm_postfinance_import(
+        conn,
+        {**payload, "preview_id": duplicate["preview_id"],
+         "confirmation_id": duplicate["confirmation_id"], "confirm": True},
+    )
+    assert duplicate["duplicate"] is True and duplicate["conflict"] is False
+    assert second["idempotent"] is True
+    assert conn.execute("SELECT COUNT(*) FROM postfinance_import_batches").fetchone()[0] == 1
+
+
+def test_official_snapshot_resolvers_honor_historical_as_of_and_cutoff(monkeypatch, tmp_path):
+    conn = make_conn()
+    monkeypatch.setenv("JARVIS_FINANCE_RUNTIME_DIR", str(tmp_path))
+    monkeypatch.setattr(service, "parse_postfinance_bundle", lambda _zip, _overview: bundle())
+    payload = request()
+    preview = service.preview_postfinance_import(conn, payload)
+    service.confirm_postfinance_import(
+        conn,
+        {**payload, "preview_id": preview["preview_id"],
+         "confirmation_id": preview["confirmation_id"], "confirm": True},
+    )
+    original_cash_total = sum(
+        (item.amount_chf for item in latest_official_postfinance_cash(conn)), Decimal("0")
+    )
+    original_position_total = sum(
+        (
+            item.market_value_chf
+            for item in latest_official_postfinance_positions(conn).values()
+        ),
+        Decimal("0"),
+    )
+    conn.execute("UPDATE accounts SET account_type='checking' WHERE account_id=?", (CASH,))
+    assert sum(
+        (
+            Decimal(item.used_value_chf)
+            for item in get_cash_summary(conn).positions
+            if item.account_id == CASH
+        ),
+        Decimal("0"),
+    ) == original_cash_total
+    platform_id = conn.execute(
+        "SELECT platform_id FROM accounts WHERE account_id=?", (CASH,)
+    ).fetchone()["platform_id"]
+    conn.execute(
+        """INSERT INTO accounts(
+               account_id,platform_id,account_name,account_type,currency,balance_mode,
+               portfolio_bucket,is_active,created_at)
+           VALUES('generic-usd-cash',?,'Generic Multi-Currency','cash','CHF','csv_calculated',
+                  'cash',1,'2026-07-27T10:00:00Z')""",
+        (platform_id,),
+    )
+    conn.execute(
+        """INSERT INTO transactions(
+               transaction_id,transaction_type,account_id,trade_date,gross_amount_original,
+               net_amount_original,currency_original,fx_rate_to_chf,fx_status,source_type,
+               source_id,is_confirmed,quality_status,created_at)
+           VALUES('generic-usd-deposit','cash_deposit','generic-usd-cash','2026-07-27',
+                  '100','100','USD','0.9','ok','csv_import','generic-usd-deposit',1,
+                  'ok','2026-07-27T10:00:00Z')"""
+    )
+    conn.execute(
+        """INSERT INTO transactions(
+               transaction_id,transaction_type,account_id,trade_date,gross_amount_original,
+               net_amount_original,currency_original,fx_rate_to_chf,fx_status,source_type,
+               source_id,is_confirmed,quality_status,created_at)
+           VALUES('generic-eur-deposit','cash_deposit','generic-usd-cash','2026-07-27',
+                  '50','50','EUR','1.1','ok','csv_import','generic-eur-deposit',1,
+                  'ok','2026-07-27T10:00:01Z')"""
+    )
+    generic_rows = [
+        row for row in get_cash_overview(conn) if row["account_id"] == "generic-usd-cash"
+    ]
+    assert {row["currency"] for row in generic_rows} == {"EUR", "USD"}
+    generic_usd = next(row for row in generic_rows if row["currency"] == "USD")
+    generic_eur = next(row for row in generic_rows if row["currency"] == "EUR")
+    assert generic_usd["amount_original"] == "100.00"
+    assert generic_usd["amount_chf"] == "90.00"
+    assert generic_eur["amount_original"] == "50.00"
+    assert generic_eur["amount_chf"] == "55.00"
+    conn.execute(
+        "UPDATE accounts SET balance_mode='manual' WHERE account_id='generic-usd-cash'"
+    )
+    conn.execute(
+        """INSERT INTO cash_account_snapshots(
+               snapshot_id,account_id,snapshot_type,balance_date,amount_original,currency,
+               amount_chf,source,created_at,created_by)
+           VALUES('generic-manual-total','generic-usd-cash','manual_balance','2026-07-27',
+                  '1000','CHF','1000','manual','2026-07-27T10:01:00Z','user')"""
+    )
+    controlled_rows = [
+        row for row in get_cash_overview(conn) if row["account_id"] == "generic-usd-cash"
+    ]
+    assert [(row["currency"], row["amount_chf"]) for row in controlled_rows] == [
+        ("CHF", "1000.00")
+    ]
+
+    batch = dict(conn.execute("SELECT * FROM postfinance_import_batches LIMIT 1").fetchone())
+    batch.update(
+        batch_id="future-batch",
+        bundle_sha256=sha256(b"future-bundle").hexdigest(),
+        snapshot_at="2026-08-01T09:14:30+02:00",
+        confirmed_at="2026-08-01T10:00:00Z",
+    )
+    columns = list(batch)
+    conn.execute(
+        f"INSERT INTO postfinance_import_batches({','.join(columns)}) VALUES({','.join('?' for _ in columns)})",
+        [batch[column] for column in columns],
+    )
+    source_snapshot = dict(conn.execute("SELECT * FROM postfinance_snapshots LIMIT 1").fetchone())
+    source_snapshot.update(
+        snapshot_id="future-snapshot",
+        batch_id="future-batch",
+        snapshot_at="2026-08-01T09:14:30+02:00",
+        total_chf="1299.00",
+        securities_chf="300.00",
+        cash_chf="999.00",
+        stocks_chf="100.00",
+        etfs_chf="200.00",
+        component_total_chf="1299.00",
+        difference_chf="0.00",
+        created_at="2026-08-01T10:00:00Z",
+    )
+    columns = list(source_snapshot)
+    conn.execute(
+        f"INSERT INTO postfinance_snapshots({','.join(columns)}) VALUES({','.join('?' for _ in columns)})",
+        [source_snapshot[column] for column in columns],
+    )
+    conn.execute(
+        """INSERT INTO postfinance_snapshot_positions(
+               snapshot_position_id,snapshot_id,source_row_reference,instrument_id,asset_class,
+               quantity,provider_average_cost_original,provider_cost_total_original,
+               price_original,price_currency,value_chf,weight_pct,computed_cost_basis_original,
+               computed_cost_basis_status,provenance_json,created_at)
+           SELECT 'future-'||snapshot_position_id,'future-snapshot',source_row_reference,
+                  instrument_id,asset_class,quantity,provider_average_cost_original,
+                  provider_cost_total_original,price_original,price_currency,value_chf,weight_pct,
+                  computed_cost_basis_original,computed_cost_basis_status,provenance_json,
+                  '2026-08-01T10:00:00Z'
+           FROM postfinance_snapshot_positions WHERE snapshot_id<> 'future-snapshot'"""
+    )
+    conn.execute(
+        """INSERT INTO postfinance_snapshot_cash(
+               snapshot_cash_id,snapshot_id,currency,amount_original,fx_rate_to_chf,value_chf,created_at)
+           SELECT 'future-'||snapshot_cash_id,'future-snapshot',currency,amount_original,
+                  fx_rate_to_chf,value_chf,'2026-08-01T10:00:00Z'
+           FROM postfinance_snapshot_cash WHERE snapshot_id<> 'future-snapshot'"""
+    )
+    conn.commit()
+
+    assert sum((item.amount_chf for item in latest_official_postfinance_cash(conn)), Decimal("0")) == Decimal("999.00")
+    historical_cash = latest_official_postfinance_cash(conn, as_of="2026-07-27")
+    assert sum((item.amount_chf for item in historical_cash), Decimal("0")) == original_cash_total
+    cutoff_cash = latest_official_postfinance_cash(
+        conn,
+        as_of="2026-08-01",
+        data_cutoff="2026-07-31T23:59:59Z",
+    )
+    assert sum((item.amount_chf for item in cutoff_cash), Decimal("0")) == original_cash_total
+    historical_positions = latest_official_postfinance_positions(conn, as_of="2026-07-27")
+    assert sum(
+        (item.market_value_chf for item in historical_positions.values()), Decimal("0")
+    ) == original_position_total
+    assert {item.valuation_at for item in historical_positions.values()} == {"2026-07-27T09:14:30+02:00"}
+    current_positions = latest_official_postfinance_positions(conn)
+    assert sum((item.market_value_chf for item in current_positions.values()), Decimal("0")) == Decimal("300.00")
+    assert {item.valuation_at for item in current_positions.values()} == {"2026-08-01T09:14:30+02:00"}
+
+    selected = next(iter(current_positions.values()))
+    baseline_quantity = selected.quantity
+    conn.execute(
+        """INSERT INTO transactions(
+               transaction_id,transaction_type,account_id,instrument_id,trade_date,
+               settlement_date,quantity,currency_original,source_type,source_id,
+               external_transaction_id,row_hash,is_confirmed,quality_status,created_at,
+               updated_at,is_voided)
+           VALUES('post-snapshot-buy','buy',?,?, '2026-08-02','2026-08-02','1',
+                  'CHF','manual_confirmed_trade','post-snapshot-buy','post-snapshot-buy',?,
+                  1,'ok','2026-08-02T10:00:00Z','2026-08-02T10:00:00Z',0)""",
+        (CASH, selected.instrument_id, sha256(b"post-snapshot-buy").hexdigest()),
+    )
+    conn.execute(
+        """INSERT INTO transactions(
+               transaction_id,transaction_type,account_id,trade_date,gross_amount_original,
+               net_amount_original,currency_original,fx_rate_to_chf,fx_status,source_type,
+               source_id,is_confirmed,quality_status,created_at,is_voided)
+           VALUES('post-snapshot-cash','cash_deposit',?,'2026-08-02','10','10','USD',
+                  '0.8','ok','manual_confirmed_cash','post-snapshot-cash',1,'ok',
+                  '2026-08-02T10:01:00Z',0)""",
+        (CASH,),
+    )
+    conn.commit()
+    post_snapshot_cash = [
+        item for item in get_cash_summary(conn).positions if item.account_id == CASH
+    ]
+    assert sum(
+        (Decimal(item.used_value_chf) for item in post_snapshot_cash), Decimal("0")
+    ) == Decimal("1007.00")
+    assert next(item for item in post_snapshot_cash if item.currency == "USD").amount == "110.00"
+    dashboard_position = next(
+        row
+        for row in get_positions(conn)
+        if row["account_id"] == DEPOT and row["instrument_id"] == selected.instrument_id
+    )
+    assert Decimal(dashboard_position["quantity"]) == baseline_quantity + Decimal("1")
+    canonical_position = next(
+        item
+        for item in confirmed_canonical_positions(conn, as_of="2026-08-02")
+        if item.account_id == DEPOT and item.instrument_id == selected.instrument_id
+    )
+    assert canonical_position.quantity == baseline_quantity + Decimal("1")


git diff -- src/jarvis_finance/services/cash_service.py src/jarvis_finance/services/equity_service.py src/jarvis_finance/services/portfolio_analytics.py src/jarvis_finance/services/portfolio_data.py src/jarvis_finance/services/postfinance_service.py


Output:
Chunk ID: 43391d
Wall time: 0.0001 seconds
Process exited with code 0
Original token count: 9179
Output:
diff --git a/src/jarvis_finance/services/cash_service.py b/src/jarvis_finance/services/cash_service.py
index 58edbd4..56b2b81 100644
--- a/src/jarvis_finance/services/cash_service.py
+++ b/src/jarvis_finance/services/cash_service.py
@@ -1,8 +1,8 @@
 from __future__ import annotations

+import json
 from datetime import UTC, datetime
 from decimal import Decimal, InvalidOperation
-import json
 from sqlite3 import Connection
 from uuid import uuid4

@@ -19,7 +19,9 @@ from jarvis_finance.api.schemas.positions import (
     ConfirmResponse,
     PreviewResponse,
 )
+from jarvis_finance.ledger.cash import _signed_cash_delta, calculate_cash_balances
 from jarvis_finance.services.api_helpers import decimal_text
+from jarvis_finance.services.portfolio_aggregation import latest_official_postfinance_cash

 CANONICAL_CASH_ACCOUNTS = [
     {"platform": "Raiffeisen", "account_name": "Raiffeisen", "balance_mode": "csv_calculated"},
@@ -147,11 +149,24 @@ def _calculated_balance(conn: Connection, account_id: str) -> Decimal:
     anchor = _latest_snapshot(conn, account_id, "csv_anchor_balance")
     if anchor:
         return Decimal(str(anchor["amount_chf"])) + _cash_movements_after_anchor(conn, account_id, anchor["balance_date"])
-    row = conn.execute(
-        "SELECT COALESCE(SUM(CAST(amount_chf AS TEXT)), '0') AS amount FROM cash_balances WHERE account_id=?",
+    rows = conn.execute(
+        "SELECT amount_chf FROM cash_balances WHERE account_id=?",
         (account_id,),
-    ).fetchone()
-    return Decimal(str(row["amount"] or "0"))
+    ).fetchall()
+    if rows:
+        return sum((Decimal(str(row["amount_chf"])) for row in rows), Decimal("0"))
+    # Legacy/demo datasets can have a transaction ledger without materialized
+    # cash_balances. Keep the shared read model compatible without overriding an
+    # explicit snapshot projection.
+    ledger = calculate_cash_balances(conn, persist_alerts=False)
+    return sum(
+        (
+            balance.amount_chf
+            for (ledger_account_id, _currency), balance in ledger.balances.items()
+            if ledger_account_id == account_id and balance.amount_chf is not None
+        ),
+        Decimal("0"),
+    )

 def _last_imported_booking(conn: Connection, account_id: str) -> str | None:
@@ -159,49 +174,185 @@ def _last_imported_booking(conn: Connection, account_id: str) -> str | None:
     return row["last_date"] if row else None

+def _post_snapshot_cash_deltas(
+    conn: Connection,
+    account_id: str,
+    snapshot_date: str,
+) -> dict[str, tuple[Decimal, Decimal | None]]:
+    rows = conn.execute(
+        """SELECT * FROM transactions
+           WHERE account_id=? AND trade_date>?
+             AND COALESCE(is_confirmed,0)=1 AND COALESCE(is_voided,0)=0
+           ORDER BY trade_date,created_at,transaction_id""",
+        (account_id, snapshot_date),
+    ).fetchall()
+    deltas: dict[str, tuple[Decimal, Decimal | None]] = {}
+    for row in rows:
+        original, amount_chf = _signed_cash_delta(row)
+        if original == 0 and amount_chf in {None, Decimal("0")}:
+            continue
+        currency = str(row["currency_original"])
+        previous_original, previous_chf = deltas.get(
+            currency,
+            (Decimal("0"), Decimal("0")),
+        )
+        deltas[currency] = (
+            previous_original + original,
+            None
+            if previous_chf is None or amount_chf is None
+            else previous_chf + amount_chf,
+        )
+    return deltas
+
+
 def list_cash_positions(conn: Connection) -> list[CashPosition]:
+    ledger = calculate_cash_balances(conn, persist_alerts=False)
     rows = conn.execute(
         """
         SELECT a.account_id, a.account_name, a.account_type, a.currency, COALESCE(a.balance_mode,'manual') AS balance_mode, p.name AS platform
         FROM accounts a JOIN platforms p ON p.platform_id=a.platform_id
-        WHERE a.is_active=1 AND a.account_type='cash'
+        WHERE a.is_active=1
+          AND (
+              a.account_type='cash'
+              OR a.account_id IN (
+                  SELECT account_id FROM postfinance_account_roles
+                  WHERE role='etrading_cash'
+              )
+          )
         ORDER BY p.name, a.account_name
         """
     ).fetchall()
     positions: list[CashPosition] = []
+    official_components = latest_official_postfinance_cash(conn)
+    official_by_account: dict[str, list] = {}
+    for component in official_components:
+        official_by_account.setdefault(component.account_id, []).append(component)
     for row in rows:
         account_id = row["account_id"]
+        if account_id in official_by_account:
+            components = {
+                component.currency: component
+                for component in official_by_account[account_id]
+            }
+            snapshot_date = next(iter(components.values())).snapshot_date
+            deltas = _post_snapshot_cash_deltas(conn, account_id, snapshot_date)
+            for currency in sorted(set(components) | set(deltas)):
+                component = components.get(currency)
+                delta_original, delta_chf = deltas.get(
+                    currency,
+                    (Decimal("0"), Decimal("0")),
+                )
+                amount_original = (
+                    component.amount_original if component else Decimal("0")
+                ) + delta_original
+                baseline_chf = component.amount_chf if component else Decimal("0")
+                amount_chf = (
+                    baseline_chf + delta_chf if delta_chf is not None else baseline_chf
+                )
+                status = (
+                    "Abgleich offen"
+                    if delta_chf is None
+                    else "Offiziell abgeglichen + Bewegungen"
+                    if delta_original or delta_chf
+                    else "Offiziell abgeglichen"
+                )
+                positions.append(
+                    CashPosition(
+                        id=f"{account_id}:{currency}",
+                        account_id=account_id,
+                        platform=row["platform"],
+                        account_label=row["account_name"],
+                        account_type=row["account_type"],
+                        currency=currency,
+                        balance_mode="official_components",
+                        amount=decimal_text(amount_original, 2),
+                        amount_chf=decimal_text(amount_chf, 2),
+                        calculated_balance_chf=decimal_text(amount_chf, 2),
+                        manual_balance_chf=None,
+                        csv_anchor_balance_chf=None,
+                        used_value_chf=decimal_text(amount_chf, 2),
+                        difference_chf="0.00",
+                        last_manual_reconciliation=snapshot_date,
+                        last_imported_booking=_last_imported_booking(conn, account_id),
+                        status=status,
+                    )
+                )
+            continue
         manual = _latest_snapshot(conn, account_id, "manual_balance")
         anchor = _latest_snapshot(conn, account_id, "csv_anchor_balance")
-        calculated = _calculated_balance(conn, account_id)
-        manual_amount = Decimal(str(manual["amount_chf"])) if manual else None
         mode = row["balance_mode"] or "manual"
-        used = manual_amount if mode == "manual" and manual_amount is not None else calculated
-        difference = (manual_amount - calculated) if manual_amount is not None else Decimal("0")
-        status = "Aktuell"
-        if manual_amount is not None and difference != 0:
-            status = "Abgleich offen"
-        positions.append(
-            CashPosition(
-                id=f"{account_id}:{row['currency']}",
-                account_id=account_id,
-                platform=row["platform"],
-                account_label=row["account_name"],
-                account_type=row["account_type"],
-                currency=row["currency"],
-                balance_mode=mode,
-                amount=decimal_text(used, 2),
-                amount_chf=decimal_text(used, 2),
-                calculated_balance_chf=decimal_text(calculated, 2),
-                manual_balance_chf=decimal_text(manual_amount, 2) if manual_amount is not None else None,
-                csv_anchor_balance_chf=decimal_text(anchor["amount_chf"], 2) if anchor else None,
-                used_value_chf=decimal_text(used, 2),
-                difference_chf=decimal_text(difference, 2),
-                last_manual_reconciliation=manual["balance_date"] if manual else None,
-                last_imported_booking=_last_imported_booking(conn, account_id),
-                status=status,
-            )
+        default_currency = str(row["currency"])
+        ledger_currencies = {
+            currency
+            for ledger_account_id, currency in ledger.balances
+            if ledger_account_id == account_id
+        }
+        # Manual balances and CSV anchors are account-level CHF controls. They must
+        # not be added to per-currency ledger rows from the same account.
+        currencies = (
+            {default_currency}
+            if manual or anchor
+            else set(ledger_currencies) or {default_currency}
         )
+        for currency in sorted(currencies):
+            ledger_balance = ledger.balances.get((account_id, currency))
+            if ledger_balance is not None:
+                calculated = ledger_balance.amount_chf or Decimal("0")
+                amount_original = ledger_balance.amount_original
+            else:
+                calculated = _calculated_balance(conn, account_id)
+                amount_original = (
+                    calculated if currency.upper() == "CHF" else Decimal("0")
+                )
+            manual_amount = (
+                Decimal(str(manual["amount_chf"]))
+                if manual and currency == default_currency
+                else None
+            )
+            used = (
+                manual_amount
+                if mode == "manual" and manual_amount is not None
+                else calculated
+            )
+            difference = (
+                manual_amount - calculated
+                if manual_amount is not None
+                else Decimal("0")
+            )
+            status = "Abgleich offen" if manual_amount is not None and difference != 0 else "Aktuell"
+            positions.append(
+                CashPosition(
+                    id=f"{account_id}:{currency}",
+                    account_id=account_id,
+                    platform=row["platform"],
+                    account_label=row["account_name"],
+                    account_type=row["account_type"],
+                    currency=currency,
+                    balance_mode=mode,
+                    amount=decimal_text(amount_original, 2),
+                    amount_chf=decimal_text(used, 2),
+                    calculated_balance_chf=decimal_text(calculated, 2),
+                    manual_balance_chf=(
+                        decimal_text(manual_amount, 2)
+                        if manual_amount is not None
+                        else None
+                    ),
+                    csv_anchor_balance_chf=(
+                        decimal_text(anchor["amount_chf"], 2)
+                        if anchor and currency == default_currency
+                        else None
+                    ),
+                    used_value_chf=decimal_text(used, 2),
+                    difference_chf=decimal_text(difference, 2),
+                    last_manual_reconciliation=(
+                        manual["balance_date"]
+                        if manual and currency == default_currency
+                        else None
+                    ),
+                    last_imported_booking=_last_imported_booking(conn, account_id),
+                    status=status,
+                )
+            )
     return positions

@@ -252,10 +403,10 @@ def confirm_cash_snapshot(conn: Connection, request: CashSnapshotConfirmRequest)

 def get_cash_position_detail(conn: Connection, position_id: str) -> CashPositionDetail:
-    account_id, _currency = position_id.split(":", 1)
-    position = next((p for p in get_cash_summary(conn).positions if p.account_id == account_id), None)
+    position = next((p for p in get_cash_summary(conn).positions if p.id == position_id), None)
     if not position:
         raise HTTPException(status_code=404, detail="Cash position not found")
+    account_id = position.account_id
     rows = conn.execute(
         """
         SELECT snapshot_id, snapshot_type, balance_date, amount_chf, source, COALESCE(note,'') AS note, audit_id
diff --git a/src/jarvis_finance/services/equity_service.py b/src/jarvis_finance/services/equity_service.py
index 614e76b..acb3fca 100644
--- a/src/jarvis_finance/services/equity_service.py
+++ b/src/jarvis_finance/services/equity_service.py
@@ -108,24 +108,57 @@ def list_equity_positions(conn: Connection) -> list[EquityPosition]:
         and row.get("instrument_status", "").lower() != "inactive"
     ]
     analysis, values, missing = _latest_analysis(conn)
-    use_cache_fallback = analysis is None
-    expected_keys = {(str(row.get("account_id") or ""), str(row.get("instrument_id") or "")) for row in source_rows}
-    complete = bool(analysis and analysis.get("quality_status") == "complete" and set(values) == expected_keys)
-    total = sum((Decimal(str(item.get("value_chf") or "0")) for item in values.values()), Decimal("0"))
+    effective_values: dict[tuple[str, str], Decimal | None] = {}
+    for row in source_rows:
+        key = (str(row.get("account_id") or ""), str(row.get("instrument_id") or ""))
+        candidate = values.get(key)
+        row_is_audited = row.get("valuation_source") in {
+            "postfinance_official_import",
+            "audited_market_run",
+        }
+        candidate_is_newer = bool(
+            analysis
+            and candidate
+            and str(analysis.get("as_of") or "") > str(row.get("valuation_as_of") or row.get("price_date") or "")
+            and candidate.get("quality_status") == "fresh"
+        )
+        raw = (
+            candidate.get("value_chf")
+            if candidate_is_newer and candidate
+            else row.get("market_value_chf") if analysis is None or row_is_audited else None
+        )
+        effective_values[key] = Decimal(str(raw)) if raw not in {None, ""} else None
+    complete = bool(source_rows) and all(value is not None for value in effective_values.values())
+    total = sum((value or Decimal("0") for value in effective_values.values()), Decimal("0"))
     positions: list[EquityPosition] = []
     for row in source_rows:
         account_id = str(row.get("account_id") or "")
         instrument_id = str(row.get("instrument_id") or "")
         snapshot_value = values.get((account_id, instrument_id))
-        value = snapshot_value if snapshot_value and snapshot_value.get("quality_status") == "fresh" else None
+        value = (
+            snapshot_value
+            if analysis
+            and snapshot_value
+            and snapshot_value.get("quality_status") == "fresh"
+            and str(analysis.get("as_of") or "") > str(row.get("valuation_as_of") or row.get("price_date") or "")
+            else None
+        )
+        row_fallback_allowed = analysis is None or row.get("valuation_source") in {
+            "postfinance_official_import",
+            "audited_market_run",
+        }
         mapping = _mapping_details(conn, instrument_id)
-        raw_market_value = value.get("value_chf") if value else (row.get("market_value_chf") if use_cache_fallback else None)
-        if use_cache_fallback and Decimal(str(row.get("quantity") or "0")) <= 0:
+        raw_market_value = (
+            value.get("value_chf")
+            if value
+            else row.get("market_value_chf") if row_fallback_allowed else None
+        )
+        if Decimal(str(row.get("quantity") or "0")) <= 0:
             raw_market_value = None
         market_value = Decimal(str(raw_market_value)) if raw_market_value not in {None, ""} else None
         share = (market_value / total * Decimal("100")) if complete and market_value is not None and total > 0 else None
         code = "valued" if market_value is not None else ("stale_price" if snapshot_value else missing.get(instrument_id, "price_missing"))
-        status = "Bewertet" if value else ("Kurs veraltet" if snapshot_value else (_position_status(row) if use_cache_fallback else "Kurs fehlt"))
+        status = "Bewertet" if market_value is not None else ("Kurs veraltet" if snapshot_value else _position_status(row))
         provenance = value.get("price_input_provenance") if value else None
         provenance_provider = provenance.get("provider") if isinstance(provenance, dict) else None
         positions.append(EquityPosition(
@@ -138,11 +171,23 @@ def list_equity_positions(conn: Connection) -> list[EquityPosition]:
             asset_class=_asset_class_label(row.get("asset_class")),
             quantity=optional_decimal_text(row.get("quantity")) or "0",
             currency=str(value.get("currency") if value else mapping.get("currency") or row.get("price_currency") or row.get("currency") or ""),
-            price=optional_decimal_text(value.get("close")) if value else (optional_decimal_text(row.get("market_price_original") or row.get("price")) if use_cache_fallback else None),
+            price=(
+                optional_decimal_text(value.get("close"))
+                if value
+                else optional_decimal_text(row.get("market_price_original") or row.get("price"))
+                if row_fallback_allowed
+                else None
+            ),
             market_value_chf=optional_decimal_text(market_value, 2) if market_value is not None else None,
             portfolio_share_pct=optional_decimal_text(share, 2) if share is not None else None,
-            price_date=str(value.get("as_of")) if value else (str(analysis.get("as_of")) if analysis else None),
-            provider=str((value.get("provider") if value else None) or provenance_provider or mapping.get("provider") or ""),
+            price_date=(
+                str(analysis.get("as_of"))
+                if value and analysis
+                else str(row.get("valuation_as_of") or row.get("price_date") or "") or None
+                if row_fallback_allowed
+                else str(analysis.get("as_of")) if analysis else None
+            ),
+            provider=str((value.get("provider") if value else None) or provenance_provider or row.get("price_provider") or mapping.get("provider") or ""),
             provider_symbol=str(mapping.get("provider_symbol") or ""),
             exchange=str(mapping.get("provider_market") or ""),
             price_currency=str(value.get("currency") if value else mapping.get("currency") or row.get("price_currency") or row.get("currency") or ""),
@@ -161,6 +206,14 @@ def get_equity_summary(conn: Connection) -> EquityPortfolioSummary:
     complete = bool(positions) and len(valued) == len(positions)
     analysis, _, _ = _latest_analysis(conn)
     status = "complete" if complete else ("partial" if positions else "unavailable")
+    effective_as_of = max((item.price_date or "" for item in valued), default="") or None
+    analysis_as_of = str(analysis.get("as_of") or "") if analysis else ""
+    response_as_of = effective_as_of or analysis_as_of or None
+    last_successful = (
+        str(analysis.get("completed_at"))
+        if analysis and (not effective_as_of or analysis_as_of >= str(effective_as_of)[:10])
+        else effective_as_of
+    )
     return EquityPortfolioSummary(
         total_positions=len(positions),
         valued_positions=len(valued),
@@ -169,8 +222,8 @@ def get_equity_summary(conn: Connection) -> EquityPortfolioSummary:
         equity_value_chf=optional_decimal_text(partial, 2) if complete else None,
         valued_partial_chf=optional_decimal_text(partial, 2) or "0.00",
         value_label="Aktien-/ETF-Gesamtwert" if complete else "Bewerteter Teilwert",
-        as_of=str(analysis.get("as_of")) if analysis else None,
-        last_successful_run=str(analysis.get("completed_at")) if analysis else None,
+        as_of=response_as_of,
+        last_successful_run=last_successful,
         status=status,
         status_label=f"{len(valued)}/{len(positions)} bewertet" if positions else "Keine Aktien-/ETF-Positionen",
         unvalued_tickers=[item.ticker for item in positions if item.market_value_chf is None],
diff --git a/src/jarvis_finance/services/portfolio_analytics.py b/src/jarvis_finance/services/portfolio_analytics.py
index f6acbd6..5331de3 100644
--- a/src/jarvis_finance/services/portfolio_analytics.py
+++ b/src/jarvis_finance/services/portfolio_analytics.py
@@ -25,6 +25,11 @@ from jarvis_finance.market_data.prices import (
     exchange_matches,
     store_market_price,
 )
+from jarvis_finance.services.portfolio_aggregation import (
+    latest_official_postfinance_positions,
+    post_snapshot_postfinance_quantity_deltas,
+    postfinance_account_roles,
+)
 from jarvis_finance.services.portfolio_performance import build_portfolio_performance

 ZERO = Decimal("0")
@@ -224,6 +229,43 @@ def confirmed_canonical_positions(conn: Connection, *, as_of: str | None = None)
         item = grouped.setdefault(key, dict(row) | {"quantity_total": ZERO})
         quantity = _decimal(row["quantity"])
         item["quantity_total"] += -abs(quantity) if str(row["transaction_type"]).lower() in SELL_TYPES else quantity
+    official = latest_official_postfinance_positions(conn, as_of=as_of)
+    post_snapshot_deltas = post_snapshot_postfinance_quantity_deltas(
+        conn,
+        official,
+        as_of=as_of,
+    )
+    trading_cash_id = postfinance_account_roles(conn).get("etrading_cash")
+    official_instruments = {key[1] for key in official}
+    for key in list(grouped):
+        if key[0] == trading_cash_id and key[1] in official_instruments:
+            grouped.pop(key)
+    for key, override in official.items():
+        item = grouped.get(key)
+        if item is None:
+            instrument = conn.execute(
+                """SELECT isin,name,lower(asset_class) asset_class,
+                          upper(COALESCE(trading_currency,currency,'')) instrument_currency
+                   FROM instruments WHERE instrument_id=? AND is_active=1""",
+                (override.instrument_id,),
+            ).fetchone()
+            if not instrument:
+                continue
+            item = {
+                "account_id": override.account_id,
+                "instrument_id": override.instrument_id,
+                "isin": instrument["isin"],
+                "name": instrument["name"],
+                "asset_class": instrument["asset_class"],
+                "instrument_currency": instrument["instrument_currency"],
+            }
+            grouped[key] = item
+        # The official snapshot is the baseline. Only confirmed trades after that
+        # snapshot are replayed, so imported history is not counted twice.
+        item["quantity_total"] = override.quantity + post_snapshot_deltas.get(
+            override.instrument_id,
+            ZERO,
+        )
     return [
         ConfirmedPosition(
             account_id=item["account_id"],
diff --git a/src/jarvis_finance/services/portfolio_data.py b/src/jarvis_finance/services/portfolio_data.py
index 7a5762e..a80d646 100644
--- a/src/jarvis_finance/services/portfolio_data.py
+++ b/src/jarvis_finance/services/portfolio_data.py
@@ -6,27 +6,33 @@ reconciliation are pure database projections. Confirm is the only write boundary

 from __future__ import annotations

-from dataclasses import dataclass
-from datetime import UTC, date, datetime, timedelta
-from decimal import Decimal, InvalidOperation
 import base64
 import binascii
 import hashlib
 import json
 import re
+import uuid
+from dataclasses import dataclass
+from datetime import UTC, date, datetime, timedelta
+from decimal import Decimal, InvalidOperation
 from sqlite3 import Connection, IntegrityError
 from typing import Any
-import uuid

 from jarvis_finance.audit.log import record_audit_event
+from jarvis_finance.imports.common import stable_id
 from jarvis_finance.imports.postfinance_etrading import (
     PostFinanceDocument,
-    decimal_text as postfinance_decimal_text,
     normalize_label,
     parse_postfinance_portfolio,
 )
-from jarvis_finance.imports.common import stable_id
+from jarvis_finance.imports.postfinance_etrading import (
+    decimal_text as postfinance_decimal_text,
+)
 from jarvis_finance.market_data.mappings import confirm_instrument_price_mapping
+from jarvis_finance.services.portfolio_aggregation import (
+    latest_official_postfinance_cash,
+    postfinance_account_roles,
+)

 INGESTION_VERSION = "portfolio_ingestion_v1"
 RECONCILIATION_VERSION = "portfolio_reconciliation_v1"
@@ -1434,7 +1440,8 @@ def _latest_fx(conn: Connection, currency: str, as_of: str, cutoff: str) -> tupl
         return Decimal("1"), as_of, "identity"
     row = conn.execute(
         """SELECT rate, rate_date, provider FROM fx_rates
-           WHERE base_currency=? AND quote_currency='CHF' AND rate_date<=? AND created_at<=?
+           WHERE base_currency=? AND quote_currency='CHF' AND rate_date<=?
+             AND datetime(created_at)<=datetime(?)
              AND rate IS NOT NULL AND quality_status IN ('ok','fresh','complete')
            ORDER BY rate_date DESC, created_at DESC, fx_rate_id DESC LIMIT 1""", (currency, as_of, cutoff)
     ).fetchone()
@@ -1463,7 +1470,8 @@ def build_portfolio_reconciliation(
         return {"as_of": as_of, "data_cutoff": cutoff, "base_currency": "CHF", "status": "unavailable", "reason_codes": ["missing_account_valuation"], "tolerance": {"version": TOLERANCE_VERSION, "absolute": str(absolute), "relative": str(relative)}, "coverage": empty_coverage, "differences": [], "account_totals": [], "input_fingerprint": _hash([as_of, cutoff, []]), "sources": [], "limit": limit, "offset": offset, "total": 0}
     placeholders = ",".join("?" for _ in account_map)
     transaction_rows = conn.execute(
-        f"""SELECT * FROM transactions WHERE account_id IN ({placeholders}) AND trade_date<=? AND created_at<=?
+        f"""SELECT * FROM transactions WHERE account_id IN ({placeholders}) AND trade_date<=?
+            AND datetime(created_at)<=datetime(?)
             AND COALESCE(is_voided,0)=0 AND COALESCE(is_confirmed,1)=1
             ORDER BY trade_date, created_at, transaction_id""", (*account_map, as_of, cutoff)
     ).fetchall()
@@ -1490,7 +1498,8 @@ def build_portfolio_reconciliation(
     reported_rows = conn.execute(
         f"""WITH ranked AS (
               SELECT p.*, ROW_NUMBER() OVER(PARTITION BY account_id,instrument_id ORDER BY snapshot_date DESC,created_at DESC,position_snapshot_id DESC) rn
-              FROM positions_snapshot p WHERE account_id IN ({placeholders}) AND snapshot_date<=? AND created_at<=?
+              FROM positions_snapshot p WHERE account_id IN ({placeholders}) AND snapshot_date<=?
+                AND datetime(created_at)<=datetime(?)
             ) SELECT * FROM ranked WHERE rn=1""", (*account_map, as_of, cutoff)
     ).fetchall()
     reported = {(str(row["account_id"]), str(row["instrument_id"])): row for row in reported_rows}
@@ -1523,7 +1532,8 @@ def build_portfolio_reconciliation(
         derived_value: Decimal | None = None
         reported_value = _stored_decimal(source["market_value_chf"], "Gemeldeter Positionswert") if source and source["market_value_chf"] not in (None, "") else None
         price_row = conn.execute(
-            """SELECT close,currency,price_date,provider FROM market_prices WHERE instrument_id=? AND price_date<=? AND created_at<=?
+            """SELECT close,currency,price_date,provider FROM market_prices WHERE instrument_id=? AND price_date<=?
+               AND datetime(created_at)<=datetime(?)
                AND quality_status IN ('ok','fresh','complete') ORDER BY price_date DESC,created_at DESC,market_price_id DESC LIMIT 1""", (instrument, as_of, cutoff)
         ).fetchone()
         price_date = str(price_row["price_date"]) if price_row else None
@@ -1586,18 +1596,37 @@ def build_portfolio_reconciliation(
                           "position_as_of": str(source["snapshot_date"]) if source else None, "price_as_of": price_date, "fx_as_of": fx_date, "price_source": price_source, "fx_source": fx_source},
             "reason_codes": sorted(set(reasons)),
         })
+    pf_roles = postfinance_account_roles(conn)
+    official_pf_cash = latest_official_postfinance_cash(
+        conn,
+        as_of=as_of,
+        data_cutoff=cutoff,
+    )
+    official_pf_cash_total = sum(
+        (component.amount_chf for component in official_pf_cash), Decimal("0")
+    )
+    official_pf_cash_date = max(
+        (component.snapshot_date for component in official_pf_cash), default=None
+    )
     account_totals: list[dict[str, Any]] = []
     account_covered = 0
     for account, meta in account_map.items():
         cash = conn.execute(
-            """SELECT amount_chf,balance_date,source FROM cash_account_snapshots WHERE account_id=? AND balance_date<=? AND created_at<=?
+            """SELECT amount_chf,balance_date,source FROM cash_account_snapshots WHERE account_id=? AND balance_date<=?
+               AND datetime(created_at)<=datetime(?)
                ORDER BY balance_date DESC,created_at DESC,snapshot_id DESC LIMIT 1""", (account, as_of, cutoff)
         ).fetchone()
         cash_value = _stored_decimal(cash["amount_chf"], "Cashbestand") if cash else None
         cash_date = str(cash["balance_date"]) if cash else None
+        if account == pf_roles.get("etrading_depot") and official_pf_cash:
+            # The provider account total spans the distinct depot and trading-cash
+            # roles. This is a reconciliation-only join, not an additive holding.
+            cash_value = official_pf_cash_total
+            cash_date = official_pf_cash_date
         legacy = conn.execute(
             """SELECT total_value_chf,valuation_date,source_type FROM account_value_snapshots
-               WHERE account_id=? AND valuation_date<=? AND created_at<=? AND COALESCE(is_active,1)=1
+               WHERE account_id=? AND valuation_date<=? AND datetime(created_at)<=datetime(?)
+                 AND COALESCE(is_active,1)=1
                ORDER BY valuation_date DESC,
                         CASE source_type WHEN 'truewealth_official_import' THEN 3 WHEN 'truewealth_manual_provisional' THEN 2 ELSE 1 END DESC,
                         COALESCE(valuation_at,created_at) DESC,created_at DESC,snapshot_id DESC LIMIT 1""", (account, as_of, cutoff)
@@ -1614,7 +1643,8 @@ def build_portfolio_reconciliation(
         if total_value_only:
             canonical = conn.execute(
                 """SELECT value_original,valuation_at FROM portfolio_valuation_snapshots
-                   WHERE scope_kind='account' AND scope_id=? AND substr(valuation_at,1,10)<=? AND captured_at<=?
+                   WHERE scope_kind='account' AND scope_id=? AND substr(valuation_at,1,10)<=?
+                     AND datetime(captured_at)<=datetime(?)
                    ORDER BY valuation_at DESC,snapshot_version DESC,captured_at DESC,snapshot_id DESC LIMIT 1""", (account, as_of, cutoff)
             ).fetchone()
             derived_total = _stored_decimal(canonical["value_original"], "Kanonischer Totalwert") if canonical else None
@@ -1651,10 +1681,10 @@ def build_portfolio_reconciliation(
         return {"covered": covered, "total": total, "ratio": _decimal_text(Decimal(covered) / Decimal(total)) if total else None}
     instrument_total = len(keys)
     opening_rows = conn.execute(
-        f"SELECT COUNT(DISTINCT account_id) AS count FROM portfolio_valuation_snapshots WHERE account_id IN ({placeholders}) AND substr(valuation_at,1,10)<? AND captured_at<=?", (*account_map, as_of, cutoff)
+        f"SELECT COUNT(DISTINCT account_id) AS count FROM portfolio_valuation_snapshots WHERE account_id IN ({placeholders}) AND substr(valuation_at,1,10)<? AND datetime(captured_at)<=datetime(?)", (*account_map, as_of, cutoff)
     ).fetchone()
     closing_rows = conn.execute(
-        f"SELECT COUNT(DISTINCT account_id) AS count FROM portfolio_valuation_snapshots WHERE account_id IN ({placeholders}) AND substr(valuation_at,1,10)=? AND captured_at<=?", (*account_map, as_of, cutoff)
+        f"SELECT COUNT(DISTINCT account_id) AS count FROM portfolio_valuation_snapshots WHERE account_id IN ({placeholders}) AND substr(valuation_at,1,10)=? AND datetime(captured_at)<=datetime(?)", (*account_map, as_of, cutoff)
     ).fetchone()
     opening_covered = int(opening_rows["count"] or 0)
     closing_covered = int(closing_rows["count"] or 0)
diff --git a/src/jarvis_finance/services/postfinance_service.py b/src/jarvis_finance/services/postfinance_service.py
index 9ea4ba8..b34d319 100644
--- a/src/jarvis_finance/services/postfinance_service.py
+++ b/src/jarvis_finance/services/postfinance_service.py
@@ -2,12 +2,12 @@ from __future__ import annotations

 import base64
 import binascii
+import json
+import os
 from collections import defaultdict
 from datetime import UTC, datetime
 from decimal import Decimal
 from hashlib import sha256
-import json
-import os
 from pathlib import Path
 from sqlite3 import Connection, IntegrityError
 from typing import Any
@@ -22,6 +22,10 @@ from jarvis_finance.imports.postfinance_documents import (
     normalize_label,
     parse_postfinance_bundle,
 )
+from jarvis_finance.services.portfolio_aggregation import (
+    latest_official_postfinance_cash,
+    latest_official_postfinance_positions,
+)

 TOLERANCE = Decimal("0.05")

@@ -876,6 +880,10 @@ def get_postfinance_summary(conn: Connection) -> dict[str, Any]:
             "event_counts": {},
             "imports": [],
         }
+    official_position_values = {
+        override.instrument_id: override.market_value_chf
+        for override in latest_official_postfinance_positions(conn).values()
+    }
     positions = [
         {
             "row_key": _hash("position", row["instrument_name"], row["quantity"])[:16],
@@ -887,13 +895,13 @@ def get_postfinance_summary(conn: Connection) -> dict[str, Any]:
             "provider_cost_total_original": row["provider_cost_total_original"],
             "price_original": row["price_original"],
             "price_currency": row["price_currency"],
-            "value_chf": row["value_chf"],
+            "value_chf": _money(official_position_values.get(row["instrument_id"], Decimal(str(row["value_chf"])))),
             "weight_pct": row["weight_pct"],
             "computed_cost_basis_original": row["computed_cost_basis_original"],
             "computed_cost_basis_status": row["computed_cost_basis_status"],
         }
         for row in conn.execute(
-            """SELECT p.asset_class,p.quantity,p.provider_average_cost_original,
+            """SELECT p.instrument_id,p.asset_class,p.quantity,p.provider_average_cost_original,
                       p.provider_cost_total_original,p.price_original,p.price_currency,p.value_chf,
                       p.weight_pct,p.computed_cost_basis_original,p.computed_cost_basis_status,
                       i.name AS instrument_name,i.ticker
@@ -905,17 +913,13 @@ def get_postfinance_summary(conn: Connection) -> dict[str, Any]:
     ]
     cash = [
         {
-            "row_key": _hash("cash", row["currency"])[:16],
-            "currency": row["currency"],
-            "amount_original": row["amount_original"],
-            "fx_rate_to_chf": row["fx_rate_to_chf"],
-            "value_chf": row["value_chf"],
+            "row_key": _hash("cash", component.currency)[:16],
+            "currency": component.currency,
+            "amount_original": _text(component.amount_original),
+            "fx_rate_to_chf": _text(component.source_fx_rate_to_chf),
+            "value_chf": _money(component.amount_chf),
         }
-        for row in conn.execute(
-            """SELECT currency,amount_original,fx_rate_to_chf,value_chf
-               FROM postfinance_snapshot_cash WHERE snapshot_id=? ORDER BY currency""",
-            (latest["snapshot_id"],),
-        ).fetchall()
+        for component in latest_official_postfinance_cash(conn)
     ]
     events = {
         str(row["event_type"]): int(row["c"])


cat src/jarvis_finance/services/portfolio_aggregation.py && grep -R "postfinance_account_roles\|postfinance_snapshot" -n src/jarvis_finance/storage/migrations | head -80