{"record":{"id":"b6b891e0c2a8ccf3","repo":"ZhuLinsen/daily_stock_analysis","slug":"futu-b6b891","errorCode":null,"errorMessage":"Futu 非零持仓返回了空证券代码","messagePattern":"Futu 非零持仓返回了空证券代码","errorType":"exception","errorClass":"FutuPortfolioError","httpStatus":null,"severity":"error","filePath":"src/brokers/futu/portfolio.py","lineNumber":292,"sourceCode":"                    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\n    if skipped_short_count:\n        logger.info(\"已跳过 %d 个 Futu SHORT 空头持仓\", skipped_short_count)","sourceCodeStart":274,"sourceCodeEnd":310,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/brokers/futu/portfolio.py#L274-L310","documentation":"Raised when a Futu position row has a non-zero quantity and the code field IS a string, but it is empty (or whitespace-only before later normalization). The loader deliberately fails instead of skipping, because a non-zero position with no code means real money is held in something the analysis cannot identify — silently dropping it would produce a misleading portfolio report.","triggerScenarios":"position_list returns a row where raw_code is a str but falsy ('' — note .strip()/.upper() happen later, so a whitespace-only string would pass this check and fail at the partition check instead). Happens when the futu SDK returns placeholder rows for suspended/delisted symbols or partial API responses.","commonSituations":"Positions in symbols being delisted or merged; futu API returning empty code for positions pending settlement; stale OpenD cache after account changes.","solutions":["Check the account in Futu OpenD console for suspended/delisted positions and close or exclude them.","Restart FutuOpenD to refresh the position cache and retry the loader.","Verify futu-api and OpenD versions match; upgrade both.","If the empty-code position is not actionable, exclude the account via FUTU_ACC_ID or filter the account selection."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"code = row.get(\"code\")\nif not (isinstance(code, str) and code.strip()):\n    raise ValueError(\"position row missing code — check Futu OpenD position cache\")","typeGuard":"def has_nonempty_code(row: dict) -> bool:\n    c = row.get(\"code\")\n    return isinstance(c, str) and c.strip() != \"\"","tryCatchPattern":"try:\n    codes = load_futu_stock_codes()\nexcept FutuPortfolioError as exc:\n    if \"空证券代码\" in str(exc):\n        logger.warning(\"Empty code in Futu positions — restart FutuOpenD and retry\")\n    raise","preventionTips":["Restart FutuOpenD after account changes to refresh caches","Audit accounts for suspended/delisted positions before automation runs","Do not feed stub rows into the loader"],"tags":["futu","broker","data-contract","validation"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}