ZhuLinsen/daily_stock_analysis · error · FutuPortfolioError

Futu 持仓数量无效{suffix}

Error message

Futu 持仓数量无效{suffix}

What it means

FutuPortfolioError raised in _load_position_codes (src/brokers/futu/portfolio.py:283) when the qty cell of a position row cannot be converted to float: the value is None/non-numeric (TypeError) or an unparseable string (ValueError). Booleans are explicitly rejected first (bool would otherwise pass float()). The offending code is appended when available.

Source

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

                    skipped_short_count += 1
                    continue
                if position_side != "LONG":
                    skipped_unknown_side_count += 1
                    continue
                raw_code = row.get("code")
                code = (
                    raw_code.strip().upper()
                    if isinstance(raw_code, str)
                    else ""
                )
                raw_quantity = row.get("qty")
                try:
                    if isinstance(raw_quantity, bool):
                        raise TypeError("boolean quantity")
                    quantity = float(raw_quantity)
                except (TypeError, ValueError) as exc:
                    suffix = f": {code}" if code else ""
                    raise FutuPortfolioError(f"Futu 持仓数量无效{suffix}") from exc
                if not math.isfinite(quantity):
                    suffix = f": {code}" if code else ""
                    raise FutuPortfolioError(f"Futu 持仓数量无效{suffix}")
                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)

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Align versions: run futu-api==10.8.6808 against a compatible OpenD build.
  2. Log the full row (row.to_dict()) when this fires to see the raw qty; if OpenD legitimately returned garbage, restart OpenD and retry.
  3. In test fixtures, always include a numeric qty per row.
  4. Retry the query once — transient partial payloads during cache refresh can produce this.

Example fix

# before (test stub)
pd.DataFrame([{'code': 'HK.00700'}])  # qty missing

# after
pd.DataFrame([{'code': 'HK.00700', 'qty': 100, 'position_side': 'LONG'}])
Defensive patterns

Strategy: validation

Validate before calling

def row_qty_is_numeric(row) -> bool:
    raw = row.get('qty')
    if raw is None or isinstance(raw, bool):
        return False
    try:
        float(raw)
        return True
    except (TypeError, ValueError):
        return False

Type guard

def is_numeric_qty(raw: object) -> bool:
    if isinstance(raw, bool) or raw is None:
        return False
    try:
        float(raw)
        return True
    except (TypeError, ValueError):
        return False

Prevention

When it happens

Trigger: position_list_query returns a row where qty is None/NaN-masked, a string like '--' or 'N/A', or True/False; typically from schema drift between OpenD and the pinned futu-api==10.8.6808, or from test mocks omitting qty.

Common situations: OpenD version mismatch changing the position payload; partially loaded position data during refresh_cache=True; stubbed DataFrames in tests that forget the qty column.

Related errors


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