     1|from __future__ import annotations
     2|
     3|import hashlib
     4|from datetime import datetime, timezone
     5|from sqlite3 import Connection
     6|
     7|from .schema import INITIAL_SCHEMA_SQL
     8|
     9|MIGRATION_VERSION = 10
    10|MIGRATION_NAME = "010_true_wealth_valuation_pilot"
    11|
    12|INSTRUMENT_OPTIONAL_COLUMNS = {
    13|    "position_category": "TEXT",
    14|    "ter": "TEXT",
    15|    "distribution_policy": "TEXT",
    16|    "index_name": "TEXT",
    17|    "fund_domicile": "TEXT",
    18|    "benchmark": "TEXT",
    19|    "is_currency_hedged": "INTEGER NOT NULL DEFAULT 0",
    20|    "hedged_to_currency": "TEXT",
    21|    "hedge_status": "TEXT NOT NULL DEFAULT 'unknown'",
    22|    "base_exposure_currency": "TEXT",
    23|    "trading_currency": "TEXT",
    24|    "instrument_status": "TEXT NOT NULL DEFAULT 'unknown'",
    25|    "valuation_policy": "TEXT NOT NULL DEFAULT 'live_price'",
    26|    "corporate_action_status": "TEXT NOT NULL DEFAULT 'not_checked'",
    27|    "split_or_corporate_action_review_required": "INTEGER NOT NULL DEFAULT 0",
    28|}
    29|
    30|TEXT_AFFINITY_COLUMNS = {
    31|    "transactions": {"quantity", "price_original", "gross_amount_original", "fee_original", "tax_original", "net_amount_original", "fx_rate_to_chf", "gross_amount_chf", "fee_chf", "tax_chf", "net_amount_chf"},
    32|    "crypto_holdings": {"quantity", "legacy_snapshot_value_original", "legacy_snapshot_value_chf"},
    33|    "crypto_transactions": {"quantity", "price_original", "gross_amount_original", "fee_quantity", "fee_original", "fx_rate_to_chf", "amount_chf"},
    34|    "crypto_prices": {"price", "market_cap", "volume_24h", "change_24h_pct"},
    35|    "fx_rates": {"rate"},
    36|    "market_prices": {"open", "high", "low", "close", "adjusted_close"},
    37|}
    38|
    39|
    40|def utc_now() -> str:
    41|    return datetime.now(timezone.utc).isoformat()
    42|
    43|
    44|def checksum_sql(sql: str) -> str:
    45|    return hashlib.sha256(sql.encode("utf-8")).hexdigest()
    46|
    47|
    48|def get_schema_version(conn: Connection) -> int:
    49|    row = conn.execute("SELECT MAX(version) AS version FROM schema_migrations").fetchone()
    50|    return int(row["version"] or 0) if row else 0
    51|
    52|
    53|def _table_columns(conn: Connection, table: str) -> dict[str, str]:
    54|    return {row["name"]: (row["type"] or "") for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}
    55|
    56|
    57|def _add_missing_instrument_columns(conn: Connection) -> None:
    58|    existing = _table_columns(conn, "instruments")
    59|    for name, col_type in INSTRUMENT_OPTIONAL_COLUMNS.items():
    60|        if name not in existing:
    61|            conn.execute(f"ALTER TABLE instruments ADD COLUMN {name} {col_type}")
    62|
    63|
    64|def _rebuild_table_with_text_columns(conn: Connection, table: str, text_columns: set[str]) -> None:
    65|    cols = conn.execute(f"PRAGMA table_info({table})").fetchall()
    66|    if not cols:
    67|        return
    68|    needs_rebuild = any(row["name"] in text_columns and (row["type"] or "").upper() != "TEXT" for row in cols)
    69|    if not needs_rebuild:
    70|        return
    71|    tmp = f"{table}__text_migration"
    72|    col_defs: list[str] = []
    73|    pk_cols = [row["name"] for row in cols if row["pk"]]
    74|    for row in cols:
    75|        name = row["name"]
    76|        col_type = "TEXT" if name in text_columns else (row["type"] or "TEXT")
    77|        parts = [name, col_type]
    78|        if row["pk"] and len(pk_cols) == 1:
    79|            parts.append("PRIMARY KEY")
    80|        if row["notnull"]:
    81|            parts.append("NOT NULL")
    82|        if row["dflt_value"] is not None:
    83|            parts.append(f"DEFAULT {row['dflt_value']}")
    84|        col_defs.append(" ".join(parts))
    85|    if len(pk_cols) > 1:
    86|        col_defs.append("PRIMARY KEY (" + ", ".join(pk_cols) + ")")
    87|    conn.execute(f"DROP TABLE IF EXISTS {tmp}")
    88|    conn.execute(f"CREATE TABLE {tmp} (" + ", ".join(col_defs) + ")")
    89|    names = [row["name"] for row in cols]
    90|    select_exprs = [f"CAST({name} AS TEXT)" if name in text_columns else name for name in names]
    91|    conn.execute(f"INSERT INTO {tmp} ({', '.join(names)}) SELECT {', '.join(select_exprs)} FROM {table}")
    92|    conn.execute(f"DROP TABLE {table}")
    93|    conn.execute(f"ALTER TABLE {tmp} RENAME TO {table}")
    94|    if table == "crypto_holdings":
    95|        conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_crypto_holdings_asset_wallet ON crypto_holdings(asset_id, wallet_id)")
    96|
    97|
    98|def _add_missing_columns(conn: Connection, table: str, columns: dict[str, str]) -> None:
    99|    existing = _table_columns(conn, table)
   100|    for name, col_type in columns.items():
   101|        if name not in existing:
   102|            conn.execute(f"ALTER TABLE {table} ADD COLUMN {name} {col_type}")
   103|
   104|
   105|def _create_broker_bank_mapping_tables(conn: Connection) -> None:
   106|    conn.executescript(
   107|        """
   108|        CREATE TABLE IF NOT EXISTS instrument_mappings (
   109|            mapping_id TEXT PRIMARY KEY,
   110|            source_name TEXT,
   111|            source_platform TEXT NOT NULL,
   112|            source_account TEXT,
   113|            source_label TEXT NOT NULL,
   114|            normalized_name TEXT NOT NULL,
   115|            isin TEXT,
   116|            ticker TEXT,
   117|            exchange TEXT,
   118|            currency TEXT,
   119|            asset_class TEXT NOT NULL DEFAULT 'other',
   120|            source_instrument_name TEXT,
   121|            source_isin TEXT,
   122|            source_ticker TEXT,
   123|            source_exchange TEXT,
   124|            source_currency TEXT,
   125|            instrument_id TEXT REFERENCES instruments(instrument_id),
   126|            mapping_status TEXT NOT NULL DEFAULT 'needs_manual_review',
   127|            confidence TEXT NOT NULL DEFAULT '0',
   128|            quality_flags_json TEXT,
   129|            notes TEXT,
   130|            created_at TEXT NOT NULL,
   131|            updated_at TEXT
   132|        );
   133|
   134|        CREATE TABLE IF NOT EXISTS platform_account_mappings (
   135|            mapping_id TEXT PRIMARY KEY,
   136|            source_name TEXT,
   137|            source_platform TEXT NOT NULL,
   138|            source_platform_label TEXT,
   139|            source_account_label TEXT NOT NULL,
   140|            normalized_platform TEXT NOT NULL,
   141|            normalized_account_name TEXT NOT NULL,
   142|            source_account_type TEXT,
   143|            account_type TEXT NOT NULL DEFAULT 'other',
   144|            source_currency TEXT,
   145|            currency TEXT,
   146|            platform_id TEXT REFERENCES platforms(platform_id),
   147|            account_id TEXT REFERENCES accounts(account_id),
   148|            internal_platform_id TEXT REFERENCES platforms(platform_id),
   149|            internal_account_id TEXT REFERENCES accounts(account_id),
   150|            mapping_status TEXT NOT NULL DEFAULT 'needs_manual_review',
   151|            quality_flags_json TEXT,
   152|            notes TEXT,
   153|            created_at TEXT NOT NULL,
   154|            updated_at TEXT
   155|        );
   156|
   157|        CREATE TABLE IF NOT EXISTS broker_import_dry_runs (
   158|            dry_run_id TEXT PRIMARY KEY,
   159|            source_name TEXT,
   160|            source_platform TEXT NOT NULL,
   161|            source_file_label TEXT,
   162|            source_file_type TEXT NOT NULL,
   163|            source_hash TEXT,
   164|            source_filename_hash TEXT,
   165|            detected_snapshot_date TEXT,
   166|            snapshot_date_status TEXT NOT NULL DEFAULT 'missing',
   167|            detected_sections_json TEXT NOT NULL,
   168|            field_coverage_json TEXT,
   169|            rows_total INTEGER NOT NULL DEFAULT 0,
   170|            rows_position_candidates INTEGER NOT NULL DEFAULT 0,
   171|            rows_cash_candidates INTEGER NOT NULL DEFAULT 0,
   172|            candidate_positions INTEGER NOT NULL DEFAULT 0,
   173|            candidate_cash_rows INTEGER NOT NULL DEFAULT 0,
   174|            mapped_positions INTEGER NOT NULL DEFAULT 0,
   175|            blocked_positions INTEGER NOT NULL DEFAULT 0,
   176|            warnings_count INTEGER NOT NULL DEFAULT 0,
   177|            errors_count INTEGER NOT NULL DEFAULT 0,
   178|            quality_flags_json TEXT NOT NULL,
   179|            summary_json TEXT NOT NULL DEFAULT '{}',
   180|            created_at TEXT NOT NULL,
   181|            notes TEXT
   182|        );
   183|        """
   184|    )
   185|    _add_missing_columns(conn, "instrument_mappings", {
   186|        "source_name": "TEXT",
   187|        "source_platform": "TEXT",
   188|        "source_account": "TEXT",
   189|        "source_label": "TEXT",
   190|        "normalized_name": "TEXT",
   191|        "isin": "TEXT",
   192|        "ticker": "TEXT",
   193|        "exchange": "TEXT",
   194|        "currency": "TEXT",
   195|        "asset_class": "TEXT DEFAULT 'other'",
   196|        "confidence": "TEXT DEFAULT '0'",
   197|    })
   198|    _add_missing_columns(conn, "platform_account_mappings", {
   199|        "source_platform": "TEXT",
   200|        "normalized_platform": "TEXT",
   201|        "normalized_account_name": "TEXT",
   202|        "account_type": "TEXT DEFAULT 'other'",
   203|        "currency": "TEXT",
   204|        "internal_platform_id": "TEXT",
   205|        "internal_account_id": "TEXT",
   206|    })
   207|    _add_missing_columns(conn, "broker_import_dry_runs", {
   208|        "source_platform": "TEXT",
   209|        "source_filename_hash": "TEXT",
   210|        "detected_snapshot_date": "TEXT",
   211|        "snapshot_date_status": "TEXT DEFAULT 'missing'",
   212|        "candidate_positions": "INTEGER DEFAULT 0",
   213|        "candidate_cash_rows": "INTEGER DEFAULT 0",
   214|        "mapped_positions": "INTEGER DEFAULT 0",
   215|        "blocked_positions": "INTEGER DEFAULT 0",
   216|        "warnings_count": "INTEGER DEFAULT 0",
   217|        "errors_count": "INTEGER DEFAULT 0",
   218|        "summary_json": "TEXT DEFAULT '{}'",
   219|        "session_status": "TEXT DEFAULT 'active'",
   220|        "is_current": "INTEGER DEFAULT 0",
   221|        "archived_at": "TEXT",
   222|        "discarded_at": "TEXT",
   223|    })
   224|
   225|
   226|def _create_broker_import_review_items(conn: Connection) -> None:
   227|    conn.executescript(
   228|        """
   229|        CREATE TABLE IF NOT EXISTS broker_import_review_items (
   230|            review_item_id TEXT PRIMARY KEY,
   231|            dry_run_id TEXT NOT NULL REFERENCES broker_import_dry_runs(dry_run_id),
   232|            source_platform TEXT NOT NULL,
   233|            source_file_type TEXT NOT NULL,
   234|            source_row_ref TEXT NOT NULL,
   235|            row_hash TEXT NOT NULL,
   236|            source_label TEXT,
   237|            normalized_name TEXT,
   238|            detected_asset_class TEXT,
   239|            detected_currency TEXT,
   240|            detected_quantity_present INTEGER NOT NULL DEFAULT 0,
   241|            detected_market_value_present INTEGER NOT NULL DEFAULT 0,
   242|            isin TEXT,
   243|            ticker TEXT,
   244|            exchange TEXT,
   245|            mapped_instrument_id TEXT REFERENCES instruments(instrument_id),
   246|            mapped_account_id TEXT REFERENCES accounts(account_id),
   247|            quality_flags_json TEXT NOT NULL,
   248|            review_status TEXT NOT NULL DEFAULT 'open',
   249|            reviewer_note TEXT,
   250|            created_at TEXT NOT NULL,
   251|            updated_at TEXT
   252|        );
   253|        CREATE INDEX IF NOT EXISTS idx_broker_review_items_dry_run ON broker_import_review_items(dry_run_id);
   254|        CREATE INDEX IF NOT EXISTS idx_broker_review_items_status ON broker_import_review_items(review_status);
   255|        """
   256|    )
   257|    _add_missing_columns(conn, "broker_import_review_items", {
   258|        "import_readiness_status": "TEXT DEFAULT 'not_ready'",
   259|        "reviewer_confirmed": "INTEGER DEFAULT 0",
   260|        "snapshot_date_confirmed": "INTEGER DEFAULT 0",
   261|        "ticker_exchange_confirmed": "INTEGER DEFAULT 0",
   262|        "account_mapping_status": "TEXT DEFAULT 'missing'",
   263|    })
   264|
   265|def _create_broker_import_execution_plans(conn: Connection) -> None:
   266|    conn.executescript(
   267|        """
   268|        CREATE TABLE IF NOT EXISTS broker_import_execution_plans (
   269|            execution_plan_id TEXT PRIMARY KEY,
   270|            dry_run_id TEXT NOT NULL REFERENCES broker_import_dry_runs(dry_run_id),
   271|            review_item_id TEXT NOT NULL REFERENCES broker_import_review_items(review_item_id),
   272|            source_platform TEXT NOT NULL,
   273|            target_account_id TEXT NOT NULL REFERENCES accounts(account_id),
   274|            target_instrument_id TEXT NOT NULL REFERENCES instruments(instrument_id),
   275|            transaction_type TEXT NOT NULL DEFAULT 'initial_position_snapshot',
   276|            snapshot_date TEXT,
   277|            payload_status TEXT NOT NULL,
   278|            payload_quality_flags_json TEXT NOT NULL DEFAULT '[]',
   279|            source_row_hash TEXT NOT NULL,
   280|            planned_write_summary_json TEXT NOT NULL DEFAULT '{}',
   281|            execution_status TEXT NOT NULL DEFAULT 'planned',
   282|            transaction_id TEXT REFERENCES transactions(transaction_id),
   283|            created_at TEXT NOT NULL,
   284|            updated_at TEXT,
   285|            executed_at TEXT,
   286|            notes TEXT
   287|        );
   288|        CREATE UNIQUE INDEX IF NOT EXISTS idx_broker_execution_plan_review_item ON broker_import_execution_plans(review_item_id);
   289|        CREATE UNIQUE INDEX IF NOT EXISTS idx_broker_execution_plan_source_hash ON broker_import_execution_plans(source_row_hash, target_account_id, target_instrument_id, transaction_type);
   290|        """
   291|    )
   292|
   293|def _add_transaction_void_columns(conn: Connection) -> None:
   294|    _add_missing_columns(conn, "transactions", {
   295|        "is_voided": "INTEGER NOT NULL DEFAULT 0",
   296|        "voided_at": "TEXT",
   297|        "void_reason": "TEXT",
   298|        "voided_by": "TEXT",
   299|        "correction_of_transaction_id": "TEXT REFERENCES transactions(transaction_id)",
   300|        "correction_reason": "TEXT",
   301|    })
   302|    conn.execute("CREATE INDEX IF NOT EXISTS idx_transactions_voided ON transactions(is_voided)")
   303|    conn.execute("CREATE INDEX IF NOT EXISTS idx_transactions_correction_of ON transactions(correction_of_transaction_id)")
   304|
   305|
   306|def _create_fx_market_data_tables(conn: Connection) -> None:
   307|    conn.executescript(
   308|        """
   309|        CREATE TABLE IF NOT EXISTS instrument_price_mappings (
   310|            mapping_id TEXT PRIMARY KEY,
   311|            instrument_id TEXT NOT NULL REFERENCES instruments(instrument_id),
   312|            isin TEXT,
   313|            ticker TEXT,
   314|            exchange TEXT,
   315|            currency TEXT,
   316|            provider TEXT NOT NULL,
   317|            provider_symbol TEXT,
   318|            provider_market TEXT,
   319|            mapping_status TEXT NOT NULL DEFAULT 'needs_manual_review',
   320|            confidence TEXT,
   321|            notes TEXT,
   322|            created_at TEXT NOT NULL,
   323|            updated_at TEXT,
   324|            UNIQUE(instrument_id, provider)
   325|        );
   326|        CREATE INDEX IF NOT EXISTS idx_instrument_price_mappings_status ON instrument_price_mappings(mapping_status);
   327|        CREATE INDEX IF NOT EXISTS idx_instrument_price_mappings_provider_symbol ON instrument_price_mappings(provider, provider_symbol);
   328|        """
   329|    )
   330|    _add_missing_columns(conn, "market_prices", {
   331|        "provider_market": "TEXT",
   332|        "error_message": "TEXT",
   333|        "corporate_action_status": "TEXT NOT NULL DEFAULT 'not_checked'",
   334|    })
   335|    _add_missing_columns(conn, "fx_rates", {
   336|        "error_status": "TEXT",
   337|        "error_message": "TEXT",
   338|    })
   339|    _add_missing_columns(conn, "instrument_price_mappings", {
   340|        "hedge_status": "TEXT NOT NULL DEFAULT 'unknown'",
   341|        "is_currency_hedged": "INTEGER NOT NULL DEFAULT 0",
   342|        "hedged_to_currency": "TEXT",
   343|        "instrument_status": "TEXT NOT NULL DEFAULT 'unknown'",
   344|        "valuation_policy": "TEXT NOT NULL DEFAULT 'live_price'",
   345|        "trading_currency": "TEXT",
   346|        "base_exposure_currency": "TEXT",
   347|    })
   348|    conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_market_prices_unique_provider_date ON market_prices(instrument_id, price_date, provider)")
   349|    conn.execute("CREATE INDEX IF NOT EXISTS idx_market_prices_instrument_date ON market_prices(instrument_id, price_date)")
   350|    conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_fx_rates_unique_provider_date ON fx_rates(base_currency, quote_currency, rate_date, provider, rate_type)")
   351|    conn.execute("CREATE INDEX IF NOT EXISTS idx_fx_rates_pair_date ON fx_rates(base_currency, quote_currency, rate_date)")
   352|
   353|
   354|def _apply_compat_migrations(conn: Connection) -> None:
   355|    _add_missing_instrument_columns(conn)
   356|    for table, text_columns in TEXT_AFFINITY_COLUMNS.items():
   357|        _rebuild_table_with_text_columns(conn, table, text_columns)
   358|    _create_broker_bank_mapping_tables(conn)
   359|    _create_broker_import_review_items(conn)
   360|    _create_broker_import_execution_plans(conn)
   361|    _add_transaction_void_columns(conn)
   362|    _create_fx_market_data_tables(conn)
   363|
   364|
   365|def apply_migrations(conn: Connection) -> None:
   366|    conn.executescript(INITIAL_SCHEMA_SQL)
   367|    existing_initial = conn.execute("SELECT 1 FROM schema_migrations WHERE version = 1").fetchone()
   368|    if not existing_initial:
   369|        conn.execute(
   370|            "INSERT INTO schema_migrations(version, name, applied_at, checksum) VALUES (?, ?, ?, ?)",
   371|            (1, "001_initial_schema", utc_now(), checksum_sql(INITIAL_SCHEMA_SQL)),
   372|        )
   373|    _apply_compat_migrations(conn)
   374|    existing = conn.execute("SELECT 1 FROM schema_migrations WHERE version = ?", (MIGRATION_VERSION,)).fetchone()
   375|    if not existing:
   376|        conn.execute(
   377|            "INSERT INTO schema_migrations(version, name, applied_at, checksum) VALUES (?, ?, ?, ?)",
   378|            (MIGRATION_VERSION, MIGRATION_NAME, utc_now(), checksum_sql(MIGRATION_NAME)),
   379|        )
   380|    conn.commit()
   381|


Explanation:
**Planning implementation details**

I'm considering implementing the table and service, and I think I might need tests, possibly using FakeSt. I’ll write candidate.py and focus on the providers' lookup interface to upsert candidates. I want to ensure that we generate data for True Wealth instruments while using a mock local lookup provider since public lookup may not be available. I'll make sure that the API doesn’t crash if no candidates are found and keep testing with mock data to ensure robustness.

from __future__ import annotations

from dataclasses import dataclass, field
from sqlite3 import Connection
from typing import Protocol

from jarvis_finance.audit.log import record_audit_event
from jarvis_finance.imports.common import stable_id, utc_now
from jarvis_finance.market_data.instruments import ensure_instrument_metadata_quality, resolve_instrument_alerts
from jarvis_finance.market_data.mappings import confirm_instrument_price_mapping
from jarvis_finance.quality.alerts import create_alert

HEDGE_MARKERS = ("chf hedged", "hedged chf", "currency hedged", "hedged to chf", "chf-hedged")

@dataclass(frozen=True)
class ProviderCandidate:
    provider: str
    provider_symbol: str
    exchange: str | None = None
    currency: str | None = None
    name: str | None = None
    isin: str | None = None
    is_hedged: bool | None = None
    hedged_to_currency: str | None = None
    instrument_status: str = "unknown"
    evidence_source: str = "provider_lookup"
    evidence_note: str = ""

class InstrumentLookupProvider(Protocol):
    name: str

    def lookup(self, *, isin: str | None, name: str | None, ticker: str | None, exchange: str | None, currency: str | None) -> list[ProviderCandidate]:
        ...

class LookupUnavailable(RuntimeError):
    pass

class EmptyLookupProvider:
    name = "manual"

    def lookup(self, *, isin: str | None, name: str | None, ticker: str | None, exchange: str | None, currency: str | None) -> list[ProviderCandidate]:
        return []

@dataclass
class CandidateGenerationResult:
    instruments_total: int = 0
    candidates_found: int = 0
    high_confidence: int = 0
    medium_confidence: int = 0
    low_confidence: int = 0
    instruments_without_candidate: int = 0
    instruments_with_hedge_unknown: int = 0
    instruments_with_status_unknown: int = 0
    provider_lookup_unavailable: int = 0
    ambiguous_provider_mapping: int = 0
    dry_run: bool = False
    warnings: list[str] = field(default_factory=list)

def _instrument_ids_for_true_wealth(conn: Connection, *, limit: int | None = None) -> list[str]:
    sql = """
        SELECT DISTINCT t.instrument_id
        FROM transactions t
        JOIN instruments i ON i.instrument_id=t.instrument_id
        WHERE t.transaction_type='initial_position_snapshot'
          AND t.source_type='broker_import_reviewed_snapshot'
          AND COALESCE(t.is_voided,0)=0
          AND t.instrument_id IS NOT NULL
        ORDER BY i.isin, i.name
    """
    rows = conn.execute(sql).fetchall()
    ids = [r["instrument_id"] for r in rows]
    return ids[:limit] if limit is not None else ids

def _infer_hedge(name: str | None, candidate: ProviderCandidate) -> tuple[bool | None, str | None]:
    if candidate.is_hedged is not None:
        return candidate.is_hedged, candidate.hedged_to_currency
    haystack = " ".join(x for x in [name, candidate.name, candidate.evidence_note] if x).lower()
    if any(marker in haystack for marker in HEDGE_MARKERS):
        return True, "CHF"
    return None, None

def _confidence(inst, cand: ProviderCandidate) -> tuple[str, str]:
    notes: list[str] = []
    isin_match = bool(inst["isin"] and cand.isin and inst["isin"].upper() == cand.isin.upper())
    ticker_match = bool(inst["ticker"] and cand.provider_symbol and inst["ticker"].upper() in cand.provider_symbol.upper())
    has_exchange = bool(cand.exchange or inst["exchange"])
    currency_match = bool((cand.currency or "").upper() == (inst["currency"] or "").upper()) if cand.currency and inst["currency"] else False
    if isin_match:
        notes.append("ISIN match")
        if cand.provider_symbol and has_exchange:
            return "high", "; ".join(notes + (["currency match"] if currency_match else []))
        return "medium", "; ".join(notes + ["provider symbol/exchange incomplete"])
    if ticker_match and has_exchange:
        return "medium", "ticker+exchange match; requires manual review"
    if cand.name:
        return "low", "name-only or weak provider match; manual review required"
    return "low", "weak candidate; manual review required"

def _review_status_for(confidence: str, count: int, inst, cand: ProviderCandidate) -> str:
    if count > 1:
        return "needs_manual_review"
    if confidence == "high":
        return "proposed"
    if confidence == "medium":
        return "needs_manual_review"
    return "needs_manual_review"

def upsert_candidate(conn: Connection, *, instrument_id: str, candidate: ProviderCandidate, confidence: str, review_status: str, evidence_note: str) -> str:
    inst = conn.execute("SELECT * FROM instruments WHERE instrument_id=?", (instrument_id,)).fetchone()
    if inst is None:
        raise ValueError(f"instrument not found: {instrument_id}")
    hedged, hedged_to = _infer_hedge(inst["name"], candidate)
    candidate_id = stable_id("ipmc", instrument_id, candidate.provider, candidate.provider_symbol, candidate.exchange or "", candidate.currency or "")
    now = utc_now()
    conn.execute(
        """
        INSERT INTO instrument_price_mapping_candidates(
            candidate_id, instrument_id, isin, candidate_provider, candidate_provider_symbol,
            candidate_exchange, candidate_currency, candidate_name, candidate_is_hedged,
            candidate_hedged_to_currency, candidate_instrument_status, confidence,
            evidence_source, evidence_note, review_status, created_at, updated_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        ON CONFLICT(candidate_id) DO UPDATE SET
            candidate_exchange=excluded.candidate_exchange,
            candidate_currency=excluded.candidate_currency,
            candidate_name=excluded.candidate_name,
            candidate_is_hedged=excluded.candidate_is_hedged,
            candidate_hedged_to_currency=excluded.candidate_hedged_to_currency,
            candidate_instrument_status=excluded.candidate_instrument_status,
            confidence=excluded.confidence,
            evidence_source=excluded.evidence_source,
            evidence_note=excluded.evidence_note,
            review_status=excluded.review_status,
            updated_at=excluded.updated_at
        """,
        (
            candidate_id, instrument_id, inst["isin"], candidate.provider, candidate.provider_symbol,
            candidate.exchange, candidate.currency or inst["currency"], candidate.name, 1 if hedged else 0 if hedged is False else None,
            hedged_to, candidate.instrument_status or "unknown", confidence, candidate.evidence_source,
            evidence_note or candidate.evidence_note, review_status, now, now,
        ),
    )
    return candidate_id

def generate_mapping_candidates(
    conn: Connection,
    *,
    provider: InstrumentLookupProvider | None = None,
    instrument_ids: list[str] | None = None,
    source: str = "true_wealth",
    limit: int | None = None,
    dry_run: bool = False,
) -> CandidateGenerationResult:
    lookup = provider or EmptyLookupProvider()
    ids = instrument_ids if instrument_ids is not None else _instrument_ids_for_true_wealth(conn, limit=limit)
    result = CandidateGenerationResult(instruments_total=len(ids), dry_run=dry_run)
    for instrument_id in ids:
        inst = conn.execute("SELECT * FROM instruments WHERE instrument_id=?", (instrument_id,)).fetchone()
        if inst is None:
            continue
        ensure_instrument_metadata_quality(conn, instrument_id=instrument_id)
        if (inst["hedge_status"] or "unknown") == "unknown":
            result.instruments_with_hedge_unknown += 1
        if (inst["instrument_status"] or "unknown") == "unknown":
            result.instruments_with_status_unknown += 1
        try:
            candidates = lookup.lookup(isin=inst["isin"], name=inst["name"], ticker=inst["ticker"], exchange=inst["exchange"], currency=inst["currency"])
        except Exception as exc:
            result.provider_lookup_unavailable += 1
            result.warnings.append(instrument_id)
            if not dry_run:
                create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="provider_lookup_unavailable", message="Provider lookup unavailable for instrument mapping candidates.", evidence={"provider": getattr(lookup, "name", "unknown"), "error_type": type(exc).__name__}, fingerprint=f"provider_lookup_unavailable:{getattr(lookup, 'name', 'unknown')}")
            continue
        if not candidates:
            result.instruments_without_candidate += 1
            if not dry_run:
                create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="missing_provider_symbol", message="No provider-symbol candidate found; manual mapping required.", evidence={"source": source, "provider": getattr(lookup, "name", "manual")}, fingerprint=f"missing_provider_symbol:{getattr(lookup, 'name', 'manual')}")
            continue
        if len(candidates) > 1:
            result.ambiguous_provider_mapping += 1
            if not dry_run:
                create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="ambiguous_provider_mapping", message="Multiple provider-symbol candidates require manual review.", evidence={"candidate_count": len(candidates), "provider": getattr(lookup, "name", "unknown")}, fingerprint=f"ambiguous_provider_mapping:{getattr(lookup, 'name', 'unknown')}:{len(candidates)}")
        for cand in candidates:
            confidence, note = _confidence(inst, cand)
            if confidence == "high":
                result.high_confidence += 1
            elif confidence == "medium":
                result.medium_confidence += 1
            else:
                result.low_confidence += 1
            status = _review_status_for(confidence, len(candidates), inst, cand)
            result.candidates_found += 1
            if not dry_run:
                upsert_candidate(conn, instrument_id=instrument_id, candidate=cand, confidence=confidence, review_status=status, evidence_note=note)
    if not dry_run:
        conn.commit()
    return result

def select_mapping_candidate(conn: Connection, *, candidate_id: str, note: str, created_by: str = "system") -> str:
    if not note.strip():
        raise ValueError("candidate selection requires a note")
    cand = conn.execute("SELECT * FROM instrument_price_mapping_candidates WHERE candidate_id=?", (candidate_id,)).fetchone()
    if cand is None:
        raise ValueError(f"candidate not found: {candidate_id}")
    mapping_id = confirm_instrument_price_mapping(
        conn,
        instrument_id=cand["instrument_id"],
        provider=cand["candidate_provider"],
        provider_symbol=cand["candidate_provider_symbol"],
        provider_market=cand["candidate_exchange"],
        confidence=cand["confidence"],
        note=note,
        exchange=cand["candidate_exchange"],
        currency=cand["candidate_currency"],
        hedge_status="hedged" if cand["candidate_is_hedged"] == 1 else None,
        hedged_to_currency=cand["candidate_hedged_to_currency"],
        instrument_status=cand["candidate_instrument_status"] if cand["candidate_instrument_status"] != "unknown" else None,
        valuation_policy="live_price" if cand["candidate_instrument_status"] == "active" else None,
        trading_currency=cand["candidate_currency"],
        created_by=created_by,
    )
    now = utc_now()
    conn.execute("UPDATE instrument_price_mapping_candidates SET review_status='selected', updated_at=? WHERE candidate_id=?", (now, candidate_id))
    conn.execute("UPDATE instrument_price_mapping_candidates SET review_status='rejected', updated_at=? WHERE instrument_id=? AND candidate_id<>? AND review_status IN ('proposed','needs_manual_review')", (now, cand["instrument_id"], candidate_id))
    record_audit_event(
        conn,
        source="instrument_price_mapping_candidate",
        action="select_mapping_candidate",
        entity_type="instrument_price_mapping_candidate",
        entity_id=candidate_id,
        new_values={"mapping_id": mapping_id, "instrument_id": cand["instrument_id"], "provider": cand["candidate_provider"], "provider_symbol": cand["candidate_provider_symbol"]},
        user_text_note=note,
        confirmed=True,
        created_by=created_by,
    )
    resolve_instrument_alerts(conn, instrument_id=cand["instrument_id"], rule_ids=["missing_provider_symbol", "ambiguous_provider_mapping"])
    conn.commit()
    return mapping_id

def get_mapping_candidate_summary(conn: Connection) -> dict[str, int]:
    rows = conn.execute("SELECT confidence, COUNT(*) AS c FROM instrument_price_mapping_candidates GROUP BY confidence").fetchall()
    out = {"total": 0, "high": 0, "medium": 0, "low": 0}
    for row in rows:
        key = row["confidence"] or "low"
        if key in out:
            out[key] += int(row["c"] or 0)
        out["total"] += int(row["c"] or 0)
    return out


patch

MIGRATION_VERSION = 10
MIGRATION_NAME = "010_true_wealth_valuation_pilot"

MIGRATION_VERSION = 11
MIGRATION_NAME = "011_true_wealth_mapping_candidate_review"


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/storage/migrations.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/storage/migrations.py
@@ -6,8 +6,8 @@

 from .schema import INITIAL_SCHEMA_SQL

-MIGRATION_VERSION = 10
-MIGRATION_NAME = "010_true_wealth_valuation_pilot"
+MIGRATION_VERSION = 11
+MIGRATION_NAME = "011_true_wealth_mapping_candidate_review"

 INSTRUMENT_OPTIONAL_COLUMNS = {
     "position_category": "TEXT",


    conn.executescript(
        """
        CREATE TABLE IF NOT EXISTS instrument_price_mapping_candidates (
            candidate_id TEXT PRIMARY KEY,
            instrument_id TEXT NOT NULL REFERENCES instruments(instrument_id),
            isin TEXT,
            candidate_provider TEXT NOT NULL,
            candidate_provider_symbol TEXT NOT NULL,
            candidate_exchange TEXT,
            candidate_currency TEXT,
            candidate_name TEXT,
            candidate_is_hedged INTEGER,
            candidate_hedged_to_currency TEXT,
            candidate_instrument_status TEXT NOT NULL DEFAULT 'unknown',
            confidence TEXT NOT NULL DEFAULT 'low',
            evidence_source TEXT,
            evidence_note TEXT,
            review_status TEXT NOT NULL DEFAULT 'proposed',
            created_at TEXT NOT NULL,
            updated_at TEXT
        );
        CREATE INDEX IF NOT EXISTS idx_ipmc_instrument ON instrument_price_mapping_candidates(instrument_id);
        CREATE INDEX IF NOT EXISTS idx_ipmc_review_status ON instrument_price_mapping_candidates(review_status);
        CREATE INDEX IF NOT EXISTS idx_ipmc_confidence ON instrument_price_mapping_candidates(confidence);
        """
    )
    conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_market_prices_unique_provider_date ON market_prices(instrument_id, price_date, provider)")


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/storage/migrations.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/storage/migrations.py
@@ -345,6 +345,32 @@
         "trading_currency": "TEXT",
         "base_exposure_currency": "TEXT",
     })
+    conn.executescript(
+        """
+        CREATE TABLE IF NOT EXISTS instrument_price_mapping_candidates (
+            candidate_id TEXT PRIMARY KEY,
+            instrument_id TEXT NOT NULL REFERENCES instruments(instrument_id),
+            isin TEXT,
+            candidate_provider TEXT NOT NULL,
+            candidate_provider_symbol TEXT NOT NULL,
+            candidate_exchange TEXT,
+            candidate_currency TEXT,
+            candidate_name TEXT,
+            candidate_is_hedged INTEGER,
+            candidate_hedged_to_currency TEXT,
+            candidate_instrument_status TEXT NOT NULL DEFAULT 'unknown',
+            confidence TEXT NOT NULL DEFAULT 'low',
+            evidence_source TEXT,
+            evidence_note TEXT,
+            review_status TEXT NOT NULL DEFAULT 'proposed',
+            created_at TEXT NOT NULL,
+            updated_at TEXT
+        );
+        CREATE INDEX IF NOT EXISTS idx_ipmc_instrument ON instrument_price_mapping_candidates(instrument_id);
+        CREATE INDEX IF NOT EXISTS idx_ipmc_review_status ON instrument_price_mapping_candidates(review_status);
+        CREATE INDEX IF NOT EXISTS idx_ipmc_confidence ON instrument_price_mapping_candidates(confidence);
+        """
+    )
     conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_market_prices_unique_provider_date ON market_prices(instrument_id, price_date, provider)")
     conn.execute("CREATE INDEX IF NOT EXISTS idx_market_prices_instrument_date ON market_prices(instrument_id, price_date)")
     conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_fx_rates_unique_provider_date ON fx_rates(base_currency, quote_currency, rate_date, provider, rate_type)")


        ("fehlende Provider-Symbole", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='missing_provider_symbol'", "warnung"),
        ("provider lookup unavailable", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='provider_lookup_unavailable'", "warnung"),
        ("ambiguous provider mapping", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='ambiguous_provider_mapping'", "warnung"),
        ("instrument status unknown", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='instrument_status_unknown'", "warnung"),
        ("manual FX override required", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id IN ('manual_override_required','manual_fx_override_required')", "kritisch"),


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/data.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/data.py
@@ -282,6 +282,10 @@
         ("historical FX unavailable", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='historical_fx_unavailable'", "kritisch"),
         ("manual override required", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='manual_override_required'", "kritisch"),
         ("fehlende Provider-Symbole", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='missing_provider_symbol'", "warnung"),
+        ("provider lookup unavailable", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='provider_lookup_unavailable'", "warnung"),
+        ("ambiguous provider mapping", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='ambiguous_provider_mapping'", "warnung"),
+        ("instrument status unknown", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='instrument_status_unknown'", "warnung"),
+        ("manual FX override required", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id IN ('manual_override_required','manual_fx_override_required')", "kritisch"),
         ("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"),

/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/data.py was modified by sibling subagent '20260515_132829_5f4002' but this agent never read it. Read the file before writing to avoid overwriting the sibling's changes.


def get_instrument_mapping_review(conn: Connection) -> list[dict[str, str]]:
    rows = conn.execute(
        """
        SELECT i.instrument_id, i.isin, i.name, i.ticker, i.exchange, i.currency,
               i.hedge_status, i.instrument_status, i.valuation_policy,
               COALESCE(m.mapping_status, 'missing_provider_symbol') AS mapping_status,
               COALESCE(m.provider, '') AS provider,
               COALESCE(m.provider_symbol, '') AS provider_symbol,
               COUNT(c.candidate_id) AS candidate_count
        FROM instruments i
        JOIN transactions t ON t.instrument_id=i.instrument_id
        LEFT JOIN instrument_price_mappings m ON m.instrument_id=i.instrument_id AND m.mapping_status='mapped'
        LEFT JOIN instrument_price_mapping_candidates c ON c.instrument_id=i.instrument_id AND c.review_status IN ('proposed','needs_manual_review')
        WHERE t.transaction_type='initial_position_snapshot'
          AND t.source_type='broker_import_reviewed_snapshot'
          AND COALESCE(t.is_voided,0)=0
        GROUP BY i.instrument_id
        ORDER BY i.isin, i.name
        """
    ).fetchall()
    return rowdicts(rows)

def get_instrument_mapping_candidates(conn: Connection, instrument_id: str | None = None) -> list[dict[str, str]]:
    if instrument_id:
        rows = conn.execute(
            """
            SELECT candidate_id, instrument_id, isin, candidate_provider, candidate_provider_symbol,
                   candidate_exchange, candidate_currency, candidate_name, candidate_is_hedged,
                   candidate_hedged_to_currency, candidate_instrument_status, confidence,
                   evidence_source, evidence_note, review_status, created_at
            FROM instrument_price_mapping_candidates WHERE instrument_id=?
            ORDER BY CASE confidence WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END, candidate_provider
            """,
            (instrument_id,),
        ).fetchall()
    else:
        rows = conn.execute(
            """
            SELECT candidate_id, instrument_id, isin, candidate_provider, candidate_provider_symbol,
                   candidate_exchange, candidate_currency, candidate_name, candidate_is_hedged,
                   candidate_hedged_to_currency, candidate_instrument_status, confidence,
                   evidence_source, evidence_note, review_status, created_at
            FROM instrument_price_mapping_candidates
            ORDER BY instrument_id, CASE confidence WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END, candidate_provider
            """
        ).fetchall()
    return rowdicts(rows)

def get_fx_override_requirements(conn: Connection) -> list[dict[str, str]]:
    rows = conn.execute(
        """
        SELECT t.currency_original AS base_currency, 'CHF' AS quote_currency, t.trade_date AS rate_date,
               COUNT(DISTINCT t.instrument_id) AS instrument_count,
               CASE WHEN fr.fx_rate_id IS NULL THEN 'missing_fx' ELSE fr.quality_status END AS fx_status
        FROM transactions t
        LEFT JOIN fx_rates fr ON fr.base_currency=t.currency_original AND fr.quote_currency='CHF' AND fr.rate_date=t.trade_date
        WHERE t.transaction_type='initial_position_snapshot'
          AND t.source_type='broker_import_reviewed_snapshot'
          AND COALESCE(t.is_voided,0)=0
          AND t.currency_original IS NOT NULL
          AND t.currency_original <> 'CHF'
        GROUP BY t.currency_original, t.trade_date, fx_status
        ORDER BY t.currency_original, t.trade_date
        """
    ).fetchall()
    return rowdicts(rows)

def get_import_wizard_summary(conn: Connection) -> dict[str, str]:


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/data.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/data.py
@@ -328,6 +328,76 @@
     return items

+def get_instrument_mapping_review(conn: Connection) -> list[dict[str, str]]:
+    rows = conn.execute(
+        """
+        SELECT i.instrument_id, i.isin, i.name, i.ticker, i.exchange, i.currency,
+               i.hedge_status, i.instrument_status, i.valuation_policy,
+               COALESCE(m.mapping_status, 'missing_provider_symbol') AS mapping_status,
+               COALESCE(m.provider, '') AS provider,
+               COALESCE(m.provider_symbol, '') AS provider_symbol,
+               COUNT(c.candidate_id) AS candidate_count
+        FROM instruments i
+        JOIN transactions t ON t.instrument_id=i.instrument_id
+        LEFT JOIN instrument_price_mappings m ON m.instrument_id=i.instrument_id AND m.mapping_status='mapped'
+        LEFT JOIN instrument_price_mapping_candidates c ON c.instrument_id=i.instrument_id AND c.review_status IN ('proposed','needs_manual_review')
+        WHERE t.transaction_type='initial_position_snapshot'
+          AND t.source_type='broker_import_reviewed_snapshot'
+          AND COALESCE(t.is_voided,0)=0
+        GROUP BY i.instrument_id
+        ORDER BY i.isin, i.name
+        """
+    ).fetchall()
+    return rowdicts(rows)
+
+
+def get_instrument_mapping_candidates(conn: Connection, instrument_id: str | None = None) -> list[dict[str, str]]:
+    if instrument_id:
+        rows = conn.execute(
+            """
+            SELECT candidate_id, instrument_id, isin, candidate_provider, candidate_provider_symbol,
+                   candidate_exchange, candidate_currency, candidate_name, candidate_is_hedged,
+                   candidate_hedged_to_currency, candidate_instrument_status, confidence,
+                   evidence_source, evidence_note, review_status, created_at
+            FROM instrument_price_mapping_candidates WHERE instrument_id=?
+            ORDER BY CASE confidence WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END, candidate_provider
+            """,
+            (instrument_id,),
+        ).fetchall()
+    else:
+        rows = conn.execute(
+            """
+            SELECT candidate_id, instrument_id, isin, candidate_provider, candidate_provider_symbol,
+                   candidate_exchange, candidate_currency, candidate_name, candidate_is_hedged,
+                   candidate_hedged_to_currency, candidate_instrument_status, confidence,
+                   evidence_source, evidence_note, review_status, created_at
+            FROM instrument_price_mapping_candidates
+            ORDER BY instrument_id, CASE confidence WHEN 'high' THEN 0 WHEN 'medium' THEN 1 ELSE 2 END, candidate_provider
+            """
+        ).fetchall()
+    return rowdicts(rows)
+
+
+def get_fx_override_requirements(conn: Connection) -> list[dict[str, str]]:
+    rows = conn.execute(
+        """
+        SELECT t.currency_original AS base_currency, 'CHF' AS quote_currency, t.trade_date AS rate_date,
+               COUNT(DISTINCT t.instrument_id) AS instrument_count,
+               CASE WHEN fr.fx_rate_id IS NULL THEN 'missing_fx' ELSE fr.quality_status END AS fx_status
+        FROM transactions t
+        LEFT JOIN fx_rates fr ON fr.base_currency=t.currency_original AND fr.quote_currency='CHF' AND fr.rate_date=t.trade_date
+        WHERE t.transaction_type='initial_position_snapshot'
+          AND t.source_type='broker_import_reviewed_snapshot'
+          AND COALESCE(t.is_voided,0)=0
+          AND t.currency_original IS NOT NULL
+          AND t.currency_original <> 'CHF'
+        GROUP BY t.currency_original, t.trade_date, fx_status
+        ORDER BY t.currency_original, t.trade_date
+        """
+    ).fetchall()
+    return rowdicts(rows)
+
+
 def get_import_wizard_summary(conn: Connection) -> dict[str, str]:
     from jarvis_finance.imports.broker_mapping import build_import_wizard_summary


Explanation:
**Planning UI updates**

I need to create a page and add a review tab for equity management. It's important to ensure that no live data is displayed. The user interface should allow for selection and saving of information. I also need to consider including an FX override UI with a note. Lastly, I have to incorporate a function that requires a Decimal format. It’s a lot to juggle, but I’m working through it!

from decimal import Decimal

from jarvis_finance.dashboard import data
from jarvis_finance.dashboard.components.tables import show_table
from jarvis_finance.equity.manage import add_equity_initial_snapshot, add_equity_transaction, get_equity_management_options
from jarvis_finance.fx.overrides import set_manual_fx_override
from jarvis_finance.market_data.candidates import select_mapping_candidate


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/page_13_equity_manage.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/page_13_equity_manage.py
@@ -1,8 +1,12 @@
 from __future__ import annotations
+
+from decimal import Decimal

 from jarvis_finance.dashboard import data
 from jarvis_finance.dashboard.components.tables import show_table
 from jarvis_finance.equity.manage import add_equity_initial_snapshot, add_equity_transaction, get_equity_management_options
+from jarvis_finance.fx.overrides import set_manual_fx_override
+from jarvis_finance.market_data.candidates import select_mapping_candidate

 PAGE_TITLE = "Equity/ETF Manage"


    with tabs[8]:
        st.subheader("Aktuelle Equity/ETF Übersicht")
        show_table(st, data.get_positions(conn))
        st.divider()
        st.subheader("Instrument Price Mapping Review")
        st.caption("Runtime-Review: Kandidaten ansehen und manuell bestätigen. Keine Live-API und keine Positionsänderung beim Rendern.")
        review_rows = data.get_instrument_mapping_review(conn)
        show_table(st, review_rows)
        candidates = data.get_instrument_mapping_candidates(conn)
        show_table(st, candidates)
        candidate_labels = [f"{c['candidate_id']} | {c['candidate_provider']} | {c['candidate_provider_symbol']} | {c['confidence']} | {c['review_status']}" for c in candidates]
        selected_candidate_label = st.selectbox("Mapping-Kandidat auswählen", candidate_labels, key="mapping_candidate_select") if candidate_labels else None
        mapping_note = st.text_area("Bestätigungsnotiz für Mapping-Auswahl", key="mapping_candidate_note")
        if st.button("Ausgewählten Mapping-Kandidaten bestätigen", key="mapping_candidate_confirm"):
            try:
                if not selected_candidate_label:
                    raise ValueError("candidate is required")
                candidate_id = selected_candidate_label.split(" | ", 1)[0]
                mapping_id = select_mapping_candidate(conn, candidate_id=candidate_id, note=mapping_note, created_by="dashboard")
                st.success("Mapping-Kandidat bestätigt; Audit-Log geschrieben.")
                st.write({"mapping_id": mapping_id})
            except Exception as exc:
                st.error(f"Mapping-Auswahl abgelehnt: {exc}")
        st.divider()
        st.subheader("FX Override Review")
        st.caption("Historische FX-Rates nur manuell mit Quelle/Notiz erfassen. Keine Rates ohne bewusste Speicherung.")
        fx_rows = data.get_fx_override_requirements(conn)
        show_table(st, fx_rows)
        fx_labels = [f"{r['base_currency']}/{r['quote_currency']} {r['rate_date']} ({r['fx_status']})" for r in fx_rows]
        fx_label = st.selectbox("FX-Paar/Datum", fx_labels, key="fx_override_select") if fx_labels else None
        fx_rate = st.text_input("FX-Rate zu CHF", key="fx_override_rate")
        fx_note = st.text_area("FX-Quelle/Notiz (Pflicht)", key="fx_override_note")
        if st.button("Manual FX Override speichern", key="fx_override_save"):
            try:
                if not fx_label:
                    raise ValueError("fx pair/date is required")
                pair, date_part = fx_label.split(" ", 1)
                base, quote = pair.split("/", 1)
                rate_date = date_part.split(" ", 1)[0]
                set_manual_fx_override(conn, base_currency=base, quote_currency=quote, rate_date=rate_date, rate=Decimal(fx_rate), note=fx_note, created_by="dashboard")
                st.success("Manual FX Override gespeichert und auditierbar erfasst.")
            except Exception as exc:
                st.error(f"FX Override abgelehnt: {exc}")


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/page_13_equity_manage.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/page_13_equity_manage.py
@@ -151,3 +151,43 @@
     with tabs[8]:
         st.subheader("Aktuelle Equity/ETF Übersicht")
         show_table(st, data.get_positions(conn))
+        st.divider()
+        st.subheader("Instrument Price Mapping Review")
+        st.caption("Runtime-Review: Kandidaten ansehen und manuell bestätigen. Keine Live-API und keine Positionsänderung beim Rendern.")
+        review_rows = data.get_instrument_mapping_review(conn)
+        show_table(st, review_rows)
+        candidates = data.get_instrument_mapping_candidates(conn)
+        show_table(st, candidates)
+        candidate_labels = [f"{c['candidate_id']} | {c['candidate_provider']} | {c['candidate_provider_symbol']} | {c['confidence']} | {c['review_status']}" for c in candidates]
+        selected_candidate_label = st.selectbox("Mapping-Kandidat auswählen", candidate_labels, key="mapping_candidate_select") if candidate_labels else None
+        mapping_note = st.text_area("Bestätigungsnotiz für Mapping-Auswahl", key="mapping_candidate_note")
+        if st.button("Ausgewählten Mapping-Kandidaten bestätigen", key="mapping_candidate_confirm"):
+            try:
+                if not selected_candidate_label:
+                    raise ValueError("candidate is required")
+                candidate_id = selected_candidate_label.split(" | ", 1)[0]
+                mapping_id = select_mapping_candidate(conn, candidate_id=candidate_id, note=mapping_note, created_by="dashboard")
+                st.success("Mapping-Kandidat bestätigt; Audit-Log geschrieben.")
+                st.write({"mapping_id": mapping_id})
+            except Exception as exc:
+                st.error(f"Mapping-Auswahl abgelehnt: {exc}")
+        st.divider()
+        st.subheader("FX Override Review")
+        st.caption("Historische FX-Rates nur manuell mit Quelle/Notiz erfassen. Keine Rates ohne bewusste Speicherung.")
+        fx_rows = data.get_fx_override_requirements(conn)
+        show_table(st, fx_rows)
+        fx_labels = [f"{r['base_currency']}/{r['quote_currency']} {r['rate_date']} ({r['fx_status']})" for r in fx_rows]
+        fx_label = st.selectbox("FX-Paar/Datum", fx_labels, key="fx_override_select") if fx_labels else None
+        fx_rate = st.text_input("FX-Rate zu CHF", key="fx_override_rate")
+        fx_note = st.text_area("FX-Quelle/Notiz (Pflicht)", key="fx_override_note")
+        if st.button("Manual FX Override speichern", key="fx_override_save"):
+            try:
+                if not fx_label:
+                    raise ValueError("fx pair/date is required")
+                pair, date_part = fx_label.split(" ", 1)
+                base, quote = pair.split("/", 1)
+                rate_date = date_part.split(" ", 1)[0]
+                set_manual_fx_override(conn, base_currency=base, quote_currency=quote, rate_date=rate_date, rate=Decimal(fx_rate), note=fx_note, created_by="dashboard")
+                st.success("Manual FX Override gespeichert und auditierbar erfasst.")
+            except Exception as exc:
+                st.error(f"FX Override abgelehnt: {exc}")


    st.button("Instrument Price Mapping Review öffnen", disabled=True)
    st.button("FX Override Review öffnen", disabled=True)
    st.button("Provider Lookup Recheck", disabled=True)

    st.button("Instrument Price Mapping öffnen", disabled=True)
    st.button("FX Override / FX Provider öffnen", disabled=True)


--- 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,8 +22,9 @@
     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("FX Override / FX Provider öffnen", disabled=True)
+    st.button("Instrument Price Mapping Review öffnen", disabled=True)
+    st.button("FX Override Review öffnen", disabled=True)
+    st.button("Provider Lookup Recheck", disabled=True)
     st.button("Instrument Status Review öffnen", disabled=True)
     st.button("Hedge Status Review öffnen", disabled=True)
     st.button("Manual Review Queue öffnen", disabled=True)


     1|from __future__ import annotations
     2|
     3|from dataclasses import dataclass, field
     4|from sqlite3 import Connection
     5|
     6|from jarvis_finance.audit.log import record_audit_event
     7|from jarvis_finance.imports.common import utc_now
     8|from jarvis_finance.quality.alerts import create_alert
     9|
    10|HEDGE_STATUSES = {"hedged", "unhedged", "unknown"}
    11|INSTRUMENT_STATUSES = {"active", "delisted", "suspended", "merged", "inactive", "unknown", "suspected_inactive"}
    12|VALUATION_POLICIES = {"live_price", "last_known_price", "manual_value", "exclude_from_auto_price_update"}
    13|CORPORATE_ACTION_STATUSES = {"none_known", "suspected", "confirmed", "adjusted", "ignored", "unknown", "not_checked"}
    14|
    15|
    16|@dataclass
    17|class InstrumentMetadataResult:
    18|    instrument_id: str
    19|    quality_flags: list[str] = field(default_factory=list)
    20|
    21|
    22|def _instrument(conn: Connection, instrument_id: str):
    23|    row = conn.execute("SELECT * FROM instruments WHERE instrument_id=?", (instrument_id,)).fetchone()
    24|    if row is None:
    25|        raise ValueError(f"instrument not found: {instrument_id}")
    26|    return row
    27|
    28|
    29|def resolve_instrument_alerts(conn: Connection, *, instrument_id: str, rule_ids: list[str]) -> int:
    30|    now = utc_now()
    31|    if not rule_ids:
    32|        return 0
    33|    rows = conn.execute(
    34|        "SELECT alert_id FROM alerts WHERE status='active' AND entity_type='instrument' AND entity_id=? AND rule_id IN (" + ",".join("?" for _ in rule_ids) + ")",
    35|        (instrument_id, *rule_ids),
    36|    ).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 ensure_instrument_metadata_quality(conn: Connection, *, instrument_id: str) -> InstrumentMetadataResult:
    43|    inst = _instrument(conn, instrument_id)
    44|    result = InstrumentMetadataResult(instrument_id=instrument_id)
    45|    hedge_status = inst["hedge_status"] or "unknown"
    46|    instrument_status = inst["instrument_status"] or "unknown"
    47|    corporate_action_status = inst["corporate_action_status"] or "not_checked"
    48|    if hedge_status == "unknown":
    49|        result.quality_flags.append("hedge_status_unknown")
    50|        create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="hedge_status_unknown", message="Currency hedge status is unknown and must be reviewed before precise valuation attribution.", evidence={"isin": inst["isin"]}, fingerprint="hedge_status_unknown")
    51|    else:
    52|        resolve_instrument_alerts(conn, instrument_id=instrument_id, rule_ids=["hedge_status_unknown"])
    53|    if instrument_status in {"delisted", "suspended", "merged", "inactive"}:
    54|        result.quality_flags.append("delisted_or_suspended")
    55|        create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="delisted_or_suspended", message="Instrument is not active; automatic price update must be excluded or manually valued.", evidence={"status": instrument_status}, fingerprint=f"delisted_or_suspended:{instrument_status}")
    56|    if corporate_action_status in {"unknown", "not_checked"}:
    57|        # Visible status, not necessarily blocking; alert only when explicit review flag is set.
    58|        if inst["split_or_corporate_action_review_required"]:
    59|            result.quality_flags.append("split_or_corporate_action_review_required")
    60|            create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="split_or_corporate_action_review_required", message="Corporate action review is required before high-confidence valuation.", evidence={"corporate_action_status": corporate_action_status}, fingerprint="split_or_corporate_action_review_required")
    61|    conn.commit()
    62|    return result
    63|
    64|
    65|def update_instrument_metadata(
    66|    conn: Connection,
    67|    *,
    68|    instrument_id: str,
    69|    hedge_status: str | None = None,
    70|    is_currency_hedged: bool | None = None,
    71|    hedged_to_currency: str | None = None,
    72|    instrument_status: str | None = None,
    73|    valuation_policy: str | None = None,
    74|    base_exposure_currency: str | None = None,
    75|    trading_currency: str | None = None,
    76|    corporate_action_status: str | None = None,
    77|    split_or_corporate_action_review_required: bool | None = None,
    78|    note: str,
    79|    created_by: str = "system",
    80|) -> None:
    81|    if not note.strip():
    82|        raise ValueError("instrument metadata update requires a note")
    83|    old = dict(_instrument(conn, instrument_id))
    84|    values: dict[str, object] = {}
    85|    if hedge_status is not None:
    86|        if hedge_status not in HEDGE_STATUSES:
    87|            raise ValueError("invalid hedge_status")
    88|        values["hedge_status"] = hedge_status
    89|        values["is_currency_hedged"] = 1 if hedge_status == "hedged" else 0
    90|    if is_currency_hedged is not None:
    91|        values["is_currency_hedged"] = 1 if is_currency_hedged else 0
    92|    if hedged_to_currency is not None:
    93|        values["hedged_to_currency"] = hedged_to_currency.upper() if hedged_to_currency else None
    94|    if instrument_status is not None:
    95|        if instrument_status not in INSTRUMENT_STATUSES:
    96|            raise ValueError("invalid instrument_status")
    97|        values["instrument_status"] = instrument_status
    98|    if valuation_policy is not None:
    99|        if valuation_policy not in VALUATION_POLICIES:
   100|            raise ValueError("invalid valuation_policy")
   101|        values["valuation_policy"] = valuation_policy
   102|    if base_exposure_currency is not None:
   103|        values["base_exposure_currency"] = base_exposure_currency.upper() if base_exposure_currency else None
   104|    if trading_currency is not None:
   105|        values["trading_currency"] = trading_currency.upper() if trading_currency else None
   106|    if corporate_action_status is not None:
   107|        if corporate_action_status not in CORPORATE_ACTION_STATUSES:
   108|            raise ValueError("invalid corporate_action_status")
   109|        values["corporate_action_status"] = corporate_action_status
   110|    if split_or_corporate_action_review_required is not None:
   111|        values["split_or_corporate_action_review_required"] = 1 if split_or_corporate_action_review_required else 0
   112|    if not values:
   113|        return
   114|    values["updated_at"] = utc_now()
   115|    assignments = ", ".join(f"{k}=?" for k in values)
   116|    conn.execute(f"UPDATE instruments SET {assignments} WHERE instrument_id=?", (*values.values(), instrument_id))
   117|    if values.get("hedge_status") and values["hedge_status"] != "unknown":
   118|        resolve_instrument_alerts(conn, instrument_id=instrument_id, rule_ids=["hedge_status_unknown"])
   119|    if values.get("instrument_status") == "active":
   120|        resolve_instrument_alerts(conn, instrument_id=instrument_id, rule_ids=["delisted_or_suspended"])
   121|    if values.get("corporate_action_status") in {"none_known", "adjusted", "ignored"}:
   122|        resolve_instrument_alerts(conn, instrument_id=instrument_id, rule_ids=["corporate_action_suspected", "split_or_corporate_action_review_required"])
   123|    record_audit_event(
   124|        conn,
   125|        source="instrument_metadata",
   126|        action="update_instrument_metadata",
   127|        entity_type="instrument",
   128|        entity_id=instrument_id,
   129|        old_values={k: old.get(k) for k in values if k != "updated_at"},
   130|        new_values={k: values[k] for k in values if k != "updated_at"},
   131|        user_text_note=note,
   132|        confirmed=True,
   133|        created_by=created_by,
   134|    )
   135|    conn.commit()
   136|


Explanation:
**Considering status alerts**

I need to add an unknown status alert. I’m pondering whether to resolve this on an active status. It seems like something I should address promptly, but I want to be sure I'm handling it correctly. I could benefit from double-checking if there's a specific process to follow for this. It feels important to keep things clear and organized, especially if there are uncertainties involved. Let’s see how I can best move forward with this!

    if instrument_status == "unknown":
        result.quality_flags.append("instrument_status_unknown")
        create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="instrument_status_unknown", message="Instrument status is unknown and must be reviewed before automatic valuation confidence.", evidence={"isin": inst["isin"]}, fingerprint="instrument_status_unknown")
    elif instrument_status in {"delisted", "suspended", "merged", "inactive"}:


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/instruments.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/instruments.py
@@ -50,7 +50,10 @@
         create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="hedge_status_unknown", message="Currency hedge status is unknown and must be reviewed before precise valuation attribution.", evidence={"isin": inst["isin"]}, fingerprint="hedge_status_unknown")
     else:
         resolve_instrument_alerts(conn, instrument_id=instrument_id, rule_ids=["hedge_status_unknown"])
-    if instrument_status in {"delisted", "suspended", "merged", "inactive"}:
+    if instrument_status == "unknown":
+        result.quality_flags.append("instrument_status_unknown")
+        create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="instrument_status_unknown", message="Instrument status is unknown and must be reviewed before automatic valuation confidence.", evidence={"isin": inst["isin"]}, fingerprint="instrument_status_unknown")
+    elif instrument_status in {"delisted", "suspended", "merged", "inactive"}:
         result.quality_flags.append("delisted_or_suspended")
         create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="delisted_or_suspended", message="Instrument is not active; automatic price update must be excluded or manually valued.", evidence={"status": instrument_status}, fingerprint=f"delisted_or_suspended:{instrument_status}")
     if corporate_action_status in {"unknown", "not_checked"}:


    if values.get("instrument_status") and values["instrument_status"] != "unknown":
        resolve_instrument_alerts(conn, instrument_id=instrument_id, rule_ids=["instrument_status_unknown"])
    if values.get("instrument_status") == "active":


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/instruments.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/instruments.py
@@ -119,6 +119,8 @@
     conn.execute(f"UPDATE instruments SET {assignments} WHERE instrument_id=?", (*values.values(), instrument_id))
     if values.get("hedge_status") and values["hedge_status"] != "unknown":
         resolve_instrument_alerts(conn, instrument_id=instrument_id, rule_ids=["hedge_status_unknown"])
+    if values.get("instrument_status") and values["instrument_status"] != "unknown":
+        resolve_instrument_alerts(conn, instrument_id=instrument_id, rule_ids=["instrument_status_unknown"])
     if values.get("instrument_status") == "active":
         resolve_instrument_alerts(conn, instrument_id=instrument_id, rule_ids=["delisted_or_suspended"])
     if values.get("corporate_action_status") in {"none_known", "adjusted", "ignored"}:


Explanation:
**Evaluating missing function overrides**

I'm thinking about the need for a manual function override. The current setup resolves a missing function, which is great, but I’m getting an alert about needing to manually override it. It seems that the update function could create this requirement. I need to run some tests to confirm this. I should also check if there are existing helpers and consider a dashboard rendering test pattern while I search for more information.


Explanation:
**Planning test creation**

I'm looking to create tests with SimpleFakeSt, but first, I need to set up the database. I wonder if I can reuse the setup from previous tests or if I should write a new one instead. It's crucial to ensure that price dry runs don’t make provider calls when there are no mappings, so I’ll call refresh_market_prices with the provider raises and ensure the total is 0. Plus, I need to remember the schema version!

from __future__ import annotations

from decimal import Decimal

import pytest

from jarvis_finance.dashboard.pages import page_13_equity_manage
from jarvis_finance.fx.overrides import set_manual_fx_override
from jarvis_finance.market_data.candidates import (
    LookupUnavailable,
    ProviderCandidate,
    generate_mapping_candidates,
    select_mapping_candidate,
)
from jarvis_finance.market_data.instruments import ensure_instrument_metadata_quality
from jarvis_finance.market_data.prices import refresh_market_prices
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations

class StaticLookupProvider:
    name = "static"

    def __init__(self, candidates_by_isin=None, *, fail=False):
        self.candidates_by_isin = candidates_by_isin or {}
        self.fail = fail

    def lookup(self, *, isin, name, ticker, exchange, currency):
        if self.fail:
            raise LookupUnavailable("lookup down")
        return list(self.candidates_by_isin.get(isin, []))

class RaisingPriceProvider:
    name = "raising"

    def get_price(self, symbol: str):
        raise AssertionError("price provider should not be called without confirmed mappings")

class FakeTab:
    def __enter__(self):
        return self

    def __exit__(self, *args):
        return False

class FakeSt:
    def title(self, *args, **kwargs): pass
    def caption(self, *args, **kwargs): pass
    def warning(self, *args, **kwargs): pass
    def subheader(self, *args, **kwargs): pass
    def write(self, *args, **kwargs): pass
    def success(self, *args, **kwargs): pass
    def error(self, *args, **kwargs): pass
    def info(self, *args, **kwargs): pass
    def divider(self, *args, **kwargs): pass
    def dataframe(self, *args, **kwargs): pass
    def table(self, *args, **kwargs): pass
    def tabs(self, labels): return [FakeTab() for _ in labels]
    def selectbox(self, label, options, **kwargs): return options[0] if options else None
    def text_input(self, *args, **kwargs): return kwargs.get("value", "")
    def text_area(self, *args, **kwargs): return ""
    def date_input(self, *args, **kwargs): return "2025-12-31"
    def checkbox(self, *args, **kwargs): return False
    def expander(self, *args, **kwargs): return FakeTab()
    def button(self, *args, **kwargs): return False

def setup_conn():
    conn = connect_memory()
    apply_migrations(conn)
    conn.execute("INSERT INTO platforms(platform_id,name,platform_type,default_currency,created_at) VALUES('p1','True Wealth','broker','CHF','now')")
    conn.execute("INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,created_at) VALUES('a1','p1','TW','brokerage','CHF','now')")
    conn.execute("INSERT INTO instruments(instrument_id,asset_class,name,ticker,isin,exchange,currency,created_at) VALUES('inst1','ETF','Global Equity ETF','GETF','CH0000000001',NULL,'USD','now')")
    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','inst1','2025-12-31','1','USD','missing','broker_import_reviewed_snapshot',1,'warning','now')")
    return conn

def test_candidate_generator_creates_high_candidate_from_isin_match() -> None:
    conn = setup_conn()
    provider = StaticLookupProvider({
        'CH0000000001': [ProviderCandidate(provider='stooq', provider_symbol='GETF.SW', exchange='SWX', currency='USD', name='Global Equity ETF', isin='CH0000000001', instrument_status='active')]
    })
    result = generate_mapping_candidates(conn, provider=provider)
    row = conn.execute("SELECT * FROM instrument_price_mapping_candidates WHERE instrument_id='inst1'").fetchone()
    assert result.candidates_found == 1
    assert result.high_confidence == 1
    assert row['confidence'] == 'high'
    assert row['review_status'] == 'proposed'

def test_name_only_candidate_is_review_required_low_confidence() -> None:
    conn = setup_conn()
    provider = StaticLookupProvider({'CH0000000001': [ProviderCandidate(provider='manual', provider_symbol='UNKNOWN', name='Global Equity ETF')]})
    generate_mapping_candidates(conn, provider=provider)
    row = conn.execute("SELECT confidence,review_status FROM instrument_price_mapping_candidates").fetchone()
    assert row['confidence'] == 'low'
    assert row['review_status'] == 'needs_manual_review'

def test_multiple_candidates_are_manual_review_and_alerted() -> None:
    conn = setup_conn()
    provider = StaticLookupProvider({'CH0000000001': [
        ProviderCandidate(provider='stooq', provider_symbol='GETF.SW', exchange='SWX', isin='CH0000000001'),
        ProviderCandidate(provider='yahoo', provider_symbol='GETF.SW', exchange='SWX', isin='CH0000000001'),
    ]})
    result = generate_mapping_candidates(conn, provider=provider)
    statuses = {r['review_status'] for r in conn.execute("SELECT review_status FROM instrument_price_mapping_candidates").fetchall()}
    assert result.ambiguous_provider_mapping == 1
    assert statuses == {'needs_manual_review'}
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='ambiguous_provider_mapping' AND status='active'").fetchone()['c'] == 1

def test_no_candidate_keeps_missing_provider_symbol_active() -> None:
    conn = setup_conn()
    result = generate_mapping_candidates(conn, provider=StaticLookupProvider({}))
    assert result.instruments_without_candidate == 1
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_provider_symbol' AND status='active'").fetchone()['c'] == 1

def test_hedge_and_instrument_status_unknown_alerts() -> None:
    conn = setup_conn()
    ensure_instrument_metadata_quality(conn, instrument_id='inst1')
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='hedge_status_unknown' AND status='active'").fetchone()['c'] == 1
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='instrument_status_unknown' AND status='active'").fetchone()['c'] == 1

def test_chf_hedged_candidate_remains_proposed_until_confirmed() -> None:
    conn = setup_conn()
    provider = StaticLookupProvider({'CH0000000001': [ProviderCandidate(provider='stooq', provider_symbol='GETF.SW', exchange='SWX', currency='CHF', name='Global Equity ETF CHF Hedged', isin='CH0000000001', instrument_status='active')]})
    generate_mapping_candidates(conn, provider=provider)
    row = conn.execute("SELECT candidate_is_hedged,candidate_hedged_to_currency,review_status FROM instrument_price_mapping_candidates").fetchone()
    inst = conn.execute("SELECT hedge_status FROM instruments WHERE instrument_id='inst1'").fetchone()
    assert row['candidate_is_hedged'] == 1
    assert row['candidate_hedged_to_currency'] == 'CHF'
    assert row['review_status'] == 'proposed'
    assert inst['hedge_status'] == 'unknown'

def test_provider_lookup_error_does_not_crash() -> None:
    conn = setup_conn()
    result = generate_mapping_candidates(conn, provider=StaticLookupProvider(fail=True))
    assert result.provider_lookup_unavailable == 1
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='provider_lookup_unavailable' AND status='active'").fetchone()['c'] == 1

def test_mapping_selection_writes_audit_and_resolves_missing_provider_symbol() -> None:
    conn = setup_conn()
    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('mps','warnung','market_data','instrument','inst1','missing_provider_symbol','missing','active','now')")
    provider = StaticLookupProvider({'CH0000000001': [ProviderCandidate(provider='stooq', provider_symbol='GETF.SW', exchange='SWX', currency='USD', isin='CH0000000001', instrument_status='active')]})
    generate_mapping_candidates(conn, provider=provider)
    candidate_id = conn.execute("SELECT candidate_id FROM instrument_price_mapping_candidates").fetchone()['candidate_id']
    mapping_id = select_mapping_candidate(conn, candidate_id=candidate_id, note='manual confirmation')
    assert mapping_id
    assert conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action IN ('confirm_instrument_price_mapping','select_mapping_candidate')").fetchone()['c'] >= 2
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_provider_symbol' AND status='active'").fetchone()['c'] == 0

def test_fx_override_requires_note_and_resolves_missing_fx() -> None:
    conn = setup_conn()
    conn.execute("INSERT INTO alerts(alert_id,priority,category,entity_type,entity_id,rule_id,message,status,created_at) VALUES('fx1','kritisch','fx','instrument','inst1','missing_fx','missing','active','now')")
    with pytest.raises(ValueError):
        set_manual_fx_override(conn, base_currency='USD', quote_currency='CHF', rate_date='2025-12-31', rate=Decimal('0.9'), note='')
    set_manual_fx_override(conn, base_currency='USD', quote_currency='CHF', rate_date='2025-12-31', rate=Decimal('0.9'), note='statement source')
    assert conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action='manual_fx_override'").fetchone()['c'] == 1
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_fx' AND status='active'").fetchone()['c'] == 0

def test_market_price_dry_run_without_confirmed_mapping_does_not_call_provider() -> None:
    conn = setup_conn()
    result = refresh_market_prices(conn, provider=RaisingPriceProvider(), asset_class='etf', limit=3, dry_run=True)
    assert result.total_mappings == 0
    assert result.updated_count == 0

def test_dashboard_review_ui_renders_without_live_api() -> None:
    conn = setup_conn()
    page_13_equity_manage.render(FakeSt(), conn)


--- 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) == 10
+    assert get_schema_version(conn) == 11

 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_132829_5f4002' but this agent never read it. Read the file before writing to avoid overwriting the sibling's changes.


terminal

PYTHONPATH=src pytest -q tests/unit/test_true_wealth_mapping_candidates_c3.py tests/unit/test_true_wealth_valuation_pilot.py tests/unit/test_schema.py


Output:
........................                                                 [100%]
24 passed in 0.49s


Explanation:
**Updating task list**

I need to update my to-do list regarding tests later. It seems like I need to generate candidates at runtime, but I'm running into a problem with the lookup provider being empty, which means no candidates and possibly missing alerts. User mentioned wanting public sources if possible. I’m considering implementing a simple Stooq provider; however, it might require a local manual workaround without an API key. I'll think about adding a CLI command for generating mapping candidates.

patch

from jarvis_finance.market_data.candidates import EmptyLookupProvider, generate_mapping_candidates
from jarvis_finance.market_data.prices import MockEquityPriceProvider, refresh_market_prices


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/cli/main.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/cli/main.py
@@ -8,6 +8,7 @@
 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.candidates import EmptyLookupProvider, generate_mapping_candidates
 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

/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/cli/main.py was modified by sibling subagent '20260515_125616_c4705b' but this agent never read it. Read the file before writing to avoid overwriting the sibling's changes.


    market_prices.add_argument("--mock-price", action="append", default=[], help="Test-only SYMBOL=PRICE")
    candidates = sub.add_parser("generate-mapping-candidates")
    candidates.add_argument("--source", default="true_wealth")
    candidates.add_argument("--limit", type=int)
    candidates.add_argument("--dry-run", action="store_true")


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/cli/main.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/cli/main.py
@@ -60,6 +60,10 @@
     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")
+    candidates = sub.add_parser("generate-mapping-candidates")
+    candidates.add_argument("--source", default="true_wealth")
+    candidates.add_argument("--limit", type=int)
+    candidates.add_argument("--dry-run", action="store_true")
     sub.add_parser("backup-runtime-db")
     restore = sub.add_parser("restore-runtime-db")
     restore.add_argument("--from", dest="backup_file", required=True)


    if args.command == "generate-mapping-candidates":
        settings = load_settings()
        ensure_runtime_dirs(settings)
        conn = connect(settings.db_path)
        try:
            apply_migrations(conn)
            result = generate_mapping_candidates(conn, provider=EmptyLookupProvider(), source=args.source, limit=args.limit, dry_run=args.dry_run)
            print(
                "MAPPING_CANDIDATES "
                f"instruments_total={result.instruments_total} candidates_found={result.candidates_found} "
                f"high={result.high_confidence} medium={result.medium_confidence} low={result.low_confidence} "
                f"instruments_without_candidate={result.instruments_without_candidate} "
                f"hedge_unknown={result.instruments_with_hedge_unknown} instrument_status_unknown={result.instruments_with_status_unknown} "
                f"provider_lookup_unavailable={result.provider_lookup_unavailable} ambiguous={result.ambiguous_provider_mapping} "
                f"dry_run={str(result.dry_run).lower()}"
            )
            return 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
@@ -188,6 +188,25 @@
             return 1 if result.error_count else 0
         finally:
             conn.close()
+    if args.command == "generate-mapping-candidates":
+        settings = load_settings()
+        ensure_runtime_dirs(settings)
+        conn = connect(settings.db_path)
+        try:
+            apply_migrations(conn)
+            result = generate_mapping_candidates(conn, provider=EmptyLookupProvider(), source=args.source, limit=args.limit, dry_run=args.dry_run)
+            print(
+                "MAPPING_CANDIDATES "
+                f"instruments_total={result.instruments_total} candidates_found={result.candidates_found} "
+                f"high={result.high_confidence} medium={result.medium_confidence} low={result.low_confidence} "
+                f"instruments_without_candidate={result.instruments_without_candidate} "
+                f"hedge_unknown={result.instruments_with_hedge_unknown} instrument_status_unknown={result.instruments_with_status_unknown} "
+                f"provider_lookup_unavailable={result.provider_lookup_unavailable} ambiguous={result.ambiguous_provider_mapping} "
+                f"dry_run={str(result.dry_run).lower()}"
+            )
+            return 0
+        finally:
+            conn.close()
     if args.command == "check-data-quality":
         settings = load_settings()
         ensure_runtime_dirs(settings)


terminal

Output:
........................................................................ [ 38%]
........................................................................ [ 76%]
.............................................                            [100%]
189 passed in 11.26s


Explanation:
**Considering runtime generation**

I need to generate a runtime before updating the todo list. I wonder if the CLI needs to run without a dry-run? Candidate creation with Empty only creates missing alerts, but it seems like it only allows candidate creation and not final items. It says, "Kandidaten erzeugen falls möglich," which means if there's no lookup, there are no candidates. Maybe I should use the productive option rather than dry-run, as it might write missing alerts. I should also consider the market price during a dry run. After that, I'll need to aggregate.

todo

Phase C3 Baseline/Git-Safety und bestehende C2-Struktur prüfen

Mapping-Candidate-Datenmodell, Services und Lookup-Fallbacks implementieren

Dashboard Review UI und FX Override Review vorbereiten

Data Quality Center um C3 Alerts/Links erweitern

Tests für Candidate Generator, Review-Auswahl, FX Override und Dashboard ergänzen

Produktiven vorsichtigen Runtime-Lauf aggregiert ausführen


Compile, pytest, Git-Safety, Commit und Push


terminal

PYTHONPATH=src python -m jarvis_finance.cli.main generate-mapping-candidates --source true_wealth --limit 3

PYTHONPATH=src python -m jarvis_finance.cli.main update-market-prices --asset-class etf --limit 3 --dry-run


Output: MAPPING_CANDIDATES instruments_total=3 candidates_found=0 high=0 medium=0 low=0 instruments_without_candidate=3 hedge_unknown=3 instrument_status_unknown=3 provider_lookup_unavailable=0 ambiguous=0 dry_run=false


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


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
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()]
cands=conn.execute("SELECT confidence, COUNT(*) AS c FROM instrument_price_mapping_candidates GROUP BY confidence").fetchall()
conf=Counter({r['confidence']: r['c'] for r in cands})
without=0
for inst in ids:
    if not conn.execute("SELECT 1 FROM instrument_price_mapping_candidates WHERE instrument_id=? LIMIT 1", (inst,)).fetchone():
        without+=1
hedge=Counter(conn.execute("SELECT hedge_status FROM instruments WHERE instrument_id=?", (inst,)).fetchone()['hedge_status'] for inst in ids)
status=Counter(conn.execute("SELECT instrument_status FROM instruments WHERE instrument_id=?", (inst,)).fetchone()['instrument_status'] for inst in ids)
alerts=Counter({r['rule_id']: r['c'] for r in conn.execute("SELECT rule_id, COUNT(*) AS c FROM alerts WHERE status='active' GROUP BY rule_id").fetchall()})
fx=Counter({(r['currency_original'], r['trade_date']): r['c'] for r in conn.execute("SELECT currency_original, trade_date, COUNT(*) AS c FROM transactions WHERE transaction_type='initial_position_snapshot' AND source_type='broker_import_reviewed_snapshot' AND COALESCE(is_voided,0)=0 AND currency_original IS NOT NULL AND currency_original<>'CHF' GROUP BY currency_original, trade_date").fetchall()})
print('INSTRUMENTS_TOTAL', len(ids))
print('CANDIDATES_TOTAL', sum(conf.values()))
print('CANDIDATES_CONF', ' '.join(f'{k}={v}' for k,v in sorted(conf.items())) or 'none')
print('WITHOUT_CANDIDATE', without)
print('HEDGE', ' '.join(f'{k}={v}' for k,v in sorted(hedge.items())))
print('INSTRUMENT_STATUS', ' '.join(f'{k}={v}' for k,v in sorted(status.items())))
print('FX_PAIRS_REQUIRED', len(fx))
print('ALERTS', ' '.join(f'{k}={v}' for k,v in sorted(alerts.items())))
conn.close()
PY


Output:
INSTRUMENTS_TOTAL 3
CANDIDATES_TOTAL 0
CANDIDATES_CONF none
WITHOUT_CANDIDATE 3
HEDGE unknown=3
INSTRUMENT_STATUS unknown=3
FX_PAIRS_REQUIRED 1
ALERTS cost_basis_uncertain=3 hedge_status_unknown=3 instrument_status_unknown=3 missing_fx=3 missing_market_price=3 missing_provider_symbol=6 snapshot_only=3


patch

create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="missing_provider_symbol", message="Instrument has no provider symbol for local market-price updates.", evidence={"isin": inst["isin"], "ticker": inst["ticker"], "provider": provider}, fingerprint="missing_provider_symbol")

create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="missing_provider_symbol", message="Instrument has no provider symbol for local market-price updates.", evidence={"isin": inst["isin"], "ticker": inst["ticker"], "provider": provider}, fingerprint=f"missing_provider_symbol:{provider}")


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/mappings.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/mappings.py
@@ -118,7 +118,7 @@
     if not inst["provider_symbol"]:
         status = "missing_provider_symbol"
         flags.append("missing_provider_symbol")
-        create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="missing_provider_symbol", message="Instrument has no provider symbol for local market-price updates.", evidence={"isin": inst["isin"], "ticker": inst["ticker"], "provider": provider}, fingerprint=f"missing_provider_symbol:{provider}")
+        create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="missing_provider_symbol", message="Instrument has no provider symbol for local market-price updates.", evidence={"isin": inst["isin"], "ticker": inst["ticker"], "provider": provider}, fingerprint="missing_provider_symbol")
     if inst["ticker"] and not inst["exchange"]:
         flags.append("ticker_without_exchange")
         create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="ambiguous_instrument_mapping", message="Instrument ticker has no exchange; market-data mapping needs manual review.", evidence={"isin": inst["isin"], "ticker": inst["ticker"], "provider": provider}, fingerprint=f"ambiguous_instrument_mapping:{provider}")


create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="missing_provider_symbol", message="No provider-symbol candidate found; manual mapping required.", evidence={"source": source, "provider": getattr(lookup, "name", "manual")}, fingerprint="missing_provider_symbol")

create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="missing_provider_symbol", message="No provider-symbol candidate found; manual mapping required.", evidence={"source": source, "provider": getattr(lookup, "name", "manual")}, fingerprint=f"missing_provider_symbol:{getattr(lookup, 'name', 'manual')}")


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/candidates.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/candidates.py
@@ -185,7 +185,7 @@
         if not candidates:
             result.instruments_without_candidate += 1
             if not dry_run:
-                create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="missing_provider_symbol", message="No provider-symbol candidate found; manual mapping required.", evidence={"source": source, "provider": getattr(lookup, "name", "manual")}, fingerprint=f"missing_provider_symbol:{getattr(lookup, 'name', 'manual')}")
+                create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="missing_provider_symbol", message="No provider-symbol candidate found; manual mapping required.", evidence={"source": source, "provider": getattr(lookup, "name", "manual")}, fingerprint="missing_provider_symbol")
             continue
         if len(candidates) > 1:
             result.ambiguous_provider_mapping += 1


Explanation:
**Optimizing runtime with scripts**

I'm thinking about how to deduplicate runtime using a script. It seems like using a new status called "instrument_status_unknown" might be acceptable. I’ll focus on resolving duplicates based on entity, rule, and priority. That way, I can streamline things and improve overall efficiency. I want to ensure I approach this methodically to avoid any issues. Let's see how well the script performs with these adjustments!

terminal

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.imports.common import utc_now
s=load_settings(); conn=connect(s.db_path); apply_migrations(conn)
resolved=0
rows=conn.execute("SELECT entity_type, entity_id, rule_id, priority, COUNT(*) AS c FROM alerts WHERE status='active' GROUP BY entity_type, entity_id, rule_id, priority HAVING c>1").fetchall()
for g in rows:
    alerts=conn.execute("SELECT alert_id FROM alerts WHERE status='active' AND entity_type IS ? AND entity_id IS ? AND rule_id IS ? AND priority=? ORDER BY created_at ASC", (g['entity_type'], g['entity_id'], g['rule_id'], g['priority'])).fetchall()
    for row in alerts[1:]:
        conn.execute("UPDATE alerts SET status='resolved', resolved_at=?, last_seen_at=? WHERE alert_id=?", (utc_now(), utc_now(), row['alert_id']))
        resolved += 1
conn.commit(); conn.close()
print(f'ALERT_DEDUPE_RESOLVED {resolved}')
PY


Output: ALERT_DEDUPE_RESOLVED 3


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
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()]
conf=Counter({r['confidence']: r['c'] for r in conn.execute("SELECT confidence, COUNT(*) AS c FROM instrument_price_mapping_candidates GROUP BY confidence").fetchall()})
without=sum(1 for inst in ids if not conn.execute("SELECT 1 FROM instrument_price_mapping_candidates WHERE instrument_id=? LIMIT 1", (inst,)).fetchone())
hedge=Counter(conn.execute("SELECT hedge_status FROM instruments WHERE instrument_id=?", (inst,)).fetchone()['hedge_status'] for inst in ids)
status=Counter(conn.execute("SELECT instrument_status FROM instruments WHERE instrument_id=?", (inst,)).fetchone()['instrument_status'] for inst in ids)
alerts=Counter({r['rule_id']: r['c'] for r in conn.execute("SELECT rule_id, COUNT(*) AS c FROM alerts WHERE status='active' GROUP BY rule_id").fetchall()})
fx=Counter({(r['currency_original'], r['trade_date']): r['c'] for r in conn.execute("SELECT currency_original, trade_date, COUNT(*) AS c FROM transactions WHERE transaction_type='initial_position_snapshot' AND source_type='broker_import_reviewed_snapshot' AND COALESCE(is_voided,0)=0 AND currency_original IS NOT NULL AND currency_original<>'CHF' GROUP BY currency_original, trade_date").fetchall()})
print('INSTRUMENTS_TOTAL', len(ids))
print('CANDIDATES_TOTAL', sum(conf.values()))
print('CANDIDATES_CONF', ' '.join(f'{k}={v}' for k,v in sorted(conf.items())) or 'none')
print('WITHOUT_CANDIDATE', without)
print('HEDGE', ' '.join(f'{k}={v}' for k,v in sorted(hedge.items())))
print('INSTRUMENT_STATUS', ' '.join(f'{k}={v}' for k,v in sorted(status.items())))
print('FX_PAIRS_REQUIRED', len(fx))
print('ALERTS', ' '.join(f'{k}={v}' for k,v in sorted(alerts.items())))
conn.close()
PY


Output:
INSTRUMENTS_TOTAL 3
CANDIDATES_TOTAL 0
CANDIDATES_CONF none
WITHOUT_CANDIDATE 3
HEDGE unknown=3
INSTRUMENT_STATUS unknown=3
FX_PAIRS_REQUIRED 1
ALERTS cost_basis_uncertain=3 hedge_status_unknown=3 instrument_status_unknown=3 missing_fx=3 missing_market_price=3 missing_provider_symbol=3 snapshot_only=3


Explanation:
**Running tests and checks**

I need to address that the runtime generated no candidates due to an empty provider situation. This means I should run full checks to compile with pytest and ensure everything's safe. Although, I must remember to remove the cache after running pytest to avoid any issues. It seems like a bit of extra work, but getting this right is worth it for a smooth process!


PYTHONPATH=src python -m compileall -q src tests


Output:
........................................................................ [ 38%]
........................................................................ [ 76%]
.............................................                            [100%]
189 passed in 11.07s


rm -rf .pytest_cache && find . -type d -name __pycache__ -prune -exec rm -rf {} + && 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_13_equity_manage.py
 M src/jarvis_finance/dashboard/pages/page_16_data_quality_center.py
 M src/jarvis_finance/market_data/instruments.py
 M src/jarvis_finance/market_data/mappings.py
 M src/jarvis_finance/storage/migrations.py
 M tests/unit/test_schema.py
?? src/jarvis_finance/market_data/candidates.py
?? tests/unit/test_true_wealth_mapping_candidates_c3.py