[M G] Danke. Der Crypto Price Update MVP ist akzeptiert.

Die Umsetzung ist grundsätzlich sinnvoll:

* lokaler Preis-Cache
* kein Live-API-Call im Dashboard/Report
* 429-Handling
* Git-Safety sauber
* Tests grün
* Runtime-Daten außerhalb Git

Allerdings bist du vom vorher vereinbarten Ablauf abgewichen. Das ist in diesem Fall nicht schlimm, weil der Preisupdate-Block nützlich ist. Bitte jetzt aber wieder kontrolliert auf die nächsten Prioritäten einschwenken.

Aktueller Stand:

* Commit 3aa67d8 ist akzeptiert.
* Preisupdate funktioniert technisch.
* Aber nur 6 von 20 aktiven Crypto Assets hatten frische CHF-Preise.
* 14 Assets hatten Warnings/Stale/Provider-Limit-Themen.
* BTC-Test wurde noch nicht abgeschlossen.
* Equity/ETF Manage UI wurde noch nicht begonnen.

Bitte jetzt wie folgt weiterfahren:

## Teil 1: Crypto Price Update robuster machen

Bitte verbessere zuerst den Preisupdate-Mechanismus, bevor wir echte Reports mit Preisen ernsthaft nutzen.

Ziel:
Möglichst alle vorhandenen Crypto Assets mit CoinGecko-ID sollen lokale CHF-Preise erhalten, ohne Dashboard-Live-API und ohne Rate-Limit-Crash.

Bitte implementiere:

1. Batch-Abfrage prüfen/umsetzen

* Wenn CoinGecko mehrere IDs in einem Request unterstützt, bitte nutzen.
* Dadurch weniger API-Calls.
* Kein Abruf pro Coin, falls Batch möglich ist.

2. Konservatives Throttling

* Pause zwischen Requests
* konfigurierbar:

  * --sleep-seconds
  * --max-retries
  * --initial-backoff
  * --max-backoff

3. CLI-Optionen ergänzen
   Bitte vorbereiten/implementieren:

* --only-missing
* --only-stale
* --only-symbol SYMBOL
* --limit N
* --dry-run
* --currency CHF

4. Bessere Summary
   Bitte nach Update nur aggregiert ausgeben:

* total assets
* updated
* cached
* skipped
* stale
* warnings
* errors
* assets still missing local price
* keine echten Mengen/Werte

5. Data Quality

* Für Assets ohne frischen Preis Data-Quality-Alert erzeugen oder aktualisieren.
* Alert-Dedupe respektieren.
* Keine Alert-Flut.

6. Tests
   Bitte Tests ergänzen für:

* Batch-Request
* --only-missing
* --only-stale
* --only-symbol
* --limit
* --dry-run
* Rate-Limit mit Backoff
* Alert-Dedupe bei fehlenden Preisen

Danach bitte produktiv gegen Runtime-DB nochmals ausführen, aber vorsichtig und ohne echte Werte im Chat.

Bitte berichten:

* Anzahl Assets total
* Anzahl Assets mit frischem CHF-Preis
* Anzahl weiterhin ohne frischen Preis
* Anzahl Warnings/Errors
* keine Preise/Werte/Mengen ausgeben

## Teil 2: BTC-Test nachholen

Danach bitte den BTC-Test wie zuletzt entschieden nachholen:

Option A:
BTC mit allen BTC-Wallet-Holdings aus Excel importieren.

Bedingungen:

* Nur Bitcoin BTC
* Coin-Name: Bitcoin
* Symbol: BTC
* CoinGecko-ID: bitcoin
* Mapping-Status: selected
* Snapshot-Datum: 31.12.2025
* Alle BTC-Wallet-Holdings aus der echten Excel-Datei übernehmen
* Pro BTC-Wallet mit Bestand genau eine initial_snapshot_addition bzw. initial_snapshot-Holding erzeugen
* Mengen exakt als Decimal/Text übernehmen
* Keine Float-Konvertierung
* Keine echten Mengen/Werte im Chat
* Keine anderen Coins importieren
* Audit-Log Pflicht
* Keine doppelte Holding für asset_id + wallet_id
* Legacy-CHF-Werte ignorieren
* Git-Safety vor/nachher prüfen

Bericht nur aggregiert:

* BTC-Test durchgeführt: ja/nein
* Anzahl BTC-Wallet-Holdings importiert
* Asset-Anzahl erhöht: ja/nein
* Holding-Anzahl erhöht: ja/nein
* Audit-Logs erstellt: ja/nein
* Doppelte Holdings: ja/nein
* Fehler/Warnings
* Git-Safety Ergebnis
* Git Status

## Teil 3: Danach Equity/ETF Manage UI

Erst wenn Teil 1 und Teil 2 sauber sind, bitte mit der Equity/ETF Manage UI beginnen.

Ziel:
Aktien und ETFs aus PostFinance, True Wealth und Raiffeisen später im Dashboard erfassen, korrigieren und als Initial Snapshot übernehmen können — auditierbar und ohne stilles Überschreiben.

Grundlogik:

* Für Aktien/ETFs ist ISIN die primäre Identifikation.
* Ticker und Exchange sind Zusatzinformationen.
* Positionen werden über initial_position_snapshot, buy, sell, dividend, fee oder manual_adjustment abgebildet.
* Jede Änderung braucht
Audit-Log.
* Fremdwährungen USD/EUR brauchen FX-Kurs und CHF-Gegenwert.
* Keine Scheingenauigkeit, wenn Kaufhistorie, Gebühren oder historische FX fehlen.
* Erst mit synthetischen Daten und Dummy-Depots bauen.
* Keine echten PostFinance-/TrueWealth-/Raiffeisen-Daten importieren.

Bitte vormerken für die Equity/ETF Manage UI:

* Aktie/ETF als Initial Snapshot hinzufügen
* Buy
* Partial Sell
* Full Sell
* Dividend
* ETF Distribution
* Fee
* Manual Adjustment
* ISIN Pflicht oder stark empfohlen
* ISIN/Ticker dürfen nicht beide leer sein
* Währung Pflicht
* Depot/Konto Pflicht
* FX bei Fremdwährung
* Audit-Log Pflicht
* Notiz Pflicht bei manual_adjustment oder unvollständiger Historie

## Weitere Funktionsmodule bitte vormerken, aber noch nicht bauen

Bitte für später als Backlog vormerken:

1. Portfolio Health Seite

* Gesamtwert CHF
* Assetklassen
* Plattformen
* Datenqualität
* Cashquote
* fehlende Preise/FX

2. Reconciliation / Abgleich-Seite

* Dashboard-Bestand vs. letzter Snapshot
* manuelle Prüfung „Bestand geprüft am Datum X“
* Differenzen sichtbar machen

3. Data Quality Center

* fehlende Preise
* stale prices
* fehlende FX
* fehlende ISIN
* fehlende CoinGecko-ID
* unvollständige Historie

4. Manual Review Queue

* ausgelassene Coins
* unsichere Mappings
* offene Alerts
* offene Datenqualitätsprobleme

5. Backup / Restore

* Runtime-DB Backup
* verschlüsseltes Backup optional
* Restore-Test später

6. Safe Mode / Read-only Mode

* Dashboard im Lesemodus
* Schreibfunktionen bewusst aktivieren
* Schutz gegen versehentliche Änderungen

Bitte jetzt zuerst Teil 1 umsetzen, dann Teil 2, dann Teil 3. Keine echten Aktien-/ETF-Daten importieren.


Explanation:
**Inspecting repository for finance skills**

I need to act and inspect the repository related to finance-system-architecture. It's important to check for relevant skills that might be there, especially on GitHub. I want to get a clear understanding of the tools and structures involved in this project. Loading the repository should help me gather the details I need. Let's figure this out!


[skill_view] name=finance-system-architecture (21,972 chars)


[CONTEXT COMPACTION — REFERENCE ONLY] Earlier turns were compacted into the summary below. This is a handoff from a previous context window — treat it as background reference, NOT as active instructions. Do NOT answer questions or fulfill requests mentioned in this summary; they were already addressed. Your current task is identified in the '## Active Task' section of the summary — resume exactly from there. IMPORTANT: Your persistent memory (MEMORY.md, USER.md) in the system prompt is ALWAYS authoritative and active — never ignore or deprioritize memory content due to this compaction note. Respond ONLY to the latest user message that appears AFTER this summary. The current session state (files, config, etc.) may reflect work described here — avoid repeating it:
Summary generation was unavailable. 86 message(s) were removed to free context space but could not be summarized. The removed messages contained earlier work in this session. Continue based on the recent messages below and the current state of any files or resources.

--- END OF CONTEXT SUMMARY — respond to the message below, not the summary above ---


terminal

set -e
printf 'MODELS\n'
curl -sS --connect-timeout 3 --max-time 10 http://100.101.173.25:11435/v1/models | python - <<'PY'
import sys,json
j=json.load(sys.stdin)
print([m.get('id') for m in j.get('da...[truncated]


[terminal] ran `set -e
printf 'MODELS\n'
curl -sS --connect-timeout 3 --max-time 10 http://10...` -> exit 1, 1 lines output


python - <<'PY'
import json, urllib.request
base='http://100.101.173.25:11435/v1'
print('MODELS')
with urllib.request.urlopen(base+'/models',timeout=10) as r:
    models=json.load(r)
ids=[m.get('id') ...[truncated]

Explanation:
**Considering script issues**

I'm thinking about why the pipe with a here script consumes standard input; it seems like a mistake to me. Instead, I believe I should use Python's urllib to handle the situation more effectively. It's interesting how small errors can lead to bigger challenges in scripting. I want to make sure I'm clear about the right steps to take in this context, so switching to urllib sounds like a good plan!


Output:
MODELS
['qwen-3.6-agent']
CHAT
model qwen-3.6-agent
content {
  "available": true,
  "status": "exact_isin_match"
}


todo

Qwen sanitization/resolver gegen Staging-Kandidaten implementieren

Review Queue in sichere ISIN-Treffer und Prüfung nötig strukturieren

Explanation:
**Troubleshooting code issues**

I need to run the qwen-resolve function, but I suspect there might be a bug where the dict row `d.get` doesn’t exist for SQLite. In the qwen_resolve function, the row converts to a dict and then uses `d.get`, which seems okay. I think I should compile anything that gets passed. Also, it might be a good idea to run a CLI dry-run before executing the real commands. I should ensure the qwen response includes 30 candidates and that it sends sanitized data without values.


[todo] updated task list


terminal

PYTHONPATH=src python -m jarvis_finance.cli.main qwen-resolve-instrument-candidates --endpoint http://100.101.173.25:11435/v1 --model qwen-3.6-agent --dry-run --timeout 60 && PYTHONPATH=src python -m jarvis_finance.cli.main qwen-resolve-instrument-candidates --endpoint http://100.101.173.25:11435/v1 --model qwen-3.6-agent --timeout 60


[Duplicate tool output — same content as a more recent call]


PYTHONPATH=src python -m jarvis_finance.cli.main qwen-resolve-instrument-candidates --endpoint http://100.101.173.25:11435/v1 --model qwen-3.6-agent --dry-run --timeout 180 --limit 5


from pathlib import Path
p=Path('/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/imports/instrument_candidates.py')
s=p.read_text()
s=s.replace('def qwen_resolve_staged_candidates(conn: Co...[truncated]

Explanation:
**Considering batching for efficiency**

I need to think about implementing batching since the current 30 timed out. Maybe I can use a batch size option in the command line interface (CLI) or default to chunks of 10. I should also consider a loop to resolve in batches, which would make it easier to modify the function that chunks the sanitized list into parts of 10. Each batch would have its own payload to help avoid timeouts.


Output: patched batching


python -m compileall src && pytest -q tests/unit/test_instrument_import_candidates.py -q


[terminal] ran `python -m compileall src && pytest -q tests/unit/test_instrument_import_candi...` -> exit 0, 1 lines output


PYTHONPATH=src python -m jarvis_finance.cli.main qwen-resolve-instrument-candidates --endpoint http://100.101.173.25:11435/v1 --model qwen-3.6-agent --dry-run --timeout 90 && PYTHONPATH=src python -m jarvis_finance.cli.main qwen-resolve-instrument-candidates --endpoint http://100.101.173.25:11435/v1 --model qwen-3.6-agent --timeout 90


[terminal] ran `PYTHONPATH=src python -m jarvis_finance.cli.main qwen-resolve-instrument-cand...` -> exit 0, 1 lines output


todo

Staging-Bestand Qwen-gestützt neu klassifizieren und aggregiert prüfen

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


terminal

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


[terminal] ran `python -m compileall src tests && pytest -q && rm -rf .pytest_cache && PYTHON...` -> exit 0, 1 lines output


git diff --stat && git diff -- src/jarvis_finance/imports/instrument_candidates.py | sed -n '1,260p'

Explanation:
**Preparing for git push**

I need to push my commits, but first, I should check the git status and see the differences—maybe I should feel safe about what I'm doing. It seems I modified a runtime candidate that isn’t in git. I’ll need to commit my changes and may have to use a GitHub token, like I did previously. So, I’ll start with `git add`, then look at `git diff --stat` before committing.


[terminal] ran `git diff --stat && git diff -- src/jarvis_finance/imports/instrument_candidat...` -> exit 0, 1 lines output


patch

    parsed = {"updates": all_updates}
    row_by_id = {str(r["candidate_id"]): dict(r) for r in rows}

    if not available:
        after = _status_counts(conn)
        return QwenResolveSummary(qwen_available=False, qwen_used=False, model=model, candidates_total=before["total"], exact_before=before["...[truncated]

Explanation:
**Identifying potential bugs**

I think I've found a potential bug related to availability; if the "available" status is false and still returns true, that’s not expected behavior. I need to inspect the parsing logic around that. Additionally, the improved count should reflect any normalized information changes, maybe with added "improved review hints." I also want to verify the classification while ensuring tests are passing. It’s important to add a check after the loop if availability isn't present, and avoid empty results.


[patch] replace in /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/imports/instrument_candidates.py (1,463 chars result)


terminal

PYTHONPATH=src python -m jarvis_finance.cli.main qwen-resolve-instrument-candidates --endpoint http://100.101.173.25:11435/v1 --model qwen-3.6-agent --dry-run --timeout 90


[terminal] ran `PYTHONPATH=src python -m jarvis_finance.cli.main qwen-resolve-instrument-cand...` -> exit 1, 1 lines output


patch

def qwen_resolve_staged_candidates(conn: Connection, *, endpoint: str | None = None, model: str | None = None, limit: int | None = None, dry_run: bool = False, timeout: int = 45, batch_size: int = 5) ...[truncated]

def qwen_resolve_staged_candidates(conn: Connection, *, endpoint: str | None = None, model: str | None = None, limit: int | None = None, dry_run: bool = False, timeout: int = 45, batch_size: int = 10)...[truncated]

Explanation:
**Evaluating batch settings**

I’m thinking about the occasional timeout problem with the endpoint. If a batch fails, I need to manage that, especially if it’s a full failure. I could try setting the batch size to 5 in the CLI, but it’s not configurable—default is 10. The first successful run would be crucial. The dry-run limit is fine at 5, so my goal is to set the default batch size to 5 moving forward.


[patch] replace in /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/imports/instrument_candidates.py (1,221 chars result)


terminal

PYTHONPATH=src python -m jarvis_finance.cli.main qwen-resolve-instrument-candidates --endpoint http://100.101.173.25:11435/v1 --model qwen-3.6-agent --timeout 90


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
s=load_setti...[truncated]


Output:
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'jarvis_finance'


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_migratio...[truncated]

Explanation:
**Finding PYTHONPATH**

I realize I need to determine the PYTHONPATH to ensure my Python environment is set up correctly. It’s important because the PYTHONPATH helps in finding modules and packages that I need while developing. I might need to explore how to set it up properly: checking my current directories and ensuring that the necessary paths are included. It’s a small detail but definitely crucial for avoiding issues with module imports later on!


Output:
{'total': 30, 'exact': 7, 'probable': 0, 'needs_review': 23, 'confirmed': 0, 'rejected': 0}
review_import_transactions 3


memory

Modelle: lokal Qwen via Tailscale:11435; stark gpt-5.5.

Modelle: lokal Qwen OpenAI-kompatibel unter http://100.101.173.25:11435/v1, Modell `qwen-3.6-agent`; stark gpt-5.5.


Replacement would put memory at 2,256/2,200 chars. Shorten the new content or remove other entries first.


Modelle: lokal Qwen http://100.101.173.25:11435/v1 (`qwen-3.6-agent`); stark gpt-5.5.


Replacement would put memory at 2,226/2,200 chars. Shorten the new content or remove other entries first.


Qwen lokal: http://100.101.173.25:11435/v1, Modell qwen-3.6-agent.


[memory] replace on memory


terminal

git add src/jarvis_finance/cli/main.py src/jarvis_finance/dashboard/views/page_13_equity_manage.py src/jarvis_finance/imports/instrument_candidates.py tests/unit/test_instrument_import_candidates.py &...[truncated]


[terminal] ran `git add src/jarvis_finance/cli/main.py src/jarvis_finance/dashboard/views/pag...` -> exit 128, 1 lines output


python - <<'PY'
from pathlib import Path
import csv, os, subprocess, tempfile, stat
p=Path('/home/agent/jarvis_runtime/finance-system/secrets/Github_token_FinanceManager.csv')
rows=list(csv.reader(p.r...[truncated]

Explanation:
**Troubleshooting Git Push**

I’m facing an issue with a Git push. It previously worked with GIT_ASKPASS or a remote token, and I need to ensure nothing leaks. I might have to inspect the remote configuration. It seems like the NETRC environment variable isn't being used properly; Git typically relies on /.netrc instead. Maybe setting the HOME temp variable could help, but perhaps it's better to use the credential helper environment or the GIT_ASKPASS script for a more secure approach.


[terminal] ran `python - <<'PY'
from pathlib import Path
import csv, os, subprocess, tempfile...` -> exit 128, 1 lines output


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_te...[truncated]

Explanation:
**Adjusting file handling**

It looks like I need to address some issues with my file handling. I've left a temporary file open, which isn't ideal! I should switch to using NamedTemporaryFile, ensuring that it's properly closed after use. That way, I can manage my file resources better. It's all about clean coding practices to avoid unnecessary messes later on! Now, let's implement that correction and ensure my code is running smoothly.


Output:
push_exit 0

To https://github.com/Gamexgit/FinanceManager.git
   71d07c8..40ff6e2  main -> main


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


Output:
GIT_SAFETY_OK
40ff6e2


[todo] updated task list


Erledigt, Sir.

## Qwen-Verbindungsstatus

- **Endpoint erreichbar:** ja
  `http://100.101.173.25:11435/v1/models`
- **Geladenes Modell:** `qwen-3.6-agent`
- **OpenAI-kompatible Chat-Completions:** ja
- **Synthetischer Testprompt:** erfolgreich
- **Echte Broker-Mengen/Werte an Qwen gesendet:** nein
- **Qwen-Ausgaben ins Git-Repo geschrieben:** nein

## Qwen Review-Lauf gegen Staging-Kandidaten

Qwen wurde nur als **sanitizte Review-/Normalisierungshilfe** eingesetzt.

An Qwen gingen nur:

- Kandidaten-ID
- Plattform/Quelle
- Produktname
- Ticker, falls vorhanden
- ISIN, falls vorhanden
- Währung
- Assetklasse
- Exchange, falls vorhanden
- aktueller Review-Status

Nicht gesendet:

- Mengen
- Depotwerte
- Einstandswerte
- Gewinne/Verluste
- Konto-/Kundennummern
- vollständige Broker-Dokumente
- echte Reports/Screenshots

## Aggregiertes Ergebnis

- **Qwen verfügbar:** ja
- **Modellname:** `qwen-3.6-agent`
- **Kandidaten total:** 30
- **exact_isin_match vorher:** 7
- **exact_isin_match nachher:** 7
- **probable vorher:** 0
- **probable nachher:** 0
- **needs_manual_review vorher:** 23
- **needs_manual_review nachher:** 23
- **confirmed:** 0
- **durch Qwen besser klassifiziert:** 0
- **weiterhin manuelle Prüfung nötig:** 23
- **Warnings:** 0
- **Errors:** 0

Interpretation: Qwen war erreichbar, hat aber korrekt keine neuen ISINs oder sicheren Klassifizierungen erfunden. Die 23 unsicheren Kandidaten bleiben manuell zu prüfen. Langweilig, aber richtig — die beste Sorte langweilig bei Finanzdaten.

## Review Queue verbessert

Die Equity/ETF Review UI wurde erweitert:

### Gruppe 1: Sichere ISIN-Treffer

Für `exact_isin_match` sichtbar:

- Name
- ISIN
- Ticker
- Währung
- Quelle/Plattform
- Resolver-Status

Aktionen:

- **Bestätigen**
- **Ablehnen**
- Review-Notiz möglich/empfohlen

Wichtig: Bestätigung setzt nur `confirmed` und schreibt Audit-Log. Kein Import.

### Gruppe 2: Prüfung nötig

Für `needs_manual_review` und `probable` sichtbar:

- Name
- Ticker
- Währung
- Status
- Unsicherheitskontext

Eingabefelder:

- ISIN ergänzen/korrigieren
- Ticker korrigieren
- Exchange setzen
- Währung korrigieren
- Review-Notiz

Aktionen:

- **Resolver erneut anwenden**
- **Als bestätigt speichern**
- **Zurückstellen**
- **Ablehnen**

Auch hier: keine produktive Buchung aus der Review Queue.

## Sicherheitsgrenzen eingehalten

- Keine produktiven Aktien-/ETF-Initial-Snapshots geschrieben.
- Keine Ticker-only Kandidaten automatisch bestätigt.
- Keine Cost Basis erfunden.
- Keine Transaktionshistorie erfunden.
- Keine echten Broker-Dateien ins Repo.
- Keine Mengen/Werte im Chat.
- `confirmed` bleibt 0.
- Exact-ISIN-Matches sind nur „bereit zur Bestätigung“, nicht importiert.

## Tests und Safety

- **Compile:** erfolgreich
- **pytest:** `254 passed`
- **Git-Safety:** `GIT_SAFETY_OK`
- **Git Status:** clean
- **Commit:** `40ff6e2`
- **Push:** erfolgreich nach `origin/main`

## Offene Risiken

- Die 23 Kandidaten ohne sichere ISIN bleiben manuell reviewpflichtig.
- Qwen kann Namen/Struktur unterstützen, aber nicht als authoritative mapping source dienen.
- Produktiver Initial Snapshot bleibt blockiert, bis Sie ihn ausdrücklich freigeben.


[M G] Sehr gut. Der Qwen-/Review-Lauf ist akzeptiert.

Ich möchte jetzt keinen produktiven Aktien-/ETF-Import aus den PostFinance-/True-Wealth-DOCX-Dateien durchführen.

Begründung:

* Die DOCX-Dateien sind strukturell zu unsicher.
* Nur wenige Positionen haben eine sichere ISIN.
* Viele Positionen sind blockiert oder reviewpflichtig.
* Cost Basis fehlt vollständig.
* Qwen konnte keine sicheren zusätzlichen Mappings herstellen.
* Ein automatischer Import wäre zu fehleranfällig.

Bitte stoppe deshalb den DOCX-Importpfad für Aktien/ETFs vorerst.

Stattdessen soll jetzt eine zuverlässige manuelle Aktien-/ETF-Erfassung mit Suchfunktion gebaut werden.

Ziel:
Ich möchte Aktien und ETFs selbst über das Dashboard erfassen können, ähnlich wie bei einer guten Portfolio-App:

1. Ich suche nach ISIN, Name oder Ticker.
2. Das System zeigt mögliche Instrumente.
3. Ich wähle bewusst das richtige Instrument aus.
4. Ich gebe Depot, Menge und Snapshot-/Kaufdaten ein.
5. Das System zeigt eine Review-Zusammenfassung.
6. Erst nach Bestätigung wird ein Initial Snapshot oder eine Transaktion gespeichert.
7. Jede Speicherung erzeugt Audit-Log.
8. Keine stillen Überschreibungen.

# 1. Instrument Search / Add Position Wizard

Bitte baue eine neue User-Mode-Funktion:

„Aktie/ETF hinzufügen“

Diese soll nicht als technische Admin-Form wirken, sondern als klarer Wizard.

## Schritt 1: Instrument suchen

Suchfeld:

* ISIN
* Name
* Ticker

Optionale Filter:

* Assetklasse: Aktie / ETF
* Börse/Exchange
* Währung
* Land
* Anbieter/Provider

Suchlogik:

* ISIN hat höchste Priorität.
* Ticker allein darf nie automatisch eindeutig sein.
* Name-Suche zeigt Kandidatenliste.
* Wenn mehrere Treffer: manuelle Auswahl Pflicht.
* Wenn kein Treffer: manuell anlegen möglich, aber mit Warnung.

Trefferliste anzeigen mit:

* Name
* ISIN
* Ticker
* Exchange
* Währung
* Assetklasse
* Datenquelle
* Mapping-Status
* letzter Kurs, falls lokal/verfügbar
* Kursdatum, falls lokal/verfügbar

Aktionen:

* Auswählen
* Details anzeigen
* Manuell anlegen

# 2. Instrument Resolver

Bitte baue einen Instrument Resolver.

Priorität:

1. ISIN
2. ISIN + Name
3. Ticker + Exchange + Currency
4. Name-Suche mit Kandidatenliste
5. Manuelle Anlage

Mögliche Provider vorbereiten:

* OpenFIGI für Identifier-/FIGI-Mapping
* Finnhub / FMP / Twelve Data / Stooq / Yahoo-Fallback für Symbol, Profil, Kurs oder Provider-Symbol
* manuelles Mapping, falls API nicht eindeutig ist

Für MVP:

* Wenn API-Key fehlt oder Provider nicht erreichbar ist, trotzdem manueller Workflow möglich.
* Keine Live-API beim normalen Dashboard-Rendern.
* Suche nur bei Klick auf „Suchen“.
* Ergebnisse lokal cachen, soweit sinnvoll.
* Keine automatische Entscheidung bei Mehrdeutigkeit.

# 3. Position hinzufügen als Initial Snapshot

Nach Auswahl eines Instruments:

Felder:

* Plattform/Depot:

  * PostFinance
  * True Wealth
  * Raiffeisen
  * anderes Konto
* Assetklasse:

  * Aktie
  * ETF
* Name
* ISIN
* Ticker
* Exchange
* Währung
* Menge
* Snapshot-Datum
* Einstand / Cost Basis optional
* falls Cost Basis fehlt: cost_basis_uncertain
* Kategorie:

  * Core
  * Opportunity
  * Unknown
* Notiz
* Quelle:

  * manual
  * initial_snapshot
  * broker_statement_manual_entry

Speicherung:

* Instrument anlegen oder bestehendes Instrument verwenden.
* initial_position_snapshot erzeugen.
* Keine vollständige Kaufhistorie erfinden.
* Audit-Log schreiben.
* Data-Quality-Hinweis setzen, wenn Cost Basis, FX oder Preis fehlt.

# 4. Neue Transaktion erfassen

Bitte im gleichen Modul vorbereiten oder implementieren:

* buy
* partial_sell
* full_sell
* dividend
* etf_distribution
* fee
* manual_adjustment

Für MVP reicht:

* Initial Snapshot
* Buy
* Sell
* Dividend/ETF Distribution
* Manual Adjustment

Jede Buchung:

* Review vor Speicherung
* Audit-Log
* keine stillen Änderungen
* keine Position direkt überschreiben

# 5. FX- und CHF-Logik

Bei Fremdwährungen USD/EUR:

* Originalwährung speichern
* FX-Kurs zum Datum speichern
* FX-Quelle speichern
* falls FX fehlt: fx_status=missing
* aktuelle Bewertung soll später über aktuelle Preise +
aktuellen FX möglich sein
* historische Performance bleibt unvollständig, falls historische FX/Cost Basis fehlt

Wichtig:
Fehlende Cost Basis darf nicht als 0 interpretiert werden.

# 6. UX-Anforderungen

Bitte nicht wieder eine Formularwüste bauen.

Der Workflow soll als Wizard erscheinen:

1. Suchen
2. Instrument auswählen
3. Depot & Menge
4. Einstand/Datum/Notiz
5. Review
6. Bestätigen

Im User Mode:

* keine instrument_id
* keine source_id
* keine raw provider fields
* keine technischen JSON-Felder
* keine langen IDs
* keine Debug-Tabellen

Admin Mode darf technische Details behalten.

# 7. Review Queue optional, aber nicht Hauptpfad

Die bestehende Review Queue für PostFinance/True Wealth kann bleiben, aber sie soll nicht der Hauptweg für mich sein.

Der Hauptweg ist:

* Ich gebe ISIN/Name/Ticker ein.
* Ich wähle das Instrument.
* Ich erfasse die Position manuell.

Die unsicheren DOCX-Kandidaten können höchstens als Erinnerung dienen, aber bitte nicht produktiv importieren.

# 8. PostFinance / True Wealth aktuelle Strategie

Bitte keine produktiven Imports aus den DOCX-Dateien.

Stattdessen:

* Plattformen/Konten für PostFinance und True Wealth sauber anlegen, falls noch nicht vorhanden.
* Manuelle Positionserfassung über den Wizard ermöglichen.
* Später kann ich bessere Broker-Exports liefern, idealerweise CSV/XLSX mit ISIN, Menge, Währung, Marktwert.
* Dann kann wieder ein Mapping-/Dry-Run-Import erfolgen.

# 9. Tests

Bitte Tests ergänzen für:

* Suche nach ISIN liefert Kandidat
* Suche nach Name liefert Kandidatenliste
* Ticker allein importiert nicht automatisch
* mehrere Treffer verlangen manuelle Auswahl
* manuelles Instrument anlegen möglich
* Initial Snapshot aus ausgewähltem Instrument
* Cost Basis fehlt → cost_basis_uncertain
* USD/EUR Position mit FX
* fehlender FX → fx_status=missing
* Review vor Speicherung erforderlich
* Audit-Log bei Speicherung
* keine echten Daten im Repo
* Dashboard rendert ohne Live-API
* User Mode zeigt keine technischen IDs
* Git-Safety bleibt sauber

# 10. Provider/API-Sicherheit

Bitte Provider-Abfragen nur auf expliziten Such-Klick ausführen.

Keine Provider-API beim normalen Rendern.

Wenn API-Key fehlt:

* klare Meldung
* manuelle Anlage erlauben

Wenn Rate Limit:

* klare Meldung
* keine harte Exception
* Retry/Backoff, falls bereits vorhanden

# 11. Abschluss

Nach Umsetzung:

* Compile
* pytest
* Git-Safety
* Commit
* Push

Bitte berichten:

* Instrument Search gebaut: ja/nein
* Add Position Wizard gebaut: ja/nein
* manuelle Anlage möglich: ja/nein
* ISIN-Suche möglich: ja/nein
* Name/Ticker-Suche möglich: ja/nein
* Initial Snapshot möglich: ja/nein
* Buy/Sell/Dividend vorbereitet oder implementiert
* Tests
* Git-Safety
* Commit-Hash
* Push-Status
* offene Grenzen

Wichtig:
Kein produktiver Import von PostFinance/True Wealth ohne spätere Freigabe. Die Energie geht jetzt in eine saubere, bedienbare manuelle Erfassung.


Explanation:
**Inspecting code skills**

I need to take action and check the code. I'm wondering if the finance skill is already loaded or relevant to what I'm doing. It might be necessary to load the finance skill, and perhaps I should look into how Github workflows play into this. I want to ensure everything is ready and relevant for my task to move forward smoothly! Let's make sure I'm on the right path.


[skill_view] name=finance-system-architecture (7,578 chars)


[skill_view] name=github-pr-workflow (11,689 chars)


todo

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

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

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


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/equity/manage.py from line 1 (24,646 chars)


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/storage/migrations.py from line 1 (26,923 chars)


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_13_equity_manage.py from line 1 (30,552 chars)


Explanation:
**Implementing forms efficiently**

I need to implement something quickly. The current setup has forms, but it feels like there's a "form desert." Maybe I should think about using a manual search resolver along with data functions to solve this issue. I'll also need to read the rest to effectively manage the process. There's quite a bit to consider, and I want to make sure I'm addressing everything carefully to get it right!


[read_file] read /home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/equity/manage.py from line 501 (7,387 chars)


     1|from __future__ import annotations
     2|
     3|import json
     4|from decimal import Decimal, InvalidOperation, ROUND_HALF_UP
     5|from pathlib import Path
     6|from sqlite3 import Connection
     7|
     8|from jarvis_finance.config.settings import Settings
     9|from jarvis_finance.crypto.holdings import calculate_crypto_holdings
    10|from jarvis_finance.ledger.cash import calculate_cash_balances
    11|from jarvis_finance.ledger.positions import calculate_positions
    12|from jarvis_finance.market.providers import is_price_stale
    13|
    14|MONEY = Decimal("0.01")
    15|
    16|
    17|def d(value: object, default: str = "0") -> Decimal:
    18|    if value is None or str(value).strip() == "":
    19|        return Decimal(default)
    20|    try:
    21|        return Decimal(str(value))
    22|    except InvalidOperation:
    23|        return Decimal(default)
    24|
    25|
    26|def dec_text(value: Decimal | int | str | None, places: str | None = None) -> str:
    27|    if value is None:
    28|        return ""
    29|    value = d(value)
    30|    if places:
    31|        value = value.quantize(Decimal(places), rounding=ROUND_HALF_UP)
    32|    return format(value, "f")
    33|
    34|
    35|def grouped_number(value: Decimal | int | str, places: str = "0.01") -> str:
    36|    text = dec_text(value, places)
    37|    whole, _, frac = text.partition(".")
    38|    sign = "-" if whole.startswith("-") else ""
    39|    whole = whole.lstrip("-")
    40|    groups: list[str] = []
    41|    while whole:
    42|        groups.append(whole[-3:])
    43|        whole = whole[:-3]
    44|    grouped = "'".join(reversed(groups)) if groups else "0"
    45|    return f"{sign}{grouped}{('.' + frac) if frac else ''}"
    46|
    47|
    48|def chf_text(value: Decimal | int | str | None) -> str:
    49|    if value is None:
    50|        return "nicht bewertet"
    51|    return f"CHF {grouped_number(value)}"
    52|
    53|
    54|def date_text(value: str | None) -> str:
    55|    if not value:
    56|        return ""
    57|    raw = str(value)
    58|    if "T" in raw:
    59|        return raw.split("T", 1)[0]
    60|    return raw[:10]
    61|
    62|
    63|def money_text(value: Decimal | None, currency: str = "CHF") -> str:
    64|    if currency.upper() == "CHF":
    65|        return chf_text(value)
    66|    if value is None:
    67|        return f"n/a {currency}"
    68|    return f"{dec_text(value, '0.01')} {currency}"
    69|
    70|
    71|STATUS_LABELS = {
    72|    "missing_market_price": "Preis fehlt",
    73|    "missing_provider_symbol": "Mapping fehlt",
    74|    "missing_price": "Preis fehlt",
    75|    "missing_fx": "FX-Kurs fehlt",
    76|    "cost_basis_uncertain": "Einstand unvollständig",
    77|    "incomplete": "Unvollständig",
    78|    "snapshot_only": "Snapshot-only",
    79|    "fresh": "Aktuell",
    80|    "stale": "Veraltet",
    81|    "missing": "Preis fehlt",
    82|    "unknown": "Ungeklärt",
    83|    "mapped": "Zugeordnet",
    84|    "confirmed": "Bestätigt",
    85|    "user_confirmed": "Bestätigt",
    86|    "ok": "Aktuell",
    87|    "warning": "Hinweis",
    88|    "critical": "Kritisch",
    89|    "kritisch": "Kritisch",
    90|    "warnung": "Wichtig",
    91|    "info": "Info",
    92|    "active": "Aktiv",
    93|    "inactive": "Inaktiv",
    94|    "verified": "Verifiziert",
    95|}
    96|
    97|
    98|def translate_status(status: str | None) -> str:
    99|    if not status:
   100|        return "Ungeklärt"
   101|    return STATUS_LABELS.get(str(status), str(status).replace("_", " ").capitalize())
   102|
   103|
   104|def is_technical_note(text: str | None) -> bool:
   105|    if not text:
   106|        return False
   107|    lowered = text.lower()
   108|    markers = ["productive initial crypto snapshot import", "no wallet address from xlsx", "initial crypto snapshot import"]
   109|    return any(marker in lowered for marker in markers)
   110|
   111|
   112|def user_note(text: str | None) -> str:
   113|    return "" if is_technical_note(text) else (text or "")
   114|
   115|
   116|def visible_user_columns(row: dict[str, str]) -> list[str]:
   117|    hidden = {"asset_id", "wallet_id", "instrument_id", "entity_id", "rule_id", "source_id", "provider_symbol_status", "hedge_status", "dedup_key", "fingerprint", "raw_json"}
   118|    return [k for k in row if not k.startswith("_") and k not in hidden]
   119|
   120|
   121|def percent_text(value: Decimal | None) -> str:
   122|    if value is None:
   123|        return ""
   124|    return f"{dec_text(value, '0.01')}%"
   125|
   126|
   127|def price_status(price: dict[str, str] | None) -> str:
   128|    if not price:
   129|        return "missing"
   130|    return price.get("quality_status") or "missing"
   131|
   132|
   133|def _latest_price_time(conn: Connection, *, currency: str = "CHF") -> str:
   134|    row = conn.execute(
   135|        """
   136|        SELECT COALESCE(MAX(provider_timestamp), MAX(fetched_at)) AS ts
   137|        FROM crypto_prices WHERE price_currency=?
   138|        """,
   139|        (currency.upper(),),
   140|    ).fetchone()
   141|    return row["ts"] if row and row["ts"] else ""
   142|
   143|
   144|def _crypto_value_rows(conn: Connection, *, price_max_age_seconds: int = 86_400) -> list[dict[str, object]]:
   145|    holdings = calculate_crypto_holdings(conn)
   146|    rows: list[dict[str, object]] = []
   147|    for asset_id, holding in sorted(holdings.total_by_asset.items()):
   148|        asset = conn.execute("SELECT * FROM crypto_assets WHERE asset_id=?", (asset_id,)).fetchone()
   149|        if not asset:
   150|            continue
   151|        price = latest_crypto_price(conn, asset_id, "CHF", max_age_seconds=price_max_age_seconds)
   152|        price_dec = d(price["price"]) if price and price["price"] else None
   153|        value = holding.quantity * price_dec if price_dec is not None else None
   154|        wallets = {wid for (wid, aid), wh in holdings.wallet_holdings.items() if aid == asset_id and wh.quantity != 0}
   155|        rows.append({
   156|            "asset_id": asset_id,
   157|            "coin": asset["coin_name"],
   158|            "symbol": asset["symbol"],
   159|            "coingecko_id": asset["coingecko_id"] or "",
   160|            "quantity": holding.quantity,
   161|            "price": price_dec,
   162|            "value": value,
   163|            "wallet_count": len(wallets),
   164|            "price_status": price_status(price),
   165|            "last_price_update": (price["provider_timestamp"] or price["fetched_at"]) if price else "",
   166|        })
   167|    return rows
   168|
   169|
   170|def rowdicts(rows) -> list[dict[str, str]]:
   171|    return [{k: ("" if row[k] is None else str(row[k])) for k in row.keys()} for row in rows]
   172|
   173|
   174|def db_exists(path: str | Path) -> bool:
   175|    return Path(path).expanduser().exists()
   176|
   177|
   178|def runtime_outside_repo(settings: Settings, repo_root: Path) -> bool:
   179|    try:
   180|        settings.runtime_paths.base_dir.resolve().relative_to(repo_root.resolve())
   181|        return False
   182|    except ValueError:
   183|        return True
   184|
   185|
   186|def latest_crypto_price(conn: Connection, asset_id: str, currency: str = "CHF", *, max_age_seconds: int = 86_400) -> dict[str, str] | None:
   187|    row = conn.execute(
   188|        """
   189|        SELECT price, price_currency, fetched_at, provider_timestamp, quality_status, provider
   190|        FROM crypto_prices
   191|        WHERE asset_id=? AND price_currency=?
   192|        ORDER BY COALESCE(provider_timestamp, fetched_at, '') DESC, fetched_at DESC LIMIT 1
   193|        """,
   194|        (asset_id, currency.upper()),
   195|    ).fetchone()
   196|    if not row:
   197|        return None
   198|    item = dict(row)
   199|    ts = item.get("provider_timestamp") or item.get("fetched_at")
   200|    if item.get("quality_status") == "fresh" and is_price_stale(ts, max_age_seconds=max_age_seconds):
   201|        item["quality_status"] = "stale"
   202|    return item
   203|
   204|
   205|def get_crypto_summary_cards(conn: Connection, *, price_max_age_seconds: int = 86_400) -> dict[str, str]:
   206|    rows = _crypto_value_rows(conn, price_max_age_seconds=price_max_age_seconds)
   207|    holdings = calculate_crypto_holdings(conn)
   208|    total_value = sum((r["value"] for r in rows if r["value"] is not None), Decimal("0"))
   209|    missing = sum(1 for r in rows if r["price_status"] == "missing")
   210|    stale = sum(1 for r in rows if r["price_status"] == "stale")
   211|    wallets_with_holdings = {wid for (wid, _aid), holding in holdings.wallet_holdings.items() if holding.quantity != 0}
   212|    status = "ok" if missing == 0 and stale == 0 else ("warning" if missing or stale else "ok")
   213|    return {
   214|        "total_value_chf": dec_text(total_value),
   215|        "coin_count": str(len(rows)),
   216|        "wallets_with_holdings": str(len(wallets_with_holdings)),
   217|        "holding_count": str(sum(1 for h in holdings.wallet_holdings.values() if h.quantity != 0)),
   218|        "coins_without_price": str(missing),
   219|        "stale_price_count": str(stale),
   220|        "last_price_update_at": _latest_price_time(conn),
   221|        "data_quality_status": status,
   222|    }
   223|
   224|
   225|def _wallet_names_for_asset(conn: Connection, asset_id: str) -> list[str]:
   226|    holdings = calculate_crypto_holdings(conn)
   227|    names: list[str] = []
   228|    for (wallet_id, aid), holding in holdings.wallet_holdings.items():
   229|        if aid == asset_id and holding.quantity != 0:
   230|            row = conn.execute("SELECT wallet_name FROM crypto_wallets WHERE wallet_id=?", (wallet_id,)).fetchone()
   231|            if row:
   232|                names.append(row["wallet_name"])
   233|    return sorted(names)
   234|
   235|
   236|def get_crypto_coin_summary(
   237|    conn: Connection,
   238|    *,
   239|    admin_mode: bool = False,
   240|    price_max_age_seconds: int = 86_400,
   241|    sort_by: str = "Wert absteigend",
   242|    search: str | None = None,
   243|    status_filter: str | None = None,
   244|    wallet_filter: str | None = None,
   245|) -> list[dict[str, str]]:
   246|    raw = _crypto_value_rows(conn, price_max_age_seconds=price_max_age_seconds)
   247|    total = sum((r["value"] for r in raw if r["value"] is not None), Decimal("0"))
   248|    out: list[dict[str, str]] = []
   249|    for r in sorted(raw, key=lambda item: item["value"] or Decimal("-1"), reverse=True):
   250|        value = r["value"]
   251|        share = ((value / total) * Decimal("100")) if value is not None and total else None
   252|        item = {
   253|            "coin": str(r["coin"]),
   254|            "symbol": str(r["symbol"]),
   255|            "gesamtmenge": dec_text(r["quantity"]),
   256|            "kurs_chf": chf_text(r["price"]) if r["price"] is not None else "nicht bewertet",
   257|            "gesamtwert_chf": chf_text(value) if value is not None else "nicht bewertet",
   258|            "anteil": percent_text(share),
   259|            "wallets": str(r["wallet_count"]),
   260|            "status": translate_status(str(r["price_status"])),
   261|

Use offset=261 to continue reading (showing 1-260 of 1103 lines)


return items

def get_instrument_catalog_entries(conn: Connection, query: str | None = None, asset_class: str | None = None) -> list[dict[str, str]]:

if query:

from jarvis_finance.market_data.catalog import search_local_catalog

return [r.__dict__ for r in search_local_catalog(conn, query, asset_class)]

return rowdicts(conn.execute(sql, tuple(params)).fetchall())

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

total = conn.execute("SELECT COUNT(*) AS c FROM instrument_catalog_entries").fetchone()["c"]

multi = conn.execute("SELECT COUNT(*) AS c FROM (SELECT isin FROM instrument_catalog_entries WHERE COALESCE(isin,'')<>'' GROUP BY isin HAVING COUNT(DISTINCT COALESCE(exchange,'') || ':' || COALESCE(trading_currency,''))>1)").fetchone()["c"]

no_provider = conn.execute("SELECT COUNT(*) AS c FROM instrument_catalog_entries WHERE COALESCE(provider_symbol,'')='' ").fetchone()["c"]


   780|        ("instrument_status_unknown", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='instrument_status_unknown'", "warnung"),
   781|        ("provider_symbol_missing", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='missing_provider_symbol'", "warnung"),
   782|        ("candidate_review_required", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='candidate_review_required'", "warnung"),
   783|        ("valuation_not_ready", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id IN ('missing_provider_symbol','missing_market_price','missing_fx','hedge_status_unknown','instrument_status_unknown','candidate_review_required')", "warnung"),
   784|        ("instrument status unknown", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='instrument_status_unknown'", "warnung"),
   785|        ("manual FX override required", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id IN ('manual_override_required','manual_fx_override_required')", "kritisch"),
   786|        ("fehlende Market Prices", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='missing_market_price'", "warnung"),
   787|        ("stale Market Prices", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='stale_market_price'", "warnung"),
   788|        ("ambiguous Instrument Mapping", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='ambiguous_instrument_mapping'", "warnung"),
   789|        ("price currency mismatch", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='price_currency_mismatch'", "warnung"),
   790|        ("hedge status unknown", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='hedge_status_unknown'", "warnung"),
   791|        ("stale valuation", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='stale_valuation'", "warnung"),
   792|        ("corporate action suspected", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='corporate_action_suspected'", "warnung"),
   793|        ("split/corporate action review", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='split_or_corporate_action_review_required'", "warnung"),
   794|        ("delisted/suspended", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='delisted_or_suspended'", "warnung"),
   795|        ("snapshot_only Alerts", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='snapshot_only'", "info"),
   796|        ("cost_basis_uncertain Alerts", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='cost_basis_uncertain'", "warnung"),
   797|        ("fehlende CoinGecko-ID", "SELECT COUNT(*) AS c FROM crypto_assets WHERE coingecko_id IS NULL OR coingecko_id='' ", "warnung"),
   798|        ("fehlende Crypto-Preise", "SELECT COUNT(*) AS c FROM crypto_assets ca LEFT JOIN crypto_prices cp ON cp.asset_id=ca.asset_id WHERE cp.crypto_price_id IS NULL", "warnung"),
   799|        ("fehlende ISIN", "SELECT COUNT(*) AS c FROM instrument_mappings WHERE asset_class IN ('equity','etf') AND (isin IS NULL OR isin='') AND mapping_status!='ignored'", "warnung"),
   800|        ("Catalog Instrumente ohne ISIN", "SELECT COUNT(*) AS c FROM instrument_catalog_entries WHERE asset_class IN ('equity','stock','etf') AND (isin IS NULL OR isin='')", "warnung"),
   801|        ("Catalog Ticker ohne Exchange", "SELECT COUNT(*) AS c FROM instrument_catalog_entries WHERE COALESCE(ticker,'')<>'' AND COALESCE(exchange,'')=''", "warnung"),
   802|        ("Catalog mehrere Listings gleicher ISIN", "SELECT COUNT(*) AS c FROM (SELECT isin FROM instrument_catalog_entries WHERE COALESCE(isin,'')<>'' GROUP BY isin HAVING COUNT(DISTINCT COALESCE(exchange,'') || ':' || COALESCE(trading_currency,''))>1)", "info"),
   803|        ("Manual Review needed", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id='manual_review_needed'", "warnung"),
   804|        ("Mapping-Kandidaten unbestätigt", "SELECT COUNT(*) AS c FROM instrument_price_mapping_candidates WHERE review_status IN ('proposed','needs_manual_review')", "warnung"),
   805|        ("Instrumente mit mehreren Mapping-Kandidaten", "SELECT COUNT(*) AS c FROM (SELECT instrument_id FROM instrument_price_mapping_candidates WHERE review_status IN ('proposed','needs_manual_review') GROUP BY instrument_id HAVING COUNT(*) > 1)", "warnung"),
   806|        ("Valuation not ready", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id IN ('missing_provider_symbol','missing_market_price','missing_fx','hedge_status_unknown','instrument_status_unknown')", "warnung"),
   807|        ("fehlende FX in Import-Reviews", "SELECT COUNT(*) AS c FROM instrument_mappings WHERE quality_flags_json LIKE '%missing_fx%' AND mapping_status!='ignored'", "warnung"),
   808|        ("snapshot_only Positionen", "SELECT COUNT(*) AS c FROM instrument_mappings WHERE quality_flags_json LIKE '%snapshot_only%' AND mapping_status!='ignored'", "info"),
   809|        ("cost_basis_uncertain", "SELECT COUNT(*) AS c FROM instrument_mappings WHERE quality_flags_json LIKE '%cost_basis_uncertain%' AND mapping_status!='ignored'", "warnung"),
   810|        ("Instrument Import Kandidaten unbestätigt", "SELECT COUNT(*) AS c FROM instrument_import_candidates WHERE mapping_status NOT IN ('confirmed','rejected')", "warnung"),
   811|        ("Instrument Import Kandidaten ohne ISIN", "SELECT COUNT(*) AS c FROM instrument_import_candidates WHERE COALESCE(proposed_isin, raw_isin, '')='' AND mapping_status!='rejected'", "warnung"),
   812|        ("aktive Alerts", "SELECT COUNT(*) AS c FROM alerts WHERE status='active'", "warnung"),
   813|        ("resolved Alerts", "SELECT COUNT(*) AS c FROM alerts WHERE status='resolved'", "ok"),
   814|        ("alte aktive Alerts", "SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND last_seen_at < datetime('now', '-7 days')", "info"),
   815|    ]
   816|    for label, sql, severity in checks:
   817|        row = conn.execute(sql).fetchone()
   818|        count = int(row["c"] or 0) if row else 0
   819|        sev = severity if count else "ok"
   820|        items.append({"check": label, "count": str(count), "severity": sev})
   821|    for flag_row in get_review_quality_flag_counts(conn):
   822|        count = int(flag_row["open_items"] or 0)
   823|        items.append({"check": f"Review Flag: {flag_row['quality_flag']}", "count": str(count), "severity": "warnung" if count else "ok"})
   824|    for ready in get_review_readiness_counts(conn):
   825|        count = int(ready["count"] or 0)
   826|        sev = "ok" if ready["readiness_status"] in {"ready_for_import", "ignored"} else ("warnung" if count else "ok")
   827|        items.append({"check": f"Review Readiness: {ready['readiness_status']}", "count": str(count), "severity": sev})
   828|    for source in get_ready_for_import_by_source(conn):
   829|        if int(source["total"] or 0) and int(source["ready_for_import"] or 0) == 0:
   830|            items.append({"check": f"Quelle ohne ready_for_import: {source['source_platform']}", "count": source["total"], "severity": "warnung"})
   831|    missing_snapshot = conn.execute("SELECT COUNT(*) AS c FROM broker_import_dry_runs WHERE snapshot_date_status='missing'").fetchone()
   832|    count = int(missing_snapshot["c"] or 0) if missing_snapshot else 0
   833|    items.append({"check": "Dry Runs ohne Snapshot Date", "count": str(count), "severity": "warnung" if count else "ok"})
   834|    return items
   835|
   836|
   837|def get_instrument_catalog_entries(conn: Connection, query: str | None = None, asset_class: str | None = None) -> list[dict[str, str]]:
   838|    if query:
   839|        from jarvis_finance.market_data.catalog import search_local_catalog
   840|        return [r.__dict__ for r in search_local_catalog(conn, query, asset_class)]
   841|    sql = """
   842|        SELECT catalog_entry_id, asset_class, name, isin, ticker, exchange, trading_currency,
   843|               instrument_currency, provider, provider_symbol, provider_market, hedge_status,
   844|               instrument_status, valuation_policy, source, source_confidence, last_verified_at
   845|        FROM instrument_catalog_entries
   846|    """
   847|    params: list[object] = []
   848|    if asset_class:
   849|        sql += " WHERE asset_class=?"
   850|        params.append(asset_class.lower())
   851|    sql += " ORDER BY name, exchange, trading_currency LIMIT 100"
   852|    return rowdicts(conn.execute(sql, tuple(params)).fetchall())
   853|
   854|
   855|def get_instrument_catalog_summary(conn: Connection) -> dict[str, str]:
   856|    total = conn.execute("SELECT COUNT(*) AS c FROM instrument_catalog_entries").fetchone()["c"]
   857|    multi = conn.execute("SELECT COUNT(*) AS c FROM (SELECT isin FROM instrument_catalog_entries WHERE COALESCE(isin,'')<>'' GROUP BY isin HAVING COUNT(DISTINCT COALESCE(exchange,'') || ':' || COALESCE(trading_currency,''))>1)").fetchone()["c"]
   858|    no_provider = conn.execute("SELECT COUNT(*) AS c FROM instrument_catalog_entries WHERE COALESCE(provider_symbol,'')='' ").fetchone()["c"]
   859|    manual = conn.execute("SELECT COUNT(*) AS c FROM alerts WHERE status='active' AND rule_id IN ('manual_review_needed','missing_provider_symbol','ambiguous_provider_mapping')").fetchone()["c"]
   860|    return {"catalog_entries": str(total or 0), "multiple_listings": str(multi or 0), "without_provider_symbol": str(no_provider or 0), "manual_review_required": str(manual or 0)}
   861|
   862|
   863|def get_instrument_mapping_review(conn: Connection) -> list[dict[str, str]]:
   864|    rows = conn.execute(
   865|        """
   866|        SELECT i.instrument_id, i.isin, i.name, i.ticker, i.exchange, i.currency,
   867|               i.hedge_status, i.instrument_status, i.valuation_policy,
   868|               COALESCE(m.mapping_status, 'missing_provider_symbol') AS mapping_status,
   869|               COALESCE(m.provider, '') AS provider,
   870|               COALESCE(m.provider_symbol, '') AS provider_symbol,
   871|               COUNT(c.candidate_id) AS candidate_count
   872|        FROM instruments i
   873|        JOIN transactions t ON t.instrument_id=i.instrument_id
   874|        LEFT JOIN instrument_price_mappings m ON m.instrument_id=i.instrument_id AND m.mapping_status='mapped'
   875|        LEFT JOIN instrument_price_mapping_candidates c ON c.instrument_id=i.instrument_id AND c.review_status IN ('proposed','needs_manual_review')
   876|        WHERE t.transaction_type='initial_position_snapshot'
   877|          AND t.source_type='broker_import_reviewed_snapshot'
   878|          AND COALESCE(t.is_voided,0)=0
   879|        GROUP BY i.instrument_id
   880|        ORDER BY i.isin, i.name
   881|        """
   882|    ).fetchall()
   883|    return rowdicts(rows)
   884|
   885|
   886|def get_instrument_import_candidates(conn: Connection, status: str | None = None) -> list[dict[str, str]]:
   887|    sql = """
   888|        SELECT candidate_id, source_file, platform, account, source_row_number,
   889|               raw_name, raw_ticker, raw_isin, raw_currency,
   890|

Use offset=890 to continue reading (showing 780-889 of 1103 lines)


     1|from __future__ import annotations
     2|
     3|import re
     4|from dataclasses import dataclass, field
     5|from sqlite3 import Connection
     6|from typing import Protocol
     7|
     8|from jarvis_finance.audit.log import record_audit_event
     9|from jarvis_finance.imports.common import stable_id, utc_now
    10|from jarvis_finance.market_data.candidates import ProviderCandidate, LookupUnavailable
    11|from jarvis_finance.market_data.instruments import ensure_instrument_metadata_quality
    12|from jarvis_finance.quality.alerts import create_alert
    13|
    14|ASSET_CLASSES = {"equity", "stock", "etf", "crypto", "cash", "other"}
    15|CONFIDENCES = {"high", "medium", "low"}
    16|HEDGE_STATUSES = {"hedged", "unhedged", "unknown"}
    17|INSTRUMENT_STATUSES = {"active", "suspended", "delisted", "merged", "inactive", "unknown", "suspected_inactive"}
    18|VALUATION_POLICIES = {"live_price", "last_known_price", "manual_value", "exclude_from_auto_price_update"}
    19|
    20|
    21|def normalize_name(value: str | None) -> str:
    22|    text = (value or "").strip().lower()
    23|    return re.sub(r"\s+", " ", re.sub(r"[^a-z0-9]+", " ", text)).strip()
    24|
    25|
    26|@dataclass(frozen=True)
    27|class CatalogEntryInput:
    28|    asset_class: str
    29|    name: str
    30|    isin: str | None = None
    31|    ticker: str | None = None
    32|    exchange: str | None = None
    33|    trading_currency: str | None = None
    34|    instrument_currency: str | None = None
    35|    provider: str | None = None
    36|    provider_symbol: str | None = None
    37|    provider_market: str | None = None
    38|    country: str | None = None
    39|    sector: str | None = None
    40|    issuer: str | None = None
    41|    fund_type: str | None = None
    42|    is_currency_hedged: bool | None = None
    43|    hedged_to_currency: str | None = None
    44|    hedge_status: str = "unknown"
    45|    instrument_status: str = "unknown"
    46|    valuation_policy: str = "live_price"
    47|    source: str = "manual"
    48|    source_confidence: str = "low"
    49|    notes: str | None = None
    50|
    51|
    52|@dataclass
    53|class CatalogSearchResult:
    54|    catalog_entry_id: str | None
    55|    asset_class: str
    56|    name: str
    57|    isin: str | None = None
    58|    ticker: str | None = None
    59|    exchange: str | None = None
    60|    trading_currency: str | None = None
    61|    instrument_currency: str | None = None
    62|    provider: str | None = None
    63|    provider_symbol: str | None = None
    64|    provider_market: str | None = None
    65|    hedge_status: str = "unknown"
    66|    instrument_status: str = "unknown"
    67|    confidence: str = "low"
    68|    source: str = "local_catalog"
    69|    evidence_note: str = ""
    70|
    71|
    72|class CatalogLookupProvider(Protocol):
    73|    name: str
    74|
    75|    def search(self, *, query: str, asset_class: str | None = None) -> list[CatalogSearchResult]:
    76|        ...
    77|
    78|
    79|class UnavailableCatalogLookupProvider:
    80|    name = "unavailable"
    81|
    82|    def search(self, *, query: str, asset_class: str | None = None) -> list[CatalogSearchResult]:
    83|        raise LookupUnavailable("external instrument lookup is not configured")
    84|
    85|
    86|def _validate_entry(entry: CatalogEntryInput) -> None:
    87|    if entry.asset_class.lower() not in ASSET_CLASSES:
    88|        raise ValueError("invalid asset_class")
    89|    if not entry.name.strip():
    90|        raise ValueError("name is required")
    91|    if entry.source_confidence not in CONFIDENCES:
    92|        raise ValueError("invalid source_confidence")
    93|    if entry.hedge_status not in HEDGE_STATUSES:
    94|        raise ValueError("invalid hedge_status")
    95|    if entry.instrument_status not in INSTRUMENT_STATUSES:
    96|        raise ValueError("invalid instrument_status")
    97|    if entry.valuation_policy not in VALUATION_POLICIES:
    98|        raise ValueError("invalid valuation_policy")
    99|
   100|
   101|def upsert_catalog_entry(conn: Connection, entry: CatalogEntryInput, *, note: str, created_by: str = "system") -> str:
   102|    if not note.strip():
   103|        raise ValueError("catalog entry upsert requires a note")
   104|    _validate_entry(entry)
   105|    isin = (entry.isin or "").strip().upper() or None
   106|    ticker = (entry.ticker or "").strip().upper() or None
   107|    exchange = (entry.exchange or "").strip().upper() or None
   108|    trading_currency = (entry.trading_currency or "").strip().upper() or None
   109|    instrument_currency = (entry.instrument_currency or trading_currency or "").strip().upper() or None
   110|    provider = (entry.provider or "manual").strip().lower()
   111|    provider_symbol = (entry.provider_symbol or "").strip() or None
   112|    provider_market = (entry.provider_market or exchange or "").strip().upper() or None
   113|    catalog_entry_id = stable_id("catalog", entry.asset_class.lower(), isin or "", ticker or "", exchange or "", trading_currency or "", provider, provider_symbol or "", entry.name)
   114|    now = utc_now()
   115|    old = conn.execute("SELECT * FROM instrument_catalog_entries WHERE catalog_entry_id=?", (catalog_entry_id,)).fetchone()
   116|    conn.execute(
   117|        """
   118|        INSERT INTO instrument_catalog_entries(
   119|            catalog_entry_id, asset_class, name, normalized_name, isin, ticker, exchange,
   120|            trading_currency, instrument_currency, provider, provider_symbol, provider_market,
   121|            country, sector, issuer, fund_type, is_currency_hedged, hedged_to_currency,
   122|            hedge_status, instrument_status, valuation_policy, source, source_confidence,
   123|            last_verified_at, notes, created_at, updated_at
   124|        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
   125|        ON CONFLICT(catalog_entry_id) DO UPDATE SET
   126|            name=excluded.name,
   127|            normalized_name=excluded.normalized_name,
   128|            provider_symbol=excluded.provider_symbol,
   129|            provider_market=excluded.provider_market,
   130|            country=excluded.country,
   131|            sector=excluded.sector,
   132|            issuer=excluded.issuer,
   133|            fund_type=excluded.fund_type,
   134|            is_currency_hedged=excluded.is_currency_hedged,
   135|            hedged_to_currency=excluded.hedged_to_currency,
   136|            hedge_status=excluded.hedge_status,
   137|            instrument_status=excluded.instrument_status,
   138|            valuation_policy=excluded.valuation_policy,
   139|            source=excluded.source,
   140|            source_confidence=excluded.source_confidence,
   141|            last_verified_at=excluded.last_verified_at,
   142|            notes=excluded.notes,
   143|            updated_at=excluded.updated_at
   144|        """,
   145|        (
   146|            catalog_entry_id, entry.asset_class.lower(), entry.name.strip(), normalize_name(entry.name), isin, ticker, exchange,
   147|            trading_currency, instrument_currency, provider, provider_symbol, provider_market,
   148|            entry.country, entry.sector, entry.issuer, entry.fund_type, 1 if entry.is_currency_hedged else 0 if entry.is_currency_hedged is False else None,
   149|            (entry.hedged_to_currency or "").strip().upper() or None, entry.hedge_status, entry.instrument_status,
   150|            entry.valuation_policy, entry.source, entry.source_confidence, now, entry.notes, now, now,
   151|        ),
   152|    )
   153|    record_audit_event(
   154|        conn,
   155|        source="instrument_catalog",
   156|        action="upsert_catalog_entry",
   157|        entity_type="instrument_catalog_entry",
   158|        entity_id=catalog_entry_id,
   159|        old_values=dict(old) if old else None,
   160|        new_values={"asset_class": entry.asset_class.lower(), "isin": isin, "ticker": ticker, "exchange": exchange, "provider": provider, "provider_symbol": provider_symbol},
   161|        user_text_note=note,
   162|        confirmed=True,
   163|        created_by=created_by,
   164|    )
   165|    conn.commit()
   166|    return catalog_entry_id
   167|
   168|
   169|def search_local_catalog(conn: Connection, query: str, asset_class: str | None = None) -> list[CatalogSearchResult]:
   170|    q = (query or "").strip()
   171|    if not q:
   172|        return []
   173|    clauses = ["(upper(COALESCE(isin,''))=upper(?) OR upper(COALESCE(ticker,''))=upper(?) OR upper(COALESCE(provider_symbol,''))=upper(?) OR normalized_name LIKE ? OR upper(COALESCE(exchange,''))=upper(?) OR upper(COALESCE(trading_currency,''))=upper(?))"]
   174|    params: list[object] = [q, q, q, f"%{normalize_name(q)}%", q, q]
   175|    if asset_class:
   176|        ac = asset_class.lower()
   177|        if ac == "equity":
   178|            clauses.append("asset_class IN ('equity','stock')")
   179|        else:
   180|            clauses.append("asset_class=?")
   181|

Use offset=181 to continue reading (showing 1-180 of 259 lines)


   169|def search_local_catalog(conn: Connection, query: str, asset_class: str | None = None) -> list[CatalogSearchResult]:
   170|    q = (query or "").strip()
   171|    if not q:
   172|        return []
   173|    clauses = ["(upper(COALESCE(isin,''))=upper(?) OR upper(COALESCE(ticker,''))=upper(?) OR upper(COALESCE(provider_symbol,''))=upper(?) OR normalized_name LIKE ? OR upper(COALESCE(exchange,''))=upper(?) OR upper(COALESCE(trading_currency,''))=upper(?))"]
   174|    params: list[object] = [q, q, q, f"%{normalize_name(q)}%", q, q]
   175|    if asset_class:
   176|        ac = asset_class.lower()
   177|        if ac == "equity":
   178|            clauses.append("asset_class IN ('equity','stock')")
   179|        else:
   180|            clauses.append("asset_class=?")
   181|            params.append(ac)
   182|    rows = conn.execute(
   183|        "SELECT * FROM instrument_catalog_entries WHERE " + " AND ".join(clauses) + " ORDER BY source_confidence, name, exchange, trading_currency LIMIT 50",
   184|        tuple(params),
   185|    ).fetchall()
   186|    return [
   187|        CatalogSearchResult(
   188|            catalog_entry_id=row["catalog_entry_id"], asset_class=row["asset_class"], name=row["name"], isin=row["isin"], ticker=row["ticker"], exchange=row["exchange"],
   189|            trading_currency=row["trading_currency"], instrument_currency=row["instrument_currency"], provider=row["provider"], provider_symbol=row["provider_symbol"],
   190|            provider_market=row["provider_market"], hedge_status=row["hedge_status"] or "unknown", instrument_status=row["instrument_status"] or "unknown",
   191|            confidence=row["source_confidence"] or "low", source="local_catalog", evidence_note="local catalog match",
   192|        )
   193|        for row in rows
   194|    ]
   195|
   196|
   197|def search_instruments(conn: Connection, query: str, asset_class: str | None = None, *, providers: list[CatalogLookupProvider] | None = None) -> tuple[list[CatalogSearchResult], list[str]]:
   198|    results = search_local_catalog(conn, query, asset_class)
   199|    warnings: list[str] = []
   200|    for provider in providers or []:
   201|        try:
   202|            external = provider.search(query=query, asset_class=asset_class)
   203|        except Exception:
   204|            warnings.append("provider_lookup_unavailable")
   205|            create_alert(conn, priority="warnung", category="market_data", entity_type="instrument_catalog", entity_id="external_lookup", rule_id="provider_lookup_unavailable", message="External instrument catalog provider unavailable.", evidence={"provider": getattr(provider, "name", "unknown")}, fingerprint=f"catalog_provider_lookup_unavailable:{getattr(provider, 'name', 'unknown')}")
   206|            continue
   207|        results.extend(external)
   208|    conn.commit()
   209|    return results, warnings
   210|
   211|
   212|def prepare_catalog_from_instruments(conn: Connection, *, instrument_ids: list[str], source: str = "true_wealth", note: str = "prepare catalog from existing instruments") -> dict[str, int]:
   213|    summary = {"instruments_total": 0, "catalog_entries_created": 0, "multiple_listings": 0, "manual_review_required": 0, "hedge_unknown": 0, "instrument_status_unknown": 0, "valuation_ready": 0}
   214|    seen_isin: dict[str, set[tuple[str | None, str | None]]] = {}
   215|    for instrument_id in instrument_ids:
   216|        row = conn.execute("SELECT * FROM instruments WHERE instrument_id=?", (instrument_id,)).fetchone()
   217|        if row is None:
   218|            continue
   219|        summary["instruments_total"] += 1
   220|        ensure_instrument_metadata_quality(conn, instrument_id=instrument_id)
   221|        entry = CatalogEntryInput(
   222|            asset_class="etf" if (row["asset_class"] or "").lower() == "etf" else "equity",
   223|            name=row["name"], isin=row["isin"], ticker=row["ticker"], exchange=row["exchange"], trading_currency=row["trading_currency"] or row["currency"], instrument_currency=row["currency"],
   224|            provider="manual", provider_symbol=None, provider_market=row["exchange"], is_currency_hedged=bool(row["is_currency_hedged"]) if row["is_currency_hedged"] is not None else None,
   225|            hedged_to_currency=row["hedged_to_currency"], hedge_status=row["hedge_status"] or "unknown", instrument_status=row["instrument_status"] or "unknown",
   226|            valuation_policy=row["valuation_policy"] or "live_price", source=source, source_confidence="medium" if row["isin"] else "low", notes="Prepared from existing runtime instrument metadata; no portfolio amounts included.",
   227|        )
   228|        before = conn.execute("SELECT COUNT(*) AS c FROM instrument_catalog_entries").fetchone()["c"]
   229|        upsert_catalog_entry(conn, entry, note=note)
   230|        after = conn.execute("SELECT COUNT(*) AS c FROM instrument_catalog_entries").fetchone()["c"]
   231|        if after > before:
   232|            summary["catalog_entries_created"] += 1
   233|        if row["isin"]:
   234|            seen_isin.setdefault(row["isin"], set()).add((row["exchange"], row["currency"]))
   235|        if (row["hedge_status"] or "unknown") == "unknown":
   236|            summary["hedge_unknown"] += 1
   237|        if (row["instrument_status"] or "unknown") == "unknown":
   238|            summary["instrument_status_unknown"] += 1
   239|        if row["provider_symbol"] and row["instrument_status"] == "active" and row["hedge_status"] != "unknown":
   240|            summary["valuation_ready"] += 1
   241|        else:
   242|            summary["manual_review_required"] += 1
   243|            create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="manual_review_needed", message="Instrument requires manual catalog/mapping review before valuation readiness.", evidence={"source": source}, fingerprint="manual_review_needed")
   244|    summary["multiple_listings"] = sum(1 for listings in seen_isin.values() if len(listings) > 1)
   245|    conn.commit()
   246|    return summary
   247|
   248|
   249|def catalog_entry_to_provider_candidate(conn: Connection, catalog_entry_id: str) -> ProviderCandidate:
   250|    row = conn.execute("SELECT * FROM instrument_catalog_entries WHERE catalog_entry_id=?", (catalog_entry_id,)).fetchone()
   251|    if row is None:
   252|        raise ValueError("catalog entry not found")
   253|    return ProviderCandidate(
   254|        provider=row["provider"] or "manual",
   255|        provider_symbol=row["provider_symbol"] or row["ticker"] or "",
   256|        exchange=row["exchange"], currency=row["trading_currency"], name=row["name"], isin=row["isin"],
   257|        is_hedged=True if row["hedge_status"] == "hedged" else False if row["hedge_status"] == "unhedged" else None,
   258|        hedged_to_currency=row["hedged_to_currency"], instrument_status=row["instrument_status"] or "unknown", evidence_source="instrument_catalog", evidence_note="manual catalog selection",
   259|    )
   260|


Explanation:
**Searching for tests**

I see that I need to look for existing tests. There's a mention of needing to possibly render something fake, but I'm not quite sure what that means. I should probably search for tests related to rendering or creating a fake version of whatever it is I'm considering. It might help clarify what’s needed! I'll get to work on that search now to find relevant information.


Explanation:
**Modifying equity snapshot tests**

I need to update `add_equity_initial_snapshot` to make the cost basis optional, as the user requested this change. Right now, it’s a required field which isn’t ideal for them. Also, there’s the issue of silent overwriting since `_ensure_instrument` updates existing entries without notifying. For a wizard selected from the catalog, it might be fine, but for manual updates, I think we should avoid silence— a review and confirmation would be better. I’ll consider adding a `dataclass` like `InstrumentSearchSummary` too.


     1|from __future__ import annotations
     2|
     3|
     4|def show_table(st, rows: list[dict[str, str]], *, empty_message: str = "Keine Daten vorhanden.") -> None:
     5|    if rows:
     6|        st.dataframe(rows, use_container_width=True, hide_index=True)
     7|    else:
     8|        st.info(empty_message)
     9|


     1|INITIAL_SCHEMA_SQL = "\nCREATE TABLE IF NOT EXISTS schema_migrations (\n    version INTEGER PRIMARY KEY,\n    name TEXT NOT NULL,\n    applied_at TEXT NOT NULL,\n    checksum TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS platforms (\n    platform_id TEXT PRIMARY KEY,\n    name TEXT NOT NULL UNIQUE,\n    platform_type TEXT NOT NULL,\n    country TEXT,\n    default_currency TEXT NOT NULL DEFAULT 'CHF',\n    is_active INTEGER NOT NULL DEFAULT 1,\n    notes TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS accounts (\n    account_id TEXT PRIMARY KEY,\n    platform_id TEXT NOT NULL REFERENCES platforms(platform_id),\n    account_name TEXT NOT NULL,\n    account_type TEXT NOT NULL,\n    currency TEXT NOT NULL DEFAULT 'CHF',\n    performance_included INTEGER NOT NULL DEFAULT 1,\n    is_health_reserve INTEGER NOT NULL DEFAULT 0,\n    target_cash_min_chf NUMERIC,\n    target_cash_max_chf NUMERIC,\n    is_active INTEGER NOT NULL DEFAULT 1,\n    notes TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT,\n    UNIQUE(platform_id, account_name)\n);\n\nCREATE TABLE IF NOT EXISTS instruments (\n    instrument_id TEXT PRIMARY KEY,\n    asset_class TEXT NOT NULL,\n    name TEXT NOT NULL,\n    ticker TEXT,\n    isin TEXT,\n    exchange TEXT,\n    currency TEXT NOT NULL,\n    country TEXT,\n    sector TEXT,\n    industry TEXT,\n    provider_symbol TEXT,\n    data_provider_primary TEXT,\n    position_category TEXT,\n    ter TEXT,\n    distribution_policy TEXT,\n    index_name TEXT,\n    fund_domicile TEXT,\n    benchmark TEXT,\n    data_provider_fallback TEXT,\n    is_active INTEGER NOT NULL DEFAULT 1,\n    notes TEXT,\n    created_at TEXT NOT NULL,\n    updated_at TEXT\n);\n\nCREATE TABLE IF NOT EXISTS transactions (\n    transaction_id TEXT PRIMARY KEY,\n    transaction_type TEXT NOT NULL,\n    account_id TEXT NOT NULL REFERENCES accounts(account_id),\n    instrument_id TEXT REFERENCES instruments(instrument_id),\n    trade_date TEXT N... [truncated]
     2|
     3|REQUIRED_TABLES = [
     4|    "schema_migrations", "platforms", "accounts", "instruments", "transactions",
     5|    "positions_snapshot", "cash_balances", "fx_rates", "market_prices",
     6|    "crypto_wallets", "crypto_assets", "crypto_holdings", "crypto_transactions",
     7|    "crypto_prices", "watchlist", "reports", "alerts", "audit_log",
     8|    "import_sessions", "decision_journal", "instrument_mappings",
     9|    "platform_account_mappings", "broker_import_dry_runs",
    10|]
    11|


Explanation:
**Patching FX for snapshot**

I see that we need to handle the missing foreign exchange (FX) rates for the initial snapshot. If the currency is foreign and doesn't have an available FX rate, I should produce an alert. It looks like there's already an existing provider, but I need to check the safety tests for that API. Also, for users on the dashboard without IDs, I should use a filtered display. Time to patch this up!

from pathlib import Path
p=Path('/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/equity/manage.py')
s=p.read_text()
# Insert ensure and search functions before add_equity_initial_snapshot
insert=r'''

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",
            entity_id="manual_broker_accounts",
            new_values={"platforms_created": created_platforms, "accounts_created": created_accounts},
            user_text_note="Ensure manual-entry broker accounts; no productive broker import.",
            confirmed=True,
            created_by="dashboard",
        )
        conn.commit()
    return {"platforms_created": created_platforms, "accounts_created": created_accounts}

def _latest_market_price(conn: Connection, instrument_id: str) -> tuple[str, str]:
    row = conn.execute(
        """
        SELECT close, adjusted_close, price_date
        FROM market_prices
        WHERE instrument_id=?
        ORDER BY price_date DESC, fetched_at DESC
        LIMIT 1
        """,
        (instrument_id,),
    ).fetchone()
    if not row:
        return "", ""
    return (row["adjusted_close"] or row["close"] or "", row["price_date"] or "")

def search_instrument_candidates(
    conn: Connection,
    *,
    query: str,
    asset_class: str | None = None,
    exchange: str | None = None,
    currency: str | None = None,
    country: str | None = None,
    provider: str | None = None,
    include_external: bool = False,
) -> dict[str, object]:
    """Search local instrument catalog. External providers are only used when explicitly enabled by caller."""
    from jarvis_finance.market_data.catalog import search_instruments, UnavailableCatalogLookupProvider

    q = str(query or "").strip()
    if not q:
        return {"results": [], "warnings": [], "selection_required": True, "provider_lookup_attempted": False}
    providers = [UnavailableCatalogLookupProvider()] if include_external else []
    results, warnings = search_instruments(conn, q, asset_class, providers=providers)
    filtered = []
    for r in results:
        if exchange and (r.exchange or "").upper() != exchange.upper():
            continue
        if currency and (r.trading_currency or r.instrument_currency or "").upper() != currency.upper():
            continue
        if provider and (r.provider or "").lower() != provider.lower():
            continue
        # country is prepared for future provider-enriched catalog entries; current result object may not include it.
        row = {
            "catalog_entry_id": r.catalog_entry_id or "",
            "name": r.name,
            "isin": r.isin or "",
            "ticker": r.ticker or "",
            "exchange": r.exchange or "",
            "currency": r.trading_currency or r.instrument_currency or "",
            "asset_class": r.asset_class,
            "data_source": r.source,
            "mapping_status": "exact_isin_match" if r.isin and q.upper() == (r.isin or "").upper() else ("manual_selection_required" if (r.ticker or "").upper() == q.upper() else "candidate"),
            "confidence": r.confidence,
            "last_price": "",
            "price_date": "",
            "selection_required": "yes",
        }
        inst = None
        if row["isin"]:
            inst = conn.execute("SELECT instrument_id FROM instruments WHERE isin=? AND COALESCE(exchange,'')=COALESCE(?, COALESCE(exchange,'')) AND currency=? LIMIT 1", (row["isin"], row["exchange"], row["currency"] or "CHF")).fetchone()
        if inst:
            row["last_price"], row["price_date"] = _latest_market_price(conn, inst["instrument_id"])
        filtered.append(row)
    # ISIN exact single hit may be unambiguous for display, but user still must click/select before writing.
    selection_required = True
    if not filtered:
        warnings.append("no_local_match_manual_entry_allowed")
    if include_external and "provider_lookup_unavailable" in warnings:
        warnings.append("manual_entry_allowed")
    return {"results": filtered, "warnings": warnings, "selection_required": selection_required, "provider_lookup_attempted": include_external}
'''
if 'def search_instrument_candidates' not in s:
    s=s.replace('\ndef add_equity_initial_snapshot', insert+'\n\ndef add_equity_initial_snapshot')
# Change cost basis param default and logic
s=s.replace('    cost_basis_original_text: str,\n    snapshot_date: str,', '    cost_basis_original_text: str | None = None,\n    snapshot_date: str,')
s=s.replace('    gross = parse_decimal_text(cost_basis_original_text, field_name="cost basis")\n    if gross < 0:\n        raise ValueError("cost basis must be >= 0")', '    warnings: list[str] = []\n    if cost_basis_original_text in {None, ""}:\n        gross = Decimal("0")\n        warnings.append("cost_basis_uncertain")\n    else:\n        gross = parse_decimal_text(str(cost_basis_original_text), field_name="cost basis")\n        if gross < 0:\n            raise ValueError("cost basis must be >= 0")')
s=s.replace('    tx_id, warnings = _insert_transaction(\n', '    tx_id, tx_warnings = _insert_transaction(\n', 1)
s=s.replace('    record_audit_event(\n        conn,\n        source="equity_manage",', '    warnings.extend(tx_warnings)\n    if "cost_basis_uncertain" in warnings:\n        create_alert(conn, priority="warnung", category="equity", entity_type="transaction", entity_id=tx_id, rule_id="cost_basis_uncertain", message="Cost basis is missing for manual initial snapshot; performance metrics are incomplete.", evidence={"instrument_id": instrument_id}, fingerprint="cost_basis_uncertain")\n    create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="missing_market_price", message="Market price is missing for manually added instrument until mapping/price update is confirmed.", evidence={}, fingerprint="missing_market_price")\n    record_audit_event(\n        conn,\n        source="equity_manage",', 1)
p.write_text(s)
print('patched manage')


Output: patched manage


Explanation:
**Checking for issues in code**

I need to make sure that replacing the transaction warnings won't interfere with the `add_manual` function. We’ve replaced the first occurrence after `add_equity`, but I should really inspect the surrounding code just to be safe. It's always better to double-check these things to avoid any unexpected issues down the line. I want to ensure everything works smoothly! Let's take a closer look at that section.


   250|            quality_status, notes, created_at
   251|        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'manual_dashboard', ?, 1, ?, ?, ?)
   252|        """,
   253|        (
   254|            tx_id,
   255|            transaction_type,
   256|            account_id,
   257|            instrument_id,
   258|            trade_date,
   259|            decimal_text(quantity) if quantity is not None else None,
   260|            decimal_text(gross),
   261|            decimal_text(fee),
   262|            decimal_text(tax),
   263|            decimal_text(gross - fee - tax),
   264|            currency,
   265|            fx_text,
   266|            fx_source_value,
   267|            status,
   268|            gross_chf,
   269|            decimal_text(fee * Decimal(fx_text), "0.0001") if fx_text else None,
   270|            decimal_text(tax * Decimal(fx_text), "0.0001") if fx_text else None,
   271|            net_chf,
   272|            tx_id,
   273|            "incomplete" if warnings else "ok",
   274|            note,
   275|            now,
   276|        ),
   277|    )
   278|    return tx_id, warnings
   279|
   280|
   281|
   282|def ensure_standard_broker_accounts(conn: Connection) -> dict[str, int]:
   283|    """Ensure manual-entry broker platforms exist. No holdings are imported."""
   284|    now = utc_now()
   285|    created_platforms = 0
   286|    created_accounts = 0
   287|    specs = [
   288|        ("postfinance", "PostFinance"),
   289|        ("true_wealth", "True Wealth"),
   290|        ("raiffeisen", "Raiffeisen"),
   291|        ("other_broker", "Anderes Konto"),
   292|    ]
   293|    for platform_id, name in specs:
   294|        row = conn.execute("SELECT platform_id FROM platforms WHERE name=?", (name,)).fetchone()
   295|        pid = row["platform_id"] if row else platform_id
   296|        if row is None:
   297|            conn.execute(
   298|                "INSERT OR IGNORE INTO platforms(platform_id,name,platform_type,country,default_currency,is_active,notes,created_at) VALUES(?,?,?,?,?,?,?,?)",
   299|                (pid, name, "broker", "CH", "CHF", 1, "Manual equity/ETF entry platform; no broker import.", now),
   300|            )
   301|            created_platforms += 1
   302|        acc = conn.execute("SELECT account_id FROM accounts WHERE platform_id=? AND account_name=?", (pid, "Manual Portfolio")).fetchone()
   303|        if acc is None:
   304|            account_id = stable_id("account", pid, "Manual Portfolio", "CHF")
   305|            conn.execute(
   306|                "INSERT OR IGNORE INTO accounts(account_id,platform_id,account_name,account_type,currency,is_active,notes,created_at) VALUES(?,?,?,?,?,?,?,?)",
   307|                (account_id, pid, "Manual Portfolio", "brokerage", "CHF", 1, "Manual equity/ETF entries only; no DOCX import.", now),
   308|            )
   309|            created_accounts += 1
   310|    if created_platforms or created_accounts:
   311|        record_audit_event(
   312|            conn,
   313|            source="equity_manage",
   314|            action="ensure_manual_broker_accounts",
   315|            entity_type="account",
   316|            entity_id="manual_broker_accounts",
   317|            new_values={"platforms_created": created_platforms, "accounts_created": created_accounts},
   318|            user_text_note="Ensure manual-entry broker accounts; no productive broker import.",
   319|            confirmed=True,
   320|            created_by="dashboard",
   321|        )
   322|        conn.commit()
   323|    return {"platforms_created": created_platforms, "accounts_created": created_accounts}
   324|
   325|
   326|def _latest_market_price(conn: Connection, instrument_id: str) -> tuple[str, str]:
   327|    row = conn.execute(
   328|        """
   329|        SELECT close, adjusted_close, price_date
   330|        FROM market_prices
   331|        WHERE instrument_id=?
   332|        ORDER BY price_date DESC, fetched_at DESC
   333|        LIMIT 1
   334|        """,
   335|        (instrument_id,),
   336|    ).fetchone()
   337|    if not row:
   338|        return "", ""
   339|    return (row["adjusted_close"] or row["close"] or "", row["price_date"] or "")
   340|
   341|
   342|def search_instrument_candidates(
   343|    conn: Connection,
   344|    *,
   345|    query: str,
   346|    asset_class: str | None = None,
   347|    exchange: str | None = None,
   348|    currency: str | None = None,
   349|    country: str | None = None,
   350|    provider: str | None = None,
   351|    include_external: bool = False,
   352|) -> dict[str, object]:
   353|    """Search local instrument catalog. External providers are only used when explicitly enabled by caller."""
   354|    from jarvis_finance.market_data.catalog import search_instruments, UnavailableCatalogLookupProvider
   355|
   356|    q = str(query or "").strip()
   357|    if not q:
   358|        return {"results": [], "warnings": [], "selection_required": True, "provider_lookup_attempted": False}
   359|    providers = [UnavailableCatalogLookupProvider()] if include_external else []
   360|    results, warnings = search_instruments(conn, q, asset_class, providers=providers)
   361|    filtered = []
   362|    for r in results:
   363|        if exchange and (r.exchange or "").upper() != exchange.upper():
   364|            continue
   365|        if currency and (r.trading_currency or r.instrument_currency or "").upper() != currency.upper():
   366|            continue
   367|        if provider and (r.provider or "").lower() != provider.lower():
   368|            continue
   369|        # country is prepared for future provider-enriched catalog entries; current result object may not include it.
   370|        row = {
   371|            "catalog_entry_id": r.catalog_entry_id or "",
   372|            "name": r.name,
   373|            "isin": r.isin or "",
   374|            "ticker": r.ticker or "",
   375|            "exchange": r.exchange or "",
   376|            "currency": r.trading_currency or r.instrument_currency or "",
   377|            "asset_class": r.asset_class,
   378|            "data_source": r.source,
   379|            "mapping_status": "exact_isin_match" if r.isin and q.upper() == (r.isin or "").upper() else ("manual_selection_required" if (r.ticker or "").upper() == q.upper() else "candidate"),
   380|            "confidence": r.confidence,
   381|            "last_price": "",
   382|            "price_date": "",
   383|            "selection_required": "yes",
   384|        }
   385|        inst = None
   386|        if row["isin"]:
   387|            inst = conn.execute("SELECT instrument_id FROM instruments WHERE isin=? AND COALESCE(exchange,'')=COALESCE(?, COALESCE(exchange,'')) AND currency=? LIMIT 1", (row["isin"], row["exchange"], row["currency"] or "CHF")).fetchone()
   388|        if inst:
   389|            row["last_price"], row["price_date"] = _latest_market_price(conn, inst["instrument_id"])
   390|        filtered.append(row)
   391|    # ISIN exact single hit may be unambiguous for display, but user still must click/select before writing.
   392|    selection_required = True
   393|    if not filtered:
   394|        warnings.append("no_local_match_manual_entry_allowed")
   395|    if include_external and "provider_lookup_unavailable" in warnings:
   396|        warnings.append("manual_entry_allowed")
   397|    return {"results": filtered, "warnings": warnings, "selection_required": selection_required, "provider_lookup_attempted": include_external}
   398|
   399|
   400|def add_equity_initial_snapshot(
   401|    conn: Connection,
   402|    *,
   403|    account_id: str,
   404|    asset_class: str,
   405|    name: str,
   406|    isin: str = "",
   407|    ticker: str = "",
   408|    exchange: str = "",
   409|    currency: str,
   410|    quantity_text: str,
   411|    cost_basis_original_text: str | None = None,
   412|    snapshot_date: str,
   413|    category: str = "Unknown",
   414|    note: str,
   415|    confirm: bool,
   416|    fx_rate_to_chf_text: str | None = None,
   417|    fx_source: str | None = None,
   418|    fx_status: str = "ok",
   419|    ter_text: str | None = None,
   420|    distribution_policy: str | None = None,
   421|    index_name: str | None = None,
   422|    fund_domicile: str | None = None,
   423|    benchmark: str | None = None,
   424|) -> EquityManageResult:
   425|    _require_confirm(confirm)
   426|    account_id = _require_account(conn, account_id)
   427|    asset_class = _normalize_asset_class(asset_class)
   428|    currency = _require_currency(currency)
   429|    snapshot_date = _require_date(snapshot_date)
   430|    note = _require_note(note, reason="initial_position_snapshot with incomplete history")
   431|    qty = parse_decimal_text(quantity_text, field_name="quantity")
   432|    if qty <= 0:
   433|        raise ValueError("quantity must be > 0")
   434|    warnings: list[str] = []
   435|    if cost_basis_original_text in {None, ""}:
   436|        gross = Decimal("0")
   437|        warnings.append("cost_basis_uncertain")
   438|    else:
   439|        gross = parse_decimal_text(str(cost_basis_original_text), field_name="cost basis")
   440|        if gross < 0:
   441|            raise ValueError("cost basis must be >= 0")
   442|    instrument_id = _ensure_instrument(
   443|        conn,
   444|        asset_class=asset_class,
   445|        name=name,
   446|        isin=isin,
   447|        ticker=ticker,
   448|        exchange=exchange,
   449|        currency=currency,
   450|        category=category,
   451|        note=note,
   452|        ter_text=ter_text,
   453|        distribution_policy=distribution_policy,
   454|        index_name=index_name,
   455|        fund_domicile=fund_domicile,
   456|        benchmark=benchmark,
   457|    )
   458|    existing_position = calculate_positions(conn).positions.get((account_id, instrument_id))
   459|    if existing_position and existing_position.quantity != 0:
   460|        raise ValueError("initial snapshot would overwrite an existing position; use manual_adjustment")
   461|    tx_id, tx_warnings = _insert_transaction(
   462|        conn,
   463|        transaction_type="initial_position_snapshot",
   464|        account_id=account_id,
   465|        instrument_id=instrument_id,
   466|        trade_date=snapshot_date,
   467|        quantity=qty,
   468|        gross=gross,
   469|        fee=Decimal("0"),
   470|        tax=Decimal("0"),
   471|        currency=currency,
   472|        fx_rate_to_chf_text=fx_rate_to_chf_text,
   473|        fx_source=fx_source,
   474|        fx_status=fx_status,
   475|        note=note,
   476|    )
   477|    warnings.extend(tx_warnings)
   478|    if "cost_basis_uncertain" in warnings:
   479|        create_alert(conn, priority="warnung", category="equity", entity_type="transaction", entity_id=tx_id, rule_id="cost_basis_uncertain", message="Cost basis is missing for manual initial snapshot; performance metrics are incomplete.", evidence={"instrument_id": instrument_id}, fingerprint="cost_basis_uncertain")
   480|    create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="missing_market_price", message="Market price is missing for manually added instrument until mapping/price update is confirmed.", evidence={}, fingerprint="missing_market_price")
   481|    record_audit_event(
   482|        conn,
   483|        source="equity_manage",
   484|        action="equity_initial_position_snapshot",
   485|        entity_type="transaction",
   486|        entity_id=tx_id,
   487|        new_values={"account_id": account_id, "instrument_id": instrument_id, "transaction_type": "initial_position_snapshot"},
   488|        user_text_note=note,
   489|        confirmed=True,
   490|        created_by="dashboard",
   491|    )
   492|    conn.commit()
   493|    return EquityManageResult(instrument_id=instrument_id, account_id=account_id, transaction_id=tx_id, warnings=warnings)
   494|
   495|
   496|def add_equity_transaction(
   497|    conn: Connection,
   498|    *,
   499|    account_id: str,
   500|    instrument_id: str,
   501|    transaction_type: str,
   502|    trade_date: str,
   503|    quantity_text: str,
   504|    gross_amount_original_text: str,
   505|    currency: str,
   506|    note: str,
   507|    confirm: bool,
   508|    fee_original_text: str = "0",
   509|    tax_original_text: str = "0",
   510|    fx_rate_to_chf_text: str | None = None,
   511|    fx_source: str | None = None,
   512|    fx_status: str = "ok",
   513|) -> EquityManageResult:
   514|    _require_confirm(confirm)
   515|    account_id = _require_account(conn, account_id)
   516|    instrument_id = _require_instrument(conn, instrument_id)
   517|    transaction_type = str(transaction_type).strip()
   518|    if transaction_type not in VALID_TRANSACTION_TYPES:
   519|        raise ValueError("unsupported equity transaction type")
   520|    trade_date = _require_date(trade_date)
   521|    currency = _require_currency(currency)
   522|    if transaction_type == "manual_adjustment":
   523|        note = _require_note(note, reason="manual_adjustment")
   524|    else:
   525|        note = str(note or "").strip() or f"manual {transaction_type}"
   526|    qty = parse_decimal_text(quantity_text, field_name="quantity")
   527|    gross = parse_decimal_text(gross_amount_original_text, field_name="gross amount")
   528|    fee = parse_decimal_text(fee_original_text, field_name="fee")
   529|    tax = parse_decimal_text(tax_original_text, field_name="tax")
   530|

Use offset=530 to continue reading (showing 250-529 of 770 lines)


[Your active task list was preserved across context compression]
- [>] manual-1. Code-Struktur für manuelle Instrument-Suche/Add-Position prüfen (in_progress)
- [ ] manual-2. Resolver/Search-Service und manuelle Snapshot/Transaction-Funktionen implementieren (pending)
- [ ] manual-3. User-Mode Wizard im Dashboard ohne Live-API/technische IDs bauen (pending)
- [ ] manual-4. Tests für Suche, Wizard-Safety, Snapshots, FX/Audit/DQ ergänzen (pending)
- [ ] manual-5. Compile, pytest, Git-Safety, Commit und Push ausführen (pending)


Explanation:
**Inspecting imports for management**

I need to continue by inspecting the imports at the top to manage everything effectively. It's crucial to ensure that all necessary components are included correctly for optimal functionality. I want to check for any potential issues or missing elements that could cause complications later on. Attention to these details is really important, and I'll keep track of how the imports contribute to the overall structure. Let's dive in!