{"record":{"id":"50012c51451bb6bf","repo":"ZhuLinsen/daily_stock_analysis","slug":"futu","errorCode":null,"errorMessage":"Futu 非零持仓返回了无效证券代码","messagePattern":"Futu 非零持仓返回了无效证券代码","errorType":"exception","errorClass":"FutuPortfolioError","httpStatus":null,"severity":"error","filePath":"src/brokers/futu/portfolio.py","lineNumber":290,"sourceCode":"                    raw_code.strip().upper()\n                    if isinstance(raw_code, str)\n                    else \"\"\n                )\n                raw_quantity = row.get(\"qty\")\n                try:\n                    if isinstance(raw_quantity, bool):\n                        raise TypeError(\"boolean quantity\")\n                    quantity = float(raw_quantity)\n                except (TypeError, ValueError) as exc:\n                    suffix = f\": {code}\" if code else \"\"\n                    raise FutuPortfolioError(f\"Futu 持仓数量无效{suffix}\") from exc\n                if not math.isfinite(quantity):\n                    suffix = f\": {code}\" if code else \"\"\n                    raise FutuPortfolioError(f\"Futu 持仓数量无效{suffix}\")\n                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","sourceCodeStart":272,"sourceCodeEnd":308,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/brokers/futu/portfolio.py#L272-L308","documentation":"Raised by FutuOpenD portfolio ingestion when a position row has a non-zero parsed quantity but the underlying code field is not a Python string (e.g. None, int, or a futu SDK enum object). The broker adapter treats a non-string code as a contract violation of the position list API (context.position_list) and refuses to silently coerce it, because guessing could analyze the wrong security. It is wrapped into FutuPortfolioError so CLI callers see one typed failure instead of a raw SDK TypeError.","triggerScenarios":"Calling the Futu portfolio loader (load_futu_stock_codes / the REAL account position scan in src/brokers/futu/portfolio.py) when futu API returns a position_list row whose 'code' key is missing (row.get defaults to None) or holds a non-string after qty parsing succeeded and quantity != 0. Typical with futu SDK versions that change the row schema or return qty for positions whose code field is dropped.","commonSituations":"FutuOpenD/otg gateway version mismatch with the installed futu-api package; mock/stub position fixtures used in tests that omit the code field; a futu account containing derivative or unsupported position types whose rows serialize code differently.","solutions":["Upgrade/pin futu-api to a version matching your FutuOpenD gateway so position_list rows always carry string 'code' fields.","Reproduce with a small script calling OpenQuoteContext.position_list(account_id) and inspect the row type for 'code' to confirm the schema drift.","If testing, fix the fixture to use real dict rows with string codes instead of objects/None.","If the account genuinely returns non-string codes, sanitize at the boundary before calling this loader and report the schema change upstream."],"exampleFix":"// before (test fixture / stub row)\nrow = {\"qty\": 100, \"code\": None}\n\n// after\nrow = {\"qty\": 100, \"code\": \"HK.00700\"}","handlingStrategy":"validation","validationCode":"def is_valid_futu_position_row(row) -> bool:\n    code = row.get(\"code\")\n    return isinstance(code, str) and bool(code)","typeGuard":"def is_futu_position_row(row: object) -> bool:\n    return isinstance(row, dict) and isinstance(row.get(\"code\"), str) and isinstance(row.get(\"qty\"), (int, float)) and not isinstance(row.get(\"qty\"), bool)","tryCatchPattern":"try:\n    codes = load_futu_stock_codes()\nexcept FutuPortfolioError as exc:\n    logger.error(\"Futu portfolio load failed: %s\", exc)\n    raise SystemExit(2) from exc","preventionTips":["Pin futu-api and FutuOpenD to matching versions","Use realistic dict fixtures with string codes in tests","Validate SDK row shapes at the adapter boundary before calling the loader"],"tags":["futu","broker","data-contract","type-error"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}