     1|from __future__ import annotations
     2|
     3|from pathlib import Path
     4|from sqlite3 import Connection
     5|
     6|from .common import ImportResult, clean, create_import_session, require_text, source_hash, stable_id, utc_now
     7|from .dry_run import read_csv_rows
     8|
     9|REQUIRED_COLUMNS = {"asset_class", "name", "currency"}
    10|
    11|
    12|def _instrument_key(row: dict[str, str]) -> tuple[str, str, str]:
    13|    isin = clean(row.get("isin")).upper()
    14|    ticker = clean(row.get("ticker")).upper()
    15|    name = clean(row.get("name"))
    16|    return isin, ticker, name
    17|
    18|
    19|def _existing_instrument(conn: Connection, row: dict[str, str]) -> bool:
    20|    isin, ticker, name = _instrument_key(row)
    21|    if isin:
    22|        existing = conn.execute("SELECT 1 FROM instruments WHERE isin = ? LIMIT 1", (isin,)).fetchone()
    23|        if existing:
    24|            return True
    25|    if ticker:
    26|        existing = conn.execute("SELECT 1 FROM instruments WHERE ticker = ? AND name = ? LIMIT 1", (ticker, name)).fetchone()
    27|        if existing:
    28|            return True
    29|    return False
    30|
    31|
    32|def import_instruments_csv(
    33|    conn: Connection,
    34|    path: str | Path,
    35|    *,
    36|    commit: bool,
    37|    source_filename: str | None = None,
    38|) -> ImportResult:
    39|    rows, errors = read_csv_rows(path, REQUIRED_COLUMNS)
    40|    source_filename = source_filename or Path(path).name
    41|    rows_new = rows_existing = 0
    42|    validated: list[dict[str, str]] = []
    43|    for line, row in enumerate(rows, start=2):
    44|        try:
    45|            asset_class = require_text(row, "asset_class", line)
    46|            name = require_text(row, "name", line)
    47|            currency = require_text(row, "currency", line).upper()
    48|            isin, ticker, _ = _instrument_key(row)
    49|            instrument_id = stable_id("instrument", isin or ticker or name, currency)
    50|            if _existing_instrument(conn, {**row, "name": name, "ticker": ticker, "isin": isin}):
    51|                rows_existing += 1
    52|            else:
    53|                rows_new += 1
    54|            validated.append({
    55|                "instrument_id": instrument_id,
    56|                "asset_class": asset_class,
    57|                "name": name,
    58|                "ticker": ticker,
    59|                "isin": isin,
    60|                "exchange": clean(row.get("exchange")),
    61|                "currency": currency,
    62|                "country": clean(row.get("country")),
    63|                "sector": clean(row.get("sector")),
    64|                "data_provider_primary": clean(row.get("data_provider_primary")),
    65|                "notes": clean(row.get("notes")),
    66|            })
    67|        except ValueError as exc:
    68|            errors.append(str(exc))
    69|    status = "failed" if errors else ("committed" if commit else "dry_run_ok")
    70|    if commit and not errors:
    71|        now = utc_now()
    72|        for item in validated:
    73|            conn.execute(
    74|                """
    75|                INSERT OR IGNORE INTO instruments(
    76|                    instrument_id, asset_class, name, ticker, isin, exchange, currency,
    77|                    country, sector, data_provider_primary, notes, created_at
    78|                ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    79|                """,
    80|                (
    81|                    item["instrument_id"], item["asset_class"], item["name"], item["ticker"], item["isin"],
    82|                    item["exchange"], item["currency"], item["country"], item["sector"],
    83|                    item["data_provider_primary"], item["notes"], now,
    84|                ),
    85|            )
    86|    session_id = create_import_session(
    87|        conn,
    88|        import_type="instruments",
    89|        source_filename=source_filename,
    90|        file_hash=source_hash(path),
    91|        status=status,
    92|        rows_total=len(rows),
    93|        rows_imported=rows_new if commit and not errors else 0,
    94|        rows_failed=len(errors),
    95|        errors=errors,
    96|        notes="dry run" if not commit else None,
    97|    )
    98|    return ImportResult(session_id, "instruments", status, len(rows), rows_new if not errors else 0, rows_existing, len(errors), errors)
    99|


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


Explanation:
**Organizing equity management backend**

I need to manage the equity backend with transactions, instruments, accounts, and platforms. It seems I should create some platforms and accounts; maybe run some tests too. I'm thinking about schema columns for TER, and it looks like the instruments might be missing ETF fields or categories. I could potentially add optional columns through migrations. Also, I want to simplify by possibly refining the notes metadata based on what the user said about preparing fields. I need tests to assert those columns and incorporate TDD—I'll write a test file anticipating a missing module failure and use write_file. Overall, it’ll help us build something manageable while ensuring I know the audit log setup.


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


from __future__ import annotations

from decimal import Decimal

import pytest

from jarvis_finance.equity.manage import (
    add_equity_initial_snapshot,
    add_equity_transaction,
    get_equity_management_options,
)
from jarvis_finance.ledger.positions import calculate_positions
from jarvis_finance.storage.database import connect_memory
from jarvis_finance.storage.migrations import apply_migrations

def setup_conn():
    conn = connect_memory()
    apply_migrations(conn)
    return conn

def seed_account(conn, platform_name: str = "Synthetic PostFinance") -> str:
    platform_id = f"platform-{platform_name.lower().replace(' ', '-')}"
    account_id = f"account-{platform_name.lower().replace(' ', '-')}"
    conn.execute(
        "INSERT INTO platforms(platform_id, name, platform_type, country, default_currency, created_at) VALUES (?, ?, 'broker', 'CH', 'CHF', '2026-01-01')",
        (platform_id, platform_name),
    )
    conn.execute(
        "INSERT INTO accounts(account_id, platform_id, account_name, account_type, currency, created_at) VALUES (?, ?, 'Synthetic Depot', 'brokerage', 'CHF', '2026-01-01')",
        (account_id, platform_id),
    )
    conn.commit()
    return account_id

def audit_count(conn, action: str) -> int:
    return conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action=?", (action,)).fetchone()["c"]

def test_add_stock_initial_snapshot_creates_instrument_transaction_and_audit() -> None:
    conn = setup_conn()
    account_id = seed_account(conn)

    result = add_equity_initial_snapshot(
        conn,
        account_id=account_id,
        asset_class="stock",
        name="Synthetic Nestle",
        isin="CH0038863350",
        ticker="NESN",
        exchange="SIX",
        currency="CHF",
        quantity_text="3.5",
        cost_basis_original_text="350.00",
        snapshot_date="2025-12-31",
        category="Core",
        note="synthetic incomplete history accepted",
        confirm=True,
    )

    tx = conn.execute("SELECT * FROM transactions WHERE transaction_id=?", (result.transaction_id,)).fetchone()
    instrument = conn.execute("SELECT * FROM instruments WHERE instrument_id=?", (result.instrument_id,)).fetchone()
    assert tx["transaction_type"] == "initial_position_snapshot"
    assert tx["quantity"] == "3.5"
    assert tx["gross_amount_original"] == "350.00"
    assert tx["fx_rate_to_chf"] == "1"
    assert tx["fx_status"] == "ok"
    assert instrument["isin"] == "CH0038863350"
    assert instrument["asset_class"] == "stock"
    assert instrument["position_category"] == "Core"
    assert audit_count(conn, "equity_initial_position_snapshot") == 1
    assert result.warnings == []

def test_add_etf_initial_snapshot_prepares_optional_etf_fields() -> None:
    conn = setup_conn()
    account_id = seed_account(conn, "Synthetic True Wealth")

    result = add_equity_initial_snapshot(
        conn,
        account_id=account_id,
        asset_class="ETF",
        name="Synthetic World ETF",
        isin="IE00B4L5Y983",
        ticker="SWLD",
        exchange="LSE",
        currency="USD",
        quantity_text="10",
        cost_basis_original_text="1000.00",
        snapshot_date="2025-12-31",
        category="Core",
        note="synthetic ETF snapshot with known FX",
        fx_rate_to_chf_text="0.91",
        fx_source="synthetic fixture",
        ter_text="0.20",
        distribution_policy="accumulating",
        index_name="MSCI World Synthetic",
        fund_domicile="IE",
        benchmark="Synthetic Benchmark",
        confirm=True,
    )

    instrument = conn.execute("SELECT * FROM instruments WHERE instrument_id=?", (result.instrument_id,)).fetchone()
    tx = conn.execute("SELECT * FROM transactions WHERE transaction_id=?", (result.transaction_id,)).fetchone()
    assert instrument["ter"] == "0.20"
    assert instrument["distribution_policy"] == "accumulating"
    assert instrument["index_name"] == "MSCI World Synthetic"
    assert instrument["fund_domicile"] == "IE"
    assert instrument["benchmark"] == "Synthetic Benchmark"
    assert tx["currency_original"] == "USD"
    assert tx["fx_rate_to_chf"] == "0.91"
    assert tx["amount_chf"] == "910.0000"

def test_usd_stock_requires_fx_or_explicit_missing_status_alert() -> None:
    conn = setup_conn()
    account_id = seed_account(conn, "Synthetic Raiffeisen")

    result = add_equity_initial_snapshot(
        conn,
        account_id=account_id,
        asset_class="stock",
        name="Synthetic US Stock",
        isin="US0378331005",
        ticker="AAPL",
        exchange="NASDAQ",
        currency="USD",
        quantity_text="2",
        cost_basis_original_text="400.00",
        snapshot_date="2025-12-31",
        category="Opportunity",
        note="synthetic missing FX accepted for review",
        fx_status="missing",
        confirm=True,
    )

    tx = conn.execute("SELECT * FROM transactions WHERE transaction_id=?", (result.transaction_id,)).fetchone()
    assert tx["fx_status"] == "missing"
    assert tx["amount_chf"] is None
    assert "missing_fx" in result.warnings
    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_fx'").fetchone()["c"] == 1

def test_sell_reduces_position_and_blocks_negative() -> None:
    conn = setup_conn()
    account_id = seed_account(conn)
    snap = add_equity_initial_snapshot(
        conn,
        account_id=account_id,
        asset_class="stock",
        name="Synthetic Sellable",
        isin="CH0000000001",
        ticker="SYN",
        exchange="SIX",
        currency="CHF",
        quantity_text="5",
        cost_basis_original_text="500",
        snapshot_date="2025-12-31",
        category="Unknown",
        note="synthetic initial",
        confirm=True,
    )

    add_equity_transaction(
        conn,
        account_id=account_id,
        instrument_id=snap.instrument_id,
        transaction_type="partial_sell",
        trade_date="2026-01-10",
        quantity_text="2",
        gross_amount_original_text="220",
        currency="CHF",
        note="synthetic partial sell",
        confirm=True,
    )
    position = calculate_positions(conn).positions[(account_id, snap.instrument_id)]
    assert position.quantity == Decimal("3")

    with pytest.raises(ValueError, match="negative"):
        add_equity_transaction(
            conn,
            account_id=account_id,
            instrument_id=snap.instrument_id,
            transaction_type="partial_sell",
            trade_date="2026-01-11",
            quantity_text="10",
            gross_amount_original_text="1000",
            currency="CHF",
            note="synthetic oversell",
            confirm=True,
        )

def test_dividend_does_not_change_quantity_and_manual_adjustment_requires_note() -> None:
    conn = setup_conn()
    account_id = seed_account(conn)
    snap = add_equity_initial_snapshot(
        conn,
        account_id=account_id,
        asset_class="ETF",
        name="Synthetic Income ETF",
        isin="IE0000000001",
        ticker="INC",
        exchange="SIX",
        currency="CHF",
        quantity_text="7",
        cost_basis_original_text="700",
        snapshot_date="2025-12-31",
        category="Core",
        note="synthetic initial",
        confirm=True,
    )

    add_equity_transaction(
        conn,
        account_id=account_id,
        instrument_id=snap.instrument_id,
        transaction_type="dividend",
        trade_date="2026-02-01",
        quantity_text="0",
        gross_amount_original_text="12.50",
        currency="CHF",
        note="synthetic dividend",
        confirm=True,
    )
    assert calculate_positions(conn).positions[(account_id, snap.instrument_id)].quantity == Decimal("7")

    with pytest.raises(ValueError, match="note"):
        add_equity_transaction(
            conn,
            account_id=account_id,
            instrument_id=snap.instrument_id,
            transaction_type="manual_adjustment",
            trade_date="2026-02-02",
            quantity_text="1",
            gross_amount_original_text="0",
            currency="CHF",
            note="",
            confirm=True,
        )

def test_equity_management_options_and_dashboard_page_import() -> None:
    conn = setup_conn()
    seed_account(conn, "Synthetic PostFinance")
    options = get_equity_management_options(conn)
    assert options["accounts"]
    from jarvis_finance.dashboard.pages import PAGES

    assert "Equity/ETF Manage" in [page.PAGE_TITLE for page in PAGES]


terminal

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


Output:
==================================== ERRORS ====================================
______________ ERROR collecting tests/unit/test_equity_manage.py _______________
ImportError while importing test module '/home/agent/.hermes/repos/FinanceManager/tests/unit/test_equity_manage.py'.
Hint: make sure your test modules/packages have valid Python names.
Traceback:
../../../.local/share/uv/python/cpython-3.11.15-linux-x86_64-gnu/lib/python3.11/importlib/__init__.py:126: in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
tests/unit/test_equity_manage.py:7: in <module>
    from jarvis_finance.equity.manage import (
E   ModuleNotFoundError: No module named 'jarvis_finance.equity'
=========================== short test summary info ============================
ERROR tests/unit/test_equity_manage.py
!!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.09s


todo

BTC mit allen Wallet-Holdings aus echter Excel kontrolliert importieren und verifizieren

Equity/ETF Manage UI Tests zuerst schreiben und RED verifizieren

Equity/ETF Manage Backend/UI implementieren


Compile, Tests, Git-Safety, Commit und Push durchführen

Aggregierten Bericht ohne echte Werte/Secrets liefern


     1|from __future__ import annotations
     2|
     3|from . import page_00_command_center
     4|from . import page_01_portfolio
     5|from . import page_02_platforms
     6|from . import page_03_equities_etfs
     7|from . import page_04_crypto
     8|from . import page_05_wallets
     9|from . import page_06_ledger
    10|from . import page_07_watchlist
    11|from . import page_08_reports
    12|from . import page_09_alerts
    13|from . import page_10_audit
    14|from . import page_11_settings_data_quality
    15|from . import page_12_crypto_manage
    16|
    17|PAGES = [
    18|    page_00_command_center,
    19|    page_01_portfolio,
    20|    page_02_platforms,
    21|    page_03_equities_etfs,
    22|    page_04_crypto,
    23|    page_05_wallets,
    24|    page_06_ledger,
    25|    page_07_watchlist,
    26|    page_08_reports,
    27|    page_09_alerts,
    28|    page_10_audit,
    29|    page_11_settings_data_quality,
    30|    page_12_crypto_manage,
    31|]
    32|


Explanation:
**Planning database migrations**

I see I need to focus on adding columns to the instruments and rebuilding the old crypto runtime. It looks like editing the schema string is going to be tricky; I'll likely need to use Python's replace function for that. Also, I need to create equity files and make sure I know which transaction columns I need to track. It feels like there’s a bit to juggle here, but I'm on it!

terminal

python - <<'PY'
from pathlib import Path
s=Path('/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/storage/schema.py').read_text()
start=s.find('CREATE TABLE IF NOT EXISTS transactions')
end=s.find('CREATE TABLE IF NOT EXISTS positions_snapshot')
print(s[start:end].replace('\\n','\n')[:3000])
PY


Output:
CREATE TABLE IF NOT EXISTS transactions (
    transaction_id TEXT PRIMARY KEY,
    transaction_type TEXT NOT NULL,
    account_id TEXT NOT NULL REFERENCES accounts(account_id),
    instrument_id TEXT REFERENCES instruments(instrument_id),
    trade_date TEXT NOT NULL,
    settlement_date TEXT,
    quantity NUMERIC,
    price_original NUMERIC,
    gross_amount_original NUMERIC,
    fee_original NUMERIC DEFAULT 0,
    tax_original NUMERIC DEFAULT 0,
    net_amount_original NUMERIC,
    currency_original TEXT NOT NULL,
    fx_rate_to_chf NUMERIC,
    fx_source TEXT,
    fx_status TEXT NOT NULL DEFAULT 'ok',
    gross_amount_chf NUMERIC,
    fee_chf NUMERIC,
    tax_chf NUMERIC,
    net_amount_chf NUMERIC,
    source_type TEXT NOT NULL,
    source_id TEXT,
    external_transaction_id TEXT,
    row_hash TEXT,
    is_confirmed INTEGER NOT NULL DEFAULT 1,
    quality_status TEXT NOT NULL DEFAULT 'ok',
    notes TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT
);


Explanation:
**Adjusting currency tests**

I need to fix the tests because "amount_chf" isn't existing. The user mentioned "CHF Gegenwert," so I should be using "net_amount_chf" or perhaps "gross_amount_chf" instead. It seems like I overlooked this detail, and patching the test will help resolve the issue. Just double-checking those adjustments will ensure everything gets aligned correctly with what the user is expecting.

patch

    assert tx["currency_original"] == "USD"
    assert tx["fx_rate_to_chf"] == "0.91"
    assert tx["amount_chf"] == "910.0000"

    assert tx["currency_original"] == "USD"
    assert tx["fx_rate_to_chf"] == "0.91"
    assert tx["gross_amount_chf"] == "910.0000"


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_equity_manage.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_equity_manage.py
@@ -111,7 +111,7 @@
     assert instrument["benchmark"] == "Synthetic Benchmark"
     assert tx["currency_original"] == "USD"
     assert tx["fx_rate_to_chf"] == "0.91"
-    assert tx["amount_chf"] == "910.0000"
+    assert tx["gross_amount_chf"] == "910.0000"

 def test_usd_stock_requires_fx_or_explicit_missing_status_alert() -> None:


    assert tx["fx_status"] == "missing"
    assert tx["gross_amount_chf"] is None

    assert tx["fx_status"] == "missing"
    assert tx["amount_chf"] is None


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

     tx = conn.execute("SELECT * FROM transactions WHERE transaction_id=?", (result.transaction_id,)).fetchone()
     assert tx["fx_status"] == "missing"
-    assert tx["amount_chf"] is None
+    assert tx["gross_amount_chf"] is None
     assert "missing_fx" in result.warnings
     assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_fx'").fetchone()["c"] == 1


from __future__ import annotations


from __future__ import annotations

from dataclasses import dataclass, field
from decimal import Decimal, InvalidOperation
from sqlite3 import Connection

from jarvis_finance.audit.log import record_audit_event
from jarvis_finance.imports.common import stable_id, utc_now
from jarvis_finance.ledger.positions import calculate_positions
from jarvis_finance.quality.alerts import create_alert

VALID_ASSET_CLASSES = {"stock", "etf"}
VALID_CURRENCIES = {"CHF", "EUR", "USD"}
VALID_CATEGORIES = {"Core", "Opportunity", "Unknown"}
VALID_TRANSACTION_TYPES = {"buy", "partial_sell", "full_sell", "dividend", "etf_distribution", "fee", "manual_adjustment"}

@dataclass(frozen=True)
class EquityManageResult:
    instrument_id: str
    account_id: str
    transaction_id: str
    warnings: list[str] = field(default_factory=list)

def parse_decimal_text(value: str, *, field_name: str) -> Decimal:
    text = str(value).strip()
    if not text:
        raise ValueError(f"{field_name} is required")
    try:
        dec = Decimal(text)
    except (InvalidOperation, ValueError) as exc:
        raise ValueError(f"{field_name} must be a Decimal string") from exc
    if not dec.is_finite():
        raise ValueError(f"{field_name} must be finite")
    return dec

def decimal_text(value: Decimal | None, places: str | None = None) -> str | None:
    if value is None:
        return None
    if places is not None:
        value = value.quantize(Decimal(places))
    return format(value, "f")

def _require_confirm(confirm: bool) -> None:
    if not confirm:
        raise ValueError("explicit confirm is required before saving")

def _require_date(value: str) -> str:
    text = str(value).strip()
    parts = text.split("-")
    if len(parts) != 3 or not all(part.isdigit() for part in parts):
        raise ValueError("date must be ISO YYYY-MM-DD")
    return text

def _require_account(conn: Connection, account_id: str) -> str:
    account_id = str(account_id).strip()
    if not account_id:
        raise ValueError("account is required")
    row = conn.execute("SELECT account_id FROM accounts WHERE account_id=?", (account_id,)).fetchone()
    if row is None:
        raise ValueError("account not found")
    return account_id

def _require_instrument(conn: Connection, instrument_id: str) -> str:
    instrument_id = str(instrument_id).strip()
    row = conn.execute("SELECT instrument_id FROM instruments WHERE instrument_id=?", (instrument_id,)).fetchone()
    if row is None:
        raise ValueError("instrument not found")
    return instrument_id

def _normalize_asset_class(asset_class: str) -> str:
    value = str(asset_class).strip().lower()
    if value not in VALID_ASSET_CLASSES:
        raise ValueError("asset_class must be stock or ETF")
    return value

def _require_currency(currency: str) -> str:
    value = str(currency).strip().upper()
    if value not in VALID_CURRENCIES:
        raise ValueError("currency must be CHF, EUR or USD")
    return value

def _require_note(note: str, *, reason: str) -> str:
    text = str(note or "").strip()
    if not text:
        raise ValueError(f"note is required for {reason}")
    return text

def _find_instrument(conn: Connection, *, isin: str, ticker: str, name: str, currency: str) -> str | None:
    if isin:
        row = conn.execute("SELECT instrument_id FROM instruments WHERE isin=?", (isin,)).fetchone()
        if row:
            return row["instrument_id"]
    if ticker:
        row = conn.execute("SELECT instrument_id FROM instruments WHERE upper(ticker)=upper(?) AND currency=?", (ticker, currency)).fetchone()
        if row:
            return row["instrument_id"]
    row = conn.execute("SELECT instrument_id FROM instruments WHERE name=? AND currency=?", (name, currency)).fetchone()
    return row["instrument_id"] if row else None

def _ensure_instrument(
    conn: Connection,
    *,
    asset_class: str,
    name: str,
    isin: str,
    ticker: str,
    exchange: str,
    currency: str,
    category: str,
    note: str,
    ter_text: str | None = None,
    distribution_policy: str | None = None,
    index_name: str | None = None,
    fund_domicile: str | None = None,
    benchmark: str | None = None,
) -> str:
    isin = str(isin or "").strip().upper()
    ticker = str(ticker or "").strip().upper()
    name = str(name or "").strip()
    if not name:
        raise ValueError("name is required")
    if not isin and not ticker:
        raise ValueError("ISIN or ticker is required")
    if category not in VALID_CATEGORIES:
        raise ValueError("category must be Core, Opportunity or Unknown")
    ter = None
    if ter_text not in {None, ""}:
        ter = decimal_text(parse_decimal_text(str(ter_text), field_name="TER"))
    existing = _find_instrument(conn, isin=isin, ticker=ticker, name=name, currency=currency)
    now = utc_now()
    if existing:
        conn.execute(
            """
            UPDATE instruments
            SET name=COALESCE(NULLIF(?, ''), name), ticker=COALESCE(NULLIF(?, ''), ticker),
                exchange=COALESCE(NULLIF(?, ''), exchange), position_category=?, ter=COALESCE(?, ter),
                distribution_policy=COALESCE(NULLIF(?, ''), distribution_policy), index_name=COALESCE(NULLIF(?, ''), index_name),
                fund_domicile=COALESCE(NULLIF(?, ''), fund_domicile), benchmark=COALESCE(NULLIF(?, ''), benchmark),
                updated_at=?
            WHERE instrument_id=?
            """,
            (name, ticker, exchange, category, ter, distribution_policy or "", index_name or "", fund_domicile or "", benchmark or "", now, existing),
        )
        return existing
    instrument_id = stable_id("instrument", isin or ticker or name, currency)
    conn.execute(
        """
        INSERT INTO instruments(
            instrument_id, asset_class, name, ticker, isin, exchange, currency,
            notes, position_category, ter, distribution_policy, index_name, fund_domicile, benchmark, created_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
        """,
        (instrument_id, asset_class, name, ticker, isin, exchange, currency, note, category, ter, distribution_policy, index_name, fund_domicile, benchmark, now),
    )
    return instrument_id

def _fx_values(conn: Connection, *, currency: str, gross: Decimal, fee: Decimal, tax: Decimal, fx_rate_text: str | None, fx_source: str | None, fx_status: str, entity_hint: str) -> tuple[str, str | None, str | None, str | None, str | None, list[str]]:
    warnings: list[str] = []
    if currency == "CHF":
        fx = Decimal("1")
        status = "ok"
        source = fx_source or "CHF base currency"
    else:
        status = str(fx_status or "").strip().lower()
        if fx_rate_text:
            fx = parse_decimal_text(fx_rate_text, field_name="fx_rate_to_chf")
            if fx <= 0:
                raise ValueError("fx_rate_to_chf must be > 0")
            status = "ok"
            source = fx_source or "manual"
        elif status == "missing":
            fx = None
            source = fx_source or "missing"
            warnings.append("missing_fx")
            create_alert(
                conn,
                priority="kritisch",
                category="equity",
                entity_type="instrument",
                entity_id=entity_hint,
                rule_id="missing_fx",
                message="Historic FX is missing; CHF value is incomplete.",
                evidence={"currency": currency},
                fingerprint="missing_fx",
            )
        else:
            raise ValueError("FX rate is required for foreign currency unless fx_status=missing")
    if fx is None:
        return status or "missing", None, source, None, None, warnings
    gross_chf = gross * fx
    fee_chf = fee * fx
    tax_chf = tax * fx
    net_chf = gross_chf - fee_chf - tax_chf
    return status or "ok", decimal_text(fx), source, decimal_text(gross_chf, "0.0001"), decimal_text(net_chf, "0.0001"), warnings

def _insert_transaction(
    conn: Connection,
    *,
    transaction_type: str,
    account_id: str,
    instrument_id: str,
    trade_date: str,
    quantity: Decimal | None,
    gross: Decimal,
    fee: Decimal,
    tax: Decimal,
    currency: str,
    fx_rate_to_chf_text: str | None,
    fx_source: str | None,
    fx_status: str,
    note: str,
) -> tuple[str, list[str]]:
    now = utc_now()
    status, fx_text, fx_source_value, gross_chf, net_chf, warnings = _fx_values(
        conn,
        currency=currency,
        gross=gross,
        fee=fee,
        tax=tax,
        fx_rate_text=fx_rate_to_chf_text,
        fx_source=fx_source,
        fx_status=fx_status,
        entity_hint=instrument_id,
    )
    tx_id = stable_id("tx", transaction_type, account_id, instrument_id, trade_date, decimal_text(quantity) if quantity is not None else "", decimal_text(gross), now)
    conn.execute(
        """
        INSERT INTO transactions(
            transaction_id, transaction_type, account_id, instrument_id, trade_date,
            quantity, gross_amount_original, fee_original, tax_original, net_amount_original,
            currency_original, fx_rate_to_chf, fx_source, fx_status, gross_amount_chf,
            fee_chf, tax_chf, net_amount_chf, source_type, source_id, is_confirmed,
            quality_status, notes, created_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'manual_dashboard', ?, 1, ?, ?, ?)
        """,
        (
            tx_id,
            transaction_type,
            account_id,
            instrument_id,
            trade_date,
            decimal_text(quantity) if quantity is not None else None,
            decimal_text(gross),
            decimal_text(fee),
            decimal_text(tax),
            decimal_text(gross - fee - tax),
            currency,
            fx_text,
            fx_source_value,
            status,
            gross_chf,
            decimal_text(fee * Decimal(fx_text), "0.0001") if fx_text else None,
            decimal_text(tax * Decimal(fx_text), "0.0001") if fx_text else None,
            net_chf,
            tx_id,
            "incomplete" if warnings else "ok",
            note,
            now,
        ),
    )
    return tx_id, warnings

def add_equity_initial_snapshot(
    conn: Connection,
    *,
    account_id: str,
    asset_class: str,
    name: str,
    isin: str = "",
    ticker: str = "",
    exchange: str = "",
    currency: str,
    quantity_text: str,
    cost_basis_original_text: str,
    snapshot_date: str,
    category: str = "Unknown",
    note: str,
    confirm: bool,
    fx_rate_to_chf_text: str | None = None,
    fx_source: str | None = None,
    fx_status: str = "ok",
    ter_text: str | None = None,
    distribution_policy: str | None = None,
    index_name: str | None = None,
    fund_domicile: str | None = None,
    benchmark: str | None = None,
) -> EquityManageResult:
    _require_confirm(confirm)
    account_id = _require_account(conn, account_id)
    asset_class = _normalize_asset_class(asset_class)
    currency = _require_currency(currency)
    snapshot_date = _require_date(snapshot_date)
    note = _require_note(note, reason="initial_position_snapshot with incomplete history")
    qty = parse_decimal_text(quantity_text, field_name="quantity")
    if qty <= 0:
        raise ValueError("quantity must be > 0")
    gross = parse_decimal_text(cost_basis_original_text, field_name="cost basis")
    if gross < 0:
        raise ValueError("cost basis must be >= 0")
    instrument_id = _ensure_instrument(
        conn,
        asset_class=asset_class,
        name=name,
        isin=isin,
        ticker=ticker,
        exchange=exchange,
        currency=currency,
        category=category,
        note=note,
        ter_text=ter_text,
        distribution_policy=distribution_policy,
        index_name=index_name,
        fund_domicile=fund_domicile,
        benchmark=benchmark,
    )
    existing_position = calculate_positions(conn).positions.get((account_id, instrument_id))
    if existing_position and existing_position.quantity != 0:
        raise ValueError("initial snapshot would overwrite an existing position; use manual_adjustment")
    tx_id, warnings = _insert_transaction(
        conn,
        transaction_type="initial_position_snapshot",
        account_id=account_id,
        instrument_id=instrument_id,
        trade_date=snapshot_date,
        quantity=qty,
        gross=gross,
        fee=Decimal("0"),
        tax=Decimal("0"),
        currency=currency,
        fx_rate_to_chf_text=fx_rate_to_chf_text,
        fx_source=fx_source,
        fx_status=fx_status,
        note=note,
    )
    record_audit_event(
        conn,
        source="equity_manage",
        action="equity_initial_position_snapshot",
        entity_type="transaction",
        entity_id=tx_id,
        new_values={"account_id": account_id, "instrument_id": instrument_id, "transaction_type": "initial_position_snapshot"},
        user_text_note=note,
        confirmed=True,
        created_by="dashboard",
    )
    conn.commit()
    return EquityManageResult(instrument_id=instrument_id, account_id=account_id, transaction_id=tx_id, warnings=warnings)

def add_equity_transaction(
    conn: Connection,
    *,
    account_id: str,
    instrument_id: str,
    transaction_type: str,
    trade_date: str,
    quantity_text: str,
    gross_amount_original_text: str,
    currency: str,
    note: str,
    confirm: bool,
    fee_original_text: str = "0",
    tax_original_text: str = "0",
    fx_rate_to_chf_text: str | None = None,
    fx_source: str | None = None,
    fx_status: str = "ok",
) -> EquityManageResult:
    _require_confirm(confirm)
    account_id = _require_account(conn, account_id)
    instrument_id = _require_instrument(conn, instrument_id)
    transaction_type = str(transaction_type).strip()
    if transaction_type not in VALID_TRANSACTION_TYPES:
        raise ValueError("unsupported equity transaction type")
    trade_date = _require_date(trade_date)
    currency = _require_currency(currency)
    if transaction_type == "manual_adjustment":
        note = _require_note(note, reason="manual_adjustment")
    else:
        note = str(note or "").strip() or f"manual {transaction_type}"
    qty = parse_decimal_text(quantity_text, field_name="quantity")
    gross = parse_decimal_text(gross_amount_original_text, field_name="gross amount")
    fee = parse_decimal_text(fee_original_text, field_name="fee")
    tax = parse_decimal_text(tax_original_text, field_name="tax")
    if transaction_type in {"buy", "partial_sell", "full_sell", "initial_position_snapshot"} and qty <= 0:
        raise ValueError("quantity must be > 0")
    if transaction_type in {"dividend", "etf_distribution", "fee"}:
        qty = Decimal("0")
    if transaction_type in {"partial_sell", "full_sell"}:
        current = calculate_positions(conn).positions.get((account_id, instrument_id))
        current_qty = current.quantity if current else Decimal("0")
        if qty > current_qty:
            raise ValueError("sell would create negative position")
        if transaction_type == "full_sell" and qty != current_qty:
            raise ValueError("full_sell quantity must match current position")
    tx_id, warnings = _insert_transaction(
        conn,
        transaction_type=transaction_type,
        account_id=account_id,
        instrument_id=instrument_id,
        trade_date=trade_date,
        quantity=qty,
        gross=gross,
        fee=fee,
        tax=tax,
        currency=currency,
        fx_rate_to_chf_text=fx_rate_to_chf_text,
        fx_source=fx_source,
        fx_status=fx_status,
        note=note,
    )
    record_audit_event(
        conn,
        source="equity_manage",
        action=f"equity_{transaction_type}",
        entity_type="transaction",
        entity_id=tx_id,
        new_values={"account_id": account_id, "instrument_id": instrument_id, "transaction_type": transaction_type},
        user_text_note=note,
        confirmed=True,
        created_by="dashboard",
    )
    conn.commit()
    return EquityManageResult(instrument_id=instrument_id, account_id=account_id, transaction_id=tx_id, warnings=warnings)

def get_equity_management_options(conn: Connection) -> dict[str, list[dict[str, str]]]:
    accounts = [
        {
            "account_id": row["account_id"],
            "account_name": row["account_name"],
            "platform_name": row["platform_name"],
            "currency": row["currency"],
        }
        for row in conn.execute(
            """
            SELECT a.account_id, a.account_name, p.name AS platform_name, a.currency
            FROM accounts a JOIN platforms p ON p.platform_id=a.platform_id
            WHERE a.is_active=1
            ORDER BY p.name, a.account_name
            """
        ).fetchall()
    ]
    instruments = [
        {"instrument_id": row["instrument_id"], "name": row["name"], "isin": row["isin"] or "", "ticker": row["ticker"] or "", "currency": row["currency"]}
        for row in conn.execute("SELECT instrument_id, name, isin, ticker, currency FROM instruments WHERE is_active=1 ORDER BY name").fetchall()
    ]
    return {"accounts": accounts, "instruments": instruments}


from __future__ import annotations

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

PAGE_TITLE = "Equity/ETF Manage"

def _account_label(row: dict[str, str]) -> str:
    return f"{row['platform_name']} / {row['account_name']} [{row['currency']} ]"

def _instrument_label(row: dict[str, str]) -> str:
    suffix = row.get("isin") or row.get("ticker") or row["instrument_id"]
    return f"{row['name']} ({suffix})"

def _selected(rows: list[dict[str, str]], label: str | None, label_fn) -> dict[str, str] | None:
    if not label:
        return None
    for row in rows:
        if label_fn(row) == label:
            return row
    return None

def render(st, conn) -> None:
    st.title("Equity/ETF Manage")
    st.caption("Auditierbare manuelle Aktien-/ETF-Erfassung. Dashboard liest lokal; keine Broker-Dateien oder Live-APIs beim Rendern.")
    st.warning("Keine stillen Überschreibungen: Initial Snapshots, Transaktionen und Korrekturen werden als bestätigte Ledger-Ereignisse mit Audit-Log gespeichert.")

    options = get_equity_management_options(conn)
    account_labels = [_account_label(row) for row in options["accounts"]]
    instrument_labels = [_instrument_label(row) for row in options["instruments"]]
    tabs = st.tabs(["Hinzufügen", "Kaufen", "Verkaufen", "Dividende/Ausschüttung", "Korrigieren", "Review"])

    with tabs[0]:
        st.subheader("Aktie/ETF als Initial Snapshot hinzufügen")
        account_label = st.selectbox("Depot/Konto", account_labels, key="equity_add_account") if account_labels else None
        asset_class = st.selectbox("Assetklasse", ["stock", "ETF"], key="equity_add_class")
        name = st.text_input("Name", key="equity_add_name")
        isin = st.text_input("ISIN (primäre Identifikation)", key="equity_add_isin")
        ticker = st.text_input("Ticker optional", key="equity_add_ticker")
        exchange = st.text_input("Exchange optional", key="equity_add_exchange")
        currency = st.selectbox("Währung", ["CHF", "EUR", "USD"], key="equity_add_currency")
        quantity = st.text_input("Menge als Decimal-Text", key="equity_add_qty")
        cost_basis = st.text_input("Einstandspreis / Cost Basis Originalwährung", key="equity_add_cost")
        fx_rate = st.text_input("FX-Kurs zu CHF (bei EUR/USD)", key="equity_add_fx")
        fx_source = st.text_input("FX-Quelle", key="equity_add_fx_source")
        fx_status = st.selectbox("FX-Status", ["ok", "missing"], key="equity_add_fx_status")
        snapshot_date = st.date_input("Snapshot-Datum", key="equity_add_date")
        category = st.selectbox("Kategorie", ["Core", "Opportunity", "Unknown"], key="equity_add_category")
        note = st.text_area("Notiz (Pflicht bei unvollständiger Historie)", key="equity_add_note")
        with st.expander("ETF-spezifische Felder"):
            ter = st.text_input("TER optional", key="equity_add_ter")
            distribution_policy = st.text_input("ausschüttend/thesaurierend optional", key="equity_add_distribution")
            index_name = st.text_input("Index optional", key="equity_add_index")
            fund_domicile = st.text_input("Fondsdomizil optional", key="equity_add_domicile")
            benchmark = st.text_input("Benchmark optional", key="equity_add_benchmark")
        confirm = st.checkbox("Ich bestätige diese einzelne Änderung", key="equity_add_confirm")
        selected_account = _selected(options["accounts"], account_label, _account_label)
        st.write({"Aktion": "initial_position_snapshot", "Name": name, "ISIN": isin or "<fehlt>", "Ticker": ticker or "<fehlt>", "Währung": currency, "Datum": str(snapshot_date), "Hinweis": "Keine Massenänderung; Speicherung erst nach Bestätigung."})
        if st.button("Bestätigt speichern", key="equity_add_save", type="primary"):
            try:
                if not selected_account:
                    raise ValueError("account is required")
                result = add_equity_initial_snapshot(
                    conn,
                    account_id=selected_account["account_id"],
                    asset_class=asset_class,
                    name=name,
                    isin=isin,
                    ticker=ticker,
                    exchange=exchange,
                    currency=currency,
                    quantity_text=quantity,
                    cost_basis_original_text=cost_basis,
                    snapshot_date=str(snapshot_date),
                    category=category,
                    note=note,
                    fx_rate_to_chf_text=fx_rate or None,
                    fx_source=fx_source or None,
                    fx_status=fx_status,
                    ter_text=ter or None,
                    distribution_policy=distribution_policy or None,
                    index_name=index_name or None,
                    fund_domicile=fund_domicile or None,
                    benchmark=benchmark or None,
                    confirm=bool(confirm),
                )
                st.success("Initial Snapshot gespeichert und auditierbar erfasst.")
                st.write({"instrument_id": result.instrument_id, "transaction_id": result.transaction_id, "warnings": result.warnings})
            except Exception as exc:
                st.error(f"Speichern abgelehnt: {exc}")

    def transaction_form(tab, title: str, tx_type: str) -> None:
        with tab:
            st.subheader(title)
            account_label = st.selectbox("Depot/Konto", account_labels, key=f"equity_{tx_type}_account") if account_labels else None
            instrument_label = st.selectbox("Instrument", instrument_labels, key=f"equity_{tx_type}_instrument") if instrument_labels else None
            date_value = st.date_input("Datum", key=f"equity_{tx_type}_date")
            qty_default = "0" if tx_type in {"dividend", "etf_distribution", "fee"} else ""
            quantity = st.text_input("Menge als Decimal-Text", value=qty_default, key=f"equity_{tx_type}_qty")
            gross = st.text_input("Brutto/Cost/Proceeds Originalwährung", key=f"equity_{tx_type}_gross")
            currency = st.selectbox("Währung", ["CHF", "EUR", "USD"], key=f"equity_{tx_type}_currency")
            fx_rate = st.text_input("FX-Kurs zu CHF (bei EUR/USD)", key=f"equity_{tx_type}_fx")
            fx_source = st.text_input("FX-Quelle", key=f"equity_{tx_type}_fx_source")
            fx_status = st.selectbox("FX-Status", ["ok", "missing"], key=f"equity_{tx_type}_fx_status")
            note = st.text_area("Notiz / Grund", key=f"equity_{tx_type}_note")
            confirm = st.checkbox("Ich bestätige diese Änderung", key=f"equity_{tx_type}_confirm")
            selected_account = _selected(options["accounts"], account_label, _account_label)
            selected_instrument = _selected(options["instruments"], instrument_label, _instrument_label)
            st.write({"Aktion": tx_type, "Instrument": instrument_label, "Datum": str(date_value), "FX-Status": fx_status})
            if st.button("Bestätigt speichern", key=f"equity_{tx_type}_save"):
                try:
                    if not selected_account or not selected_instrument:
                        raise ValueError("account and instrument are required")
                    result = add_equity_transaction(
                        conn,
                        account_id=selected_account["account_id"],
                        instrument_id=selected_instrument["instrument_id"],
                        transaction_type=tx_type,
                        trade_date=str(date_value),
                        quantity_text=quantity,
                        gross_amount_original_text=gross,
                        currency=currency,
                        fx_rate_to_chf_text=fx_rate or None,
                        fx_source=fx_source or None,
                        fx_status=fx_status,
                        note=note,
                        confirm=bool(confirm),
                    )
                    st.success("Transaktion gespeichert und auditierbar erfasst.")
                    st.write({"transaction_id": result.transaction_id, "warnings": result.warnings})
                except Exception as exc:
                    st.error(f"Speichern abgelehnt: {exc}")

    transaction_form(tabs[1], "Kauf erfassen", "buy")
    transaction_form(tabs[2], "Verkauf erfassen", "partial_sell")
    with tabs[3]:
        st.subheader("Dividende/Ausschüttung")
        choice = st.radio("Typ", ["dividend", "etf_distribution", "fee"], key="equity_income_choice")
        st.info("Bitte nach Auswahl die Seite neu laden, falls du zwischen Dividende/Ausschüttung/Fee wechselst. Kleine MVP-Unschönheit, très chic.")
    transaction_form(tabs[4], "Korrektur als manual_adjustment", "manual_adjustment")

    with tabs[5]:
        st.subheader("Aktuelle Equity/ETF Übersicht")
        show_table(st, data.get_positions(conn))


Explanation:
**Identifying UI issues**

I noticed a UI tab bug with tabs[3], and it seems like the transaction form is missing the dividend. I realize that tests only focus on the import, but the user asked specifically about the UI forms. Maybe I should plan to simplify things later. It also looks like I need to include `__init__`. There’s definitely a bit to sort through here, but I’ll figure it out step by step!

from . import page_12_crypto_manage
from . import page_13_equity_manage


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/__init__.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/__init__.py
@@ -13,6 +13,7 @@
 from . import page_10_audit
 from . import page_11_settings_data_quality
 from . import page_12_crypto_manage
+from . import page_13_equity_manage

 PAGES = [
     page_00_command_center,


    page_12_crypto_manage,
    page_13_equity_manage,
]


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/__init__.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/pages/__init__.py
@@ -29,4 +29,5 @@
     page_10_audit,
     page_11_settings_data_quality,
     page_12_crypto_manage,
+    page_13_equity_manage,
 ]


Explanation:
**Updating migration files**

I need to add columns to the migration files. It’s important to rewrite the migrations file to make it more robust. I also have to update the schema for the instrument columns. I think I’ll use Python to replace strings as needed during this process. It’s a straightforward approach, but I want to ensure everything is handled properly to avoid any issues down the line. Let’s keep everything organized!

terminal

python - <<'PY'
from pathlib import Path
p=Path('/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/storage/schema.py')
s=p.read_text()
old="""    provider_symbol TEXT,\\n    data_provider_primary TEXT,"""
new="""    provider_symbol TEXT,\\n    data_provider_primary TEXT,\\n    position_category TEXT,\\n    ter TEXT,\\n    distribution_policy TEXT,\\n    index_name TEXT,\\n    fund_domicile TEXT,\\n    benchmark TEXT,"""
if old not in s:
    raise SystemExit('pattern missing')
p.write_text(s.replace(old,new))
PY


from __future__ import annotations

import hashlib
from datetime import datetime, timezone
from sqlite3 import Connection

from .schema import INITIAL_SCHEMA_SQL

MIGRATION_VERSION = 2
MIGRATION_NAME = "002_equity_manage_and_crypto_text_compat"

INSTRUMENT_OPTIONAL_COLUMNS = {
    "position_category": "TEXT",
    "ter": "TEXT",
    "distribution_policy": "TEXT",
    "index_name": "TEXT",
    "fund_domicile": "TEXT",
    "benchmark": "TEXT",
}

CRYPTO_TEXT_COLUMNS = {
    "crypto_holdings": {"quantity", "legacy_snapshot_value_original", "legacy_snapshot_value_chf"},
    "crypto_transactions": {"quantity", "price_original", "gross_amount_original", "fee_quantity", "fee_original", "fx_rate_to_chf", "amount_chf"},
    "crypto_prices": {"price", "market_cap", "volume_24h", "change_24h_pct"},
}

def utc_now() -> str:
    return datetime.now(timezone.utc).isoformat()

def checksum_sql(sql: str) -> str:
    return hashlib.sha256(sql.encode("utf-8")).hexdigest()

def get_schema_version(conn: Connection) -> int:
    row = conn.execute("SELECT MAX(version) AS version FROM schema_migrations").fetchone()
    return int(row["version"] or 0) if row else 0

def _table_columns(conn: Connection, table: str) -> dict[str, str]:
    return {row["name"]: (row["type"] or "") for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}

def _add_missing_instrument_columns(conn: Connection) -> None:
    existing = _table_columns(conn, "instruments")
    for name, col_type in INSTRUMENT_OPTIONAL_COLUMNS.items():
        if name not in existing:
            conn.execute(f"ALTER TABLE instruments ADD COLUMN {name} {col_type}")

def _rebuild_table_with_text_columns(conn: Connection, table: str, text_columns: set[str]) -> None:
    cols = conn.execute(f"PRAGMA table_info({table})").fetchall()
    if not cols:
        return
    needs_rebuild = any(row["name"] in text_columns and (row["type"] or "").upper() != "TEXT" for row in cols)
    if not needs_rebuild:
        return
    tmp = f"{table}__text_migration"
    col_defs: list[str] = []
    pk_cols = [row["name"] for row in cols if row["pk"]]
    for row in cols:
        name = row["name"]
        col_type = "TEXT" if name in text_columns else (row["type"] or "TEXT")
        parts = [name, col_type]
        if row["pk"] and len(pk_cols) == 1:
            parts.append("PRIMARY KEY")
        if row["notnull"]:
            parts.append("NOT NULL")
        if row["dflt_value"] is not None:
            parts.append(f"DEFAULT {row['dflt_value']}")
        col_defs.append(" ".join(parts))
    if len(pk_cols) > 1:
        col_defs.append("PRIMARY KEY (" + ", ".join(pk_cols) + ")")
    conn.execute(f"DROP TABLE IF EXISTS {tmp}")
    conn.execute(f"CREATE TABLE {tmp} (" + ", ".join(col_defs) + ")")
    names = [row["name"] for row in cols]
    select_exprs = [f"CAST({name} AS TEXT)" if name in text_columns else name for name in names]
    conn.execute(f"INSERT INTO {tmp} ({', '.join(names)}) SELECT {', '.join(select_exprs)} FROM {table}")
    conn.execute(f"DROP TABLE {table}")
    conn.execute(f"ALTER TABLE {tmp} RENAME TO {table}")
    if table == "crypto_holdings":
        conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_crypto_holdings_asset_wallet ON crypto_holdings(asset_id, wallet_id)")

def _apply_compat_migrations(conn: Connection) -> None:
    _add_missing_instrument_columns(conn)
    for table, text_columns in CRYPTO_TEXT_COLUMNS.items():
        _rebuild_table_with_text_columns(conn, table, text_columns)

def apply_migrations(conn: Connection) -> None:
    conn.executescript(INITIAL_SCHEMA_SQL)
    existing_initial = conn.execute("SELECT 1 FROM schema_migrations WHERE version = 1").fetchone()
    if not existing_initial:
        conn.execute(
            "INSERT INTO schema_migrations(version, name, applied_at, checksum) VALUES (?, ?, ?, ?)",
            (1, "001_initial_schema", utc_now(), checksum_sql(INITIAL_SCHEMA_SQL)),
        )
    _apply_compat_migrations(conn)
    existing = conn.execute("SELECT 1 FROM schema_migrations WHERE version = ?", (MIGRATION_VERSION,)).fetchone()
    if not existing:
        conn.execute(
            "INSERT INTO schema_migrations(version, name, applied_at, checksum) VALUES (?, ?, ?, ?)",
            (MIGRATION_VERSION, MIGRATION_NAME, utc_now(), checksum_sql(MIGRATION_NAME)),
        )
    conn.commit()


Explanation:
**Evaluating schema tests**

I need to determine if the tests for the schema expect version 1. Maybe I'll run a subset to check. I’m also concerned about losing the unique constraint; I created an index that seems okay, but the schema table might not have the unique index named correctly. It seems like the foreign keys might be lost too, but I guess that’s something to consider further. I should also look into getting dashboard data for positions.


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

result = calculate_positions(conn)


PYTHONPATH=src pytest tests/unit/test_equity_manage.py tests/unit/test_crypto_manage.py tests/unit/test_schema.py -q


Output:
FF............F                                                          [100%]
=================================== FAILURES ===================================
___ test_add_stock_initial_snapshot_creates_instrument_transaction_and_audit ___

    def test_add_stock_initial_snapshot_creates_instrument_transaction_and_audit() -> None:
        conn = setup_conn()
        account_id = seed_account(conn)

        result = add_equity_initial_snapshot(
            conn,
            account_id=account_id,
            asset_class="stock",
            name="Synthetic Nestle",
            isin="CH0038863350",
            ticker="NESN",
            exchange="SIX",
            currency="CHF",
            quantity_text="3.5",
            cost_basis_original_text="350.00",
            snapshot_date="2025-12-31",
            category="Core",
            note="synthetic incomplete history accepted",
            confirm=True,
        )

        tx = conn.execute("SELECT * FROM transactions WHERE transaction_id=?", (result.transaction_id,)).fetchone()
        instrument = conn.execute("SELECT * FROM instruments WHERE instrument_id=?", (result.instrument_id,)).fetchone()
        assert tx["transaction_type"] == "initial_position_snapshot"
>       assert tx["quantity"] == "3.5"
E       AssertionError: assert 3.5 == '3.5'

tests/unit/test_equity_manage.py:66: AssertionError
__________ test_add_etf_initial_snapshot_prepares_optional_etf_fields __________

    def test_add_etf_initial_snapshot_prepares_optional_etf_fields() -> None:
        conn = setup_conn()
        account_id = seed_account(conn, "Synthetic True Wealth")

        result = add_equity_initial_snapshot(
            conn,
            account_id=account_id,
            asset_class="ETF",
            name="Synthetic World ETF",
            isin="IE00B4L5Y983",
            ticker="SWLD",
            exchange="LSE",
            currency="USD",
            quantity_text="10",
            cost_basis_original_text="1000.00",
            snapshot_date="2025-12-31",
            category="Core",
            note="synthetic ETF snapshot with known FX",
            fx_rate_to_chf_text="0.91",
            fx_source="synthetic fixture",
            ter_text="0.20",
            distribution_policy="accumulating",
            index_name="MSCI World Synthetic",
            fund_domicile="IE",
            benchmark="Synthetic Benchmark",
            confirm=True,
        )

        instrument = conn.execute("SELECT * FROM instruments WHERE instrument_id=?", (result.instrument_id,)).fetchone()
        tx = conn.execute("SELECT * FROM transactions WHERE transaction_id=?", (result.transaction_id,)).fetchone()
        assert instrument["ter"] == "0.20"
        assert instrument["distribution_policy"] == "accumulating"
        assert instrument["index_name"] == "MSCI World Synthetic"
        assert instrument["fund_domicile"] == "IE"
        assert instrument["benchmark"] == "Synthetic Benchmark"
        assert tx["currency_original"] == "USD"
>       assert tx["fx_rate_to_chf"] == "0.91"
E       AssertionError: assert 0.91 == '0.91'

tests/unit/test_equity_manage.py:113: AssertionError
_________________________ test_schema_version_recorded _________________________

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

tests/unit/test_schema.py:18: AssertionError
=========================== short test summary info ============================
FAILED tests/unit/test_equity_manage.py::test_add_stock_initial_snapshot_creates_instrument_transaction_and_audit
FAILED tests/unit/test_equity_manage.py::test_add_etf_initial_snapshot_prepares_optional_etf_fields
FAILED tests/unit/test_schema.py::test_schema_version_recorded - assert 2 == 1
3 failed, 12 passed in 0.14s


python - <<'PY'
from pathlib import Path
p=Path('/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/storage/schema.py')
s=p.read_text()
for old,new in [
('    quantity NUMERIC,\\n    price_original NUMERIC,\\n    gross_amount_original NUMERIC,\\n    fee_original NUMERIC DEFAULT 0,\\n    tax_original NUMERIC DEFAULT 0,\\n    net_amount_original NUMERIC,\\n    currency_original TEXT NOT NULL,\\n    fx_rate_to_chf NUMERIC,',
 '    quantity TEXT,\\n    price_original TEXT,\\n    gross_amount_original TEXT,\\n    fee_original TEXT DEFAULT \'0\',\\n    tax_original TEXT DEFAULT \'0\',\\n    net_amount_original TEXT,\\n    currency_original TEXT NOT NULL,\\n    fx_rate_to_chf TEXT,'),
('    gross_amount_chf NUMERIC,\\n    fee_chf NUMERIC,\\n    tax_chf NUMERIC,\\n    net_amount_chf NUMERIC,',
 '    gross_amount_chf TEXT,\\n    fee_chf TEXT,\\n    tax_chf TEXT,\\n    net_amount_chf TEXT,')]:
    if old not in s: print('missing pattern', old[:30])
    s=s.replace(old,new)
p.write_text(s)
PY
python - <<'PY'
from pathlib import Path
p=Path('/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/storage/migrations.py')
s=p.read_text()
old='''CRYPTO_TEXT_COLUMNS = {\n    "crypto_holdings": {"quantity", "legacy_snapshot_value_original", "legacy_snapshot_value_chf"},\n    "crypto_transactions": {"quantity", "price_original", "gross_amount_original", "fee_quantity", "fee_original", "fx_rate_to_chf", "amount_chf"},\n    "crypto_prices": {"price", "market_cap", "volume_24h", "change_24h_pct"},\n}\n'''
new='''TEXT_AFFINITY_COLUMNS = {\n    "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"},\n    "crypto_holdings": {"quantity", "legacy_snapshot_value_original", "legacy_snapshot_value_chf"},\n    "crypto_transactions": {"quantity", "price_original", "gross_amount_original", "fee_quantity", "fee_original", "fx_rate_to_chf", "amount_chf"},\n    "crypto_prices": {"price", "market_cap", "volume_24h", "change_24h_pct"},\n}\n'''
if old not in s: raise SystemExit('pattern missing')
s=s.replace(old,new).replace('for table, text_columns in CRYPTO_TEXT_COLUMNS.items():', 'for table, text_columns in TEXT_AFFINITY_COLUMNS.items():')
p.write_text(s)
PY


     1|from __future__ import annotations
     2|
     3|from jarvis_finance.storage.database import connect_memory
     4|from jarvis_finance.storage.migrations import apply_migrations, get_schema_version
     5|from jarvis_finance.storage.schema import REQUIRED_TABLES
     6|
     7|
     8|def test_db_schema_can_be_created() -> None:
     9|    conn = connect_memory()
    10|    apply_migrations(conn)
    11|    tables = {row["name"] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")}
    12|    assert set(REQUIRED_TABLES).issubset(tables)
    13|
    14|
    15|def test_schema_version_recorded() -> None:
    16|    conn = connect_memory()
    17|    apply_migrations(conn)
    18|    assert get_schema_version(conn) == 1
    19|


patch

    assert get_schema_version(conn) == 2

def test_decimal_sensitive_columns_use_text_affinity() -> None:
    conn = connect_memory()
    apply_migrations(conn)
    tx_cols = {row["name"]: row["type"] for row in conn.execute("PRAGMA table_info(transactions)").fetchall()}
    crypto_cols = {row["name"]: row["type"] for row in conn.execute("PRAGMA table_info(crypto_holdings)").fetchall()}
    instrument_cols = {row["name"] for row in conn.execute("PRAGMA table_info(instruments)").fetchall()}
    assert tx_cols["quantity"].upper() == "TEXT"
    assert tx_cols["fx_rate_to_chf"].upper() == "TEXT"
    assert crypto_cols["quantity"].upper() == "TEXT"
    assert {"position_category", "ter", "distribution_policy", "index_name", "fund_domicile", "benchmark"}.issubset(instrument_cols)


--- a//home/agent/.hermes/repos/FinanceManager/tests/unit/test_schema.py
+++ b//home/agent/.hermes/repos/FinanceManager/tests/unit/test_schema.py
@@ -15,4 +15,16 @@
 def test_schema_version_recorded() -> None:
     conn = connect_memory()
     apply_migrations(conn)
-    assert get_schema_version(conn) == 1
+    assert get_schema_version(conn) == 2
+
+
+def test_decimal_sensitive_columns_use_text_affinity() -> None:
+    conn = connect_memory()
+    apply_migrations(conn)
+    tx_cols = {row["name"]: row["type"] for row in conn.execute("PRAGMA table_info(transactions)").fetchall()}
+    crypto_cols = {row["name"]: row["type"] for row in conn.execute("PRAGMA table_info(crypto_holdings)").fetchall()}
+    instrument_cols = {row["name"] for row in conn.execute("PRAGMA table_info(instruments)").fetchall()}
+    assert tx_cols["quantity"].upper() == "TEXT"
+    assert tx_cols["fx_rate_to_chf"].upper() == "TEXT"
+    assert crypto_cols["quantity"].upper() == "TEXT"
+    assert {"position_category", "ter", "distribution_policy", "index_name", "fund_domicile", "benchmark"}.issubset(instrument_cols)


terminal

Output:
................                                                         [100%]
16 passed in 0.08s


Explanation:
**Fixing dividend UI**

I see there's an issue with the UI in the dividend tab that's a bit weird. I'm considering whether to implement forms for dividends, distributions, and fees separately and adjust the correction tabs as well. Right now, there are six tabs, but tab three has just info with no dividend form, while tab four is for corrections. Since the user asked, it could make sense to simplify things with around seven tabs and make a patch at the bottom. Let's see how that works!

patch

tabs = st.tabs(["Hinzufügen", "Kaufen", "Verkaufen", "Dividende/Ausschüttung", "Korrigieren", "Review"])

tabs = st.tabs(["Hinzufügen", "Kaufen", "Verkaufen", "Dividende", "ETF-Ausschüttung", "Fee", "Korrigieren", "Review"])


--- 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
@@ -33,7 +33,7 @@
     options = get_equity_management_options(conn)
     account_labels = [_account_label(row) for row in options["accounts"]]
     instrument_labels = [_instrument_label(row) for row in options["instruments"]]
-    tabs = st.tabs(["Hinzufügen", "Kaufen", "Verkaufen", "Dividende/Ausschüttung", "Korrigieren", "Review"])
+    tabs = st.tabs(["Hinzufügen", "Kaufen", "Verkaufen", "Dividende", "ETF-Ausschüttung", "Fee", "Korrigieren", "Review"])

     with tabs[0]:
         st.subheader("Aktie/ETF als Initial Snapshot hinzufügen")


    transaction_form(tabs[1], "Kauf erfassen", "buy")
    transaction_form(tabs[2], "Verkauf erfassen", "partial_sell")
    transaction_form(tabs[3], "Dividende erfassen", "dividend")
    transaction_form(tabs[4], "ETF-Ausschüttung erfassen", "etf_distribution")
    transaction_form(tabs[5], "Fee erfassen", "fee")
    transaction_form(tabs[6], "Korrektur als manual_adjustment", "manual_adjustment")

    with tabs[7]:

    transaction_form(tabs[1], "Kauf erfassen", "buy")
    transaction_form(tabs[2], "Verkauf erfassen", "partial_sell")
    with tabs[3]:
        st.subheader("Dividende/Ausschüttung")
        choice = st.radio("Typ", ["dividend", "etf_distribution", "fee"], key="equity_income_choice")
        st.info("Bitte nach Auswahl die Seite neu laden, falls du zwischen Dividende/Ausschüttung/Fee wechselst. Kleine MVP-Unschönheit, très chic.")
    transaction_form(tabs[4], "Korrektur als manual_adjustment", "manual_adjustment")

    with tabs[5]:


--- 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
@@ -138,12 +138,11 @@

     transaction_form(tabs[1], "Kauf erfassen", "buy")
     transaction_form(tabs[2], "Verkauf erfassen", "partial_sell")
-    with tabs[3]:
-        st.subheader("Dividende/Ausschüttung")
-        choice = st.radio("Typ", ["dividend", "etf_distribution", "fee"], key="equity_income_choice")
-        st.info("Bitte nach Auswahl die Seite neu laden, falls du zwischen Dividende/Ausschüttung/Fee wechselst. Kleine MVP-Unschönheit, très chic.")
-    transaction_form(tabs[4], "Korrektur als manual_adjustment", "manual_adjustment")
+    transaction_form(tabs[3], "Dividende erfassen", "dividend")
+    transaction_form(tabs[4], "ETF-Ausschüttung erfassen", "etf_distribution")
+    transaction_form(tabs[5], "Fee erfassen", "fee")
+    transaction_form(tabs[6], "Korrektur als manual_adjustment", "manual_adjustment")

-    with tabs[5]:
+    with tabs[7]:
         st.subheader("Aktuelle Equity/ETF Übersicht")
         show_table(st, data.get_positions(conn))


terminal

set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
PYTHONPATH=src python - <<'PY'
from jarvis_finance.config.settings import load_settings, find_repo_root
from jarvis_finance.storage.database import connect
from jarvis_finance.storage.migrations import apply_migrations
from jarvis_finance.crypto.holdings import calculate_crypto_holdings
from jarvis_finance.reports.crypto_inventory import build_crypto_report_context
s=load_settings(repo_root=find_repo_root())
conn=connect(s.db_path)
try:
    apply_migrations(conn)
    asset=conn.execute("SELECT asset_id FROM crypto_assets WHERE coingecko_id='bitcoin' AND upper(symbol)='BTC' AND lower(coin_name)='bitcoin'").fetchone()
    if not asset: raise SystemExit('btc_asset_missing')
    asset_id=asset['asset_id']
    btc_holdings=conn.execute('SELECT COUNT(*) c FROM crypto_holdings WHERE asset_id=?',(asset_id,)).fetchone()['c']
    source_count=2
    dups=conn.execute('SELECT COUNT(*) c FROM (SELECT asset_id,wallet_id,COUNT(*) n FROM crypto_holdings GROUP BY asset_id,wallet_id HAVING n>1)').fetchone()['c']
    type_groups=conn.execute('SELECT typeof(quantity) t, COUNT(*) c FROM crypto_holdings WHERE asset_id=? GROUP BY typeof(quantity)',(asset_id,)).fetchall()
    audit_holding=conn.execute("SELECT COUNT(*) c FROM audit_log WHERE entity_type='crypto_holding' AND action='initial_crypto_holding_snapshot' AND entity_id IN (SELECT crypto_holding_id FROM crypto_holdings WHERE asset_id=?)",(asset_id,)).fetchone()['c']
    read_model_has_btc = asset_id in calculate_crypto_holdings(conn).total_by_asset
    ctx=build_crypto_report_context(conn)
    report_has_btc=any(r.get('symbol')=='BTC' and r.get('coingecko_id')=='bitcoin' for r in ctx['coins'])
    print('btc_asset_present=True')
    print('btc_holding_count_matches_source=' + str(btc_holdings == source_count))
    print('btc_holdings_imported=' + str(btc_holdings))
    print('duplicate_holdings=' + str(dups > 0))
    print('quantity_type_groups=' + ','.join(f"{r['t']}:{r['c']}" for r in type_groups))
    print('quantity_text_decimal=' + str(len(type_groups)==1 and type_groups[0]['t']=='text'))
    print('audit_logs_for_each_holding=' + str(audit_holding == btc_holdings == source_count))
    print('read_model_has_btc=' + str(read_model_has_btc))
    print('report_context_has_btc=' + str(report_has_btc))
finally:
    conn.close()
PY
find . -type d \( -name __pycache__ -o -name .pytest_cache \) -prune -exec rm -rf {} +
PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan .
git status --short --branch


Output:
btc_asset_present=True
btc_holding_count_matches_source=True
btc_holdings_imported=2
duplicate_holdings=False
quantity_type_groups=text:2
quantity_text_decimal=True
audit_logs_for_each_holding=True
read_model_has_btc=True
report_context_has_btc=True
GIT_SAFETY_OK
## main...origin/main
 M src/jarvis_finance/dashboard/pages/__init__.py
 M src/jarvis_finance/storage/migrations.py
 M src/jarvis_finance/storage/schema.py
 M tests/unit/test_schema.py
?? src/jarvis_finance/dashboard/pages/page_13_equity_manage.py
?? src/jarvis_finance/equity/
?? tests/unit/test_equity_manage.py


Explanation:
**Considering compilation errors**

I need to run full tests, but my initial thought is that there might be a compilation failure. I’m wondering if I should check the code for any issues before moving ahead with the testing. It feels like starting tests without ensuring a successful compile could lead to problems later on. So, I guess tackling the compilation issue first makes the most sense before diving deeper into the testing phase!

set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
PYTHONPATH=src python -m compileall src tests
PYTHONPATH=src pytest tests -q


Output:
Listing 'src'...
Listing 'src/jarvis_finance'...
Compiling 'src/jarvis_finance/app.py'...
Listing 'src/jarvis_finance/audit'...
Compiling 'src/jarvis_finance/audit/__init__.py'...
Compiling 'src/jarvis_finance/audit/decorators.py'...
Compiling 'src/jarvis_finance/audit/log.py'...
Compiling 'src/jarvis_finance/audit/validators.py'...
Listing 'src/jarvis_finance/cli'...
Listing 'src/jarvis_finance/config'...
Compiling 'src/jarvis_finance/config/logging.py'...
Listing 'src/jarvis_finance/crypto'...
Compiling 'src/jarvis_finance/crypto/__init__.py'...
Compiling 'src/jarvis_finance/crypto/assets.py'...
Compiling 'src/jarvis_finance/crypto/holdings.py'...
Compiling 'src/jarvis_finance/crypto/manage.py'...
Compiling 'src/jarvis_finance/crypto/transactions.py'...
Compiling 'src/jarvis_finance/crypto/validators.py'...
Compiling 'src/jarvis_finance/crypto/valuation.py'...
Compiling 'src/jarvis_finance/crypto/wallets.py'...
Listing 'src/jarvis_finance/dashboard'...
Compiling 'src/jarvis_finance/dashboard/__init__.py'...
Listing 'src/jarvis_finance/dashboard/components'...
Compiling 'src/jarvis_finance/dashboard/components/__init__.py'...
Compiling 'src/jarvis_finance/dashboard/components/charts.py'...
Compiling 'src/jarvis_finance/dashboard/components/forms.py'...
Compiling 'src/jarvis_finance/dashboard/components/tables.py'...
Compiling 'src/jarvis_finance/dashboard/components/warnings.py'...
Compiling 'src/jarvis_finance/dashboard/data.py'...
Compiling 'src/jarvis_finance/dashboard/demo_data.py'...
Compiling 'src/jarvis_finance/dashboard/main.py'...
Listing 'src/jarvis_finance/dashboard/pages'...
Compiling 'src/jarvis_finance/dashboard/pages/00_command_center.py'...
Compiling 'src/jarvis_finance/dashboard/pages/01_portfolio.py'...
Compiling 'src/jarvis_finance/dashboard/pages/02_platforms.py'...
Compiling 'src/jarvis_finance/dashboard/pages/03_equities_etfs.py'...
Compiling 'src/jarvis_finance/dashboard/pages/04_crypto.py'...
Compiling 'src/jarvis_finance/dashboard/pages/05_wallets.py'...
Compiling 'src/jarvis_finance/dashboard/pages/06_ledger.py'...
Compiling 'src/jarvis_finance/dashboard/pages/07_watchlist.py'...
Compiling 'src/jarvis_finance/dashboard/pages/08_reports.py'...
Compiling 'src/jarvis_finance/dashboard/pages/09_alerts.py'...
Compiling 'src/jarvis_finance/dashboard/pages/10_audit.py'...
Compiling 'src/jarvis_finance/dashboard/pages/11_settings_data_quality.py'...
Compiling 'src/jarvis_finance/dashboard/pages/__init__.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_00_command_center.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_01_portfolio.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_02_platforms.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_03_equities_etfs.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_04_crypto.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_05_wallets.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_06_ledger.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_07_watchlist.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_08_reports.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_09_alerts.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_10_audit.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_11_settings_data_quality.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_12_crypto_manage.py'...
Compiling 'src/jarvis_finance/dashboard/pages/page_13_equity_manage.py'...
Listing 'src/jarvis_finance/equity'...
Compiling 'src/jarvis_finance/equity/__init__.py'...
Compiling 'src/jarvis_finance/equity/manage.py'...
Listing 'src/jarvis_finance/fx'...
Compiling 'src/jarvis_finance/fx/__init__.py'...
Compiling 'src/jarvis_finance/fx/conversion.py'...
Compiling 'src/jarvis_finance/fx/overrides.py'...
Compiling 'src/jarvis_finance/fx/providers.py'...
Compiling 'src/jarvis_finance/fx/rates.py'...
Listing 'src/jarvis_finance/imports'...
Compiling 'src/jarvis_finance/imports/__init__.py'...
Compiling 'src/jarvis_finance/imports/accounts_importer.py'...
Compiling 'src/jarvis_finance/imports/cash_balances_importer.py'...
Compiling 'src/jarvis_finance/imports/common.py'...
Compiling 'src/jarvis_finance/imports/crypto_holdings_importer.py'...
Compiling 'src/jarvis_finance/imports/crypto_transactions_importer.py'...
Compiling 'src/jarvis_finance/imports/crypto_wallets_importer.py'...
Compiling 'src/jarvis_finance/imports/csv_dialect.py'...
Compiling 'src/jarvis_finance/imports/dry_run.py'...
Compiling 'src/jarvis_finance/imports/instruments_importer.py'...
Compiling 'src/jarvis_finance/imports/row_hash.py'...
Compiling 'src/jarvis_finance/imports/transactions_importer.py'...
Compiling 'src/jarvis_finance/imports/validators.py'...
Compiling 'src/jarvis_finance/imports/watchlist_importer.py'...
Listing 'src/jarvis_finance/ledger'...
Compiling 'src/jarvis_finance/ledger/__init__.py'...
Compiling 'src/jarvis_finance/ledger/cash.py'...
Compiling 'src/jarvis_finance/ledger/cost_basis.py'...
Compiling 'src/jarvis_finance/ledger/performance.py'...
Compiling 'src/jarvis_finance/ledger/positions.py'...
Compiling 'src/jarvis_finance/ledger/transactions.py'...
Compiling 'src/jarvis_finance/ledger/validators.py'...
Listing 'src/jarvis_finance/market'...
Compiling 'src/jarvis_finance/market/__init__.py'...
Compiling 'src/jarvis_finance/market/providers.py'...
Listing 'src/jarvis_finance/market_data'...
Compiling 'src/jarvis_finance/market_data/__init__.py'...
Compiling 'src/jarvis_finance/market_data/cache.py'...
Compiling 'src/jarvis_finance/market_data/coingecko.py'...
Compiling 'src/jarvis_finance/market_data/providers.py'...
Compiling 'src/jarvis_finance/market_data/staleness.py'...
Listing 'src/jarvis_finance/models'...
Compiling 'src/jarvis_finance/models/__init__.py'...
Compiling 'src/jarvis_finance/models/datatypes.py'...
Compiling 'src/jarvis_finance/models/dto.py'...
Compiling 'src/jarvis_finance/models/enums.py'...
Listing 'src/jarvis_finance/quality'...
Compiling 'src/jarvis_finance/quality/alerts.py'...
Compiling 'src/jarvis_finance/quality/checks.py'...
Compiling 'src/jarvis_finance/quality/rules.py'...
Listing 'src/jarvis_finance/reports'...
Compiling 'src/jarvis_finance/reports/__init__.py'...
Compiling 'src/jarvis_finance/reports/crypto_inventory.py'...
Compiling 'src/jarvis_finance/reports/crypto_pdf.py'...
Compiling 'src/jarvis_finance/reports/metadata.py'...
Compiling 'src/jarvis_finance/reports/render.py'...
Listing 'src/jarvis_finance/reports/templates'...
Compiling 'src/jarvis_finance/reports/templates/__init__.py'...
Listing 'src/jarvis_finance/storage'...
Compiling 'src/jarvis_finance/storage/repositories.py'...
Compiling 'src/jarvis_finance/storage/units_of_work.py'...
Listing 'tests'...
Compiling 'tests/conftest.py'...
Listing 'tests/fixtures'...
Listing 'tests/integration'...
Compiling 'tests/integration/test_dummy_data_load.py'...
Compiling 'tests/integration/test_end_to_end_dummy_portfolio.py'...
Listing 'tests/unit'...
Compiling 'tests/unit/test_audit.py'...
Compiling 'tests/unit/test_coingecko_provider.py'...
Compiling 'tests/unit/test_crypto.py'...
Compiling 'tests/unit/test_crypto_core.py'...
Compiling 'tests/unit/test_crypto_manage.py'...
Compiling 'tests/unit/test_crypto_report.py'...
Compiling 'tests/unit/test_csv_importers_extended.py'...
Compiling 'tests/unit/test_dashboard_mvp.py'...
Compiling 'tests/unit/test_equity_manage.py'...
Compiling 'tests/unit/test_fx.py'...
Compiling 'tests/unit/test_fx_logic.py'...
Compiling 'tests/unit/test_git_safety.py'...
Compiling 'tests/unit/test_importers_instruments.py'...
Compiling 'tests/unit/test_importers_platforms_accounts.py'...
Compiling 'tests/unit/test_imports.py'...
Compiling 'tests/unit/test_ledger_cash.py'...
Compiling 'tests/unit/test_ledger_cash_wac_positions.py'...
Compiling 'tests/unit/test_ledger_equities.py'...
Compiling 'tests/unit/test_market_alerts.py'...
Compiling 'tests/unit/test_quality.py'...
Compiling 'tests/unit/test_row_hash_dedup.py'...
Compiling 'tests/unit/test_schema.py'...
Compiling 'tests/unit/test_settings.py'...
Compiling 'tests/unit/test_transactions_importer.py'...
........................................................................ [ 75%]
.......................                                                  [100%]
95 passed in 0.36s


Explanation:
**Ensuring Git safety**

I need to ensure safety with Git, but I should check for any code issues first. It seems like I also need to import the dashboard smoke tests. Then there’s the cosmetic issue with that extra space in the account label on page 13. It would be good to test the import pages to see if they pass. I’ll also need to inspect the Git diff to confirm everything is working correctly.

set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
find . -type d \( -name __pycache__ -o -name .pytest_cache \) -prune -exec rm -rf {} +
PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan .
git status --short
git diff -- src/jarvis_finance/equity/manage.py src/jarvis_finance/storage/migrations.py src/jarvis_finance/dashboard/pages/page_13_equity_manage.py | sed -n '1,260p'


Output:
GIT_SAFETY_OK
 M src/jarvis_finance/dashboard/pages/__init__.py
 M src/jarvis_finance/storage/migrations.py
 M src/jarvis_finance/storage/schema.py
 M tests/unit/test_schema.py
?? src/jarvis_finance/dashboard/pages/page_13_equity_manage.py
?? src/jarvis_finance/equity/
?? tests/unit/test_equity_manage.py
diff --git a/src/jarvis_finance/storage/migrations.py b/src/jarvis_finance/storage/migrations.py
index c0ac890..f056c8d 100644
--- a/src/jarvis_finance/storage/migrations.py
+++ b/src/jarvis_finance/storage/migrations.py
@@ -6,8 +6,24 @@ from sqlite3 import Connection

 from .schema import INITIAL_SCHEMA_SQL

-MIGRATION_VERSION = 1
-MIGRATION_NAME = "001_initial_schema"
+MIGRATION_VERSION = 2
+MIGRATION_NAME = "002_equity_manage_and_crypto_text_compat"
+
+INSTRUMENT_OPTIONAL_COLUMNS = {
+    "position_category": "TEXT",
+    "ter": "TEXT",
+    "distribution_policy": "TEXT",
+    "index_name": "TEXT",
+    "fund_domicile": "TEXT",
+    "benchmark": "TEXT",
+}
+
+TEXT_AFFINITY_COLUMNS = {
+    "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"},
+    "crypto_holdings": {"quantity", "legacy_snapshot_value_original", "legacy_snapshot_value_chf"},
+    "crypto_transactions": {"quantity", "price_original", "gross_amount_original", "fee_quantity", "fee_original", "fx_rate_to_chf", "amount_chf"},
+    "crypto_prices": {"price", "market_cap", "volume_24h", "change_24h_pct"},
+}

 def utc_now() -> str:
@@ -23,12 +39,70 @@ def get_schema_version(conn: Connection) -> int:
     return int(row["version"] or 0) if row else 0

+def _table_columns(conn: Connection, table: str) -> dict[str, str]:
+    return {row["name"]: (row["type"] or "") for row in conn.execute(f"PRAGMA table_info({table})").fetchall()}
+
+
+def _add_missing_instrument_columns(conn: Connection) -> None:
+    existing = _table_columns(conn, "instruments")
+    for name, col_type in INSTRUMENT_OPTIONAL_COLUMNS.items():
+        if name not in existing:
+            conn.execute(f"ALTER TABLE instruments ADD COLUMN {name} {col_type}")
+
+
+def _rebuild_table_with_text_columns(conn: Connection, table: str, text_columns: set[str]) -> None:
+    cols = conn.execute(f"PRAGMA table_info({table})").fetchall()
+    if not cols:
+        return
+    needs_rebuild = any(row["name"] in text_columns and (row["type"] or "").upper() != "TEXT" for row in cols)
+    if not needs_rebuild:
+        return
+    tmp = f"{table}__text_migration"
+    col_defs: list[str] = []
+    pk_cols = [row["name"] for row in cols if row["pk"]]
+    for row in cols:
+        name = row["name"]
+        col_type = "TEXT" if name in text_columns else (row["type"] or "TEXT")
+        parts = [name, col_type]
+        if row["pk"] and len(pk_cols) == 1:
+            parts.append("PRIMARY KEY")
+        if row["notnull"]:
+            parts.append("NOT NULL")
+        if row["dflt_value"] is not None:
+            parts.append(f"DEFAULT {row['dflt_value']}")
+        col_defs.append(" ".join(parts))
+    if len(pk_cols) > 1:
+        col_defs.append("PRIMARY KEY (" + ", ".join(pk_cols) + ")")
+    conn.execute(f"DROP TABLE IF EXISTS {tmp}")
+    conn.execute(f"CREATE TABLE {tmp} (" + ", ".join(col_defs) + ")")
+    names = [row["name"] for row in cols]
+    select_exprs = [f"CAST({name} AS TEXT)" if name in text_columns else name for name in names]
+    conn.execute(f"INSERT INTO {tmp} ({', '.join(names)}) SELECT {', '.join(select_exprs)} FROM {table}")
+    conn.execute(f"DROP TABLE {table}")
+    conn.execute(f"ALTER TABLE {tmp} RENAME TO {table}")
+    if table == "crypto_holdings":
+        conn.execute("CREATE UNIQUE INDEX IF NOT EXISTS idx_crypto_holdings_asset_wallet ON crypto_holdings(asset_id, wallet_id)")
+
+
+def _apply_compat_migrations(conn: Connection) -> None:
+    _add_missing_instrument_columns(conn)
+    for table, text_columns in TEXT_AFFINITY_COLUMNS.items():
+        _rebuild_table_with_text_columns(conn, table, text_columns)
+
+
 def apply_migrations(conn: Connection) -> None:
     conn.executescript(INITIAL_SCHEMA_SQL)
+    existing_initial = conn.execute("SELECT 1 FROM schema_migrations WHERE version = 1").fetchone()
+    if not existing_initial:
+        conn.execute(
+            "INSERT INTO schema_migrations(version, name, applied_at, checksum) VALUES (?, ?, ?, ?)",
+            (1, "001_initial_schema", utc_now(), checksum_sql(INITIAL_SCHEMA_SQL)),
+        )
+    _apply_compat_migrations(conn)
     existing = conn.execute("SELECT 1 FROM schema_migrations WHERE version = ?", (MIGRATION_VERSION,)).fetchone()
     if not existing:
         conn.execute(
             "INSERT INTO schema_migrations(version, name, applied_at, checksum) VALUES (?, ?, ?, ?)",
-            (MIGRATION_VERSION, MIGRATION_NAME, utc_now(), checksum_sql(INITIAL_SCHEMA_SQL)),
+            (MIGRATION_VERSION, MIGRATION_NAME, utc_now(), checksum_sql(MIGRATION_NAME)),
         )
     conn.commit()


set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
git diff --stat
PYTHONPATH=src python -m compileall src tests >/tmp/fm_compile.log
PYTHONPATH=src pytest tests -q
find . -type d \( -name __pycache__ -o -name .pytest_cache \) -prune -exec rm -rf {} +
PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan .
git status --short --branch


Output:
src/jarvis_finance/dashboard/pages/__init__.py |  2 +
 src/jarvis_finance/storage/migrations.py       | 80 +++++++++++++++++++++++++-
 src/jarvis_finance/storage/schema.py           |  2 +-
 tests/unit/test_schema.py                      | 14 ++++-
 4 files changed, 93 insertions(+), 5 deletions(-)
........................................................................ [ 75%]
.......................                                                  [100%]
95 passed in 0.40s
GIT_SAFETY_OK
## main...origin/main
 M src/jarvis_finance/dashboard/pages/__init__.py
 M src/jarvis_finance/storage/migrations.py
 M src/jarvis_finance/storage/schema.py
 M tests/unit/test_schema.py
?? src/jarvis_finance/dashboard/pages/page_13_equity_manage.py
?? src/jarvis_finance/equity/
?? tests/unit/test_equity_manage.py


Explanation:
**Confirming commits**

I see that I didn't include untracked files in my last check. That means I need to commit those changes to ensure everything is accounted for. I want to make sure that nothing important is left out. So, I'll gather up those untracked files and prepare to commit them, keeping things tidy. It’s so easy to overlook this step, but I want to make sure I'm thorough!

set -euo pipefail
cd /home/agent/.hermes/repos/FinanceManager
git add src/jarvis_finance/dashboard/pages/__init__.py src/jarvis_finance/dashboard/pages/page_13_equity_manage.py src/jarvis_finance/equity src/jarvis_finance/storage/migrations.py src/jarvis_finance/storage/schema.py tests/unit/test_equity_manage.py tests/unit/test_schema.py
git commit -m "feat: add equity ETF management UI"
git rev-parse --short HEAD
git status --short --branch


Output:
[main 1131cde] feat: add equity ETF management UI
 8 files changed, 956 insertions(+), 5 deletions(-)
 create mode 100644 src/jarvis_finance/dashboard/pages/page_13_equity_manage.py
 create mode 100644 src/jarvis_finance/equity/__init__.py
 create mode 100644 src/jarvis_finance/equity/manage.py
 create mode 100644 tests/unit/test_equity_manage.py
1131cde
## main...origin/main [voraus 1]