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
- Confirm the installed version matches the pin: pip show futu-api (must be 10.8.6808); downgrade/upgrade to it if it drifted.
- Reproduce the exact call (context.get_acc_list()) standalone against OpenD and inspect type(data) to see what actually came back.
- If you mock the SDK in tests, return pandas DataFrames (pandas.DataFrame([...])) so the table contract holds.
- 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
- Pin futu-api==10.8.6808 everywhere the Futu source runs.
- When mocking the SDK, return pandas DataFrames, not tuples or dicts.
- Smoke-test the Futu path after any dependency upgrade.
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
- Futu 账户查询返回了无效账户 ID
- Futu 持仓数量无效{suffix}
- 未安装 Futu OpenAPI SDK;请先执行 `pip install "futu-api==10.8.6808"
- 加载 Futu OpenAPI SDK 失败: {exc}
- FUTU_OPEND_PORT 不是有效端口: {raw_port!r}
AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15).
Data as JSON: /api/errors/1e856db4218709bf.
Report an issue: GitHub.