HKUDS/Vibe-Trading · error · ValueError

symbols must be a non-empty list of strings

Error message

symbols must be a non-empty list of strings

What it means

PortfolioRiskTool._run requires 'symbols' to be a non-empty JSON array whose entries are all non-blank strings; anything else (missing key, string, empty list, non-string or whitespace-only entries) raises this before any downstream processing.

Source

Thrown at agent/src/tools/portfolio_risk_tool.py:99

        # Injectable for tests; production uses the real fallback chain.
        self._fetch = data_fetcher or fetch_market_data

    def execute(self, **kwargs: Any) -> str:
        try:
            return self._run(**kwargs)
        except Exception as exc:  # noqa: BLE001 — tool must always return JSON
            logger.warning("portfolio_risk_xray failed: %s", exc)
            return json.dumps(
                {"status": "error", "error": str(exc)}, ensure_ascii=False, allow_nan=False
            )

    # ------------------------------------------------------------------
    def _run(self, **kwargs: Any) -> str:
        symbols = kwargs.get("symbols")
        if not isinstance(symbols, list) or not symbols or not all(
            isinstance(s, str) and s.strip() for s in symbols
        ):
            raise ValueError("symbols must be a non-empty list of strings")
        symbols = [s.strip() for s in symbols]
        if len(symbols) > _MAX_SYMBOLS:
            raise ValueError(f"too many symbols ({len(symbols)}); cap is {_MAX_SYMBOLS}")

        weights = self._parse_weights(kwargs.get("weights"), symbols)
        start_date, end_date = self._parse_dates(kwargs.get("start_date"), kwargs.get("end_date"))
        source = str(kwargs.get("source") or "auto")
        interval = str(kwargs.get("interval") or "1D")

        raw = self._fetch(
            codes=symbols,
            start_date=start_date,
            end_date=end_date,
            source=source,
            interval=interval,
        )
        closes = self._closes_frame(raw, symbols)
        unresolved = raw.get("_unresolved") if isinstance(raw, Mapping) else None

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Pass a list of trimmed tickers: ['AAPL', 'MSFT']
  2. Split-and-filter string input before calling
  3. Cap length to _MAX_SYMBOLS to avoid the next error

Example fix

# before
execute(symbols="AAPL,MSFT")
# after
execute(symbols=[s.strip() for s in "AAPL,MSFT".split(",") if s.strip()])
Defensive patterns

Strategy: type-guard

Validate before calling

symbols = symbols if isinstance(symbols, list) else (
    [s.strip() for s in str(symbols).split(",") if s.strip()] if symbols else []
)
if not symbols:
    raise ArgumentError("no symbols")

Type guard

def is_symbol_list(v: object) -> bool:
    return (
        isinstance(v, list) and bool(v)
        and all(isinstance(s, str) and s.strip() for s in v)
    )

Try / catch

try:
    out = tool.execute(symbols=symbols)
except ValueError as e:
    if "non-empty list of strings" in str(e):
        out = tool.execute(symbols=coerce_symbol_list(symbols))

Prevention

When it happens

Trigger: symbols='AAPL,MSFT' (string not list), symbols=[], symbols=['AAPL', ''], or symbols=['AAPL', 5].

Common situations: LLM serializing a comma-joined string, splitting that yields empty tokens, or reusing a ticker dict instead of list.

Related errors


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