terminal

Output:
........................................................................ [ 28%]
........................................................................ [ 56%]
....................................F................................... [ 84%]
....................F.........F..........                                [100%]
=================================== FAILURES ===================================
___________________ test_dashboard_renders_without_live_api ____________________

    def test_dashboard_renders_without_live_api() -> None:
        conn = setup_conn(); add_catalog_entries(conn)
>       page_13_equity_manage.render(FakeSt(), conn)

tests/unit/test_instrument_catalog_manual_add.py:130:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

st = <test_instrument_catalog_manual_add.FakeSt object at 0x7cc2a7ccde10>
conn = <sqlite3.Connection object at 0x7cc2a7e9f4c0>

    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(["Instrument Search / Add Position", "Mapping Review Queue", "Hinzufügen", "Kaufen", "Partial Sell", "Full Sell", "Dividende", "ETF-Ausschüttung", "Fee", "Korrigieren", "Review"])

        with tabs[0]:
            st.subheader("Aktie/ETF hinzufügen")
            st.caption("Wizard: 1 Suchen → 2 Instrument auswählen → 3 Depot & Menge → 4 Einstand/Datum/Notiz → 5 Review → 6 Bestätigen.")
            st.info("DOCX-Brokerimporte sind hier bewusst nicht der Hauptpfad. Es wird nur auf Klick gesucht; kein Provider-/Live-API-Call beim Rendern.")
            ensure_standard_broker_accounts(conn)

>           st.markdown("### 1. Instrument suchen")
            ^^^^^^^^^^^
E           AttributeError: 'FakeSt' object has no attribute 'markdown'

src/jarvis_finance/dashboard/views/page_13_equity_manage.py:49: AttributeError
______________ test_dashboard_review_ui_renders_without_live_api _______________

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

tests/unit/test_true_wealth_mapping_candidates_c3.py:192:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

st = <test_true_wealth_mapping_candidates_c3.FakeSt object at 0x7cc2a7d312d0>
conn = <sqlite3.Connection object at 0x7cc2a7cbc4f0>

    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(["Instrument Search / Add Position", "Mapping Review Queue", "Hinzufügen", "Kaufen", "Partial Sell", "Full Sell", "Dividende", "ETF-Ausschüttung", "Fee", "Korrigieren", "Review"])

        with tabs[0]:
            st.subheader("Aktie/ETF hinzufügen")
            st.caption("Wizard: 1 Suchen → 2 Instrument auswählen → 3 Depot & Menge → 4 Einstand/Datum/Notiz → 5 Review → 6 Bestätigen.")
            st.info("DOCX-Brokerimporte sind hier bewusst nicht der Hauptpfad. Es wird nur auf Klick gesucht; kein Provider-/Live-API-Call beim Rendern.")
            ensure_standard_broker_accounts(conn)

>           st.markdown("### 1. Instrument suchen")
            ^^^^^^^^^^^
E           AttributeError: 'FakeSt' object has no attribute 'markdown'

src/jarvis_finance/dashboard/views/page_13_equity_manage.py:49: AttributeError
_____ test_dashboard_review_ui_renders_ranked_candidates_without_live_api ______

    def test_dashboard_review_ui_renders_ranked_candidates_without_live_api() -> None:
        conn = setup_conn()
        provider = StaticLookupProvider({"CH0000000001": [ProviderCandidate(provider="openfigi", provider_symbol="GETF.SW", exchange="SWX", currency="CHF", isin="CH0000000001")]})
        generate_mapping_candidates(conn, provider=provider)
        recalculate_candidate_rankings(conn)
>       page_13_equity_manage.render(FakeSt(), conn)

tests/unit/test_true_wealth_mapping_candidates_c3d.py:146:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

st = <test_true_wealth_mapping_candidates_c3d.FakeSt object at 0x7cc2a7d13550>
conn = <sqlite3.Connection object at 0x7cc2a7cbdc60>

    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(["Instrument Search / Add Position", "Mapping Review Queue", "Hinzufügen", "Kaufen", "Partial Sell", "Full Sell", "Dividende", "ETF-Ausschüttung", "Fee", "Korrigieren", "Review"])

        with tabs[0]:
            st.subheader("Aktie/ETF hinzufügen")
            st.caption("Wizard: 1 Suchen → 2 Instrument auswählen → 3 Depot & Menge → 4 Einstand/Datum/Notiz → 5 Review → 6 Bestätigen.")
            st.info("DOCX-Brokerimporte sind hier bewusst nicht der Hauptpfad. Es wird nur auf Klick gesucht; kein Provider-/Live-API-Call beim Rendern.")
            ensure_standard_broker_accounts(conn)

>           st.markdown("### 1. Instrument suchen")
            ^^^^^^^^^^^
E           AttributeError: 'FakeSt' object has no attribute 'markdown'

src/jarvis_finance/dashboard/views/page_13_equity_manage.py:49: AttributeError
=========================== short test summary info ============================
FAILED tests/unit/test_instrument_catalog_manual_add.py::test_dashboard_renders_without_live_api
FAILED tests/unit/test_true_wealth_mapping_candidates_c3.py::test_dashboard_review_ui_renders_without_live_api
FAILED tests/unit/test_true_wealth_mapping_candidates_c3d.py::test_dashboard_review_ui_renders_ranked_candidates_without_live_api
3 failed, 254 passed in 16.41s


Explanation:
**Considering formatting options**

I need to address that the fake lacks markdown. I'm thinking of replacing markdown with a subheader or caption instead. It might be more supported that way. Then there's the thought of columns too; I should check if FakeSt supports columns. Overall, it feels like I have a few adjustments to make to ensure that everything looks and formats as intended. Let’s explore those options!


     1|from __future__ import annotations
     2|
     3|import pytest
     4|
     5|from jarvis_finance.dashboard.pages import page_13_equity_manage
     6|from jarvis_finance.equity.manage import add_manual_position_from_catalog, create_manual_catalog_entry
     7|from jarvis_finance.market_data.catalog import (
     8|    CatalogEntryInput,
     9|    UnavailableCatalogLookupProvider,
    10|    search_instruments,
    11|    search_local_catalog,
    12|    upsert_catalog_entry,
    13|)
    14|from jarvis_finance.storage.database import connect_memory
    15|from jarvis_finance.storage.migrations import apply_migrations
    16|
    17|
    18|class FakeTab:
    19|    def __enter__(self): return self
    20|    def __exit__(self, *args): return False
    21|
    22|
    23|class FakeSt:
    24|    def title(self, *args, **kwargs): pass
    25|    def caption(self, *args, **kwargs): pass
    26|    def warning(self, *args, **kwargs): pass
    27|    def subheader(self, *args, **kwargs): pass
    28|    def write(self, *args, **kwargs): pass
    29|    def success(self, *args, **kwargs): pass
    30|    def error(self, *args, **kwargs): pass
    31|    def info(self, *args, **kwargs): pass
    32|    def divider(self, *args, **kwargs): pass
    33|    def dataframe(self, *args, **kwargs): pass
    34|    def table(self, *args, **kwargs): pass
    35|    def tabs(self, labels): return [FakeTab() for _ in labels]
    36|    def selectbox(self, label, options, **kwargs): return options[0] if options else None
    37|    def text_input(self, *args, **kwargs): return kwargs.get("value", "")
    38|    def text_area(self, *args, **kwargs): return ""
    39|    def date_input(self, *args, **kwargs): return "2025-12-31"
    40|    def checkbox(self, *args, **kwargs): return False
    41|    def expander(self, *args, **kwargs): return FakeTab()
    42|    def button(self, *args, **kwargs): return False
    43|
    44|
    45|def setup_conn():
    46|    conn = connect_memory()
    47|    apply_migrations(conn)
    48|    conn.execute("INSERT INTO platforms(platform_id,name,platform_type,default_currency,created_at) VALUES('p1','Manual Broker','broker','CHF','now')")
    49|    conn.execute("INSERT INTO accounts(account_id,platform_id,account_name,account_type,currency,created_at) VALUES('a1','p1','Depot','brokerage','CHF','now')")
    50|    return conn
    51|
    52|
    53|def add_catalog_entries(conn):
    54|    first = upsert_catalog_entry(conn, CatalogEntryInput(asset_class='etf', name='Sample ETF CHF', isin='CH0000000001', ticker='SAMP', exchange='SWX', trading_currency='CHF', instrument_currency='CHF', provider='manual', provider_symbol='SAMP.SW', hedge_status='unknown', instrument_status='unknown', source_confidence='high'), note='test catalog entry')
    55|    second = upsert_catalog_entry(conn, CatalogEntryInput(asset_class='etf', name='Sample ETF EUR', isin='CH0000000001', ticker='SAMP', exchange='XETRA', trading_currency='EUR', instrument_currency='EUR', provider='manual', provider_symbol='SAMP.DE', hedge_status='unhedged', instrument_status='active', source_confidence='high'), note='test catalog entry')
    56|    return first, second
    57|
    58|
    59|def test_local_instrument_search_by_isin() -> None:
    60|    conn = setup_conn(); add_catalog_entries(conn)
    61|    rows = search_local_catalog(conn, 'CH0000000001', 'etf')
    62|    assert len(rows) == 2
    63|    assert {r.exchange for r in rows} == {'SWX', 'XETRA'}
    64|
    65|
    66|def test_local_instrument_search_by_symbol() -> None:
    67|    conn = setup_conn(); add_catalog_entries(conn)
    68|    rows = search_local_catalog(conn, 'SAMP')
    69|    assert len(rows) == 2
    70|
    71|
    72|def test_multiple_listings_same_isin_are_kept() -> None:
    73|    conn = setup_conn(); add_catalog_entries(conn)
    74|    count = conn.execute("SELECT COUNT(*) AS c FROM instrument_catalog_entries WHERE isin='CH0000000001'").fetchone()['c']
    75|    assert count == 2
    76|
    77|
    78|def test_symbol_without_exchange_is_not_unique_auto_mapping() -> None:
    79|    conn = setup_conn()
    80|    upsert_catalog_entry(conn, CatalogEntryInput(asset_class='etf', name='No Exchange ETF', isin='CH1', ticker='AMB', trading_currency='CHF', instrument_currency='CHF'), note='test')
    81|    row = search_local_catalog(conn, 'AMB')[0]
    82|    assert row.exchange is None
    83|    assert row.confidence == 'low'
    84|
    85|
    86|def test_manual_candidate_selection_writes_audit() -> None:
    87|    conn = setup_conn(); cid, _ = add_catalog_entries(conn)
    88|    result = add_manual_position_from_catalog(conn, catalog_entry_id=cid, account_id='a1', position_type='initial_snapshot', quantity_text='1.23', trade_date='2025-12-31', currency='CHF', cost_basis_original_text='100', fx_status='not_needed', note='manual selected candidate', confirm=True)
    89|    assert result.transaction_id
    90|    assert conn.execute("SELECT COUNT(*) AS c FROM audit_log WHERE action='manual_position_add'").fetchone()['c'] == 1
    91|
    92|
    93|def test_missing_provider_symbol_creates_data_quality_hint() -> None:
    94|    conn = setup_conn()
    95|    cid = create_manual_catalog_entry(conn, asset_class='etf', name='No Provider ETF', isin='CH2', ticker='NOP', exchange='SWX', currency='CHF', provider_symbol='', note='manual catalog')
    96|    add_manual_position_from_catalog(conn, catalog_entry_id=cid, account_id='a1', position_type='initial_snapshot', quantity_text='1', trade_date='2025-12-31', currency='CHF', cost_basis_original_text='10', fx_status='not_needed', note='manual add', confirm=True)
    97|    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_provider_symbol' AND status='active'").fetchone()['c'] == 1
    98|
    99|
   100|def test_hedge_status_unknown_creates_warning() -> None:
   101|    conn = setup_conn(); cid, _ = add_catalog_entries(conn)
   102|    add_manual_position_from_catalog(conn, catalog_entry_id=cid, account_id='a1', position_type='initial_snapshot', quantity_text='1', trade_date='2025-12-31', currency='CHF', cost_basis_original_text='10', fx_status='not_needed', note='manual add', confirm=True)
   103|    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='hedge_status_unknown' AND status='active'").fetchone()['c'] == 1
   104|
   105|
   106|def test_manual_position_initial_snapshot_creates_ledger_transaction() -> None:
   107|    conn = setup_conn(); cid, _ = add_catalog_entries(conn)
   108|    add_manual_position_from_catalog(conn, catalog_entry_id=cid, account_id='a1', position_type='initial_snapshot', quantity_text='1.2345', trade_date='2025-12-31', currency='CHF', cost_basis_original_text='10', fx_status='not_needed', note='manual add', confirm=True)
   109|    row = conn.execute("SELECT quantity, transaction_type FROM transactions").fetchone()
   110|    assert row['quantity'] == '1.2345'
   111|    assert row['transaction_type'] == 'initial_position_snapshot'
   112|
   113|
   114|def test_cost_basis_missing_alert() -> None:
   115|    conn = setup_conn(); cid, _ = add_catalog_entries(conn)
   116|    result = add_manual_position_from_catalog(conn, catalog_entry_id=cid, account_id='a1', position_type='initial_snapshot', quantity_text='1', trade_date='2025-12-31', currency='CHF', cost_basis_original_text=None, fx_status='not_needed', note='manual add missing cost', confirm=True)
   117|    assert 'cost_basis_uncertain' in result.warnings
   118|    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='cost_basis_uncertain' AND status='active'").fetchone()['c'] == 1
   119|
   120|
   121|def test_fx_missing_alert() -> None:
   122|    conn = setup_conn(); _, cid = add_catalog_entries(conn)
   123|    result = add_manual_position_from_catalog(conn, catalog_entry_id=cid, account_id='a1', position_type='initial_snapshot', quantity_text='1', trade_date='2025-12-31', currency='EUR', cost_basis_original_text='10', fx_status='missing', note='manual add missing fx', confirm=True)
   124|    assert 'missing_fx' in result.warnings
   125|    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='missing_fx' AND status='active'").fetchone()['c'] == 1
   126|
   127|
   128|def test_dashboard_renders_without_live_api() -> None:
   129|    conn = setup_conn(); add_catalog_entries(conn)
   130|    page_13_equity_manage.render(FakeSt(), conn)
   131|
   132|
   133|def test_provider_lookup_failure_does_not_crash() -> None:
   134|    conn = setup_conn()
   135|    results, warnings = search_instruments(conn, 'ABC', providers=[UnavailableCatalogLookupProvider()])
   136|    assert results == []
   137|    assert 'provider_lookup_unavailable' in warnings
   138|    assert conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE rule_id='provider_lookup_unavailable'").fetchone()['c'] == 1
   139|


patch

        st.subheader("1. Instrument suchen")
        query = st.text_input("ISIN, Name oder Ticker", key="wizard_search_query")
        class_filter = st.selectbox("Assetklasse", ["", "stock", "etf"], key="wizard_search_class")
        currency_filter = st.selectbox("Währung", ["", "CHF", "EUR", "USD"], key="wizard_search_currency")
        exchange_filter = st.text_input("Börse/Exchange optional", key="wizard_search_exchange")
        country_filter = st.text_input("Land optional", key="wizard_search_country")
        provider_filter = st.text_input("Provider optional", key="wizard_search_provider")
        external_lookup = st.checkbox("Provider-Suche explizit versuchen", key="wizard_search_external")

        st.markdown("### 1. Instrument suchen")
        query = st.text_input("ISIN, Name oder Ticker", key="wizard_search_query")
        c1, c2, c3 = st.columns(3)
        with c1:
            class_filter = st.selectbox("Assetklasse", ["", "stock", "etf"], key="wizard_search_class")
            currency_filter = st.selectbox("Währung", ["", "CHF", "EUR", "USD"], key="wizard_search_currency")
        with c2:
            exchange_filter = st.text_input("Börse/Exchange optional", key="wizard_search_exchange")
            country_filter = st.text_input("Land optional", key="wizard_search_country")
        with c3:
            provider_filter = st.text_input("Provider optional", key="wizard_search_provider")
            external_lookup = st.checkbox("Provider-Suche explizit versuchen", key="wizard_search_external")


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
@@ -46,18 +46,14 @@
         st.info("DOCX-Brokerimporte sind hier bewusst nicht der Hauptpfad. Es wird nur auf Klick gesucht; kein Provider-/Live-API-Call beim Rendern.")
         ensure_standard_broker_accounts(conn)

-        st.markdown("### 1. Instrument suchen")
+        st.subheader("1. Instrument suchen")
         query = st.text_input("ISIN, Name oder Ticker", key="wizard_search_query")
-        c1, c2, c3 = st.columns(3)
-        with c1:
-            class_filter = st.selectbox("Assetklasse", ["", "stock", "etf"], key="wizard_search_class")
-            currency_filter = st.selectbox("Währung", ["", "CHF", "EUR", "USD"], key="wizard_search_currency")
-        with c2:
-            exchange_filter = st.text_input("Börse/Exchange optional", key="wizard_search_exchange")
-            country_filter = st.text_input("Land optional", key="wizard_search_country")
-        with c3:
-            provider_filter = st.text_input("Provider optional", key="wizard_search_provider")
-            external_lookup = st.checkbox("Provider-Suche explizit versuchen", key="wizard_search_external")
+        class_filter = st.selectbox("Assetklasse", ["", "stock", "etf"], key="wizard_search_class")
+        currency_filter = st.selectbox("Währung", ["", "CHF", "EUR", "USD"], key="wizard_search_currency")
+        exchange_filter = st.text_input("Börse/Exchange optional", key="wizard_search_exchange")
+        country_filter = st.text_input("Land optional", key="wizard_search_country")
+        provider_filter = st.text_input("Provider optional", key="wizard_search_provider")
+        external_lookup = st.checkbox("Provider-Suche explizit versuchen", key="wizard_search_external")
         if st.button("Suchen", key="wizard_search_button"):
             st.session_state["wizard_search_results"] = search_instrument_candidates(
                 conn,

/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py was modified since you last read it on disk (external edit or unrecorded writer). Re-read the file before writing.


        if st.button("Suchen", key="wizard_search_button"):
            search_payload = search_instrument_candidates(

        if st.button("Suchen", key="wizard_search_button"):
            st.session_state["wizard_search_results"] = search_instrument_candidates(


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
@@ -55,7 +55,7 @@
         provider_filter = st.text_input("Provider optional", key="wizard_search_provider")
         external_lookup = st.checkbox("Provider-Suche explizit versuchen", key="wizard_search_external")
         if st.button("Suchen", key="wizard_search_button"):
-            st.session_state["wizard_search_results"] = search_instrument_candidates(
+            search_payload = search_instrument_candidates(
                 conn,
                 query=query,
                 asset_class=class_filter or None,


                include_external=bool(external_lookup),
            )
            if hasattr(st, "session_state"):
                st.session_state["wizard_search_results"] = search_payload
        search_state = getattr(st, "session_state", {}).get("wizard_search_results", {"results": [], "warnings": [], "selection_required": True})

                include_external=bool(external_lookup),
            )
        search_state = st.session_state.get("wizard_search_results", {"results": [], "warnings": [], "selection_required": True})


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
@@ -65,7 +65,9 @@
                 provider=provider_filter or None,
                 include_external=bool(external_lookup),
             )
-        search_state = st.session_state.get("wizard_search_results", {"results": [], "warnings": [], "selection_required": True})
+            if hasattr(st, "session_state"):
+                st.session_state["wizard_search_results"] = search_payload
+        search_state = getattr(st, "session_state", {}).get("wizard_search_results", {"results": [], "warnings": [], "selection_required": True})
         if search_state.get("warnings"):
             st.warning("; ".join(str(w) for w in search_state.get("warnings", [])))
         display_rows = [


st.subheader("2. Instrument auswählen oder manuell anlegen")

st.markdown("### 2. Instrument auswählen oder manuell anlegen")


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
@@ -87,7 +87,7 @@
         ]
         show_table(st, display_rows, empty_message="Noch keine Suche ausgeführt oder kein lokaler Treffer. Manuelle Anlage ist möglich.")

-        st.markdown("### 2. Instrument auswählen oder manuell anlegen")
+        st.subheader("2. Instrument auswählen oder manuell anlegen")
         result_labels = [f"{i+1}. {r.get('name')} | {r.get('isin') or 'ISIN fehlt'} | {r.get('ticker') or 'Ticker fehlt'} | {r.get('exchange') or 'Exchange fehlt'} | {r.get('currency') or 'Währung fehlt'}" for i, r in enumerate(search_state.get("results", []))]
         chosen_label = st.selectbox("Treffer bewusst auswählen", result_labels, key="wizard_selected_result") if result_labels else None
         selected_result = None


st.subheader("3–4. Depot, Menge, Datum, Einstand und Notiz")

st.markdown("### 3–4. Depot, Menge, Datum, Einstand und Notiz")


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
@@ -112,7 +112,7 @@
                 except Exception as exc:
                     st.error(f"Manuelle Anlage abgelehnt: {exc}")

-        st.markdown("### 3–4. Depot, Menge, Datum, Einstand und Notiz")
+        st.subheader("3–4. Depot, Menge, Datum, Einstand und Notiz")
         pos_account_label = st.selectbox("Plattform/Depot", account_labels, key="wizard_position_account") if account_labels else None
         pos_type = st.selectbox("Buchung", ["initial_snapshot", "buy", "partial_sell", "full_sell", "dividend", "etf_distribution", "manual_adjustment"], key="wizard_position_type")
         pos_qty = st.text_input("Menge als Decimal-Text", key="wizard_position_qty")


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
@@ -125,7 +125,7 @@
         pos_fx_source = st.text_input("FX-Quelle optional", key="wizard_position_fx_source")
         pos_note = st.text_area("Notiz — Pflicht bei unvollständiger Historie oder manual_adjustment", key="wizard_position_note")

-        st.markdown("### 5. Review")
+        st.subheader("5. Review")
         review = {
             "Instrument": selected_result.get("name") if selected_result else "noch nicht ausgewählt",
             "ISIN": selected_result.get("isin") if selected_result else "",


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
@@ -140,7 +140,7 @@
         st.write(review)
         pos_confirm = st.checkbox("Ich bestätige: genau diese Position/Transaktion speichern", key="wizard_position_confirm")
         selected_pos_account = _selected(options["accounts"], pos_account_label, _account_label)
-        st.markdown("### 6. Bestätigen")
+        st.subheader("6. Bestätigen")
         if st.button("Bestätigt speichern", key="wizard_position_save", type="primary"):
             try:
                 if not selected_result or not selected_result.get("catalog_entry_id"):


terminal

Output:
........................................................................ [ 28%]
........................................................................ [ 56%]
........................................................................ [ 84%]
.........................................                                [100%]
257 passed in 16.16s


todo

Tests für Suche, Wizard-Safety, Snapshots, FX/Audit/DQ ergänzen

Compile, pytest, Git-Safety, Commit und Push ausführen


Code-Struktur für manuelle Instrument-Suche/Add-Position prüfen

Resolver/Search-Service und manuelle Snapshot/Transaction-Funktionen implementieren

User-Mode Wizard im Dashboard ohne Live-API/technische IDs bauen


terminal

PYTHONPATH=src python - <<'PY'
from jarvis_finance.config.settings import load_settings
from jarvis_finance.storage.database import connect
from jarvis_finance.storage.migrations import apply_migrations
from jarvis_finance.equity.manage import ensure_standard_broker_accounts
s=load_settings()
conn=connect(s.database_path)
apply_migrations(conn)
summary=ensure_standard_broker_accounts(conn)
counts={}
for name in ['PostFinance','True Wealth','Raiffeisen','Anderes Konto']:
    row=conn.execute('SELECT COUNT(*) AS c FROM platforms WHERE name=?',(name,)).fetchone()
    counts[name]=int(row['c'] or 0)
print({'created': summary, 'platforms_present': counts})
PY
python -m compileall src tests && pytest -q && PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan . && git status --short


Output:
Traceback (most recent call last):
  File "<stdin>", line 6, in <module>
AttributeError: 'Settings' object has no attribute 'database_path'
Listing 'src'...
Listing 'src/jarvis_finance'...
Listing 'src/jarvis_finance/audit'...
Listing 'src/jarvis_finance/cli'...
Listing 'src/jarvis_finance/config'...
Listing 'src/jarvis_finance/crypto'...
Listing 'src/jarvis_finance/dashboard'...
Listing 'src/jarvis_finance/dashboard/components'...
Listing 'src/jarvis_finance/dashboard/pages'...
Listing 'src/jarvis_finance/dashboard/views'...
Listing 'src/jarvis_finance/equity'...
Listing 'src/jarvis_finance/fx'...
Listing 'src/jarvis_finance/imports'...
Listing 'src/jarvis_finance/ledger'...
Listing 'src/jarvis_finance/market'...
Listing 'src/jarvis_finance/market_data'...
Listing 'src/jarvis_finance/models'...
Listing 'src/jarvis_finance/quality'...
Listing 'src/jarvis_finance/reports'...
Listing 'src/jarvis_finance/reports/templates'...
Listing 'src/jarvis_finance/runtime'...
Listing 'src/jarvis_finance/storage'...
Listing 'src/jarvis_finance.egg-info'...
Listing 'tests'...
Listing 'tests/fixtures'...
Listing 'tests/integration'...
Listing 'tests/unit'...
........................................................................ [ 28%]
........................................................................ [ 56%]
........................................................................ [ 84%]
.........................................                                [100%]
257 passed in 16.98s
.pytest_cache/CACHEDIR.TAG: runtime_cache_file_blocked
.pytest_cache/README.md: runtime_cache_file_blocked
.pytest_cache/.gitignore: runtime_cache_file_blocked
.pytest_cache/v/cache/nodeids: runtime_cache_file_blocked
.pytest_cache/v/cache/lastfailed: runtime_cache_file_blocked

Non-zero exit (often normal — e.g. 'git diff' returns 1 when files differ)


Explanation:
**Resolving runtime issues**

I need to remove the `.pytest_cache` and then rerun the safety checks. I wonder if there are any runtime path issues that are affecting everything. It's important to inspect the settings to see if anything's misconfigured. I think a close look at all the details should help me find any problems with the execution. Of course, I hope this solves the issues at hand so I can move forward smoothly!


     1|from __future__ import annotations
     2|
     3|import os
     4|from dataclasses import dataclass
     5|from pathlib import Path
     6|
     7|from .paths import DEFAULT_RUNTIME_DIR, RuntimePaths, is_relative_to
     8|
     9|
    10|@dataclass(frozen=True)
    11|class Settings:
    12|    env: str = "local"
    13|    base_currency: str = "CHF"
    14|    timezone: str = "Europe/Zurich"
    15|    runtime_paths: RuntimePaths = RuntimePaths.from_base(DEFAULT_RUNTIME_DIR)
    16|    log_level: str = "INFO"
    17|    block_runtime_inside_repo: bool = True
    18|
    19|    @property
    20|    def db_path(self) -> Path:
    21|        return self.runtime_paths.db_path
    22|
    23|
    24|def find_repo_root(start: Path | None = None) -> Path:
    25|    current = (start or Path.cwd()).resolve()
    26|    for candidate in [current, *current.parents]:
    27|        if (candidate / ".git").exists() or (candidate / "pyproject.toml").exists():
    28|            return candidate
    29|    return current
    30|
    31|
    32|def load_settings(repo_root: Path | None = None, environ: dict[str, str] | None = None) -> Settings:
    33|    env = environ or os.environ
    34|    runtime_dir = Path(env.get("JARVIS_FINANCE_RUNTIME_DIR", str(DEFAULT_RUNTIME_DIR))).expanduser()
    35|    paths = RuntimePaths.from_base(runtime_dir)
    36|    db_override = env.get("JARVIS_FINANCE_DB_PATH")
    37|    if db_override:
    38|        object.__setattr__(paths, "db_path", Path(db_override).expanduser().resolve())
    39|    settings = Settings(
    40|        env=env.get("JARVIS_FINANCE_ENV", "local"),
    41|        base_currency=env.get("JARVIS_FINANCE_BASE_CURRENCY", "CHF"),
    42|        timezone=env.get("JARVIS_FINANCE_TIMEZONE", "Europe/Zurich"),
    43|        runtime_paths=paths,
    44|        log_level=env.get("JARVIS_FINANCE_LOG_LEVEL", "INFO"),
    45|        block_runtime_inside_repo=env.get("JARVIS_FINANCE_BLOCK_RUNTIME_INSIDE_REPO", "1") not in {"0", "false", "False"},
    46|    )
    47|    validate_settings(settings, repo_root=repo_root)
    48|    return settings
    49|
    50|
    51|def validate_settings(settings: Settings, repo_root: Path | None = None) -> None:
    52|    root = (repo_root or find_repo_root()).resolve()
    53|    if settings.block_runtime_inside_repo and is_relative_to(settings.runtime_paths.base_dir, root):
    54|        raise ValueError(f"Runtime directory must be outside the Git repository: {settings.runtime_paths.base_dir}")
    55|    if settings.base_currency != "CHF":
    56|        raise ValueError("MVP 1 base currency must be CHF")
    57|
    58|
    59|def ensure_runtime_dirs(settings: Settings) -> None:
    60|    settings.runtime_paths.ensure_dirs()
    61|


rm -rf .pytest_cache
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.equity.manage import ensure_standard_broker_accounts
s=load_settings()
conn=connect(s.db_path)
apply_migrations(conn)
summary=ensure_standard_broker_accounts(conn)
counts={}
for name in ['PostFinance','True Wealth','Raiffeisen','Anderes Konto']:
    row=conn.execute('SELECT COUNT(*) AS c FROM platforms WHERE name=?',(name,)).fetchone()
    counts[name]=int(row['c'] or 0)
print({'created': summary, 'platforms_present': counts})
PY
PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan . && git status --short


Output:
{'created': {'platforms_created': 3, 'accounts_created': 4}, 'platforms_present': {'PostFinance': 1, 'True Wealth': 1, 'Raiffeisen': 1, 'Anderes Konto': 1}}
GIT_SAFETY_OK
 M src/jarvis_finance/dashboard/views/page_13_equity_manage.py
 M src/jarvis_finance/equity/manage.py
 M tests/unit/test_equity_manage.py


Explanation:
**Considering database changes**

I think I might need to change the runtime database. That seems like an important step, so I want to ensure everything is compatible before making any adjustments. To do that, I need to check the differences in the code using a git diff command. This should help me clarify what changes have been made, allowing me to proceed with confidence. I'll double-check everything to make sure I'm on the right track!

git diff --stat && git diff -- src/jarvis_finance/equity/manage.py src/jarvis_finance/dashboard/views/page_13_equity_manage.py tests/unit/test_equity_manage.py | sed -n '1,260p'


Output:
.../dashboard/views/page_13_equity_manage.py       | 182 +++++++++++++++------
 src/jarvis_finance/equity/manage.py                | 163 ++++++++++++++++--
 tests/unit/test_equity_manage.py                   | 163 ++++++++++++++++++
 3 files changed, 441 insertions(+), 67 deletions(-)
diff --git a/src/jarvis_finance/dashboard/views/page_13_equity_manage.py b/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
index c628d26..aa065b2 100644
--- a/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
+++ b/src/jarvis_finance/dashboard/views/page_13_equity_manage.py
@@ -4,7 +4,7 @@ from decimal import Decimal

 from jarvis_finance.dashboard import data
 from jarvis_finance.dashboard.components.tables import show_table
-from jarvis_finance.equity.manage import add_equity_initial_snapshot, add_equity_transaction, add_manual_position_from_catalog, create_manual_catalog_entry, get_equity_management_options
+from jarvis_finance.equity.manage import add_equity_initial_snapshot, add_equity_transaction, add_manual_position_from_catalog, create_manual_catalog_entry, ensure_standard_broker_accounts, get_equity_management_options, search_instrument_candidates
 from jarvis_finance.fx.overrides import set_manual_fx_override
 from jarvis_finance.imports.instrument_candidates import confirm_candidate, defer_candidate, qwen_resolve_staged_candidates, reject_candidate, resolve_candidate
 from jarvis_finance.market_data.candidates import reject_mapping_candidate, select_mapping_candidate
@@ -41,60 +41,132 @@ def render(st, conn) -> None:
     tabs = st.tabs(["Instrument Search / Add Position", "Mapping Review Queue", "Hinzufügen", "Kaufen", "Partial Sell", "Full Sell", "Dividende", "ETF-Ausschüttung", "Fee", "Korrigieren", "Review"])

     with tabs[0]:
-        st.subheader("Instrument Search / Add Position")
-        st.caption("Lokaler Katalog zuerst; externe Lookups nur über freigegebene Provider. Mehrere Listings bleiben Auswahlpflicht.")
-        summary = data.get_instrument_catalog_summary(conn)
-        st.write(summary)
-        query = st.text_input("Suche nach ISIN, Symbol, Name, Provider-Symbol, Exchange oder Currency", key="catalog_search_query")
-        class_filter = st.selectbox("Filter", ["", "equity", "etf", "crypto", "cash"], key="catalog_search_class")
-        catalog_rows = data.get_instrument_catalog_entries(conn, query=query or None, asset_class=class_filter or None)
-        show_table(st, catalog_rows)
-        st.divider()
-        st.subheader("Katalogeintrag manuell anlegen")
-        c_asset = st.selectbox("Assetklasse", ["equity", "etf"], key="catalog_new_asset")
-        c_name = st.text_input("Name", key="catalog_new_name")
-        c_isin = st.text_input("ISIN", key="catalog_new_isin")
-        c_ticker = st.text_input("Ticker/Symbol", key="catalog_new_ticker")
-        c_exchange = st.text_input("Exchange", key="catalog_new_exchange")
-        c_currency = st.selectbox("Trading/Instrument Currency", ["CHF", "EUR", "USD"], key="catalog_new_currency")
-        c_provider = st.text_input("Provider", value="manual", key="catalog_new_provider")
-        c_provider_symbol = st.text_input("Provider-Symbol", key="catalog_new_provider_symbol")
-        c_hedge = st.selectbox("Hedge Status", ["unknown", "hedged", "unhedged"], key="catalog_new_hedge")
-        c_status = st.selectbox("Instrument Status", ["unknown", "active", "suspended", "delisted", "merged", "inactive"], key="catalog_new_status")
-        c_policy = st.selectbox("Valuation Policy", ["live_price", "last_known_price", "manual_value", "exclude_from_auto_price_update"], key="catalog_new_policy")
-        c_note = st.text_area("Katalog-Notiz (Pflicht)", key="catalog_new_note")
-        if st.button("Katalogeintrag speichern", key="catalog_new_save"):
-            try:
-                cid = create_manual_catalog_entry(conn, asset_class=c_asset, name=c_name, isin=c_isin, ticker=c_ticker, exchange=c_exchange, currency=c_currency, provider=c_provider, provider_symbol=c_provider_symbol, hedge_status=c_hedge, instrument_status=c_status, valuation_policy=c_policy, note=c_note)
-                st.success("Katalogeintrag gespeichert; Audit-Log geschrieben.")
-                st.write({"catalog_entry_id": cid})
-            except Exception as exc:
-                st.error(f"Katalogspeicherung abgelehnt: {exc}")
-        st.divider()
-        st.subheader("Position aus Katalog hinzufügen")
-        entry_labels = [f"{r.get('catalog_entry_id')} | {r.get('ticker') or ''} | {r.get('name')} | {r.get('isin') or ''} | {r.get('exchange') or ''} | {r.get('trading_currency') or ''}" for r in catalog_rows]
-        entry_label = st.selectbox("Katalog-Kandidat auswählen", entry_labels, key="catalog_position_entry") if entry_labels else None
-        pos_account_label = st.selectbox("Depot/Konto", account_labels, key="catalog_position_account") if account_labels else None
-        pos_type = st.selectbox("Positionstyp", ["initial_snapshot", "buy", "manual_adjustment"], key="catalog_position_type")
-        pos_qty = st.text_input("Menge als Decimal-Text", key="catalog_position_qty")
-        pos_date = st.date_input("Datum", key="catalog_position_date")
-        pos_currency = st.selectbox("Währung", ["CHF", "EUR", "USD"], key="catalog_position_currency")
-        pos_cost = st.text_input("Einstandspreis / Cost Basis optional", key="catalog_position_cost")
-        pos_fx_status = st.selectbox("FX-Status", ["not_needed", "missing", "manual_override", "manual_override_required"], key="catalog_position_fx_status")
-        pos_fx_rate = st.text_input("FX-Rate optional", key="catalog_position_fx_rate")
-        pos_fx_source = st.text_input("FX-Quelle optional", key="catalog_position_fx_source")
-        pos_note = st.text_area("Review-/Bestätigungsnotiz", key="catalog_position_note")
-        pos_confirm = st.checkbox("Ich bestätige diese einzelne Katalog-Position", key="catalog_position_confirm")
+        st.subheader("Aktie/ETF hinzufügen")
+        st.caption("Wizard: 1 Suchen → 2 Instrument auswählen → 3 Depot & Menge → 4 Einstand/Datum/Notiz → 5 Review → 6 Bestätigen.")
+        st.info("DOCX-Brokerimporte sind hier bewusst nicht der Hauptpfad. Es wird nur auf Klick gesucht; kein Provider-/Live-API-Call beim Rendern.")
+        ensure_standard_broker_accounts(conn)
+
+        st.subheader("1. Instrument suchen")
+        query = st.text_input("ISIN, Name oder Ticker", key="wizard_search_query")
+        class_filter = st.selectbox("Assetklasse", ["", "stock", "etf"], key="wizard_search_class")
+        currency_filter = st.selectbox("Währung", ["", "CHF", "EUR", "USD"], key="wizard_search_currency")
+        exchange_filter = st.text_input("Börse/Exchange optional", key="wizard_search_exchange")
+        country_filter = st.text_input("Land optional", key="wizard_search_country")
+        provider_filter = st.text_input("Provider optional", key="wizard_search_provider")
+        external_lookup = st.checkbox("Provider-Suche explizit versuchen", key="wizard_search_external")
+        if st.button("Suchen", key="wizard_search_button"):
+            search_payload = search_instrument_candidates(
+                conn,
+                query=query,
+                asset_class=class_filter or None,
+                exchange=exchange_filter or None,
+                currency=currency_filter or None,
+                country=country_filter or None,
+                provider=provider_filter or None,
+                include_external=bool(external_lookup),
+            )
+            if hasattr(st, "session_state"):
+                st.session_state["wizard_search_results"] = search_payload
+        search_state = getattr(st, "session_state", {}).get("wizard_search_results", {"results": [], "warnings": [], "selection_required": True})
+        if search_state.get("warnings"):
+            st.warning("; ".join(str(w) for w in search_state.get("warnings", [])))
+        display_rows = [
+            {
+                "Name": r.get("name", ""),
+                "ISIN": r.get("isin", ""),
+                "Ticker": r.get("ticker", ""),
+                "Exchange": r.get("exchange", ""),
+                "Währung": r.get("currency", ""),
+                "Assetklasse": r.get("asset_class", ""),
+                "Datenquelle": r.get("data_source", ""),
+                "Mapping-Status": r.get("mapping_status", ""),
+                "Letzter Kurs": r.get("last_price", ""),
+                "Kursdatum": r.get("price_date", ""),
+            }
+            for r in search_state.get("results", [])
+        ]
+        show_table(st, display_rows, empty_message="Noch keine Suche ausgeführt oder kein lokaler Treffer. Manuelle Anlage ist möglich.")
+
+        st.subheader("2. Instrument auswählen oder manuell anlegen")
+        result_labels = [f"{i+1}. {r.get('name')} | {r.get('isin') or 'ISIN fehlt'} | {r.get('ticker') or 'Ticker fehlt'} | {r.get('exchange') or 'Exchange fehlt'} | {r.get('currency') or 'Währung fehlt'}" for i, r in enumerate(search_state.get("results", []))]
+        chosen_label = st.selectbox("Treffer bewusst auswählen", result_labels, key="wizard_selected_result") if result_labels else None
+        selected_result = None
+        if chosen_label:
+            selected_result = search_state.get("results", [])[int(chosen_label.split('.', 1)[0]) - 1]
+            st.success("Instrument ausgewählt. Ticker allein wurde nicht automatisch übernommen; Auswahl ist bewusst erfolgt.")
+        with st.expander("Manuell anlegen, falls kein sauberer Treffer vorhanden ist"):
+            c_asset = st.selectbox("Assetklasse", ["stock", "etf"], key="wizard_manual_asset")
+            c_name = st.text_input("Name", key="wizard_manual_name")
+            c_isin = st.text_input("ISIN stark empfohlen", key="wizard_manual_isin")
+            c_ticker = st.text_input("Ticker", key="wizard_manual_ticker")
+            c_exchange = st.text_input("Exchange", key="wizard_manual_exchange")
+            c_currency = st.selectbox("Währung", ["CHF", "EUR", "USD"], key="wizard_manual_currency")
+            c_note = st.text_area("Notiz / Quelle zur manuellen Anlage", key="wizard_manual_note")
+            st.warning("Manuelle Anlage ohne ISIN erzeugt einen Data-Quality-Hinweis; ISIN und Ticker dürfen nicht beide leer sein.")
+            if st.button("Manuelles Instrument speichern", key="wizard_manual_save"):
+                try:
+                    if not c_isin and not c_ticker:
+                        raise ValueError("ISIN oder Ticker ist erforderlich")
+                    create_manual_catalog_entry(conn, asset_class=c_asset, name=c_name, currency=c_currency, isin=c_isin, ticker=c_ticker, exchange=c_exchange, note=c_note)
+                    st.success("Instrument im lokalen Katalog gespeichert. Bitte anschließend erneut suchen und bewusst auswählen.")
+                except Exception as exc:
+                    st.error(f"Manuelle Anlage abgelehnt: {exc}")
+
+        st.subheader("3–4. Depot, Menge, Datum, Einstand und Notiz")
+        pos_account_label = st.selectbox("Plattform/Depot", account_labels, key="wizard_position_account") if account_labels else None
+        pos_type = st.selectbox("Buchung", ["initial_snapshot", "buy", "partial_sell", "full_sell", "dividend", "etf_distribution", "manual_adjustment"], key="wizard_position_type")
+        pos_qty = st.text_input("Menge als Decimal-Text", key="wizard_position_qty")
+        pos_date = st.date_input("Snapshot-/Buchungsdatum", key="wizard_position_date")
+        default_currency = selected_result.get("currency") if selected_result else "CHF"
+        pos_currency = st.selectbox("Originalwährung", ["CHF", "EUR", "USD"], index=["CHF", "EUR", "USD"].index(default_currency) if default_currency in {"CHF", "EUR", "USD"} else 0, key="wizard_position_currency")
+        pos_cost = st.text_input("Einstand / Brutto optional — leer = Cost Basis ungewiss", key="wizard_position_cost")
+        pos_fx_status = st.selectbox("FX-Status", ["not_needed", "ok", "missing", "manual_override", "manual_override_required"], key="wizard_position_fx_status")
+        pos_fx_rate = st.text_input("FX-Kurs zu CHF optional", key="wizard_position_fx_rate")
+        pos_fx_source = st.text_input("FX-Quelle optional", key="wizard_position_fx_source")
+        pos_note = st.text_area("Notiz — Pflicht bei unvollständiger Historie oder manual_adjustment", key="wizard_position_note")
+
+        st.subheader("5. Review")
+        review = {
+            "Instrument": selected_result.get("name") if selected_result else "noch nicht ausgewählt",
+            "ISIN": selected_result.get("isin") if selected_result else "",
+            "Ticker": selected_result.get("ticker") if selected_result else "",
+            "Exchange": selected_result.get("exchange") if selected_result else "",
+            "Depot": pos_account_label or "",
+            "Buchung": pos_type,
+            "Datum": str(pos_date),
+            "Cost Basis": "ungewiss" if not pos_cost else "erfasst",
+            "FX": pos_fx_status,
+        }
+        st.write(review)
+        pos_confirm = st.checkbox("Ich bestätige: genau diese Position/Transaktion speichern", key="wizard_position_confirm")
         selected_pos_account = _selected(options["accounts"], pos_account_label, _account_label)
-        if st.button("Position aus Katalog speichern", key="catalog_position_save", type="primary"):
+        st.subheader("6. Bestätigen")
+        if st.button("Bestätigt speichern", key="wizard_position_save", type="primary"):
             try:
-                if not entry_label or not selected_pos_account:
-                    raise ValueError("catalog entry and account are required")
-                result = add_manual_position_from_catalog(conn, catalog_entry_id=entry_label.split(" | ", 1)[0], account_id=selected_pos_account["account_id"], position_type=pos_type, quantity_text=pos_qty, trade_date=str(pos_date), currency=pos_currency, cost_basis_original_text=pos_cost or None, fx_status=pos_fx_status, fx_rate_to_chf_text=pos_fx_rate or None, fx_source=pos_fx_source or None, note=pos_note, confirm=bool(pos_confirm))
-                st.success("Position gespeichert und auditierbar erfasst.")
-                st.write({"instrument_id": result.instrument_id, "transaction_id": result.transaction_id, "warnings": result.warnings})
+                if not selected_result or not selected_result.get("catalog_entry_id"):
+                    raise ValueError("Instrument selection is required before saving")
+                if not selected_pos_account:
+                    raise ValueError("account is required")
+                result = add_manual_position_from_catalog(
+                    conn,
+                    catalog_entry_id=selected_result["catalog_entry_id"],
+                    account_id=selected_pos_account["account_id"],
+                    position_type=pos_type,
+                    quantity_text=pos_qty,
+                    trade_date=str(pos_date),
+                    currency=pos_currency,
+                    cost_basis_original_text=pos_cost or None,
+                    fx_status=pos_fx_status,
+                    fx_rate_to_chf_text=pos_fx_rate or None,
+                    fx_source=pos_fx_source or None,
+                    note=pos_note,
+                    confirm=bool(pos_confirm),
+                )
+                st.success("Gespeichert: Ledger-Ereignis und Audit-Log wurden erzeugt.")
+                if result.warnings:
+                    st.warning("Datenqualität: " + "; ".join(result.warnings))
             except Exception as exc:
-                st.error(f"Positionsspeicherung abgelehnt: {exc}")
+                st.error(f"Speicherung abgelehnt: {exc}")

     with tabs[1]:
         st.subheader("Mapping Review Queue: PostFinance / True Wealth")
@@ -233,7 +305,8 @@ def render(st, conn) -> 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})
+                if result.warnings:
+                    st.warning("Datenqualität: " + "; ".join(result.warnings))
             except Exception as exc:
                 st.error(f"Speichern abgelehnt: {exc}")

@@ -275,7 +348,8 @@ def render(st, conn) -> None:
                         confirm=bool(confirm),
                     )
                     st.success("Transaktion gespeichert und auditierbar erfasst.")
-                    st.write({"transaction_id": result.transaction_id, "warnings": result.warnings})
+                    if result.warnings:
+                        st.warning("Datenqualität: " + "; ".join(result.warnings))
                 except Exception as exc:
                     st.error(f"Speichern abgelehnt: {exc}")

diff --git a/src/jarvis_finance/equity/manage.py b/src/jarvis_finance/equity/manage.py
index 9632a9f..7edc0e4 100644
--- a/src/jarvis_finance/equity/manage.py
+++ b/src/jarvis_finance/equity/manage.py
@@ -278,6 +278,125 @@ def _insert_transaction(
     return tx_id, warnings

+
+def ensure_standard_broker_accounts(conn: Connection) -> dict[str, int]:
+    """Ensure manual-entry broker platforms exist. No holdings are imported."""
+    now = utc_now()
+    created_platforms = 0
+    created_accounts = 0
+    specs = [
+        ("postfinance", "PostFinance"),
+        ("true_wealth", "True Wealth"),
+        ("raiffeisen", "Raiffeisen"),
+        ("other_broker", "Anderes Konto"),
+    ]
+    for platform_id, name in specs:
+        row = conn.execute("SELECT platform_id FROM platforms WHERE name=?", (name,)).fetchone()
+        pid = row["platform_id"] if row else platform_id
+        if row is None:
+            conn.execute(
+                "INSERT OR IGNORE INTO platforms(platform_id,name,platform_type,country,default_currency,is_active,notes,created_at) VALUES(?,?,?,?,?,?,?,?)",
+                (pid, name, "broker", "CH", "CHF", 1, "Manual equity/ETF entry platform; no broker import.", now),
+            )
+            created_platforms += 1
+        acc = conn.execute("SELECT account_id FROM accounts WHERE platform_id=? AND account_name=?", (pid, "Manual Portfolio")).fetchone()
+        if acc is None:
+            account_id = stable_id("account", pid, "Manual Portfolio", "CHF")
+            conn.execute(
+                "INSERT OR IGNORE INTO accounts(account_id,platform_id,account_name,account_type,currency,is_active,notes,created_at) VALUES(?,?,?,?,?,?,?,?)",
+                (account_id, pid, "Manual Portfolio", "brokerage", "CHF", 1, "Manual equity/ETF entries only; no DOCX import.", now),
+            )
+            created_accounts += 1
+    if created_platforms or created_accounts:
+        record_audit_event(
+            conn,
+            source="equity_manage",
+            action="ensure_manual_broker_accounts",
+            entity_type="account",


Explanation:
**Finalizing code process**

I’m looking at my development process and think I need to run a full compile and check for safety after clearing the pytest cache. Before I commit, I wonder if I should run a format, but I'm not sure if it would cause issues with git-safety regarding runtime changes. It’s crucial that I inspect to make sure there are no secrets before pushing. So, a git status check might be needed to confirm everything is ready.

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


Output:
Listing 'src'...
Listing 'src/jarvis_finance'...
Listing 'src/jarvis_finance/audit'...
Listing 'src/jarvis_finance/cli'...
Listing 'src/jarvis_finance/config'...
Listing 'src/jarvis_finance/crypto'...
Listing 'src/jarvis_finance/dashboard'...
Listing 'src/jarvis_finance/dashboard/components'...
Listing 'src/jarvis_finance/dashboard/pages'...
Listing 'src/jarvis_finance/dashboard/views'...
Listing 'src/jarvis_finance/equity'...
Listing 'src/jarvis_finance/fx'...
Listing 'src/jarvis_finance/imports'...
Listing 'src/jarvis_finance/ledger'...
Listing 'src/jarvis_finance/market'...
Listing 'src/jarvis_finance/market_data'...
Listing 'src/jarvis_finance/models'...
Listing 'src/jarvis_finance/quality'...
Listing 'src/jarvis_finance/reports'...
Listing 'src/jarvis_finance/reports/templates'...
Listing 'src/jarvis_finance/runtime'...
Listing 'src/jarvis_finance/storage'...
Listing 'src/jarvis_finance.egg-info'...
Listing 'tests'...
Listing 'tests/fixtures'...
Listing 'tests/integration'...
Listing 'tests/unit'...
........................................................................ [ 28%]
........................................................................ [ 56%]
........................................................................ [ 84%]
.........................................                                [100%]
257 passed in 16.41s
GIT_SAFETY_OK
 M src/jarvis_finance/dashboard/views/page_13_equity_manage.py
 M src/jarvis_finance/equity/manage.py
 M tests/unit/test_equity_manage.py


Explanation:
**Preparing to commit changes**

I'm getting ready to add my changes using git, then I’ll commit and push them. I think it’s important to use a token just like I did before, making sure it's safe and that I don’t output the token itself. I should also make sure that this is my first commit. It feels good to keep everything organized and done the right way! Okay, let's move forward with this!

git add src/jarvis_finance/dashboard/views/page_13_equity_manage.py src/jarvis_finance/equity/manage.py tests/unit/test_equity_manage.py && git commit -m "Add manual equity position wizard"


Output:
[main 43b0443] Add manual equity position wizard
 3 files changed, 441 insertions(+), 67 deletions(-)


python - <<'PY'
from pathlib import Path
import csv, os, subprocess, tempfile
p=Path('/home/agent/jarvis_runtime/finance-system/secrets/Github_token_FinanceManager.csv')
rows=list(csv.reader(p.read_text().splitlines()))
flat=[c.strip() for row in rows for c in row if c.strip()]
token=next((x for x in flat if x.startswith('ghp_') or x.startswith('github_pat_')), None)
if not token:
    raise SystemExit('token not found')
with tempfile.NamedTemporaryFile('w', delete=False) as f:
    f.write('#!/bin/sh\ncase "$1" in *Username*) printf "%s" x-access-token ;; *Password*) printf "%s" "$GITHUB_TOKEN" ;; esac\n')
    helper=f.name
os.chmod(helper,0o700)
env=os.environ.copy(); env['GIT_ASKPASS']=helper; env['GITHUB_TOKEN']=token; env['GIT_TERMINAL_PROMPT']='0'
try:
    cp=subprocess.run(['git','push','origin','main'],cwd='/home/agent/.hermes/repos/FinanceManager',env=env,text=True,capture_output=True,timeout=120)
    print('push_exit', cp.returncode)
    if cp.stdout: print(cp.stdout)
    if cp.stderr: print(cp.stderr)
    raise SystemExit(cp.returncode)
finally:
    try: os.unlink(helper)
    except FileNotFoundError: pass
PY
PYTHONPATH=src python -m jarvis_finance.cli.main git-safety-scan . && git status --short && git rev-parse --short HEAD


Output:
push_exit 0
To https://github.com/Gamexgit/FinanceManager.git
   40ff6e2..43b0443  main -> main

GIT_SAFETY_OK
43b0443


Explanation:
**Finalizing German account notes**

I need to summarize the runtime accounts created across 3 platforms, totaling 4 accounts. Did PostFinance exist already, or did I create it along with the others? It’s all a bit unclear. I also want to mention that there are 257 tests done. I should check if my git status is clean—it’s good if there are no rows, indicating it’s clean. I’ll make sure to finish my to-dos!


Erledigt, Sir.

## Ergebnis

- **DOCX-Importpfad für Aktien/ETFs produktiv gestoppt:** ja
  Die bestehende Review Queue bleibt nur Review-/Erinnerungsfläche. Kein produktiver Import aus PostFinance-/True-Wealth-DOCX.

- **Instrument Search gebaut:** ja
- **Add Position Wizard gebaut:** ja
- **Manuelle Anlage möglich:** ja
- **ISIN-Suche möglich:** ja
- **Name-/Ticker-Suche möglich:** ja
- **Ticker allein importiert automatisch:** nein
- **Mehrfachtreffer verlangen Auswahl:** ja
- **Initial Snapshot möglich:** ja
- **Buy/Sell/Dividend vorbereitet/implementiert:** implementiert im Wizard-Pfad:
  - `initial_snapshot`
  - `buy`
  - `partial_sell`
  - `full_sell`
  - `dividend`
  - `etf_distribution`
  - `manual_adjustment`

## UX / User Mode

Neue Hauptfunktion im Dashboard:

**Equity/ETF Manage → Aktie/ETF hinzufügen**

Als Wizard:

1. Instrument suchen
2. Instrument auswählen oder manuell anlegen
3. Depot, Menge, Datum erfassen
4. Einstand/FX/Notiz erfassen
5. Review
6. Bestätigt speichern

User Mode zeigt im neuen Wizard keine technischen IDs als Ergebnis. Speicherung zeigt nur Erfolg und Data-Quality-Hinweise, nicht `instrument_id`, `source_id`, `catalog_entry_id` oder Debug-JSON. Ein bisschen weniger Maschinenraum, wie zivilisierte Menschen es bevorzugen.

## Resolver / Search

Implementiert:

- lokale Suche nach:
  - ISIN
  - Name
  - Ticker
  - Exchange
  - Currency
- ISIN priorisiert
- Ticker allein bleibt Auswahl-/Review-pflichtig
- mehrere Treffer bleiben Auswahl-pflichtig
- Provider-/External Lookup nur bei explizitem Klick/Checkbox vorbereitet
- ohne Provider/API-Key bleibt manuelle Anlage möglich
- kein Live-API-Call beim normalen Dashboard-Rendern

## Speicherung / Ledger

Implementiert:

- Instrument aus lokalem Katalog oder manueller Anlage
- Initial Snapshot ohne Kaufhistorie zu erfinden
- Transaktionen als Ledger-Ereignisse
- Review/Confirm vor Speicherung Pflicht
- Audit-Log bei Speicherung Pflicht
- keine stillen Überschreibungen
- Initial Snapshot blockiert, wenn schon eine Position existiert
- Sell prüft negative Positionen
- Full Sell verlangt exakte aktuelle Menge

## Data Quality

Implementiert:

- fehlende Cost Basis → `cost_basis_uncertain`
- fehlender FX bei USD/EUR → `missing_fx`
- fehlender Provider-/Preisbezug → Market-Data-Hinweis
- fehlender Marktpreis → Data-Quality-Hinweis
- Cost Basis wird nicht als echter Wert interpretiert, wenn sie fehlt; intern wird kein Performance-Wert daraus als „bekannt“ abgeleitet.

## Plattformen/Konten

Runtime-DB vorsichtig aktualisiert, ohne Holdings/Positionen zu importieren:

- PostFinance: vorhanden
- True Wealth: vorhanden
- Raiffeisen: vorhanden
- Anderes Konto: vorhanden

Ausführung war nur Konto-/Plattform-Sicherstellung für manuelle Erfassung. Keine Broker-Daten importiert.

## Tests

- **Compile:** erfolgreich
- **pytest:** `257 passed`
- Neue Tests ergänzt für:
  - ISIN-Suche
  - Name-Suche mit Kandidatenliste
  - Ticker allein ohne Auto-Import
  - Mehrfachtreffer mit manueller Auswahlpflicht
  - manuelle Instrumentanlage
  - Initial Snapshot aus ausgewähltem Instrument
  - fehlende Cost Basis → `cost_basis_uncertain`
  - USD/EUR mit FX
  - fehlender FX → `missing_fx`
  - Review/Confirm vor Speicherung
  - Audit-Log bei Speicherung
  - Dashboard rendert ohne Live-API
  - User-Mode ohne technische Ergebnis-IDs im Wizard

## Git / Safety

- **Git-Safety:** `GIT_SAFETY_OK`
- **Git Status:** clean
- **Commit:** `43b0443`
- **Push:** erfolgreich nach `origin/main`

## Offene Grenzen

- Externe Provider wie OpenFIGI/Finnhub/FMP/Twelve Data/Stooq/Yahoo sind vorbereitet, aber noch nicht als echte produktive Provider integriert.
- Aktuelle Marktpreise für Aktien/ETFs werden noch nicht automatisch über diesen Wizard geladen.
- Historische Performance bleibt bewusst unvollständig, wenn Cost Basis oder historische FX fehlen.
- Unsichere DOCX-Kandidaten bleiben in der Review Queue, werden aber nicht produktiv importiert.