ZhuLinsen/daily_stock_analysis · error · FutuPortfolioError

查询 Futu 真实持仓失败: {exc}

Error message

查询 Futu 真实持仓失败: {exc}

What it means

Catch-all translator inside the Futu position loader: any exception that is not already a FutuPortfolioError (SDK errors, network failures, attribute errors from unexpected response shapes) is re-raised as FutuPortfolioError with the original message and __cause__ preserved. This gives CLI callers one exception type to handle for the whole 'query real positions' step; the context is closed via _safe_close in finally.

Source

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

                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)
    if skipped_unknown_side_count:
        logger.warning(
            "已跳过 %d 个持仓方向不是 LONG 的 Futu 持仓",
            skipped_unknown_side_count,
        )
    return codes


def _market_prefix(code: str) -> str:
    """Extract the Futu market prefix from a qualified security code."""

    return code.split(".", 1)[0] if "." in code else ""

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Read the embedded original message (exc) — it names the real cause; address that (start FutuOpenD, unlock trade, fix port).
  2. Confirm FutuOpenD is running and reachable, and the account is a REAL account type as required by the loader.
  3. Upgrade futu-api and FutuOpenD to matching versions if the inner error is a protocol/parse error.
  4. Retry once after restoring the gateway; the loader itself does not retry.
Defensive patterns

Strategy: try-catch

Try / catch

from src.brokers.futu.portfolio import FutuPortfolioError

try:
    codes = load_futu_stock_codes()
except FutuPortfolioError as exc:
    cause = exc.__cause__
    logger.error("Futu query failed: %s (root: %s)", exc, cause)
    return []  # or surface to user

Prevention

When it happens

Trigger: OpenQuoteContext construction or position_list raising futu.api errors (ret != RET_OK paths are separately handled, so this is for actual exceptions): connection refused to FutuOpenD, unlock-trade timeouts, JSON decode failures on malformed responses, AttributeError on unexpected row types.

Common situations: FutuOpenD not running or listening on a different port; trade password not unlocked; network interruption between the host and the gateway; futu-api internal errors.

Related errors


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