{"record":{"id":"1e856db4218709bf","repo":"ZhuLinsen/daily_stock_analysis","slug":"operation","errorCode":null,"errorMessage":"{operation}返回了非表格数据","messagePattern":"(.+?)返回了非表格数据","errorType":"exception","errorClass":"FutuPortfolioError","httpStatus":null,"severity":"error","filePath":"src/brokers/futu/portfolio.py","lineNumber":100,"sourceCode":"        TrdMarket=TrdMarket,\n    )\n\n\ndef _enum_text(value: Any) -> str:\n    \"\"\"Normalize SDK enum-like values for stable comparisons.\"\"\"\n\n    if value is None:\n        return \"\"\n    name = getattr(value, \"name\", None)\n    return str(name if name is not None else value).strip().upper()\n\n\ndef _iter_rows(data: Any, operation: str) -> Iterable[Any]:\n    \"\"\"Iterate the pandas-style table returned by the pinned Futu SDK.\"\"\"\n\n    iterrows = getattr(data, \"iterrows\", None)\n    if not callable(iterrows):\n        raise FutuPortfolioError(f\"{operation}返回了非表格数据\")\n    return (row for _, row in iterrows())\n\n\ndef _safe_close(context: Any) -> None:\n    \"\"\"Close an SDK context without masking the primary operation result.\"\"\"\n\n    if context is None:\n        return\n    try:\n        context.close()\n    except Exception:  # pragma: no cover - closing is best effort\n        logger.debug(\"关闭 Futu OpenD 连接失败\", exc_info=True)\n\n\ndef _connection_settings() -> tuple[str, int]:\n    \"\"\"Return the validated IPv4 OpenD host and port from environment settings.\"\"\"\n\n    host = (os.getenv(\"FUTU_OPEND_HOST\") or \"127.0.0.1\").strip()","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/brokers/futu/portfolio.py#L82-L118","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# before (test stub) — triggers '返回了非表格数据'\nmock_ctx.get_acc_list.return_value = (0, [{'acc_id': 1}])\n\n# after — honor the DataFrame contract\nimport pandas as pd\nmock_ctx.get_acc_list.return_value = (\n    0, pd.DataFrame([{'acc_id': 1, 'trd_env': 'REAL', 'acc_status': 'ACTIVE', 'acc_role': 'NORMAL'}])\n)","handlingStrategy":"type-guard","validationCode":"def is_sdk_table(data) -> bool:\n    return callable(getattr(data, 'iterrows', None))","typeGuard":"from typing import Any\n\ndef is_dataframe_like(data: Any) -> bool:\n    \"\"\"Matches futu-api==10.8.6808's pandas DataFrame return contract.\"\"\"\n    return callable(getattr(data, 'iterrows', None))","tryCatchPattern":"from src.brokers.futu.portfolio import FutuPortfolioError\ntry:\n    rows = list(_iter_rows(data, op))\nexcept FutuPortfolioError as exc:\n    if '非表格数据' in str(exc):\n        log.error('futu-api return contract broken; check version pin 10.8.6808')\n    raise","preventionTips":["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."],"tags":["futu","sdk-contract","pandas","version-pin"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}