ZhuLinsen/daily_stock_analysis · error · FutuPortfolioError
Futu 非零持仓返回了无效证券代码
Error message
Futu 非零持仓返回了无效证券代码
What it means
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.
Source
Thrown at src/brokers/futu/portfolio.py:290
raw_code.strip().upper()
if isinstance(raw_code, str)
else ""
)
raw_quantity = row.get("qty")
try:
if isinstance(raw_quantity, bool):
raise TypeError("boolean quantity")
quantity = float(raw_quantity)
except (TypeError, ValueError) as exc:
suffix = f": {code}" if code else ""
raise FutuPortfolioError(f"Futu 持仓数量无效{suffix}") from exc
if not math.isfinite(quantity):
suffix = f": {code}" if code else ""
raise FutuPortfolioError(f"Futu 持仓数量无效{suffix}")
if quantity == 0:
continue
if not isinstance(raw_code, str):
raise FutuPortfolioError("Futu 非零持仓返回了无效证券代码")
if not code:
raise FutuPortfolioError("Futu 非零持仓返回了空证券代码")
market, separator, symbol = code.partition(".")
if not separator or not market or not symbol:
raise FutuPortfolioError(
f"Futu 非零持仓返回了无效证券代码: {code}"
)
if code in seen_codes:
continue
seen_codes.add(code)
codes.append(code)
except FutuPortfolioError:
raise
except Exception as exc: # noqa: BLE001 - translate SDK/network errors for CLI callers
raise FutuPortfolioError(f"查询 Futu 真实持仓失败: {exc}") from exc
finally:
_safe_close(context)
View on GitHub (pinned to 5159bd72e8)
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.
Example fix
// before (test fixture / stub row)
row = {"qty": 100, "code": None}
// after
row = {"qty": 100, "code": "HK.00700"} Defensive patterns
Strategy: validation
Validate before calling
def is_valid_futu_position_row(row) -> bool:
code = row.get("code")
return isinstance(code, str) and bool(code) Type guard
def is_futu_position_row(row: object) -> bool:
return isinstance(row, dict) and isinstance(row.get("code"), str) and isinstance(row.get("qty"), (int, float)) and not isinstance(row.get("qty"), bool) Try / catch
try:
codes = load_futu_stock_codes()
except FutuPortfolioError as exc:
logger.error("Futu portfolio load failed: %s", exc)
raise SystemExit(2) from exc Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Futu 非零持仓返回了空证券代码
- Futu 非零持仓返回了无效证券代码: {code}
- 无法确认证券类型的 Futu 持仓: {codes}
- 查询 Futu 真实持仓失败: {exc}
- 查询 Futu 持仓证券类型失败({prefix}): {data}
AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15).
Data as JSON: /api/errors/50012c51451bb6bf.
Report an issue: GitHub.