{"record":{"id":"5e332e97205b4cfb","repo":"ZhuLinsen/daily_stock_analysis","slug":"futu-suffix","errorCode":null,"errorMessage":"Futu 持仓数量无效{suffix}","messagePattern":"Futu 持仓数量无效(.+?)","errorType":"exception","errorClass":"FutuPortfolioError","httpStatus":null,"severity":"error","filePath":"src/brokers/futu/portfolio.py","lineNumber":283,"sourceCode":"                    skipped_short_count += 1\n                    continue\n                if position_side != \"LONG\":\n                    skipped_unknown_side_count += 1\n                    continue\n                raw_code = row.get(\"code\")\n                code = (\n                    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)","sourceCodeStart":265,"sourceCodeEnd":301,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/brokers/futu/portfolio.py#L265-L301","documentation":"FutuPortfolioError raised in _load_position_codes (src/brokers/futu/portfolio.py:283) when the qty cell of a position row cannot be converted to float: the value is None/non-numeric (TypeError) or an unparseable string (ValueError). Booleans are explicitly rejected first (bool would otherwise pass float()). The offending code is appended when available.","triggerScenarios":"position_list_query returns a row where qty is None/NaN-masked, a string like '--' or 'N/A', or True/False; typically from schema drift between OpenD and the pinned futu-api==10.8.6808, or from test mocks omitting qty.","commonSituations":"OpenD version mismatch changing the position payload; partially loaded position data during refresh_cache=True; stubbed DataFrames in tests that forget the qty column.","solutions":["Align versions: run futu-api==10.8.6808 against a compatible OpenD build.","Log the full row (row.to_dict()) when this fires to see the raw qty; if OpenD legitimately returned garbage, restart OpenD and retry.","In test fixtures, always include a numeric qty per row.","Retry the query once — transient partial payloads during cache refresh can produce this."],"exampleFix":"# before (test stub)\npd.DataFrame([{'code': 'HK.00700'}])  # qty missing\n\n# after\npd.DataFrame([{'code': 'HK.00700', 'qty': 100, 'position_side': 'LONG'}])","handlingStrategy":"validation","validationCode":"def row_qty_is_numeric(row) -> bool:\n    raw = row.get('qty')\n    if raw is None or isinstance(raw, bool):\n        return False\n    try:\n        float(raw)\n        return True\n    except (TypeError, ValueError):\n        return False","typeGuard":"def is_numeric_qty(raw: object) -> bool:\n    if isinstance(raw, bool) or raw is None:\n        return False\n    try:\n        float(raw)\n        return True\n    except (TypeError, ValueError):\n        return False","tryCatchPattern":null,"preventionTips":["Always provide a numeric qty in position fixtures and mocks.","Keep futu-api==10.8.6808 paired with a compatible OpenD.","Log the full offending row when this fires to diagnose payload drift."],"tags":["futu","sdk-contract","positions","data-integrity"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}