HKUDS/Vibe-Trading · error · ValueError

sources must be a list

Error message

sources must be a list

What it means

Portfolio settings require the 'sources' key to be a JSON array; any other type (object, string, null, number) is rejected at parse time before per-source validation runs. An absent sources key yields None and also fails.

Source

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

        payload: Raw settings dict from the Web UI, CLI or a settings file.
        connection_store: Local connection registry; a default store is used
            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,

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Make sources a JSON list of source objects: "sources": [ {...}, ... ]
  2. If empty, use "sources": [] rather than removing the key or using {}
  3. After external edits, validate shape with json before calling load()

Example fix

# before
{"sources": {"conn1": {"label": "Main"}}}
# after
{"sources": [{"connection_id": "conn1", "label": "Main"}]}
Defensive patterns

Strategy: type-guard

Validate before calling

sources = payload.get('sources')
if not isinstance(sources, list):
    if isinstance(sources, dict):
        payload['sources'] = [{'connection_id': k, **v} for k, v in sources.items()]
    else:
        payload['sources'] = []

Type guard

def has_sources_list(payload) -> bool:
    return isinstance(payload.get('sources'), list)

Try / catch

try:
    settings = parse_settings(payload, store)
except ValueError as exc:
    if 'sources must be a list' in str(exc):
        payload['sources'] = normalize_sources(payload['sources'])
        settings = parse_settings(payload, store)

Prevention

When it happens

Trigger: parse_settings({'sources': {...}}), {'sources': null}, or omitting 'sources' entirely; loading a settings file where sources was reshaped into an object keyed by connection id.

Common situations: Hand-editing or generating the settings file with the wrong shape, schema drift after a config format migration, or YAML-to-JSON conversion quirks that turn a list into a map.

Related errors


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