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 NoneView on GitHub (pinned to 80ffdda44c)
Solutions
- Pass a list of trimmed tickers: ['AAPL', 'MSFT']
- Split-and-filter string input before calling
- 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
- Always send JSON arrays of tickers
- Trim and drop empty tokens when splitting
- Keep tickers as strings, never numeric tickers
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
- too many symbols ({len(symbols)}); cap is {_MAX_SYMBOLS}
- weights must be an object mapping symbol → number
- weights missing basket symbols: {sorted(missing)}
- delay requires n >= 1 (lookahead ban)
- delay requires n >= 1 (lookahead ban)
AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28).
Data as JSON: /api/errors/51a5dea80e487f42.
Report an issue: GitHub.