ZhuLinsen/daily_stock_analysis · error · FutuPortfolioError

Futu 非零持仓返回了空证券代码

Error message

Futu 非零持仓返回了空证券代码

What it means

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.

Source

Thrown at src/brokers/futu/portfolio.py:292

                    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)

    if skipped_short_count:
        logger.info("已跳过 %d 个 Futu SHORT 空头持仓", skipped_short_count)

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Check the account in Futu OpenD console for suspended/delisted positions and close or exclude them.
  2. Restart FutuOpenD to refresh the position cache and retry the loader.
  3. Verify futu-api and OpenD versions match; upgrade both.
  4. If the empty-code position is not actionable, exclude the account via FUTU_ACC_ID or filter the account selection.
Defensive patterns

Strategy: validation

Validate before calling

code = row.get("code")
if not (isinstance(code, str) and code.strip()):
    raise ValueError("position row missing code — check Futu OpenD position cache")

Type guard

def has_nonempty_code(row: dict) -> bool:
    c = row.get("code")
    return isinstance(c, str) and c.strip() != ""

Try / catch

try:
    codes = load_futu_stock_codes()
except FutuPortfolioError as exc:
    if "空证券代码" in str(exc):
        logger.warning("Empty code in Futu positions — restart FutuOpenD and retry")
    raise

Prevention

When it happens

Trigger: 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.

Common situations: Positions in symbols being delisted or merged; futu API returning empty code for positions pending settlement; stale OpenD cache after account changes.

Related errors


AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15). Data as JSON: /api/errors/b6b891e0c2a8ccf3. Report an issue: GitHub.