     1|from __future__ import annotations
     2|
     3|import argparse
     4|from pathlib import Path
     5|
     6|from jarvis_finance.config.settings import ensure_runtime_dirs, load_settings
     7|from jarvis_finance.market.providers import CoinGeckoClient, refresh_crypto_prices
     8|from jarvis_finance.quality.data_quality import check_crypto_data_quality
     9|from jarvis_finance.quality.git_safety import assert_safe, scan_path
    10|from jarvis_finance.runtime.backup import backup_runtime_db, restore_runtime_db, verify_backup
    11|from jarvis_finance.storage.database import connect
    12|from jarvis_finance.storage.migrations import apply_migrations, get_schema_version
    13|
    14|
    15|def main(argv: list[str] | None = None) -> int:
    16|    parser = argparse.ArgumentParser(prog="finance")
    17|    sub = parser.add_subparsers(dest="command", required=True)
    18|    sub.add_parser("init-db")
    19|    sub.add_parser("migrate")
    20|    scan = sub.add_parser("git-safety-scan")
    21|    scan.add_argument("path", nargs="?", default=".")
    22|    prices = sub.add_parser("update-crypto-prices")
    23|    prices.add_argument("--currency", default="CHF")
    24|    prices.add_argument("--max-age-seconds", type=int, default=3600)
    25|    prices.add_argument("--sleep-seconds", type=float, default=0.0)
    26|    prices.add_argument("--max-retries", type=int, default=2)
    27|    prices.add_argument("--initial-backoff", type=float, default=1.0)
    28|    prices.add_argument("--max-backoff", type=float, default=8.0)
    29|    prices.add_argument("--only-missing", action="store_true")
    30|    prices.add_argument("--only-stale", action="store_true")
    31|    prices.add_argument("--only-symbol")
    32|    prices.add_argument("--limit", type=int)
    33|    prices.add_argument("--dry-run", action="store_true")
    34|    dq = sub.add_parser("check-data-quality")
    35|    dq.add_argument("--scope", choices=["crypto"], default="crypto")
    36|    dq.add_argument("--resolve-fixed", action="store_true")
    37|    dq.add_argument("--dry-run", action="store_true")
    38|    dq.add_argument("--currency", default="CHF")
    39|    dq.add_argument("--max-age-seconds", type=int, default=86_400)
    40|    sub.add_parser("backup-runtime-db")
    41|    restore = sub.add_parser("restore-runtime-db")
    42|    restore.add_argument("--from", dest="backup_file", required=True)
    43|    restore.add_argument("--yes", action="store_true")
    44|    verify = sub.add_parser("verify-backup")
    45|    verify.add_argument("--file", required=True)
    46|    args = parser.parse_args(argv)
    47|
    48|    if args.command in {"init-db", "migrate"}:
    49|        settings = load_settings()
    50|        ensure_runtime_dirs(settings)
    51|        conn = connect(settings.db_path)
    52|        apply_migrations(conn)
    53|        print(f"SCHEMA_VERSION={get_schema_version(conn)}")
    54|        return 0
    55|    if args.command == "git-safety-scan":
    56|        findings = scan_path(Path(args.path))
    57|        if findings:
    58|            for finding in findings:
    59|                print(f"{finding.path}: {finding.reason}")
    60|            return 1
    61|        assert_safe(Path(args.path))
    62|        print("GIT_SAFETY_OK")
    63|        return 0
    64|    if args.command == "update-crypto-prices":
    65|        settings = load_settings()
    66|        ensure_runtime_dirs(settings)
    67|        conn = connect(settings.db_path)
    68|        try:
    69|            apply_migrations(conn)
    70|            result = refresh_crypto_prices(
    71|                conn,
    72|                provider=CoinGeckoClient(
    73|                    max_retries=args.max_retries,
    74|                    initial_backoff_seconds=args.initial_backoff,
    75|                    max_backoff_seconds=args.max_backoff,
    76|                ),
    77|                currency=args.currency,
    78|                max_age_seconds=args.max_age_seconds,
    79|                only_missing=args.only_missing,
    80|                only_stale=args.only_stale,
    81|                only_symbol=args.only_symbol,
    82|                limit=args.limit,
    83|                dry_run=args.dry_run,
    84|                sleep_seconds=args.sleep_seconds,
    85|            )
    86|            print(
    87|                "CRYPTO_PRICE_UPDATE "
    88|                f"currency={result.currency} total_assets={result.total_assets} "
    89|                f"updated={result.updated_count} cached={result.cached_count} "
    90|                f"skipped={result.skipped_count} stale={result.stale_count} "
    91|                f"warnings={result.warning_count} errors={result.error_count} "
    92|                f"assets_still_missing_local_price={result.missing_local_price_count} "
    93|                f"dry_run={str(result.dry_run).lower()}"
    94|            )
    95|            return 1 if result.error_count else 0
    96|        finally:
    97|            conn.close()
    98|    if args.command == "check-data-quality":
    99|        settings = load_settings()
   100|        ensure_runtime_dirs(settings)
   101|        conn = connect(settings.db_path)
   102|        try:
   103|            apply_migrations(conn)
   104|            result = check_crypto_data_quality(
   105|                conn,
   106|                currency=args.currency,
   107|                max_age_seconds=args.max_age_seconds,
   108|                resolve_fixed=args.resolve_fixed,
   109|                dry_run=args.dry_run,
   110|            )
   111|            print(
   112|                "DATA_QUALITY_CHECK "
   113|                f"scope={result.scope} active_crypto_assets={result.active_crypto_assets} "
   114|                f"fresh_price_assets={result.fresh_price_assets} "
   115|                f"active_stale_price_alerts_before={result.active_stale_price_alerts_before} "
   116|                f"active_stale_price_alerts_after={result.active_stale_price_alerts_after} "
   117|                f"resolved_alerts={result.resolved_alerts} "
   118|                f"warnings={result.warnings} errors={result.errors} "
   119|                f"dry_run={str(result.dry_run).lower()}"
   120|            )
   121|            return 1 if result.errors else 0
   122|        finally:
   123|            conn.close()
   124|    if args.command == "backup-runtime-db":
   125|        settings = load_settings()
   126|        ensure_runtime_dirs(settings)
   127|        result = backup_runtime_db(
   128|            db_path=settings.db_path,
   129|            backups_dir=settings.runtime_paths.backups_dir,
   130|            repo_root=Path.cwd(),
   131|        )
   132|        print(
   133|            "RUNTIME_DB_BACKUP "
   134|            f"backup_file={result.backup_file} checksum_file={result.checksum_file} "
   135|            f"sha256={result.sha256} size_bytes={result.size_bytes}"
   136|        )
   137|        return 0
   138|    if args.command == "verify-backup":
   139|        result = verify_backup(backup_file=Path(args.file), repo_root=Path.cwd())
   140|        print(
   141|            "BACKUP_VERIFY "
   142|            f"backup_file={result.backup_file} checksum_file={result.checksum_file} "
   143|            f"exists={str(result.exists).lower()} checksum_ok={str(result.checksum_ok).lower()} "
   144|            f"sha256={result.sha256}"
   145|        )
   146|        return 0 if result.exists and result.checksum_ok else 1
   147|    if args.command == "restore-runtime-db":
   148|        settings = load_settings()
   149|        ensure_runtime_dirs(settings)
   150|        result = restore_runtime_db(
   151|            backup_file=Path(args.backup_file),
   152|            db_path=settings.db_path,
   153|            repo_root=Path.cwd(),
   154|            yes=args.yes,
   155|        )
   156|        print(
   157|            "RUNTIME_DB_RESTORE "
   158|            f"source_file={result.source_file} restored_to={result.restored_to} "
   159|            f"verified={str(result.verified).lower()}"
   160|        )
   161|        return 0
   162|    return 2
   163|
   164|
   165|if __name__ == "__main__":
   166|    raise SystemExit(main())
   167|


     1|from __future__ import annotations
     2|
     3|import json
     4|import uuid
     5|from datetime import datetime, timezone
     6|from sqlite3 import Connection
     7|from typing import Any
     8|
     9|
    10|def utc_now() -> str:
    11|    return datetime.now(timezone.utc).isoformat()
    12|
    13|
    14|def record_audit_event(
    15|    conn: Connection,
    16|    *,
    17|    source: str,
    18|    action: str,
    19|    entity_type: str,
    20|    entity_id: str,
    21|    old_values: dict[str, Any] | None = None,
    22|    new_values: dict[str, Any] | None = None,
    23|    user_text_note: str | None = None,
    24|    confirmed: bool = True,
    25|    created_by: str = "system",
    26|) -> str:
    27|    audit_id = str(uuid.uuid4())
    28|    now = utc_now()
    29|    conn.execute(
    30|        """
    31|        INSERT INTO audit_log(
    32|            audit_id, timestamp, source, action, entity_type, entity_id,
    33|            old_values_json, new_values_json, user_text_note, confirmed,
    34|            confirmation_timestamp, created_by, created_at
    35|        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    36|        """,
    37|        (
    38|            audit_id,
    39|            now,
    40|            source,
    41|            action,
    42|            entity_type,
    43|            entity_id,
    44|            json.dumps(old_values or {}, sort_keys=True),
    45|            json.dumps(new_values or {}, sort_keys=True),
    46|            user_text_note,
    47|            1 if confirmed else 0,
    48|            now if confirmed else None,
    49|            created_by,
    50|            now,
    51|        ),
    52|    )
    53|    return audit_id
    54|


     1|from __future__ import annotations
     2|
     3|from dataclasses import dataclass
     4|from decimal import Decimal
     5|from sqlite3 import Connection
     6|from typing import Protocol, Sequence
     7|
     8|from jarvis_finance.imports.common import stable_id, utc_now
     9|from jarvis_finance.quality.alerts import create_alert
    10|
    11|
    12|class FxProvider(Protocol):
    13|    def get_rate(self, base_currency: str, quote_currency: str, rate_date: str | None = None) -> Decimal | None: ...
    14|
    15|
    16|@dataclass
    17|class FxUpdateResult:
    18|    updated_count: int = 0
    19|    cached_count: int = 0
    20|    skipped_count: int = 0
    21|    warning_count: int = 0
    22|    error_count: int = 0
    23|    resolved_alerts: int = 0
    24|    dry_run: bool = False
    25|
    26|
    27|def _resolve_alerts(conn: Connection, *, rule_id: str, entity_ids: Sequence[str] | None = None) -> int:
    28|    now = utc_now()
    29|    params: list[object] = [rule_id]
    30|    where = "rule_id=? AND status='active'"
    31|    if entity_ids is not None:
    32|        if not entity_ids:
    33|            return 0
    34|        where += " AND entity_id IN (" + ",".join("?" for _ in entity_ids) + ")"
    35|        params.extend(entity_ids)
    36|    rows = conn.execute(f"SELECT alert_id FROM alerts WHERE {where}", tuple(params)).fetchall()
    37|    for row in rows:
    38|        conn.execute("UPDATE alerts SET status='resolved', resolved_at=?, last_seen_at=? WHERE alert_id=?", (now, now, row["alert_id"]))
    39|    return len(rows)
    40|
    41|
    42|def _transaction_instruments_for_currency(conn: Connection, currency: str) -> list[str]:
    43|    return [
    44|        row["instrument_id"]
    45|        for row in conn.execute(
    46|            """
    47|            SELECT DISTINCT instrument_id FROM transactions
    48|            WHERE instrument_id IS NOT NULL AND currency_original=? AND COALESCE(is_voided,0)=0
    49|            """,
    50|            (currency.upper(),),
    51|        ).fetchall()
    52|    ]
    53|
    54|
    55|def _mark_chf_transactions_not_needed(conn: Connection) -> int:
    56|    now = utc_now()
    57|    rows = conn.execute(
    58|        """
    59|        SELECT transaction_id FROM transactions
    60|        WHERE currency_original='CHF'
    61|          AND (fx_status IS NULL OR fx_status='missing' OR fx_rate_to_chf IS NULL OR fx_source IS NULL)
    62|        """
    63|    ).fetchall()
    64|    for row in rows:
    65|        conn.execute(
    66|            "UPDATE transactions SET fx_rate_to_chf='1', fx_source='not_needed', fx_status='not_needed', updated_at=? WHERE transaction_id=?",
    67|            (now, row["transaction_id"]),
    68|        )
    69|    if rows:
    70|        _resolve_alerts(conn, rule_id="missing_fx", entity_ids=_transaction_instruments_for_currency(conn, "CHF"))
    71|        conn.commit()
    72|    return len(rows)
    73|
    74|
    75|def upsert_fx_rate(
    76|    conn: Connection,
    77|    *,
    78|    base_currency: str,
    79|    quote_currency: str,
    80|    rate_date: str,
    81|    rate: Decimal,
    82|    provider: str,
    83|    rate_type: str,
    84|    quality_status: str = "fresh",
    85|) -> str:
    86|    base = base_currency.upper()
    87|    quote = quote_currency.upper()
    88|    fx_rate_id = stable_id("fx", base, quote, rate_date, provider, rate_type)
    89|    conn.execute(
    90|        """
    91|        INSERT INTO fx_rates(
    92|            fx_rate_id, base_currency, quote_currency, rate_date, rate, provider,
    93|            rate_type, quality_status, created_at
    94|        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    95|        ON CONFLICT(base_currency, quote_currency, rate_date, provider, rate_type)
    96|        DO UPDATE SET rate=excluded.rate, quality_status=excluded.quality_status
    97|        """,
    98|        (fx_rate_id, base, quote, rate_date, format(rate, "f"), provider, rate_type, quality_status, utc_now()),
    99|    )
   100|    return fx_rate_id
   101|
   102|
   103|def latest_fx_rate(conn: Connection, *, base_currency: str, quote_currency: str = "CHF", rate_date: str | None = None):
   104|    base = base_currency.upper(); quote = quote_currency.upper()
   105|    if base == quote:
   106|        return None
   107|    if rate_date:
   108|        return conn.execute(
   109|            """
   110|            SELECT * FROM fx_rates WHERE base_currency=? AND quote_currency=? AND rate_date<=? AND quality_status IN ('fresh','ok','manual')
   111|            ORDER BY rate_date DESC, created_at DESC LIMIT 1
   112|            """,
   113|            (base, quote, rate_date),
   114|        ).fetchone()
   115|    return conn.execute(
   116|        """
   117|        SELECT * FROM fx_rates WHERE base_currency=? AND quote_currency=? AND quality_status IN ('fresh','ok','manual')
   118|        ORDER BY rate_date DESC, created_at DESC LIMIT 1
   119|        """,
   120|        (base, quote),
   121|    ).fetchone()
   122|
   123|
   124|def get_fx_rate_to_chf(conn: Connection, *, base_currency: str, rate_date: str | None = None, resolve_fixed: bool = False) -> Decimal | None:
   125|    base = base_currency.upper()
   126|    if base == "CHF":
   127|        _mark_chf_transactions_not_needed(conn)
   128|        return Decimal("1")
   129|    row = latest_fx_rate(conn, base_currency=base, quote_currency="CHF", rate_date=rate_date)
   130|    if row is None:
   131|        return None
   132|    if resolve_fixed:
   133|        _resolve_alerts(conn, rule_id="missing_fx", entity_ids=_transaction_instruments_for_currency(conn, base))
   134|        conn.commit()
   135|    return Decimal(str(row["rate"]))
   136|
   137|
   138|def update_fx_rates(
   139|    conn: Connection,
   140|    *,
   141|    provider: FxProvider,
   142|    currencies: Sequence[str],
   143|    rate_date: str,
   144|    latest: bool = False,
   145|    missing_only: bool = False,
   146|    dry_run: bool = False,
   147|    resolve_fixed: bool = True,
   148|) -> FxUpdateResult:
   149|    result = FxUpdateResult(dry_run=dry_run)
   150|    for cur in dict.fromkeys(c.upper() for c in currencies):
   151|        if cur == "CHF":
   152|            if not dry_run:
   153|                _mark_chf_transactions_not_needed(conn)
   154|            result.cached_count += 1
   155|            continue
   156|        if missing_only and latest_fx_rate(conn, base_currency=cur, quote_currency="CHF", rate_date=rate_date):
   157|            result.cached_count += 1
   158|            continue
   159|        rate = provider.get_rate(cur, "CHF", None if latest else rate_date)
   160|        if rate is None:
   161|            result.warning_count += 1
   162|            create_alert(conn, priority="kritisch", category="fx", entity_type="currency_pair", entity_id=f"{cur}/CHF", rule_id="missing_fx", message="FX rate to CHF is missing.", evidence={"base_currency": cur, "quote_currency": "CHF", "rate_date": rate_date}, fingerprint=f"missing_fx_rate:{cur}:CHF:{rate_date}")
   163|            continue
   164|        if not dry_run:
   165|            upsert_fx_rate(conn, base_currency=cur, quote_currency="CHF", rate_date=rate_date, rate=rate, provider=getattr(provider, "name", "mock"), rate_type="latest" if latest else "close", quality_status="fresh")
   166|            if resolve_fixed:
   167|                result.resolved_alerts += _resolve_alerts(conn, rule_id="missing_fx", entity_ids=_transaction_instruments_for_currency(conn, cur))
   168|        result.updated_count += 1
   169|    if not dry_run:
   170|        conn.commit()
   171|    return result
   172|


terminal

PYTHONPATH=src python - <<'PY'
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations
conn=connect_memory(); apply_migrations(conn)
print('ok')
for t in ['market_prices','fx_rates','instrument_price_mappings']:
 print(t, [r['name'] for r in conn.execute(f'PRAGMA table_info({t})').fetchall()])
 print('idx', [dict(r) for r in conn.execute(f'PRAGMA index_list({t})').fetchall()])
PY


Output:
ok
market_prices ['market_price_id', 'instrument_id', 'price_date', 'price_timestamp', 'open', 'high', 'low', 'close', 'adjusted_close', 'currency', 'provider', 'provider_symbol', 'quality_status', 'created_at', 'provider_market', 'error_message']
idx [{'seq': 0, 'name': 'idx_market_prices_instrument_date', 'unique': 0, 'origin': 'c', 'partial': 0}, {'seq': 1, 'name': 'idx_market_prices_unique_provider_date', 'unique': 1, 'origin': 'c', 'partial': 0}, {'seq': 2, 'name': 'sqlite_autoindex_market_prices_1', 'unique': 1, 'origin': 'pk', 'partial': 0}]
fx_rates ['fx_rate_id', 'base_currency', 'quote_currency', 'rate_date', 'rate_timestamp', 'rate', 'provider', 'rate_type', 'quality_status', 'created_at']
idx [{'seq': 0, 'name': 'idx_fx_rates_pair_date', 'unique': 0, 'origin': 'c', 'partial': 0}, {'seq': 1, 'name': 'idx_fx_rates_unique_provider_date', 'unique': 1, 'origin': 'c', 'partial': 0}, {'seq': 2, 'name': 'sqlite_autoindex_fx_rates_1', 'unique': 1, 'origin': 'pk', 'partial': 0}]
instrument_price_mappings ['mapping_id', 'instrument_id', 'isin', 'ticker', 'exchange', 'currency', 'provider', 'provider_symbol', 'provider_market', 'mapping_status', 'confidence', 'notes', 'created_at', 'updated_at']
idx [{'seq': 0, 'name': 'idx_instrument_price_mappings_provider_symbol', 'unique': 0, 'origin': 'c', 'partial': 0}, {'seq': 1, 'name': 'idx_instrument_price_mappings_status', 'unique': 0, 'origin': 'c', 'partial': 0}, {'seq': 2, 'name': 'sqlite_autoindex_instrument_price_mappings_2', 'unique': 1, 'origin': 'u', 'partial': 0}, {'seq': 3, 'name': 'sqlite_autoindex_instrument_price_mappings_1', 'unique': 1, 'origin': 'pk', 'partial': 0}]


Explanation:
**Planning for tests**

I think I need to add some tests, maybe even for the command line interface (CLI), but full pytest could uncover more issues. I’m aware that we might have around 160 current tests, and we’ll run them later. Implementing more tests feels essential, especially for patching the fx/rates. I should also import the record audit and ensure that we have a function to recheck the transaction FX status with a summary. Let's get to patching!

patch

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


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/fx/rates.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/fx/rates.py
@@ -5,6 +5,7 @@
 from sqlite3 import Connection
 from typing import Protocol, Sequence

+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 FxUpdateResult:
    updated_count: int = 0
    cached_count: int = 0
    skipped_count: int = 0
    warning_count: int = 0
    error_count: int = 0
    resolved_alerts: int = 0
    dry_run: bool = False

@dataclass
class FxRecheckResult:
    transaction_count: int = 0
    chf_positions: int = 0
    foreign_currency_positions: int = 0
    justified_missing_fx_alerts: int = 0
    false_positive_missing_fx_alerts: int = 0
    corrected_transactions: int = 0
    resolved_alerts: int = 0
    audit_events: int = 0
    dry_run: bool = False


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/fx/rates.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/fx/rates.py
@@ -22,6 +22,19 @@
     warning_count: int = 0
     error_count: int = 0
     resolved_alerts: int = 0
+    dry_run: bool = False
+
+
+@dataclass
+class FxRecheckResult:
+    transaction_count: int = 0
+    chf_positions: int = 0
+    foreign_currency_positions: int = 0
+    justified_missing_fx_alerts: int = 0
+    false_positive_missing_fx_alerts: int = 0
+    corrected_transactions: int = 0
+    resolved_alerts: int = 0
+    audit_events: int = 0
     dry_run: bool = False


def _mark_chf_transactions_not_needed(conn: Connection, *, dry_run: bool = False) -> int:
    now = utc_now()
    rows = conn.execute(
        """
        SELECT transaction_id, fx_rate_to_chf, fx_source, fx_status FROM transactions
        WHERE currency_original='CHF'
          AND (fx_status IS NULL OR fx_status='missing' OR fx_rate_to_chf IS NULL OR fx_source IS NULL)
        """
    ).fetchall()
    if dry_run:
        return len(rows)
    for row in rows:
        conn.execute(
            "UPDATE transactions SET fx_rate_to_chf='1', fx_source='not_needed', fx_status='not_needed', updated_at=? WHERE transaction_id=?",
            (now, row["transaction_id"]),
        )
        record_audit_event(
            conn,
            source="fx_recheck",
            action="set_chf_fx_not_needed",
            entity_type="transaction",
            entity_id=row["transaction_id"],
            old_values={"fx_rate_to_chf": row["fx_rate_to_chf"], "fx_source": row["fx_source"], "fx_status": row["fx_status"]},
            new_values={"fx_rate_to_chf": "1", "fx_source": "not_needed", "fx_status": "not_needed"},
            user_text_note="CHF original currency requires no FX conversion.",
        )
    if rows:
        _resolve_alerts(conn, rule_id="missing_fx", entity_ids=_transaction_instruments_for_currency(conn, "CHF"))
        conn.commit()
    return len(rows)

def _mark_chf_transactions_not_needed(conn: Connection) -> int:
    now = utc_now()
    rows = conn.execute(
        """
        SELECT transaction_id FROM transactions
        WHERE currency_original='CHF'
          AND (fx_status IS NULL OR fx_status='missing' OR fx_rate_to_chf IS NULL OR fx_source IS NULL)
        """
    ).fetchall()
    for row in rows:
        conn.execute(
            "UPDATE transactions SET fx_rate_to_chf='1', fx_source='not_needed', fx_status='not_needed', updated_at=? WHERE transaction_id=?",
            (now, row["transaction_id"]),
        )
    if rows:
        _resolve_alerts(conn, rule_id="missing_fx", entity_ids=_transaction_instruments_for_currency(conn, "CHF"))
        conn.commit()
    return len(rows)


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/fx/rates.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/fx/rates.py
@@ -66,19 +66,31 @@
     ]

-def _mark_chf_transactions_not_needed(conn: Connection) -> int:
+def _mark_chf_transactions_not_needed(conn: Connection, *, dry_run: bool = False) -> int:
     now = utc_now()
     rows = conn.execute(
         """
-        SELECT transaction_id FROM transactions
+        SELECT transaction_id, fx_rate_to_chf, fx_source, fx_status FROM transactions
         WHERE currency_original='CHF'
           AND (fx_status IS NULL OR fx_status='missing' OR fx_rate_to_chf IS NULL OR fx_source IS NULL)
         """
     ).fetchall()
+    if dry_run:
+        return len(rows)
     for row in rows:
         conn.execute(
             "UPDATE transactions SET fx_rate_to_chf='1', fx_source='not_needed', fx_status='not_needed', updated_at=? WHERE transaction_id=?",
             (now, row["transaction_id"]),
+        )
+        record_audit_event(
+            conn,
+            source="fx_recheck",
+            action="set_chf_fx_not_needed",
+            entity_type="transaction",
+            entity_id=row["transaction_id"],
+            old_values={"fx_rate_to_chf": row["fx_rate_to_chf"], "fx_source": row["fx_source"], "fx_status": row["fx_status"]},
+            new_values={"fx_rate_to_chf": "1", "fx_source": "not_needed", "fx_status": "not_needed"},
+            user_text_note="CHF original currency requires no FX conversion.",
         )
     if rows:
         _resolve_alerts(conn, rule_id="missing_fx", entity_ids=_transaction_instruments_for_currency(conn, "CHF"))


Explanation:
**Considering data operations**

I’m thinking about data operations and the process of using “upsert.” It seems like I need to insert data before I can perform that action properly. I wonder if this means I should check whether the data already exists, because sometimes just a straightforward upsert might not work as intended. I’m curious about the best way to structure this, ensuring everything runs smoothly. Let's double-check to make sure I have everything right.

def recheck_transaction_fx_status(conn: Connection, *, source_type: str | None = None, dry_run: bool = False) -> FxRecheckResult:
    result = FxRecheckResult(dry_run=dry_run)
    params: list[object] = []
    where = "COALESCE(is_voided,0)=0 AND transaction_type='initial_position_snapshot'"
    if source_type:
        where += " AND source_type=?"
        params.append(source_type)
    rows = conn.execute(
        f"""
        SELECT transaction_id, instrument_id, currency_original, fx_status, fx_rate_to_chf, fx_source
        FROM transactions
        WHERE {where}
        """,
        tuple(params),
    ).fetchall()
    result.transaction_count = len(rows)
    chf_ids: list[str] = []
    foreign_ids: list[str] = []
    false_positive_ids: list[str] = []
    justified_ids: list[str] = []
    for row in rows:
        cur = (row["currency_original"] or "").upper()
        inst = row["instrument_id"]
        if cur == "CHF":
            result.chf_positions += 1
            if inst:
                chf_ids.append(inst)
            if row["fx_status"] == "missing" or row["fx_rate_to_chf"] in (None, "") or row["fx_source"] in (None, ""):
                result.corrected_transactions += 1
                if inst:
                    false_positive_ids.append(inst)
                if not dry_run:
                    conn.execute(
                        "UPDATE transactions SET fx_rate_to_chf='1', fx_source='not_needed', fx_status='not_needed', updated_at=? WHERE transaction_id=?",
                        (utc_now(), row["transaction_id"]),
                    )
                    record_audit_event(
                        conn,
                        source="fx_recheck",
                        action="set_chf_fx_not_needed",
                        entity_type="transaction",
                        entity_id=row["transaction_id"],
                        old_values={"fx_rate_to_chf": row["fx_rate_to_chf"], "fx_source": row["fx_source"], "fx_status": row["fx_status"]},
                        new_values={"fx_rate_to_chf": "1", "fx_source": "not_needed", "fx_status": "not_needed"},
                        user_text_note="CHF initial snapshot requires no FX conversion.",
                    )
                    result.audit_events += 1
        else:
            result.foreign_currency_positions += 1
            if inst:
                foreign_ids.append(inst)
            if row["fx_status"] == "missing" or not latest_fx_rate(conn, base_currency=cur, quote_currency="CHF", rate_date=None):
                if inst:
                    justified_ids.append(inst)
    if false_positive_ids:
        result.false_positive_missing_fx_alerts = len(conn.execute(
            "SELECT alert_id FROM alerts WHERE status='active' AND rule_id='missing_fx' AND entity_id IN (" + ",".join("?" for _ in false_positive_ids) + ")",
            tuple(false_positive_ids),
        ).fetchall())
        if not dry_run:
            result.resolved_alerts += _resolve_alerts(conn, rule_id="missing_fx", entity_ids=false_positive_ids)
    if justified_ids:
        result.justified_missing_fx_alerts = len(conn.execute(
            "SELECT alert_id FROM alerts WHERE status='active' AND rule_id='missing_fx' AND entity_id IN (" + ",".join("?" for _ in justified_ids) + ")",
            tuple(justified_ids),
        ).fetchall())
    if not dry_run:
        conn.commit()
    return result

def upsert_fx_rate(


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/fx/rates.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/fx/rates.py
@@ -96,6 +96,77 @@
         _resolve_alerts(conn, rule_id="missing_fx", entity_ids=_transaction_instruments_for_currency(conn, "CHF"))
         conn.commit()
     return len(rows)
+
+
+def recheck_transaction_fx_status(conn: Connection, *, source_type: str | None = None, dry_run: bool = False) -> FxRecheckResult:
+    result = FxRecheckResult(dry_run=dry_run)
+    params: list[object] = []
+    where = "COALESCE(is_voided,0)=0 AND transaction_type='initial_position_snapshot'"
+    if source_type:
+        where += " AND source_type=?"
+        params.append(source_type)
+    rows = conn.execute(
+        f"""
+        SELECT transaction_id, instrument_id, currency_original, fx_status, fx_rate_to_chf, fx_source
+        FROM transactions
+        WHERE {where}
+        """,
+        tuple(params),
+    ).fetchall()
+    result.transaction_count = len(rows)
+    chf_ids: list[str] = []
+    foreign_ids: list[str] = []
+    false_positive_ids: list[str] = []
+    justified_ids: list[str] = []
+    for row in rows:
+        cur = (row["currency_original"] or "").upper()
+        inst = row["instrument_id"]
+        if cur == "CHF":
+            result.chf_positions += 1
+            if inst:
+                chf_ids.append(inst)
+            if row["fx_status"] == "missing" or row["fx_rate_to_chf"] in (None, "") or row["fx_source"] in (None, ""):
+                result.corrected_transactions += 1
+                if inst:
+                    false_positive_ids.append(inst)
+                if not dry_run:
+                    conn.execute(
+                        "UPDATE transactions SET fx_rate_to_chf='1', fx_source='not_needed', fx_status='not_needed', updated_at=? WHERE transaction_id=?",
+                        (utc_now(), row["transaction_id"]),
+                    )
+                    record_audit_event(
+                        conn,
+                        source="fx_recheck",
+                        action="set_chf_fx_not_needed",
+                        entity_type="transaction",
+                        entity_id=row["transaction_id"],
+                        old_values={"fx_rate_to_chf": row["fx_rate_to_chf"], "fx_source": row["fx_source"], "fx_status": row["fx_status"]},
+                        new_values={"fx_rate_to_chf": "1", "fx_source": "not_needed", "fx_status": "not_needed"},
+                        user_text_note="CHF initial snapshot requires no FX conversion.",
+                    )
+                    result.audit_events += 1
+        else:
+            result.foreign_currency_positions += 1
+            if inst:
+                foreign_ids.append(inst)
+            if row["fx_status"] == "missing" or not latest_fx_rate(conn, base_currency=cur, quote_currency="CHF", rate_date=None):
+                if inst:
+                    justified_ids.append(inst)
+    if false_positive_ids:
+        result.false_positive_missing_fx_alerts = len(conn.execute(
+            "SELECT alert_id FROM alerts WHERE status='active' AND rule_id='missing_fx' AND entity_id IN (" + ",".join("?" for _ in false_positive_ids) + ")",
+            tuple(false_positive_ids),
+        ).fetchall())
+        if not dry_run:
+            result.resolved_alerts += _resolve_alerts(conn, rule_id="missing_fx", entity_ids=false_positive_ids)
+    if justified_ids:
+        result.justified_missing_fx_alerts = len(conn.execute(
+            "SELECT alert_id FROM alerts WHERE status='active' AND rule_id='missing_fx' AND entity_id IN (" + ",".join("?" for _ in justified_ids) + ")",
+            tuple(justified_ids),
+        ).fetchall())
+    if not dry_run:
+        conn.commit()
+    return result

 def upsert_fx_rate(


Explanation:
**Evaluating code updates**

I’m considering some code updates. I need to check the foreign IDs and linting. Maybe I should add tests for audits, since the existing ones could be failing. I think updating the `_mark` calls could help, but first, I’ll check on the Pyright type issues. I’m also thinking I need a provider CLI, perhaps adding `NullFxProvider` or `Fixed`. Using Mock could be useful, and keeping the CLI options simple would be ideal. I’ll remember to update the main imports too!

from jarvis_finance.config.settings import ensure_runtime_dirs, load_settings
from jarvis_finance.fx.providers import MockFxProvider
from jarvis_finance.fx.rates import recheck_transaction_fx_status, update_fx_rates
from jarvis_finance.market.providers import CoinGeckoClient, refresh_crypto_prices
from jarvis_finance.market_data.prices import MockEquityPriceProvider, refresh_market_prices
from jarvis_finance.quality.data_quality import check_crypto_data_quality

from jarvis_finance.config.settings import ensure_runtime_dirs, load_settings
from jarvis_finance.market.providers import CoinGeckoClient, refresh_crypto_prices
from jarvis_finance.quality.data_quality import check_crypto_data_quality


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/cli/main.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/cli/main.py
@@ -4,7 +4,10 @@
 from pathlib import Path

 from jarvis_finance.config.settings import ensure_runtime_dirs, load_settings
+from jarvis_finance.fx.providers import MockFxProvider
+from jarvis_finance.fx.rates import recheck_transaction_fx_status, update_fx_rates
 from jarvis_finance.market.providers import CoinGeckoClient, refresh_crypto_prices
+from jarvis_finance.market_data.prices import MockEquityPriceProvider, refresh_market_prices
 from jarvis_finance.quality.data_quality import check_crypto_data_quality
 from jarvis_finance.quality.git_safety import assert_safe, scan_path
 from jarvis_finance.runtime.backup import backup_runtime_db, restore_runtime_db, verify_backup


    dq.add_argument("--max-age-seconds", type=int, default=86_400)
    fx = sub.add_parser("update-fx-rates")
    fx.add_argument("--date", dest="rate_date")
    fx.add_argument("--latest", action="store_true")
    fx.add_argument("--missing-only", action="store_true")
    fx.add_argument("--dry-run", action="store_true")
    fx.add_argument("--currency", action="append", dest="currencies")
    fx.add_argument("--mock-rate", action="append", default=[], help="Test-only BASE=RATE, e.g. USD=0.9")
    fx.add_argument("--recheck-transactions", action="store_true")
    fx.add_argument("--source-type")
    market_prices = sub.add_parser("update-market-prices")
    market_prices.add_argument("--asset-class", choices=["etf", "equity"], required=True)
    market_prices.add_argument("--only-missing", action="store_true")
    market_prices.add_argument("--only-stale", action="store_true")
    market_prices.add_argument("--only-isin")
    market_prices.add_argument("--limit", type=int)
    market_prices.add_argument("--dry-run", action="store_true")
    market_prices.add_argument("--date", dest="price_date")
    market_prices.add_argument("--mock-price", action="append", default=[], help="Test-only SYMBOL=PRICE")
    sub.add_parser("backup-runtime-db")

    dq.add_argument("--max-age-seconds", type=int, default=86_400)
    sub.add_parser("backup-runtime-db")


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/cli/main.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/cli/main.py
@@ -40,6 +40,24 @@
     dq.add_argument("--dry-run", action="store_true")
     dq.add_argument("--currency", default="CHF")
     dq.add_argument("--max-age-seconds", type=int, default=86_400)
+    fx = sub.add_parser("update-fx-rates")
+    fx.add_argument("--date", dest="rate_date")
+    fx.add_argument("--latest", action="store_true")
+    fx.add_argument("--missing-only", action="store_true")
+    fx.add_argument("--dry-run", action="store_true")
+    fx.add_argument("--currency", action="append", dest="currencies")
+    fx.add_argument("--mock-rate", action="append", default=[], help="Test-only BASE=RATE, e.g. USD=0.9")
+    fx.add_argument("--recheck-transactions", action="store_true")
+    fx.add_argument("--source-type")
+    market_prices = sub.add_parser("update-market-prices")
+    market_prices.add_argument("--asset-class", choices=["etf", "equity"], required=True)
+    market_prices.add_argument("--only-missing", action="store_true")
+    market_prices.add_argument("--only-stale", action="store_true")
+    market_prices.add_argument("--only-isin")
+    market_prices.add_argument("--limit", type=int)
+    market_prices.add_argument("--dry-run", action="store_true")
+    market_prices.add_argument("--date", dest="price_date")
+    market_prices.add_argument("--mock-price", action="append", default=[], help="Test-only SYMBOL=PRICE")
     sub.add_parser("backup-runtime-db")
     restore = sub.add_parser("restore-runtime-db")
     restore.add_argument("--from", dest="backup_file", required=True)


Explanation:
**Considering imports**

I need to think about importing the Decimal module. It seems like it could simplify any calculations that require high precision, especially with financial data. Hmm, I should check if this will improve the accuracy of my results. It’s important to ensure I’m using the right tools for the task at hand. So, I'll double-check whether I've used the Decimal module before and if it’s suitable for my needs right now.

import argparse
from decimal import Decimal
from pathlib import Path

import argparse
from pathlib import Path


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/cli/main.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/cli/main.py
@@ -1,6 +1,7 @@
 from __future__ import annotations

 import argparse
+from decimal import Decimal
 from pathlib import Path

 from jarvis_finance.config.settings import ensure_runtime_dirs, load_settings


    if args.command == "check-data-quality":
        settings = load_settings()


        finally:
            conn.close()
    if args.command == "update-fx-rates":
        settings = load_settings()
        ensure_runtime_dirs(settings)
        conn = connect(settings.db_path)
        try:
            apply_migrations(conn)
            if args.recheck_transactions:
                result = recheck_transaction_fx_status(conn, source_type=args.source_type, dry_run=args.dry_run)
                print(
                    "FX_RECHECK "
                    f"transactions={result.transaction_count} chf_positions={result.chf_positions} "
                    f"foreign_currency_positions={result.foreign_currency_positions} "
                    f"justified_missing_fx_alerts={result.justified_missing_fx_alerts} "
                    f"false_positive_missing_fx_alerts={result.false_positive_missing_fx_alerts} "
                    f"corrected_transactions={result.corrected_transactions} resolved_alerts={result.resolved_alerts} "
                    f"audit_events={result.audit_events} dry_run={str(result.dry_run).lower()}"
                )
                return 0
            if not args.rate_date and not args.latest:
                parser.error("update-fx-rates requires --date or --latest unless --recheck-transactions is used")
            mock_rates: dict[tuple[str, str, str], Decimal | None] = {}
            for item in args.mock_rate:
                base, rate = item.split("=", 1)
                mock_rates[(base.upper(), "CHF", args.rate_date or "latest")] = Decimal(rate)
            currencies = args.currencies or sorted({r["currency_original"] for r in conn.execute("SELECT DISTINCT currency_original FROM transactions WHERE currency_original IS NOT NULL").fetchall()})
            provider = MockFxProvider(mock_rates)
            result = update_fx_rates(conn, provider=provider, currencies=currencies, rate_date=args.rate_date or "latest", latest=args.latest, missing_only=args.missing_only, dry_run=args.dry_run)
            print(
                "FX_RATE_UPDATE "
                f"updated={result.updated_count} cached={result.cached_count} skipped={result.skipped_count} "
                f"warnings={result.warning_count} errors={result.error_count} resolved_alerts={result.resolved_alerts} "
                f"dry_run={str(result.dry_run).lower()}"
            )
            return 1 if result.error_count else 0
        finally:
            conn.close()
    if args.command == "update-market-prices":
        settings = load_settings()
        ensure_runtime_dirs(settings)
        conn = connect(settings.db_path)
        try:
            apply_migrations(conn)
            prices = {}
            for item in args.mock_price:
                symbol, price = item.split("=", 1)
                prices[symbol] = Decimal(price)
            result = refresh_market_prices(
                conn,
                provider=MockEquityPriceProvider(prices),
                asset_class=args.asset_class,
                price_date=args.price_date,
                only_missing=args.only_missing,
                only_stale=args.only_stale,
                only_isin=args.only_isin,
                limit=args.limit,
                dry_run=args.dry_run,
            )
            print(
                "MARKET_PRICE_UPDATE "
                f"asset_class={result.asset_class} total_mappings={result.total_mappings} updated={result.updated_count} "
                f"cached={result.cached_count} skipped={result.skipped_count} stale={result.stale_count} "
                f"warnings={result.warning_count} errors={result.error_count} dry_run={str(result.dry_run).lower()}"
            )
            return 1 if result.error_count else 0
        finally:
            conn.close()
    if args.command == "check-data-quality":


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/cli/main.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/cli/main.py
@@ -117,6 +117,72 @@
             return 1 if result.error_count else 0
         finally:
             conn.close()
+    if args.command == "update-fx-rates":
+        settings = load_settings()
+        ensure_runtime_dirs(settings)
+        conn = connect(settings.db_path)
+        try:
+            apply_migrations(conn)
+            if args.recheck_transactions:
+                result = recheck_transaction_fx_status(conn, source_type=args.source_type, dry_run=args.dry_run)
+                print(
+                    "FX_RECHECK "
+                    f"transactions={result.transaction_count} chf_positions={result.chf_positions} "
+                    f"foreign_currency_positions={result.foreign_currency_positions} "
+                    f"justified_missing_fx_alerts={result.justified_missing_fx_alerts} "
+                    f"false_positive_missing_fx_alerts={result.false_positive_missing_fx_alerts} "
+                    f"corrected_transactions={result.corrected_transactions} resolved_alerts={result.resolved_alerts} "
+                    f"audit_events={result.audit_events} dry_run={str(result.dry_run).lower()}"
+                )
+                return 0
+            if not args.rate_date and not args.latest:
+                parser.error("update-fx-rates requires --date or --latest unless --recheck-transactions is used")
+            mock_rates: dict[tuple[str, str, str], Decimal | None] = {}
+            for item in args.mock_rate:
+                base, rate = item.split("=", 1)
+                mock_rates[(base.upper(), "CHF", args.rate_date or "latest")] = Decimal(rate)
+            currencies = args.currencies or sorted({r["currency_original"] for r in conn.execute("SELECT DISTINCT currency_original FROM transactions WHERE currency_original IS NOT NULL").fetchall()})
+            provider = MockFxProvider(mock_rates)
+            result = update_fx_rates(conn, provider=provider, currencies=currencies, rate_date=args.rate_date or "latest", latest=args.latest, missing_only=args.missing_only, dry_run=args.dry_run)
+            print(
+                "FX_RATE_UPDATE "
+                f"updated={result.updated_count} cached={result.cached_count} skipped={result.skipped_count} "
+                f"warnings={result.warning_count} errors={result.error_count} resolved_alerts={result.resolved_alerts} "
+                f"dry_run={str(result.dry_run).lower()}"
+            )
+            return 1 if result.error_count else 0
+        finally:
+            conn.close()
+    if args.command == "update-market-prices":
+        settings = load_settings()
+        ensure_runtime_dirs(settings)
+        conn = connect(settings.db_path)
+        try:
+            apply_migrations(conn)
+            prices = {}
+            for item in args.mock_price:
+                symbol, price = item.split("=", 1)
+                prices[symbol] = Decimal(price)
+            result = refresh_market_prices(
+                conn,
+                provider=MockEquityPriceProvider(prices),
+                asset_class=args.asset_class,
+                price_date=args.price_date,
+                only_missing=args.only_missing,
+                only_stale=args.only_stale,
+                only_isin=args.only_isin,
+                limit=args.limit,
+                dry_run=args.dry_run,
+            )
+            print(
+                "MARKET_PRICE_UPDATE "
+                f"asset_class={result.asset_class} total_mappings={result.total_mappings} updated={result.updated_count} "
+                f"cached={result.cached_count} skipped={result.skipped_count} stale={result.stale_count} "
+                f"warnings={result.warning_count} errors={result.error_count} dry_run={str(result.dry_run).lower()}"
+            )
+            return 1 if result.error_count else 0
+        finally:
+            conn.close()
     if args.command == "check-data-quality":
         settings = load_settings()
         ensure_runtime_dirs(settings)


     1|from __future__ import annotations
     2|
     3|from decimal import Decimal
     4|
     5|
     6|class MockFxProvider:
     7|    name = "mock"
     8|
     9|    def __init__(self, rates: dict[tuple[str, str, str], Decimal | str | None]) -> None:
    10|        self.rates = {(a.upper(), b.upper(), d): (Decimal(str(v)) if v is not None else None) for (a, b, d), v in rates.items()}
    11|
    12|    def get_rate(self, base_currency: str, quote_currency: str, rate_date: str | None = None) -> Decimal | None:
    13|        if base_currency.upper() == quote_currency.upper():
    14|            return Decimal("1")
    15|        if rate_date is None:
    16|            candidates = [(d, v) for (a, b, d), v in self.rates.items() if a == base_currency.upper() and b == quote_currency.upper()]
    17|            if not candidates:
    18|                return None
    19|            return sorted(candidates, key=lambda item: item[0])[-1][1]
    20|        return self.rates.get((base_currency.upper(), quote_currency.upper(), rate_date))
    21|


from jarvis_finance.fx.rates import get_fx_rate_to_chf, recheck_transaction_fx_status, update_fx_rates, upsert_fx_rate

from jarvis_finance.fx.rates import get_fx_rate_to_chf, update_fx_rates, upsert_fx_rate


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_fx_market_data_phase_c.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_fx_market_data_phase_c.py
@@ -4,7 +4,7 @@

 import pytest

-from jarvis_finance.fx.rates import get_fx_rate_to_chf, update_fx_rates, upsert_fx_rate
+from jarvis_finance.fx.rates import get_fx_rate_to_chf, recheck_transaction_fx_status, update_fx_rates, upsert_fx_rate
 from jarvis_finance.fx.overrides import set_manual_fx_override
 from jarvis_finance.fx.providers import MockFxProvider
 from jarvis_finance.market_data.prices import MockEquityPriceProvider, refresh_market_prices, store_market_price


def test_chf_fx_is_not_needed_and_missing_alert_can_be_resolved() -> None:
    conn = setup_conn()
    conn.execute("""
        INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, currency_original, fx_status, source_type, is_confirmed, quality_status, created_at)
        VALUES('tx-chf','initial_position_snapshot','a1','chf1','2025-12-31','1','CHF','missing','test',1,'warning','now')
    """)
    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('al-chf','kritisch','ledger','instrument','chf1','missing','active','now')")

    assert get_fx_rate_to_chf(conn, base_currency='CHF') == Decimal('1')
    row = conn.execute("SELECT fx_rate_to_chf, fx_source, fx_status FROM transactions WHERE transaction_id='tx-chf'").fetchone()
    assert row['fx_rate_to_chf'] == '1'
    assert row['fx_source'] == 'not_needed'
    assert row['fx_status'] == 'not_needed'

def test_recheck_transaction_fx_status_corrects_chf_false_positive_with_audit() -> None:
    conn = setup_conn()
    conn.execute("""
        INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, currency_original, fx_status, source_type, is_confirmed, quality_status, created_at)
        VALUES('tx-chf','initial_position_snapshot','a1','chf1','2025-12-31','1','CHF','missing','true_wealth',1,'warning','now')
    """)
    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('al-chf','kritisch','ledger','instrument','chf1','missing_fx','missing','active','now')")

    result = recheck_transaction_fx_status(conn, source_type='true_wealth')

    assert result.transaction_count == 1
    assert result.chf_positions == 1
    assert result.false_positive_missing_fx_alerts == 1
    assert result.corrected_transactions == 1
    assert result.resolved_alerts == 1
    assert result.audit_events == 1
    assert conn.execute("SELECT status FROM alerts WHERE alert_id='al-chf'").fetchone()['status'] == 'resolved'
    assert conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action='set_chf_fx_not_needed'").fetchone()['c'] == 1

def test_update_fx_rates_stores_usd_eur_and_resolves_missing_fx() -> None:

def test_chf_fx_is_not_needed_and_missing_alert_can_be_resolved() -> None:
    conn = setup_conn()
    conn.execute("""
        INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, currency_original, fx_status, source_type, is_confirmed, quality_status, created_at)
        VALUES('tx-chf','initial_position_snapshot','a1','chf1','2025-12-31','1','CHF','missing','test',1,'warning','now')
    """)
    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('al-chf','kritisch','ledger','instrument','chf1','missing','active','now')")

    assert get_fx_rate_to_chf(conn, base_currency='CHF') == Decimal('1')
    row = conn.execute("SELECT fx_rate_to_chf, fx_source, fx_status FROM transactions WHERE transaction_id='tx-chf'").fetchone()
    assert row['fx_rate_to_chf'] == '1'
    assert row['fx_source'] == 'not_needed'
    assert row['fx_status'] == 'not_needed'

def test_update_fx_rates_stores_usd_eur_and_resolves_missing_fx() -> None:


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_fx_market_data_phase_c.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_fx_market_data_phase_c.py
@@ -24,22 +24,42 @@
     return conn

-def test_chf_to_chf_is_not_needed_and_missing_fx_alert_resolves() -> None:
+def test_chf_fx_is_not_needed_and_missing_alert_can_be_resolved() -> None:
     conn = setup_conn()
     conn.execute("""
         INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, currency_original, fx_status, source_type, is_confirmed, quality_status, created_at)
-        VALUES('tx1','initial_position_snapshot','a1','chf1','2025-12-31','1','CHF','missing','test',1,'warning','now')
+        VALUES('tx-chf','initial_position_snapshot','a1','chf1','2025-12-31','1','CHF','missing','test',1,'warning','now')
     """)
-    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('al1','kritisch','ledger','instrument','chf1','missing_fx','missing','active','now')")
+    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('al-chf','kritisch','ledger','instrument','chf1','missing','active','now')")

-    rate = get_fx_rate_to_chf(conn, base_currency="CHF", rate_date="2025-12-31", resolve_fixed=True)
+    assert get_fx_rate_to_chf(conn, base_currency='CHF') == Decimal('1')
+    row = conn.execute("SELECT fx_rate_to_chf, fx_source, fx_status FROM transactions WHERE transaction_id='tx-chf'").fetchone()
+    assert row['fx_rate_to_chf'] == '1'
+    assert row['fx_source'] == 'not_needed'
+    assert row['fx_status'] == 'not_needed'

-    row = conn.execute("SELECT fx_rate_to_chf, fx_source, fx_status FROM transactions WHERE transaction_id='tx1'").fetchone()
-    alert = conn.execute("SELECT status FROM alerts WHERE alert_id='al1'").fetchone()
-    assert rate == Decimal("1")
-    assert row["fx_rate_to_chf"] == "1"
-    assert row["fx_source"] == "not_needed"
-    assert row["fx_status"] == "not_needed"
+
+def test_recheck_transaction_fx_status_corrects_chf_false_positive_with_audit() -> None:
+    conn = setup_conn()
+    conn.execute("""
+        INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, currency_original, fx_status, source_type, is_confirmed, quality_status, created_at)
+        VALUES('tx-chf','initial_position_snapshot','a1','chf1','2025-12-31','1','CHF','missing','true_wealth',1,'warning','now')
+    """)
+    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('al-chf','kritisch','ledger','instrument','chf1','missing_fx','missing','active','now')")
+
+    result = recheck_transaction_fx_status(conn, source_type='true_wealth')
+
+    assert result.transaction_count == 1
+    assert result.chf_positions == 1
+    assert result.false_positive_missing_fx_alerts == 1
+    assert result.corrected_transactions == 1
+    assert result.resolved_alerts == 1
+    assert result.audit_events == 1
+    assert conn.execute("SELECT status FROM alerts WHERE alert_id='al-chf'").fetchone()['status'] == 'resolved'
+    assert conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action='set_chf_fx_not_needed'").fetchone()['c'] == 1
+
+
+def test_update_fx_rates_stores_usd_eur_and_resolves_missing_fx() -> None:
     assert alert["status"] == "resolved"


     1|from __future__ import annotations
     2|
     3|from decimal import Decimal
     4|
     5|import pytest
     6|
     7|from jarvis_finance.fx.rates import get_fx_rate_to_chf, recheck_transaction_fx_status, update_fx_rates, upsert_fx_rate
     8|from jarvis_finance.fx.overrides import set_manual_fx_override
     9|from jarvis_finance.fx.providers import MockFxProvider
    10|from jarvis_finance.market_data.prices import MockEquityPriceProvider, refresh_market_prices, store_market_price
    11|from jarvis_finance.market_data.mappings import confirm_instrument_price_mapping, ensure_instrument_price_mapping_quality
    12|from jarvis_finance.ledger.positions import calculate_positions
    13|from jarvis_finance.storage.database import connect_memory
    14|from jarvis_finance.storage.migrations import apply_migrations
    15|
    16|
    17|def setup_conn():
    18|    conn = connect_memory()
    19|    apply_migrations(conn)
    20|    conn.execute("INSERT INTO platforms(platform_id,name,platform_type,default_currency,created_at) VALUES('p1','Broker','broker','CHF','now')")
    21|    conn.execute("INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,created_at) VALUES('a1','p1','Account','brokerage','CHF','now')")
    22|    conn.execute("INSERT INTO instruments(instrument_id,asset_class,name,ticker,isin,exchange,currency,created_at) VALUES('chf1','ETF','CHF ETF','CHETF','CH0000000001','SWX','CHF','now')")
    23|    conn.execute("INSERT INTO instruments(instrument_id,asset_class,name,ticker,isin,exchange,currency,created_at) VALUES('usd1','ETF','USD ETF','USETF','US0000000001','NYSE','USD','now')")
    24|    return conn
    25|
    26|
    27|def test_chf_fx_is_not_needed_and_missing_alert_can_be_resolved() -> None:
    28|    conn = setup_conn()
    29|    conn.execute("""
    30|        INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, currency_original, fx_status, source_type, is_confirmed, quality_status, created_at)
    31|        VALUES('tx-chf','initial_position_snapshot','a1','chf1','2025-12-31','1','CHF','missing','test',1,'warning','now')
    32|    """)
    33|    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('al-chf','kritisch','ledger','instrument','chf1','missing','active','now')")
    34|
    35|    assert get_fx_rate_to_chf(conn, base_currency='CHF') == Decimal('1')
    36|    row = conn.execute("SELECT fx_rate_to_chf, fx_source, fx_status FROM transactions WHERE transaction_id='tx-chf'").fetchone()
    37|    assert row['fx_rate_to_chf'] == '1'
    38|    assert row['fx_source'] == 'not_needed'
    39|    assert row['fx_status'] == 'not_needed'
    40|
    41|
    42|def test_recheck_transaction_fx_status_corrects_chf_false_positive_with_audit() -> None:
    43|    conn = setup_conn()
    44|    conn.execute("""
    45|        INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, currency_original, fx_status, source_type, is_confirmed, quality_status, created_at)
    46|        VALUES('tx-chf','initial_position_snapshot','a1','chf1','2025-12-31','1','CHF','missing','true_wealth',1,'warning','now')
    47|    """)
    48|    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('al-chf','kritisch','ledger','instrument','chf1','missing_fx','missing','active','now')")
    49|
    50|    result = recheck_transaction_fx_status(conn, source_type='true_wealth')
    51|
    52|    assert result.transaction_count == 1
    53|    assert result.chf_positions == 1
    54|    assert result.false_positive_missing_fx_alerts == 1
    55|    assert result.corrected_transactions == 1
    56|    assert result.resolved_alerts == 1
    57|    assert result.audit_events == 1
    58|    assert conn.execute("SELECT status FROM alerts WHERE alert_id='al-chf'").fetchone()['status'] == 'resolved'
    59|    assert conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action='set_chf_fx_not_needed'").fetchone()['c'] == 1
    60|
    61|
    62|def test_update_fx_rates_stores_usd_eur_and_resolves_missing_fx() -> None:
    63|    assert alert["status"] == "resolved"
    64|
    65|
    66|def test_update_fx_rates_stores_usd_eur_and_resolves_missing_fx() -> None:
    67|    conn = setup_conn()
    68|    conn.execute("""
    69|        INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, currency_original, fx_status, source_type, is_confirmed, quality_status, created_at)
    70|        VALUES('tx-usd','initial_position_snapshot','a1','usd1','2025-12-31','1','USD','missing','test',1,'warning','now')
    71|    """)
    72|    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('al-usd','kritisch','ledger','instrument','usd1','missing_fx','missing','active','now')")
    73|    provider = MockFxProvider({('USD', 'CHF', '2025-12-31'): Decimal('0.9000'), ('EUR', 'CHF', '2025-12-31'): Decimal('0.9500')})
    74|
    75|    result = update_fx_rates(conn, provider=provider, currencies=['USD', 'EUR'], rate_date='2025-12-31', resolve_fixed=True)
    76|
    77|    assert result.updated_count == 2
    78|    assert get_fx_rate_to_chf(conn, base_currency='USD', rate_date='2025-12-31') == Decimal('0.9000')
    79|    assert conn.execute("SELECT status FROM alerts WHERE alert_id='al-usd'").fetchone()["status"] == "resolved"
    80|
    81|
    82|def test_manual_fx_override_requires_note_and_audit() -> None:
    83|    conn = setup_conn()
    84|    with pytest.raises(ValueError, match="note"):
    85|        set_manual_fx_override(conn, base_currency='USD', quote_currency='CHF', rate_date='2025-12-31', rate=Decimal('0.9'), note='')
    86|
    87|    rate_id = set_manual_fx_override(conn, base_currency='USD', quote_currency='CHF', rate_date='2025-12-31', rate=Decimal('0.9'), note='bank statement')
    88|
    89|    assert rate_id
    90|    assert conn.execute("SELECT COUNT(*) AS n FROM audit_log WHERE action='manual_fx_override'").fetchone()["n"] == 1
    91|
    92|
    93|def test_instrument_price_mapping_requires_provider_symbol_and_exchange_review() -> None:
    94|    conn = setup_conn()
    95|    conn.execute("UPDATE instruments SET exchange=NULL WHERE instrument_id='usd1'")
    96|
    97|    result = ensure_instrument_price_mapping_quality(conn, instrument_id='usd1')
    98|
    99|    assert result.mapping_status == 'missing_provider_symbol'
   100|    assert 'ticker_without_exchange' in result.quality_flags
   101|    assert conn.execute("SELECT COUNT(*) AS n FROM alerts WHERE rule_id='missing_provider_symbol' AND status='active'").fetchone()["n"] == 1
   102|    assert conn.execute("SELECT COUNT(*) AS n FROM alerts WHERE rule_id='ambiguous_instrument_mapping' AND status='active'").fetchone()["n"] == 1
   103|
   104|
   105|def test_confirm_instrument_price_mapping_writes_audit() -> None:
   106|    conn = setup_conn()
   107|
   108|    mapping_id = confirm_instrument_price_mapping(conn, instrument_id='usd1', provider='mock', provider_symbol='USETF.N', provider_market='NYSE', confidence='manual', note='confirmed from broker')
   109|
   110|    mapping = conn.execute("SELECT * FROM instrument_price_mappings WHERE mapping_id=?", (mapping_id,)).fetchone()
   111|    assert mapping['mapping_status'] == 'mapped'
   112|    assert mapping['provider_symbol'] == 'USETF.N'
   113|    assert conn.execute("SELECT COUNT(*) AS n FROM audit_log WHERE action='confirm_instrument_price_mapping'").fetchone()["n"] == 1
   114|
   115|
   116|def test_refresh_market_prices_with_mock_provider_stores_and_resolves_alert() -> None:
   117|    conn = setup_conn()
   118|    confirm_instrument_price_mapping(conn, instrument_id='usd1', provider='mock', provider_symbol='USETF.N', provider_market='NYSE', confidence='manual', note='test mapping')
   119|    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('mp1','warnung','market_data','instrument','usd1','missing_market_price','missing','active','now')")
   120|    provider = MockEquityPriceProvider({'USETF.N': Decimal('10.25')})
   121|
   122|    result = refresh_market_prices(conn, provider=provider, asset_class='etf', price_date='2026-01-02')
   123|
   124|    assert result.updated_count == 1
   125|    price = conn.execute("SELECT close, quality_status FROM market_prices WHERE instrument_id='usd1'").fetchone()
   126|    assert price['close'] == '10.25'
   127|    assert price['quality_status'] == 'fresh'
   128|    assert conn.execute("SELECT status FROM alerts WHERE alert_id='mp1'").fetchone()["status"] == "resolved"
   129|
   130|
   131|def test_missing_and_stale_market_price_alerts_are_deduped() -> None:
   132|    conn = setup_conn()
   133|    confirm_instrument_price_mapping(conn, instrument_id='usd1', provider='mock', provider_symbol='USETF.N', provider_market='NYSE', confidence='manual', note='test mapping')
   134|    provider = MockEquityPriceProvider({'USETF.N': None})
   135|
   136|    result = refresh_market_prices(conn, provider=provider, asset_class='etf', price_date='2026-01-02')
   137|    result2 = refresh_market_prices(conn, provider=provider, asset_class='etf', price_date='2026-01-02')
   138|
   139|    assert result.warning_count == 1
   140|    assert result2.warning_count == 1
   141|

Use offset=141 to continue reading (showing 1-140 of 162 lines)


Explanation:
**Refining alert rule details**

I noticed that I need to remove some duplicate lines to keep things organized. There's also an issue with line 33; it looks like rule_id is missing when it should be there, but not the missing_fx. I’m wondering if the process really should not fail in this case? I need to create an alert rule that addresses this! It’s all about making sure things are running smoothly. Let’s sort this out!

def test_update_fx_rates_stores_usd_eur_and_resolves_missing_fx() -> None:
    assert alert["status"] == "resolved"


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_fx_market_data_phase_c.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_fx_market_data_phase_c.py
@@ -57,10 +57,6 @@
     assert result.audit_events == 1
     assert conn.execute("SELECT status FROM alerts WHERE alert_id='al-chf'").fetchone()['status'] == 'resolved'
     assert conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action='set_chf_fx_not_needed'").fetchone()['c'] == 1
-
-
-def test_update_fx_rates_stores_usd_eur_and_resolves_missing_fx() -> None:
-    assert alert["status"] == "resolved"

 def test_update_fx_rates_stores_usd_eur_and_resolves_missing_fx() -> None:

/home/agent/.hermes/repos/FinanceManager/tests/unit/test_fx_market_data_phase_c.py was last read with offset/limit pagination (partial view). Re-read the whole file before overwriting it.


Explanation:
**Fixing SQL alert issues**

I’m trying to fix the first alert SQL. It has the rule_id set as 'missing', a message of 'active', and the status marked as 'now'. But I believe the columns like alert_id, priority, category, entity_type, entity_id, rule_id, message, status, and created_at don’t quite align. The values don’t assert correctly, so I might need to clean this up. I could consider using missing_fx to correct it with 'missing active now'.


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_fx_market_data_phase_c.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_fx_market_data_phase_c.py
@@ -30,7 +30,7 @@
         INSERT INTO transactions(transaction_id, transaction_type, account_id, instrument_id, trade_date, quantity, currency_original, fx_status, source_type, is_confirmed, quality_status, created_at)
         VALUES('tx-chf','initial_position_snapshot','a1','chf1','2025-12-31','1','CHF','missing','test',1,'warning','now')
     """)
-    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('al-chf','kritisch','ledger','instrument','chf1','missing','active','now')")
+    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('al-chf','kritisch','ledger','instrument','chf1','missing_fx','missing','active','now')")

     assert get_fx_rate_to_chf(conn, base_currency='CHF') == Decimal('1')
     row = conn.execute("SELECT fx_rate_to_chf, fx_source, fx_status FROM transactions WHERE transaction_id='tx-chf'").fetchone()


Explanation:
**Addressing data quality issues**

I need to add rows to the DQ (data quality) center and make sure to patch the function that retrieves data quality. This sounds straightforward, but I want to ensure I'm not overlooking any details. It's important to make sure everything gets implemented correctly. I should probably double-check the data quality standards we’re aiming for! Alright, let’s tackle this step by step to make sure it all goes smoothly.

        ("fehlende FX", "SELECT COUNT(*) AS c FROM transactions WHERE fx_status='missing'", "kritisch"),
        ("fehlende/stale FX Alerts", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id IN ('missing_fx','stale_fx')", "kritisch"),
        ("fehlende Provider-Symbole", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='missing_provider_symbol'", "warnung"),
        ("fehlende Market Prices", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='missing_market_price'", "warnung"),
        ("stale Market Prices", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='stale_market_price'", "warnung"),
        ("ambiguous Instrument Mapping", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='ambiguous_instrument_mapping'", "warnung"),
        ("price currency mismatch", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='price_currency_mismatch'", "warnung"),
        ("snapshot_only Alerts", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='snapshot_only'", "info"),
        ("cost_basis_uncertain Alerts", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='cost_basis_uncertain'", "warnung"),


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/data.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/data.py
@@ -270,6 +270,14 @@
     items: list[dict[str, str]] = []
     checks = [
         ("fehlende FX", "SELECT COUNT(*) AS c FROM transactions WHERE fx_status='missing'", "kritisch"),
+        ("fehlende/stale FX Alerts", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id IN ('missing_fx','stale_fx')", "kritisch"),
+        ("fehlende Provider-Symbole", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='missing_provider_symbol'", "warnung"),
+        ("fehlende Market Prices", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='missing_market_price'", "warnung"),
+        ("stale Market Prices", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='stale_market_price'", "warnung"),
+        ("ambiguous Instrument Mapping", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='ambiguous_instrument_mapping'", "warnung"),
+        ("price currency mismatch", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='price_currency_mismatch'", "warnung"),
+        ("snapshot_only Alerts", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='snapshot_only'", "info"),
+        ("cost_basis_uncertain Alerts", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='cost_basis_uncertain'", "warnung"),
         ("fehlende CoinGecko-ID", "SELECT COUNT(*) AS c FROM crypto_assets WHERE coingecko_id IS NULL OR coingecko_id='' ", "warnung"),
         ("fehlende Crypto-Preise", "SELECT COUNT(*) AS c FROM crypto_assets ca LEFT JOIN crypto_prices cp ON cp.asset_id=ca.asset_id WHERE cp.crypto_price_id IS NULL", "warnung"),
         ("fehlende ISIN", "SELECT COUNT(*) AS c FROM instrument_mappings WHERE asset_class IN ('equity','etf') AND (isin IS NULL OR isin='') AND mapping_status!='ignored'", "warnung"),


    st.button("Data Quality Recheck", disabled=True)
    st.button("Resolve Fixed Alerts", disabled=True)
    st.button("Instrument Price Mapping öffnen", disabled=True)
    st.button("Manual Review Queue öffnen", disabled=True)
    st.button("Review Items aktualisieren", disabled=True)
    st.button("Produktiven Import starten", disabled=True)
    st.info("Links zur Review Queue und zum Instrument Price Mapping sind vorbereitet; produktive Commit-Aktionen bleiben deaktiviert.")

    st.button("Data Quality Recheck", disabled=True)
    st.button("Resolve Fixed Alerts", disabled=True)
    st.button("Review Items aktualisieren", disabled=True)
    st.button("Produktiven Import starten", disabled=True)
    st.info("Links zur Review Queue können über die Navigation/Filter genutzt werden; produktive Commit-Aktionen bleiben deaktiviert.")


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/page_16_data_quality_center.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/page_16_data_quality_center.py
@@ -22,6 +22,8 @@
     st.subheader("Sichere Aktionen")
     st.button("Data Quality Recheck", disabled=True)
     st.button("Resolve Fixed Alerts", disabled=True)
+    st.button("Instrument Price Mapping öffnen", disabled=True)
+    st.button("Manual Review Queue öffnen", disabled=True)
     st.button("Review Items aktualisieren", disabled=True)
     st.button("Produktiven Import starten", disabled=True)
-    st.info("Links zur Review Queue können über die Navigation/Filter genutzt werden; produktive Commit-Aktionen bleiben deaktiviert.")
+    st.info("Links zur Review Queue und zum Instrument Price Mapping sind vorbereitet; produktive Commit-Aktionen bleiben deaktiviert.")


terminal

PYTHONPATH=src pytest -q tests/unit/test_fx_market_data_phase_c.py


Output:
.........                                                                [100%]
9 passed in 0.17s


Output:
........................................................................ [ 42%]
........................................................................ [ 85%]
................F........                                                [100%]
=================================== FAILURES ===================================
_________________________ test_schema_version_recorded _________________________

    def test_schema_version_recorded() -> None:
        conn = connect_memory()
        apply_migrations(conn)
>       assert get_schema_version(conn) == 8
E       assert 9 == 8
E        +  where 9 = get_schema_version(<sqlite3.Connection object at 0x752a4030e3e0>)

tests/unit/test_schema.py:18: AssertionError
=========================== short test summary info ============================
FAILED tests/unit/test_schema.py::test_schema_version_recorded - assert 9 == 8
1 failed, 168 passed in 8.41s


patch

--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_schema.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_schema.py
@@ -15,7 +15,7 @@
 def test_schema_version_recorded() -> None:
     conn = connect_memory()
     apply_migrations(conn)
-    assert get_schema_version(conn) == 8
+    assert get_schema_version(conn) == 9

 def test_decimal_sensitive_columns_use_text_affinity() -> None:

/home/agent/.hermes/repos/FinanceManager/tests/unit/test_schema.py was modified by sibling subagent '20260515_120728_75980c' but this agent never read it. Read the file before writing to avoid overwriting the sibling's changes.


terminal

Output:
........................................................................ [ 42%]
........................................................................ [ 85%]
.........................                                                [100%]
169 passed in 8.09s


Explanation:
**Planning for production runtime**

I need to get the production runtime for C0/C7 set up, but I should probably run the compile first. After that, I need to carefully apply migrations to the runtime, especially regarding any unique index duplicates. A backup sounds wise too. The user asked about C8 compile pytest git safety status, so I’ll need to commit and push before running the product after implementation. I’m considering checking settings for the database path, and I might need to investigate the source types more closely.

PYTHONPATH=src python - <<'PY'
from jarvis_finance.config.settings import load_settings
from jarvis_finance.storage.database import connect
from jarvis_finance.storage.migrations import apply_migrations
s=load_settings(); conn=connect(s.db_path); apply_migrations(conn)
for r in conn.execute("SELECT COALESCE(source_type,'') AS source_type, COUNT(*) AS c FROM transactions WHERE transaction_type='initial_position_snapshot' AND COALESCE(is_voided,0)=0 GROUP BY COALESCE(source_type,'') ORDER BY c DESC").fetchall():
 print(f"source_type={r['source_type']} count={r['c']}")
conn.close()
PY


Output: source_type=broker_import_reviewed_snapshot count=3


PYTHONPATH=src python -m jarvis_finance.cli.main update-fx-rates --recheck-transactions --source-type broker_import_reviewed_snapshot


Output: FX_RECHECK transactions=3 chf_positions=0 foreign_currency_positions=3 justified_missing_fx_alerts=3 false_positive_missing_fx_alerts=0 corrected_transactions=0 resolved_alerts=0 audit_events=0 dry_run=false


PYTHONPATH=src python - <<'PY'
from jarvis_finance.config.settings import load_settings
from jarvis_finance.storage.database import connect
from jarvis_finance.storage.migrations import apply_migrations
from jarvis_finance.market_data.mappings import ensure_instrument_price_mapping_quality
s=load_settings(); conn=connect(s.db_path); apply_migrations(conn)
ids=[r['instrument_id'] for r in conn.execute("SELECT DISTINCT instrument_id FROM transactions WHERE transaction_type='initial_position_snapshot' AND source_type='broker_import_reviewed_snapshot' AND COALESCE(is_voided,0)=0 AND instrument_id IS NOT NULL").fetchall()]
for instrument_id in ids:
    ensure_instrument_price_mapping_quality(conn, instrument_id=instrument_id)
print(f"MAPPING_RECHECK instruments={len(ids)}")
conn.close()
PY
PYTHONPATH=src python -m jarvis_finance.cli.main update-market-prices --asset-class etf --limit 3 --dry-run


Output:
MAPPING_RECHECK instruments=3
MARKET_PRICE_UPDATE asset_class=etf total_mappings=0 updated=0 cached=0 skipped=0 stale=0 warnings=0 errors=0 dry_run=true


Explanation:
**Gathering valuation status**

I'm looking into the C4 valuation status aggregate and assessing that I need to use a script without specific values. My focus is on counting items and examining active alerts categorized by rule. I'm also considering if the position calculations might generate a missing market price. It looks like there's a function calling _add_warning that creates alerts for any missing market price—so I need to check positions for clarity there.


     1|from __future__ import annotations
     2|
     3|from dataclasses import dataclass, field
     4|from decimal import Decimal, ROUND_HALF_UP
     5|from sqlite3 import Connection
     6|
     7|from jarvis_finance.audit.log import record_audit_event
     8|from jarvis_finance.imports.common import stable_id, utc_now
     9|from jarvis_finance.quality.alerts import create_alert
    10|from jarvis_finance.fx.rates import latest_fx_rate
    11|
    12|from .cost_basis import WeightedAverageLot
    13|
    14|MONEY = Decimal("0.01")
    15|
    16|
    17|def _d(value: object, default: str = "0") -> Decimal:
    18|    if value is None or str(value).strip() == "":
    19|        return Decimal(default)
    20|    return Decimal(str(value))
    21|
    22|
    23|def _money(value: Decimal | None) -> Decimal | None:
    24|    if value is None:
    25|        return None
    26|    return value.quantize(MONEY, rounding=ROUND_HALF_UP)
    27|
    28|
    29|@dataclass
    30|class Position:
    31|    account_id: str
    32|    instrument_id: str
    33|    quantity: Decimal = Decimal("0")
    34|    cost_basis_original: Decimal = Decimal("0")
    35|    cost_basis_chf: Decimal = Decimal("0")
    36|    average_cost_original: Decimal | None = None
    37|    average_cost_chf: Decimal | None = None
    38|    realized_pnl_chf: Decimal = Decimal("0")
    39|    income_chf: Decimal = Decimal("0")
    40|    fees_chf: Decimal = Decimal("0")
    41|    taxes_chf: Decimal = Decimal("0")
    42|    market_price_original: Decimal | None = None
    43|    market_value_original: Decimal | None = None
    44|    market_value_chf: Decimal | None = None
    45|    unrealized_pnl_chf: Decimal | None = None
    46|    total_return_chf: Decimal | None = None
    47|    data_quality_status: str = "ok"
    48|    quality_warnings: list[str] = field(default_factory=list)
    49|
    50|
    51|@dataclass
    52|class PositionCalculationResult:
    53|    positions: dict[tuple[str, str], Position] = field(default_factory=dict)
    54|    as_of_date: str | None = None
    55|
    56|
    57|def _latest_market_price(conn: Connection, instrument_id: str, as_of_date: str | None):
    58|    if as_of_date:
    59|        return conn.execute(
    60|            """
    61|            SELECT close, currency FROM market_prices
    62|            WHERE instrument_id=? AND price_date <= ? AND quality_status IN ('fresh','ok')
    63|            ORDER BY price_date DESC, created_at DESC LIMIT 1
    64|            """,
    65|            (instrument_id, as_of_date),
    66|        ).fetchone()
    67|    return conn.execute(
    68|        """
    69|        SELECT close, currency FROM market_prices
    70|        WHERE instrument_id=? AND quality_status IN ('fresh','ok')
    71|        ORDER BY price_date DESC, created_at DESC LIMIT 1
    72|        """,
    73|        (instrument_id,),
    74|    ).fetchone()
    75|
    76|
    77|def _add_warning(conn: Connection, pos: Position, rule_id: str, message: str) -> None:
    78|    if rule_id not in pos.quality_warnings:
    79|        pos.quality_warnings.append(rule_id)
    80|    pos.data_quality_status = "incomplete"
    81|    create_alert(
    82|        conn,
    83|        priority="kritisch" if rule_id == "missing_fx" else "warnung",
    84|        category="ledger",
    85|        entity_type="instrument",
    86|        entity_id=pos.instrument_id,
    87|        rule_id=rule_id,
    88|        message=message,
    89|        evidence={"account_id": pos.account_id, "instrument_id": pos.instrument_id},
    90|    )
    91|
    92|
    93|def calculate_positions(conn: Connection, *, as_of_date: str | None = None) -> PositionCalculationResult:
    94|    result = PositionCalculationResult(as_of_date=as_of_date)
    95|    lots: dict[tuple[str, str], WeightedAverageLot] = {}
    96|    params: tuple[object, ...] = ()
    97|    where = "WHERE instrument_id IS NOT NULL AND COALESCE(is_voided, 0) = 0"
    98|    if as_of_date:
    99|        where += " AND trade_date <= ?"
   100|        params = (as_of_date,)
   101|    rows = conn.execute(
   102|        f"""
   103|        SELECT * FROM transactions
   104|        {where}
   105|        ORDER BY account_id, instrument_id, trade_date, created_at, transaction_id
   106|        """,
   107|        params,
   108|    ).fetchall()
   109|    for row in rows:
   110|        key = (row["account_id"], row["instrument_id"])
   111|        pos = result.positions.setdefault(key, Position(row["account_id"], row["instrument_id"]))
   112|        lot = lots.setdefault(key, WeightedAverageLot())
   113|        t = row["transaction_type"]
   114|        qty = _d(row["quantity"])
   115|        gross = _d(row["gross_amount_original"])
   116|        fee = _d(row["fee_original"])
   117|        tax = _d(row["tax_original"])
   118|        fx = _d(row["fx_rate_to_chf"], "1") if row["fx_rate_to_chf"] is not None else None
   119|        if fx is None or row["fx_status"] == "missing":
   120|            _add_warning(conn, pos, "missing_fx", "Historic FX is missing; precise CHF total return cannot be calculated.")
   121|        if t in {"initial_position_snapshot", "buy"}:
   122|            if qty > 0:
   123|                cost_original = gross + fee
   124|                cost_chf = (gross + fee) * fx if fx is not None else Decimal("0")
   125|                lot.buy(qty, cost_original, cost_chf)
   126|                pos.fees_chf += fee * fx if fx is not None else Decimal("0")
   127|                pos.taxes_chf += tax * fx if fx is not None else Decimal("0")
   128|        elif t in {"partial_sell", "full_sell"}:
   129|            if qty > 0:
   130|                proceeds_chf = (gross - fee - tax) * fx if fx is not None else Decimal("0")
   131|                lot.sell(qty, proceeds_chf)
   132|                pos.fees_chf += fee * fx if fx is not None else Decimal("0")
   133|                pos.taxes_chf += tax * fx if fx is not None else Decimal("0")
   134|        elif t in {"dividend", "etf_distribution"}:
   135|            income_original = _d(row["net_amount_original"]) if row["net_amount_original"] is not None else gross - fee - tax
   136|            if fx is not None:
   137|                pos.income_chf += income_original * fx
   138|                pos.fees_chf += fee * fx
   139|                pos.taxes_chf += tax * fx
   140|        if t == "initial_position_snapshot":
   141|            _add_warning(conn, pos, "snapshot_only", "Initial snapshot lacks full transaction history; performance is incomplete.")
   142|            if gross == 0 or fx is None:
   143|                _add_warning(conn, pos, "cost_basis_uncertain", "Cost basis is uncertain for this initial snapshot.")
   144|    for key, lot in lots.items():
   145|        pos = result.positions[key]
   146|        pos.quantity = lot.quantity.normalize()
   147|        pos.cost_basis_original = _money(lot.cost_basis_original) or Decimal("0")
   148|        pos.cost_basis_chf = _money(lot.cost_basis_chf) or Decimal("0")
   149|        pos.average_cost_original = lot.average_cost_original
   150|        pos.average_cost_chf = lot.average_cost_chf
   151|        pos.realized_pnl_chf = _money(lot.realized_pnl_chf) or Decimal("0")
   152|        pos.income_chf = _money(pos.income_chf) or Decimal("0")
   153|        pos.fees_chf = _money(pos.fees_chf) or Decimal("0")
   154|        pos.taxes_chf = _money(pos.taxes_chf) or Decimal("0")
   155|        market = _latest_market_price(conn, pos.instrument_id, as_of_date)
   156|        if market is None:
   157|            _add_warning(conn, pos, "missing_market_price", "Market price is missing; unrealized P&L cannot be calculated.")
   158|        else:
   159|            pos.market_price_original = _d(market["close"])
   160|            pos.market_value_original = _money(pos.quantity * pos.market_price_original)
   161|            # MVP: use only local FX data; dashboards never call providers during render.
   162|            if market["currency"] == "CHF":
   163|                pos.market_value_chf = pos.market_value_original
   164|                pos.unrealized_pnl_chf = _money(pos.market_value_chf - pos.cost_basis_chf)
   165|            else:
   166|                fx_row = latest_fx_rate(conn, base_currency=market["currency"], quote_currency="CHF", rate_date=as_of_date)
   167|                if fx_row:
   168|                    market_fx = _d(fx_row["rate"])
   169|                    pos.market_value_chf = _money(pos.market_value_original * market_fx) if pos.market_value_original is not None else None
   170|                    if pos.market_value_chf is not None:
   171|                        pos.unrealized_pnl_chf = _money(pos.market_value_chf - pos.cost_basis_chf)
   172|                else:
   173|                    _add_warning(conn, pos, "missing_fx", "Market FX is missing; CHF valuation is incomplete.")
   174|        if pos.market_value_chf is not None and "missing_fx" not in pos.quality_warnings and "cost_basis_uncertain" not in pos.quality_warnings and "snapshot_only" not in pos.quality_warnings:
   175|            pos.total_return_chf = _money((pos.unrealized_pnl_chf or Decimal("0")) + pos.realized_pnl_chf + pos.income_chf - pos.fees_chf - pos.taxes_chf)
   176|        else:
   177|            pos.total_return_chf = None
   178|    conn.commit()
   179|    return result
   180|
   181|
   182|def save_position_snapshots(conn: Connection, result: PositionCalculationResult, *, note: str) -> list[str]:
   183|    if not note.strip():
   184|        raise ValueError("position snapshot requires a note")
   185|    now = utc_now()
   186|    snapshot_date = result.as_of_date or now[:10]
   187|    ids: list[str] = []
   188|    for (account_id, instrument_id), pos in result.positions.items():
   189|        row = conn.execute("SELECT platform_id FROM accounts WHERE account_id=?", (account_id,)).fetchone()
   190|        platform_id = row["platform_id"] if row else None
   191|        snapshot_id = stable_id("possnap", snapshot_date, account_id, instrument_id)
   192|        conn.execute(
   193|            """
   194|            INSERT OR REPLACE INTO positions_snapshot(
   195|                position_snapshot_id, snapshot_date, account_id, platform_id, instrument_id,
   196|                quantity, average_cost_original, cost_basis_original, cost_basis_chf,
   197|                market_price_original, market_value_original, market_value_chf,
   198|                unrealized_price_pnl_chf, realized_pnl_chf, income_chf, fees_chf,
   199|                taxes_chf, total_return_chf, data_quality_status, created_at
   200|            ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
   201|            """,
   202|            (
   203|                snapshot_id, snapshot_date, account_id, platform_id, instrument_id,
   204|                str(pos.quantity), str(pos.average_cost_original) if pos.average_cost_original is not None else None,
   205|                str(pos.cost_basis_original), str(pos.cost_basis_chf),
   206|                str(pos.market_price_original) if pos.market_price_original is not None else None,
   207|                str(pos.market_value_original) if pos.market_value_original is not None else None,
   208|                str(pos.market_value_chf) if pos.market_value_chf is not None else None,
   209|                str(pos.unrealized_pnl_chf) if pos.unrealized_pnl_chf is not None else None,
   210|                str(pos.realized_pnl_chf), str(pos.income_chf), str(pos.fees_chf),
   211|                str(pos.taxes_chf), str(pos.total_return_chf) if pos.total_return_chf is not None else None,
   212|                pos.data_quality_status, now,
   213|            ),
   214|        )
   215|        record_audit_event(
   216|            conn,
   217|            source="ledger_positions",
   218|            action="calculate_position_snapshot",
   219|            entity_type="position_snapshot",
   220|            entity_id=snapshot_id,
   221|

Use offset=221 to continue reading (showing 1-220 of 228 lines)


Explanation:
**Calculating positions for alerts**

I need to calculate positions, which might involve creating active alerts. Should I do this specifically for C7? It seems like the data quality is okay, but I notice there's no position snapshot writes happening. I need to consider how to aggregate runtime for this process. It's all about making sure everything is accurate and functioning well!

PYTHONPATH=src python - <<'PY'
from collections import Counter
from jarvis_finance.config.settings import load_settings
from jarvis_finance.storage.database import connect
from jarvis_finance.storage.migrations import apply_migrations
from jarvis_finance.ledger.positions import calculate_positions
s=load_settings(); conn=connect(s.db_path); apply_migrations(conn)
# Trigger local-only valuation/data-quality calculation; no snapshot writes.
positions=calculate_positions(conn).positions
source_instruments={r['instrument_id'] for r in conn.execute("SELECT DISTINCT instrument_id FROM transactions WHERE transaction_type='initial_position_snapshot' AND source_type='broker_import_reviewed_snapshot' AND COALESCE(is_voided,0)=0").fetchall()}
relevant=[p for (acct, inst), p in positions.items() if inst in source_instruments]
fx_ok=sum(1 for p in relevant if 'missing_fx' not in p.quality_warnings)
missing_fx=sum(1 for p in relevant if 'missing_fx' in p.quality_warnings)
market_ok=sum(1 for p in relevant if p.market_price_original is not None)
missing_market=sum(1 for p in relevant if p.market_price_original is None)
full=sum(1 for p in relevant if p.market_value_chf is not None and 'missing_fx' not in p.quality_warnings and 'missing_market_price' not in p.quality_warnings)
alerts=conn.execute("SELECT rule_id, COUNT(*) AS c FROM alerts WHERE status='active' GROUP BY rule_id ORDER BY rule_id").fetchall()
print(f"C7_AGG positions_total={len(relevant)} fx_ok_or_not_needed={fx_ok} missing_fx={missing_fx} market_price_ok={market_ok} missing_market_price={missing_market} complete_chf_valuation={full}")
print("ACTIVE_ALERTS " + " ".join(f"{r['rule_id']}={r['c']}" for r in alerts))
conn.close()
PY


Output:
C7_AGG positions_total=3 fx_ok_or_not_needed=0 missing_fx=3 market_price_ok=0 missing_market_price=3 complete_chf_valuation=0
ACTIVE_ALERTS cost_basis_uncertain=3 missing_fx=3 missing_market_price=3 missing_provider_symbol=3 snapshot_only=3


PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan .


Output:
.pytest_cache/CACHEDIR.TAG: runtime_cache_file_blocked
.pytest_cache/README.md: runtime_cache_file_blocked
.pytest_cache/.gitignore: runtime_cache_file_blocked
.pytest_cache/v/cache/nodeids: runtime_cache_file_blocked
.pytest_cache/v/cache/lastfailed: runtime_cache_file_blocked


Output:
M src/jarvis_finance/cli/main.py
 M src/jarvis_finance/dashboard/data.py
 M src/jarvis_finance/dashboard/pages/page_16_data_quality_center.py
 M src/jarvis_finance/fx/overrides.py
 M src/jarvis_finance/fx/providers.py
 M src/jarvis_finance/fx/rates.py
 M src/jarvis_finance/ledger/positions.py
 M src/jarvis_finance/storage/migrations.py
 M tests/unit/test_schema.py
?? src/jarvis_finance/market_data/mappings.py
?? src/jarvis_finance/market_data/prices.py
?? tests/unit/test_fx_market_data_phase_c.py


rm -rf .pytest_cache && PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan . && git status --short


Output:
GIT_SAFETY_OK
 M src/jarvis_finance/cli/main.py
 M src/jarvis_finance/dashboard/data.py
 M src/jarvis_finance/dashboard/pages/page_16_data_quality_center.py
 M src/jarvis_finance/fx/overrides.py
 M src/jarvis_finance/fx/providers.py
 M src/jarvis_finance/fx/rates.py
 M src/jarvis_finance/ledger/positions.py
 M src/jarvis_finance/storage/migrations.py
 M tests/unit/test_schema.py
?? src/jarvis_finance/market_data/mappings.py
?? src/jarvis_finance/market_data/prices.py
?? tests/unit/test_fx_market_data_phase_c.py


Explanation:
**Reassessing test procedures**

I’m considering running pytest after clearing the cache. The order should be: run pytest, then remove the cache, and finally ensure safety. That seems like a solid plan. I might need to inspect any differences afterward. Maybe checking git diff could be helpful too, especially if there aren’t any values to compare. Let’s make sure everything is in order!


Output:
src/jarvis_finance/cli/main.py                     |  88 ++++++++
 src/jarvis_finance/dashboard/data.py               |  13 ++
 .../dashboard/pages/page_16_data_quality_center.py |   4 +-
 src/jarvis_finance/fx/overrides.py                 |  42 ++++
 src/jarvis_finance/fx/providers.py                 |  18 ++
 src/jarvis_finance/fx/rates.py                     | 242 ++++++++++++++++++++-
 src/jarvis_finance/ledger/positions.py             |  22 +-
 src/jarvis_finance/storage/migrations.py           |  41 +++-
 tests/unit/test_schema.py                          |   2 +-
 9 files changed, 460 insertions(+), 12 deletions(-)


Explanation:
**Considering version control best practices**

I realize I have some untracked files that need attention. I think the first step is to use `git add`, then maybe run `pytest` to ensure everything works as expected. Also, should I run `git safety` after adding files? It checks for sensitive data. I need to confirm whether including untracked files requires running `git status` first. Finally, I’ll commit and push, and maybe consider using the GitHub PR workflow for this process.