     1|from __future__ import annotations
     2|
     3|from dataclasses import dataclass
     4|from datetime import datetime, timezone
     5|from sqlite3 import Connection
     6|
     7|from jarvis_finance.quality.alerts import create_alert, utc_now
     8|
     9|CRYPTO_PRICE_ALERT_RULES = {
    10|    "crypto_price_stale",
    11|    "crypto_price_missing",
    12|    "crypto_price_missing_local",
    13|}
    14|
    15|
    16|@dataclass
    17|class DataQualityCheckResult:
    18|    scope: str
    19|    dry_run: bool = False
    20|    resolve_fixed: bool = False
    21|    active_crypto_assets: int = 0
    22|    fresh_price_assets: int = 0
    23|    active_stale_price_alerts_before: int = 0
    24|    active_stale_price_alerts_after: int = 0
    25|    resolved_alerts: int = 0
    26|    warnings: int = 0
    27|    errors: int = 0
    28|    missing_price_assets: int = 0
    29|
    30|
    31|def _active_price_alert_count(conn: Connection) -> int:
    32|    placeholders = ",".join("?" for _ in CRYPTO_PRICE_ALERT_RULES)
    33|    return conn.execute(
    34|        f"SELECT COUNT(*) AS n FROM alerts WHERE status='active' AND rule_id IN ({placeholders})",
    35|        tuple(sorted(CRYPTO_PRICE_ALERT_RULES)),
    36|    ).fetchone()["n"]
    37|
    38|
    39|def _parse_dt(value: str | None) -> datetime | None:
    40|    if not value:
    41|        return None
    42|    try:
    43|        dt = datetime.fromisoformat(value.replace("Z", "+00:00"))
    44|    except ValueError:
    45|        return None
    46|    return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
    47|
    48|
    49|def _is_stale(timestamp: str | None, *, max_age_seconds: int) -> bool:
    50|    dt = _parse_dt(timestamp)
    51|    if dt is None:
    52|        return True
    53|    return (datetime.now(timezone.utc) - dt).total_seconds() > max_age_seconds
    54|
    55|
    56|def _latest_crypto_price_row(conn: Connection, asset_id: str, currency: str = "CHF"):
    57|    return conn.execute(
    58|        """
    59|        SELECT * FROM crypto_prices
    60|        WHERE asset_id=? AND price_currency=?
    61|        ORDER BY COALESCE(provider_timestamp, fetched_at, '') DESC, fetched_at DESC
    62|        LIMIT 1
    63|        """,
    64|        (asset_id, currency.upper()),
    65|    ).fetchone()
    66|
    67|
    68|def has_fresh_crypto_price(conn: Connection, asset_id: str, *, currency: str = "CHF", max_age_seconds: int = 86_400) -> bool:
    69|    latest = _latest_crypto_price_row(conn, asset_id, currency)
    70|    return bool(latest and latest["quality_status"] == "fresh" and latest["price"] not in (None, "") and not _is_stale(latest["fetched_at"], max_age_seconds=max_age_seconds))
    71|
    72|
    73|def resolve_fixed_crypto_price_alerts(
    74|    conn: Connection,
    75|    *,
    76|    currency: str = "CHF",
    77|    max_age_seconds: int = 86_400,
    78|    dry_run: bool = False,
    79|) -> int:
    80|    """Resolve active stale/missing crypto-price alerts whose asset now has a fresh local price.
    81|
    82|    Alert history is retained; rows are never deleted. Updating last_seen_at on resolution makes the
    83|    lifecycle explicit without creating another alert occurrence.
    84|    """
    85|    placeholders = ",".join("?" for _ in CRYPTO_PRICE_ALERT_RULES)
    86|    alerts = conn.execute(
    87|        f"""
    88|        SELECT alert_id, entity_id
    89|        FROM alerts
    90|        WHERE status='active'
    91|          AND entity_type='crypto_asset'
    92|          AND rule_id IN ({placeholders})
    93|        ORDER BY created_at
    94|        """,
    95|        tuple(sorted(CRYPTO_PRICE_ALERT_RULES)),
    96|    ).fetchall()
    97|    now = utc_now()
    98|    resolved = 0
    99|    for alert in alerts:
   100|        asset_id = alert["entity_id"]
   101|        if asset_id and has_fresh_crypto_price(conn, asset_id, currency=currency, max_age_seconds=max_age_seconds):
   102|            resolved += 1
   103|            if not dry_run:
   104|                conn.execute(
   105|                    """
   106|                    UPDATE alerts
   107|                    SET status='resolved', resolved_at=?, last_seen_at=?
   108|                    WHERE alert_id=? AND status='active'
   109|                    """,
   110|                    (now, now, alert["alert_id"]),
   111|                )
   112|    if resolved and not dry_run:
   113|        conn.commit()
   114|    return resolved
   115|
   116|
   117|def check_crypto_data_quality(
   118|    conn: Connection,
   119|    *,
   120|    currency: str = "CHF",
   121|    max_age_seconds: int = 86_400,
   122|    resolve_fixed: bool = False,
   123|    dry_run: bool = False,
   124|) -> DataQualityCheckResult:
   125|    result = DataQualityCheckResult(scope="crypto", dry_run=dry_run, resolve_fixed=resolve_fixed)
   126|    assets = conn.execute(
   127|        "SELECT asset_id, symbol, coingecko_id FROM crypto_assets WHERE is_active=1 ORDER BY symbol"
   128|    ).fetchall()
   129|    result.active_crypto_assets = len(assets)
   130|    result.active_stale_price_alerts_before = _active_price_alert_count(conn)
   131|
   132|    for asset in assets:
   133|        if has_fresh_crypto_price(conn, asset["asset_id"], currency=currency, max_age_seconds=max_age_seconds):
   134|            result.fresh_price_assets += 1
   135|            continue
   136|        result.missing_price_assets += 1
   137|        result.warnings += 1
   138|        if not dry_run:
   139|            create_alert(
   140|                conn,
   141|                priority="warnung",
   142|                category="market_data",
   143|                entity_type="crypto_asset",
   144|                entity_id=asset["asset_id"],
   145|                rule_id="crypto_price_missing_local",
   146|                message="Crypto asset has no fresh local price during data quality recheck.",
   147|                evidence={"symbol": asset["symbol"], "coingecko_id": asset["coingecko_id"], "currency": currency.upper()},
   148|                fingerprint=f"crypto_price_missing_local:{currency.upper()}",
   149|            )
   150|
   151|    if resolve_fixed:
   152|        result.resolved_alerts = resolve_fixed_crypto_price_alerts(
   153|            conn,
   154|            currency=currency,
   155|            max_age_seconds=max_age_seconds,
   156|            dry_run=dry_run,
   157|        )
   158|    if not dry_run:
   159|        conn.commit()
   160|    result.active_stale_price_alerts_after = _active_price_alert_count(conn)
   161|    if dry_run and resolve_fixed:
   162|        result.active_stale_price_alerts_after = max(0, result.active_stale_price_alerts_before - result.resolved_alerts)
   163|    return result
   164|


Explanation:
**Running data quality check**

I need to run a check on the data quality, and it seems I should inspect the command-line arguments to make sure everything is set up correctly. This is an important step as it will help me ensure that the data being processed meets the necessary quality standards. It's always good to double-check the parameters, so I'm feeling like this will lead to more reliable results. Let's get it done!

terminal

set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
PYTHONPATH=src python -m jarvis_finance.cli.main check-data-quality --help
PYTHONPATH=src python -m jarvis_finance.cli.main update-crypto-prices --help | sed -n '1,220p'


Output:
usage: finance check-data-quality [-h] [--scope {crypto}] [--resolve-fixed]
                                  [--dry-run] [--currency CURRENCY]
                                  [--max-age-seconds MAX_AGE_SECONDS]

options:
  -h, --help            show this help message and exit
  --scope {crypto}
  --resolve-fixed
  --dry-run
  --currency CURRENCY
  --max-age-seconds MAX_AGE_SECONDS
usage: finance update-crypto-prices [-h] [--currency CURRENCY]
                                    [--max-age-seconds MAX_AGE_SECONDS]
                                    [--sleep-seconds SLEEP_SECONDS]
                                    [--max-retries MAX_RETRIES]
                                    [--initial-backoff INITIAL_BACKOFF]
                                    [--max-backoff MAX_BACKOFF]
                                    [--only-missing] [--only-stale]
                                    [--only-symbol ONLY_SYMBOL]
                                    [--limit LIMIT] [--dry-run]

options:
  -h, --help            show this help message and exit
  --currency CURRENCY
  --max-age-seconds MAX_AGE_SECONDS
  --sleep-seconds SLEEP_SECONDS
  --max-retries MAX_RETRIES
  --initial-backoff INITIAL_BACKOFF
  --max-backoff MAX_BACKOFF
  --only-missing
  --only-stale
  --only-symbol ONLY_SYMBOL
  --limit LIMIT
  --dry-run


set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
PYTHONPATH=src python - <<'PY'
import json
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.quality.data_quality import has_fresh_crypto_price
conn=connect(load_settings().db_path); apply_migrations(conn)
assets=conn.execute("SELECT asset_id FROM crypto_assets WHERE is_active=1").fetchall()
fresh=sum(1 for a in assets if has_fresh_crypto_price(conn,a['asset_id'],currency='CHF'))
active_stale=conn.execute("SELECT COUNT(*) n FROM alerts WHERE rule_id='crypto_price_stale' AND status='active'").fetchone()['n']
resolved_stale=conn.execute("SELECT COUNT(*) n FROM alerts WHERE rule_id='crypto_price_stale' AND status='resolved'").fetchone()['n']
active_price_rules=[dict(r) for r in conn.execute("SELECT rule_id,status,COUNT(*) n FROM alerts WHERE rule_id IN ('crypto_price_stale','crypto_price_missing','crypto_price_missing_local') GROUP BY rule_id,status ORDER BY rule_id,status").fetchall()]
print(json.dumps({'active_stale_before':active_stale,'resolved_stale_existing':resolved_stale,'active_assets':len(assets),'fresh_chf_assets_before':fresh,'price_alert_rules_before':active_price_rules}, sort_keys=True))
PY
PYTHONPATH=src python -m jarvis_finance.cli.main check-data-quality --scope crypto --resolve-fixed --currency CHF > /tmp/b6d_dq_crypto.txt
cat /tmp/b6d_dq_crypto.txt
PYTHONPATH=src python - <<'PY'
import json
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.quality.data_quality import has_fresh_crypto_price
conn=connect(load_settings().db_path); apply_migrations(conn)
assets=conn.execute("SELECT asset_id FROM crypto_assets WHERE is_active=1").fetchall()
fresh=sum(1 for a in assets if has_fresh_crypto_price(conn,a['asset_id'],currency='CHF'))
print(json.dumps({
 'active_stale_after': conn.execute("SELECT COUNT(*) n FROM alerts WHERE rule_id='crypto_price_stale' AND status='active'").fetchone()['n'],
 'resolved_stale_after_total': conn.execute("SELECT COUNT(*) n FROM alerts WHERE rule_id='crypto_price_stale' AND status='resolved'").fetchone()['n'],
 'active_assets': len(assets),
 'fresh_chf_assets_after': fresh,
 'price_alert_rules_after': [dict(r) for r in conn.execute("SELECT rule_id,status,COUNT(*) n FROM alerts WHERE rule_id IN ('crypto_price_stale','crypto_price_missing','crypto_price_missing_local') GROUP BY rule_id,status ORDER BY rule_id,status").fetchall()]
}, sort_keys=True))
PY


Output:
{"active_assets": 20, "active_stale_before": 0, "fresh_chf_assets_before": 20, "price_alert_rules_before": [{"n": 14, "rule_id": "crypto_price_stale", "status": "resolved"}], "resolved_stale_existing": 14}
DATA_QUALITY_CHECK scope=crypto active_crypto_assets=20 fresh_price_assets=20 active_stale_price_alerts_before=0 active_stale_price_alerts_after=0 resolved_alerts=0 warnings=0 errors=0 dry_run=false
{"active_assets": 20, "active_stale_after": 0, "fresh_chf_assets_after": 20, "price_alert_rules_after": [{"n": 14, "rule_id": "crypto_price_stale", "status": "resolved"}], "resolved_stale_after_total": 14}


Explanation:
**Clarifying the issue**

I need to figure out why this question wasn't resolved. It looks like everything was actually addressed, but it seems that B6c accounted for all alerts instead of just the status. Maybe it was resolved earlier than I thought. I should also consider importing the third backup to verify the import process. Plus, I'll need to use the execution function to make sure everything, including the active price, is okay.

todo

Crypto-Stale-Alerts prüfen, resolve-fixed/Price Refresh falls nötig aggregiert

Drittes remaining True-Wealth-ready Item mit Backup/Verify importieren


Konsolidierter Import-, Review-Queue- und Data-Quality-Check

Compile/pytest/Git-Safety/Git Status; ggf. Commit/Push


terminal

set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
PYTHONPATH=src python -m jarvis_finance.cli.main backup-runtime-db > /tmp/b6d_backup_out.txt
PYTHONPATH=src python - <<'PY'
import re, subprocess, json, os
from pathlib import Path
text=Path('/tmp/b6d_backup_out.txt').read_text(); m=re.search(r'backup_file=([^\s]+)', text)
if not m: raise SystemExit('backup_file not found')
backup=Path(m.group(1)).resolve(); repo=Path('/home/agent/.hermes/repos/FinanceManager')
verify=subprocess.run(['python','-m','jarvis_finance.cli.main','verify-backup','--file',str(backup)], cwd=repo, env={**os.environ,'PYTHONPATH':'src'}, text=True, capture_output=True, check=True)
print(json.dumps({'backup_created':backup.exists(), 'backup_verified':'checksum_ok=true' in verify.stdout, 'backup_dir':str(backup.parent)}, sort_keys=True))
PY
PYTHONPATH=src python - <<'PY'
import json
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.imports.execution import execute_initial_snapshot_plan
conn=connect(load_settings().db_path); apply_migrations(conn)
before_tx=conn.execute("SELECT COUNT(*) n FROM transactions WHERE source_type='broker_import_reviewed_snapshot' AND transaction_type='initial_position_snapshot'").fetchone()['n']
before_ps=conn.execute('SELECT COUNT(*) n FROM positions_snapshot').fetchone()['n']
before_cash=conn.execute('SELECT COUNT(*) n FROM cash_balances').fetchone()['n']
plan=conn.execute("SELECT execution_plan_id FROM broker_import_execution_plans WHERE source_platform='True Wealth' AND payload_status='ready' AND execution_status='planned' ORDER BY created_at, execution_plan_id LIMIT 1").fetchone()
if not plan:
    raise SystemExit('no remaining planned ready payload')
result=execute_initial_snapshot_plan(conn, execution_plan_id=plan['execution_plan_id'], note='B6d final controlled True Wealth mini-import from reviewed execution plan; no mass import.', created_by='phase_b6d')
after_tx=conn.execute("SELECT COUNT(*) n FROM transactions WHERE source_type='broker_import_reviewed_snapshot' AND transaction_type='initial_position_snapshot'").fetchone()['n']
after_ps=conn.execute('SELECT COUNT(*) n FROM positions_snapshot').fetchone()['n']
after_cash=conn.execute('SELECT COUNT(*) n FROM cash_balances').fetchone()['n']
print(json.dumps({'third_mini_import_performed':result.imported,'new_transactions_delta':after_tx-before_tx,'total_tw_initial_snapshots':after_tx,'positions_snapshot_delta':after_ps-before_ps,'cash_balances_delta':after_cash-before_cash,'remaining_ready_planned_payloads': conn.execute("SELECT COUNT(*) n FROM broker_import_execution_plans WHERE source_platform='True Wealth' AND payload_status='ready' AND execution_status='planned'").fetchone()['n']}, sort_keys=True))
PY


Output:
{"backup_created": true, "backup_dir": "/home/agent/jarvis_runtime/finance-system/backups", "backup_verified": true}
{"cash_balances_delta": 0, "new_transactions_delta": 1, "positions_snapshot_delta": 0, "remaining_ready_planned_payloads": 0, "third_mini_import_performed": true, "total_tw_initial_snapshots": 3}


set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
PYTHONPATH=src python - <<'PY'
import json
from decimal import Decimal
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
from jarvis_finance.dashboard import data as dashboard_data
conn=connect(load_settings().db_path); apply_migrations(conn)
rows=conn.execute("""
SELECT t.transaction_id,t.transaction_type,t.source_type,t.source_id,t.row_hash,t.instrument_id,t.account_id,t.quantity,t.trade_date,t.quality_status,t.fx_status,
       ep.execution_plan_id,ep.review_item_id,ep.execution_status,ri.review_status
FROM transactions t
LEFT JOIN broker_import_execution_plans ep ON ep.execution_plan_id=t.source_id OR ep.transaction_id=t.transaction_id
LEFT JOIN broker_import_review_items ri ON ri.review_item_id=ep.review_item_id
WHERE t.source_type='broker_import_reviewed_snapshot' AND t.transaction_type='initial_position_snapshot'
ORDER BY t.created_at,t.transaction_id
""").fetchall()
parseable=0
all_linked=True
all_core=True
for r in rows:
    Decimal(str(r['quantity'])); parseable+=1
    all_linked = all_linked and bool(r['execution_plan_id']) and bool(r['review_item_id'])
    all_core = all_core and bool(r['account_id']) and bool(r['instrument_id']) and r['source_type']=='broker_import_reviewed_snapshot' and r['execution_status']=='imported' and r['review_status']=='imported'
# duplicates without sensitive ids in output
dup_execution=conn.execute("SELECT COUNT(*) n FROM (SELECT source_id FROM transactions WHERE source_type='broker_import_reviewed_snapshot' GROUP BY source_id HAVING COUNT(*)>1)").fetchone()['n']
dup_row_hash=conn.execute("SELECT COUNT(*) n FROM (SELECT row_hash FROM transactions WHERE source_type='broker_import_reviewed_snapshot' GROUP BY row_hash HAVING COUNT(*)>1)").fetchone()['n']
dup_tuple=conn.execute("""SELECT COUNT(*) n FROM (SELECT account_id,instrument_id,trade_date FROM transactions WHERE source_type='broker_import_reviewed_snapshot' AND transaction_type='initial_position_snapshot' AND COALESCE(is_voided,0)=0 GROUP BY account_id,instrument_id,trade_date HAVING COUNT(*)>1)""").fetchone()['n']
positions=calculate_positions(conn)
position_warnings=[]
for pos in positions.positions.values(): position_warnings.extend(pos.quality_warnings)
review_status=[dict(r) for r in conn.execute("SELECT source_platform, review_status, import_readiness_status, COUNT(*) n FROM broker_import_review_items GROUP BY source_platform, review_status, import_readiness_status ORDER BY source_platform, review_status, import_readiness_status").fetchall()]
exec_status=[dict(r) for r in conn.execute("SELECT source_platform,payload_status,execution_status,COUNT(*) n FROM broker_import_execution_plans GROUP BY source_platform,payload_status,execution_status ORDER BY source_platform,payload_status,execution_status").fetchall()]
alerts=[dict(r) for r in conn.execute("SELECT rule_id,status,COUNT(*) n FROM alerts GROUP BY rule_id,status ORDER BY rule_id,status").fetchall()]
summary={
 'tw_initial_snapshots':len(rows),
 'all_have_core_fields':bool(all_core),
 'all_have_execution_and_review_links':bool(all_linked),
 'qty_decimal_parseable':parseable,
 'audit_import_events':conn.execute("SELECT COUNT(*) n FROM audit_log WHERE action IN ('execute_mini_import','mark_review_item_imported')").fetchone()['n'],
 'duplicate_execution_plan_ids':dup_execution,
 'duplicate_row_hashes':dup_row_hash,
 'duplicate_account_instrument_snapshot_date':dup_tuple,
 'positions_calculated':len(positions.positions),
 'dashboard_positions_count':len(dashboard_data.get_positions(conn)),
 'total_return_precise_count':sum(1 for p in positions.positions.values() if p.total_return_chf is not None),
 'position_warning_rules':sorted(set(position_warnings)),
 'review_status':review_status,
 'execution_status':exec_status,
 'alerts':alerts,
 'postfinance_imported_transactions':conn.execute("SELECT COUNT(*) n FROM transactions WHERE source_type LIKE '%postfinance%' COLLATE NOCASE").fetchone()['n'],
 'raiffeisen_imported_transactions':conn.execute("SELECT COUNT(*) n FROM transactions WHERE source_type LIKE '%raiffeisen%' COLLATE NOCASE").fetchone()['n'],
 'positions_snapshot_rows':conn.execute('SELECT COUNT(*) n FROM positions_snapshot').fetchone()['n'],
 'cash_balance_rows':conn.execute('SELECT COUNT(*) n FROM cash_balances').fetchone()['n'],
}
print(json.dumps(summary, sort_keys=True))
PY


Output: {"alerts": [{"n": 14, "rule_id": "crypto_price_stale", "status": "resolved"}, {"n": 3, "rule_id": "missing_fx", "status": "active"}, {"n": 3, "rule_id": "missing_market_price", "status": "active"}], "all_have_core_fields": true, "all_have_execution_and_review_links": true, "audit_import_events": 6, "cash_balance_rows": 0, "dashboard_positions_count": 3, "duplicate_account_instrument_snapshot_date": 0, "duplicate_execution_plan_ids": 0, "duplicate_row_hashes": 0, "execution_status": [{"execution_status": "imported", "n": 3, "payload_status": "ready", "source_platform": "True Wealth"}], "position_warning_rules": ["missing_fx", "missing_market_price"], "positions_calculated": 3, "positions_snapshot_rows": 0, "postfinance_imported_transactions": 0, "qty_decimal_parseable": 3, "raiffeisen_imported_transactions": 0, "review_status": [{"import_readiness_status": "not_ready", "n": 33, "review_status": "open", "source_platform": "PostFinance"}, {"import_readiness_status": "review_needed", "n": 1, "review_status": "open", "source_platform": "PostFinance"}, {"import_readiness_status": "blocked", "n": 4, "review_status": "blocked", "source_platform": "Raiffeisen"}, {"import_readiness_status": "review_needed", "n": 3, "review_status": "open", "source_platform": "Raiffeisen"}, {"import_readiness_status": "ready_for_import", "n": 3, "review_status": "imported", "source_platform": "True Wealth"}, {"import_readiness_status": "not_ready", "n": 14, "review_status": "open", "source_platform": "True Wealth"}, {"import_readiness_status": "review_needed", "n": 1, "review_status": "open", "source_platform": "True Wealth"}], "total_return_precise_count": 0, "tw_initial_snapshots": 3}


Explanation:
**Checking data quality**

It seems like I need to ensure the data quality for the cost basis is uncertain and focus on the snapshot only, without worrying about position warnings in the payload. I should check some counts and then run safety tests. Since this involves only runtime checks with no code changes, the `check_data_quality` function might have been created without any code commits. However, I need to compile and run `pytest` to verify everything works correctly.


set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
PYTHONPATH=src python -m compileall -q src tests
PYTHONPATH=src pytest -q
rm -rf .pytest_cache src/jarvis_finance/**/__pycache__ tests/**/__pycache__
PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan .
git status --short --branch
PYTHONPATH=src python - <<'PY'
import json
from jarvis_finance.config.settings import load_settings
from jarvis_finance.storage.database import connect
from jarvis_finance.storage.migrations import apply_migrations
conn=connect(load_settings().db_path); apply_migrations(conn)
print(json.dumps({
 'tw_initial_snapshots': conn.execute("SELECT COUNT(*) n FROM transactions WHERE source_type='broker_import_reviewed_snapshot' AND transaction_type='initial_position_snapshot'").fetchone()['n'],
 'ready_planned': conn.execute("SELECT COUNT(*) n FROM broker_import_execution_plans WHERE source_platform='True Wealth' AND payload_status='ready' AND execution_status='planned'").fetchone()['n'],
 'crypto_stale_active': conn.execute("SELECT COUNT(*) n FROM alerts WHERE rule_id='crypto_price_stale' AND status='active'").fetchone()['n'],
 'crypto_stale_resolved': conn.execute("SELECT COUNT(*) n FROM alerts WHERE rule_id='crypto_price_stale' AND status='resolved'").fetchone()['n'],
 'missing_fx_active': conn.execute("SELECT COUNT(*) n FROM alerts WHERE rule_id='missing_fx' AND status='active'").fetchone()['n'],
 'missing_market_price_active': conn.execute("SELECT COUNT(*) n FROM alerts WHERE rule_id='missing_market_price' AND status='active'").fetchone()['n'],
}, sort_keys=True))
PY


Output:
........................................................................ [ 45%]
........................................................................ [ 90%]
................                                                         [100%]
160 passed in 7.97s
GIT_SAFETY_OK
## main...origin/main
{"crypto_stale_active": 0, "crypto_stale_resolved": 14, "missing_fx_active": 3, "missing_market_price_active": 3, "ready_planned": 0, "tw_initial_snapshots": 3}


Phase B6d ist abgeschlossen, Sir.

## Kurzfazit

- Crypto-Stale-Alert-Bereinigung: **sauber**
- Dritter True-Wealth-Mini-Import durchgeführt: **ja**
- Neue Transaktionen: **1**
- Produktive True-Wealth Initial-Snapshot-Transaktionen insgesamt: **3**
- Remaining ready planned True-Wealth Items: **0**
- Direkte `positions_snapshot` Writes: **0**
- Cash-Snapshots: **0**
- PostFinance importiert: **nein**
- Raiffeisen importiert: **nein**
- Backup erzeugt/verifiziert: **ja / ja**
- Tests: **160 passed**
- Git-Safety: **GIT_SAFETY_OK**
- Git Status: **clean**, `main...origin/main`
- Code geändert: **nein**
- Commit/Push nötig: **nein**
- Letzter Code-Commit bleibt: `374fdd5`

Keine echten Mengen, Werte, Kontostände oder sensitiven Dateiinhalte wurden ausgegeben.

---

## 1. Crypto-Stale-Alerts geklärt

### Ergebnis

- Active crypto stale alerts vorher: **0**
- Active crypto stale alerts nachher: **0**
- Bereits resolved stale alerts: **14**
- Durch `--resolve-fixed` neu resolved in diesem Lauf: **0**
- Aktive Crypto Assets: **20**
- Assets mit frischem lokalem CHF-Preis: **20**
- Warnings: **0**
- Errors: **0**

Ausgeführt:

```bash
finance check-data-quality --scope crypto --resolve-fixed --currency CHF
```

Ergebnis:

- `active_crypto_assets=20`
- `fresh_price_assets=20`
- `active_stale_price_alerts_before=0`
- `active_stale_price_alerts_after=0`
- `resolved_alerts=0`
- `warnings=0`
- `errors=0`

### Warum standen im B6c-Bericht noch „14 stale crypto price Alerts“?

Das war **kein aktiver Lifecycle-Bug**.

Ursache:

- Die B6c-Zählung hatte die Alerts aggregiert, aber nicht sauber nach `status='active'` gefiltert.
- Die 14 `crypto_price_stale` Alerts waren bereits **resolved**.
- Sie bleiben historisch in der Tabelle erhalten, wie vorgesehen.
- Aktive stale Crypto-Alerts gab es zum Start von B6d bereits **0**.

Bewertung:

- Kein Rule-ID-Konflikt.
- Kein alter Fingerprint-Problem.
- Kein unresolved Lifecycle-Bug.
- Es war eine Reporting-/Interpretationsunschärfe: historische resolved Alerts wurden als „bestehend“ erwähnt.

Sehr menschlich. Also natürlich ärgerlich.

---

## 2. Dritter True-Wealth-Mini-Import

Vorher:

- Runtime-DB Backup erzeugt: **ja**
- Backup verifiziert: **ja**
- Git-Safety vorher unauffällig aus vorheriger Phase / nachher geprüft

Import:

- Dritter Mini-Import durchgeführt: **ja**
- Genau 1 Position importiert: **ja**
- Neue produktive Transaktionen: **1**
- `transaction_type = initial_position_snapshot`: **ja**
- `source_type = broker_import_reviewed_snapshot`: **ja**
- Execution Plan verknüpft: **ja**
- Review Item Status aktualisiert: **ja**
- Audit-Log geschrieben: **ja**
- Row Hash / Execution Plan Re-Import-Schutz bleibt aktiv: **ja**
- Keine Massenübernahme: **ja**
- Keine anderen True-Wealth/PostFinance/Raiffeisen-Items importiert: **ja**
- Keine echten Dateien ins Git: **ja**
- Keine direkten `positions_snapshot` Writes: **ja**
- Keine Cash-Snapshots: **ja**

Status nach Import:

- Produktive True-Wealth Initial-Snapshot-Transaktionen insgesamt: **3**
- Remaining ready planned True-Wealth Items: **0**
- `positions_snapshot` Rows: **0**
- `cash_balances` Rows: **0**

---

## 3. Konsolidierter True-Wealth-Import-Check

Alle 3 produktiven True-Wealth Initial-Snapshot-Transaktionen geprüft.

### Core-Felder

Alle 3 haben:

- `account_id`: **ja**
- `instrument_id`: **ja**
- `quantity`: **ja**
- quantity Decimal-parsebar: **3 / 3**
- `source_type = broker_import_reviewed_snapshot`: **ja**
- `source_id / execution_plan_id`: **ja**
- linked `review_item_id`: **ja**
- Audit-Log: **ja**
- Review Item Status `imported`: **ja**
- Execution Plan Status `imported`: **ja**

Audit:

- Import-bezogene Audit Events: **6**
  - je Import Execution
  - je Review Item imported Marker

### Duplicate Checks

Keine Duplikate nach:

- `execution_plan_id`: **0**
- `row_hash`: **0**
- `account_id + instrument_id + snapshot_date`: **0**

### Ledger / Dashboard

- Positionen aus Ledger berechenbar: **ja**
- Berechnete Positionen: **3**
- Portfolio/Equity-Seite erkennt Positionen: **ja**
- Dashboard Positions Count: **3**
- Total Return / Performance präzise behauptet: **nein**
- Total Return präzise berechnet trotz fehlender Daten: **0 Fälle**

Das System tut also nicht so, als könne es aus halber Historie eine perfekte Performance zaubern. Sehr beruhigend.

---

## 4. Review Queue und Data Quality Center

### True Wealth

Status:

- imported / ready_for_import: **3**
- remaining ready_for_import planned: **0**
- open / not_ready: **14**
- open / review_needed: **1**

### PostFinance

Status:

- open / not_ready: **33**
- open / review_needed: **1**
- importierte PostFinance-Transaktionen: **0**

Unverändert, wie verlangt.

### Raiffeisen

Status:

- blocked: **4**
- open / review_needed: **3**
- importierte Raiffeisen-Transaktionen: **0**

Aggregate-only bleibt faktisch blockierend; es wurde nichts importiert.

### Data Quality Center

Aktive Data Quality Hinweise nach B6d:

- `missing_fx`: **3 active**
- `missing_market_price`: **3 active**
- `crypto_price_stale`: **0 active**, **14 resolved historisch**

Korrekt angezeigt/geführt:

- importierte True-Wealth Items: **ja**
- offene Review Items: **ja**
- fehlende FX: **ja**
- fehlende Market Prices: **ja**
- veraltete aktive Crypto-Stale-Alerts, obwohl Preise frisch sind: **nein**

Zu den True-Wealth Initial Snapshots bleiben fachlich korrekt:

- `missing_fx`
- `missing_market_price`
- `cost_basis_uncertain`
- `snapshot_only`

Die letzten beiden sind im Execution-/Import-Kontext geführt; die aktive Alert-Lage im Data Quality Center zeigt aktuell vor allem FX und Market Price.

---

## 5. Tests / Safety

Ausgeführt:

- Compile: **OK**
- pytest: **160 passed**
- Git-Safety: **GIT_SAFETY_OK**
- Git Status: **clean**

Git:

- Codeänderungen: **keine**
- Commit erstellt: **nein**
- Push durchgeführt: **nein**
- Letzter Commit bleibt: `374fdd5`
- Branch Status: `main...origin/main`

---

## Bericht nach Vorgabe

### Crypto-Stale-Alert-Bereinigung

- Active stale alerts vorher: **0**
- Active stale alerts nachher: **0**
- Neu resolved durch Lauf: **0**
- Historisch bereits resolved: **14**
- Preisstatus aggregiert:
  - aktive Crypto Assets: **20**
  - frische lokale CHF-Preise: **20**
- Warnings/Errors: **0 / 0**

### True-Wealth Mini-Import

- Dritter Mini-Import durchgeführt: **ja**
- Neue Transaktionen: **1**
- Produktive True-Wealth Initial-Snapshot-Transaktionen insgesamt: **3**
- Audit-Logs vorhanden: **ja**
- Execution Plans Status:
  - imported: **3**
  - planned ready: **0**
- Review Items Status True Wealth:
  - imported / ready_for_import: **3**
  - open / not_ready: **14**
  - open / review_needed: **1**
- Data Quality Warnings:
  - missing FX: **3**
  - missing market price: **3**
  - cost basis uncertain / snapshot only weiterhin fachlich relevant
- Portfolio/Equity-Seite erkennt Positionen: **ja**
- Backup erzeugt/verifiziert: **ja / ja**
- Tests: **160 passed**
- Git-Safety: **GIT_SAFETY_OK**
- Git Status: **clean**
- Commit Hash: **kein neuer Commit**, letzter `374fdd5`
- Push Status: **nicht nötig**

---

## Empfehlung für den nächsten Schritt

Meine klare Empfehlung: **C. FX-/Market-Data-Modul für Aktien/ETFs beginnen**

### Begründung

Die drei kontrollierten True-Wealth Initial Snapshots sind jetzt sauber im Ledger. Der Engpass ist nicht mehr der Mini-Import-Mechanismus, sondern die Datenqualität danach:

- `missing_fx`: aktiv bei allen 3 importierten Positionen
- `missing_market_price`: aktiv bei allen 3 importierten Positionen
- Cost Basis bleibt unsicher
- Snapshot-only bedeutet: Bestand ja, saubere Performance nein

Ohne FX-/Market-Data-Modul würden weitere Imports nur mehr Positionen mit denselben Quality-Warnings erzeugen. Das wäre zwar hübsch voll, aber analytisch ungefähr so präzise wie ein Schweizer Fahrplan während eines Meteoriteneinschlags.

### Priorität danach

1. **C — FX-/Market-Data-Modul für Aktien/ETFs**
   - FX-Kurse für Fremdwährungen
   - lokale Market Prices für ETFs/Aktien
   - klare Staleness-Regeln
   - Alert Lifecycle wie bei Crypto sauber anwenden

2. Danach **A — Weitere True-Wealth-Items mappen/importieren**
   - Erst wenn die Datenqualität besser verwertet werden kann

3. Danach **B — PostFinance Review/Mappings starten**
   - Größere Quelle, also besser erst nach stabiler FX/Market-Data-Basis

4. **D — Data Quality / Dashboard weiter verbessern**
   - Parallel sinnvoll, aber nach FX/Market-Data deutlich nützlicher

Kurz: Der Importkanal funktioniert. Jetzt braucht das System bessere Markt- und FX-Sensorik, sonst importieren wir nur mehr Nebel mit Audit-Log.