virattt/ai-hedge-fund · error · ValueError

universe is empty — a run needs at least one ticker

Error message

universe is empty — a run needs at least one ticker

What it means

Raised by normalize_universe (hedge_fund/fund/spec.py:156) — the single normalizer for every entry point (CLI flag, TUI input, API) — when the ticker list is empty after trimming, upper-casing, and de-duping. A cycle with nothing to trade is defined as a caller mistake: the library raises instead of returning an empty result.

Source

Thrown at hedge_fund/fund/spec.py:156

        if duplicates:
            raise ValueError(f"duplicate strategy names: {sorted(duplicates)}")
        return strategies


def normalize_universe(tickers: list[str]) -> list[str]:
    """Clean a run's ticker list: upper-cased, de-duped, order preserved.

    The single normalizer for every entry point (CLI flag, TUI input, a future
    API), so what the engine trades can't drift by caller. Empty raises: a
    cycle with nothing to trade is a caller mistake, not an empty result.
    """
    universe: list[str] = []
    for ticker in tickers:
        upper = ticker.strip().upper()
        if upper and upper not in universe:
            universe.append(upper)
    if not universe:
        raise ValueError("universe is empty — a run needs at least one ticker")
    return universe


def load_spec(path: str | Path) -> FundSpec:
    """Load a mandate from YAML. Validation errors carry the pydantic detail."""
    with open(path) as f:
        data = yaml.safe_load(f)
    # Mandates used to carry a `universe`. Tickers are a run-time input now
    # (see FundSpec), so drop the legacy key rather than fail extra='forbid'
    # on funds saved by an older build.
    data.pop("universe", None)
    return FundSpec(**data)


def load_strategy(path: str | Path) -> StrategySpec:
    """Load one strategy (a library file under hedge_fund/strategies/) from YAML."""
    with open(path) as f:
        data = yaml.safe_load(f)

View on GitHub (pinned to eff8a7320f)

Solutions

  1. Check the ticker list before invoking the run: if not tickers or all entries are blank, exit with a usage message instead of calling the engine.
  2. Fix the upstream producer: make the CLI flag required (nargs='+'), guard the TUI submit handler on non-empty input.
  3. Strip/split carefully: [t.strip().upper() for t in raw.split(',') if t.strip()] so empty comma segments don't create phantom entries.

Example fix

# before
result = run_backtest(fund, client, start, end, universe=arg_universe)  # arg_universe == [] -> raises

# after
if not arg_universe:
    raise SystemExit("pass at least one ticker, e.g. --universe AAPL MSFT")
result = run_backtest(fund, client, start, end, universe=arg_universe)
Defensive patterns

Strategy: validation

Validate before calling

def clean_universe_arg(raw: str | list[str]) -> list[str]:
    """Parse/normalize before the engine sees it; None/empty -> explicit exit."""
    if isinstance(raw, str):
        items = [t.strip() for t in raw.split(",")]
    else:
        items = [t.strip() for t in raw]
    tickers = [t.upper() for t in items if t]
    if not tickers:
        raise SystemExit("--universe needs at least one ticker, e.g. AAPL,MSFT")
    return tickers

Type guard

def is_non_empty_universe(u: object) -> bool:
    return (
        isinstance(u, (list, tuple))
        and len(u) > 0
        and all(isinstance(t, str) and t.strip() for t in u)
    )

Try / catch

try:
    result = run_backtest(fund, client, start, end, universe)
except ValueError as e:
    if "universe is empty" in str(e):
        raise SystemExit("pass at least one ticker") from e
    raise

Prevention

When it happens

Trigger: Passing [] as the universe argument to run_cycle or the backtest runner; passing [' ', ''] (only whitespace entries — all filtered out); a CLI invocation whose --universe flag ends up empty after parsing; the TUI submitting an empty ticker input box. Note the FundSpec itself no longer carries a universe (load_spec pops the legacy key), so this is purely a run-time input.

Common situations: Script iterates a filtered ticker list and the filter removes everything; CLI arg parsing bug dropping the universe values; TUI submitted before the user typed tickers; empty string in a config fed through split(',') producing [''].

Related errors


AI-assisted analysis of virattt/ai-hedge-fund@eff8a7320f (2026-08-15). Data as JSON: /api/errors/f2f812e48bd03afa. Report an issue: GitHub.