Output:
Chunk ID: 36994b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4131
Output:
            "last_successful_confirmed_at": batch["confirmed_at"] if batch else None,
            "last_attempt_at": (attempt["finished_at"] if attempt else None) or (batch["confirmed_at"] if batch else None),
            "last_attempt_status": (attempt["status"] if attempt else None) or (batch["status"] if batch else None),
            "known_gaps": ["Keine neue externe Provider-Abfrage", "Keine automatische Ingestion beim Start"],
        })
    fingerprint = _hash([[row["source_key"], row["record_count"], row["last_successful_confirmed_at"]] for row in rows])
    return {"sources": rows, "data_cutoff": _now().isoformat(), "input_fingerprint": fingerprint}

def ingestion_history(conn: Connection, *, limit: int = 50, offset: int = 0) -> dict[str, Any]:
    if limit < 1 or limit > 100 or offset < 0:
        raise ValueError("Pagination ist ungültig")
    total = int(conn.execute("SELECT COUNT(*) AS count FROM portfolio_ingestion_batches").fetchone()["count"])
    rows = conn.execute(
        """SELECT batch_id, source_key, scope_kind, scope_id, period_from, period_to, data_cutoff,
                  input_fingerprint, status, counts_json, confirmed_at
           FROM portfolio_ingestion_batches ORDER BY confirmed_at DESC LIMIT ? OFFSET ?""", (limit, offset)
    ).fetchall()
    return {
        "items": [{key: value for key, value in {
                    **dict(row), "batch_id": _safe_ref("batch", str(row["batch_id"])),
                    "scope_id": _safe_ref("account", str(row["scope_id"])) if row["scope_id"] else None,
                    "counts": json.loads(row["counts_json"]),
                  }.items() if key != "counts_json"} for row in rows],
        "limit": limit, "offset": offset, "total": total,
    }

def _status(diff: Decimal, base: Decimal | None, absolute: Decimal, relative: Decimal) -> str:
    if diff == 0:
        return "matched"
    if abs(diff) <= absolute:
        return "within_tolerance"
    if base is not None and base != 0 and abs(diff / base) <= relative:
        return "within_tolerance"
    return "mismatch"

def _latest_fx(conn: Connection, currency: str, as_of: str, cutoff: str) -> tuple[Decimal | None, str | None, str | None]:
    if currency == "CHF":
        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 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()
    if not row:
        return None, None, None
    return _stored_decimal(row["rate"], "FX", positive=True), str(row["rate_date"]), str(row["provider"])

def build_portfolio_reconciliation(
    conn: Connection, *, as_of: str, data_cutoff: str | None = None, account_id: str | None = None,
    base_currency: str = "CHF", absolute_tolerance: str = "0.01", relative_tolerance: str = "0.001",
    limit: int = 200, offset: int = 0,
) -> dict[str, Any]:
    _date(as_of, "Stichtag")
    cutoff = _instant(data_cutoff or _now().isoformat(), "Data-Cutoff").isoformat()
    if base_currency.upper() != "CHF":
        raise ValueError("Reconciliation v1 unterstützt als Basiswährung ausschliesslich CHF")
    absolute = _decimal(absolute_tolerance, "Absolute Toleranz", non_negative=True)
    relative = _decimal(relative_tolerance, "Relative Toleranz", non_negative=True)
    if limit < 1 or limit > 500 or offset < 0:
        raise ValueError("Pagination ist ungültig")
    accounts = _validate_scope(conn, "account" if account_id else "portfolio", account_id)
    account_map = {row["account_id"]: row for row in accounts}
    if not account_map:
        empty_coverage = {key: {"covered": 0, "total": 0, "ratio": None} for key in ("accounts", "instruments", "activities", "opening_valuations", "closing_valuations", "prices", "fx", "cost_basis", "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 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()
    quantities: dict[tuple[str, str], Decimal] = {}
    activity_supported = 0
    cost_basis_keys: set[tuple[str, str]] = set()
    for row in transaction_rows:
        kind = str(row["activity_kind"] or row["transaction_type"] or "").lower()
        if kind in SUPPORTED_ACTIVITIES:
            activity_supported += 1
        if not row["instrument_id"] or row["quantity"] in (None, ""):
            continue
        key = (str(row["account_id"]), str(row["instrument_id"]))
        try:
            quantity = _stored_decimal(row["quantity"], "Menge", non_negative=True)
        except ValueError:
            continue
        if kind in {"buy", "initial_position_snapshot"}:
            quantities[key] = quantities.get(key, Decimal("0")) + quantity
            if row["gross_amount_original"] not in (None, ""):
                cost_basis_keys.add(key)
        elif kind in {"partial_sell", "full_sell", "sell"}:
            quantities[key] = quantities.get(key, Decimal("0")) - quantity
    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 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}
    keys = sorted(set(quantities) | set(reported))
    differences: list[dict[str, Any]] = []
    price_covered = fx_covered = recon_covered = 0
    account_position_values: dict[str, Decimal] = {key: Decimal("0") for key in account_map}
    account_position_dates: dict[str, set[str]] = {key: set() for key in account_map}
    account_position_dates: dict[str, set[str]] = {key: set() for key in account_map}
    for account, instrument in keys:
        ledger_qty = quantities.get((account, instrument))
        source = reported.get((account, instrument))
        source_qty = _stored_decimal(source["quantity"], "Gemeldete Menge") if source and source["quantity"] not in (None, "") else None
        reasons: list[str] = []
        if source and str(source["snapshot_date"]) != as_of:
            reasons.append("stale_snapshot")
        if source and str(source["data_quality_status"] or "").lower() not in {"ok", "fresh", "complete"}:
            reasons.append("source_error")
        quantity_status = "unavailable"
        quantity_diff: Decimal | None = None
        if ledger_qty is None or source_qty is None:
            reasons.append("missing_position_snapshot")
        else:
            quantity_diff = ledger_qty - source_qty
            if source and str(source["snapshot_date"]) != as_of:
                quantity_status = "not_comparable"
            else:
                quantity_status = _status(quantity_diff, source_qty, absolute, relative)
                if quantity_status == "mismatch":
                    reasons.append("quantity_mismatch")
        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 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
        price_source = str(price_row["provider"]) if price_row else None
        fx_date = fx_source = None
        if source and price_row and str(source["snapshot_date"]) != price_date:
            reasons.append("cutoff_mismatch")
        if ledger_qty is None:
            reasons.append("missing_position_snapshot")
        elif not price_row:
            reasons.append("missing_price")
        else:
            if price_date != as_of:
                reasons.append("stale_price")
            else:
                price_covered += 1
            price = _stored_decimal(price_row["close"], "Marktpreis", non_negative=True)
            fx, fx_date, fx_source = _latest_fx(conn, str(price_row["currency"]).upper(), as_of, cutoff)
            if fx is None:
                reasons.append("missing_fx")
            else:
                if fx_date != as_of:
                    reasons.append("stale_fx")
                else:
                    fx_covered += 1
                derived_value = ledger_qty * price * fx
        value_status = "unavailable"
        value_diff: Decimal | None = None
        relative_diff: Decimal | None = None
        if derived_value is not None and reported_value is not None:
            if source and (
                "cutoff_mismatch" in reasons
                or "stale_snapshot" in reasons
                or "stale_price" in reasons
                or "stale_fx" in reasons
                or (fx_date and fx_date != price_date)
            ):
                value_status = "not_comparable"
                if "cutoff_mismatch" not in reasons:
                    reasons.append("cutoff_mismatch")
            else:
                value_diff = derived_value - reported_value
                relative_diff = value_diff / reported_value if reported_value != 0 else None
                value_status = _status(value_diff, reported_value, absolute, relative)
                if value_status == "mismatch":
                    reasons.append("value_mismatch")
                recon_covered += 1
        elif reported_value is None:
            reasons.append("missing_position_snapshot")
        if source and reported_value is not None:
            account_position_values[account] += reported_value
            account_position_dates[account].add(str(source["snapshot_date"]))
        instrument_row = conn.execute("SELECT name FROM instruments WHERE instrument_id=?", (instrument,)).fetchone()
        differences.append({
            "difference_ref": _safe_ref("difference", f"{account}:{instrument}:{as_of}"),
            "account_ref": _safe_ref("account", account), "account_label": _safe_label(account_map[account]["account_name"]),
            "instrument_ref": _safe_ref("instrument", instrument), "instrument_label": str(instrument_row["name"]) if instrument_row else "Instrument (unbekannt)",
            "quantity": {"ledger": _decimal_text(ledger_qty), "reported": _decimal_text(source_qty), "difference": _decimal_text(quantity_diff), "status": quantity_status},
            "valuation": {"derived": _decimal_text(derived_value), "reported": _decimal_text(reported_value), "difference": _decimal_text(value_diff), "relative_difference": _decimal_text(relative_diff), "status": value_status,
                          "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 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 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)
        ).fetchone()
        reported_total = _stored_decimal(legacy["total_value_chf"], "Kontototal") if legacy else None
        total_date = str(legacy["valuation_date"]) if legacy else None
        has_positions = any(key[0] == account for key in reported)
        derived_total = account_position_values[account] + (cash_value or Decimal("0")) if has_positions or cash else None
        reasons: list[str] = []
        total_status = "unavailable"
        diff: Decimal | None = None
        total_value_only = meta["account_type"] in {"robo_advisor", "total_value", "pension", "truewealth"} or (legacy is not None and not any(key[0] == account for key in keys))
        canonical_date: str | None = None
        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 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
            canonical_date = str(canonical["valuation_at"])[:10] if canonical else None
            if canonical and total_date != canonical_date:
                reasons.append("cutoff_mismatch")
        dates = set(account_position_dates[account]) | ({cash_date} if cash_date else set()) | ({total_date} if total_date else set()) | ({canonical_date} if canonical_date else set())
        if derived_total is None:
            reasons.append("missing_position_snapshot" if not total_value_only else "missing_account_valuation")
        if reported_total is None:
            reasons.append("missing_account_valuation")
        if derived_total is not None and reported_total is not None:
            stale_dates = {item for item in dates if item and item != as_of}
            if len({item for item in dates if item}) > 1 or stale_dates or "cutoff_mismatch" in reasons:
                total_status = "not_comparable"
                if stale_dates:
                    reasons.append("stale_snapshot")
                if "cutoff_mismatch" not in reasons:


Output:
Chunk ID: 0e4bde
Wall time: 30.0013 seconds
Process running with session ID 51776
Original token count: 0
Output:


sed -n '1,180p' src/jarvis_finance/ledger/positions.py


Output:
Chunk ID: 073f0c
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1666
Output:
from __future__ import annotations

from dataclasses import dataclass, field
from datetime import datetime, timezone
from decimal import Decimal, ROUND_HALF_UP
from sqlite3 import Connection

from jarvis_finance.audit.log import record_audit_event
from jarvis_finance.imports.common import stable_id, utc_now
from jarvis_finance.quality.alerts import create_alert
from jarvis_finance.fx.rates import latest_fx_rate

from .cost_basis import WeightedAverageLot

MONEY = Decimal("0.01")

def _d(value: object, default: str = "0") -> Decimal:
    if value is None or str(value).strip() == "":
        return Decimal(default)
    return Decimal(str(value))

def _money(value: Decimal | None) -> Decimal | None:
    if value is None:
        return None
    return value.quantize(MONEY, rounding=ROUND_HALF_UP)

@dataclass
class Position:
    account_id: str
    instrument_id: str
    quantity: Decimal = Decimal("0")
    cost_basis_original: Decimal = Decimal("0")
    cost_basis_chf: Decimal = Decimal("0")
    average_cost_original: Decimal | None = None
    average_cost_chf: Decimal | None = None
    realized_pnl_chf: Decimal = Decimal("0")
    income_chf: Decimal = Decimal("0")
    fees_chf: Decimal = Decimal("0")
    taxes_chf: Decimal = Decimal("0")
    market_price_original: Decimal | None = None
    market_value_original: Decimal | None = None
    market_value_chf: Decimal | None = None
    unrealized_pnl_chf: Decimal | None = None
    total_return_chf: Decimal | None = None
    data_quality_status: str = "ok"
    valuation_status: str = "not_valuable"
    valuation_timestamp_status: str = "missing_timestamp"
    hedge_status: str = "unknown"
    instrument_status: str = "unknown"
    valuation_policy: str = "live_price"
    corporate_action_status: str = "not_checked"
    quality_warnings: list[str] = field(default_factory=list)

@dataclass
class PositionCalculationResult:
    positions: dict[tuple[str, str], Position] = field(default_factory=dict)
    as_of_date: str | None = None

def _latest_market_price(conn: Connection, instrument_id: str, as_of_date: str | None):
    if as_of_date:
        return conn.execute(
            """
            SELECT close, currency, price_timestamp, price_date, corporate_action_status FROM market_prices
            WHERE instrument_id=? AND price_date <= ? AND quality_status IN ('fresh','ok')
            ORDER BY price_date DESC, created_at DESC LIMIT 1
            """,
            (instrument_id, as_of_date),
        ).fetchone()
    return conn.execute(
        """
        SELECT close, currency, price_timestamp, price_date, corporate_action_status FROM market_prices
        WHERE instrument_id=? AND quality_status IN ('fresh','ok')
        ORDER BY price_date DESC, created_at DESC LIMIT 1
        """,
        (instrument_id,),
    ).fetchone()

def _add_warning(
    conn: Connection,
    pos: Position,
    rule_id: str,
    message: str,
    *,
    persist: bool = True,
) -> None:
    if rule_id not in pos.quality_warnings:
        pos.quality_warnings.append(rule_id)
    pos.data_quality_status = "incomplete"
    if persist:
        create_alert(
            conn,
            priority="kritisch" if rule_id == "missing_fx" else "warnung",
            category="ledger",
            entity_type="instrument",
            entity_id=pos.instrument_id,
            rule_id=rule_id,
            message=message,
            evidence={"account_id": pos.account_id, "instrument_id": pos.instrument_id},
        )

def _timestamp_status(price_ts: str | None, fx_ts: str | None, *, max_delta_hours: int = 24) -> str:
    if not price_ts or not fx_ts:
        return "missing_timestamp"
    try:
        p = datetime.fromisoformat(price_ts.replace("Z", "+00:00"))
        f = datetime.fromisoformat(fx_ts.replace("Z", "+00:00"))
        if p.tzinfo is None:
            p = p.replace(tzinfo=timezone.utc)
        if f.tzinfo is None:
            f = f.replace(tzinfo=timezone.utc)
    except ValueError:
        return "missing_timestamp"
    delta_hours = abs((p - f).total_seconds()) / 3600
    if delta_hours <= 1:
        return "aligned"
    if delta_hours <= max_delta_hours:
        return "acceptable"
    return "stale_mismatch"

def _instrument_metadata(conn: Connection, instrument_id: str) -> dict[str, object]:
    row = conn.execute("SELECT * FROM instruments WHERE instrument_id=?", (instrument_id,)).fetchone()
    if not row:
        return {"hedge_status": "unknown", "instrument_status": "unknown", "valuation_policy": "live_price", "corporate_action_status": "not_checked"}
    return dict(row)

def calculate_positions(
    conn: Connection,
    *,
    as_of_date: str | None = None,
    max_price_fx_time_delta_hours: int = 24,
    persist_alerts: bool = True,
) -> PositionCalculationResult:
    def add_warning(pos: Position, rule_id: str, message: str) -> None:
        _add_warning(conn, pos, rule_id, message, persist=persist_alerts)

    result = PositionCalculationResult(as_of_date=as_of_date)
    lots: dict[tuple[str, str], WeightedAverageLot] = {}
    params: tuple[object, ...] = ()
    where = "WHERE instrument_id IS NOT NULL AND COALESCE(is_voided, 0) = 0"
    if as_of_date:
        where += " AND trade_date <= ?"
        params = (as_of_date,)
    rows = conn.execute(
        f"""
        SELECT * FROM transactions
        {where}
        ORDER BY account_id, instrument_id, trade_date, created_at, transaction_id
        """,
        params,
    ).fetchall()
    for row in rows:
        key = (row["account_id"], row["instrument_id"])
        pos = result.positions.setdefault(key, Position(row["account_id"], row["instrument_id"]))
        lot = lots.setdefault(key, WeightedAverageLot())
        t = row["transaction_type"]
        qty = _d(row["quantity"])
        gross = _d(row["gross_amount_original"])
        fee = _d(row["fee_original"])
        tax = _d(row["tax_original"])
        fx = _d(row["fx_rate_to_chf"], "1") if row["fx_rate_to_chf"] is not None else None
        if fx is None or row["fx_status"] == "missing":
            add_warning(pos, "missing_fx", "Historic FX is missing; precise CHF total return cannot be calculated.")
        if t in {"initial_position_snapshot", "buy"}:
            if qty > 0:
                cost_original = gross + fee
                cost_chf = (gross + fee) * fx if fx is not None else Decimal("0")
                lot.buy(qty, cost_original, cost_chf)
                pos.fees_chf += fee * fx if fx is not None else Decimal("0")
                pos.taxes_chf += tax * fx if fx is not None else Decimal("0")
        elif t in {"partial_sell", "full_sell"}:
            if qty > 0:


Output:
Chunk ID: 0908f0
Wall time: 30.0018 seconds
Process running with session ID 51776
Original token count: 0
Output:


Output:
Chunk ID: db7efa
Wall time: 30.0010 seconds
Process running with session ID 51776
Original token count: 0
Output:


git diff --unified=0 -- src/jarvis_finance/services/portfolio_aggregation.py src/jarvis_finance/services/equity_service.py src/jarvis_finance/services/portfolio_data.py | head -200


Output:
Chunk ID: 530555
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2800
Output:
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
@@ -111,4 +111,22 @@ def list_equity_positions(conn: Connection) -> list[EquityPosition]:
-    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"))
@@ -120 +138,12 @@ def list_equity_positions(conn: Connection) -> list[EquityPosition]:
-        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",
+        }
@@ -122,2 +151,6 @@ def list_equity_positions(conn: Connection) -> list[EquityPosition]:
-        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:
@@ -128 +161 @@ def list_equity_positions(conn: Connection) -> list[EquityPosition]:
-        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))
@@ -141 +174,7 @@ def list_equity_positions(conn: Connection) -> list[EquityPosition]:
-            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
+            ),
@@ -144,2 +183,8 @@ def list_equity_positions(conn: Connection) -> list[EquityPosition]:
-            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 ""),
@@ -163,0 +209,8 @@ def get_equity_summary(conn: Connection) -> EquityPortfolioSummary:
+    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
+    )
@@ -172,2 +225,2 @@ def get_equity_summary(conn: Connection) -> EquityPortfolioSummary:
-        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,
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
@@ -9,3 +8,0 @@ from __future__ import annotations
-from dataclasses import dataclass
-from datetime import UTC, date, datetime, timedelta
-from decimal import Decimal, InvalidOperation
@@ -16,0 +14,4 @@ import re
+import uuid
+from dataclasses import dataclass
+from datetime import UTC, date, datetime, timedelta
+from decimal import Decimal, InvalidOperation
@@ -19 +19,0 @@ from typing import Any
-import uuid
@@ -21,0 +22 @@ from jarvis_finance.audit.log import record_audit_event
+from jarvis_finance.imports.common import stable_id
@@ -24 +24,0 @@ from jarvis_finance.imports.postfinance_etrading import (
-    decimal_text as postfinance_decimal_text,
@@ -28 +28,3 @@ from jarvis_finance.imports.postfinance_etrading import (
-from jarvis_finance.imports.common import stable_id
+from jarvis_finance.imports.postfinance_etrading import (
+    decimal_text as postfinance_decimal_text,
+)
@@ -29,0 +32,4 @@ 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,
+)
@@ -1437 +1443,2 @@ def _latest_fx(conn: Connection, currency: str, as_of: str, cutoff: str) -> tupl
-           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(?)
@@ -1466 +1473,2 @@ def build_portfolio_reconciliation(
-        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(?)
@@ -1493 +1501,2 @@ def build_portfolio_reconciliation(
-              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(?)
@@ -1526 +1535,2 @@ def build_portfolio_reconciliation(
-            """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(?)
@@ -1588,0 +1599,12 @@ def build_portfolio_reconciliation(
+    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
+    )
@@ -1593 +1615,2 @@ def build_portfolio_reconciliation(
-            """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(?)
@@ -1597,0 +1621,5 @@ def build_portfolio_reconciliation(
+        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
@@ -1600 +1628,2 @@ def build_portfolio_reconciliation(
-               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
@@ -1617 +1646,2 @@ def build_portfolio_reconciliation(
-                   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(?)
@@ -1654 +1684 @@ def build_portfolio_reconciliation(
-        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)
@@ -1657 +1687 @@ def build_portfolio_reconciliation(
-        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)


Output:
Chunk ID: 3cbd99
Wall time: 30.0013 seconds
Process running with session ID 51776
Original token count: 0
Output:


grep -R "source_value_chf" -n src/jarvis_finance/imports src/jarvis_finance/services tests | head -60


Output:
Chunk ID: 681b30
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 620
Output:
grep: src/jarvis_finance/imports/__pycache__/postfinance_documents.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/imports/__pycache__/truewealth_tax_statement.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/imports/__pycache__/postfinance_documents.cpython-312.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/postfinance_service.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/truewealth_service.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/postfinance_service.cpython-312.pyc: binary file matches
src/jarvis_finance/imports/postfinance_documents.py:91:    source_value_chf: Decimal
src/jarvis_finance/imports/postfinance_documents.py:143:        replace(item, source_value_chf=value)
src/jarvis_finance/imports/truewealth_tax_statement.py:34:    source_value_chf: Decimal
src/jarvis_finance/imports/truewealth_tax_statement.py:59:        return sum((row.source_value_chf for row in self.positions), Decimal("0"))
src/jarvis_finance/imports/truewealth_tax_statement.py:182:                    source_value_chf=_decimal(position.group(7)),
src/jarvis_finance/imports/truewealth_tax_statement.py:194:    difference = source_total - (sum((row.source_value_chf for row in positions), Decimal("0")) + sum((row.value_chf for row in cash), Decimal("0")))
src/jarvis_finance/services/postfinance_service.py:804:                    _money(cash.source_value_chf),
src/jarvis_finance/services/postfinance_service.py:817:                    _money(cash.source_value_chf),
src/jarvis_finance/services/truewealth_service.py:142:                "source_value_chf": _decimal_text(row.source_value_chf),
src/jarvis_finance/services/truewealth_service.py:151:                "source_value_chf": _decimal_text(row.value_chf),
src/jarvis_finance/services/truewealth_service.py:359:                     price_currency,source_price,source_value_chf,source_evidence_json,created_at)
src/jarvis_finance/services/truewealth_service.py:370:                    _decimal_text(row.source_value_chf),
src/jarvis_finance/services/truewealth_service.py:379:                     source_value_chf,source_evidence_json,created_at)
src/jarvis_finance/services/truewealth_service.py:594:                      source_price,source_value_chf
src/jarvis_finance/services/truewealth_service.py:599:            """SELECT snapshot_cash_id,currency,amount_original,fx_rate_to_chf,source_value_chf


Output:
Chunk ID: 6ec3f0
Wall time: 30.0012 seconds
Process running with session ID 51776
Original token count: 0
Output:


Output:
Chunk ID: 925f54
Wall time: 30.0010 seconds
Process running with session ID 51776
Original token count: 0
Output:


sed -n '1080,1180p' src/jarvis_finance/dashboard/data.py


Output:
Chunk ID: b3d3bd
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1274
Output:
        "db_mode": "Runtime",
        "write_mode": "Read-only",
        "last_backup_at": backup["created_at"] if backup else "nicht verfügbar",
        "git_safety_status": "zuletzt OK / CLI prüfen",
        "data_location": "Echte Daten liegen außerhalb Git",
    }

def get_broker_review_items(conn: Connection, limit: int = 100, **filters) -> list[dict[str, str]]:
    from jarvis_finance.imports.broker_mapping import get_broker_review_items as _items

    return _items(conn, limit=limit, **filters)

def get_review_readiness_counts(conn: Connection) -> list[dict[str, str]]:
    from jarvis_finance.imports.broker_mapping import get_review_readiness_counts as _counts

    return _counts(conn)

def get_ready_for_import_by_source(conn: Connection) -> list[dict[str, str]]:
    from jarvis_finance.imports.broker_mapping import get_ready_for_import_by_source as _counts

    return _counts(conn)

def get_review_quality_flag_counts(conn: Connection) -> list[dict[str, str]]:
    from jarvis_finance.imports.broker_mapping import aggregate_review_quality_flags

    return [{"quality_flag": k, "open_items": str(v)} for k, v in sorted(aggregate_review_quality_flags(conn).items())]

def equity_etf_user_label(summary: dict[str, str]) -> str:
    value = d(summary.get("equity_etf_total_chf"))
    return chf_text(value) if value > 0 else "noch nicht bewertet"

def equity_etf_is_user_ready(summary: dict[str, str]) -> bool:
    return d(summary.get("equity_etf_total_chf")) > 0 and summary.get("data_quality_status") == "ok"

def get_top_user_hints(conn: Connection, limit: int = 3) -> list[str]:
    hints: list[str] = []
    summary = get_command_center_summary(conn)
    if equity_etf_user_label(summary) == "noch nicht bewertet":
        hints.append("Aktien/ETF: Bewertung unvollständig")
    crypto = get_crypto_summary_cards(conn)
    if d(crypto.get("coins_without_price")) > 0:
        hints.append("Crypto-Preise fehlen für einzelne Coins")
    elif d(crypto.get("stale_price_count")) > 0:
        hints.append("Crypto-Preise teilweise veraltet")
    else:
        hints.append("Crypto-Preise aktuell")
    for alert in get_alert_cards(conn, limit=limit):
        text = alert["title"]
        if not any(marker in text for marker in ("asset_id", "wallet_id", "instrument-", "rule_id", "entity_id")):
            hints.append(text)
        if len(hints) >= limit:
            break
    return hints[:limit]

def compact_chart_rows(rows: list[dict[str, str]], *, limit: int = 10) -> list[dict[str, str]]:
    ordered = sorted(rows, key=lambda row: d(row.get("value_chf")), reverse=True)
    if len(ordered) <= limit:
        return ordered
    head = ordered[:limit]
    other = sum((d(row.get("value_chf")) for row in ordered[limit:]), Decimal("0"))
    if other > 0:
        head.append({"label": "Andere", "value_chf": dec_text(other, "0.01")})
    return head

def get_command_center_summary(conn: Connection) -> dict[str, str]:
    cash_rows = get_cash_overview(conn)
    crypto_summary = get_crypto_summary_cards(conn)
    positions = [r for r in get_positions(conn) if r.get("asset_class", "").lower() in {"stock", "equity", "etf"} and r.get("instrument_status", "").lower() != "inactive"]
    cash_total = sum(d(r["amount_chf"]) for r in cash_rows if r["amount_chf"])
    crypto_total = d(crypto_summary["total_value_chf"])
    stock_rows = [r for r in positions if r.get("asset_class", "").lower() in {"stock", "equity"}]
    etf_rows = [r for r in positions if r.get("asset_class", "").lower() == "etf"]
    stock_total = sum(d(r["market_value_chf"]) for r in stock_rows if r.get("market_value_chf"))
    etf_total = sum(d(r["market_value_chf"]) for r in etf_rows if r.get("market_value_chf"))
    equity_total = stock_total + etf_total
    missing_price_count = sum(1 for r in positions if not r.get("market_price_original"))
    missing_fx_count = sum(1 for r in positions if r.get("fx_status") == "missing_fx")
    incomplete_cost_count = sum(1 for r in positions if "cost_basis_uncertain" in (r.get("quality_warnings") or "") or not r.get("cost_basis_chf"))
    unvalued_count = missing_price_count + missing_fx_count
    latest_equity_price = max((r.get("price_date") or "" for r in positions), default="")
    alert_counts = conn.execute("SELECT priority, COUNT(*) AS c FROM alerts WHERE status='active' GROUP BY priority").fetchall()
    critical = sum(int(r["c"] or 0) for r in alert_counts if r["priority"] == "kritisch")
    quality = get_data_quality(conn)
    dq = "kritisch" if any(i["severity"] == "kritisch" and i["count"] != "0" for i in quality) else ("warnung" if any(i["severity"] == "warnung" and i["count"] != "0" for i in quality) else "ok")
    total = cash_total + crypto_total + equity_total
    return {
        "base_currency": "CHF",
        "cash_total_chf": dec_text(cash_total, "0.01"),
        "crypto_total_chf": dec_text(crypto_total, "0.01"),
        "stock_total_chf": dec_text(stock_total, "0.01"),
        "etf_total_chf": dec_text(etf_total, "0.01"),
        "equity_etf_total_chf": dec_text(equity_total, "0.01"),


Output:
Chunk ID: 312f46
Wall time: 30.0010 seconds
Process running with session ID 51776
Original token count: 0
Output:


Output: write_stdin failed: stdin is closed for this session; rerun exec_command with tty=true to keep stdin open


nl -ba src/jarvis_finance/dashboard/data.py | sed -n '590,610p' && nl -ba src/jarvis_finance/services/portfolio_analytics.py | sed -n '255,262p'


Output:
Chunk ID: a734d4
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 387
Output:
   590	            """,
   591	            (instrument_id, account_id),
   592	        ).fetchone()
   593	        override = official.get((account_id, instrument_id))
   594	        market_value = audited.get((account_id, instrument_id))
   595	        use_market_run = bool(
   596	            market_value
   597	            and (not override or market_value.as_of > override.snapshot_date)
   598	        )
   599	        if override and not use_market_run:
   600	            quantity = override.quantity
   601	            market_price = override.market_price_original
   602	            market_value_chf = override.market_value_chf
   603	            price_currency = override.price_currency
   604	            price_date = override.valuation_at
   605	            price_provider = "PostFinance official snapshot"
   606	            valuation_source = "postfinance_official_import"
   607	        elif use_market_run and market_value:
   608	            quantity = override.quantity if override else pos.quantity
   609	            market_price = market_value.close
   610	            market_value_chf = market_value.value_chf
   255	            }
   256	            grouped[key] = item
   257	        # The latest official snapshot is the authoritative holding quantity. Historical
   258	        # buys already represented by the old baseline snapshot must not be added twice.
   259	        item["quantity_total"] = override.quantity
   260	    return [
   261	        ConfirmedPosition(
   262	            account_id=item["account_id"],


[P2] Replay trades after the official snapshot

When a confirmed buy or sell is booked after the latest PostFinance snapshot, this branch replaces the ledger-calculated `pos.quantity` with the older official snapshot quantity, so the dashboard and equity API keep showing stale holdings until another official snapshot is imported. The official snapshot should be treated as a baseline and later confirmed transactions should still be applied.

[P2] Preserve post-snapshot trades in market valuations

For a market valuation run with `as_of` after the latest official snapshot, any confirmed trades between the snapshot date and `as_of` are accumulated in `grouped` and then discarded here by resetting `quantity_total` to the snapshot quantity. This causes future market-analysis snapshots to price stale quantities whenever the user trades after an official PostFinance import.

The patch fixes double-counting but introduces stale quantities for PostFinance positions once confirmed trades occur after the latest official snapshot. Both current read models and future market valuation runs can misstate holdings in that scenario.