{"record":{"id":"bf88e506d1401327","repo":"virattt/ai-hedge-fund","slug":"duplicate-strategy-names-sorted-duplicates","errorCode":null,"errorMessage":"duplicate strategy names: {sorted(duplicates)}","messagePattern":"duplicate strategy names: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"hedge_fund/fund/spec.py","lineNumber":139,"sourceCode":"    )\n    benchmark: str = Field(\n        default=\"SPY\",\n        description=\"what the fund measures itself against; also the source \"\n        \"of the backtest's trading-day grid\",\n    )\n\n    @field_validator(\"benchmark\")\n    @classmethod\n    def _uppercase_benchmark(cls, ticker: str) -> str:\n        return ticker.upper()\n\n    @field_validator(\"strategies\")\n    @classmethod\n    def _unique_strategy_names(cls, strategies: list[StrategySpec]) -> list[StrategySpec]:\n        names = [s.name for s in strategies]\n        duplicates = {n for n in names if names.count(n) > 1}\n        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","sourceCodeStart":121,"sourceCodeEnd":157,"githubUrl":"https://github.com/virattt/ai-hedge-fund/blob/eff8a7320fcf0b473b135690fa1a5b0d9b022a83/hedge_fund/fund/spec.py#L121-L157","documentation":"Raised by the _unique_strategy_names field validator on FundSpec (hedge_fund/fund/spec.py:139) when two or more strategies in the mandate share the same name. Strategy names are the join key between the FundSpec and the instantiated models (Fund wires models by models[strategy.name]), so duplicates would make that lookup ambiguous — hence the spec refuses to construct.","triggerScenarios":"Creating FundSpec(strategies=[...]) where two StrategySpec entries have name='value' (copy-paste of a YAML block without renaming); loading a YAML mandate with duplicated strategy keys after manual editing. Pydantic runs this during model validation, so the error surfaces at FundSpec(**data) / load_spec time, before any backtest work starts.","commonSituations":"Copy-pasting a strategy block in the mandate YAML and forgetting to change the name; YAML anchors/merge keys accidentally producing two identically-named entries; merging two mandate files that each define a 'momentum' strategy.","solutions":["Rename the duplicate strategies in the mandate YAML so every name is unique (e.g. 'value_core' and 'value_tilt').","If you meant the strategies to be identical, delete the duplicate instead of renaming it.","Check for YAML merge-key accidents: an anchor like <<: *value-strategy combined with an explicit name that collides."],"exampleFix":"# before (mandate.yaml)\nstrategies:\n  - name: momentum\n    weight: 0.6\n    ...\n  - name: momentum      # duplicate -> ValueError: duplicate strategy names: ['momentum']\n    weight: 0.4\n\n# after\nstrategies:\n  - name: momentum-core\n    weight: 0.6\n  - name: momentum-tilt\n    weight: 0.4","handlingStrategy":"validation","validationCode":"def validate_mandate_spec(spec) -> list[str]:\n    names = [s.name for s in spec.strategies]\n    seen, dupes = set(), set()\n    for n in names:\n        (dupes if n in seen else seen).add(n)\n    return sorted(dupes)  # empty list == ok (FundSpec enforces this too)","typeGuard":"from hedge_fund.fund.spec import FundSpec\n\ndef has_unique_strategy_names(spec: FundSpec) -> bool:\n    names = [s.name for s in spec.strategies]\n    return len(names) == len(set(names))","tryCatchPattern":"from pydantic import ValidationError\n\ntry:\n    spec = load_spec(\"mandate.yaml\")\nexcept ValidationError as e:\n    if \"duplicate strategy names\" in str(e):\n        raise SystemExit(\"fix the mandate YAML: \" + str(e)) from e\n    raise","preventionTips":["Let FundSpec validation run at load time — never construct strategies by hand from raw YAML.","Lint shipped mandates in CI by loading them with load_spec.","After copy-pasting a strategy block in YAML, the first edit is always the name."],"tags":["config","validation","pydantic","yaml"],"backgroundTag":null,"analyzedSha":"eff8a7320fcf0b473b135690fa1a5b0d9b022a83","analyzedAt":"2026-08-15T00:22:46.567Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}