> ## Documentation Index
> Fetch the complete documentation index at: https://mathematicalcompany.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Evolutionary Optimizer

> Production-grade genetic algorithm engine with 6 selection methods, NSGA-II multi-objective optimization, island model parallelism, and adaptive mutation. Inspired by the Darwin SDK and the ProFiT framework.

<Warning>
  **Ultra Feature.** Requires an Ultra subscription. [Get started at api.mathematicalcompany.com](https://api.mathematicalcompany.com)
</Warning>

# Evolutionary Optimizer

Grid search is exponential. Random search is wasteful. Evolutionary optimization finds good strategy parameters by mimicking natural selection: the fittest parameter sets survive, mutate, and recombine. Horizon's evolution engine draws from the Darwin SDK and the ProFiT framework (Siper et al., "Program Search for Financial Trading") to deliver parameter optimization with 6 selection methods, NSGA-II multi-objective optimization, island model parallelism, and adaptive mutation. All genetic operators run in Rust; the fitness function calls `hz.backtest()` in Python.

## Overview

<CardGroup cols={2}>
  <Card title="6 Selection Methods" icon="trophy">
    Tournament, Uniform, Roulette Wheel, Rank, UCB1, and Epsilon-Greedy.
  </Card>

  <Card title="NSGA-II Multi-Objective" icon="chart-mixed">
    Pareto fronts, crowding distance, and non-dominated sorting for multi-objective optimization.
  </Card>

  <Card title="Island Model" icon="island-tropical">
    Parallel populations with Ring and FullyConnected migration topologies.
  </Card>

  <Card title="Adaptive Mutation" icon="gauge-high">
    Rechenberg's 1/5 success rule for self-tuning mutation rates.
  </Card>

  <Card title="Convergence Detection" icon="shield-halved">
    Early stopping via patience exhaustion and diversity collapse detection.
  </Card>

  <Card title="Walk-Forward Validation" icon="forward">
    Train/validate split prevents overfitting to in-sample data.
  </Card>
</CardGroup>

***

## Selection Methods (Rust)

All selection methods are deterministic given a seed and run in Rust.

### hz.tournament\_select

Select parent indices using tournament selection. Pick `tournament_size` individuals at random; the one with the highest fitness wins.

```python theme={null}
import horizon as hz

fitnesses = [0.1, 0.5, 0.3, 0.9, 0.2]
selected = hz.tournament_select(
    fitnesses=fitnesses,
    tournament_size=3,
    count=10,
    seed=42,
)
```

### hz.roulette\_select

Probability proportional to fitness. Handles negative fitnesses by shifting the minimum to a small positive value.

```python theme={null}
selected = hz.roulette_select(fitnesses=[0.1, 0.5, 0.9], count=10, seed=42)
```

### hz.rank\_select

Probability proportional to rank position. Rank 1 (worst) gets weight 1, rank N (best) gets weight N. Insensitive to fitness magnitude.

```python theme={null}
selected = hz.rank_select(fitnesses=[0.1, 0.5, 0.9], count=10, seed=42)
```

### hz.ucb1\_select

Upper Confidence Bound 1. Balances exploitation (high fitness) with exploration (rarely selected individuals). From Auer et al. (2002), used in the ProFiT framework for parent selection.

```python theme={null}
selected = hz.ucb1_select(
    fitnesses=[0.5, 0.8, 0.3, 0.9],
    selection_counts=[10, 10, 1, 10],   # how many times each was selected
    total_selections=31,
    count=2,
)
# Index 2 gets an exploration bonus (selected only once)
```

| Parameter          | Type          | Description                             |
| ------------------ | ------------- | --------------------------------------- |
| `fitnesses`        | `list[float]` | Fitness values per individual           |
| `selection_counts` | `list[int]`   | Times each individual has been selected |
| `total_selections` | `int`         | Total selections across all individuals |
| `count`            | `int`         | Number of selections to make            |

### hz.epsilon\_greedy\_select

With probability epsilon pick a random individual; otherwise pick the best. Simple exploration-exploitation tradeoff.

```python theme={null}
selected = hz.epsilon_greedy_select(
    fitnesses=[0.1, 0.5, 0.9],
    epsilon=0.1,     # 10% random, 90% best
    count=10,
    seed=42,
)
```

### hz.uniform\_select

Pure random selection with equal probability. Useful as a baseline or for maximum exploration.

```python theme={null}
selected = hz.uniform_select(fitnesses=[0.1, 0.5, 0.9], count=10, seed=42)
```

***

## Genetic Operators (Rust)

### hz.gaussian\_mutate

Apply bounded Gaussian noise to a genome. Discrete parameters are rounded after mutation.

```python theme={null}
mutated = hz.gaussian_mutate(
    genome=[0.5, 0.5, 0.5],
    bounds_min=[0.0, 0.0, 0.0],
    bounds_max=[1.0, 1.0, 1.0],
    is_discrete=[False, False, True],
    sigma=0.1,       # noise scale (fraction of range)
    rate=0.3,        # 30% chance per gene
    seed=42,
)
```

### hz.uniform\_crossover

Recombine genes from two parents. Each gene has probability `rate` of coming from parent B.

```python theme={null}
child = hz.uniform_crossover(
    parent_a=[0.0, 0.0, 0.0],
    parent_b=[1.0, 1.0, 1.0],
    rate=0.5,
    seed=42,
)
```

### hz.init\_population

Generate a random initial population within bounds.

```python theme={null}
population = hz.init_population(
    bounds_min=[0.0, 10.0],
    bounds_max=[1.0, 100.0],
    is_discrete=[False, True],
    pop_size=50,
    seed=42,
)
```

***

## NSGA-II Multi-Objective Optimization

Multi-objective optimization using Non-dominated Sorting Genetic Algorithm II. Optimizes multiple objectives simultaneously (e.g., maximize Sharpe AND minimize drawdown) without collapsing them into a single scalar. Returns the Pareto front of non-dominated solutions.

Based on Deb et al., "A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-II" (2002).

### hz.non\_dominated\_sort

Fast non-dominated sorting. Returns fronts where `fronts[0]` is the Pareto front.

```python theme={null}
objectives = [
    [3.0, 1.0],   # high Sharpe, low stability
    [1.0, 3.0],   # low Sharpe, high stability
    [2.0, 2.0],   # balanced
    [1.0, 1.0],   # dominated
]
fronts = hz.non_dominated_sort(objectives)
# fronts[0] = [0, 1, 2]  (non-dominated)
# fronts[1] = [3]         (dominated by all in front 0)
```

### hz.crowding\_distance\_assign

Compute crowding distance for individuals in the same front. Boundary individuals get infinite distance; interior individuals get distance proportional to objective-space gaps.

```python theme={null}
distances = hz.crowding_distance_assign(objectives, front_indices=[0, 1, 2])
```

### hz.nsga2\_select

Select N individuals using NSGA-II ranking (front rank + crowding distance).

```python theme={null}
result = hz.nsga2_select(objectives, n_select=3)
print(result.selected_indices)       # top 3 by NSGA-II ranking
print(result.pareto_front_indices)   # Pareto front
print(result.fronts)                 # all fronts
```

### hz.pareto\_front

Extract the Pareto front (non-dominated set) indices.

```python theme={null}
front = hz.pareto_front(objectives)
```

***

## Island Model

Multiple populations evolve in parallel using different selection methods, with periodic migration of top individuals between islands. Each island uses a different selection strategy for diversity, following the ProFiT framework's approach of per-island specialization.

### hz.island\_migrate

Perform migration between islands based on the configured topology.

```python theme={null}
new_islands = hz.island_migrate(
    islands=[pop_a, pop_b, pop_c, pop_d],
    fitnesses=[fits_a, fits_b, fits_c, fits_d],
    config=hz.IslandConfig(
        n_islands=4,
        migration_interval=5,
        migration_count=2,
        topology=hz.MigrationTopology.Ring,
    ),
)
```

### hz.island\_selection\_methods

Get the default selection method assignment per island. Methods cycle through Roulette, Rank, UCB1, and Epsilon-Greedy.

```python theme={null}
methods = hz.island_selection_methods(4)
# [RouletteWheel, Rank, Ucb1, EpsilonGreedy]
```

***

## Convergence Detection

### hz.check\_convergence

Detect when evolution has stalled (no improvement for `patience` generations) or the population has collapsed (diversity below threshold).

```python theme={null}
state = hz.check_convergence(
    best_fitness=1.5,
    diversity=0.3,
    prev_best_fitness=1.4,
    gens_without_improvement=0,
    patience=20,
    min_improvement=1e-4,
    diversity_threshold=0.01,
)
if state.converged:
    print(f"Stopped: {state.reason}")
```

***

## Adaptive Mutation

### hz.adapt\_mutation\_sigma

Self-tune mutation sigma using Rechenberg's 1/5 success rule. If the acceptance rate exceeds the target (default 20%), increase sigma to explore more broadly. If below, decrease sigma to fine-tune. Also used in the Continuous Program Search framework (Siper et al.) for adapting mutation strength.

```python theme={null}
state = hz.adapt_mutation_sigma(
    current_sigma=0.1,
    success_count=30,
    total_count=100,
    target_success_rate=0.2,
    adaptation_factor=1.5,
)
print(f"New sigma: {state.sigma:.4f}")  # increased (30% > 20%)
```

***

## Population Analytics

### hz.population\_diversity

Mean pairwise L2 distance between genomes, normalized by dimension. Higher values indicate more diverse populations.

```python theme={null}
diversity = hz.population_diversity(population)
```

### hz.compute\_generation\_stats

Per-generation statistics: best, median, mean, worst, std, and diversity.

```python theme={null}
stats = hz.compute_generation_stats(generation=5, fitnesses=[0.1, 0.5, 0.3, 0.8])
print(f"Best: {stats.best_fitness}, Diversity: {stats.diversity:.4f}")
```

***

## Types

### SelectionMethod

```python theme={null}
hz.SelectionMethod.Tournament
hz.SelectionMethod.Uniform
hz.SelectionMethod.RouletteWheel
hz.SelectionMethod.Rank
hz.SelectionMethod.Ucb1
hz.SelectionMethod.EpsilonGreedy
```

### MigrationTopology

```python theme={null}
hz.MigrationTopology.Ring            # i -> i+1 (wraps around)
hz.MigrationTopology.FullyConnected  # every island sends to every other
```

### IslandConfig

```python theme={null}
config = hz.IslandConfig(
    n_islands=4,
    migration_interval=5,
    migration_count=2,
    topology=hz.MigrationTopology.Ring,
)
```

### EvolutionConfig, Individual, ParamBound, EvolutionResult

```python theme={null}
config = hz.EvolutionConfig(pop_size=50, generations=100, mutation_rate=0.1,
                             crossover_rate=0.7, tournament_size=3, elite_count=2, sigma=0.1)
ind = hz.Individual(genome=[0.5, 0.3], fitness=1.5)
bound = hz.ParamBound(name="spread", min_val=0.01, max_val=0.10, is_discrete=False)
```

***

## Python Orchestration

### hz.evolve

Full evolutionary optimization loop with walk-forward validation.

```python theme={null}
result = hz.evolve(
    data=historical_data,
    pipeline_factory=lambda params: [my_strategy_factory(**params)],
    param_bounds=[
        {"name": "spread", "min": 0.01, "max": 0.10},
        {"name": "window", "min": 5, "max": 100, "discrete": True},
    ],
    pop_size=50,
    generations=100,
    selection="ucb1",          # or "tournament", "roulette", "rank", "epsilon_greedy"
    early_stopping=True,
    patience=20,
    adaptive_mutation=True,
    seed=42,
)
print(f"Best fitness: {result.best_fitness:.4f}")
```

| Parameter           | Type         | Default        | Description                                     |
| ------------------- | ------------ | -------------- | ----------------------------------------------- |
| `data`              | `Any`        | required       | Historical data for backtesting                 |
| `pipeline_factory`  | `Callable`   | required       | Function(params) to pipeline                    |
| `param_bounds`      | `list[dict]` | required       | Parameter bounds                                |
| `pop_size`          | `int`        | `50`           | Population size                                 |
| `generations`       | `int`        | `100`          | Maximum generations                             |
| `selection`         | `str`        | `"tournament"` | Selection method                                |
| `early_stopping`    | `bool`       | `True`         | Enable convergence detection                    |
| `patience`          | `int`        | `20`           | Generations without improvement before stopping |
| `adaptive_mutation` | `bool`       | `False`        | Enable Rechenberg's 1/5 rule                    |
| `epsilon`           | `float`      | `0.1`          | Epsilon for epsilon-greedy selection            |
| `fraction_train`    | `float`      | `0.7`          | Train/validate split                            |
| `fitness_fn`        | `Callable`   | `None`         | Custom fitness(result) to float                 |

### hz.evolve\_nsga2

Multi-objective evolution using NSGA-II. Returns the Pareto front.

```python theme={null}
result = hz.evolve_nsga2(
    data=historical_data,
    pipeline_factory=lambda params: [my_strategy_factory(**params)],
    param_bounds=[
        {"name": "spread", "min": 0.01, "max": 0.10},
        {"name": "size", "min": 1, "max": 50, "discrete": True},
    ],
    objectives=[
        {"metric": "sharpe_ratio", "direction": "maximize"},
        {"metric": "max_drawdown", "direction": "minimize"},
    ],
    pop_size=50,
    generations=100,
)
for solution in result["pareto_front"]:
    print(f"Sharpe={solution['objectives']['sharpe_ratio']:.2f}, "
          f"DD={solution['objectives']['max_drawdown']:.4f}")
```

| Parameter        | Type         | Default  | Description                  |
| ---------------- | ------------ | -------- | ---------------------------- |
| `objectives`     | `list[dict]` | required | Objectives to optimize       |
| `pop_size`       | `int`        | `50`     | Population size              |
| `generations`    | `int`        | `100`    | Maximum generations          |
| `early_stopping` | `bool`       | `True`   | Enable convergence detection |

### hz.evolve\_islands

Island model evolution with parallel populations and periodic migration.

```python theme={null}
result = hz.evolve_islands(
    data=historical_data,
    pipeline_factory=lambda params: [my_strategy_factory(**params)],
    param_bounds=[
        {"name": "spread", "min": 0.01, "max": 0.10},
        {"name": "size", "min": 1, "max": 50, "discrete": True},
    ],
    n_islands=4,
    migration_interval=5,
    migration_count=2,
    topology="ring",
    pop_size=100,
    generations=200,
)
print(f"Best fitness: {result['best_fitness']:.4f}")
print(f"Best params: {result['best_params']}")
for s in result["top_strategies"][:3]:
    print(f"  fitness={s['fitness']:.4f}, params={s['params']}")
```

| Parameter            | Type  | Default  | Description                     |
| -------------------- | ----- | -------- | ------------------------------- |
| `n_islands`          | `int` | `4`      | Number of parallel populations  |
| `migration_interval` | `int` | `5`      | Generations between migrations  |
| `migration_count`    | `int` | `2`      | Top individuals per migration   |
| `topology`           | `str` | `"ring"` | `"ring"` or `"fully_connected"` |

***

## Examples

### Optimize Market Making Parameters

```python theme={null}
import horizon as hz

def make_strategy(params):
    spread = params["spread"]
    size = int(params["size"])
    def strategy(ctx):
        if ctx.feed is None or ctx.feed.price <= 0:
            return []
        return hz.quotes(fair=ctx.feed.price, spread=spread, size=size)
    return strategy

result = hz.evolve(
    data=my_historical_data,
    pipeline_factory=lambda p: [make_strategy(p)],
    param_bounds=[
        {"name": "spread", "min": 0.01, "max": 0.15},
        {"name": "size", "min": 1, "max": 100, "discrete": True},
    ],
    selection="ucb1",
    adaptive_mutation=True,
    pop_size=30,
    generations=50,
    seed=42,
)
print(f"Optimal spread: {result.best_genome[0]:.4f}")
print(f"Optimal size: {int(result.best_genome[1])}")
```

### Multi-Objective: Sharpe vs Drawdown

```python theme={null}
import horizon as hz

result = hz.evolve_nsga2(
    data=historical_data,
    pipeline_factory=lambda p: [make_strategy(p)],
    param_bounds=[
        {"name": "spread", "min": 0.01, "max": 0.15},
        {"name": "size", "min": 1, "max": 100, "discrete": True},
        {"name": "window", "min": 10, "max": 200, "discrete": True},
    ],
    objectives=[
        {"metric": "sharpe_ratio", "direction": "maximize"},
        {"metric": "max_drawdown", "direction": "minimize"},
    ],
    pop_size=50,
    generations=100,
)
# Result contains the Pareto front: no solution dominates another
for s in result["pareto_front"]:
    print(f"Sharpe={s['objectives']['sharpe_ratio']:.2f}, "
          f"DD={s['objectives']['max_drawdown']:.4f}, "
          f"params={s['params']}")
```

### Island Model with Diverse Selection

```python theme={null}
import horizon as hz

result = hz.evolve_islands(
    data=historical_data,
    pipeline_factory=lambda p: [make_strategy(p)],
    param_bounds=[
        {"name": "spread", "min": 0.01, "max": 0.15},
        {"name": "size", "min": 1, "max": 100, "discrete": True},
    ],
    n_islands=4,              # 4 parallel populations
    migration_interval=5,     # migrate every 5 generations
    migration_count=2,        # top 2 individuals per migration
    topology="ring",          # ring migration topology
    pop_size=100,             # total across all islands
    generations=200,
)
# Each island uses a different selection method (Roulette, Rank, UCB1, Epsilon-Greedy)
# for maximum diversity in the search
print(f"Best fitness: {result['best_fitness']:.4f}")
```

***

## Mathematical Background

<AccordionGroup>
  <Accordion title="Tournament Selection">
    For each selection, pick `tournament_size` individuals at random (with replacement). The one with the highest fitness wins. Larger tournaments increase selection pressure (faster convergence but less diversity).
  </Accordion>

  <Accordion title="UCB1 Selection (Auer et al., 2002)">
    Treats each individual as a multi-armed bandit arm. Selects the individual that maximizes:

    `UCB1(i) = fitness(i) + sqrt(2 * ln(T) / n_i)`

    where T is the total number of selections and n\_i is the number of times individual i has been selected. The first term exploits known-good individuals; the second term explores rarely-selected ones. This method was adopted by the ProFiT framework (Siper et al.) for parent selection in evolutionary strategy discovery.
  </Accordion>

  <Accordion title="Roulette Wheel Selection">
    Probability of selecting individual i is proportional to its fitness. Fitnesses are shifted so the minimum becomes a small positive value, ensuring valid probabilities even with negative fitnesses: `p(i) = (f(i) - f_min + epsilon) / sum`.
  </Accordion>

  <Accordion title="Rank Selection">
    Individuals are sorted by fitness and assigned ranks 1 (worst) through N (best). Selection probability is proportional to rank: `p(i) = rank(i) / (N*(N+1)/2)`. Unlike roulette wheel, this is insensitive to fitness magnitudes, preventing a single dominant individual from monopolizing selection.
  </Accordion>

  <Accordion title="NSGA-II (Deb et al., 2002)">
    Non-dominated Sorting Genetic Algorithm II for multi-objective optimization:

    1. **Non-dominated sorting**: Partition population into fronts. Front 0 (Pareto front) contains solutions where no other solution is better on all objectives. Front 1 is dominated only by front 0, etc.

    2. **Crowding distance**: Within each front, measure the density of nearby solutions in objective space. Boundary solutions get infinite distance. This preserves diversity along the Pareto front.

    3. **Selection**: Prefer lower-rank fronts. Within the same front, prefer higher crowding distance (less crowded regions).

    Individual A dominates B iff A is at least as good as B on all objectives and strictly better on at least one.
  </Accordion>

  <Accordion title="Island Model">
    Multiple populations (islands) evolve independently with different selection methods. Periodically, top individuals migrate between islands:

    * **Ring topology**: Island i sends to island (i+1) mod N
    * **Fully connected**: Every island sends to every other island

    Per-island selection specialization (Roulette, Rank, UCB1, Epsilon-Greedy) ensures diverse search strategies. Migration spreads good genetic material across populations while maintaining local diversity.
  </Accordion>

  <Accordion title="Rechenberg's 1/5 Rule (Adaptive Mutation)">
    Track the fraction of mutations that produce offspring fitter than their parents. If the success rate exceeds 1/5 (20%), increase mutation strength (sigma) to explore more broadly. If below, decrease sigma to fine-tune near optima.

    `sigma_new = sigma * c`    if success\_rate is greater than 0.2

    `sigma_new = sigma / c`    if success\_rate is less than 0.2

    where c is the adaptation factor (default 1.5). Sigma is clamped to `[sigma_min, sigma_max]`.

    This self-tuning mechanism was also adopted by the Continuous Program Search framework (Siper et al.) for adapting mutation strength based on acceptance rates.
  </Accordion>

  <Accordion title="Convergence Detection">
    Evolution stops early when:

    1. **Patience exhausted**: No improvement exceeding `min_improvement` for `patience` consecutive generations.
    2. **Diversity collapse**: Population diversity (mean pairwise L2 distance) falls below `diversity_threshold`, indicating premature convergence.

    Both checks run each generation. Early stopping prevents wasting compute on stagnant populations.
  </Accordion>

  <Accordion title="Walk-Forward Validation">
    Data is split into train (first `fraction_train`) and validate (rest). Evolution optimizes fitness on the training set. The final reported fitness is evaluated on the held-out validation set, preventing overfitting to in-sample noise.
  </Accordion>
</AccordionGroup>

***

## References

* Auer, P., Cesa-Bianchi, N., Fischer, P. (2002). "Finite-time Analysis of the Multiarmed Bandit Problem." Machine Learning, 47(2-3), 235-256.
* Deb, K., Pratap, A., Agarwal, S., Meyarivan, T. (2002). "A Fast and Elitist Multiobjective Genetic Algorithm: NSGA-II." IEEE Transactions on Evolutionary Computation, 6(2), 182-197.
* Rechenberg, I. (1973). Evolutionsstrategie: Optimierung technischer Systeme nach Prinzipien der biologischen Evolution. Frommann-Holzboog.
* Siper, S., Almog, G., Shoham, Y. et al. "ProFiT: Program Search for Financial Trading." Preprint.
* Siper, S., Almog, G., Shoham, Y. et al. "Continuous Program Search." SSRN 6169788.
