virattt/ai-hedge-fund · error · ValueError

duplicate strategy names

Error message

duplicate strategy names: {sorted(duplicates)}

What it means

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.

Solutions

  1. Rename the duplicate strategies in the mandate YAML so every name is unique (e.g. 'value_core' and 'value_tilt').
  2. If you meant the strategies to be identical, delete the duplicate instead of renaming it.
  3. Check for YAML merge-key accidents: an anchor like <<: *value-strategy combined with an explicit name that collides.

Example fix

# before (mandate.yaml)
strategies:
  - name: momentum
    weight: 0.6
    ...
  - name: momentum      # duplicate -> ValueError: duplicate strategy names: ['momentum']
    weight: 0.4

# after
strategies:
  - name: momentum-core
    weight: 0.6
  - name: momentum-tilt
    weight: 0.4
Defensive patterns

Strategy: validation

Validate before calling

def validate_mandate_spec(spec) -> list[str]:
    names = [s.name for s in spec.strategies]
    seen, dupes = set(), set()
    for n in names:
        (dupes if n in seen else seen).add(n)
    return sorted(dupes)  # empty list == ok (FundSpec enforces this too)

Type guard

from hedge_fund.fund.spec import FundSpec

def has_unique_strategy_names(spec: FundSpec) -> bool:
    names = [s.name for s in spec.strategies]
    return len(names) == len(set(names))

Try / catch

from pydantic import ValidationError

try:
    spec = load_spec("mandate.yaml")
except ValidationError as e:
    if "duplicate strategy names" in str(e):
        raise SystemExit("fix the mandate YAML: " + str(e)) from e
    raise

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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

Appendix: source

Thrown at hedge_fund/fund/spec.py:139

    )
    benchmark: str = Field(
        default="SPY",
        description="what the fund measures itself against; also the source "
        "of the backtest's trading-day grid",
    )

    @field_validator("benchmark")
    @classmethod
    def _uppercase_benchmark(cls, ticker: str) -> str:
        return ticker.upper()

    @field_validator("strategies")
    @classmethod
    def _unique_strategy_names(cls, strategies: list[StrategySpec]) -> list[StrategySpec]:
        names = [s.name for s in strategies]
        duplicates = {n for n in names if names.count(n) > 1}
        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

View on GitHub (pinned to eff8a7320f)