{"record":{"id":"ebde8f9cc0e96e8a","repo":"ZhuLinsen/daily_stock_analysis","slug":"futu-data-ebde8f","errorCode":null,"errorMessage":"查询 Futu 真实持仓失败: {data}","messagePattern":"查询 Futu 真实持仓失败: (.+?)","errorType":"exception","errorClass":"FutuPortfolioError","httpStatus":null,"severity":"error","filePath":"src/brokers/futu/portfolio.py","lineNumber":261,"sourceCode":"    skipped_short_count = 0\n    skipped_unknown_side_count = 0\n\n    for account in accounts:\n        context = None\n        try:\n            context = api.OpenSecTradeContext(\n                host=host,\n                port=port,\n                filter_trdmarket=api.TrdMarket.NONE,\n                security_firm=account.security_firm,\n            )\n            ret, data = context.position_list_query(\n                trd_env=api.TrdEnv.REAL,\n                acc_id=account.acc_id,\n                refresh_cache=True,\n            )\n            if ret != api.RET_OK:\n                raise FutuPortfolioError(f\"查询 Futu 真实持仓失败: {data}\")\n            for row in _iter_rows(data, \"Futu 持仓查询\"):\n                position_side = _enum_text(row.get(\"position_side\"))\n                if position_side == \"SHORT\":\n                    skipped_short_count += 1\n                    continue\n                if position_side != \"LONG\":\n                    skipped_unknown_side_count += 1\n                    continue\n                raw_code = row.get(\"code\")\n                code = (\n                    raw_code.strip().upper()\n                    if isinstance(raw_code, str)\n                    else \"\"\n                )\n                raw_quantity = row.get(\"qty\")\n                try:\n                    if isinstance(raw_quantity, bool):\n                        raise TypeError(\"boolean quantity\")","sourceCodeStart":243,"sourceCodeEnd":279,"githubUrl":"https://github.com/ZhuLinsen/daily_stock_analysis/blob/5159bd72e8373d215492dff122acc9d389e219c9/src/brokers/futu/portfolio.py#L243-L279","documentation":"FutuPortfolioError raised in _load_position_codes (src/brokers/futu/portfolio.py:261) when position_list_query(trd_env=REAL, acc_id=..., refresh_cache=True) for a selected account returns ret != RET_OK. The SDK's error string is embedded. refresh_cache=True forces a fresh OpenD fetch, so stale-cache masking is deliberately avoided.","triggerScenarios":"The account loses trading permission for the queried market; OpenD session interrupted between account discovery and position query; the security_firm used to open the context doesn't match the account; market closed with data unavailable; rate limiting from rapid refresh_cache=True calls.","commonSituations":"Remote OpenD over unstable links; iterating many accounts rapidly and tripping OpenD throttles; firm mismatch after discovery picked a returned_firm that fails for positions; OpenD reconnect mid-run.","solutions":["Read the embedded SDK error string — it distinguishes permission/connect/throttle causes.","Retry after confirming OpenD is still connected and logged in; transient errors are common with refresh_cache=True.","Ensure the security_firm passed to OpenSecTradeContext matches the account (discovery already maps returned firms — check logs if it fell back).","Slow down loops over multiple accounts or add small backoff to avoid OpenD rate limits."],"exampleFix":"# before\nret, data = ctx.position_list_query(trd_env=TrdEnv.REAL,\n                                    acc_id=acc_id, refresh_cache=True)\n\n# after — honor (ret, data) and retry once on failure\nret, data = ctx.position_list_query(trd_env=TrdEnv.REAL,\n                                    acc_id=acc_id, refresh_cache=True)\nif ret != RET_OK:\n    time.sleep(1)\n    ret, data = ctx.position_list_query(trd_env=TrdEnv.REAL,\n                                        acc_id=acc_id, refresh_cache=True)\nif ret != RET_OK:\n    raise RuntimeError(f\"position_list_query failed: {data}\")","handlingStrategy":"retry","validationCode":"def can_query_positions(ctx, acc_id) -> bool:\n    ret, _ = ctx.position_list_query(trd_env=ctx.get_trd_env(), acc_id=acc_id, refresh_cache=False)\n    return ret == 0","typeGuard":null,"tryCatchPattern":"from src.brokers.futu.portfolio import FutuPortfolioError\nimport time\n\nfor attempt in range(2):\n    try:\n        codes = load_position_codes(api, host, port, accounts)\n        break\n    except FutuPortfolioError as exc:\n        if '查询 Futu 真实持仓失败' in str(exc) and attempt == 0:\n            time.sleep(1)\n            continue\n        raise","preventionTips":["Treat embedded SDK error strings as the diagnosis source (permission vs connection vs throttle).","Add backoff when iterating position queries across many accounts (refresh_cache=True is expensive).","Keep OpenD sessions stable during batch runs; reconnect before retrying after drops."],"tags":["futu","opend","positions","network"],"backgroundTag":null,"analyzedSha":"5159bd72e8373d215492dff122acc9d389e219c9","analyzedAt":"2026-08-15T01:59:36.292Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}