HKUDS/Vibe-Trading · error · UsdMObservationError

duplicate {source} position symbol

Error message

duplicate {source} position symbol

What it means

Raised by _active_by_symbol when two non-zero position rows share the same uppercased symbol in the same source payload. In one-way mode each symbol must appear at most once among active positions, so a duplicate indicates a malformed or contradictory positionRisk (or, historically, hedge-mode rows for the same symbol). The connector refuses to guess which row is authoritative.

Source

Thrown at agent/src/trading/connectors/binance/usdm.py:304

        raise UsdMObservationError(f"Binance USD-M {name} payload is invalid")
    return value


def _require_one_way(rows: list[Mapping[str, Any]]) -> None:
    if any(str(row.get("positionSide") or "").upper() != "BOTH" for row in rows):
        raise UsdMObservationError("Binance USD-M Shadow Account requires one-way position mode")


def _active_by_symbol(rows: list[Mapping[str, Any]], source: str) -> dict[str, Mapping[str, Any]]:
    active: dict[str, Mapping[str, Any]] = {}
    for row in rows:
        quantity = _number(row.get("positionAmt"), "positionAmt")
        if quantity == 0:
            continue
        symbol = str(row.get("symbol") or "").upper()
        _canonical_symbol(symbol)
        if symbol in active:
            raise UsdMObservationError(f"duplicate {source} position symbol")
        active[symbol] = row
    return active


def _canonical_symbol(symbol: str) -> str:
    if not symbol.endswith("USDT") or len(symbol) <= 4 or not symbol.isalnum():
        raise UsdMObservationError("Binance USD-M supports canonical */USDT symbols only")
    return f"{symbol[:-4]}-USDT-PERP"


def _number(
    value: Any,
    name: str,
    *,
    non_negative: bool = False,
    positive: bool = False,
) -> float:
    try:

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Inspect the raw positionRisk payload for duplicate symbols with non-zero positionAmt
  2. If LONG/SHORT rows for the same symbol appear, resolve the hedge-mode positions (see error 1223) — the account is not in one-way mode
  3. Deduplicate or regenerate mock data so each symbol appears once
  4. Re-fetch a fresh snapshot; transient partial responses can self-correct on retry

Example fix

// before
[{"symbol":"BTCUSDT","positionAmt":"1"}, {"symbol":"BTCUSDT","positionAmt":"-1"}]
// after
[{"symbol":"BTCUSDT","positionAmt":"0"}]
Defensive patterns

Strategy: validation

Validate before calling

from collections import Counter

def no_duplicate_active_symbols(rows: list[dict]) -> bool:
    active = [str(r.get("symbol", "")).upper() for r in rows if float(r.get("positionAmt", 0)) != 0]
    return len(active) == len(set(active))

assert no_duplicate_active_symbols(rows)

Try / catch

try:
    obs = connector.read_account_observation(...)
except UsdMObservationError as exc:
    if "duplicate" in str(exc):
        rows = refetch_positions()  # fresh snapshot, retry once
    else:
        raise

Prevention

When it happens

Trigger: _join_positions processes a positionRisk list containing two rows with symbol 'BTCUSDT' and non-zero positionAmt; commonly happens when hedge-mode LONG and SHORT rows are both present after collapsing, or when a mock concatenates two snapshots.

Common situations: Account switched modes leaving stale duplicate rows; test data duplicating symbols; retry logic appending instead of replacing the previous payload; exchange returning merged cross-margin and isolated rows for the same symbol in a shape the connector doesn't dedupe.

Related errors


AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28). Data as JSON: /api/errors/88e03154f2cab544. Report an issue: GitHub.