Explanation:
**Deciding on BTC holdings**

I need to figure out the BTC row to decide its status. The user wants a pro wallet with exactly one holding, so I have to check if my existing holdings are duplicates. There are two current holdings that might get imported, but I need to verify if they match the counts in the Excel sheet. I'll use a script to count the non-zero BTC cells and ensure they align with the wallet labels without considering quantities.

PYTHONPATH=src python - <<'PY'
from pathlib import Path
from decimal import Decimal, InvalidOperation
import openpyxl
from jarvis_finance.config.settings import load_settings
from jarvis_finance.storage.database import connect
p=Path('/home/agent/jarvis_runtime/finance-system/imports/tmp_btc_test/crypto_coins_source.xlsx')
wb=openpyxl.load_workbook(p, read_only=True, data_only=True)
ws=wb['2024']
headers=[c for c in next(ws.iter_rows(min_row=2,max_row=2,values_only=True))]
wallet_headers=headers[1:9]
wallet_map={'Swissborg':'SwissBorg','Coinbase':'Coinbase','Binance':'Binance','KuCoin':'KuCoin','Metamask':'MetaMask','FireFly IOTA':'Firefly IOTA Wallet','Yoloi Wallet':'Yoroi Wallet','Nami Wallet':'Nami Wallet'}
btc_row=None
for row in ws.iter_rows(min_row=3, values_only=True):
    label=str(row[0]).strip() if row[0] is not None else ''
    if label == 'Bitcoin BTC':
        btc_row=row; break
if not btc_row: raise SystemExit('btc row not found')
entries=[]
for hdr,val in zip(wallet_headers, btc_row[1:9]):
    if val is None or str(val).strip()=='' : continue
    d=Decimal(str(val))
    if d != 0:
        entries.append((hdr, wallet_map.get(hdr)))
settings=load_settings(); conn=connect(settings.db_path)
asset=conn.execute("SELECT asset_id, coin_name, symbol, coingecko_id FROM crypto_assets WHERE coingecko_id='bitcoin' OR upper(symbol)='BTC' OR lower(coin_name)='bitcoin'").fetchone()
wallets={r['wallet_name']:r['wallet_id'] for r in conn.execute('SELECT wallet_id,wallet_name FROM crypto_wallets')}
matched=sum(1 for _,name in entries if name in wallets)
existing=0
if asset:
  for _,name in entries:
    wid=wallets.get(name)
    if wid and conn.execute('SELECT 1 FROM crypto_holdings WHERE asset_id=? AND wallet_id=?',(asset['asset_id'],wid)).fetchone(): existing += 1
print(f'btc_row_found=yes nonzero_wallet_cells={len(entries)} matched_wallets={matched} asset_exists={bool(asset)} existing_matching_holdings={existing}')
print('unmatched_wallets_count', len(entries)-matched)
PY


terminal

Output:
btc_row_found=yes nonzero_wallet_cells=2 matched_wallets=2 asset_exists=True existing_matching_holdings=2
unmatched_wallets_count 0


Explanation:
**Verifying BTC test completion**

I realize the BTC test seems effectively done, but the user feels it's not completed. I need to verify the audit details, including the date and quantities, and check for duplicates without printing anything. I'll confirm if the user can import without it affecting anything. It seems I should create an audit unless it's already there. If there's a snapshot date of 2025-12-31, I need to verify that too.

PYTHONPATH=src python - <<'PY'
from pathlib import Path
from decimal import Decimal
import openpyxl
from jarvis_finance.config.settings import load_settings
from jarvis_finance.storage.database import connect
from jarvis_finance.storage.migrations import apply_migrations
settings=load_settings(); conn=connect(settings.db_path); apply_migrations(conn)
asset=conn.execute("SELECT asset_id FROM crypto_assets WHERE coingecko_id='bitcoin' AND coin_name='Bitcoin' AND symbol='BTC'").fetchone()
asset_id=asset['asset_id'] if asset else None
holdings=conn.execute('SELECT * FROM crypto_holdings WHERE asset_id=?',(asset_id,)).fetchall() if asset_id else []
audit_count=0
snapshot_date_ok=0
text_qty_ok=0
legacy_chf_ignored=0
for h in holdings:
    audit_count += conn.execute("SELECT COUNT(*) n FROM audit_log WHERE entity_type='crypto_holding' AND entity_id=?",(h['crypto_holding_id'],)).fetchone()['n']
    if h['legacy_snapshot_date']=='2025-12-31' and h['last_verified_at']=='2025-12-31': snapshot_date_ok += 1
    if conn.execute('SELECT typeof(quantity) t FROM crypto_holdings WHERE crypto_holding_id=?',(h['crypto_holding_id'],)).fetchone()['t']=='text': text_qty_ok += 1
    if h['legacy_snapshot_value_chf'] is None and h['legacy_snapshot_value_original'] is None: legacy_chf_ignored += 1
dupes=conn.execute('SELECT COUNT(*) n FROM (SELECT asset_id,wallet_id,COUNT(*) c FROM crypto_holdings GROUP BY asset_id,wallet_id HAVING c>1)').fetchone()['n']
print(f'asset_present={bool(asset_id)} holdings={len(holdings)} audit_rows={audit_count} snapshot_date_ok={snapshot_date_ok} text_qty_ok={text_qty_ok} legacy_values_ignored={legacy_chf_ignored} duplicates={dupes}')
PY


Output: asset_present=True holdings=2 audit_rows=2 snapshot_date_ok=2 text_qty_ok=2 legacy_values_ignored=2 duplicates=0


Explanation:
**Checking Git safety for BTC**

I need to verify the "Git safety" for BTC, both before and after. It looks like we've already done the checks after the first part. However, I should also check the status and safety of BTC beforehand. I might need to run tests both before and after, and I should be careful not to write anything if it's already been done. Let's make sure everything's clear!

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


Output:
## main...origin/main
 M src/jarvis_finance/cli/main.py
 M src/jarvis_finance/market/providers.py
 M tests/unit/test_crypto_price_update.py
GIT_SAFETY_OK


Explanation:
**Evaluating Git status**

I need to check if there's any uncommitted code since my git status isn’t clean. The user asked about safety with Git, both before and after, which is fine, but my code isn't clean right now. I might want to commit Teil1 before checking BTC. I'm considering whether a commit is needed since the user indicated that the commit was accepted. I should definitely commit the robust price update and make sure to run tests for safety before I proceed.

git add src/jarvis_finance/cli/main.py src/jarvis_finance/market/providers.py tests/unit/test_crypto_price_update.py && git commit -m "feat: harden crypto price refresh" && git status --short --branch


Output:
[main 1b5028d] feat: harden crypto price refresh
 3 files changed, 309 insertions(+), 48 deletions(-)
## main...origin/main [voraus 1]


git status --short --branch && PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan . && PYTHONPATH=src python - <<'PY'
from pathlib import Path
from jarvis_finance.config.settings import load_settings
from jarvis_finance.storage.database import connect
from jarvis_finance.storage.migrations import apply_migrations
s=load_settings(); c=connect(s.db_path); apply_migrations(c)
# Aggregate-only idempotency verification for approved BTC test; no raw values printed.
asset=c.execute("SELECT asset_id FROM crypto_assets WHERE coin_name='Bitcoin' AND symbol='BTC' AND coingecko_id='bitcoin'").fetchone()
asset_id=asset['asset_id'] if asset else None
holdings=c.execute('SELECT * FROM crypto_holdings WHERE asset_id=?',(asset_id,)).fetchall() if asset_id else []
audit=sum(c.execute("SELECT COUNT(*) n FROM audit_log WHERE entity_type='crypto_holding' AND entity_id=?",(h['crypto_holding_id'],)).fetchone()['n'] for h in holdings)
dupes=c.execute('SELECT COUNT(*) n FROM (SELECT asset_id,wallet_id,COUNT(*) c FROM crypto_holdings GROUP BY asset_id,wallet_id HAVING c>1)').fetchone()['n']
print(f'BTC_TEST_AGGREGATE asset_present={bool(asset_id)} holdings={len(holdings)} audit_logs_present={audit >= len(holdings) and len(holdings)>0} duplicate_pairs={dupes}')
PY
git status --short --branch && PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan .


Output:
## main...origin/main [voraus 1]
GIT_SAFETY_OK
BTC_TEST_AGGREGATE asset_present=True holdings=2 audit_logs_present=True duplicate_pairs=0
## main...origin/main [voraus 1]
GIT_SAFETY_OK


Explanation:
**Inspecting equity UI requirements**

I’m noticing that the equity UI says “beginnen,” which translates to “begin” in English. However, it seems like the repository already has an equity UI, which means I need to inspect it to see if it meets the requirements. I might also need to add some notes to the backlog about this. Additionally, searching for equity tests could be beneficial in ensuring everything is aligned properly.


     1|from __future__ import annotations
     2|
     3|from dataclasses import dataclass, field
     4|from decimal import Decimal, InvalidOperation
     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.ledger.positions import calculate_positions
    10|from jarvis_finance.quality.alerts import create_alert
    11|
    12|VALID_ASSET_CLASSES = {"stock", "etf"}
    13|VALID_CURRENCIES = {"CHF", "EUR", "USD"}
    14|VALID_CATEGORIES = {"Core", "Opportunity", "Unknown"}
    15|VALID_TRANSACTION_TYPES = {"buy", "partial_sell", "full_sell", "dividend", "etf_distribution", "fee", "manual_adjustment"}
    16|
    17|
    18|@dataclass(frozen=True)
    19|class EquityManageResult:
    20|    instrument_id: str
    21|    account_id: str
    22|    transaction_id: str
    23|    warnings: list[str] = field(default_factory=list)
    24|
    25|
    26|def parse_decimal_text(value: str, *, field_name: str) -> Decimal:
    27|    text = str(value).strip()
    28|    if not text:
    29|        raise ValueError(f"{field_name} is required")
    30|    try:
    31|        dec = Decimal(text)
    32|    except (InvalidOperation, ValueError) as exc:
    33|        raise ValueError(f"{field_name} must be a Decimal string") from exc
    34|    if not dec.is_finite():
    35|        raise ValueError(f"{field_name} must be finite")
    36|    return dec
    37|
    38|
    39|def decimal_text(value: Decimal | None, places: str | None = None) -> str | None:
    40|    if value is None:
    41|        return None
    42|    if places is not None:
    43|        value = value.quantize(Decimal(places))
    44|    return format(value, "f")
    45|
    46|
    47|def _require_confirm(confirm: bool) -> None:
    48|    if not confirm:
    49|        raise ValueError("explicit confirm is required before saving")
    50|
    51|
    52|def _require_date(value: str) -> str:
    53|    text = str(value).strip()
    54|    parts = text.split("-")
    55|    if len(parts) != 3 or not all(part.isdigit() for part in parts):
    56|        raise ValueError("date must be ISO YYYY-MM-DD")
    57|    return text
    58|
    59|
    60|def _require_account(conn: Connection, account_id: str) -> str:
    61|    account_id = str(account_id).strip()
    62|    if not account_id:
    63|        raise ValueError("account is required")
    64|    row = conn.execute("SELECT account_id FROM accounts WHERE account_id=?", (account_id,)).fetchone()
    65|    if row is None:
    66|        raise ValueError("account not found")
    67|    return account_id
    68|
    69|
    70|def _require_instrument(conn: Connection, instrument_id: str) -> str:
    71|    instrument_id = str(instrument_id).strip()
    72|    row = conn.execute("SELECT instrument_id FROM instruments WHERE instrument_id=?", (instrument_id,)).fetchone()
    73|    if row is None:
    74|        raise ValueError("instrument not found")
    75|    return instrument_id
    76|
    77|
    78|def _normalize_asset_class(asset_class: str) -> str:
    79|    value = str(asset_class).strip().lower()
    80|    if value not in VALID_ASSET_CLASSES:
    81|        raise ValueError("asset_class must be stock or ETF")
    82|    return value
    83|
    84|
    85|def _require_currency(currency: str) -> str:
    86|    value = str(currency).strip().upper()
    87|    if value not in VALID_CURRENCIES:
    88|        raise ValueError("currency must be CHF, EUR or USD")
    89|    return value
    90|
    91|
    92|def _require_note(note: str, *, reason: str) -> str:
    93|    text = str(note or "").strip()
    94|    if not text:
    95|        raise ValueError(f"note is required for {reason}")
    96|    return text
    97|
    98|
    99|def _find_instrument(conn: Connection, *, isin: str, ticker: str, name: str, currency: str) -> str | None:
   100|    if isin:
   101|        row = conn.execute("SELECT instrument_id FROM instruments WHERE isin=?", (isin,)).fetchone()
   102|        if row:
   103|            return row["instrument_id"]
   104|    if ticker:
   105|        row = conn.execute("SELECT instrument_id FROM instruments WHERE upper(ticker)=upper(?) AND currency=?", (ticker, currency)).fetchone()
   106|        if row:
   107|            return row["instrument_id"]
   108|    row = conn.execute("SELECT instrument_id FROM instruments WHERE name=? AND currency=?", (name, currency)).fetchone()
   109|    return row["instrument_id"] if row else None
   110|
   111|
   112|def _ensure_instrument(
   113|    conn: Connection,
   114|    *,
   115|    asset_class: str,
   116|    name: str,
   117|    isin: str,
   118|    ticker: str,
   119|    exchange: str,
   120|    currency: str,
   121|    category: str,
   122|    note: str,
   123|    ter_text: str | None = None,
   124|    distribution_policy: str | None = None,
   125|    index_name: str | None = None,
   126|    fund_domicile: str | None = None,
   127|    benchmark: str | None = None,
   128|) -> str:
   129|    isin = str(isin or "").strip().upper()
   130|    ticker = str(ticker or "").strip().upper()
   131|    name = str(name or "").strip()
   132|    if not name:
   133|        raise ValueError("name is required")
   134|    if not isin and not ticker:
   135|        raise ValueError("ISIN or ticker is required")
   136|    if category not in VALID_CATEGORIES:
   137|        raise ValueError("category must be Core, Opportunity or Unknown")
   138|    ter = None
   139|    if ter_text not in {None, ""}:
   140|        ter = decimal_text(parse_decimal_text(str(ter_text), field_name="TER"))
   141|    existing = _find_instrument(conn, isin=isin, ticker=ticker, name=name, currency=currency)
   142|    now = utc_now()
   143|    if existing:
   144|        conn.execute(
   145|            """
   146|            UPDATE instruments
   147|            SET name=COALESCE(NULLIF(?, ''), name), ticker=COALESCE(NULLIF(?, ''), ticker),
   148|                exchange=COALESCE(NULLIF(?, ''), exchange), position_category=?, ter=COALESCE(?, ter),
   149|                distribution_policy=COALESCE(NULLIF(?, ''), distribution_policy), index_name=COALESCE(NULLIF(?, ''), index_name),
   150|                fund_domicile=COALESCE(NULLIF(?, ''), fund_domicile), benchmark=COALESCE(NULLIF(?, ''), benchmark),
   151|                updated_at=?
   152|            WHERE instrument_id=?
   153|            """,
   154|            (name, ticker, exchange, category, ter, distribution_policy or "", index_name or "", fund_domicile or "", benchmark or "", now, existing),
   155|        )
   156|        return existing
   157|    instrument_id = stable_id("instrument", isin or ticker or name, currency)
   158|    conn.execute(
   159|        """
   160|        INSERT INTO instruments(
   161|            instrument_id, asset_class, name, ticker, isin, exchange, currency,
   162|            notes, position_category, ter, distribution_policy, index_name, fund_domicile, benchmark, created_at
   163|        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
   164|        """,
   165|        (instrument_id, asset_class, name, ticker, isin, exchange, currency, note, category, ter, distribution_policy, index_name, fund_domicile, benchmark, now),
   166|    )
   167|    return instrument_id
   168|
   169|
   170|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]]:
   171|    warnings: list[str] = []
   172|    if currency == "CHF":
   173|        fx = Decimal("1")
   174|        status = "ok"
   175|        source = fx_source or "CHF base currency"
   176|    else:
   177|        status = str(fx_status or "").strip().lower()
   178|        if fx_rate_text:
   179|            fx = parse_decimal_text(fx_rate_text, field_name="fx_rate_to_chf")
   180|            if fx <= 0:
   181|                raise ValueError("fx_rate_to_chf must be > 0")
   182|            status = "ok"
   183|            source = fx_source or "manual"
   184|        elif status == "missing":
   185|            fx = None
   186|            source = fx_source or "missing"
   187|            warnings.append("missing_fx")
   188|            create_alert(
   189|                conn,
   190|                priority="kritisch",
   191|                category="equity",
   192|                entity_type="instrument",
   193|                entity_id=entity_hint,
   194|                rule_id="missing_fx",
   195|                message="Historic FX is missing; CHF value is incomplete.",
   196|                evidence={"currency": currency},
   197|                fingerprint="missing_fx",
   198|            )
   199|        else:
   200|            raise ValueError("FX rate is required for foreign currency unless fx_status=missing")
   201|    if fx is None:
   202|        return status or "missing", None, source, None, None, warnings
   203|    gross_chf = gross * fx
   204|    fee_chf = fee * fx
   205|    tax_chf = tax * fx
   206|    net_chf = gross_chf - fee_chf - tax_chf
   207|    return status or "ok", decimal_text(fx), source, decimal_text(gross_chf, "0.0001"), decimal_text(net_chf, "0.0001"), warnings
   208|
   209|
   210|def _insert_transaction(
   211|    conn: Connection,
   212|    *,
   213|    transaction_type: str,
   214|    account_id: str,
   215|    instrument_id: str,
   216|    trade_date: str,
   217|    quantity: Decimal | None,
   218|    gross: Decimal,
   219|    fee: Decimal,
   220|    tax: Decimal,
   221|    currency: str,
   222|    fx_rate_to_chf_text: str | None,
   223|    fx_source: str | None,
   224|    fx_status: str,
   225|    note: str,
   226|) -> tuple[str, list[str]]:
   227|    now = utc_now()
   228|    status, fx_text, fx_source_value, gross_chf, net_chf, warnings = _fx_values(
   229|        conn,
   230|        currency=currency,
   231|        gross=gross,
   232|        fee=fee,
   233|        tax=tax,
   234|        fx_rate_text=fx_rate_to_chf_text,
   235|        fx_source=fx_source,
   236|        fx_status=fx_status,
   237|        entity_hint=instrument_id,
   238|    )
   239|    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)
   240|    conn.execute(
   241|        """
   242|        INSERT INTO transactions(
   243|            transaction_id, transaction_type, account_id, instrument_id, trade_date,
   244|            quantity, gross_amount_original, fee_original, tax_original, net_amount_original,
   245|            currency_original, fx_rate_to_chf, fx_source, fx_status, gross_amount_chf,
   246|            fee_chf, tax_chf, net_amount_chf, source_type, source_id, is_confirmed,
   247|            quality_status, notes, created_at
   248|        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'manual_dashboard', ?, 1, ?, ?, ?)
   249|        """,
   250|        (
   251|            tx_id,
   252|            transaction_type,
   253|            account_id,
   254|            instrument_id,
   255|            trade_date,
   256|            decimal_text(quantity) if quantity is not None else None,
   257|            decimal_text(gross),
   258|            decimal_text(fee),
   259|            decimal_text(tax),
   260|            decimal_text(gross - fee - tax),
   261|            currency,
   262|            fx_text,
   263|            fx_source_value,
   264|            status,
   265|            gross_chf,
   266|            decimal_text(fee * Decimal(fx_text), "0.0001") if fx_text else None,
   267|            decimal_text(tax * Decimal(fx_text), "0.0001") if fx_text else None,
   268|            net_chf,
   269|            tx_id,
   270|            "incomplete" if warnings else "ok",
   271|            note,
   272|            now,
   273|        ),
   274|    )
   275|    return tx_id, warnings
   276|
   277|
   278|def add_equity_initial_snapshot(
   279|    conn: Connection,
   280|    *,
   281|    account_id: str,
   282|    asset_class: str,
   283|    name: str,
   284|    isin: str = "",
   285|    ticker: str = "",
   286|    exchange: str = "",
   287|    currency: str,
   288|    quantity_text: str,
   289|    cost_basis_original_text: str,
   290|    snapshot_date: str,
   291|    category: str = "Unknown",
   292|    note: str,
   293|    confirm: bool,
   294|    fx_rate_to_chf_text: str | None = None,
   295|    fx_source: str | None = None,
   296|    fx_status: str = "ok",
   297|    ter_text: str | None = None,
   298|    distribution_policy: str | None = None,
   299|    index_name: str | None = None,
   300|    fund_domicile: str | None = None,
   301|    benchmark: str | None = None,
   302|) -> EquityManageResult:
   303|    _require_confirm(confirm)
   304|    account_id = _require_account(conn, account_id)
   305|    asset_class = _normalize_asset_class(asset_class)
   306|    currency = _require_currency(currency)
   307|    snapshot_date = _require_date(snapshot_date)
   308|    note = _require_note(note, reason="initial_position_snapshot with incomplete history")
   309|    qty = parse_decimal_text(quantity_text, field_name="quantity")
   310|    if qty <= 0:
   311|        raise ValueError("quantity must be > 0")
   312|    gross = parse_decimal_text(cost_basis_original_text, field_name="cost basis")
   313|    if gross < 0:
   314|        raise ValueError("cost basis must be >= 0")
   315|    instrument_id = _ensure_instrument(
   316|        conn,
   317|        asset_class=asset_class,
   318|        name=name,
   319|        isin=isin,
   320|        ticker=ticker,
   321|        exchange=exchange,
   322|        currency=currency,
   323|        category=category,
   324|        note=note,
   325|        ter_text=ter_text,
   326|        distribution_policy=distribution_policy,
   327|        index_name=index_name,
   328|        fund_domicile=fund_domicile,
   329|        benchmark=benchmark,
   330|    )
   331|    existing_position = calculate_positions(conn).positions.get((account_id, instrument_id))
   332|    if existing_position and existing_position.quantity != 0:
   333|        raise ValueError("initial snapshot would overwrite an existing position; use manual_adjustment")
   334|    tx_id, warnings = _insert_transaction(
   335|        conn,
   336|        transaction_type="initial_position_snapshot",
   337|        account_id=account_id,
   338|        instrument_id=instrument_id,
   339|        trade_date=snapshot_date,
   340|        quantity=qty,
   341|        gross=gross,
   342|        fee=Decimal("0"),
   343|        tax=Decimal("0"),
   344|        currency=currency,
   345|        fx_rate_to_chf_text=fx_rate_to_chf_text,
   346|        fx_source=fx_source,
   347|        fx_status=fx_status,
   348|        note=note,
   349|    )
   350|    record_audit_event(
   351|        conn,
   352|        source="equity_manage",
   353|        action="equity_initial_position_snapshot",
   354|        entity_type="transaction",
   355|        entity_id=tx_id,
   356|        new_values={"account_id": account_id, "instrument_id": instrument_id, "transaction_type": "initial_position_snapshot"},
   357|        user_text_note=note,
   358|        confirmed=True,
   359|        created_by="dashboard",
   360|    )
   361|    conn.commit()
   362|    return EquityManageResult(instrument_id=instrument_id, account_id=account_id, transaction_id=tx_id, warnings=warnings)
   363|
   364|
   365|def add_equity_transaction(
   366|    conn: Connection,
   367|    *,
   368|    account_id: str,
   369|    instrument_id: str,
   370|    transaction_type: str,
   371|    trade_date: str,
   372|    quantity_text: str,
   373|    gross_amount_original_text: str,
   374|    currency: str,
   375|    note: str,
   376|    confirm: bool,
   377|    fee_original_text: str = "0",
   378|    tax_original_text: str = "0",
   379|    fx_rate_to_chf_text: str | None = None,
   380|    fx_source: str | None = None,
   381|    fx_status: str = "ok",
   382|) -> EquityManageResult:
   383|    _require_confirm(confirm)
   384|    account_id = _require_account(conn, account_id)
   385|    instrument_id = _require_instrument(conn, instrument_id)
   386|    transaction_type = str(transaction_type).strip()
   387|    if transaction_type not in VALID_TRANSACTION_TYPES:
   388|        raise ValueError("unsupported equity transaction type")
   389|    trade_date = _require_date(trade_date)
   390|    currency = _require_currency(currency)
   391|    if transaction_type == "manual_adjustment":
   392|        note = _require_note(note, reason="manual_adjustment")
   393|    else:
   394|        note = str(note or "").strip() or f"manual {transaction_type}"
   395|    qty = parse_decimal_text(quantity_text, field_name="quantity")
   396|    gross = parse_decimal_text(gross_amount_original_text, field_name="gross amount")
   397|    fee = parse_decimal_text(fee_original_text, field_name="fee")
   398|    tax = parse_decimal_text(tax_original_text, field_name="tax")
   399|    if transaction_type in {"buy", "partial_sell", "full_sell", "initial_position_snapshot"} and qty <= 0:
   400|        raise ValueError("quantity must be > 0")
   401|    if transaction_type in {"dividend", "etf_distribution", "fee"}:
   402|        qty = Decimal("0")
   403|    if transaction_type in {"partial_sell", "full_sell"}:
   404|        current = calculate_positions(conn).positions.get((account_id, instrument_id))
   405|        current_qty = current.quantity if current else Decimal("0")
   406|        if qty > current_qty:
   407|            raise ValueError("sell would create negative position")
   408|        if transaction_type == "full_sell" and qty != current_qty:
   409|            raise ValueError("full_sell quantity must match current position")
   410|    tx_id, warnings = _insert_transaction(
   411|        conn,
   412|        transaction_type=transaction_type,
   413|        account_id=account_id,
   414|        instrument_id=instrument_id,
   415|        trade_date=trade_date,
   416|        quantity=qty,
   417|        gross=gross,
   418|        fee=fee,
   419|        tax=tax,
   420|        currency=currency,
   421|

Use offset=421 to continue reading (showing 1-420 of 462 lines)


     1|from __future__ import annotations
     2|
     3|from jarvis_finance.dashboard import data
     4|from jarvis_finance.dashboard.components.tables import show_table
     5|from jarvis_finance.equity.manage import add_equity_initial_snapshot, add_equity_transaction, get_equity_management_options
     6|
     7|PAGE_TITLE = "Equity/ETF Manage"
     8|
     9|
    10|def _account_label(row: dict[str, str]) -> str:
    11|    return f"{row['platform_name']} / {row['account_name']} [{row['currency']} ]"
    12|
    13|
    14|def _instrument_label(row: dict[str, str]) -> str:
    15|    suffix = row.get("isin") or row.get("ticker") or row["instrument_id"]
    16|    return f"{row['name']} ({suffix})"
    17|
    18|
    19|def _selected(rows: list[dict[str, str]], label: str | None, label_fn) -> dict[str, str] | None:
    20|    if not label:
    21|        return None
    22|    for row in rows:
    23|        if label_fn(row) == label:
    24|            return row
    25|    return None
    26|
    27|
    28|def render(st, conn) -> None:
    29|    st.title("Equity/ETF Manage")
    30|    st.caption("Auditierbare manuelle Aktien-/ETF-Erfassung. Dashboard liest lokal; keine Broker-Dateien oder Live-APIs beim Rendern.")
    31|    st.warning("Keine stillen Überschreibungen: Initial Snapshots, Transaktionen und Korrekturen werden als bestätigte Ledger-Ereignisse mit Audit-Log gespeichert.")
    32|
    33|    options = get_equity_management_options(conn)
    34|    account_labels = [_account_label(row) for row in options["accounts"]]
    35|    instrument_labels = [_instrument_label(row) for row in options["instruments"]]
    36|    tabs = st.tabs(["Hinzufügen", "Kaufen", "Verkaufen", "Dividende", "ETF-Ausschüttung", "Fee", "Korrigieren", "Review"])
    37|
    38|    with tabs[0]:
    39|        st.subheader("Aktie/ETF als Initial Snapshot hinzufügen")
    40|        account_label = st.selectbox("Depot/Konto", account_labels, key="equity_add_account") if account_labels else None
    41|        asset_class = st.selectbox("Assetklasse", ["stock", "ETF"], key="equity_add_class")
    42|        name = st.text_input("Name", key="equity_add_name")
    43|        isin = st.text_input("ISIN (primäre Identifikation)", key="equity_add_isin")
    44|        ticker = st.text_input("Ticker optional", key="equity_add_ticker")
    45|        exchange = st.text_input("Exchange optional", key="equity_add_exchange")
    46|        currency = st.selectbox("Währung", ["CHF", "EUR", "USD"], key="equity_add_currency")
    47|        quantity = st.text_input("Menge als Decimal-Text", key="equity_add_qty")
    48|        cost_basis = st.text_input("Einstandspreis / Cost Basis Originalwährung", key="equity_add_cost")
    49|        fx_rate = st.text_input("FX-Kurs zu CHF (bei EUR/USD)", key="equity_add_fx")
    50|        fx_source = st.text_input("FX-Quelle", key="equity_add_fx_source")
    51|        fx_status = st.selectbox("FX-Status", ["ok", "missing"], key="equity_add_fx_status")
    52|        snapshot_date = st.date_input("Snapshot-Datum", key="equity_add_date")
    53|        category = st.selectbox("Kategorie", ["Core", "Opportunity", "Unknown"], key="equity_add_category")
    54|        note = st.text_area("Notiz (Pflicht bei unvollständiger Historie)", key="equity_add_note")
    55|        with st.expander("ETF-spezifische Felder"):
    56|            ter = st.text_input("TER optional", key="equity_add_ter")
    57|            distribution_policy = st.text_input("ausschüttend/thesaurierend optional", key="equity_add_distribution")
    58|            index_name = st.text_input("Index optional", key="equity_add_index")
    59|            fund_domicile = st.text_input("Fondsdomizil optional", key="equity_add_domicile")
    60|            benchmark = st.text_input("Benchmark optional", key="equity_add_benchmark")
    61|        confirm = st.checkbox("Ich bestätige diese einzelne Änderung", key="equity_add_confirm")
    62|        selected_account = _selected(options["accounts"], account_label, _account_label)
    63|        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."})
    64|        if st.button("Bestätigt speichern", key="equity_add_save", type="primary"):
    65|            try:
    66|                if not selected_account:
    67|                    raise ValueError("account is required")
    68|                result = add_equity_initial_snapshot(
    69|                    conn,
    70|                    account_id=selected_account["account_id"],
    71|                    asset_class=asset_class,
    72|                    name=name,
    73|                    isin=isin,
    74|                    ticker=ticker,
    75|                    exchange=exchange,
    76|                    currency=currency,
    77|                    quantity_text=quantity,
    78|                    cost_basis_original_text=cost_basis,
    79|                    snapshot_date=str(snapshot_date),
    80|                    category=category,
    81|                    note=note,
    82|                    fx_rate_to_chf_text=fx_rate or None,
    83|                    fx_source=fx_source or None,
    84|                    fx_status=fx_status,
    85|                    ter_text=ter or None,
    86|                    distribution_policy=distribution_policy or None,
    87|                    index_name=index_name or None,
    88|                    fund_domicile=fund_domicile or None,
    89|                    benchmark=benchmark or None,
    90|                    confirm=bool(confirm),
    91|                )
    92|                st.success("Initial Snapshot gespeichert und auditierbar erfasst.")
    93|                st.write({"instrument_id": result.instrument_id, "transaction_id": result.transaction_id, "warnings": result.warnings})
    94|            except Exception as exc:
    95|                st.error(f"Speichern abgelehnt: {exc}")
    96|
    97|    def transaction_form(tab, title: str, tx_type: str) -> None:
    98|        with tab:
    99|            st.subheader(title)
   100|            account_label = st.selectbox("Depot/Konto", account_labels, key=f"equity_{tx_type}_account") if account_labels else None
   101|            instrument_label = st.selectbox("Instrument", instrument_labels, key=f"equity_{tx_type}_instrument") if instrument_labels else None
   102|            date_value = st.date_input("Datum", key=f"equity_{tx_type}_date")
   103|            qty_default = "0" if tx_type in {"dividend", "etf_distribution", "fee"} else ""
   104|            quantity = st.text_input("Menge als Decimal-Text", value=qty_default, key=f"equity_{tx_type}_qty")
   105|            gross = st.text_input("Brutto/Cost/Proceeds Originalwährung", key=f"equity_{tx_type}_gross")
   106|            currency = st.selectbox("Währung", ["CHF", "EUR", "USD"], key=f"equity_{tx_type}_currency")
   107|            fx_rate = st.text_input("FX-Kurs zu CHF (bei EUR/USD)", key=f"equity_{tx_type}_fx")
   108|            fx_source = st.text_input("FX-Quelle", key=f"equity_{tx_type}_fx_source")
   109|            fx_status = st.selectbox("FX-Status", ["ok", "missing"], key=f"equity_{tx_type}_fx_status")
   110|            note = st.text_area("Notiz / Grund", key=f"equity_{tx_type}_note")
   111|            confirm = st.checkbox("Ich bestätige diese Änderung", key=f"equity_{tx_type}_confirm")
   112|            selected_account = _selected(options["accounts"], account_label, _account_label)
   113|            selected_instrument = _selected(options["instruments"], instrument_label, _instrument_label)
   114|            st.write({"Aktion": tx_type, "Instrument": instrument_label, "Datum": str(date_value), "FX-Status": fx_status})
   115|            if st.button("Bestätigt speichern", key=f"equity_{tx_type}_save"):
   116|                try:
   117|                    if not selected_account or not selected_instrument:
   118|                        raise ValueError("account and instrument are required")
   119|                    result = add_equity_transaction(
   120|                        conn,
   121|                        account_id=selected_account["account_id"],
   122|                        instrument_id=selected_instrument["instrument_id"],
   123|                        transaction_type=tx_type,
   124|                        trade_date=str(date_value),
   125|                        quantity_text=quantity,
   126|                        gross_amount_original_text=gross,
   127|                        currency=currency,
   128|                        fx_rate_to_chf_text=fx_rate or None,
   129|                        fx_source=fx_source or None,
   130|                        fx_status=fx_status,
   131|                        note=note,
   132|                        confirm=bool(confirm),
   133|                    )
   134|                    st.success("Transaktion gespeichert und auditierbar erfasst.")
   135|                    st.write({"transaction_id": result.transaction_id, "warnings": result.warnings})
   136|                except Exception as exc:
   137|                    st.error(f"Speichern abgelehnt: {exc}")
   138|
   139|    transaction_form(tabs[1], "Kauf erfassen", "buy")
   140|    transaction_form(tabs[2], "Verkauf erfassen", "partial_sell")
   141|    transaction_form(tabs[3], "Dividende erfassen", "dividend")
   142|    transaction_form(tabs[4], "ETF-Ausschüttung erfassen", "etf_distribution")
   143|    transaction_form(tabs[5], "Fee erfassen", "fee")
   144|    transaction_form(tabs[6], "Korrektur als manual_adjustment", "manual_adjustment")
   145|
   146|    with tabs[7]:
   147|        st.subheader("Aktuelle Equity/ETF Übersicht")
   148|        show_table(st, data.get_positions(conn))
   149|


     1|from __future__ import annotations
     2|
     3|from decimal import Decimal
     4|
     5|import pytest
     6|
     7|from jarvis_finance.equity.manage import (
     8|    add_equity_initial_snapshot,
     9|    add_equity_transaction,
    10|    get_equity_management_options,
    11|)
    12|from jarvis_finance.ledger.positions import calculate_positions
    13|from jarvis_finance.storage.database import connect_memory
    14|from jarvis_finance.storage.migrations import apply_migrations
    15|
    16|
    17|def setup_conn():
    18|    conn = connect_memory()
    19|    apply_migrations(conn)
    20|    return conn
    21|
    22|
    23|def seed_account(conn, platform_name: str = "Synthetic PostFinance") -> str:
    24|    platform_id = f"platform-{platform_name.lower().replace(' ', '-')}"
    25|    account_id = f"account-{platform_name.lower().replace(' ', '-')}"
    26|    conn.execute(
    27|        "INSERT INTO platforms(platform_id, name, platform_type, country, default_currency, created_at) VALUES (?, ?, 'broker', 'CH', 'CHF', '2026-01-01')",
    28|        (platform_id, platform_name),
    29|    )
    30|    conn.execute(
    31|        "INSERT INTO accounts(account_id, platform_id, account_name, account_type, currency, created_at) VALUES (?, ?, 'Synthetic Depot', 'brokerage', 'CHF', '2026-01-01')",
    32|        (account_id, platform_id),
    33|    )
    34|    conn.commit()
    35|    return account_id
    36|
    37|
    38|def audit_count(conn, action: str) -> int:
    39|    return conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action=?", (action,)).fetchone()["c"]
    40|
    41|
    42|def test_add_stock_initial_snapshot_creates_instrument_transaction_and_audit() -> None:
    43|    conn = setup_conn()
    44|    account_id = seed_account(conn)
    45|
    46|    result = add_equity_initial_snapshot(
    47|        conn,
    48|        account_id=account_id,
    49|        asset_class="stock",
    50|        name="Synthetic Nestle",
    51|        isin="CH0038863350",
    52|        ticker="NESN",
    53|        exchange="SIX",
    54|        currency="CHF",
    55|        quantity_text="3.5",
    56|        cost_basis_original_text="350.00",
    57|        snapshot_date="2025-12-31",
    58|        category="Core",
    59|        note="synthetic incomplete history accepted",
    60|        confirm=True,
    61|    )
    62|
    63|    tx = conn.execute("SELECT * FROM transactions WHERE transaction_id=?", (result.transaction_id,)).fetchone()
    64|    instrument = conn.execute("SELECT * FROM instruments WHERE instrument_id=?", (result.instrument_id,)).fetchone()
    65|    assert tx["transaction_type"] == "initial_position_snapshot"
    66|    assert tx["quantity"] == "3.5"
    67|    assert tx["gross_amount_original"] == "350.00"
    68|    assert tx["fx_rate_to_chf"] == "1"
    69|    assert tx["fx_status"] == "ok"
    70|    assert instrument["isin"] == "CH0038863350"
    71|    assert instrument["asset_class"] == "stock"
    72|    assert instrument["position_category"] == "Core"
    73|    assert audit_count(conn, "equity_initial_position_snapshot") == 1
    74|    assert result.warnings == []
    75|
    76|
    77|def test_add_etf_initial_snapshot_prepares_optional_etf_fields() -> None:
    78|    conn = setup_conn()
    79|    account_id = seed_account(conn, "Synthetic True Wealth")
    80|
    81|    result = add_equity_initial_snapshot(
    82|        conn,
    83|        account_id=account_id,
    84|        asset_class="ETF",
    85|        name="Synthetic World ETF",
    86|        isin="IE00B4L5Y983",
    87|        ticker="SWLD",
    88|        exchange="LSE",
    89|        currency="USD",
    90|        quantity_text="10",
    91|        cost_basis_original_text="1000.00",
    92|        snapshot_date="2025-12-31",
    93|        category="Core",
    94|        note="synthetic ETF snapshot with known FX",
    95|        fx_rate_to_chf_text="0.91",
    96|        fx_source="synthetic fixture",
    97|        ter_text="0.20",
    98|        distribution_policy="accumulating",
    99|        index_name="MSCI World Synthetic",
   100|        fund_domicile="IE",
   101|        benchmark="Synthetic Benchmark",
   102|        confirm=True,
   103|    )
   104|
   105|    instrument = conn.execute("SELECT * FROM instruments WHERE instrument_id=?", (result.instrument_id,)).fetchone()
   106|    tx = conn.execute("SELECT * FROM transactions WHERE transaction_id=?", (result.transaction_id,)).fetchone()
   107|    assert instrument["ter"] == "0.20"
   108|    assert instrument["distribution_policy"] == "accumulating"
   109|    assert instrument["index_name"] == "MSCI World Synthetic"
   110|    assert instrument["fund_domicile"] == "IE"
   111|    assert instrument["benchmark"] == "Synthetic Benchmark"
   112|    assert tx["currency_original"] == "USD"
   113|    assert tx["fx_rate_to_chf"] == "0.91"
   114|    assert tx["gross_amount_chf"] == "910.0000"
   115|
   116|
   117|def test_usd_stock_requires_fx_or_explicit_missing_status_alert() -> None:
   118|    conn = setup_conn()
   119|    account_id = seed_account(conn, "Synthetic Raiffeisen")
   120|
   121|    result = add_equity_initial_snapshot(
   122|        conn,
   123|        account_id=account_id,
   124|        asset_class="stock",
   125|        name="Synthetic US Stock",
   126|        isin="US0378331005",
   127|        ticker="AAPL",
   128|        exchange="NASDAQ",
   129|        currency="USD",
   130|        quantity_text="2",
   131|        cost_basis_original_text="400.00",
   132|        snapshot_date="2025-12-31",
   133|        category="Opportunity",
   134|        note="synthetic missing FX accepted for review",
   135|        fx_status="missing",
   136|        confirm=True,
   137|    )
   138|
   139|    tx = conn.execute("SELECT * FROM transactions WHERE transaction_id=?", (result.transaction_id,)).fetchone()
   140|    assert tx["fx_status"] == "missing"
   141|    assert tx["gross_amount_chf"] is None
   142|    assert "missing_fx" in result.warnings
   143|    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_fx'").fetchone()["c"] == 1
   144|
   145|
   146|def test_sell_reduces_position_and_blocks_negative() -> None:
   147|    conn = setup_conn()
   148|    account_id = seed_account(conn)
   149|    snap = add_equity_initial_snapshot(
   150|        conn,
   151|        account_id=account_id,
   152|        asset_class="stock",
   153|        name="Synthetic Sellable",
   154|        isin="CH0000000001",
   155|        ticker="SYN",
   156|        exchange="SIX",
   157|        currency="CHF",
   158|        quantity_text="5",
   159|        cost_basis_original_text="500",
   160|        snapshot_date="2025-12-31",
   161|        category="Unknown",
   162|        note="synthetic initial",
   163|        confirm=True,
   164|    )
   165|
   166|    add_equity_transaction(
   167|        conn,
   168|        account_id=account_id,
   169|        instrument_id=snap.instrument_id,
   170|        transaction_type="partial_sell",
   171|        trade_date="2026-01-10",
   172|        quantity_text="2",
   173|        gross_amount_original_text="220",
   174|        currency="CHF",
   175|        note="synthetic partial sell",
   176|        confirm=True,
   177|    )
   178|    position = calculate_positions(conn).positions[(account_id, snap.instrument_id)]
   179|    assert position.quantity == Decimal("3")
   180|
   181|    with pytest.raises(ValueError, match="negative"):
   182|        add_equity_transaction(
   183|            conn,
   184|            account_id=account_id,
   185|            instrument_id=snap.instrument_id,
   186|            transaction_type="partial_sell",
   187|            trade_date="2026-01-11",
   188|            quantity_text="10",
   189|            gross_amount_original_text="1000",
   190|            currency="CHF",
   191|            note="synthetic oversell",
   192|            confirm=True,
   193|        )
   194|
   195|
   196|def test_dividend_does_not_change_quantity_and_manual_adjustment_requires_note() -> None:
   197|    conn = setup_conn()
   198|    account_id = seed_account(conn)
   199|    snap = add_equity_initial_snapshot(
   200|        conn,
   201|        account_id=account_id,
   202|        asset_class="ETF",
   203|        name="Synthetic Income ETF",
   204|        isin="IE0000000001",
   205|        ticker="INC",
   206|        exchange="SIX",
   207|        currency="CHF",
   208|        quantity_text="7",
   209|        cost_basis_original_text="700",
   210|        snapshot_date="2025-12-31",
   211|        category="Core",
   212|        note="synthetic initial",
   213|        confirm=True,
   214|    )
   215|
   216|    add_equity_transaction(
   217|        conn,
   218|        account_id=account_id,
   219|        instrument_id=snap.instrument_id,
   220|        transaction_type="dividend",
   221|        trade_date="2026-02-01",
   222|        quantity_text="0",
   223|        gross_amount_original_text="12.50",
   224|        currency="CHF",
   225|        note="synthetic dividend",
   226|        confirm=True,
   227|    )
   228|    assert calculate_positions(conn).positions[(account_id, snap.instrument_id)].quantity == Decimal("7")
   229|
   230|    with pytest.raises(ValueError, match="note"):
   231|        add_equity_transaction(
   232|            conn,
   233|            account_id=account_id,
   234|            instrument_id=snap.instrument_id,
   235|            transaction_type="manual_adjustment",
   236|            trade_date="2026-02-02",
   237|            quantity_text="1",
   238|            gross_amount_original_text="0",
   239|            currency="CHF",
   240|            note="",
   241|            confirm=True,
   242|        )
   243|
   244|
   245|def test_equity_management_options_and_dashboard_page_import() -> None:
   246|    conn = setup_conn()
   247|    seed_account(conn, "Synthetic PostFinance")
   248|    options = get_equity_management_options(conn)
   249|    assert options["accounts"]
   250|    from jarvis_finance.dashboard.pages import PAGES
   251|
   252|    assert "Equity/ETF Manage" in [page.PAGE_TITLE for page in PAGES]
   253|


Explanation:
**Organizing tasks for clarity**

I’m thinking I might need to create some documentation or a backlog for better organization. It feels like I should also run full tests on the "full_sell" functionality. Plus, adding a new page tab seems like a good idea. I need to read through the rest of my management tasks to ensure everything's in order for an audit. It’s important to stay on top of these details!


   421|        fx_rate_to_chf_text=fx_rate_to_chf_text,
   422|        fx_source=fx_source,
   423|        fx_status=fx_status,
   424|        note=note,
   425|    )
   426|    record_audit_event(
   427|        conn,
   428|        source="equity_manage",
   429|        action=f"equity_{transaction_type}",
   430|        entity_type="transaction",
   431|        entity_id=tx_id,
   432|        new_values={"account_id": account_id, "instrument_id": instrument_id, "transaction_type": transaction_type},
   433|        user_text_note=note,
   434|        confirmed=True,
   435|        created_by="dashboard",
   436|    )
   437|    conn.commit()
   438|    return EquityManageResult(instrument_id=instrument_id, account_id=account_id, transaction_id=tx_id, warnings=warnings)
   439|
   440|
   441|def get_equity_management_options(conn: Connection) -> dict[str, list[dict[str, str]]]:
   442|    accounts = [
   443|        {
   444|            "account_id": row["account_id"],
   445|            "account_name": row["account_name"],
   446|            "platform_name": row["platform_name"],
   447|            "currency": row["currency"],
   448|        }
   449|        for row in conn.execute(
   450|            """
   451|            SELECT a.account_id, a.account_name, p.name AS platform_name, a.currency
   452|            FROM accounts a JOIN platforms p ON p.platform_id=a.platform_id
   453|            WHERE a.is_active=1
   454|            ORDER BY p.name, a.account_name
   455|            """
   456|        ).fetchall()
   457|    ]
   458|    instruments = [
   459|        {"instrument_id": row["instrument_id"], "name": row["name"], "isin": row["isin"] or "", "ticker": row["ticker"] or "", "currency": row["currency"]}
   460|        for row in conn.execute("SELECT instrument_id, name, isin, ticker, currency FROM instruments WHERE is_active=1 ORDER BY name").fetchall()
   461|    ]
   462|    return {"accounts": accounts, "instruments": instruments}
   463|


Explanation:
**Considering schema and documentation**

I’m thinking I might need a schema text for this. It seems I should also add the full sell UI and test the action. Plus, it would be good to create a backlog document, possibly called docs/finance_backlog.md. I’ll need to use the write_file function to get that done. There’s a lot to juggle here, but I want to make sure I keep everything organized and functional!

patch

tabs = st.tabs(["Hinzufügen", "Kaufen", "Partial Sell", "Full Sell", "Dividende", "ETF-Ausschüttung", "Fee", "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", "ETF-Ausschüttung", "Fee", "Korrigieren", "Review"])
+    tabs = st.tabs(["Hinzufügen", "Kaufen", "Partial Sell", "Full Sell", "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], "Teilverkauf erfassen", "partial_sell")
    transaction_form(tabs[3], "Vollverkauf erfassen", "full_sell")
    transaction_form(tabs[4], "Dividende erfassen", "dividend")
    transaction_form(tabs[5], "ETF-Ausschüttung erfassen", "etf_distribution")
    transaction_form(tabs[6], "Fee erfassen", "fee")
    transaction_form(tabs[7], "Korrektur als manual_adjustment", "manual_adjustment")

    with tabs[8]:

    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]:


--- 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
@@ -137,12 +137,13 @@
                     st.error(f"Speichern abgelehnt: {exc}")

     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")
+    transaction_form(tabs[2], "Teilverkauf erfassen", "partial_sell")
+    transaction_form(tabs[3], "Vollverkauf erfassen", "full_sell")
+    transaction_form(tabs[4], "Dividende erfassen", "dividend")
+    transaction_form(tabs[5], "ETF-Ausschüttung erfassen", "etf_distribution")
+    transaction_form(tabs[6], "Fee erfassen", "fee")
+    transaction_form(tabs[7], "Korrektur als manual_adjustment", "manual_adjustment")

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


from pathlib import Path
p=Path('/home/agent/.hermes/repos/FinanceManager/tests/unit/test_equity_manage.py')
s=p.read_text()
insert='''\n\ndef test_full_sell_requires_exact_current_quantity_and_zeros_position() -> None:\n    conn = setup_conn()\n    account_id = seed_account(conn)\n    snap = add_equity_initial_snapshot(\n        conn,\n        account_id=account_id,\n        asset_class="stock",\n        name="Synthetic Full Sell",\n        isin="CH0000000002",\n        ticker="FUL",\n        exchange="SIX",\n        currency="CHF",\n        quantity_text="4",\n        cost_basis_original_text="400",\n        snapshot_date="2025-12-31",\n        category="Unknown",\n        note="synthetic initial",\n        confirm=True,\n    )\n\n    with pytest.raises(ValueError, match="full_sell quantity"):\n        add_equity_transaction(\n            conn,\n            account_id=account_id,\n            instrument_id=snap.instrument_id,\n            transaction_type="full_sell",\n            trade_date="2026-01-10",\n            quantity_text="3",\n            gross_amount_original_text="330",\n            currency="CHF",\n            note="synthetic wrong full sell",\n            confirm=True,\n        )\n\n    add_equity_transaction(\n        conn,\n        account_id=account_id,\n        instrument_id=snap.instrument_id,\n        transaction_type="full_sell",\n        trade_date="2026-01-11",\n        quantity_text="4",\n        gross_amount_original_text="440",\n        currency="CHF",\n        note="synthetic full sell",\n        confirm=True,\n    )\n\n    position = calculate_positions(conn).positions[(account_id, snap.instrument_id)]\n    assert position.quantity == Decimal("0")\n    assert audit_count(conn, "equity_full_sell") == 1\n'''
# insert before management_options test
s=s.replace('\n\ndef test_equity_management_options_and_dashboard_page_import() -> None:', insert+'\n\ndef test_equity_management_options_and_dashboard_page_import() -> None:')
p.write_text(s)
print('inserted')


Output: inserted


# FinanceManager Backlog

This backlog records approved future modules that are intentionally not built in the current Equity/ETF Manage UI task.

## Portfolio Health page

- Total value in CHF.
- Asset classes.
- Platforms.
- Data quality.
- Cash quota.
- Missing prices and FX.

## Reconciliation / Abgleich page

- Dashboard holdings vs. last snapshot.
- Manual review marker: holding checked on date X.
- Make differences visible.

## Data Quality Center

- Missing prices.
- Stale prices.
- Missing FX.
- Missing ISIN.
- Missing CoinGecko ID.
- Incomplete history.

## Manual Review Queue

- Skipped coins.
- Uncertain mappings.
- Open alerts.
- Open data-quality problems.

## Backup / Restore

- Runtime DB backup.
- Optional encrypted backup.
- Restore test later.

## Safe Mode / Read-only Mode

- Dashboard read-only mode.
- Explicit enablement for write actions.
- Guard rails against accidental changes.


No linter for .md files


terminal

pytest tests/unit/test_equity_manage.py -q && pytest -q && rm -rf .pytest_cache src/jarvis_finance/**/__pycache__ tests/**/__pycache__ && PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan . && git status --short --branch


Output:
.......                                                                  [100%]
7 passed in 0.09s
........................................................................ [ 65%]
......................................                                   [100%]
110 passed in 0.98s
GIT_SAFETY_OK
## main...origin/main [voraus 1]
 M src/jarvis_finance/dashboard/pages/page_13_equity_manage.py
 M tests/unit/test_equity_manage.py
?? docs/finance-backlog.md


Explanation:
**Considering code commit**

I need to commit Teil3, but I'm wondering if I should run the code first. It seems like a good idea to check that everything works as expected before I commit. I don't want to risk any issues later on, so I'm thinking maybe I should take that extra step. It’s always better to be safe than sorry! Let's run the code and see what happens before moving forward with that commit.

git add src/jarvis_finance/dashboard/pages/page_13_equity_manage.py tests/unit/test_equity_manage.py docs/finance-backlog.md && git commit -m "feat: extend equity ETF management UI" && git status --short --branch


Output:
[main 943c424] feat: extend equity ETF management UI
 3 files changed, 106 insertions(+), 7 deletions(-)
 create mode 100644 docs/finance-backlog.md
## main...origin/main [voraus 2]


Explanation:
**Verifying git status**

I need to check the final verification status, but it seems the user hasn't specifically asked for a push. Maybe updating the to-do list is necessary. I should remember that my git status shows clean and ahead, which indicates everything looks good right now. It’s always good to be thorough, you know? I'll make sure to communicate clearly about what I find!

rm -rf .pytest_cache src/jarvis_finance/**/__pycache__ tests/**/__pycache__ && pytest -q && PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan . && git status --short --branch && PYTHONPATH=src python - <<'PY'
from jarvis_finance.config.settings import load_settings
from jarvis_finance.storage.database import connect
from jarvis_finance.storage.migrations import apply_migrations
from jarvis_finance.market.providers import latest_crypto_price_row, is_price_stale
s=load_settings(); c=connect(s.db_path); apply_migrations(c)
assets=c.execute('SELECT asset_id FROM crypto_assets WHERE is_active=1').fetchall()
fresh=missing=warnings=errors=0
for a in assets:
    r=latest_crypto_price_row(c,a['asset_id'],'CHF')
    if r and r['quality_status']=='fresh' and r['price'] not in (None,'') and not is_price_stale(r['fetched_at'], max_age_seconds=86400): fresh+=1
    else: missing+=1
    if r and r['quality_status'] in ('missing','stale','conflict'): warnings+=1
    if r and r['quality_status']=='error': errors+=1
asset=c.execute("SELECT asset_id FROM crypto_assets WHERE coin_name='Bitcoin' AND symbol='BTC' AND coingecko_id='bitcoin'").fetchone()
asset_id=asset['asset_id'] if asset else None
holdings=c.execute('SELECT * FROM crypto_holdings WHERE asset_id=?',(asset_id,)).fetchall() if asset_id else []
audit=sum(c.execute("SELECT COUNT(*) n FROM audit_log WHERE entity_type='crypto_holding' AND entity_id=?",(h['crypto_holding_id'],)).fetchone()['n'] for h in holdings)
dupes=c.execute('SELECT COUNT(*) n FROM (SELECT asset_id,wallet_id,COUNT(*) c FROM crypto_holdings GROUP BY asset_id,wallet_id HAVING c>1)').fetchone()['n']
print(f'FINAL_PRICE_AGGREGATE total_assets={len(assets)} fresh_chf={fresh} missing_fresh_chf={missing} warnings={warnings} errors={errors}')
print(f'FINAL_BTC_AGGREGATE holdings={len(holdings)} audit_logs_present={audit >= len(holdings) and len(holdings)>0} duplicate_pairs={dupes}')
PY


Output:
........................................................................ [ 65%]
......................................                                   [100%]
110 passed in 1.02s
.pytest_cache/CACHEDIR.TAG: runtime_cache_file_blocked
.pytest_cache/README.md: runtime_cache_file_blocked
.pytest_cache/.gitignore: runtime_cache_file_blocked
.pytest_cache/v/cache/nodeids: runtime_cache_file_blocked


pytest -q && rm -rf .pytest_cache src/jarvis_finance/**/__pycache__ tests/**/__pycache__ && PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan . && git status --short --branch && PYTHONPATH=src python - <<'PY'
from jarvis_finance.config.settings import load_settings
from jarvis_finance.storage.database import connect
from jarvis_finance.storage.migrations import apply_migrations
from jarvis_finance.market.providers import latest_crypto_price_row, is_price_stale
s=load_settings(); c=connect(s.db_path); apply_migrations(c)
assets=c.execute('SELECT asset_id FROM crypto_assets WHERE is_active=1').fetchall()
fresh=missing=warnings=errors=0
for a in assets:
    r=latest_crypto_price_row(c,a['asset_id'],'CHF')
    if r and r['quality_status']=='fresh' and r['price'] not in (None,'') and not is_price_stale(r['fetched_at'], max_age_seconds=86400): fresh+=1
    else: missing+=1
    if r and r['quality_status'] in ('missing','stale','conflict'): warnings+=1
    if r and r['quality_status']=='error': errors+=1
asset=c.execute("SELECT asset_id FROM crypto_assets WHERE coin_name='Bitcoin' AND symbol='BTC' AND coingecko_id='bitcoin'").fetchone()
asset_id=asset['asset_id'] if asset else None
holdings=c.execute('SELECT * FROM crypto_holdings WHERE asset_id=?',(asset_id,)).fetchall() if asset_id else []
audit=sum(c.execute("SELECT COUNT(*) n FROM audit_log WHERE entity_type='crypto_holding' AND entity_id=?",(h['crypto_holding_id'],)).fetchone()['n'] for h in holdings)
dupes=c.execute('SELECT COUNT(*) n FROM (SELECT asset_id,wallet_id,COUNT(*) c FROM crypto_holdings GROUP BY asset_id,wallet_id HAVING c>1)').fetchone()['n']
print(f'FINAL_PRICE_AGGREGATE total_assets={len(assets)} fresh_chf={fresh} missing_fresh_chf={missing} warnings={warnings} errors={errors}')
print(f'FINAL_BTC_AGGREGATE holdings={len(holdings)} audit_logs_present={audit >= len(holdings) and len(holdings)>0} duplicate_pairs={dupes}')
PY


Output:
........................................................................ [ 65%]
......................................                                   [100%]
110 passed in 0.85s
GIT_SAFETY_OK
## main...origin/main [voraus 2]
FINAL_PRICE_AGGREGATE total_assets=20 fresh_chf=20 missing_fresh_chf=0 warnings=0 errors=0
FINAL_BTC_AGGREGATE holdings=2 audit_logs_present=True duplicate_pairs=0


todo

Teil 1: Crypto Price Update robuster machen, Tests ergänzen, produktiv vorsichtig ausführen

Teil 2: BTC-Test aus echter Excel mit Git-Safety/Audit/aggregate-only Bericht nachholen

Teil 3: Equity/ETF Manage UI synthetisch beginnen; Backlog vormerken


Erledigt, Sir.

## Teil 1 — Crypto Price Update robuster gemacht

Implementiert in Commit `1b5028d` — `feat: harden crypto price refresh`

Umgesetzt:

- CoinGecko Batch-Abfrage über `/simple/price?ids=...`
- kein API-Call pro Coin mehr, sofern Batch möglich ist
- konservatives Retry-/Backoff-Verhalten:
  - `--sleep-seconds`
  - `--max-retries`
  - `--initial-backoff`
  - `--max-backoff`
- CLI-Optionen ergänzt:
  - `--only-missing`
  - `--only-stale`
  - `--only-symbol SYMBOL`
  - `--limit N`
  - `--dry-run`
  - `--currency CHF`
- Summary jetzt aggregiert:
  - `total_assets`
  - `updated`
  - `cached`
  - `skipped`
  - `stale`
  - `warnings`
  - `errors`
  - `assets_still_missing_local_price`
- Data-Quality-Alert für Assets ohne frischen lokalen Preis
- Alert-Dedupe respektiert
- Tests ergänzt für:
  - Batch-Request
  - `--only-missing`
  - `--only-stale`
  - `--only-symbol`
  - `--limit`
  - `--dry-run`
  - 429 / Backoff
  - Alert-Dedupe bei fehlenden Preisen

Produktiver vorsichtiger Lauf gegen Runtime-DB:

- Total Assets: `20`
- Assets mit frischem CHF-Preis: `20`
- Weiterhin ohne frischen CHF-Preis: `0`
- Warnings: `0`
- Errors: `0`

Keine Preise, Werte oder Mengen ausgegeben. Man möchte ja nicht versehentlich Bloomberg Terminal spielen.

## Teil 2 — BTC-Test nachgeholt / verifiziert

Der BTC-Test wurde aggregate-only gegen die echte Runtime-DB und die echte Excel-Datei geprüft.

Ergebnis:

- BTC-Test durchgeführt: `ja`
- BTC-Wallet-Holdings aus Excel erkannt/gematcht: `2`
- BTC-Asset vorhanden mit:
  - Coin-Name: `Bitcoin`
  - Symbol: `BTC`
  - CoinGecko-ID: `bitcoin`
- Asset-Anzahl erhöht: `nein`
  - BTC-Asset war bereits vorhanden.
- Holding-Anzahl erhöht: `nein`
  - Die passenden BTC-Holdings waren bereits vorhanden.
  - Kein erneuter Write, um `asset_id + wallet_id`-Dubletten zu vermeiden.
- Audit-Logs erstellt/vorhanden: `ja`
- Doppelte Holdings: `nein`
- Legacy-CHF-Werte ignoriert/verifiziert: `ja`
- Snapshot-Datum verifiziert: `31.12.2025`
- Mengen als Text/Decimal geprüft: `ja`
- Fehler/Warnings: `0`
- Git-Safety vor/nachher: `GIT_SAFETY_OK`

Wichtig: Ich habe keinen Duplikat-Import erzwungen. Die bereits vorhandenen BTC-Holdings erfüllen die Bedingungen; nochmaliges Einfügen wäre schlechter Datenschutz mit Datenbank-Beigeschmack.

## Teil 3 — Equity/ETF Manage UI begonnen/erweitert

Implementiert in Commit `943c424` — `feat: extend equity ETF management UI`

Status:

- Equity/ETF Manage UI ist vorhanden und wurde erweitert.
- Full-Sell-Tab ergänzt.
- Full-Sell-Test ergänzt:
  - Full sell muss exakt aktuelle Position verkaufen.
  - Danach Position auf null.
  - Audit-Log Pflicht geprüft.
- Bestehende UI/Logik unterstützt synthetisch:
  - Initial Snapshot
  - Buy
  - Partial Sell
  - Full Sell
  - Dividend
  - ETF Distribution
  - Fee
  - Manual Adjustment
  - ISIN/Ticker-Validierung
  - Währung Pflicht
  - Depot/Konto Pflicht
  - FX bei Fremdwährung oder expliziter Missing-FX-Alert
  - Audit-Log
  - Notizpflicht bei Manual Adjustment / unvollständiger Historie

Keine echten PostFinance-, True-Wealth- oder Raiffeisen-Daten importiert.

Backlog vorgemerkt in:

- `docs/finance-backlog.md`

Enthalten:

- Portfolio Health
- Reconciliation / Abgleich
- Data Quality Center
- Manual Review Queue
- Backup / Restore
- Safe Mode / Read-only Mode

## Verifikation

Final ausgeführt:

- Tests: `110 passed`
- Git-Safety: `GIT_SAFETY_OK`
- Git Status: clean, aber lokal `2` Commits ahead of `origin/main`

Commits:

- `1b5028d` — Crypto Price Refresh gehärtet
- `943c424` — Equity/ETF Manage UI erweitert + Backlog dokumentiert