    80|    price_date: str | None = None
    81|    price_source: str | None = None
    82|
    83|
    84|class CatalogLookupProvider(Protocol):
    85|    name: str
    86|
    87|    def search(self, *, query: str, asset_class: str | None = None) -> list[CatalogSearchResult]:
    88|        ...
    89|
    90|
    91|class UnavailableCatalogLookupProvider:
    92|    name = "unavailable"
    93|
    94|    def search(self, *, query: str, asset_class: str | None = None) -> list[CatalogSearchResult]:
    95|        raise LookupUnavailable("external instrument lookup is not configured")
    96|
    97|
    98|def _runtime_secret_value(names: tuple[str, ...]) -> str | None:
    99|    for name in names:
   100|        if os.environ.get(name):
   101|            return os.environ[name]
   102|    runtime_dir = Path(os.environ.get("JARVIS_FINANCE_RUNTIME_DIR", "~/jarvis_runtime/finance-system")).expanduser()
   103|    for candidate in [runtime_dir / "secrets" / ".env", runtime_dir / ".env"]:
   104|        if not candidate.exists():
   105|            continue
   106|        for line in candidate.read_text(encoding="utf-8").splitlines():
   107|            raw = line.strip()
   108|            if not raw or raw.startswith("#") or "=" not in raw:
   109|                continue
   110|            key, value = raw.split("=", 1)
   111|            key = key.strip().removeprefix("export ").strip()
   112|            if key in names:
   113|                return value.strip().strip('"').strip("'") or None
   114|    return None
   115|
   116|
   117|class _HttpJsonLookupProvider:
   118|    name = "provider"
   119|    api_key_env: tuple[str, ...] = ()
   120|
   121|    def __init__(self, *, api_key: str | None = None, timeout_seconds: float = 8.0, min_interval_seconds: float = 0.2) -> None:
   122|        self.api_key = api_key or _runtime_secret_value(self.api_key_env)
   123|        self.timeout_seconds = timeout_seconds
   124|        self.min_interval_seconds = min_interval_seconds
   125|        self._last_request_at = 0.0
   126|
   127|    def _throttle(self) -> None:
   128|        elapsed = time.monotonic() - self._last_request_at
   129|        if elapsed < self.min_interval_seconds:
   130|            time.sleep(self.min_interval_seconds - elapsed)
   131|        self._last_request_at = time.monotonic()
   132|
   133|    def _get_json(self, url: str, *, headers: dict[str, str] | None = None) -> object:
   134|        self._throttle()
   135|        req = request.Request(url, headers=headers or {})
   136|        try:
   137|            with request.urlopen(req, timeout=self.timeout_seconds) as response:  # noqa: S310 - explicit user-triggered provider lookup
   138|                return json.loads(response.read().decode("utf-8"))
   139|        except error.HTTPError as exc:
   140|            if exc.code in {401, 403}:
   141|                raise LookupUnavailable(f"{self.name}_auth_failed") from exc
   142|            if exc.code == 429:
   143|                reset = exc.headers.get("ratelimit-reset") if exc.headers else None
   144|                try:
   145|                    wait = min(max(float(reset or "1"), 1.0), 3.0)
   146|                except ValueError:
   147|                    wait = 1.0
   148|                time.sleep(wait)
   149|                try:
   150|                    with request.urlopen(req, timeout=self.timeout_seconds) as response:  # noqa: S310 - explicit user-triggered provider lookup retry after 429
   151|                        return json.loads(response.read().decode("utf-8"))
   152|                except error.HTTPError as retry_exc:
   153|                    if retry_exc.code == 429:
   154|                        raise LookupUnavailable(f"{self.name}_rate_limited") from retry_exc
   155|                    if retry_exc.code in {401, 403}:
   156|                        raise LookupUnavailable(f"{self.name}_auth_failed") from retry_exc
   157|                    raise LookupUnavailable(f"{self.name}_provider_error") from retry_exc
   158|            raise LookupUnavailable(f"{self.name}_provider_error") from exc
   159|        except error.URLError as exc:
   160|            raise LookupUnavailable(f"{self.name}_network_error") from exc
   161|
   162|    def _post_json(self, url: str, payload: object, *, headers: dict[str, str] | None = None) -> object:
   163|        self._throttle()
   164|        data = json.dumps(payload).encode("utf-8")
   165|        req = request.Request(url, data=data, headers={"Content-Type": "application/json", **(headers or {})})
   166|        try:
   167|            with request.urlopen(req, timeout=self.timeout_seconds) as response:  # noqa: S310 - explicit user-triggered provider lookup
   168|                return json.loads(response.read().decode("utf-8"))
   169|        except error.HTTPError as exc:
   170|            if exc.code in {401, 403}:
   171|                raise LookupUnavailable(f"{self.name}_auth_failed") from exc
   172|            if exc.code == 429:
   173|                reset = exc.headers.get("ratelimit-reset") if exc.headers else None
   174|                try:
   175|                    wait = min(max(float(reset or "1"), 1.0), 3.0)
   176|                except ValueError:
   177|                    wait = 1.0
   178|                time.sleep(wait)
   179|                try:
   180|                    with request.urlopen(req, timeout=self.timeout_seconds) as response:  # noqa: S310 - explicit user-triggered provider lookup retry after 429
   181|                        return json.loads(response.read().decode("utf-8"))
   182|                except error.HTTPError as retry_exc:
   183|                    if retry_exc.code == 429:
   184|                        raise LookupUnavailable(f"{self.name}_rate_limited") from retry_exc
   185|                    if retry_exc.code in {401, 403}:
   186|                        raise LookupUnavailable(f"{self.name}_auth_failed") from retry_exc
   187|                    raise LookupUnavailable(f"{self.name}_provider_error") from retry_exc
   188|            raise LookupUnavailable(f"{self.name}_provider_error") from exc
   189|        except error.URLError as exc:
   190|            raise LookupUnavailable(f"{self.name}_network_error") from exc
   191|
   192|
   193|class OpenFigiLookupProvider(_HttpJsonLookupProvider):
   194|    name = "openfigi"
   195|    api_key_env = ("OPENFIGI_API_KEY", "JARVIS_OPENFIGI_API_KEY")
   196|
   197|    def _request(self, query: str) -> object:
   198|        if not self.api_key:
   199|            raise LookupUnavailable("openfigi_api_key_missing")
   200|        q = (query or "").strip().upper()
   201|        headers = {"X-OPENFIGI-APIKEY": self.api_key}
   202|        if ISIN_RE.match(q):
   203|            return self._post_json("https://api.openfigi.com/v3/mapping", [{"idType": "ID_ISIN", "idValue": q}], headers=headers)
   204|        parts = q.replace(":", " ").replace("/", " ").split()
   205|        if len(parts) >= 2 and len(parts[0]) <= 8:
   206|            return self._post_json("https://api.openfigi.com/v3/mapping", [{"idType": "TICKER", "idValue": parts[0], "exchCode": parts[1]}], headers=headers)
   207|        return self._post_json("https://api.openfigi.com/v3/search", {"query": query}, headers=headers)
   208|
   209|    @staticmethod
   210|    def _rows(payload: object) -> list[dict]:
   211|        if isinstance(payload, list):
   212|            if payload and isinstance(payload[0], dict) and isinstance(payload[0].get("data"), list):
   213|                return [row for row in payload[0].get("data", []) if isinstance(row, dict)]
   214|            return [row for row in payload if isinstance(row, dict)]
   215|        if isinstance(payload, dict):
   216|            return [row for row in payload.get("data", []) if isinstance(row, dict)]
   217|        return []
   218|
   219|    def search(self, *, query: str, asset_class: str | None = None) -> list[CatalogSearchResult]:
   220|        q = (query or "").strip().upper()
   221|        rows = self._rows(self._request(query))
   222|        results: list[CatalogSearchResult] = []
   223|        for item in rows[:10]:
   224|            name = str(item.get("name") or item.get("securityDescription") or "").strip()
   225|            ticker = str(item.get("ticker") or "").strip().upper() or None
   226|            exchange = str(item.get("exchCode") or item.get("marketSector") or "").strip().upper() or None
   227|            if not name and not ticker:
   228|                continue
   229|            sec_type = str(item.get("securityType") or item.get("marketSecDes") or "").lower()
   230|            inferred_class = asset_class or ("etf" if any(token in sec_type for token in ("fund", "etf", "etp")) else "stock")
   231|            isin = q if ISIN_RE.match(q) else str(item.get("idValue") or item.get("isin") or "").strip().upper() or None
   232|            confidence = "high" if isin and ISIN_RE.match(isin) and q == isin else "medium"
   233|            results.append(
   234|                CatalogSearchResult(
   235|                    None,
   236|                    inferred_class,
   237|                    name or ticker or query,
   238|                    isin=isin,
   239|                    ticker=ticker,
   240|                    exchange=exchange,
   241|                    trading_currency=str(item.get("currency") or "").strip().upper() or None,
   242|                    provider="openfigi",
   243|                    provider_symbol=str(item.get("compositeFIGI") or item.get("figi") or item.get("shareClassFIGI") or "").strip() or None,
   244|                    provider_market=exchange,
   245|                    country=str(item.get("securityType2") or item.get("marketSector") or "").strip() or None,
   246|                    confidence=confidence,
   247|                    source="openfigi",
   248|                    evidence_note="OpenFIGI ISIN/FIGI mapping; explicit user selection required" if confidence == "high" else "OpenFIGI search result; manual selection required",
   249|                )
   250|            )
   251|        return results
   252|
   253|
   254|class FinnhubSymbolLookupProvider(_HttpJsonLookupProvider):
   255|    name = "finnhub"
   256|    api_key_env = ("FINNHUB_API_KEY", "JARVIS_FINNHUB_API_KEY")
   257|
   258|    def _request(self, query: str) -> object:
   259|        if not self.api_key:
   260|            raise LookupUnavailable("finnhub_api_key_missing")
   261|        url = "https://finnhub.io/api/v1/search?" + parse.urlencode({"q": query, "token": self.api_key})
   262|        return self._get_json(url)
   263|
   264|    def search(self, *, query: str, asset_class: str | None = None) -> list[CatalogSearchResult]:
   265|        payload = self._request(query)
   266|        rows = payload.get("result", []) if isinstance(payload, dict) else []
   267|        results: list[CatalogSearchResult] = []
   268|        for item in rows[:10]:
   269|            if not isinstance(item, dict):
   270|                continue
   271|            symbol = str(item.get("symbol") or "").strip().upper()
   272|            description = str(item.get("description") or symbol or query).strip()
   273|            if not symbol:
   274|                continue
   275|            inferred_class = asset_class or ("etf" if "etf" in description.lower() else "stock")
   276|            results.append(CatalogSearchResult(None, inferred_class, description, ticker=symbol.split(".")[0], exchange=None, provider="finnhub", provider_symbol=symbol, confidence="low", source="finnhub", evidence_note="Finnhub symbol lookup; manual selection required"))
   277|        return results
   278|
   279|
   280|class FmpSearchProvider(_HttpJsonLookupProvider):
   281|    name = "fmp"
   282|    api_key_env = ("FMP_API_KEY", "FINANCIAL_MODELING_PREP_API_KEY", "JARVIS_FMP_API_KEY")
   283|
   284|    def _get_stable(self, endpoint: str, params: dict[str, str]) -> object:
   285|        if not self.api_key:
   286|            raise LookupUnavailable("fmp_api_key_missing")
   287|        url = "https://financialmodelingprep.com/stable/" + endpoint.lstrip("/") + "?" + parse.urlencode({**params, "apikey": self.api_key})
   288|        return self._get_json(url, headers={"Accept": "application/json"})
   289|
   290|    def _request_many(self, query: str) -> list[dict[str, object]]:
   291|        q = (query or "").strip()
   292|        if not q:
   293|            return []
   294|        endpoints: list[tuple[str, dict[str, str]]] = []
   295|        if ISIN_RE.match(q.upper()):
   296|            endpoints.append(("search-isin", {"isin": q.upper()}))
   297|        if len(q.split()) == 1 and len(q) <= 12:
   298|            endpoints.append(("search-symbol", {"query": q.upper()}))
   299|            endpoints.append(("search-exchange-variants", {"symbol": q.upper()}))
   300|        endpoints.append(("search-name", {"query": q}))
   301|        seen: set[tuple[str, str, str]] = set()
   302|        rows: list[dict[str, object]] = []
   303|        last_error: LookupUnavailable | None = None
   304|        for endpoint, params in endpoints:
   305|            try:
   306|                payload = self._get_stable(endpoint, params)
   307|            except LookupUnavailable as exc:
   308|                last_error = exc
   309|                continue
   310|            items = payload if isinstance(payload, list) else payload.get("data", []) if isinstance(payload, dict) else []
   311|            for item in items[:10]:
   312|                if not isinstance(item, dict):
   313|                    continue
   314|                symbol = str(item.get("symbol") or item.get("ticker") or "").strip().upper()
   315|                name = str(item.get("name") or item.get("companyName") or item.get("securityName") or "").strip()
   316|                exch = str(item.get("exchangeShortName") or item.get("exchange") or item.get("exchangeCode") or "").strip().upper()
   317|                key = (symbol, name.lower(), exch)
   318|                if key in seen or not (symbol or name):
   319|                    continue
   320|                seen.add(key)
   321|                item = dict(item)
   322|                item["_fmp_endpoint"] = endpoint
   323|                rows.append(item)
   324|        if not rows and last_error is not None:
   325|            raise last_error
   326|        return rows
   327|
   328|    def _profile(self, symbol: str) -> dict[str, object]:
   329|        if not symbol or not self.api_key:
   330|            return {}
   331|        try:
   332|            payload = self._get_stable("profile", {"symbol": symbol})
   333|        except LookupUnavailable:
   334|            return {}
   335|        if isinstance(payload, list) and payload and isinstance(payload[0], dict):
   336|            return payload[0]
   337|        if isinstance(payload, dict):
   338|            data = payload.get("data")
   339|            if isinstance(data, list) and data and isinstance(data[0], dict):
   340|                return data[0]
   341|            return payload
   342|        return {}
   343|
   344|    def search(self, *, query: str, asset_class: str | None = None) -> list[CatalogSearchResult]:
   345|        rows = self._request_many(query)
   346|        results: list[CatalogSearchResult] = []
   347|        for item in rows[:25]:
   348|            symbol = str(item.get("symbol") or item.get("ticker") or "").strip().upper()
   349|            name = str(item.get("name") or item.get("companyName") or item.get("securityName") or symbol or query).strip()
   350|            if not symbol and not name:
   351|                continue
   352|            profile = self._profile(symbol) if symbol else {}
   353|            isin = str(item.get("isin") or profile.get("isin") or "").strip().upper() or None
   354|            exchange = str(item.get("exchangeShortName") or item.get("exchange") or item.get("exchangeCode") or profile.get("exchangeShortName") or profile.get("exchange") or "").strip().upper() or None
   355|            currency = str(item.get("currency") or profile.get("currency") or profile.get("priceCurrency") or "").strip().upper() or None
   356|            is_etf = bool(profile.get("isEtf")) or "etf" in name.lower() or "fund" in name.lower()
   357|            inferred_class = asset_class or ("etf" if is_etf else "stock")
   358|            confidence = "medium" if isin or (symbol and exchange) else "low"
   359|            raw_price = profile.get("price") or profile.get("lastPrice") or profile.get("mktCapPrice")
   360|            last_price = str(raw_price).strip() if raw_price not in {None, "", 0, "0"} else None
   361|            results.append(
   362|                CatalogSearchResult(
   363|                    None,
   364|                    inferred_class,
   365|                    name,
   366|                    isin=isin,
   367|                    ticker=symbol.split(".")[0] if symbol else None,
   368|                    exchange=exchange,
   369|                    trading_currency=currency,
   370|                    provider="fmp",
   371|                    provider_symbol=symbol or None,
   372|                    provider_market=exchange,
   373|                    country=str(profile.get("country") or item.get("country") or "").strip().upper() or None,
   374|                    confidence=confidence,
   375|                    source="fmp",
   376|                    evidence_note="Financial Modeling Prep stable search/profile result; manual selection required",
   377|                    last_price=last_price,
   378|                    price_currency=currency,
   379|                    price_date=utc_now()[:10] if last_price else None,
   380|                    price_source="fmp profile" if last_price else None,
   381|                )
   382|            )
   383|        return results
   384|
   385|
   386|def default_instrument_lookup_providers() -> list[CatalogLookupProvider]:
   387|    return [OpenFigiLookupProvider(), FmpSearchProvider(), FinnhubSymbolLookupProvider()]
   388|
   389|
   390|def _provider_error_category(message: str) -> str:
   391|    text = (message or "").lower()
   392|    if "api_key_missing" in text or "key_missing" in text:
   393|        return "key_missing"
   394|    if "auth_failed" in text:
   395|        return "auth_failed"
   396|    if "rate_limited" in text:
   397|        return "rate_limited"
   398|    if "network_error" in text:
   399|        return "network_error"
   400|    if "no_results" in text:
   401|        return "no_results"
   402|    if "parsing_error" in text:
   403|        return "parsing_error"
   404|    return "provider_error"
   405|
   406|
   407|def _status_from_category(category: str, *, optional: bool = False) -> str:
   408|    if category == "key_missing":
   409|        return "optional / API-Key fehlt" if optional else "API-Key fehlt"
   410|

Use offset=410 to continue reading (showing 80-409 of 737 lines)


   560|    return catalog_entry_id
   561|
   562|
   563|def search_local_catalog(conn: Connection, query: str, asset_class: str | None = None) -> list[CatalogSearchResult]:
   564|    q = (query or "").strip()
   565|    if not q:
   566|        return []
   567|    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(?))"]
   568|    params: list[object] = [q, q, q, f"%{normalize_name(q)}%", q, q]
   569|    if asset_class:
   570|        ac = asset_class.lower()
   571|        if ac == "equity":
   572|            clauses.append("asset_class IN ('equity','stock')")
   573|        else:
   574|            clauses.append("asset_class=?")
   575|            params.append(ac)
   576|    rows = conn.execute(
   577|        "SELECT * FROM instrument_catalog_entries WHERE " + " AND ".join(clauses) + " ORDER BY source_confidence, name, exchange, trading_currency LIMIT 50",
   578|        tuple(params),
   579|    ).fetchall()
   580|    return [
   581|        CatalogSearchResult(
   582|            catalog_entry_id=row["catalog_entry_id"], asset_class=row["asset_class"], name=row["name"], isin=row["isin"], ticker=row["ticker"], exchange=row["exchange"],
   583|            trading_currency=row["trading_currency"], instrument_currency=row["instrument_currency"], provider=row["provider"], provider_symbol=row["provider_symbol"],
   584|            provider_market=row["provider_market"], country=row["country"], hedge_status=row["hedge_status"] or "unknown", instrument_status=row["instrument_status"] or "unknown",
   585|            confidence=row["source_confidence"] or "low", source="local_catalog", evidence_note="local catalog match",
   586|        )
   587|        for row in rows
   588|    ]
   589|
   590|
   591|def _candidate_key(item: CatalogSearchResult) -> tuple[str, ...]:
   592|    if item.isin:
   593|        return ("isin", item.isin.upper(), (item.exchange or "").upper(), (item.trading_currency or item.instrument_currency or "").upper())
   594|    if item.ticker:
   595|        return ("ticker", item.ticker.upper(), (item.exchange or "").upper(), (item.trading_currency or item.instrument_currency or "").upper())
   596|    return ("name", normalize_name(item.name), (item.exchange or "").upper(), (item.trading_currency or item.instrument_currency or "").upper())
   597|
   598|
   599|def _confidence_rank(value: str) -> int:
   600|    return {"low": 1, "medium": 2, "high": 3}.get(value, 1)
   601|
   602|
   603|def _merge_search_results(items: list[CatalogSearchResult]) -> list[CatalogSearchResult]:
   604|    merged: dict[tuple[str, ...], CatalogSearchResult] = {}
   605|    for item in items:
   606|        key = _candidate_key(item)
   607|        existing = merged.get(key)
   608|        if existing is None:
   609|            merged[key] = item
   610|            continue
   611|        providers = sorted({p for p in [existing.provider, item.provider] if p})
   612|        sources = sorted({s for s in [existing.source, item.source] if s})
   613|        conflict = any(
   614|            a and b and str(a).upper() != str(b).upper()
   615|            for a, b in [
   616|                (existing.ticker, item.ticker),
   617|                (existing.exchange, item.exchange),
   618|                (existing.trading_currency or existing.instrument_currency, item.trading_currency or item.instrument_currency),
   619|            ]
   620|        )
   621|        if conflict:
   622|            existing.confidence = "low"
   623|            existing.evidence_note = (existing.evidence_note + "; provider conflict requires manual review").strip("; ")
   624|        else:
   625|            rank = max(_confidence_rank(existing.confidence), _confidence_rank(item.confidence))
   626|            if len(providers) > 1 and rank < 3:
   627|                rank += 1
   628|            existing.confidence = {1: "low", 2: "medium", 3: "high"}[min(rank, 3)]
   629|            existing.evidence_note = (existing.evidence_note + "; corroborated by " + ", ".join(providers)).strip("; ") if len(providers) > 1 else existing.evidence_note
   630|        existing.provider = "+".join(providers) if providers else existing.provider
   631|        existing.source = "+".join(sources) if sources else existing.source
   632|        existing.provider_symbol = existing.provider_symbol or item.provider_symbol
   633|        existing.country = existing.country or item.country
   634|        existing.last_price = existing.last_price or item.last_price
   635|        existing.price_currency = existing.price_currency or item.price_currency
   636|        existing.price_date = existing.price_date or item.price_date
   637|        existing.price_source = existing.price_source or item.price_source
   638|    return list(merged.values())
   639|
   640|
   641|def search_instruments(conn: Connection, query: str, asset_class: str | None = None, *, providers: list[CatalogLookupProvider] | None = None) -> tuple[list[CatalogSearchResult], list[str]]:
   642|    results = search_local_catalog(conn, query, asset_class)
   643|    warnings: list[str] = []
   644|    for provider in providers or []:
   645|        try:
   646|            external = provider.search(query=query, asset_class=asset_class)
   647|        except LookupUnavailable as exc:
   648|            warning = str(exc) or "provider_lookup_unavailable"
   649|            if warning == "external instrument lookup is not configured":
   650|                warning = "provider_lookup_unavailable"
   651|            warnings.append(warning)
   652|            create_alert(conn, priority="info", category="market_data", entity_type="instrument_catalog", entity_id="external_lookup", rule_id=warning, message="External instrument catalog provider is not configured or unavailable.", evidence={"provider": getattr(provider, "name", "unknown")}, fingerprint=f"catalog_provider_lookup_unavailable:{getattr(provider, 'name', 'unknown')}:{warning}")
   653|            continue
   654|        except Exception:
   655|            warnings.append("provider_lookup_unavailable")
   656|            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')}")
   657|            continue
   658|        for item in external:
   659|            if item.catalog_entry_id is None:
   660|                try:
   661|                    catalog_id = upsert_catalog_entry(
   662|                        conn,
   663|                        CatalogEntryInput(
   664|                            asset_class=item.asset_class,
   665|                            name=item.name,
   666|                            isin=item.isin,
   667|                            ticker=item.ticker,
   668|                            exchange=item.exchange,
   669|                            trading_currency=item.trading_currency,
   670|                            instrument_currency=item.instrument_currency or item.trading_currency,
   671|                            provider=(item.provider or getattr(provider, "name", "external")).split("+", 1)[0],
   672|                            provider_symbol=item.provider_symbol,
   673|                            provider_market=item.provider_market,
   674|                            country=item.country,
   675|                            source=item.source,
   676|                            source_confidence=item.confidence if item.confidence in CONFIDENCES else "low",
   677|                            notes=item.evidence_note,
   678|                        ),
   679|                        note=f"Cache explicit instrument search result from {getattr(provider, 'name', 'external')}",
   680|                        created_by="dashboard_search",
   681|                    )
   682|                    item.catalog_entry_id = catalog_id
   683|                except Exception:
   684|                    warnings.append("provider_result_cache_failed")
   685|            results.append(item)
   686|    conn.commit()
   687|    return _merge_search_results(results), warnings
   688|
   689|
   690|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]:
   691|    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}
   692|    seen_isin: dict[str, set[tuple[str | None, str | None]]] = {}
   693|    for instrument_id in instrument_ids:
   694|        row = conn.execute("SELECT * FROM instruments WHERE instrument_id=?", (instrument_id,)).fetchone()
   695|        if row is None:
   696|            continue
   697|        summary["instruments_total"] += 1
   698|        ensure_instrument_metadata_quality(conn, instrument_id=instrument_id)
   699|        entry = CatalogEntryInput(
   700|            asset_class="etf" if (row["asset_class"] or "").lower() == "etf" else "equity",
   701|            name=row["name"], isin=row["isin"], ticker=row["ticker"], exchange=row["exchange"], trading_currency=row["trading_currency"] or row["currency"], instrument_currency=row["currency"],
   702|            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,
   703|            hedged_to_currency=row["hedged_to_currency"], hedge_status=row["hedge_status"] or "unknown", instrument_status=row["instrument_status"] or "unknown",
   704|            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.",
   705|        )
   706|        before = conn.execute("SELECT COUNT(*) AS c FROM instrument_catalog_entries").fetchone()["c"]
   707|        upsert_catalog_entry(conn, entry, note=note)
   708|        after = conn.execute("SELECT COUNT(*) AS c FROM instrument_catalog_entries").fetchone()["c"]
   709|        if after > before:
   710|            summary["catalog_entries_created"] += 1
   711|        if row["isin"]:
   712|            seen_isin.setdefault(row["isin"], set()).add((row["exchange"], row["currency"]))
   713|        if (row["hedge_status"] or "unknown") == "unknown":
   714|            summary["hedge_unknown"] += 1
   715|        if (row["instrument_status"] or "unknown") == "unknown":
   716|            summary["instrument_status_unknown"] += 1
   717|        if row["provider_symbol"] and row["instrument_status"] == "active" and row["hedge_status"] != "unknown":
   718|            summary["valuation_ready"] += 1
   719|        else:
   720|            summary["manual_review_required"] += 1
   721|            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")
   722|    summary["multiple_listings"] = sum(1 for listings in seen_isin.values() if len(listings) > 1)
   723|    conn.commit()
   724|    return summary
   725|
   726|
   727|def catalog_entry_to_provider_candidate(conn: Connection, catalog_entry_id: str) -> ProviderCandidate:
   728|    row = conn.execute("SELECT * FROM instrument_catalog_entries WHERE catalog_entry_id=?", (catalog_entry_id,)).fetchone()
   729|    if row is None:
   730|

Use offset=730 to continue reading (showing 560-729 of 737 lines)


Explanation:
**Considering storage options**

I’m thinking about the cache that's stored in SQLite for explicit searches. It feels like I might need a user interface to manage that better. Let’s explore how to present this information efficiently. It’s crucial to balance simplicity and functionality so the user can easily access and understand the cached data. I want to ensure it’s intuitive, so the user can interact with it smoothly. I’ll have to plan this out carefully!


     1|from __future__ import annotations
     2|
     3|from jarvis_finance.dashboard.components.tables import show_table
     4|from jarvis_finance.equity.manage import (
     5|    add_manual_position_from_catalog,
     6|    create_manual_catalog_entry,
     7|    ensure_standard_broker_accounts,
     8|    get_equity_management_options,
     9|    search_instrument_candidates,
    10|)
    11|from jarvis_finance.market_data.catalog import instrument_provider_statuses
    12|
    13|PAGE_TITLE = "Position hinzufügen"
    14|SECRET_ENV_PATH = "~/jarvis_runtime/finance-system/secrets/.env"
    15|SUPPORTED_PROVIDER_KEYS = "OPENFIGI_API_KEY, FMP_API_KEY, FINNHUB_API_KEY, EODHD_API_KEY"
    16|
    17|
    18|def _account_label(row: dict) -> str:
    19|    return f"{row['platform_name']} · {row['account_name']} · {row['currency']}"
    20|
    21|
    22|def _selected(rows: list[dict], label: str | None, labeler) -> dict | None:
    23|    if not label:
    24|        return None
    25|    for row in rows:
    26|        if labeler(row) == label:
    27|            return row
    28|    return None
    29|
    30|
    31|def _asset_label(value: str) -> str:
    32|    return "Aktie" if value == "stock" else "ETF" if value == "etf" else value
    33|
    34|
    35|def _search_rows(results: list[dict]) -> list[dict[str, str]]:
    36|    return [
    37|        {
    38|            "Name": r.get("name", ""),
    39|            "ISIN": r.get("isin", ""),
    40|            "Ticker": r.get("ticker", ""),
    41|            "Exchange": r.get("exchange", ""),
    42|            "Währung": r.get("currency", ""),
    43|            "Assetklasse": _asset_label(r.get("asset_class", "")),
    44|            "Land": r.get("country", ""),
    45|            "Datenquelle": r.get("data_source", ""),
    46|            "Status": r.get("trust_status", r.get("mapping_status", "Manuell prüfen")),
    47|        }
    48|        for r in results
    49|    ]
    50|
    51|
    52|def _price_preview(selected: dict | None) -> str:
    53|    if not selected:
    54|        return "Preis noch nicht verfügbar"
    55|    price = selected.get("last_price")
    56|    if not price:
    57|        return "Preis noch nicht verfügbar"
    58|    currency = selected.get("price_currency") or selected.get("currency") or ""
    59|    date = selected.get("price_date") or "Datum unbekannt"
    60|    source = selected.get("price_source") or "lokale market_prices"
    61|    return f"{price} {currency} per {date} · Quelle: {source}".strip()
    62|
    63|
    64|def _quality_warnings(*, selected: dict | None, quantity: str, account_label: str | None, cost_basis: str, currency: str, fx_status: str, fx_rate: str, note: str, transaction_type: str) -> list[str]:
    65|    warnings: list[str] = []
    66|    if not selected:
    67|        warnings.append("Instrument noch nicht ausgewählt")
    68|    if not account_label:
    69|        warnings.append("Depot/Konto fehlt")
    70|    if not str(quantity or "").strip() and transaction_type not in {"Dividende / Ausschüttung"}:
    71|        warnings.append("Menge fehlt")
    72|    if not str(cost_basis or "").strip():
    73|        warnings.append("cost_basis_uncertain: Einstand/Cost Basis fehlt; Performance bleibt unvollständig")
    74|    if currency in {"USD", "EUR"} and not str(fx_rate or "").strip() and fx_status not in {"missing", "manual_override_required"}:
    75|        warnings.append("FX fehlt: Fremdwährung braucht FX-Kurs oder Status 'missing'")
    76|    if transaction_type in {"Manuelle Korrektur"} and not str(note or "").strip():
    77|        warnings.append("Notiz fehlt für manuelle Korrektur")
    78|    if selected and selected.get("trust_status") in {"Unsicher", "Manuell prüfen"}:
    79|        warnings.append("Treffer ist prüfpflichtig; Auswahl bewusst bestätigen")
    80|    return warnings
    81|
    82|
    83|def render(st, conn) -> None:
    84|    st.title("Position hinzufügen")
    85|    st.caption("Geführter User-Mode-Wizard für manuelle Aktien-/ETF-Erfassung. Kein DOCX-Import, keine Live-API beim normalen Laden.")
    86|    st.info("Hauptpfad: Assetklasse wählen → lokal/online suchen → Treffer bewusst auswählen → Position erfassen → Review → bestätigen.")
    87|
    88|    ensure_standard_broker_accounts(conn)
    89|    options = get_equity_management_options(conn)
    90|    account_labels = [_account_label(row) for row in options["accounts"]]
    91|
    92|    st.subheader("Provider-Status")
    93|    show_table(st, instrument_provider_statuses())
    94|    st.caption(f"API-Keys lokal ablegen unter: {SECRET_ENV_PATH}")
    95|    st.caption(f"Unterstützte Variablen: {SUPPORTED_PROVIDER_KEYS}")
    96|    st.caption("Keys werden nicht ins Repo geschrieben, nicht geloggt und nicht im Dashboard angezeigt.")
    97|
    98|    st.subheader("1. Assetklasse wählen")
    99|    asset_choice = st.selectbox("Assetklasse", ["Aktie", "ETF", "Crypto", "Cash"], key="position_add_asset_choice")
   100|    if asset_choice in {"Crypto", "Cash"}:
   101|        st.warning("Für Crypto/Cash bitte vorerst die bestehenden Crypto-/Wallet-Funktionen nutzen. Dieser Sprint macht Aktie und ETF vollständig nutzbar.")
   102|        return
   103|    asset_class = "stock" if asset_choice == "Aktie" else "etf"
   104|
   105|    st.subheader("2. Instrument suchen")
   106|    query = st.text_input("ISIN, Name oder Ticker", key="position_add_query")
   107|    currency_filter = st.selectbox("Währung optional", ["", "CHF", "EUR", "USD"], key="position_add_currency")
   108|    exchange_filter = st.text_input("Börse/Exchange optional", key="position_add_exchange")
   109|    country_filter = st.text_input("Land optional", key="position_add_country")
   110|    provider_filter = st.text_input("Provider optional", key="position_add_provider")
   111|    c1, c2, c3 = st.columns(3)
   112|    if c1.button("Lokal suchen", key="position_add_local_search"):
   113|        st.session_state["position_add_search_results"] = search_instrument_candidates(
   114|            conn,
   115|            query=query,
   116|            asset_class=asset_class,
   117|            exchange=exchange_filter or None,
   118|            currency=currency_filter or None,
   119|            country=country_filter or None,
   120|            provider=provider_filter or None,
   121|            include_external=False,
   122|        )
   123|        st.session_state.pop("position_add_selected_index", None)
   124|    if c2.button("Online suchen", key="position_add_online_search"):
   125|        st.session_state["position_add_search_results"] = search_instrument_candidates(
   126|            conn,
   127|            query=query,
   128|            asset_class=asset_class,
   129|            exchange=exchange_filter or None,
   130|            currency=currency_filter or None,
   131|            country=country_filter or None,
   132|            provider=provider_filter or None,
   133|            include_external=True,
   134|        )
   135|        st.session_state.pop("position_add_selected_index", None)
   136|    c3.caption("Online-Suche läuft nur bei Klick. Fehlende Keys erzeugen Hinweis, keinen Crash.")
   137|
   138|    search_state = st.session_state.get("position_add_search_results", {"results": [], "warnings": [], "selection_required": True, "provider_lookup_attempted": False})
   139|    warnings = [str(w) for w in search_state.get("warnings", [])]
   140|    if warnings:
   141|        st.warning("; ".join(warnings))
   142|    results = list(search_state.get("results", []))
   143|    show_table(st, _search_rows(results), empty_message="Noch keine Suche ausgeführt oder kein Treffer. Nutze 'Lokal suchen', 'Online suchen' oder 'Manuell anlegen'.")
   144|
   145|    st.subheader("3. Instrument auswählen")
   146|    selected = None
   147|    labels = [f"{i + 1}. {r.get('name')} | {r.get('isin') or 'ISIN fehlt'} | {r.get('ticker') or 'Ticker fehlt'} | {r.get('exchange') or 'Exchange fehlt'} | {r.get('currency') or 'Währung fehlt'} | {r.get('trust_status', 'Manuell prüfen')}" for i, r in enumerate(results)]
   148|    if labels:
   149|        chosen = st.selectbox("Treffer auswählen", labels, key="position_add_selected_label")
   150|        chosen_index = int(chosen.split('.', 1)[0]) - 1 if chosen else 0
   151|        if st.button("Auswählen", key="position_add_select_result"):
   152|            st.session_state["position_add_selected_index"] = chosen_index
   153|        selected_index = st.session_state.get("position_add_selected_index", chosen_index)
   154|        if 0 <= int(selected_index) < len(results):
   155|            selected = results[int(selected_index)]
   156|            st.success("Instrument ausgewählt. Ticker allein wird nie automatisch übernommen; die Auswahl bleibt bewusst.")
   157|            show_table(st, [_search_rows([selected])[0]])
   158|            st.subheader("Preisvorschau")
   159|            preview = _price_preview(selected)
   160|            if preview == "Preis noch nicht verfügbar":
   161|                st.info(preview)
   162|            else:
   163|                st.success(preview)
   164|    else:
   165|        st.info("Noch kein Instrument ausgewählt.")
   166|
   167|    with st.expander("Manuell anlegen"):
   168|        manual_name = st.text_input("Name", key="position_add_manual_name")
   169|        manual_isin = st.text_input("ISIN stark empfohlen", key="position_add_manual_isin")
   170|        manual_ticker = st.text_input("Ticker", key="position_add_manual_ticker")
   171|        manual_exchange = st.text_input("Exchange", key="position_add_manual_exchange")
   172|        manual_currency = st.selectbox("Währung", ["CHF", "EUR", "USD"], key="position_add_manual_currency")
   173|        manual_note = st.text_area("Notiz / Quelle", key="position_add_manual_note")
   174|        st.warning("Manuelle Anlage ohne ISIN bleibt prüfpflichtig. ISIN und Ticker dürfen nicht beide leer sein.")
   175|        if st.button("Manuell anlegen", key="position_add_manual_save"):
   176|            try:
   177|                if not manual_isin and not manual_ticker:
   178|                    raise ValueError("ISIN oder Ticker ist erforderlich")
   179|                create_manual_catalog_entry(conn, asset_class=asset_class, name=manual_name, currency=manual_currency, isin=manual_isin, ticker=manual_ticker, exchange=manual_exchange, note=manual_note)
   180|                st.success("Instrument lokal gespeichert. Jetzt lokal suchen und bewusst auswählen.")
   181|            except Exception as exc:
   182|                st.error(f"Manuelle Anlage abgelehnt: {exc}")
   183|
   184|    st.subheader("4. Position erfassen")
   185|    account_label = st.selectbox("Plattform/Depot", account_labels, key="position_add_account") if account_labels else None
   186|    transaction_type = st.selectbox("Transaktionstyp", ["Initial Snapshot", "Kauf", "Dividende / Ausschüttung", "Manuelle Korrektur"], key="position_add_tx_type")
   187|    tx_map = {"Initial Snapshot": "initial_snapshot", "Kauf": "buy", "Dividende / Ausschüttung": "dividend", "Manuelle Korrektur": "manual_adjustment"}
   188|    quantity = st.text_input("Menge als Decimal-Text", key="position_add_quantity")
   189|    trade_date = st.date_input("Snapshot-/Buchungsdatum", key="position_add_date")
   190|    default_currency = selected.get("currency") if selected else "CHF"
   191|    currencies = ["CHF", "EUR", "USD"]
   192|    currency = st.selectbox("Originalwährung", currencies, index=currencies.index(default_currency) if default_currency in currencies else 0, key="position_add_pos_currency")
   193|    cost_basis = st.text_input("Einstand / Cost Basis optional", key="position_add_cost_basis")
   194|    fees = st.text_input("Gebühren optional", key="position_add_fees")
   195|    fx_status = st.selectbox("FX-Status", ["not_needed", "ok", "missing", "manual_override", "manual_override_required"], key="position_add_fx_status")
   196|    fx_rate = st.text_input("FX-Kurs zu CHF optional", key="position_add_fx_rate")
   197|    fx_source = st.text_input("FX-Quelle optional", key="position_add_fx_source")
   198|    category = st.selectbox("Kategorie", ["Core", "Opportunity", "Unknown"], key="position_add_category")
   199|    note = st.text_area("Notiz — Pflicht bei manueller Korrektur oder unvollständiger Historie", key="position_add_note")
   200|
   201|    st.subheader("5. Review")
   202|    missing = _quality_warnings(selected=selected, quantity=quantity, account_label=account_label, cost_basis=cost_basis, currency=currency, fx_status=fx_status, fx_rate=fx_rate, note=note, transaction_type=transaction_type)
   203|    review = {
   204|        "Was wird gespeichert?": transaction_type,
   205|        "Instrument": selected.get("name") if selected else "noch nicht ausgewählt",
   206|        "ISIN": selected.get("isin") if selected else "",
   207|        "Ticker": selected.get("ticker") if selected else "",
   208|        "Exchange": selected.get("exchange") if selected else "",
   209|        "Datenquelle": selected.get("data_source") if selected else "",
   210|        "Depot": account_label or "fehlt",
   211|        "Menge": quantity or "fehlt",
   212|        "Datum": str(trade_date),
   213|        "Kategorie": category,
   214|        "Cost Basis": "erfasst" if cost_basis else "fehlt/ungewiss",
   215|        "Gebühren": "notiert" if fees else "keine/fehlt",
   216|        "FX": fx_status,
   217|        "Datenqualitätswarnungen": "; ".join(missing) if missing else "keine",
   218|    }
   219|    st.write(review)
   220|    if missing:
   221|        st.warning("Review-Hinweise: " + "; ".join(missing))
   222|    confirmed = st.checkbox("Ich bestätige diese Buchung.", key="position_add_confirm")
   223|
   224|    st.subheader("6. Speichern")
   225|    selected_account = _selected(options["accounts"], account_label, _account_label)
   226|    if st.button("Position speichern", key="position_add_save", type="primary"):
   227|        try:
   228|            if not selected or not selected.get("catalog_entry_id"):
   229|                raise ValueError("Instrument muss vor Speicherung ausgewählt werden")
   230|            if not selected_account:
   231|                raise ValueError("Depot/Konto ist erforderlich")
   232|            final_note = note if not fees else f"{note}\nGebühren notiert: {fees}".strip()
   233|            result = add_manual_position_from_catalog(
   234|                conn,
   235|                catalog_entry_id=selected["catalog_entry_id"],
   236|                account_id=selected_account["account_id"],
   237|                position_type=tx_map[transaction_type],
   238|                quantity_text=quantity,
   239|                trade_date=str(trade_date),
   240|                currency=currency,
   241|                cost_basis_original_text=cost_basis or None,
   242|                fx_status=fx_status,
   243|                fx_rate_to_chf_text=fx_rate or None,
   244|                fx_source=fx_source or None,
   245|                note=final_note,
   246|                category=category,
   247|                confirm=bool(confirmed),
   248|            )
   249|            st.success("Position gespeichert: Ledger-Ereignis und Audit-Log wurden erzeugt.")
   250|            st.info("Position in Aktien & ETFs anzeigen: öffne im User Mode die Seite 'Aktien & ETFs'.")
   251|            if result.warnings:
   252|                st.warning("Datenqualität: " + "; ".join(result.warnings))
   253|        except Exception as exc:
   254|            st.error(f"Speicherung abgelehnt: {exc}")
   255|


   345|
   346|
   347|def search_instrument_candidates(
   348|    conn: Connection,
   349|    *,
   350|    query: str,
   351|    asset_class: str | None = None,
   352|    exchange: str | None = None,
   353|    currency: str | None = None,
   354|    country: str | None = None,
   355|    provider: str | None = None,
   356|    include_external: bool = False,
   357|) -> dict[str, object]:
   358|    """Search local instrument catalog. External providers are only used when explicitly enabled by caller."""
   359|    q = str(query or "").strip()
   360|    if not q:
   361|        return {"results": [], "warnings": [], "selection_required": True, "provider_lookup_attempted": False}
   362|    providers = default_instrument_lookup_providers() if include_external else []
   363|    results, warnings = search_instruments(conn, q, asset_class, providers=providers)
   364|    filtered = []
   365|    for r in results:
   366|        if exchange and (r.exchange or "").upper() != exchange.upper():
   367|            continue
   368|        if currency and (r.trading_currency or r.instrument_currency or "").upper() != currency.upper():
   369|            continue
   370|        if provider and (r.provider or "").lower() != provider.lower():
   371|            continue
   372|        if country and (getattr(r, "country", None) or "").upper() != country.upper():
   373|            continue
   374|        row = {
   375|            "catalog_entry_id": r.catalog_entry_id or "",
   376|            "name": r.name,
   377|            "isin": r.isin or "",
   378|            "ticker": r.ticker or "",
   379|            "exchange": r.exchange or "",
   380|            "currency": r.trading_currency or r.instrument_currency or "",
   381|            "asset_class": r.asset_class,
   382|            "country": getattr(r, "country", None) or "",
   383|            "data_source": r.source,
   384|            "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"),
   385|            "trust_status": "Eindeutig" if r.isin and q.upper() == (r.isin or "").upper() else ("Manuell prüfen" if (r.ticker or "").upper() == q.upper() else ("Wahrscheinlich" if r.confidence == "medium" else "Unsicher")),
   386|            "confidence": r.confidence,
   387|            "last_price": r.last_price or "",
   388|            "price_date": r.price_date or "",
   389|            "price_currency": r.price_currency or "",
   390|            "price_source": r.price_source or "",
   391|            "selection_required": "yes",
   392|        }
   393|        inst = None
   394|        if row["isin"]:
   395|            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()
   396|        if inst:
   397|            row.update(_latest_market_price(conn, inst["instrument_id"]))
   398|        filtered.append(row)
   399|    # ISIN exact single hit may be unambiguous for display, but user still must click/select before writing.
   400|    selection_required = True
   401|    if not filtered:
   402|        warnings.append("no_local_match_manual_entry_allowed")
   403|    if include_external and "provider_lookup_unavailable" in warnings:
   404|        warnings.append("manual_entry_allowed")
   405|    return {"results": filtered, "warnings": warnings, "selection_required": selection_required, "provider_lookup_attempted": include_external}
   406|
   407|
   408|def add_equity_initial_snapshot(
   409|    conn: Connection,
   410|    *,
   411|    account_id: str,
   412|    asset_class: str,
   413|    name: str,
   414|    isin: str = "",
   415|    ticker: str = "",
   416|    exchange: str = "",
   417|    currency: str,
   418|    quantity_text: str,
   419|    snapshot_date: str,
   420|    cost_basis_original_text: str | None = None,
   421|    category: str = "Unknown",
   422|    note: str = "",
   423|    confirm: bool = False,
   424|    fx_rate_to_chf_text: str | None = None,
   425|    fx_source: str | None = None,
   426|    fx_status: str = "ok",
   427|    ter_text: str | None = None,
   428|    distribution_policy: str | None = None,
   429|    index_name: str | None = None,
   430|    fund_domicile: str | None = None,
   431|    benchmark: str | None = None,
   432|) -> EquityManageResult:
   433|    _require_confirm(confirm)
   434|    account_id = _require_account(conn, account_id)
   435|    asset_class = _normalize_asset_class(asset_class)
   436|    currency = _require_currency(currency)
   437|    snapshot_date = _require_date(snapshot_date)
   438|    note = _require_note(note, reason="initial_position_snapshot with incomplete history")
   439|    qty = parse_decimal_text(quantity_text, field_name="quantity")
   440|    if qty <= 0:
   441|        raise ValueError("quantity must be > 0")
   442|    warnings: list[str] = []
   443|    if cost_basis_original_text in {None, ""}:
   444|        gross = Decimal("0")
   445|        warnings.append("cost_basis_uncertain")
   446|    else:
   447|        gross = parse_decimal_text(str(cost_basis_original_text), field_name="cost basis")
   448|        if gross < 0:
   449|            raise ValueError("cost basis must be >= 0")
   450|    instrument_id = _ensure_instrument(
   451|        conn,
   452|        asset_class=asset_class,
   453|        name=name,
   454|        isin=isin,
   455|        ticker=ticker,
   456|        exchange=exchange,
   457|        currency=currency,
   458|        category=category,
   459|        note=note,
   460|

Use offset=460 to continue reading (showing 345-459 of 791 lines)


patch

            if exc.code in {401, 403}:
                raise LookupUnavailable(f"{self.name}_auth_failed") from exc
            if exc.code == 429:

            if exc.code in {401, 403}:
                raise LookupUnavailable(f"{self.name}_auth_failed") from exc
            if exc.code == 402:
                raise LookupUnavailable(f"{self.name}_endpoint_restricted") from exc
            if exc.code == 429:


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/catalog.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/catalog.py
@@ -139,6 +139,8 @@
         except error.HTTPError as exc:
             if exc.code in {401, 403}:
                 raise LookupUnavailable(f"{self.name}_auth_failed") from exc
+            if exc.code == 402:
+                raise LookupUnavailable(f"{self.name}_endpoint_restricted") from exc
             if exc.code == 429:
                 reset = exc.headers.get("ratelimit-reset") if exc.headers else None
                 try:
@@ -169,6 +171,8 @@
         except error.HTTPError as exc:
             if exc.code in {401, 403}:
                 raise LookupUnavailable(f"{self.name}_auth_failed") from exc
+            if exc.code == 402:
+                raise LookupUnavailable(f"{self.name}_endpoint_restricted") from exc
             if exc.code == 429:
                 reset = exc.headers.get("ratelimit-reset") if exc.headers else None
                 try:

/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/catalog.py was last read with offset/limit pagination (partial view). Re-read the whole file before overwriting it.


                    if retry_exc.code == 429:
                        raise LookupUnavailable(f"{self.name}_rate_limited") from retry_exc
                    if retry_exc.code in {401, 403}:
                        raise LookupUnavailable(f"{self.name}_auth_failed") from retry_exc
                    if retry_exc.code == 402:
                        raise LookupUnavailable(f"{self.name}_endpoint_restricted") from retry_exc
                    raise LookupUnavailable(f"{self.name}_provider_error") from retry_exc

                    if retry_exc.code == 429:
                        raise LookupUnavailable(f"{self.name}_rate_limited") from retry_exc
                    if retry_exc.code in {401, 403}:
                        raise LookupUnavailable(f"{self.name}_auth_failed") from retry_exc
                    raise LookupUnavailable(f"{self.name}_provider_error") from retry_exc


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/catalog.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/catalog.py
@@ -156,6 +156,8 @@
                         raise LookupUnavailable(f"{self.name}_rate_limited") from retry_exc
                     if retry_exc.code in {401, 403}:
                         raise LookupUnavailable(f"{self.name}_auth_failed") from retry_exc
+                    if retry_exc.code == 402:
+                        raise LookupUnavailable(f"{self.name}_endpoint_restricted") from retry_exc
                     raise LookupUnavailable(f"{self.name}_provider_error") from retry_exc
             raise LookupUnavailable(f"{self.name}_provider_error") from exc
         except error.URLError as exc:
@@ -188,6 +190,8 @@
                         raise LookupUnavailable(f"{self.name}_rate_limited") from retry_exc
                     if retry_exc.code in {401, 403}:
                         raise LookupUnavailable(f"{self.name}_auth_failed") from retry_exc
+                    if retry_exc.code == 402:
+                        raise LookupUnavailable(f"{self.name}_endpoint_restricted") from retry_exc
                     raise LookupUnavailable(f"{self.name}_provider_error") from retry_exc
             raise LookupUnavailable(f"{self.name}_provider_error") from exc
         except error.URLError as exc:


            except LookupUnavailable as exc:
                if "endpoint_restricted" not in str(exc):
                    last_error = exc
                continue

            except LookupUnavailable as exc:
                last_error = exc
                continue


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/catalog.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/catalog.py
@@ -313,7 +313,8 @@
             try:
                 payload = self._get_stable(endpoint, params)
             except LookupUnavailable as exc:
-                last_error = exc
+                if "endpoint_restricted" not in str(exc):
+                    last_error = exc
                 continue
             items = payload if isinstance(payload, list) else payload.get("data", []) if isinstance(payload, dict) else []
             for item in items[:10]:


    if "network_error" in text:
        return "network_error"
    if "endpoint_restricted" in text:
        return "endpoint_restricted"
    if "no_results" in text:

    if "network_error" in text:
        return "network_error"
    if "no_results" in text:


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/catalog.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/catalog.py
@@ -406,6 +406,8 @@
         return "rate_limited"
     if "network_error" in text:
         return "network_error"
+    if "endpoint_restricted" in text:
+        return "endpoint_restricted"
     if "no_results" in text:
         return "no_results"
     if "parsing_error" in text:


    if category == "network_error":
        return "Netzwerkfehler"
    if category == "endpoint_restricted":
        return "Endpoint im Tarif nicht verfügbar"
    if category == "no_results":

    if category == "network_error":
        return "Netzwerkfehler"
    if category == "no_results":


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/catalog.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/market_data/catalog.py
@@ -424,6 +424,8 @@
         return "Rate Limit"
     if category == "network_error":
         return "Netzwerkfehler"
+    if category == "endpoint_restricted":
+        return "Endpoint im Tarif nicht verfügbar"
     if category == "no_results":
         return "Keine Treffer"
     if category == "parsing_error":


    50|class MockEquityPriceProvider:
    51|    name = "mock"
    52|
    53|    def __init__(self, prices: dict[str, Decimal | str | None], *, currency: str = "USD", timestamps: dict[str, str] | None = None) -> None:
    54|        self.prices = {k: (Decimal(str(v)) if v is not None else None) for k, v in prices.items()}
    55|        self.currency = currency.upper()
    56|        self.timestamps = timestamps or {}
    57|
    58|    def get_price(self, provider_symbol: str, *, price_date: str | None = None) -> EquityPriceQuote:
    59|        if provider_symbol not in self.prices or self.prices[provider_symbol] is None:
    60|            return EquityPriceQuote(provider_symbol=provider_symbol, currency=self.currency, close=None, quality_status="missing", error_message="price missing", provider=self.name)
    61|        return EquityPriceQuote(provider_symbol=provider_symbol, currency=self.currency, close=self.prices[provider_symbol], price_timestamp=self.timestamps.get(provider_symbol), provider=self.name)
    62|
    63|
    64|class FmpEquityPriceProvider:
    65|    name = "fmp"
    66|
    67|    def __init__(self, *, api_key: str | None = None, timeout_seconds: float = 8.0) -> None:
    68|        self.api_key = api_key or _runtime_secret_value(("FMP_API_KEY", "FINANCIAL_MODELING_PREP_API_KEY", "JARVIS_FMP_API_KEY"))
    69|        self.timeout_seconds = timeout_seconds
    70|
    71|    def _get_json(self, path: str, params: dict[str, str]) -> object:
    72|        if not self.api_key:
    73|            raise RuntimeError("fmp_api_key_missing")
    74|        path = path if path.startswith("/") else "/" + path
    75|        url = "https://financialmodelingprep.com" + path + "?" + parse.urlencode({**params, "apikey": self.api_key})
    76|        req = request.Request(url, headers={"Accept": "application/json"})
    77|        try:
    78|            with request.urlopen(req, timeout=self.timeout_seconds) as response:  # noqa: S310 - explicit user-triggered provider lookup
    79|                return json.loads(response.read().decode("utf-8"))
    80|        except error.HTTPError as exc:
    81|            if exc.code in {401, 403}:
    82|                raise RuntimeError("fmp_auth_failed") from exc
    83|            if exc.code == 429:
    84|                raise RuntimeError("fmp_rate_limited") from exc
    85|            raise RuntimeError("fmp_provider_error") from exc
    86|        except error.URLError as exc:
    87|            raise RuntimeError("fmp_network_error") from exc
    88|
    89|    def get_price(self, provider_symbol: str, *, price_date: str | None = None) -> EquityPriceQuote:
    90|        symbol = (provider_symbol or "").strip().upper()
    91|        if not symbol:
    92|            return EquityPriceQuote(provider_symbol=symbol, currency="", close=None, provider=self.name, quality_status="missing", error_message="provider_symbol_missing")
    93|        try:
    94|            payload = self._get_json("/stable/profile", {"symbol": symbol})
    95|        except RuntimeError as exc:
    96|            return EquityPriceQuote(provider_symbol=symbol, currency="", close=None, provider=self.name, quality_status="missing", error_message=str(exc))
    97|        row = payload[0] if isinstance(payload, list) and payload and isinstance(payload[0], dict) else payload if isinstance(payload, dict) else {}
    98|        if isinstance(row.get("data"), list) and row["data"] and isinstance(row["data"][0], dict):
    99|            row = row["data"][0]
   100|        price = row.get("price") or row.get("lastPrice")
   101|        try:
   102|            close = Decimal(str(price)) if price not in {None, ""} else None
   103|        except (InvalidOperation, ValueError):
   104|            close = None
   105|        if close is None or close <= 0:
   106|            return EquityPriceQuote(provider_symbol=symbol, currency="", close=None, provider=self.name, quality_status="missing", error_message="price_missing")
   107|        currency = str(row.get("currency") or row.get("priceCurrency") or "").upper()
   108|        return EquityPriceQuote(provider_symbol=symbol, currency=currency or "USD", close=close, provider=self.name, price_timestamp=utc_now(), quality_status="fresh")
   109|
   110|
   111|def _resolve_market_alerts(conn: Connection, *, instrument_id: str) -> int:
   112|    return resolve_instrument_alerts(conn, instrument_id=instrument_id, rule_ids=["missing_market_price", "stale_market_price"])
   113|
   114|
   115|def _previous_price(conn: Connection, *, instrument_id: str, price_date: str, provider: str):
   116|    return conn.execute(
   117|        """
   118|        SELECT * FROM market_prices
   119|        WHERE instrument_id=? AND provider=? AND price_date<? AND close IS NOT NULL AND close!=''
   120|        ORDER BY price_date DESC, created_at DESC LIMIT 1
   121|        """,
   122|        (instrument_id, provider, price_date),
   123|    ).fetchone()
   124|
   125|
   126|def _corporate_action_status(conn: Connection, *, instrument_id: str, price_date: str, provider: str, close: Decimal | None) -> str:
   127|    if close is None or close <= 0:
   128|        return "not_checked"
   129|    prev = _previous_price(conn, instrument_id=instrument_id, price_date=price_date, provider=provider)
   130|    if not prev or not prev["close"]:
   131|        return "not_checked"
   132|    old = Decimal(str(prev["close"]))
   133|    if old <= 0:
   134|        return "not_checked"
   135|    change = abs((close - old) / old)
   136|    if change > Decimal("0.25"):
   137|        create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="corporate_action_suspected", message="Large local price move detected; corporate action review required before high-confidence valuation.", evidence={"previous_price_date": prev["price_date"], "price_date": price_date}, fingerprint="corporate_action_suspected")
   138|        create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id="split_or_corporate_action_review_required", message="Split/corporate-action review required; no automatic split correction is applied.", evidence={"previous_price_date": prev["price_date"], "price_date": price_date}, fingerprint="split_or_corporate_action_review_required")
   139|        update_instrument_metadata(conn, instrument_id=instrument_id, corporate_action_status="suspected", split_or_corporate_action_review_required=True, note="Automatic C2 heuristic detected >25% local price move; review required.")
   140|        return "suspected"
   141|    return "none_known"
   142|
   143|
   144|def store_market_price(
   145|    conn: Connection,
   146|    *,
   147|    instrument_id: str,
   148|    price_date: str,
   149|    close: Decimal | None,
   150|    currency: str,
   151|    provider: str,
   152|    provider_symbol: str | None,
   153|    provider_market: str | None = None,
   154|    price_timestamp: str | None = None,
   155|    adjusted_close: Decimal | None = None,
   156|    quality_status: str = "fresh",
   157|    error_message: str | None = None,
   158|) -> str:
   159|    now = utc_now()
   160|    corp_status = _corporate_action_status(conn, instrument_id=instrument_id, price_date=price_date, provider=provider, close=close) if quality_status == "fresh" else "not_checked"
   161|    market_price_id = stable_id("marketprice", instrument_id, price_date, provider, provider_symbol or "", now)
   162|    conn.execute(
   163|        """
   164|        INSERT INTO market_prices(
   165|            market_price_id, instrument_id, price_date, price_timestamp, close, adjusted_close,
   166|            currency, provider, provider_symbol, quality_status, created_at, provider_market, error_message,
   167|            corporate_action_status
   168|        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
   169|        ON CONFLICT(instrument_id, price_date, provider) DO UPDATE SET
   170|            price_timestamp=excluded.price_timestamp,
   171|            close=excluded.close,
   172|            adjusted_close=excluded.adjusted_close,
   173|            currency=excluded.currency,
   174|            provider_symbol=excluded.provider_symbol,
   175|            quality_status=excluded.quality_status,
   176|            created_at=excluded.created_at,
   177|            provider_market=excluded.provider_market,
   178|            error_message=excluded.error_message,
   179|            corporate_action_status=excluded.corporate_action_status
   180|        """,
   181|        (market_price_id, instrument_id, price_date, price_timestamp, format(close, "f") if close is not None else "", format(adjusted_close, "f") if adjusted_close is not None else None, currency.upper(), provider, provider_symbol, quality_status, now, provider_market, error_message, corp_status),
   182|    )
   183|    if quality_status in {"missing", "stale", "error", "conflict"} or close is None:
   184|        rule = "stale_market_price" if quality_status == "stale" else "missing_market_price"
   185|        create_alert(conn, priority="warnung", category="market_data", entity_type="instrument", entity_id=instrument_id, rule_id=rule, message="Instrument local market price is not fresh.", evidence={"provider": provider, "provider_symbol": provider_symbol, "price_date": price_date, "quality_status": quality_status}, fingerprint=f"{rule}:{provider}:{provider_symbol}")
   186|    elif quality_status == "fresh":
   187|        _resolve_market_alerts(conn, instrument_id=instrument_id)
   188|    conn.commit()
   189|    return market_price_id
   190|
   191|
   192|def refresh_market_prices(
   193|    conn: Connection,
   194|    *,
   195|    provider: EquityPriceProvider,
   196|    asset_class: str,
   197|    price_date: str | None = None,
   198|    only_missing: bool = False,
   199|    only_stale: bool = False,
   200|

Use offset=200 to continue reading (showing 50-199 of 251 lines)


    asset_choice = st.selectbox("Assetklasse", ["Alle", "Aktie", "ETF", "Crypto", "Cash"], key="position_add_asset_choice")
    if asset_choice in {"Crypto", "Cash"}:
        st.warning("Für Crypto/Cash bitte vorerst die bestehenden Crypto-/Wallet-Funktionen nutzen. Dieser Sprint macht Aktie und ETF vollständig nutzbar.")
        return
    asset_class = None if asset_choice == "Alle" else "stock" if asset_choice == "Aktie" else "etf"

    asset_choice = st.selectbox("Assetklasse", ["Aktie", "ETF", "Crypto", "Cash"], key="position_add_asset_choice")
    if asset_choice in {"Crypto", "Cash"}:
        st.warning("Für Crypto/Cash bitte vorerst die bestehenden Crypto-/Wallet-Funktionen nutzen. Dieser Sprint macht Aktie und ETF vollständig nutzbar.")
        return
    asset_class = "stock" if asset_choice == "Aktie" else "etf"


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_17_position_add.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_17_position_add.py
@@ -96,11 +96,11 @@
     st.caption("Keys werden nicht ins Repo geschrieben, nicht geloggt und nicht im Dashboard angezeigt.")

     st.subheader("1. Assetklasse wählen")
-    asset_choice = st.selectbox("Assetklasse", ["Aktie", "ETF", "Crypto", "Cash"], key="position_add_asset_choice")
+    asset_choice = st.selectbox("Assetklasse", ["Alle", "Aktie", "ETF", "Crypto", "Cash"], key="position_add_asset_choice")
     if asset_choice in {"Crypto", "Cash"}:
         st.warning("Für Crypto/Cash bitte vorerst die bestehenden Crypto-/Wallet-Funktionen nutzen. Dieser Sprint macht Aktie und ETF vollständig nutzbar.")
         return
-    asset_class = "stock" if asset_choice == "Aktie" else "etf"
+    asset_class = None if asset_choice == "Alle" else "stock" if asset_choice == "Aktie" else "etf"

     st.subheader("2. Instrument suchen")
     query = st.text_input("ISIN, Name oder Ticker", key="position_add_query")

LSP diagnostics introduced by this edit:
<diagnostics file="/home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_17_position_add.py">
ERROR [179:63] Argument of type "Literal['stock', 'etf'] | None" cannot be assigned to parameter "asset_class" of type "str" in function "create_manual_catalog_entry"
  Type "Literal['stock', 'etf'] | None" is not assignable to type "str"
    "None" is not assignable to "str" [reportArgumentType] (Pyright)
</diagnostics>


        if st.button("Manuell anlegen", key="position_add_manual_save"):
            try:
                if not manual_isin and not manual_ticker:
                    raise ValueError("ISIN oder Ticker ist erforderlich")
                manual_asset_class = asset_class or "stock"
                create_manual_catalog_entry(conn, asset_class=manual_asset_class, name=manual_name, currency=manual_currency, isin=manual_isin, ticker=manual_ticker, exchange=manual_exchange, note=manual_note)

        if st.button("Manuell anlegen", key="position_add_manual_save"):
            try:
                if not manual_isin and not manual_ticker:
                    raise ValueError("ISIN oder Ticker ist erforderlich")
                create_manual_catalog_entry(conn, asset_class=asset_class, name=manual_name, currency=manual_currency, isin=manual_isin, ticker=manual_ticker, exchange=manual_exchange, note=manual_note)


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_17_position_add.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_17_position_add.py
@@ -176,7 +176,8 @@
             try:
                 if not manual_isin and not manual_ticker:
                     raise ValueError("ISIN oder Ticker ist erforderlich")
-                create_manual_catalog_entry(conn, asset_class=asset_class, name=manual_name, currency=manual_currency, isin=manual_isin, ticker=manual_ticker, exchange=manual_exchange, note=manual_note)
+                manual_asset_class = asset_class or "stock"
+                create_manual_catalog_entry(conn, asset_class=manual_asset_class, name=manual_name, currency=manual_currency, isin=manual_isin, ticker=manual_ticker, exchange=manual_exchange, note=manual_note)
                 st.success("Instrument lokal gespeichert. Jetzt lokal suchen und bewusst auswählen.")
             except Exception as exc:
                 st.error(f"Manuelle Anlage abgelehnt: {exc}")


    results = list(search_state.get("results", []))
    if results:
        for label, predicate in [
            ("Lokale Treffer", lambda row: "local" in str(row.get("data_source", "")).lower()),
            ("OpenFIGI Treffer", lambda row: "openfigi" in str(row.get("data_source", "")).lower()),
            ("FMP Treffer", lambda row: "fmp" in str(row.get("data_source", "")).lower()),
        ]:
            grouped = [row for row in results if predicate(row)]
            st.caption(f"{label}: {len(grouped)}")
            if grouped:
                show_table(st, _search_rows(grouped))
        other = [row for row in results if not any(token in str(row.get("data_source", "")).lower() for token in ("local", "openfigi", "fmp"))]
        if other:
            st.caption(f"Weitere Treffer: {len(other)}")
            show_table(st, _search_rows(other))
        st.caption("Manuell anlegen: verfügbar, falls lokale/Online-Treffer nicht eindeutig passen.")
    else:
        show_table(st, [], empty_message="Noch keine Suche ausgeführt oder kein Treffer. Nutze 'Lokal suchen', 'Online suchen' oder 'Manuell anlegen'.")

    results = list(search_state.get("results", []))
    show_table(st, _search_rows(results), empty_message="Noch keine Suche ausgeführt oder kein Treffer. Nutze 'Lokal suchen', 'Online suchen' oder 'Manuell anlegen'.")


--- a//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_17_position_add.py
+++ b//home/agent/.hermes/repos/FinanceManager/src/jarvis_finance/dashboard/views/page_17_position_add.py
@@ -140,7 +140,23 @@
     if warnings:
         st.warning("; ".join(warnings))
     results = list(search_state.get("results", []))
-    show_table(st, _search_rows(results), empty_message="Noch keine Suche ausgeführt oder kein Treffer. Nutze 'Lokal suchen', 'Online suchen' oder 'Manuell anlegen'.")
+    if results:
+        for label, predicate in [
+            ("Lokale Treffer", lambda row: "local" in str(row.get("data_source", "")).lower()),
+            ("OpenFIGI Treffer", lambda row: "openfigi" in str(row.get("data_source", "")).lower()),
+            ("FMP Treffer", lambda row: "fmp" in str(row.get("data_source", "")).lower()),
+        ]:
+            grouped = [row for row in results if predicate(row)]
+            st.caption(f"{label}: {len(grouped)}")
+            if grouped:
+                show_table(st, _search_rows(grouped))
+        other = [row for row in results if not any(token in str(row.get("data_source", "")).lower() for token in ("local", "openfigi", "fmp"))]
+        if other:
+            st.caption(f"Weitere Treffer: {len(other)}")
+            show_table(st, _search_rows(other))
+        st.caption("Manuell anlegen: verfügbar, falls lokale/Online-Treffer nicht eindeutig passen.")
+    else:
+        show_table(st, [], empty_message="Noch keine Suche ausgeführt oder kein Treffer. Nutze 'Lokal suchen', 'Online suchen' oder 'Manuell anlegen'.")

     st.subheader("3. Instrument auswählen")
     selected = None


terminal

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


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


from hermes_tools import terminal
script=r'''
from jarvis_finance.market_data.catalog import FmpSearchProvider
from jarvis_finance.market_data.candidates import LookupUnavailable
for q in ['US9229087690','Vanguard Total Stock','VTI']:
    try:
        rows=FmpSearchProvider(timeout_seconds=15).search(query=q, asset_class='etf')
        print(q, 'ok', bool(rows), len(rows))
    except LookupUnavailable as exc:
        print(q, 'error', str(exc))
'''
from pathlib import Path
Path('/tmp/fmp_query_smoke.py').write_text(script)
print(terminal('python /tmp/fmp_query_smoke.py', timeout=120, workdir='/home/agent/.hermes/repos/FinanceManager')['output'])


Output:
US9229087690 ok False 0
Vanguard Total Stock ok True 4
VTI ok True 10


Explanation:
**Verifying API key integration**

The user mentioned something about a key being correct, and I tested without a key, which got rejected. It seems there's confusion around the API key line, where the key name is "openfigi," but I need to test with an actual provider. It seems like the integration works without the key, but I need to run a full verification to see what's happening with the rejected key. Let's clarify this!

todo

OpenFIGI/FMP echte Endpunkte, Normalisierung, Cache, Preisvorschau ggf. anpassen

Wizard-UI und expliziten Preisupdate-Workflow verifizieren/anpassen


Aktuellen Provider-Code gegen Anforderungen prüfen

Mock-Tests ergänzen/aktualisieren und echten aggregierten Smoke-Test ausführen

Compile, pytest, Git-Safety, diff-check, status

Commit und Push, falls Änderungen


terminal

python -m jarvis_finance.cli.main update-equity-prices --dry-run --limit 1


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


todo

Commit und Push, falls Änderungen


from hermes_tools import terminal
script = r'''
from pathlib import Path
from jarvis_finance.market_data.catalog import OpenFigiLookupProvider, FmpSearchProvider, _runtime_secret_value
from jarvis_finance.market_data.prices import FmpEquityPriceProvider
from jarvis_finance.market_data.candidates import LookupUnavailable
import stat

def yes(v): return 'ja' if v else 'nein'
secret=Path.home()/'jarvis_runtime/finance-system/secrets/.env'
print('SECRETS_FILE_EXISTS', yes(secret.exists()))
print('SECRETS_FILE_MODE_RESTRICTIVE', yes(secret.exists() and (stat.S_IMODE(secret.stat().st_mode) & 0o077)==0))
print('OPENFIGI_KEY_PRESENT', yes(bool(_runtime_secret_value(('OPENFIGI_API_KEY','JARVIS_OPENFIGI_API_KEY')))))
print('FMP_KEY_PRESENT', yes(bool(_runtime_secret_value(('FMP_API_KEY','JARVIS_FMP_API_KEY')))))
try:
    rows=OpenFigiLookupProvider(timeout_seconds=15).search(query='US9229087690', asset_class='etf')
    print('OPENFIGI_REACHABLE ja')
    print('OPENFIGI_CANDIDATES_FOUND', yes(bool(rows)))
    print('OPENFIGI_ERROR_CATEGORY none' if rows else 'OPENFIGI_ERROR_CATEGORY no_results')
except LookupUnavailable as exc:
    msg=str(exc)
    category='auth_failed' if 'auth_failed' in msg else 'rate_limited' if 'rate_limited' in msg else 'network_error' if 'network_error' in msg else 'key_missing' if 'key_missing' in msg else 'provider_error'
    print('OPENFIGI_REACHABLE', 'ja' if category in {'auth_failed','rate_limited','provider_error'} else 'nein')
    print('OPENFIGI_CANDIDATES_FOUND nein')
    print('OPENFIGI_ERROR_CATEGORY', category)
try:
    fmp_rows=[]
    fmp_errors=[]
    for q in ['US9229087690','Vanguard Total Stock','VTI']:
        try:
            fmp_rows.extend(FmpSearchProvider(timeout_seconds=15).search(query=q, asset_class='etf'))
        except LookupUnavailable as exc:
            fmp_errors.append(str(exc))
    print('FMP_REACHABLE ja')
    print('FMP_CANDIDATES_FOUND', yes(bool(fmp_rows)))
    print('FMP_ERROR_CATEGORY', 'none' if fmp_rows else (fmp_errors[0] if fmp_errors else 'no_results'))
except Exception as exc:
    print('FMP_REACHABLE nein')
    print('FMP_CANDIDATES_FOUND nein')
    print('FMP_ERROR_CATEGORY provider_error')
quote=FmpEquityPriceProvider(timeout_seconds=15).get_price('VTI')
print('PRICE_PREVIEW_POSSIBLE', yes(bool(quote.close)))
print('PRICE_ERROR_CATEGORY', quote.error_message or 'none')
print('NO_POSITION_SAVED ja')
print('NO_PORTFOLIO_DATA_USED ja')
'''
from pathlib import Path
Path('/tmp/provider_smoke_real.py').write_text(script)
print(terminal('python /tmp/provider_smoke_real.py', timeout=180, workdir='/home/agent/.hermes/repos/FinanceManager')['output'])


Output:
SECRETS_FILE_EXISTS ja
SECRETS_FILE_MODE_RESTRICTIVE ja
OPENFIGI_KEY_PRESENT ja
FMP_KEY_PRESENT ja
OPENFIGI_REACHABLE ja
OPENFIGI_CANDIDATES_FOUND nein
OPENFIGI_ERROR_CATEGORY auth_failed
FMP_REACHABLE ja
FMP_CANDIDATES_FOUND ja
FMP_ERROR_CATEGORY none
PRICE_PREVIEW_POSSIBLE ja
PRICE_ERROR_CATEGORY none
NO_POSITION_SAVED ja
NO_PORTFOLIO_DATA_USED ja


terminal

python -m compileall -q src tests && pytest -q && python -m jarvis_finance.cli.main git-safety-scan . && git diff --check && git status --short


Output:
........................................................................ [ 25%]
........................................................................ [ 51%]
........................................................................ [ 76%]
..................................................................       [100%]
282 passed in 14.26s
.pytest_cache/CACHEDIR.TAG: runtime_cache_file_blocked
.pytest_cache/README.md: runtime_cache_file_blocked
.pytest_cache/.gitignore: runtime_cache_file_blocked
.pytest_cache/v/cache/nodeids: runtime_cache_file_blocked
.pytest_cache/v/cache/lastfailed: runtime_cache_file_blocked

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


python - <<'PY'
from pathlib import Path
import shutil
for p in [Path('.pytest_cache')]:
    if p.exists(): shutil.rmtree(p)
for base in [Path('src'), Path('tests')]:
    for p in sorted(base.rglob('__pycache__'), reverse=True):
        shutil.rmtree(p)
PY
python -m jarvis_finance.cli.main git-safety-scan . && git diff --check && git status --short


Output:
GIT_SAFETY_OK
 M src/jarvis_finance/dashboard/views/page_17_position_add.py
 M src/jarvis_finance/equity/manage.py
 M src/jarvis_finance/market_data/catalog.py
 M src/jarvis_finance/market_data/prices.py
 M tests/unit/test_equity_manage.py


Explanation:
**Analyzing cache removal and tests**

I’m considering whether I need to run full tests after removing the cache. It seems like I should compile the created pycache, but if I run the tests after ensuring safety, I’ll need to remove it afterward. I think I'll need to inspect the diff stat too, just to be safe and make sure there aren’t any secret strings left behind. I should search specifically for “API_KEY=” to check for those.


Explanation:
**Investigating split strings**

I noticed that the search didn’t return any results, possibly due to how the strings were split. That's interesting! I think I might need to explore using `git diff` in the terminal to see the differences. It seems like that might be more effective than using `sed`. I'll just dive in and see how it goes with the terminal for this! Let's keep experimenting until I find a solution.

git diff --stat && git diff -- src/jarvis_finance/market_data/catalog.py src/jarvis_finance/market_data/prices.py src/jarvis_finance/equity/manage.py src/jarvis_finance/dashboard/views/page_17_position_add.py tests/unit/test_equity_manage.py | head -n 260