{"record":{"id":"664558b867c349ff","repo":"ZhuLinsen/daily_stock_analysis","slug":"futu-exc-664558","errorCode":null,"errorMessage":"查询 Futu 真实持仓失败: {exc}","messagePattern":"查询 Futu 真实持仓失败: (.+?)","errorType":"exception","errorClass":"FutuPortfolioError","httpStatus":null,"severity":"error","filePath":"src/brokers/futu/portfolio.py","lineNumber":305,"sourceCode":"                if quantity == 0:\n                    continue\n                if not isinstance(raw_code, str):\n                    raise FutuPortfolioError(\"Futu 非零持仓返回了无效证券代码\")\n                if not code:\n                    raise FutuPortfolioError(\"Futu 非零持仓返回了空证券代码\")\n                market, separator, symbol = code.partition(\".\")\n                if not separator or not market or not symbol:\n                    raise FutuPortfolioError(\n                        f\"Futu 非零持仓返回了无效证券代码: {code}\"\n                    )\n                if code in seen_codes:\n                    continue\n                seen_codes.add(code)\n                codes.append(code)\n        except FutuPortfolioError:\n            raise\n        except Exception as exc:  # noqa: BLE001 - translate SDK/network errors for CLI callers\n            raise FutuPortfolioError(f\"查询 Futu 真实持仓失败: {exc}\") from exc\n        finally:\n            _safe_close(context)\n\n    if skipped_short_count:\n        logger.info(\"已跳过 %d 个 Futu SHORT 空头持仓\", skipped_short_count)\n    if skipped_unknown_side_count:\n        logger.warning(\n            \"已跳过 %d 个持仓方向不是 LONG 的 Futu 持仓\",\n            skipped_unknown_side_count,\n        )\n    return codes\n\n\ndef _market_prefix(code: str) -> str:\n    \"\"\"Extract the Futu market prefix from a qualified security code.\"\"\"\n\n    return code.split(\".\", 1)[0] if \".\" in code else \"\"\n","sourceCodeStart":287,"sourceCodeEnd":323,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/brokers/futu/portfolio.py#L287-L323","documentation":"Catch-all translator inside the Futu position loader: any exception that is not already a FutuPortfolioError (SDK errors, network failures, attribute errors from unexpected response shapes) is re-raised as FutuPortfolioError with the original message and __cause__ preserved. This gives CLI callers one exception type to handle for the whole 'query real positions' step; the context is closed via _safe_close in finally.","triggerScenarios":"OpenQuoteContext construction or position_list raising futu.api errors (ret != RET_OK paths are separately handled, so this is for actual exceptions): connection refused to FutuOpenD, unlock-trade timeouts, JSON decode failures on malformed responses, AttributeError on unexpected row types.","commonSituations":"FutuOpenD not running or listening on a different port; trade password not unlocked; network interruption between the host and the gateway; futu-api internal errors.","solutions":["Read the embedded original message (exc) — it names the real cause; address that (start FutuOpenD, unlock trade, fix port).","Confirm FutuOpenD is running and reachable, and the account is a REAL account type as required by the loader.","Upgrade futu-api and FutuOpenD to matching versions if the inner error is a protocol/parse error.","Retry once after restoring the gateway; the loader itself does not retry."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"from src.brokers.futu.portfolio import FutuPortfolioError\n\ntry:\n    codes = load_futu_stock_codes()\nexcept FutuPortfolioError as exc:\n    cause = exc.__cause__\n    logger.error(\"Futu query failed: %s (root: %s)\", exc, cause)\n    return []  # or surface to user","preventionTips":["Ensure FutuOpenD is running and unlocked before analysis jobs","Keep gateway and SDK versions matched","Catch FutuPortfolioError at the orchestration layer — it wraps every failure mode of this loader"],"tags":["futu","broker","network","wrapper-exception"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}