HKUDS/Vibe-Trading · warning · ValueError

at most 50 portfolio sources are allowed

Error message

at most 50 portfolio sources are allowed

What it means

Portfolio settings enforce a hard cap of 50 sources per configuration; len(raw_sources) > 50 raises immediately. The limit bounds fan-out of read-only portfolio aggregation per settings file.

Source

Thrown at agent/src/portfolio/config.py:154

            when omitted.

    Returns:
        Validated, order-normalized settings.

    Raises:
        ValueError: If the display currency, the source list, a connection id,
            a label, or a referenced profile's eligibility is invalid.
    """
    store = connection_store or ConnectionStore()
    currency = str(payload.get("display_currency") or "USD").strip().upper()
    if currency not in {"USD", "CNY"}:
        raise ValueError("display_currency must be USD or CNY")

    raw_sources = payload.get("sources")
    if not isinstance(raw_sources, list):
        raise ValueError("sources must be a list")
    if len(raw_sources) > 50:
        raise ValueError("at most 50 portfolio sources are allowed")

    seen_ids: set[str] = set()
    sources: list[PortfolioSource] = []
    for index, raw in enumerate(raw_sources):
        if not isinstance(raw, dict):
            raise ValueError("each portfolio source must be an object")
        connection_id = (
            str(raw.get("connection_id") or raw.get("id") or "").strip().lower()
        )
        if not _SOURCE_ID_RE.fullmatch(connection_id):
            raise ValueError(f"invalid portfolio connection id: {connection_id or '?'}")
        legacy_profile_id = str(raw.get("profile_id") or "").strip().lower()
        if legacy_profile_id:
            legacy_profile = profile_by_id(legacy_profile_id)
            connection = store.ensure(
                connection_id,
                legacy_profile.id,
                str(raw.get("label") or legacy_profile.label),

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Prune sources to the 50 you actually need (deduplicate by connection_id first)
  2. Split aggregation across multiple settings files/profiles if genuinely required
  3. Audit for stale entries from previous connections before adding new ones
Defensive patterns

Strategy: validation

Validate before calling

seen = set()
deduped = [s for s in payload['sources']
           if (id_ := (s.get('connection_id') or s.get('id'))) not in seen and not seen.add(id_)]
payload['sources'] = deduped[:50]
if len(deduped) > 50:
    log.warning('truncated sources to 50 (dropped %d)', len(deduped) - 50)

Type guard

def within_source_limit(payload) -> bool:
    return isinstance(payload.get('sources'), list) and len(payload['sources']) <= 50

Try / catch

try:
    settings = parse_settings(payload, store)
except ValueError as exc:
    if 'at most 50' in str(exc):
        payload['sources'] = dedupe_and_cap(payload['sources'], 50)
        settings = parse_settings(payload, store)

Prevention

When it happens

Trigger: A settings file with 51+ entries in sources, typically produced by merging multiple configs or programmatically enumerating every exchange connection.

Common situations: Concatenating settings files, scripts that add a source per account/subaccount, or copying a bulk test fixture into production config.

Related errors


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