ZhuLinsen/daily_stock_analysis · error · FutuPortfolioError

{operation}返回了非表格数据

Error message

{operation}返回了非表格数据

What it means

FutuPortfolioError raised by _iter_rows (src/brokers/futu/portfolio.py:100) when a value returned by the Futu SDK for an account or position query does not expose a callable pandas-style .iterrows() method. The code is written against the pinned futu-api==10.8.6808, which returns pandas DataFrames; getting anything else (None, a tuple, a plain dict, an error string) means the SDK response shape is unexpected.

Source

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

        TrdMarket=TrdMarket,
    )


def _enum_text(value: Any) -> str:
    """Normalize SDK enum-like values for stable comparisons."""

    if value is None:
        return ""
    name = getattr(value, "name", None)
    return str(name if name is not None else value).strip().upper()


def _iter_rows(data: Any, operation: str) -> Iterable[Any]:
    """Iterate the pandas-style table returned by the pinned Futu SDK."""

    iterrows = getattr(data, "iterrows", None)
    if not callable(iterrows):
        raise FutuPortfolioError(f"{operation}返回了非表格数据")
    return (row for _, row in iterrows())


def _safe_close(context: Any) -> None:
    """Close an SDK context without masking the primary operation result."""

    if context is None:
        return
    try:
        context.close()
    except Exception:  # pragma: no cover - closing is best effort
        logger.debug("关闭 Futu OpenD 连接失败", exc_info=True)


def _connection_settings() -> tuple[str, int]:
    """Return the validated IPv4 OpenD host and port from environment settings."""

    host = (os.getenv("FUTU_OPEND_HOST") or "127.0.0.1").strip()

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Confirm the installed version matches the pin: pip show futu-api (must be 10.8.6808); downgrade/upgrade to it if it drifted.
  2. Reproduce the exact call (context.get_acc_list()) standalone against OpenD and inspect type(data) to see what actually came back.
  3. If you mock the SDK in tests, return pandas DataFrames (pandas.DataFrame([...])) so the table contract holds.
  4. Check OpenD connectivity/health — a half-initialized connection can produce malformed payloads.

Example fix

# before (test stub) — triggers '返回了非表格数据'
mock_ctx.get_acc_list.return_value = (0, [{'acc_id': 1}])

# after — honor the DataFrame contract
import pandas as pd
mock_ctx.get_acc_list.return_value = (
    0, pd.DataFrame([{'acc_id': 1, 'trd_env': 'REAL', 'acc_status': 'ACTIVE', 'acc_role': 'NORMAL'}])
)
Defensive patterns

Strategy: type-guard

Validate before calling

def is_sdk_table(data) -> bool:
    return callable(getattr(data, 'iterrows', None))

Type guard

from typing import Any

def is_dataframe_like(data: Any) -> bool:
    """Matches futu-api==10.8.6808's pandas DataFrame return contract."""
    return callable(getattr(data, 'iterrows', None))

Try / catch

from src.brokers.futu.portfolio import FutuPortfolioError
try:
    rows = list(_iter_rows(data, op))
except FutuPortfolioError as exc:
    if '非表格数据' in str(exc):
        log.error('futu-api return contract broken; check version pin 10.8.6808')
    raise

Prevention

When it happens

Trigger: Calling get_acc_list() or position_list_query() and passing the 'data' part to _iter_rows when ret==RET_OK but data is not a DataFrame: happens after upgrading futu-api to a version with a changed return contract, when a mocked SDK returns tuples, or when an SDK-internal error yields a non-table payload with a success ret code.

Common situations: Unpinned futu-api upgrades (the code explicitly pins 10.8.6808); unit tests that stub the SDK with (ret, data) tuples instead of DataFrames; SDK degradation returning strings on success code.

Related errors


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