Output:
Chunk ID: d07455
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1357
Output:
    """Return the latest immutable market-analysis values without writing on GET."""

    row = conn.execute(
        """SELECT pas.as_of,pas.summary_json,mdr.completed_at
           FROM portfolio_analysis_snapshots pas
           JOIN market_data_runs mdr ON mdr.run_id=pas.run_id
           ORDER BY pas.as_of DESC,pas.created_at DESC LIMIT 1"""
    ).fetchone()
    if not row:
        return {}
    try:
        payload = json.loads(row["summary_json"] or "{}")
    except json.JSONDecodeError:
        return {}
    values: dict[tuple[str, str], AuditedMarketValue] = {}
    for item in payload.get("positions", []):
        if not isinstance(item, dict) or item.get("quality_status") != "fresh":
            continue
        account_id = str(item.get("account_id") or "")
        instrument_id = str(item.get("instrument_id") or "")
        if not account_id or not instrument_id or item.get("value_chf") in {None, ""}:
            continue
        provenance = item.get("price_input_provenance")
        provider = (
            str(item.get("provider") or "")
            or (str(provenance.get("provider") or "") if isinstance(provenance, dict) else "")
        )
        values[(account_id, instrument_id)] = AuditedMarketValue(
            account_id=account_id,
            instrument_id=instrument_id,
            value_chf=_decimal(item["value_chf"]),
            close=_decimal(item["close"]) if item.get("close") not in {None, ""} else None,
            currency=str(item.get("currency") or ""),
            as_of=str(row["as_of"]),
            completed_at=str(row["completed_at"]),
            provider=provider,
            quantity=(
                _decimal(item["quantity"])
                if item.get("quantity") not in {None, ""}
                else None
            ),
        )
    return values
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"])


grep -R "postfinance_account_roles\|postfinance_snapshots\|postfinance_snapshot_cash\|postfinance_import_batches" -n src tests | head -120 && sed -n '820,960p' src/jarvis_finance/services/postfinance_service.py


Output:
Chunk ID: 4416fe
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 3713
Output:
grep: src/jarvis_finance/storage/__pycache__/postfinance_schema.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/dashboard/__pycache__/data.cpython-312.pyc: binary file matches
grep: src/jarvis_finance/dashboard/__pycache__/data.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/postfinance_service.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/portfolio_data.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/cash_service.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/portfolio_aggregation.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/portfolio_analytics.cpython-312.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/portfolio_aggregation.cpython-312.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/portfolio_analytics.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/postfinance_service.cpython-312.pyc: binary file matches
grep: tests/__pycache__/test_postfinance_sprint13.cpython-311-pytest-9.1.1.pyc: binary file matches
src/jarvis_finance/storage/postfinance_schema.py:15:        CREATE TABLE IF NOT EXISTS postfinance_account_roles (
src/jarvis_finance/storage/postfinance_schema.py:22:        CREATE TABLE IF NOT EXISTS postfinance_import_batches (
src/jarvis_finance/storage/postfinance_schema.py:52:            batch_id TEXT NOT NULL REFERENCES postfinance_import_batches(batch_id),
src/jarvis_finance/storage/postfinance_schema.py:56:        CREATE TABLE IF NOT EXISTS postfinance_snapshots (
src/jarvis_finance/storage/postfinance_schema.py:58:            batch_id TEXT NOT NULL UNIQUE REFERENCES postfinance_import_batches(batch_id),
src/jarvis_finance/storage/postfinance_schema.py:77:            snapshot_id TEXT NOT NULL REFERENCES postfinance_snapshots(snapshot_id),
src/jarvis_finance/storage/postfinance_schema.py:95:        CREATE TABLE IF NOT EXISTS postfinance_snapshot_cash (
src/jarvis_finance/storage/postfinance_schema.py:97:            snapshot_id TEXT NOT NULL REFERENCES postfinance_snapshots(snapshot_id),
src/jarvis_finance/storage/postfinance_schema.py:160:        CREATE TRIGGER IF NOT EXISTS postfinance_batches_immutable BEFORE UPDATE ON postfinance_import_batches
src/jarvis_finance/storage/postfinance_schema.py:162:        CREATE TRIGGER IF NOT EXISTS postfinance_batches_no_delete BEFORE DELETE ON postfinance_import_batches
src/jarvis_finance/storage/postfinance_schema.py:172:        CREATE TRIGGER IF NOT EXISTS postfinance_snapshots_immutable BEFORE UPDATE ON postfinance_snapshots
src/jarvis_finance/storage/postfinance_schema.py:174:        CREATE TRIGGER IF NOT EXISTS postfinance_snapshots_no_delete BEFORE DELETE ON postfinance_snapshots
src/jarvis_finance/storage/postfinance_schema.py:180:        CREATE TRIGGER IF NOT EXISTS postfinance_snapshot_cash_immutable BEFORE UPDATE ON postfinance_snapshot_cash
src/jarvis_finance/storage/postfinance_schema.py:182:        CREATE TRIGGER IF NOT EXISTS postfinance_snapshot_cash_no_delete BEFORE DELETE ON postfinance_snapshot_cash
src/jarvis_finance/dashboard/data.py:16:    postfinance_account_roles,
src/jarvis_finance/dashboard/data.py:564:    roles = postfinance_account_roles(conn)
src/jarvis_finance/services/portfolio_aggregation.py:65:def postfinance_account_roles(conn: Connection) -> dict[str, str]:
src/jarvis_finance/services/portfolio_aggregation.py:71:            "SELECT role,account_id FROM postfinance_account_roles ORDER BY role"
src/jarvis_finance/services/portfolio_aggregation.py:102:    roles = postfinance_account_roles(conn)
src/jarvis_finance/services/portfolio_aggregation.py:117:            FROM postfinance_snapshots {where}
src/jarvis_finance/services/portfolio_aggregation.py:125:           FROM postfinance_snapshot_cash WHERE snapshot_id=? ORDER BY currency""",
src/jarvis_finance/services/portfolio_aggregation.py:155:    roles = postfinance_account_roles(conn)
src/jarvis_finance/services/portfolio_aggregation.py:170:            FROM postfinance_snapshots {where}
src/jarvis_finance/services/portfolio_aggregation.py:225:    roles = postfinance_account_roles(conn)
src/jarvis_finance/services/portfolio_data.py:34:    postfinance_account_roles,
src/jarvis_finance/services/portfolio_data.py:1599:    pf_roles = postfinance_account_roles(conn)
src/jarvis_finance/services/portfolio_analytics.py:31:    postfinance_account_roles,
src/jarvis_finance/services/portfolio_analytics.py:238:    trading_cash_id = postfinance_account_roles(conn).get("etrading_cash")
src/jarvis_finance/services/postfinance_service.py:167:        "postfinance_import_batches",
src/jarvis_finance/services/postfinance_service.py:171:        if table == "postfinance_import_batches":
src/jarvis_finance/services/postfinance_service.py:319:        "SELECT batch_id FROM postfinance_import_batches WHERE bundle_sha256=?",
src/jarvis_finance/services/postfinance_service.py:323:        "SELECT batch_id FROM postfinance_import_batches WHERE substr(snapshot_at,1,10)=? AND bundle_sha256<>?",
src/jarvis_finance/services/postfinance_service.py:501:            "SELECT batch_id,audit_id FROM postfinance_import_batches WHERE bungrep: tests/__pycache__/test_postfinance_sprint13.cpython-311.pyc: binary file matches
grep: tests/__pycache__/test_postfinance_sprint13.cpython-311-pytest-9.0.2.pyc: binary file matches
dle_sha256=?",
src/jarvis_finance/services/postfinance_service.py:541:                "INSERT OR IGNORE INTO postfinance_account_roles(role,account_id,source_reference_hash,created_at) VALUES(?,?,NULL,?)",
src/jarvis_finance/services/postfinance_service.py:545:                "SELECT account_id FROM postfinance_account_roles WHERE role=?", (role,)
src/jarvis_finance/services/postfinance_service.py:553:            """INSERT INTO postfinance_import_batches(batch_id,bundle_sha256,zip_sha256,overview_sha256,parser_id,parser_version,db_revision,archive_reference,snapshot_at,status,audit_id,confirmed_at,confirmed_by) VALUES(?,?,?,?,?,?,?,?,?,'confirmed',?,?,'user')""",
src/jarvis_finance/services/postfinance_service.py:602:            """INSERT INTO postfinance_snapshots(snapshot_id,batch_id,account_id,snapshot_at,total_chf,securities_chf,cash_chf,stocks_chf,etfs_chf,component_total_chf,difference_chf,tolerance_chf,position_count,cash_count,open_orders,reconciliation_status,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,? ,?,?,?,?,?)""",
src/jarvis_finance/services/postfinance_service.py:797:                "INSERT INTO postfinance_snapshot_cash(snapshot_cash_id,snapshot_id,currency,amount_original,fx_rate_to_chf,value_chf,created_at) VALUES(?,?,?,?,?,?,?)",
src/jarvis_finance/services/postfinance_service.py:840:            "SELECT batch_id,audit_id FROM postfinance_import_batches WHERE bundle_sha256=?",
src/jarvis_finance/services/postfinance_service.py:873:           FROM postfinance_snapshots ORDER BY snapshot_at DESC,created_at DESC LIMIT 1"""
src/jarvis_finance/services/postfinance_service.py:937:               FROM postfinance_import_batches b JOIN postfinance_snapshots s ON s.batch_id=b.batch_id
src/jarvis_finance/services/cash_service.py:279:                  SELECT account_id FROM postfinance_account_roles
tests/test_postfinance_sprint13.py:298:    assert conn.execute("SELECT COUNT(*) FROM postfinance_import_batches").fetchone()[0] == 1
tests/test_postfinance_sprint13.py:299:    assert conn.execute("SELECT COUNT(*) FROM postfinance_snapshots").fetchone()[0] == 1
tests/test_postfinance_sprint13.py:301:    assert conn.execute("SELECT COUNT(*) FROM postfinance_account_roles").fetchone()[0] == 3
tests/test_postfinance_sprint13.py:302:    roles = dict(conn.execute("SELECT role,account_id FROM postfinance_account_roles"))
tests/test_postfinance_sprint13.py:430:    assert conn.execute("SELECT COUNT(*) FROM postfinance_import_batches").fetchone()[0] == 1
tests/test_postfinance_sprint13.py:469:            "UPDATE postfinance_snapshots SET total_chf='0' WHERE snapshot_id=?",
tests/test_postfinance_sprint13.py:474:            "DELETE FROM postfinance_snapshots WHERE snapshot_id=?", (result["snapshot_id"],)
tests/test_postfinance_sprint13.py:479:        "postfinance_snapshot_cash": "created_at=created_at",
tests/test_postfinance_sprint13.py:636:    assert conn.execute("SELECT COUNT(*) FROM postfinance_import_batches").fetchone()[0] == 1
tests/test_postfinance_sprint13.py:742:    batch = dict(conn.execute("SELECT * FROM postfinance_import_batches LIMIT 1").fetchone())
tests/test_postfinance_sprint13.py:751:        f"INSERT INTO postfinance_import_batches({','.join(columns)}) VALUES({','.join('?' for _ in columns)})",
tests/test_postfinance_sprint13.py:754:    source_snapshot = dict(conn.execute("SELECT * FROM postfinance_snapshots LIMIT 1").fetchone())
tests/test_postfinance_sprint13.py:770:        f"INSERT INTO postfinance_snapshots({','.join(columns)}) VALUES({','.join('?' for _ in columns)})",
tests/test_postfinance_sprint13.py:787:        """INSERT INTO postfinance_snapshot_cash(
tests/test_postfinance_sprint13.py:791:           FROM postfinance_snapshot_cash WHERE snapshot_id<> 'future-snapshot'"""
                    "Official PostFinance account overview",
                    now,
                ),
            )
        conn.execute(
            "INSERT INTO account_value_snapshots(snapshot_id,account_id,valuation_date,total_value_chf,currency,source_type,quality_status,notes,created_at,valuation_at,source_reference,is_active) VALUES(?,?,?,?,'CHF','postfinance_official_import','ok','Official PostFinance account overview',?,?,?,1)",
            (
                _id("acctval"),
                accounts["etrading_depot"],
                snapshot.valuation_at[:10],
                _money(snapshot.total_value_chf),
                now,
                snapshot.valuation_at,
                snapshot_id,
            ),
        )
        conn.commit()
    except IntegrityError as exc:
        conn.rollback()
        duplicate = conn.execute(
            "SELECT batch_id,audit_id FROM postfinance_import_batches WHERE bundle_sha256=?",
            (bundle.bundle_hash,),
        ).fetchone()
        if duplicate:
            return {
                "status": "confirmed",
                "batch_id": duplicate["batch_id"],
                "audit_id": duplicate["audit_id"],
                "idempotent": True,
            }
        for ref, created in archive_refs:
            if created:
                _remove_archive(ref)
        raise ValueError("PostFinance import conflicted with immutable data") from exc
    except Exception:
        conn.rollback()
        for ref, created in archive_refs:
            if created:
                _remove_archive(ref)
        raise
    return {
        "status": "confirmed",
        "batch_id": batch_id,
        "snapshot_id": snapshot_id,
        "audit_id": audit_id,
        "idempotent": False,
    }

def get_postfinance_summary(conn: Connection) -> dict[str, Any]:
    latest = conn.execute(
        """SELECT snapshot_id,snapshot_at,total_chf,securities_chf,cash_chf,stocks_chf,etfs_chf,
                  position_count,cash_count,open_orders,difference_chf,reconciliation_status,created_at
           FROM postfinance_snapshots ORDER BY snapshot_at DESC,created_at DESC LIMIT 1"""
    ).fetchone()
    if not latest:
        return {
            "latest_snapshot": None,
            "positions": [],
            "cash": [],
            "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],
            "instrument_name": row["instrument_name"],
            "ticker": row["ticker"],
            "asset_class": row["asset_class"],
            "quantity": row["quantity"],
            "provider_average_cost_original": row["provider_average_cost_original"],
            "provider_cost_total_original": row["provider_cost_total_original"],
            "price_original": row["price_original"],
            "price_currency": row["price_currency"],
            "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.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
               FROM postfinance_snapshot_positions p
               JOIN instruments i ON i.instrument_id=p.instrument_id
               WHERE p.snapshot_id=? ORDER BY CAST(p.value_chf AS REAL) DESC""",
            (latest["snapshot_id"],),
        ).fetchall()
    ]
    cash = [
        {
            "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 component in latest_official_postfinance_cash(conn)
    ]
    events = {
        str(row["event_type"]): int(row["c"])
        for row in conn.execute(
            "SELECT event_type,COUNT(*) c FROM postfinance_ledger_events GROUP BY event_type ORDER BY event_type"
        ).fetchall()
    }
    imports = [
        dict(row)
        for row in conn.execute(
            """SELECT b.snapshot_at,b.parser_id,b.parser_version,b.confirmed_at,b.status,
                      s.total_chf,s.securities_chf,s.cash_chf,s.position_count,s.cash_count,
                      s.difference_chf,s.reconciliation_status,
                      (SELECT COUNT(*) FROM postfinance_batch_documents d WHERE d.batch_id=b.batch_id) AS document_count
               FROM postfinance_import_batches b JOIN postfinance_snapshots s ON s.batch_id=b.batch_id
               ORDER BY b.snapshot_at DESC,b.confirmed_at DESC"""
        ).fetchall()
    ]
    safe_latest = {key: latest[key] for key in latest.keys() if key != "snapshot_id"}
    return {
        "latest_snapshot": safe_latest,
        "positions": positions,
        "cash": cash,
        "event_counts": events,
        "imports": imports,
    }


grep -R "class CashPosition" -n src && sed -n '1,120p' src/jarvis_finance/api/schemas/positions.py && grep -R "get_cash_position_detail\|cash_position" -n src tests | head -50


Output:
Chunk ID: 95cb6a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1360
Output:
src/jarvis_finance/api/schemas/positions.py:148:class CashPosition(BaseModel):
src/jarvis_finance/api/schemas/positions.py:168:class CashPositionDetail(CashPosition):
from __future__ import annotations

from pydantic import BaseModel

class ActionItem(BaseModel):
    label: str
    enabled: bool
    reason: str | None = None

class PricePoint(BaseModel):
    date: str
    value: str
    currency: str = "CHF"
    provider: str | None = None
    quality_status: str | None = None

class CryptoPosition(BaseModel):
    asset_id: str
    name: str
    symbol: str
    quantity_total: str
    price_chf: str | None = None
    market_value_chf: str | None = None
    portfolio_share_pct: str
    wallet_count: int
    price_status: str
    last_price_update: str | None = None
    coingecko_url: str | None = None

class WalletAllocation(BaseModel):
    wallet_id: str
    wallet: str
    quantity: str
    market_value_chf: str | None = None

class WalletCoin(BaseModel):
    asset_id: str
    name: str
    symbol: str
    quantity: str
    market_value_chf: str | None = None
    status: str

class CoinGeckoInfo(BaseModel):
    market_cap_rank: int | None = None
    market_cap: str | None = None
    volume_24h: str | None = None
    change_24h_pct: str | None = None
    change_7d_pct: str | None = None
    homepage: str | None = None
    image_url: str | None = None

class CryptoPositionDetail(CryptoPosition):
    wallet_allocations: list[WalletAllocation]
    coingecko_info: CoinGeckoInfo | None = None
    available_actions: list[ActionItem] = []
    price_history: list[PricePoint] = []
    read_only_note: str = "Live-Modus später; keine Provider-Abfrage beim Rendern."

class WalletSummary(BaseModel):
    wallet_id: str
    name: str
    wallet_type: str
    provider: str = ""
    coin_count: int
    market_value_chf: str | None = None
    status: str

class WalletDetail(WalletSummary):
    coins: list[WalletCoin]

class EquityPosition(BaseModel):
    position_id: str
    instrument_id: str | None = None
    name: str
    ticker: str = ""
    isin: str = ""
    account: str = ""
    asset_class: str
    quantity: str
    currency: str
    price: str | None = None
    market_value_chf: str | None = None
    portfolio_share_pct: str | None = None
    price_date: str | None = None
    provider: str | None = None
    provider_symbol: str | None = None
    exchange: str | None = None
    price_currency: str | None = None
    data_status: str = "missing"
    status_code: str = "price_missing"
    status_reason: str | None = None
    status: str

class EquityPortfolioSummary(BaseModel):
    total_positions: int
    valued_positions: int
    unvalued_positions: int
    coverage_complete: bool
    equity_value_chf: str | None = None
    valued_partial_chf: str
    value_label: str
    as_of: str | None = None
    last_successful_run: str | None = None
    status: str
    status_label: str
    unvalued_tickers: list[str] = []

grep: src/jarvis_finance/dashboard/__pycache__/data.cpython-312.pyc: binary file matches
grep: src/jarvis_finance/dashboard/__pycache__/data.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/api/routers/__pycache__/cash.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/cash_service.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/manual_entry_service.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/cash_service.cpython-312.pyc: binary file matches
grep: tests/__pycache__/test_postfinance_sprint13.cpython-311-pytest-9.1.1.pyc: binary file matches
grep: tests/__pycache__/test_postfinance_sprint13.cpython-311.pyc: binary file matches
grep: tests/__pycache__/test_postfinance_sprint13.cpython-311-pytest-9.0.2.pyc: binary file matches
src/jarvis_finance/dashboard/data.py:543:    from jarvis_finance.services.cash_service import list_cash_positions
src/jarvis_finance/dashboard/data.py:555:        for position in list_cash_positions(conn)
src/jarvis_finance/api/routers/cash.py:10:from jarvis_finance.services.cash_service import confirm_cash_snapshot, get_cash_position_detail, get_cash_summary, preview_cash_snapshot
src/jarvis_finance/api/routers/cash.py:22:def cash_position(position_id: str, conn: Connection = Depends(get_db)) -> CashPositionDetail:
src/jarvis_finance/api/routers/cash.py:23:    return get_cash_position_detail(conn, position_id)
src/jarvis_finance/services/manual_entry_service.py:517:    audit_id = _audit(conn, action="cash_confirm", entity_type="cash_position", entity_id=f"{request.account_id}:{request.currency.upper()}", payload=request.model_dump(), note=request.note, quality_status=quality)
src/jarvis_finance/services/manual_entry_service.py:571:def get_cash_position_detail(conn: Connection, position_id: str) -> CashPositionDetail:
src/jarvis_finance/services/cash_service.py:269:def list_cash_positions(conn: Connection) -> list[CashPosition]:
src/jarvis_finance/services/cash_service.py:477:    positions = list_cash_positions(conn)
src/jarvis_finance/services/cash_service.py:521:def get_cash_position_detail(conn: Connection, position_id: str) -> CashPositionDetail:
tests/test_postfinance_sprint13.py:26:from jarvis_finance.services.cash_service import get_cash_position_detail, get_cash_summary
tests/test_postfinance_sprint13.py:577:    assert get_cash_position_detail(conn, usd_position.id).currency == "USD"


sed -n '140,210p' src/jarvis_finance/api/schemas/positions.py && sed -n '450,570p' src/jarvis_finance/services/cash_service.py


Output:
Chunk ID: 2bf608
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 1883
Output:
    balance_date: str
    amount_chf: str
    source: str
    note: str = ""
    difference_to_previous_chf: str | None = None
    audit_hint: str | None = None

class CashPosition(BaseModel):
    id: str
    account_id: str = ""
    platform: str
    account_label: str
    account_type: str = "cash"
    currency: str
    balance_mode: str = "manual"
    amount: str
    amount_chf: str | None = None
    calculated_balance_chf: str = "0.00"
    manual_balance_chf: str | None = None
    csv_anchor_balance_chf: str | None = None
    used_value_chf: str = "0.00"
    difference_chf: str = "0.00"
    last_manual_reconciliation: str | None = None
    last_imported_booking: str | None = None
    status: str

class CashPositionDetail(CashPosition):
    last_change: str | None = None
    history: list[CashSnapshot] = []
    available_actions: list[ActionItem]

class InstrumentSearchCandidate(BaseModel):
    candidate_id: str
    label: str
    asset_class: str
    isin: str = ""
    ticker: str = ""
    exchange: str = ""
    currency: str = ""
    provider: str = ""
    provider_symbol: str = ""
    exchange_name: str = ""
    mic: str = ""
    country: str = ""
    security_type: str = ""
    last_price: str | None = None
    price_currency: str | None = None
    price_date: str | None = None
    confidence_label: str
    review_required: bool

class InstrumentSearchRequest(BaseModel):
    asset_class: str
    query: str
    currency: str | None = None
    exchange: str | None = None
    country: str | None = None
    main_listing_only: bool = True
    hide_cdr_adr_derivatives: bool = True

class ProviderSearchResponse(BaseModel):
    candidates: list[InstrumentSearchCandidate]
    warnings: list[str] = []
    selection_required: bool = True

                    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

def get_cash_summary(conn: Connection) -> CashSummary:
    # Read paths must never seed or refresh accounts; setup is an explicit Confirm action.
    positions = list_cash_positions(conn)
    total = sum(Decimal(p.used_value_chf) for p in positions)
    last_dates = [p.last_manual_reconciliation for p in positions if p.last_manual_reconciliation]
    return CashSummary(
        base_currency="CHF",
        cash_chf=decimal_text(total, 2),
        manual_accounts_count=sum(1 for p in positions if p.balance_mode == "manual"),
        csv_calculated_accounts_count=sum(1 for p in positions if p.balance_mode == "csv_calculated"),
        open_reconciliation_count=sum(1 for p in positions if p.status == "Abgleich offen"),
        last_reconciliation_at=max(last_dates) if last_dates else None,
        positions=positions,
    )

def preview_cash_snapshot(conn: Connection, request: CashSnapshotPreviewRequest) -> PreviewResponse:
    row = conn.execute("SELECT account_id, account_type FROM accounts WHERE account_id=?", (request.account_id,)).fetchone()
    if not row or row["account_type"] != "cash":
        raise HTTPException(status_code=404, detail="Cash account not found")
    if request.snapshot_type not in {"manual_balance", "csv_anchor_balance", "calculated_balance", "reconciliation"}:
        raise HTTPException(status_code=422, detail="Invalid snapshot_type")
    amount = _decimal(request.amount_chf, "amount_chf")
    return PreviewResponse(preview_id=_uuid("preview"), asset_class="cash", summary=f"{request.snapshot_type} CHF per {request.balance_date}", amount_chf=decimal_text(amount, 2), fx_status="not_needed", warnings=[])

def confirm_cash_snapshot(conn: Connection, request: CashSnapshotConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=400, detail="Explicit confirm required")
    preview_cash_snapshot(conn, request)
    amount = _decimal(request.amount_chf, "amount_chf")
    snapshot_id = _uuid("cashsnap")
    now = _now()
    source = "manual" if request.snapshot_type in {"manual_balance", "reconciliation"} else "csv_import" if request.snapshot_type == "csv_anchor_balance" else "system_calculated"
    audit_id = _audit(conn, action=f"cash_{request.snapshot_type}_confirm", entity_type="cash_account", entity_id=request.account_id, payload=request.model_dump(), note=request.note)
    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, audit_id)
        VALUES (?, ?, ?, ?, ?, 'CHF', ?, ?, ?, ?, 'user', ?)
        """,
        (snapshot_id, request.account_id, request.snapshot_type, request.balance_date, decimal_text(amount, 2), decimal_text(amount, 2), source, request.note, now, audit_id),
    )
    conn.commit()
    return ConfirmResponse(status="confirmed", entity_id=snapshot_id, audit_id=audit_id, message="Kontostand gespeichert · Audit geschrieben")

def get_cash_position_detail(conn: Connection, position_id: str) -> CashPositionDetail:
    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
        FROM cash_account_snapshots
        WHERE account_id=?
        ORDER BY balance_date DESC, created_at DESC
        LIMIT 30
        """,
        (account_id,),
    ).fetchall()
    history: list[CashSnapshot] = []
    previous: Decimal | None = None
    for row in reversed(rows):
        current = Decimal(str(row["amount_chf"]))
        diff = None if previous is None else decimal_text(current - previous, 2)
        previous = current
        history.append(CashSnapshot(snapshot_id=row["snapshot_id"], snapshot_type=row["snapshot_type"], balance_date=row["balance_date"], amount_chf=decimal_text(current, 2), source=row["source"], note=row["note"], difference_to_previous_chf=diff, audit_hint="Audit vorhanden" if row["audit_id"] else None))
    history = list(reversed(history))
    return CashPositionDetail(**position.model_dump(), last_change=position.last_manual_reconciliation, history=history, available_actions=[ActionItem(label="Einzahlung", enabled=True), ActionItem(label="Auszahlung", enabled=True), ActionItem(label="Kontostand aktualisieren", enabled=True), ActionItem(label="CSV-Anfangsstand setzen", enabled=True), ActionItem(label="Abgleich bestätigen", enabled=True), ActionItem(label="Verlauf anzeigen", enabled=True)])


git show HEAD:src/jarvis_finance/services/cash_service.py | sed -n '130,230p' && git show HEAD:src/jarvis_finance/ledger/cash.py | sed -n '1,120p'


Output:
Chunk ID: 9d7a1a
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2328
Output:
def _cash_movements_after_anchor(conn: Connection, account_id: str, anchor_date: str | None) -> Decimal:
    if not anchor_date:
        return Decimal("0")
    row = conn.execute(
        """
        SELECT COALESCE(SUM(CAST(net_amount_chf AS TEXT)), '0') AS movement
        FROM transactions
        WHERE account_id=? AND COALESCE(is_voided,0)=0 AND COALESCE(is_confirmed,1)=1
          AND source_type LIKE '%csv%'
          AND trade_date > ?
        """,
        (account_id, anchor_date),
    ).fetchone()
    return Decimal(str(row["movement"] or "0"))

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=?",
        (account_id,),
    ).fetchone()
    return Decimal(str(row["amount"] or "0"))

def _last_imported_booking(conn: Connection, account_id: str) -> str | None:
    row = conn.execute("SELECT MAX(trade_date) AS last_date FROM transactions WHERE account_id=? AND source_type LIKE '%csv%'", (account_id,)).fetchone()
    return row["last_date"] if row else None

def list_cash_positions(conn: Connection) -> list[CashPosition]:
    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'
        ORDER BY p.name, a.account_name
        """
    ).fetchall()
    positions: list[CashPosition] = []
    for row in rows:
        account_id = row["account_id"]
        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,
            )
        )
    return positions

def get_cash_summary(conn: Connection) -> CashSummary:
    # Read paths must never seed or refresh accounts; setup is an explicit Confirm action.
    positions = list_cash_positions(conn)
    total = sum(Decimal(p.used_value_chf) for p in positions)
    last_dates = [p.last_manual_reconciliation for p in positions if p.last_manual_reconciliation]
    return CashSummary(
        base_currency="CHF",
        cash_chf=decimal_text(total, 2),
        manual_accounts_count=sum(1 for p in positions if p.balance_mode == "manual"),
        csv_calculated_accounts_count=sum(1 for p in positions if p.balance_mode == "csv_calculated"),
        open_reconciliation_count=sum(1 for p in positions if p.status == "Abgleich offen"),
        last_reconciliation_at=max(last_dates) if last_dates else None,
        positions=positions,
    )

def preview_cash_snapshot(conn: Connection, request: CashSnapshotPreviewRequest) -> PreviewResponse:
    row = conn.execute("SELECT account_id, account_type FROM accounts WHERE account_id=?", (request.account_id,)).fetchone()
    if not row or row["account_type"] != "cash":
        raise HTTPException(status_code=404, detail="Cash account not found")
    if request.snapshot_type not in {"manual_balance", "csv_anchor_balance", "calculated_balance", "reconciliation"}:
        raise HTTPException(status_code=422, detail="Invalid snapshot_type")
    amount = _decimal(request.amount_chf, "amount_chf")
from __future__ import annotations

from dataclasses import dataclass, field
from decimal import Decimal
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

@dataclass
class CashBalance:
    account_id: str
    currency: str
    amount_original: Decimal = Decimal("0")
    amount_chf: Decimal | None = Decimal("0")
    quality_status: str = "ok"

@dataclass
class CashCalculationResult:
    balances: dict[tuple[str, str], CashBalance] = field(default_factory=dict)
    negative_cash_warnings: list[str] = field(default_factory=list)

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

def _signed_cash_delta(row) -> tuple[Decimal, Decimal | None]:
    t = row["transaction_type"]
    gross = _d(row["gross_amount_original"])
    net = _d(row["net_amount_original"]) if row["net_amount_original"] is not None else None
    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 t == "initial_cash_snapshot":
        original = net if net is not None else gross
    elif t == "cash_deposit":
        original = net if net is not None else gross
    elif t == "cash_withdrawal":
        original = -(net if net is not None else gross)
    elif t == "buy":
        original = -(net if net is not None else gross + fee + tax)
    elif t in {"partial_sell", "full_sell"}:
        original = net if net is not None else gross - fee - tax
    elif t in {"dividend", "etf_distribution"}:
        original = net if net is not None else gross - tax - fee
    elif t in {"fee", "tax"}:
        original = -(net if net is not None else gross or fee or tax)
    elif t == "fx_conversion":
        original = net if net is not None else gross
    elif t == "manual_cash_correction":
        original = net if net is not None else gross
    else:
        original = Decimal("0")
    chf = original * fx if fx is not None else None
    return original, chf

def calculate_cash_balances(conn: Connection, *, as_of_date: str | None = None) -> CashCalculationResult:
    result = CashCalculationResult()
    params: tuple[object, ...] = ()
    where = "WHERE 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, currency_original, trade_date, created_at, transaction_id
        """,
        params,
    ).fetchall()
    for row in rows:
        account_id = row["account_id"]
        currency = row["currency_original"]
        key = (account_id, currency)
        balance = result.balances.setdefault(key, CashBalance(account_id, currency))
        original, chf = _signed_cash_delta(row)
        balance.amount_original += original
        if chf is None:
            balance.amount_chf = None
            balance.quality_status = "incomplete"
        elif balance.amount_chf is not None:
            balance.amount_chf += chf
    for key, balance in result.balances.items():
        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()
    return result

def apply_manual_cash_correction(
    conn: Connection,
    *,
    account_id: str,
    currency: str,
    amount: Decimal,
    note: str,
    trade_date: str | None = None,
) -> str:
    if not note.strip():
        raise ValueError("manual cash correction requires a note")


Output:
Chunk ID: d81cab
Wall time: 2.7709 seconds
Process exited with code 0
Original token count: 25
Output:
.........                                                                [100%]
9 passed in 2.56s


nl -ba src/jarvis_finance/services/portfolio_aggregation.py | sed -n '90,210p'; nl -ba src/jarvis_finance/dashboard/data.py | sed -n '560,640p'; nl -ba src/jarvis_finance/services/cash_service.py | sed -n '180,370p'


Output:
Chunk ID: f9091b
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 4903
Output:
    90	        allocated[largest] = _money(allocated[largest] + residual)
    91	    if sum(allocated, Decimal("0")) != _money(target):
    92	        raise ValueError("Official portfolio component allocation does not reconcile")
    93	    return allocated
    94
    95
    96	def latest_official_postfinance_cash(
    97	    conn: Connection,
    98	    *,
    99	    as_of: str | None = None,
   100	    data_cutoff: str | None = None,
   101	) -> list[OfficialCashComponent]:
   102	    roles = postfinance_account_roles(conn)
   103	    account_id = roles.get("etrading_cash")
   104	    if not account_id:
   105	        return []
   106	    conditions: list[str] = []
   107	    params: list[str] = []
   108	    if as_of:
   109	        conditions.append("substr(snapshot_at,1,10)<=substr(?,1,10)")
   110	        params.append(as_of)
   111	    if data_cutoff:
   112	        conditions.append("datetime(created_at)<=datetime(?)")
   113	        params.append(data_cutoff)
   114	    where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
   115	    snapshot = conn.execute(
   116	        f"""SELECT snapshot_id,snapshot_at,cash_chf
   117	            FROM postfinance_snapshots {where}
   118	            ORDER BY snapshot_at DESC,datetime(created_at) DESC LIMIT 1""",
   119	        params,
   120	    ).fetchone()
   121	    if not snapshot:
   122	        return []
   123	    rows = conn.execute(
   124	        """SELECT currency,amount_original,fx_rate_to_chf
   125	           FROM postfinance_snapshot_cash WHERE snapshot_id=? ORDER BY currency""",
   126	        (snapshot["snapshot_id"],),
   127	    ).fetchall()
   128	    raw_values = [
   129	        _decimal(row["amount_original"]) * _decimal(row["fx_rate_to_chf"])
   130	        for row in rows
   131	    ]
   132	    target = _money(_decimal(snapshot["cash_chf"]))
   133	    allocated = _allocate_pro_rata(raw_values, target)
   134	    valuation_at = str(snapshot["snapshot_at"])
   135	    return [
   136	        OfficialCashComponent(
   137	            account_id=account_id,
   138	            currency=str(row["currency"]),
   139	            amount_original=_decimal(row["amount_original"]),
   140	            source_fx_rate_to_chf=_decimal(row["fx_rate_to_chf"]),
   141	            amount_chf=value,
   142	            snapshot_date=valuation_at[:10],
   143	            valuation_at=valuation_at,
   144	        )
   145	        for row, value in zip(rows, allocated, strict=True)
   146	    ]
   147
   148
   149	def latest_official_postfinance_positions(
   150	    conn: Connection,
   151	    *,
   152	    as_of: str | None = None,
   153	    data_cutoff: str | None = None,
   154	) -> dict[tuple[str, str], OfficialPositionOverride]:
   155	    roles = postfinance_account_roles(conn)
   156	    account_id = roles.get("etrading_depot")
   157	    if not account_id:
   158	        return {}
   159	    conditions: list[str] = []
   160	    params: list[str] = []
   161	    if as_of:
   162	        conditions.append("substr(snapshot_at,1,10)<=substr(?,1,10)")
   163	        params.append(as_of)
   164	    if data_cutoff:
   165	        conditions.append("datetime(created_at)<=datetime(?)")
   166	        params.append(data_cutoff)
   167	    where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
   168	    snapshot = conn.execute(
   169	        f"""SELECT snapshot_id,snapshot_at,stocks_chf,etfs_chf
   170	            FROM postfinance_snapshots {where}
   171	            ORDER BY snapshot_at DESC,datetime(created_at) DESC LIMIT 1""",
   172	        params,
   173	    ).fetchone()
   174	    if not snapshot:
   175	        return {}
   176	    rows = conn.execute(
   177	        """SELECT instrument_id,asset_class,quantity,price_original,price_currency,value_chf
   178	           FROM postfinance_snapshot_positions
   179	           WHERE snapshot_id=? ORDER BY asset_class,instrument_id""",
   180	        (snapshot["snapshot_id"],),
   181	    ).fetchall()
   182	    targets = {
   183	        "stock": _money(_decimal(snapshot["stocks_chf"])),
   184	        "etf": _money(_decimal(snapshot["etfs_chf"])),
   185	    }
   186	    allocated_by_instrument: dict[str, Decimal] = {}
   187	    for asset_class, target in targets.items():
   188	        members = [row for row in rows if str(row["asset_class"]).lower() == asset_class]
   189	        allocated = _allocate_pro_rata(
   190	            [_decimal(row["value_chf"]) for row in members], target
   191	        )
   192	        allocated_by_instrument.update(
   193	            {
   194	                str(row["instrument_id"]): value
   195	                for row, value in zip(members, allocated, strict=True)
   196	            }
   197	        )
   198	    valuation_at = str(snapshot["snapshot_at"])
   199	    return {
   200	        (account_id, str(row["instrument_id"])): OfficialPositionOverride(
   201	            account_id=account_id,
   202	            instrument_id=str(row["instrument_id"]),
   203	            asset_class=str(row["asset_class"]).lower(),
   204	            quantity=_decimal(row["quantity"]),
   205	            market_price_original=_decimal(row["price_original"]),
   206	            price_currency=str(row["price_currency"]),
   207	            market_value_chf=allocated_by_instrument[str(row["instrument_id"])],
   208	            snapshot_date=valuation_at[:10],
   209	            valuation_at=valuation_at,
   210	        )
   560	    result = calculate_positions(conn, persist_alerts=False)
   561	    official = latest_official_postfinance_positions(conn)
   562	    post_snapshot_deltas = post_snapshot_postfinance_quantity_deltas(conn, official)
   563	    audited = latest_audited_market_values(conn)
   564	    roles = postfinance_account_roles(conn)
   565	    trading_cash_id = roles.get("etrading_cash")
   566	    official_instruments = {key[1] for key in official}
   567	    for key in list(result.positions):
   568	        if key[0] == trading_cash_id and key[1] in official_instruments:
   569	            # Instrument-bearing settlement events live on trading cash, while the
   570	            # authoritative holding belongs to the distinct depot role.
   571	            result.positions.pop(key)
   572	    for key, override in official.items():
   573	        result.positions.setdefault(
   574	            key, Position(account_id=override.account_id, instrument_id=override.instrument_id)
   575	        )
   576	    rows: list[dict[str, str]] = []
   577	    for (account_id, instrument_id), pos in sorted(result.positions.items()):
   578	        row = conn.execute(
   579	            """
   580	            SELECT a.account_name, p.name AS platform, i.name, i.ticker, i.isin, i.asset_class, i.currency,
   581	                   i.hedge_status, i.instrument_status, i.valuation_policy, i.corporate_action_status,
   582	                   mp.close AS latest_price, mp.currency AS latest_price_currency, mp.price_date AS latest_price_date,
   583	                   mp.provider AS latest_price_provider
   584	            FROM accounts a JOIN platforms p ON p.platform_id=a.platform_id
   585	            JOIN instruments i ON i.instrument_id=?
   586	            LEFT JOIN market_prices mp ON mp.market_price_id = (
   587	                SELECT market_price_id FROM market_prices
   588	                WHERE instrument_id=i.instrument_id AND quality_status IN ('fresh','ok')
   589	                ORDER BY price_date DESC, created_at DESC LIMIT 1
   590	            )
   591	            WHERE a.account_id=?
   592	            """,
   593	            (instrument_id, account_id),
   594	        ).fetchone()
   595	        override = official.get((account_id, instrument_id))
   596	        effective_quantity = (
   597	            override.quantity + post_snapshot_deltas.get(instrument_id, Decimal("0"))
   598	            if override
   599	            else pos.quantity
   600	        )
   601	        if override and effective_quantity <= 0:
   602	            continue
   603	        market_value = audited.get((account_id, instrument_id))
   604	        use_market_run = bool(
   605	            market_value
   606	            and (not override or market_value.as_of > override.snapshot_date)
   607	        )
   608	        if override and not use_market_run:
   609	            quantity = effective_quantity
   610	            market_price = override.market_price_original
   611	            market_value_chf = (
   612	                override.market_value_chf * effective_quantity / override.quantity
   613	                if override.quantity
   614	                else Decimal("0")
   615	            )
   616	            price_currency = override.price_currency
   617	            price_date = override.valuation_at
   618	            price_provider = "PostFinance official snapshot"
   619	            valuation_source = "postfinance_official_import"
   620	        elif use_market_run and market_value:
   621	            quantity = effective_quantity
   622	            market_price = market_value.close
   623	            market_value_chf = (
   624	                market_value.value_chf * effective_quantity / market_value.quantity
   625	                if market_value.quantity
   626	                else market_value.value_chf
   627	            )
   628	            price_currency = market_value.currency
   629	            price_date = market_value.as_of
   630	            price_provider = market_value.provider
   631	            valuation_source = "audited_market_run"
   632	        else:
   633	            quantity = pos.quantity
   634	            market_price = pos.market_price_original
   635	            market_value_chf = pos.market_value_chf
   636	            price_currency = row["latest_price_currency"] if row else ""
   637	            price_date = date_text(row["latest_price_date"] if row else "")
   638	            price_provider = row["latest_price_provider"] if row else ""
   639	            valuation_source = "canonical_ledger_market"
   640	        rows.append({
   180	    row = conn.execute("SELECT MAX(trade_date) AS last_date FROM transactions WHERE account_id=? AND source_type LIKE '%csv%'", (account_id,)).fetchone()
   181	    return row["last_date"] if row else None
   182
   183
   184	def _post_snapshot_cash_deltas(
   185	    conn: Connection,
   186	    account_id: str,
   187	    snapshot_date: str,
   188	) -> dict[str, tuple[Decimal, Decimal | None]]:
   189	    rows = conn.execute(
   190	        """SELECT * FROM transactions
   191	           WHERE account_id=? AND trade_date>?
   192	             AND COALESCE(is_confirmed,0)=1 AND COALESCE(is_voided,0)=0
   193	           ORDER BY trade_date,created_at,transaction_id""",
   194	        (account_id, snapshot_date),
   195	    ).fetchall()
   196	    deltas: dict[str, tuple[Decimal, Decimal | None]] = {}
   197
   198	    def add_delta(
   199	        currency: str,
   200	        original: Decimal,
   201	        amount_chf: Decimal | None,
   202	    ) -> None:
   203	        previous_original, previous_chf = deltas.get(
   204	            currency,
   205	            (Decimal("0"), Decimal("0")),
   206	        )
   207	        deltas[currency] = (
   208	            previous_original + original,
   209	            None
   210	            if previous_chf is None or amount_chf is None
   211	            else previous_chf + amount_chf,
   212	        )
   213
   214	    direct_transaction_ids: set[str] = set()
   215	    for row in rows:
   216	        original, amount_chf = _signed_cash_delta(row)
   217	        if original == 0 and amount_chf in {None, Decimal("0")}:
   218	            continue
   219	        direct_transaction_ids.add(str(row["transaction_id"]))
   220	        add_delta(str(row["currency_original"]), original, amount_chf)
   221
   222	    # Existing equity workflows post the cash leg to the selected cash account,
   223	    # while their security transaction remains on the depot account.
   224	    postings = conn.execute(
   225	        """SELECT currency,amount_original,amount_chf,source_type,created_at
   226	             FROM cash_balances
   227	            WHERE account_id=? AND balance_date>?
   228	              AND (source_type LIKE 'vue_equity_sale:%'
   229	                   OR source_type='vue_equity_dividend')
   230	            ORDER BY balance_date,created_at,cash_balance_id""",
   231	        (account_id, snapshot_date),
   232	    ).fetchall()
   233	    for posting in postings:
   234	        source_type = str(posting["source_type"] or "")
   235	        linked_transaction_id = (
   236	            source_type.split(":", 1)[1]
   237	            if source_type.startswith("vue_equity_sale:")
   238	            else None
   239	        )
   240	        if linked_transaction_id in direct_transaction_ids:
   241	            continue
   242	        if source_type == "vue_equity_dividend":
   243	            duplicate = conn.execute(
   244	                """SELECT 1 FROM transactions
   245	                    WHERE account_id=? AND source_type='vue_equity_dividend'
   246	                      AND created_at=? AND currency_original=?
   247	                      AND CAST(net_amount_original AS TEXT)=CAST(? AS TEXT)
   248	                      AND COALESCE(is_confirmed,0)=1 AND COALESCE(is_voided,0)=0
   249	                    LIMIT 1""",
   250	                (
   251	                    account_id,
   252	                    posting["created_at"],
   253	                    posting["currency"],
   254	                    posting["amount_original"],
   255	                ),
   256	            ).fetchone()
   257	            if duplicate:
   258	                continue
   259	        add_delta(
   260	            str(posting["currency"]),
   261	            Decimal(str(posting["amount_original"])),
   262	            Decimal(str(posting["amount_chf"]))
   263	            if posting["amount_chf"] is not None
   264	            else None,
   265	        )
   266	    return deltas
   267
   268
   269	def list_cash_positions(conn: Connection) -> list[CashPosition]:
   270	    ledger = calculate_cash_balances(conn, persist_alerts=False)
   271	    rows = conn.execute(
   272	        """
   273	        SELECT a.account_id, a.account_name, a.account_type, a.currency, COALESCE(a.balance_mode,'manual') AS balance_mode, p.name AS platform
   274	        FROM accounts a JOIN platforms p ON p.platform_id=a.platform_id
   275	        WHERE a.is_active=1
   276	          AND (
   277	              a.account_type='cash'
   278	              OR a.account_id IN (
   279	                  SELECT account_id FROM postfinance_account_roles
   280	                  WHERE role='etrading_cash'
   281	              )
   282	          )
   283	        ORDER BY p.name, a.account_name
   284	        """
   285	    ).fetchall()
   286	    positions: list[CashPosition] = []
   287	    official_components = latest_official_postfinance_cash(conn)
   288	    official_by_account: dict[str, list] = {}
   289	    for component in official_components:
   290	        official_by_account.setdefault(component.account_id, []).append(component)
   291	    for row in rows:
   292	        account_id = row["account_id"]
   293	        if account_id in official_by_account:
   294	            components = {
   295	                component.currency: component
   296	                for component in official_by_account[account_id]
   297	            }
   298	            snapshot_date = next(iter(components.values())).snapshot_date
   299	            deltas = _post_snapshot_cash_deltas(conn, account_id, snapshot_date)
   300	            for currency in sorted(set(components) | set(deltas)):
   301	                component = components.get(currency)
   302	                delta_original, delta_chf = deltas.get(
   303	                    currency,
   304	                    (Decimal("0"), Decimal("0")),
   305	                )
   306	                amount_original = (
   307	                    component.amount_original if component else Decimal("0")
   308	                ) + delta_original
   309	                baseline_chf = component.amount_chf if component else Decimal("0")
   310	                amount_chf = (
   311	                    baseline_chf + delta_chf if delta_chf is not None else baseline_chf
   312	                )
   313	                status = (
   314	                    "Abgleich offen"
   315	                    if delta_chf is None
   316	                    else "Offiziell abgeglichen + Bewegungen"
   317	                    if delta_original or delta_chf
   318	                    else "Offiziell abgeglichen"
   319	                )
   320	                positions.append(
   321	                    CashPosition(
   322	                        id=f"{account_id}:{currency}",
   323	                        account_id=account_id,
   324	                        platform=row["platform"],
   325	                        account_label=row["account_name"],
   326	                        account_type=row["account_type"],
   327	                        currency=currency,
   328	                        balance_mode="official_components",
   329	                        amount=decimal_text(amount_original, 2),
   330	                        amount_chf=decimal_text(amount_chf, 2),
   331	                        calculated_balance_chf=decimal_text(amount_chf, 2),
   332	                        manual_balance_chf=None,
   333	                        csv_anchor_balance_chf=None,
   334	                        used_value_chf=decimal_text(amount_chf, 2),
   335	                        difference_chf="0.00",
   336	                        last_manual_reconciliation=snapshot_date,
   337	                        last_imported_booking=_last_imported_booking(conn, account_id),
   338	                        status=status,
   339	                    )
   340	                )
   341	            continue
   342	        manual = _latest_snapshot(conn, account_id, "manual_balance")
   343	        anchor = _latest_snapshot(conn, account_id, "csv_anchor_balance")
   344	        mode = row["balance_mode"] or "manual"
   345	        default_currency = str(row["currency"])
   346	        materialized_rows = conn.execute(
   347	            """SELECT currency,amount_original,amount_chf,source_type
   348	                 FROM cash_balances WHERE account_id=?
   349	                 ORDER BY balance_date,created_at,cash_balance_id""",
   350	            (account_id,),
   351	        ).fetchall()
   352	        materialized_by_currency: dict[str, list[Row]] = {}
   353	        for balance_row in materialized_rows:
   354	            materialized_by_currency.setdefault(
   355	                str(balance_row["currency"]), []
   356	            ).append(balance_row)
   357	        ledger_currencies = {
   358	            currency
   359	            for ledger_account_id, currency in ledger.balances
   360	            if ledger_account_id == account_id
   361	        }
   362	        # Manual balances and CSV anchors are account-level CHF controls. They must
   363	        # not be added to per-currency ledger rows from the same account.
   364	        currencies = (
   365	            {default_currency}
   366	            if manual or anchor
   367	            else set(ledger_currencies) | set(materialized_by_currency)
   368	            or {default_currency}
   369	        )
   370	        for currency in sorted(currencies):


sed -n '1,120p' src/jarvis_finance/storage/postfinance_schema.py && grep -R "portfolio_analysis_snapshots" -n src tests | head -80


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

from sqlite3 import Connection

def create_postfinance_ledger_import_v1(conn: Connection) -> None:
    """Create immutable PostFinance source, ledger, snapshot and cost-basis tables."""
    position_columns = {row[1] for row in conn.execute("PRAGMA table_info(positions_snapshot)")}
    if "source_type" not in position_columns:
        conn.execute("ALTER TABLE positions_snapshot ADD COLUMN source_type TEXT")
    if "source_reference" not in position_columns:
        conn.execute("ALTER TABLE positions_snapshot ADD COLUMN source_reference TEXT")
    conn.executescript(
        """
        CREATE TABLE IF NOT EXISTS postfinance_account_roles (
            role TEXT PRIMARY KEY CHECK(role IN ('efinance','etrading_depot','etrading_cash')),
            account_id TEXT NOT NULL REFERENCES accounts(account_id),
            source_reference_hash TEXT,
            created_at TEXT NOT NULL,
            UNIQUE(account_id, role)
        );
        CREATE TABLE IF NOT EXISTS postfinance_import_batches (
            batch_id TEXT PRIMARY KEY,
            bundle_sha256 TEXT NOT NULL UNIQUE CHECK(length(bundle_sha256)=64),
            zip_sha256 TEXT NOT NULL CHECK(length(zip_sha256)=64),
            overview_sha256 TEXT NOT NULL CHECK(length(overview_sha256)=64),
            parser_id TEXT NOT NULL,
            parser_version TEXT NOT NULL,
            db_revision TEXT NOT NULL,
            archive_reference TEXT NOT NULL,
            snapshot_at TEXT NOT NULL,
            status TEXT NOT NULL CHECK(status='confirmed'),
            audit_id TEXT NOT NULL,
            confirmed_at TEXT NOT NULL,
            confirmed_by TEXT NOT NULL DEFAULT 'user'
        );
        CREATE TABLE IF NOT EXISTS postfinance_documents (
            document_hash TEXT PRIMARY KEY CHECK(length(document_hash)=64),
            filename_hash TEXT NOT NULL CHECK(length(filename_hash)=64),
            semantic_identity TEXT NOT NULL UNIQUE CHECK(length(semantic_identity)=64),
            semantic_reference_hash TEXT NOT NULL CHECK(length(semantic_reference_hash)=64),
            document_type TEXT NOT NULL,
            document_date TEXT,
            account_reference_hash TEXT,
            account_role TEXT NOT NULL CHECK(account_role IN ('efinance','etrading_depot','etrading_cash')),
            page_count INTEGER NOT NULL CHECK(page_count > 0),
            archive_reference TEXT NOT NULL,
            parser_version TEXT NOT NULL,
            created_at TEXT NOT NULL
        );
        CREATE TABLE IF NOT EXISTS postfinance_batch_documents (
            batch_id TEXT NOT NULL REFERENCES postfinance_import_batches(batch_id),
            document_hash TEXT NOT NULL REFERENCES postfinance_documents(document_hash),
            PRIMARY KEY(batch_id, document_hash)
        );
        CREATE TABLE IF NOT EXISTS postfinance_snapshots (
            snapshot_id TEXT PRIMARY KEY,
            batch_id TEXT NOT NULL UNIQUE REFERENCES postfinance_import_batches(batch_id),
            account_id TEXT NOT NULL REFERENCES accounts(account_id),
            snapshot_at TEXT NOT NULL,
            total_chf TEXT NOT NULL,
            securities_chf TEXT NOT NULL,
            cash_chf TEXT NOT NULL,
            stocks_chf TEXT NOT NULL,
            etfs_chf TEXT NOT NULL,
            component_total_chf TEXT NOT NULL,
            difference_chf TEXT NOT NULL,
            tolerance_chf TEXT NOT NULL,
            position_count INTEGER NOT NULL,
            cash_count INTEGER NOT NULL,
            open_orders INTEGER NOT NULL,
            reconciliation_status TEXT NOT NULL CHECK(reconciliation_status IN ('matched','within_tolerance')),
            created_at TEXT NOT NULL
        );
        CREATE TABLE IF NOT EXISTS postfinance_snapshot_positions (
            snapshot_position_id TEXT PRIMARY KEY,
            snapshot_id TEXT NOT NULL REFERENCES postfinance_snapshots(snapshot_id),
            source_row_reference TEXT NOT NULL,
            instrument_id TEXT NOT NULL REFERENCES instruments(instrument_id),
            asset_class TEXT NOT NULL CHECK(asset_class IN ('stock','etf')),
            quantity TEXT NOT NULL,
            provider_average_cost_original TEXT,
            provider_cost_total_original TEXT,
            price_original TEXT NOT NULL,
            price_currency TEXT NOT NULL,
            value_chf TEXT NOT NULL,
            weight_pct TEXT NOT NULL,
            computed_cost_basis_original TEXT,
            computed_cost_basis_status TEXT NOT NULL CHECK(computed_cost_basis_status IN ('complete','partial','unavailable')),
            provenance_json TEXT NOT NULL CHECK(json_valid(provenance_json)),
            created_at TEXT NOT NULL,
            UNIQUE(snapshot_id, source_row_reference),
            UNIQUE(snapshot_id, instrument_id)
        );
        CREATE TABLE IF NOT EXISTS postfinance_snapshot_cash (
            snapshot_cash_id TEXT PRIMARY KEY,
            snapshot_id TEXT NOT NULL REFERENCES postfinance_snapshots(snapshot_id),
            currency TEXT NOT NULL,
            amount_original TEXT NOT NULL,
            fx_rate_to_chf TEXT NOT NULL,
            value_chf TEXT NOT NULL,
            created_at TEXT NOT NULL,
            UNIQUE(snapshot_id, currency)
        );
        CREATE TABLE IF NOT EXISTS postfinance_ledger_events (
            event_id TEXT PRIMARY KEY,
            event_fingerprint TEXT NOT NULL UNIQUE CHECK(length(event_fingerprint)=64),
            document_hash TEXT NOT NULL REFERENCES postfinance_documents(document_hash),
            semantic_reference_hash TEXT NOT NULL CHECK(length(semantic_reference_hash)=64),
            event_type TEXT NOT NULL CHECK(event_type IN ('buy','sell','dividend','interest','fee','internal_transfer','fx','split','corporate_action')),
            account_role TEXT NOT NULL CHECK(account_role IN ('efinance','etrading_depot','etrading_cash')),
            account_id TEXT NOT NULL REFERENCES accounts(account_id),
            occurred_on TEXT NOT NULL,
            settlement_on TEXT,
            direction TEXT NOT NULL CHECK(direction IN ('in','out','neutral')),
            instrument_id TEXT REFERENCES instruments(instrument_id),
            quantity TEXT,
            price_original TEXT,
            gross_original TEXT,
            fee_original TEXT NOT NULL DEFAULT '0',
grep: src/jarvis_finance/storage/__pycache__/migrations.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/portfolio_aggregation.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/equity_service.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/portfolio_analytics.cpython-312.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/portfolio_aggregation.cpython-312.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/equity_service.cpython-312.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/portfolio_analytics.cpython-311.pyc: binary file matches
grep: tests/__pycache__/test_postfinance_sprint13.cpython-311-pytest-9.1.1.pyc: binary file matches
grep: tests/__pycache__/test_postfinance_sprint13.cpython-311.pyc: binary file matches
grep: tests/__pycache__/test_postfinance_sprint13.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_portfolio_market_analytics_v1.cpython-311.pyc: binary file matches
grep: tests/unit/__pycache__/test_portfolio_market_analytics_v1.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_portfolio_market_analytics_v1.cpython-311-pytest-9.1.1.pyc: binary file matches
src/jarvis_finance/storage/migrations.py:1891:        CREATE TABLE IF NOT EXISTS portfolio_analysis_snapshots (
src/jarvis_finance/storage/migrations.py:1907:        CREATE INDEX IF NOT EXISTS idx_portfolio_analysis_snapshots_time
src/jarvis_finance/storage/migrations.py:1908:            ON portfolio_analysis_snapshots(as_of DESC, created_at DESC);
src/jarvis_finance/storage/migrations.py:1919:        CREATE TRIGGER IF NOT EXISTS portfolio_analysis_snapshots_immutable
src/jarvis_finance/storage/migrations.py:1920:        BEFORE UPDATE ON portfolio_analysis_snapshots
src/jarvis_finance/storage/migrations.py:1922:        CREATE TRIGGER IF NOT EXISTS portfolio_analysis_snapshots_no_delete
src/jarvis_finance/storage/migrations.py:1923:        BEFORE DELETE ON portfolio_analysis_snapshots
src/jarvis_finance/services/portfolio_aggregation.py:264:           FROM portfolio_analysis_snapshots pas
src/jarvis_finance/services/equity_service.py:32:             FROM portfolio_analysis_snapshots pas
src/jarvis_finance/services/portfolio_analytics.py:731:            """INSERT OR REPLACE INTO portfolio_analysis_snapshots(
src/jarvis_finance/services/portfolio_analytics.py:929:    latest = conn.execute("SELECT * FROM portfolio_analysis_snapshots ORDER BY as_of DESC,created_at DESC LIMIT 1").fetchone()
tests/unit/test_portfolio_market_analytics_v1.py:177:    payload = json.loads(conn.execute("SELECT summary_json FROM portfolio_analysis_snapshots").fetchone()[0])
tests/unit/test_portfolio_market_analytics_v1.py:181:    assert conn.execute("SELECT total_value_chf FROM portfolio_analysis_snapshots").fetchone()[0] == "1635.30"
tests/unit/test_portfolio_market_analytics_v1.py:270:    payload = json.loads(conn.execute("SELECT summary_json FROM portfolio_analysis_snapshots").fetchone()[0])
tests/unit/test_portfolio_market_analytics_v1.py:356:    values = json.loads(conn.execute("SELECT summary_json FROM portfolio_analysis_snapshots").fetchone()[0])["positions"]
tests/test_postfinance_sprint13.py:614:        """INSERT INTO portfolio_analysis_snapshots(analysis_snapshot_id,run_id,as_of,


pytest -q tests/unit/test_portfolio_market_analytics_v1.py tests/unit/test_positions* 2>/dev/null || true


Output:
Chunk ID: cec939
Wall time: 0.1136 seconds
Process exited with code 0
Original token count: 6
Output:

no tests ran in 0.00s


ls tests/unit | head -50 && pytest -q tests/unit/test_portfolio_market_analytics_v1.py


Output:
Chunk ID: ec8687
Wall time: 1.0637 seconds
Process exited with code 5
Original token count: 420
Output:
__pycache__
test_api_cors_origins.py
test_api_write_security.py
test_audit.py
test_broker_mapping_import_wizard.py
test_broker_parser_dry_run_pipeline.py
test_budget_analytics_dashboard_v2.py
test_budget_analytics_data_explorer_v1.py
test_budget_categories_cleanup_duplicates.py
test_budget_categories_ux_fix.py
test_budget_duplicate_handling_hotfix.py
test_budget_import_production_v1.py
test_budget_import_production_v2.py
test_budget_import_status_audit_v1.py
test_budget_import_upload_ux_v1.py
test_budget_internal_transfer_hotfix.py
test_budget_monthly_import_rule_learning_v1.py
test_budget_overview_plans.py
test_budget_phase1.py
test_budget_phase11.py
test_budget_phase110_2026_core.py
test_budget_phase111_migros_monthly_analytics.py
test_budget_phase111_status_income_formatting.py
test_budget_phase12_seed_review.py
test_budget_phase14_workview.py
test_budget_phase15_source_aware_imports.py
test_budget_phase16_user_rules.py
test_budget_phase17_ux_workflow.py
test_budget_phase18_core_flow.py
test_budget_phase19_review_productivity.py
test_budget_planning_forecast_v1.py
test_budget_planning_usability_v2.py
test_budget_review_bulk_assign_confirm.py
test_budget_ux_foundation_v3.py
test_budget_ux_v2.py
test_budget_workflow_help_ux.py
test_cash_truewealth_management.py
test_coingecko_provider.py
test_crypto.py
test_crypto_core.py
test_crypto_manage.py
test_crypto_price_update.py
test_crypto_report.py
test_crypto_trader_bridge.py
test_csv_importers_extended.py
test_dashboard_functional_actions_v2.py
test_dashboard_functional_ux_stabilization.py
test_dashboard_mvp.py
test_dashboard_mvp_regressions.py
test_dashboard_real_ui_acceptance.py

no tests ran in 0.73s


find tests -name '*portfolio*analytics*' -o -name '*market*analytics*'


Output:
Chunk ID: 9aa373
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 75
Output:
tests/unit/__pycache__/test_portfolio_market_analytics_v1.cpython-311.pyc
tests/unit/__pycache__/test_portfolio_market_analytics_v1.cpython-311-pytest-9.0.2.pyc
tests/unit/__pycache__/test_portfolio_market_analytics_v1.cpython-311-pytest-9.1.1.pyc
tests/unit/test_portfolio_market_analytics_v1.py


ls -l tests/unit/test_portfolio_market_analytics_v1.py && head -5 tests/unit/test_portfolio_market_analytics_v1.py && grep -n "def test" tests/unit/test_portfolio_market_analytics_v1.py | head


Output:
Chunk ID: f40808
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 46
Output:
-rw-r-----+ 1 agent agent 25598 Jul 27 14:35 tests/unit/test_portfolio_market_analytics_v1.py
from __future__ import annotations

import fcntl
import json
from decimal import Decimal


python -m py_compile src/jarvis_finance/services/portfolio_aggregation.py src/jarvis_finance/services/equity_service.py src/jarvis_finance/services/cash_service.py src/jarvis_finance/dashboard/data.py


Output:
Chunk ID: f66fc8
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 0
Output:


grep -R "vue_equity_" -n src tests | head -100


Output:
Chunk ID: 5f9c33
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 852
Output:
grep: src/jarvis_finance/services/__pycache__/cash_service.cpython-311.pyc: binary file matches
grep: src/jarvis_finance/services/__pycache__/manual_entry_service.cpython-311.pyc: binary file matches
grep: tests/__pycache__/test_postfinance_sprint13.cpython-311-pytest-9.1.1.pyc: binary file matches
grep: tests/__pycache__/test_postfinance_sprint13.cpython-311.pyc: binary file matches
grep: tests/__pycache__/test_postfinance_sprint13.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_dashboard_functional_actions_v2.cpython-311-pytest-9.0.2.pyc: binary file matches
grep: tests/unit/__pycache__/test_dashboard_functional_actions_v2.cpython-311-pytest-9.1.1.pyc: binary file matches
grep: tests/unit/__pycache__/test_dashboard_functional_actions_v2.cpython-311.pyc: binary file matches
src/jarvis_finance/services/manual_entry_service.py:916:        VALUES (?, 'sell', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'vue_equity_sale', 1, ?, ?, ?)
src/jarvis_finance/services/manual_entry_service.py:919:        conn.execute("INSERT INTO cash_balances(cash_balance_id, account_id, balance_date, currency, amount_original, fx_rate_to_chf, amount_chf, source_type, quality_status, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (_uuid("cash"), request.cash_account_id, request.trade_date, request.currency.upper(), _format_decimal(net), fx_rate, net_chf, f"vue_equity_sale:{tx_id}", "ok" if fx_status != "missing" else "missing_fx", request.note, ts))
src/jarvis_finance/services/manual_entry_service.py:955:        VALUES (?, 'dividend', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'vue_equity_dividend', 1, ?, ?, ?)
src/jarvis_finance/services/manual_entry_service.py:957:    conn.execute("INSERT INTO cash_balances(cash_balance_id, account_id, balance_date, currency, amount_original, fx_rate_to_chf, amount_chf, source_type, quality_status, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'vue_equity_dividend', ?, ?, ?)", (_uuid("cash"), request.cash_account_id, request.payment_date, request.currency.upper(), _format_decimal(net), fx_rate, net_chf, "ok" if fx_status != "missing" else "missing_fx", request.note, ts))
src/jarvis_finance/services/cash_service.py:228:              AND (source_type LIKE 'vue_equity_sale:%'
src/jarvis_finance/services/cash_service.py:229:                   OR source_type='vue_equity_dividend')
src/jarvis_finance/services/cash_service.py:237:            if source_type.startswith("vue_equity_sale:")
src/jarvis_finance/services/cash_service.py:242:        if source_type == "vue_equity_dividend":
src/jarvis_finance/services/cash_service.py:245:                    WHERE account_id=? AND source_type='vue_equity_dividend'
src/jarvis_finance/services/cash_service.py:381:                        ("vue_equity_sale:", "vue_equity_dividend")
tests/unit/test_dashboard_functional_actions_v2.py:102:    assert conn.execute("SELECT COUNT(*) FROM cash_balances WHERE source_type LIKE 'vue_equity_sale:%'").fetchone()[0] == 1
tests/unit/test_dashboard_functional_actions_v2.py:128:    assert conn.execute("SELECT COUNT(*) FROM cash_balances WHERE source_type='vue_equity_dividend'").fetchone()[0] == 1
tests/test_postfinance_sprint13.py:703:                  '1.1','11','vue_equity_dividend','ok','2026-07-27T10:00:02Z')"""
tests/test_postfinance_sprint13.py:863:                  'vue_equity_dividend','ok','2026-08-02T10:03:00Z')""",


sed -n '840,970p' src/jarvis_finance/services/manual_entry_service.py


Output:
Chunk ID: 92a965
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 2716
Output:
        raise HTTPException(status_code=422, detail=str(exc)) from exc
    audit_row = conn.execute("SELECT audit_id FROM audit_log WHERE entity_id=? ORDER BY created_at DESC LIMIT 1", (entity_id,)).fetchone()
    audit_id = audit_row["audit_id"] if audit_row else _audit(conn, action="crypto_action_confirm", entity_type="crypto_transaction", entity_id=entity_id, payload=request.model_dump(), note=request.note, quality_status="ok")
    return ConfirmResponse(status="confirmed", entity_id=entity_id, audit_id=audit_id, message="Crypto Aktion bestätigt · Audit geschrieben")

def _position_ids(position_id: str) -> tuple[str, str]:
    if ":" not in position_id:
        raise HTTPException(status_code=422, detail="position_id must be account_id:instrument_id")
    return tuple(position_id.split(":", 1))  # type: ignore[return-value]

def _current_equity_quantity(conn: Connection, *, account_id: str, instrument_id: str) -> Decimal:
    row = conn.execute("SELECT SUM(CAST(quantity AS TEXT)) AS quantity FROM transactions WHERE account_id=? AND instrument_id=? AND coalesce(is_voided,0)=0 AND quantity IS NOT NULL", (account_id, instrument_id)).fetchone()
    return Decimal(str(row["quantity"] or "0")) if row else Decimal("0")

def _weighted_average_cost(conn: Connection, *, account_id: str, instrument_id: str) -> Decimal | None:
    rows = conn.execute("SELECT quantity, gross_amount_original FROM transactions WHERE account_id=? AND instrument_id=? AND coalesce(is_voided,0)=0 AND CAST(quantity AS TEXT) > '0' AND gross_amount_original IS NOT NULL", (account_id, instrument_id)).fetchall()
    total_qty = Decimal("0"); total_cost = Decimal("0")
    for row in rows:
        qty = Decimal(str(row["quantity"] or "0"))
        cost = Decimal(str(row["gross_amount_original"] or "0"))
        if qty > 0:
            total_qty += qty; total_cost += cost
    if total_qty <= 0:
        return None
    return total_cost / total_qty

def _cash_account_exists(conn: Connection, account_id: str | None) -> bool:
    if not account_id:
        return False
    return conn.execute("SELECT account_id FROM accounts WHERE account_id=? AND is_active=1", (account_id,)).fetchone() is not None

def preview_equity_sell(conn: Connection, request: EquitySellPreviewRequest) -> PreviewResponse:
    account_id, instrument_id = _position_ids(request.position_id)
    quantity = _decimal(request.quantity, "quantity")
    price = _decimal(request.price_original, "price_original")
    fees = _decimal_non_negative(request.fees_original, "fees_original")
    current = _current_equity_quantity(conn, account_id=account_id, instrument_id=instrument_id)
    if quantity > current:
        raise HTTPException(status_code=422, detail="Verkauf würde Bestand negativ machen")
    gross = quantity * price
    net = gross - fees
    if net < 0:
        raise HTTPException(status_code=422, detail="fees exceed gross proceeds")
    currency = request.currency.upper()
    fx_status, fx_rate, _fx_source, fx_warning = _fx_for_explicit_save(conn, currency=currency, trade_date=request.trade_date)
    amount_chf = _chf_amount(_format_decimal(net), Decimal(fx_rate) if fx_rate else None)
    avg = _weighted_average_cost(conn, account_id=account_id, instrument_id=instrument_id)
    warnings = [fx_warning] if fx_warning else []
    if avg is None:
        warnings.append("Einstand unvollständig: realisierter P&L nur eingeschränkt verfügbar.")
    return PreviewResponse(preview_id=_uuid("preview"), asset_class="equity", summary=f"Verkauf {quantity} · Erlös {currency} {_format_decimal(net)}", amount_chf=amount_chf, fx_status=fx_status, warnings=warnings)

def confirm_equity_sell(conn: Connection, request: EquitySellConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=400, detail="Explicit confirm required")
    preview = preview_equity_sell(conn, request)
    account_id, instrument_id = _position_ids(request.position_id)
    quantity = _decimal(request.quantity, "quantity")
    price = _decimal(request.price_original, "price_original")
    fees = _decimal_non_negative(request.fees_original, "fees_original")
    gross = quantity * price
    net = gross - fees
    fx_status, fx_rate, fx_source, _ = _fx_for_explicit_save(conn, currency=request.currency, trade_date=request.trade_date)
    gross_chf = _chf_amount(_format_decimal(gross), Decimal(fx_rate) if fx_rate else None)
    fee_chf = _chf_amount(_format_decimal(fees), Decimal(fx_rate) if fx_rate else None)
    net_chf = _chf_amount(_format_decimal(net), Decimal(fx_rate) if fx_rate else None)
    tx_id = _uuid("tx")
    ts = _now()
    conn.execute("""
        INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, price_original, gross_amount_original, fee_original, net_amount_original, currency_original, fx_rate_to_chf, fx_source, fx_status, gross_amount_chf, fee_chf, net_amount_chf, source_type, is_confirmed, quality_status, notes, created_at)
        VALUES (?, 'sell', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'vue_equity_sale', 1, ?, ?, ?)
    """, (tx_id, account_id, instrument_id, request.trade_date, _format_decimal(-quantity), _format_decimal(price), _format_decimal(gross), _format_decimal(fees), _format_decimal(net), request.currency.upper(), fx_rate, fx_source, fx_status, gross_chf, fee_chf, net_chf, "ok" if fx_status != "missing" else "missing_fx", request.note, ts))
    if _cash_account_exists(conn, request.cash_account_id):
        conn.execute("INSERT INTO cash_balances(cash_balance_id, account_id, balance_date, currency, amount_original, fx_rate_to_chf, amount_chf, source_type, quality_status, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", (_uuid("cash"), request.cash_account_id, request.trade_date, request.currency.upper(), _format_decimal(net), fx_rate, net_chf, f"vue_equity_sale:{tx_id}", "ok" if fx_status != "missing" else "missing_fx", request.note, ts))
    audit_id = _audit(conn, action="equity_sell_confirm", entity_type="position", entity_id=request.position_id, payload={**request.model_dump(), "net_amount_original": _format_decimal(net), "gross_amount_original": _format_decimal(gross)}, note=request.note, quality_status="ok" if fx_status != "missing" else "missing_fx")
    conn.commit()
    return ConfirmResponse(status="confirmed", entity_id=tx_id, audit_id=audit_id, message="Verkauf bestätigt · Audit geschrieben")

def preview_equity_dividend(conn: Connection, request: EquityDividendPreviewRequest) -> PreviewResponse:
    _position_ids(request.position_id)
    gross = _decimal(request.gross_amount, "gross_amount")
    taxes = _decimal_non_negative(request.swiss_tax, "swiss_tax") + _decimal_non_negative(request.foreign_tax, "foreign_tax") + _decimal_non_negative(request.other_deductions, "other_deductions")
    net = gross - taxes
    if net < 0:
        raise HTTPException(status_code=422, detail="deductions exceed gross dividend")
    if not _cash_account_exists(conn, request.cash_account_id):
        raise HTTPException(status_code=404, detail="Cash account not found")
    fx_status, fx_rate, _fx_source, fx_warning = _fx_for_explicit_save(conn, currency=request.currency, trade_date=request.payment_date)
    warnings = [fx_warning] if fx_warning else []
    return PreviewResponse(preview_id=_uuid("preview"), asset_class="income", summary=f"Dividende netto {request.currency.upper()} {_format_decimal(net)}", amount_chf=_chf_amount(_format_decimal(net), Decimal(fx_rate) if fx_rate else None), fx_status=fx_status, warnings=warnings)

def confirm_equity_dividend(conn: Connection, request: EquityDividendConfirmRequest) -> ConfirmResponse:
    if not request.confirm:
        raise HTTPException(status_code=400, detail="Explicit confirm required")
    preview = preview_equity_dividend(conn, request)
    account_id, instrument_id = _position_ids(request.position_id)
    gross = _decimal(request.gross_amount, "gross_amount")
    taxes = _decimal_non_negative(request.swiss_tax, "swiss_tax") + _decimal_non_negative(request.foreign_tax, "foreign_tax") + _decimal_non_negative(request.other_deductions, "other_deductions")
    net = gross - taxes
    fx_status, fx_rate, fx_source, _ = _fx_for_explicit_save(conn, currency=request.currency, trade_date=request.payment_date)
    gross_chf = _chf_amount(_format_decimal(gross), Decimal(fx_rate) if fx_rate else None)
    tax_chf = _chf_amount(_format_decimal(taxes), Decimal(fx_rate) if fx_rate else None)
    net_chf = _chf_amount(_format_decimal(net), Decimal(fx_rate) if fx_rate else None)
    tx_id = _uuid("tx")
    ts = _now()
    conn.execute("""
        INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, gross_amount_original, tax_original, net_amount_original, currency_original, fx_rate_to_chf, fx_source, fx_status, gross_amount_chf, tax_chf, net_amount_chf, source_type, is_confirmed, quality_status, notes, created_at)
        VALUES (?, 'dividend', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'vue_equity_dividend', 1, ?, ?, ?)
    """, (tx_id, account_id, instrument_id, request.payment_date, _format_decimal(gross), _format_decimal(taxes), _format_decimal(net), request.currency.upper(), fx_rate, fx_source, fx_status, gross_chf, tax_chf, net_chf, "ok" if fx_status != "missing" else "missing_fx", request.note, ts))
    conn.execute("INSERT INTO cash_balances(cash_balance_id, account_id, balance_date, currency, amount_original, fx_rate_to_chf, amount_chf, source_type, quality_status, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 'vue_equity_dividend', ?, ?, ?)", (_uuid("cash"), request.cash_account_id, request.payment_date, request.currency.upper(), _format_decimal(net), fx_rate, net_chf, "ok" if fx_status != "missing" else "missing_fx", request.note, ts))
    audit_id = _audit(conn, action="equity_dividend_confirm", entity_type="position", entity_id=request.position_id, payload={**request.model_dump(), "net_amount_original": _format_decimal(net), "tax_original": _format_decimal(taxes)}, note=request.note, quality_status="ok" if fx_status != "missing" else "missing_fx")
    conn.commit()
    return ConfirmResponse(status="confirmed", entity_id=tx_id, audit_id=audit_id, message="Dividende bestätigt · Audit geschrieben")

def cleanup_review_artifacts_preview(conn: Connection) -> CleanupPreviewResponse:
    plan = identify_review_test_equity_artifacts(conn)
    warnings = []
    if plan.artifact_count:
        warnings.append("Review-/Test-Artefakte werden archiviert; manuelle Positionen bleiben erhalten.")
    return CleanupPreviewResponse(accounts=len(plan.account_ids), instruments=len(plan.instrument_ids), transactions=len(plan.transaction_ids), alerts=len(plan.alert_ids), manual_positions_preserved=plan.manual_position_count, warnings=warnings)


Output:
Chunk ID: 89c89f
Wall time: 0.0000 seconds
Process exited with code 0
Original token count: 911
Output:
# Sprint 13 hotfix: global portfolio aggregation

## Scope

This hotfix changes the shared portfolio read model only. It does not rewrite or delete official source bytes, import batches, ledger events, snapshots, cost-basis lots, canonical projections, or historical manual values.

## Root cause

The PostFinance overview contains currency rows that combine a true currency cash amount and rounded FX rate with a CHF column representing the whole portfolio's currency exposure. Sprint 13 projected that exposure column as cash. The two projected values therefore summed to the official account total instead of the official cash subtotal.

A second inclusion collision existed in equity: instrument-bearing cash-settlement ledger rows could create shadow holdings on the trading-cash role, while the official holdings belong to the separate depot role. An older market-analysis snapshot could also override a newer official holdings snapshot.

## Contract

- Account totals from `postfinance_official_import` are controls, never additive holdings.
- Cash is resolved from the explicit `etrading_cash` role, original currency amounts, rounded source FX, and the signed-off cash subtotal.
- Rounded component CHF values are deterministically reconciled pro rata to the official subtotal; no adjustment asset is created.
- Holdings are resolved from the explicit `etrading_depot` role and official position snapshot.
- Cash-settlement ledger rows remain available for audit and cost basis but do not create holdings on the cash role when the official depot snapshot covers the instrument.
- A released market-analysis value replaces an official source value only when its `as_of` is newer. The effective valuation timestamp remains visible.
- Historical manual placeholders and immutable canonical rows remain present but are superseded by provenance-aware read-model selection.
- TrueWealth and non-PostFinance aggregation rules remain unchanged.

## Shared consumers

The shared cash resolver feeds the cash API and dashboard/Command Center cash overview. The role-aware official position resolver feeds dashboard positions, equity APIs, overview/advisor allocations, and future portfolio-analysis runs. The PostFinance summary uses the same reconciled source components.

## Delayed review findings reverified at the merge base

All three findings were rerun read-only against exact commit `17ad9ad9eba002b74ef18664fa1a008d926bf5fd` before applying the hotfix:

1. Same-day conflicts in `positions_snapshot` and `cash_balances`: **stale/already fixed**. The preview conflict test passed and confirm was not required to discover the conflict.
2. Immutable canonical projections in `transactions`, `positions_snapshot`, and `cash_balances`: **stale/already fixed**. Update and delete trigger assertions passed for all three tables.
3. `/api/postfinance/summary` privacy allowlist: **stale/already fixed**. The summary contract test passed and exposes no account, batch, snapshot, instrument, ISIN, document, customer, or account-number identifiers.

Evidence: the three exact base-commit tests passed (`3 passed`). The findings must be rerun again with the complete suite and final review at the hotfix HEAD.

## Regression coverage

The synthetic production-shape contract includes 22 positions, two currency cash rows, a misleading account-total-sized exposure projection, a historical manual placeholder, a separate E-Finance role, internal transfers, TrueWealth, and both older and newer market-analysis snapshots. It verifies no double counting, read-only GET behavior, historical preservation, privacy, and repeated-import idempotency.


grep -n "snapshot.cash\|snapshot.positions" -n src/jarvis_finance/services/postfinance_service.py | head -30 && sed -n '760,815p' src/jarvis_finance/services/postfinance_service.py