---
title: "Evolutionary Optimizer"
description: "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."
icon: "dna"
---

<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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
diversity = hz.population_diversity(population)
```

### hz.compute_generation_stats

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

```python
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
hz.SelectionMethod.Tournament
hz.SelectionMethod.Uniform
hz.SelectionMethod.RouletteWheel
hz.SelectionMethod.Rank
hz.SelectionMethod.Ucb1
hz.SelectionMethod.EpsilonGreedy
```

### MigrationTopology

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

### IslandConfig

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

### EvolutionConfig, Individual, ParamBound, EvolutionResult

```python
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
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
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
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
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
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
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.
