Skip to main content

Overview

Selection determines which bots pass their genes to the next generation. The system uses two complementary mechanisms:
  1. Elitism: Guaranteed survival of the best performers
  2. Tournament Selection: Probabilistic selection favoring high fitness

Elitism

The top ELITE_COUNT bots survive unchanged into the next generation.
genetic/evolution.py
ELITE_COUNT = 5

def evolve(population: list[GeneticBot]) -> list[Genome]:
    ranked = sorted(population, key=evaluate_fitness, reverse=True)
    gen_num = ranked[0].genome.generation + 1
    next_gen: list[Genome] = []
    
    # 1. Elitism: keep top N unchanged
    for bot in ranked[:ELITE_COUNT]:
        elite = bot.genome.clone()
        elite.generation = gen_num
        elite.parent_ids = [bot.genome.id]
        next_gen.append(elite)

Why Elitism?

Without elitism, the best genome could be lost due to crossover/mutation randomness. Elitism guarantees monotonic improvement in best fitness.
By keeping proven winners, the population converges faster toward profitable strategies.
Elite genomes serve as performance benchmarks for the rest of the population.
Configuration:
genetic/config.py
ELITE_COUNT = 5  # Top 5% of population (5/100)
Elite genomes still receive a new ID and have their generation number incremented, but their gene values remain identical.

Tournament Selection

For the remaining 90 slots (after elitism and before immigration), parents are chosen via tournament selection.

Algorithm

1

Sample Candidates

Randomly select TOURNAMENT_SIZE bots from the population.
candidates = random.sample(population, TOURNAMENT_SIZE)
2

Evaluate Fitness

Compute fitness for each candidate.
fitness_scores = [evaluate_fitness(bot) for bot in candidates]
3

Select Winner

Return the candidate with highest fitness.
return max(candidates, key=evaluate_fitness)

Implementation

genetic/evolution.py
TOURNAMENT_SIZE = 7

def select_parent(population: list[GeneticBot]) -> GeneticBot:
    """Tournament selection: pick TOURNAMENT_SIZE random bots, return best."""
    candidates = random.sample(population, min(TOURNAMENT_SIZE, len(population)))
    return max(candidates, key=evaluate_fitness)

Selection Pressure

Tournament size controls selection pressure:
Tournament SizeSelection PressureBest Bot Selection Probability
1None (random)1%
3Low~9%
7 (default)Medium~26%
15High~58%
100Maximum100% (always best)
Calculation: For a bot ranked k-th out of N=100:
P(selected) = (N - k + 1)^t - (N - k)^t / N^t

where t = tournament size
With t=7, the best bot has:
P = (100^7 - 99^7) / 100^7 ≈ 26%

Why Tournament Selection?

Tournament size 7 gives strong bias toward fit individuals while still allowing diversity.
Unlike roulette-wheel selection, tournament works with negative fitness values and doesn’t require normalization.
O(k) complexity per selection, no sorting required.
Easy to adjust selection pressure by changing tournament size.

Breeding Process

After elitism, the remaining slots are filled through breeding:
genetic/evolution.py
def evolve(population: list[GeneticBot]) -> list[Genome]:
    # ... elitism code ...
    
    # 2. Breed to fill
    breed_count = POPULATION_SIZE - ELITE_COUNT - IMMIGRATION_COUNT
    for _ in range(breed_count):
        parent_a = select_parent(population)
        
        if random.random() < CROSSOVER_RATE:
            parent_b = select_parent(population)
            child = crossover(parent_a.genome, parent_b.genome, gen_num)
        else:
            child = parent_a.genome.clone()
            child.generation = gen_num
            child.parent_ids = [parent_a.genome.id]
        
        child = mutate(child)
        next_gen.append(child)

Crossover vs Clone

With CROSSOVER_RATE = 0.7:
  • 70% of offspring: Created by combining two parents via crossover
  • 30% of offspring: Cloned from single parent (asexual reproduction)
Why allow cloning?
  • Preserves good genomes that might be disrupted by crossover
  • Allows incremental improvement via mutation alone
  • Maintains diversity when crossover would create similar offspring

Immigration

The final 5 genomes are completely random (“immigrants”):
genetic/evolution.py
IMMIGRATION_COUNT = 5

def evolve(population: list[GeneticBot]) -> list[Genome]:
    # ... elitism and breeding ...
    
    # 3. Immigration: random new genomes for diversity
    for _ in range(IMMIGRATION_COUNT):
        next_gen.append(Genome.random(generation=gen_num))
    
    return next_gen

Why Immigration?

If the population gets stuck in a local optimum, immigration introduces fresh genetic material.
Random genomes can discover strategies the current population would never reach through crossover/mutation.
Continuous injection of new genes prevents the population from becoming too homogeneous.
Trade-off:
  • Too much immigration (greater than 10%) wastes slots on likely poor performers
  • Too little immigration (less than 2%) risks getting stuck in local optima
  • 5% is a balanced middle ground

Generation Composition

Each generation of 100 genomes consists of:
100 Total Genomes
├── 5 Elite (exact copies of previous generation's top 5)
├── 90 Offspring
│   ├── 63 from Crossover (70% of 90)
│   └── 27 from Cloning (30% of 90)
└── 5 Immigrants (random)
All 90 offspring go through mutation (15% per gene).

Selection Statistics

Expected number of offspring per bot, by rank:
Fitness RankElite?Expected OffspringTotal Copies in Next Gen
1 (Best)Yes~3.2~4.2 (including elite)
2Yes~2.8~3.8
3Yes~2.5~3.5
4Yes~2.2~3.2
5Yes~2.0~3.0
10No~1.5~1.5
25No~0.9~0.9
50 (Median)No~0.4~0.4
75No~0.1~0.1
100 (Worst)No~0.01~0.01
The best bot appears in the next generation on average 4.2 times (1 elite copy + 3.2 as parent in breeding).

Selection Pressure Over Time

As generations progress:
1

Early Generations (0-10)

  • High diversity, wide fitness distribution
  • Tournament selection explores many strategies
  • Immigration finds new niches
2

Mid Generations (10-50)

  • Convergence toward profitable signals
  • Elites dominate breeding pool
  • Mutation fine-tunes parameters
3

Late Generations (50+)

  • Population mostly similar (descendants of early winners)
  • Immigration provides main source of diversity
  • Evolution plateaus unless market conditions change

Configuration Reference

genetic/config.py
# Selection
ELITE_COUNT = 5           # Top performers that survive unchanged
TOURNAMENT_SIZE = 7       # Number of candidates per tournament
IMMIGRATION_COUNT = 5     # Random genomes injected each generation

# Breeding
CROSSOVER_RATE = 0.7      # Probability of crossover vs cloning
MUTATION_RATE = 0.15      # Per-gene mutation probability
MUTATION_SIGMA = 0.10     # Mutation perturbation stddev

# Population
POPULATION_SIZE = 100     # Total genomes per generation

Tuning Selection Pressure

Increase Exploitation (faster convergence)

  • Increase ELITE_COUNT to 10
  • Increase TOURNAMENT_SIZE to 15
  • Decrease IMMIGRATION_COUNT to 2
  • Increase CROSSOVER_RATE to 0.9

Increase Exploration (more diversity)

  • Decrease ELITE_COUNT to 2
  • Decrease TOURNAMENT_SIZE to 3
  • Increase IMMIGRATION_COUNT to 10
  • Decrease CROSSOVER_RATE to 0.5
Changing these parameters affects convergence speed and final solution quality. Test carefully.

Next Steps

Evolution Operators

Crossover and mutation implementation

Fitness Evaluation

How bots are scored

Analysis

Analyze selection effectiveness

Build docs developers (and LLMs) love