{"record":{"id":"f2f812e48bd03afa","repo":"virattt/ai-hedge-fund","slug":"universe-is-empty-a-run-needs-at-least-one-ticke","errorCode":null,"errorMessage":"universe is empty — a run needs at least one ticker","messagePattern":"universe is empty — a run needs at least one ticker","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"hedge_fund/fund/spec.py","lineNumber":156,"sourceCode":"        if duplicates:\n            raise ValueError(f\"duplicate strategy names: {sorted(duplicates)}\")\n        return strategies\n\n\ndef normalize_universe(tickers: list[str]) -> list[str]:\n    \"\"\"Clean a run's ticker list: upper-cased, de-duped, order preserved.\n\n    The single normalizer for every entry point (CLI flag, TUI input, a future\n    API), so what the engine trades can't drift by caller. Empty raises: a\n    cycle with nothing to trade is a caller mistake, not an empty result.\n    \"\"\"\n    universe: list[str] = []\n    for ticker in tickers:\n        upper = ticker.strip().upper()\n        if upper and upper not in universe:\n            universe.append(upper)\n    if not universe:\n        raise ValueError(\"universe is empty — a run needs at least one ticker\")\n    return universe\n\n\ndef load_spec(path: str | Path) -> FundSpec:\n    \"\"\"Load a mandate from YAML. Validation errors carry the pydantic detail.\"\"\"\n    with open(path) as f:\n        data = yaml.safe_load(f)\n    # Mandates used to carry a `universe`. Tickers are a run-time input now\n    # (see FundSpec), so drop the legacy key rather than fail extra='forbid'\n    # on funds saved by an older build.\n    data.pop(\"universe\", None)\n    return FundSpec(**data)\n\n\ndef load_strategy(path: str | Path) -> StrategySpec:\n    \"\"\"Load one strategy (a library file under hedge_fund/strategies/) from YAML.\"\"\"\n    with open(path) as f:\n        data = yaml.safe_load(f)","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/virattt/ai-hedge-fund/blob/eff8a7320fcf0b473b135690fa1a5b0d9b022a83/hedge_fund/fund/spec.py#L138-L174","documentation":"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.","triggerScenarios":"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.","commonSituations":"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 [''].","solutions":["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.","Fix the upstream producer: make the CLI flag required (nargs='+'), guard the TUI submit handler on non-empty input.","Strip/split carefully: [t.strip().upper() for t in raw.split(',') if t.strip()] so empty comma segments don't create phantom entries."],"exampleFix":"# before\nresult = run_backtest(fund, client, start, end, universe=arg_universe)  # arg_universe == [] -> raises\n\n# after\nif not arg_universe:\n    raise SystemExit(\"pass at least one ticker, e.g. --universe AAPL MSFT\")\nresult = run_backtest(fund, client, start, end, universe=arg_universe)","handlingStrategy":"validation","validationCode":"def clean_universe_arg(raw: str | list[str]) -> list[str]:\n    \"\"\"Parse/normalize before the engine sees it; None/empty -> explicit exit.\"\"\"\n    if isinstance(raw, str):\n        items = [t.strip() for t in raw.split(\",\")]\n    else:\n        items = [t.strip() for t in raw]\n    tickers = [t.upper() for t in items if t]\n    if not tickers:\n        raise SystemExit(\"--universe needs at least one ticker, e.g. AAPL,MSFT\")\n    return tickers","typeGuard":"def is_non_empty_universe(u: object) -> bool:\n    return (\n        isinstance(u, (list, tuple))\n        and len(u) > 0\n        and all(isinstance(t, str) and t.strip() for t in u)\n    )","tryCatchPattern":"try:\n    result = run_backtest(fund, client, start, end, universe)\nexcept ValueError as e:\n    if \"universe is empty\" in str(e):\n        raise SystemExit(\"pass at least one ticker\") from e\n    raise","preventionTips":["Make the universe CLI flag required (argparse nargs='+'), so argparse rejects empty input before the engine does.","Guard TUI submit handlers on non-blank input.","Split comma lists with a filter for empty segments: [t for t in raw.split(',') if t.strip()]."],"tags":["validation","universe","input-validation","cli"],"backgroundTag":null,"analyzedSha":"eff8a7320fcf0b473b135690fa1a5b0d9b022a83","analyzedAt":"2026-08-15T00:22:46.567Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}