Overview
The evolution module implements the genetic algorithm mechanics: fitness evaluation, tournament selection, crossover, mutation, and generation advancement.
Fitness Evaluation
evaluate_fitness(bot)
Calculate fitness score for a bot based on ROI.
ROI percentage, or INACTIVE_FITNESS_PENALTY if insufficient trades
Fitness function:
- If
n_settled < MIN_SETTLED_TRADES: return -100.0 (penalty)
- Otherwise: return
roi_pct (realized profit / initial bankroll × 100)
from genetic.evolution import evaluate_fitness
from genetic.bot import GeneticBot
fitness = evaluate_fitness(bot)
if fitness == -100.0:
print(f"Bot {bot.bot_id}: INACTIVE (not enough trades)")
else:
print(f"Bot {bot.bot_id}: {fitness:+.2f}% ROI")
Selection
select_parent(population)
Select one parent using tournament selection.
Current population of bots
Algorithm:
- Randomly sample
TOURNAMENT_SIZE bots from population
- Return the bot with highest fitness
from genetic.evolution import select_parent
parent = select_parent(population)
print(f"Selected parent: {parent.genome.id}")
print(f"Parent fitness: {evaluate_fitness(parent):+.2f}%")
Genetic Operators
crossover(parent_a, parent_b, gen_num)
Create a child genome by uniform crossover of two parents.
Generation number for child
New child genome with mixed genes
Algorithm:
- For each gene: randomly pick from parent_a (50%) or parent_b (50%)
- Child inherits IDs of both parents
from genetic.evolution import crossover
from genetic.genome import Genome
parent_a = Genome.random()
parent_b = Genome.random()
child = crossover(parent_a, parent_b, gen_num=5)
print(f"Child ID: {child.id}")
print(f"Parents: {child.parent_ids}")
print(f"Signal type: {child.signal_type:.3f}")
mutate(genome)
Apply Gaussian mutation to genome.
Genome to mutate (will be deep copied)
New genome with mutations applied
Algorithm:
- For each gene:
- With probability
MUTATION_RATE (default 15%):
- Add Gaussian noise: N(0,
MUTATION_SIGMA)
- Clamp result to [0.0, 1.0]
from genetic.evolution import mutate
from genetic.genome import Genome
original = Genome.random()
mutated = mutate(original)
# Count differences
diffs = 0
for gene in Genome.gene_names():
if getattr(original, gene) != getattr(mutated, gene):
diffs += 1
print(f"Mutated {diffs} out of {len(Genome.gene_names())} genes")
Evolution Pipeline
evolve(population)
Produce the next generation from the current population.
Current generation of bots
List of genomes for next generation (size = POPULATION_SIZE)
Algorithm:
- Sort by fitness (high to low)
- Elitism: Keep top
ELITE_COUNT genomes unchanged (default 5)
- Breeding: Fill remaining slots:
- Select parent via tournament
- With prob
CROSSOVER_RATE (70%): crossover with second parent
- Otherwise: clone parent
- Apply mutation
- Immigration: Add
IMMIGRATION_COUNT random genomes (default 5)
from genetic.evolution import evolve
# After generation completes
next_genomes = evolve(population)
print(f"Generated {len(next_genomes)} genomes for next generation")
for i, genome in enumerate(next_genomes[:5]):
print(f" {i+1}. {genome.id} (gen {genome.generation}) parents={genome.parent_ids}")
Evolution Configuration
All constants are defined in genetic/config.py:
# Population
POPULATION_SIZE = 100
# Evolution
ELITE_COUNT = 5 # Top N survive unchanged
TOURNAMENT_SIZE = 7 # Tournament selection pressure
CROSSOVER_RATE = 0.7 # Probability of crossover vs clone
MUTATION_RATE = 0.15 # Per-gene mutation probability
MUTATION_SIGMA = 0.10 # Gaussian stddev for perturbation
IMMIGRATION_COUNT = 5 # Random new genomes each generation
# Fitness
MIN_SETTLED_TRADES = 5
INACTIVE_FITNESS_PENALTY = -100.0
Example: Full Evolution Cycle
from genetic.evolution import evolve, evaluate_fitness
from genetic.bot import GeneticBot
from genetic.genome import Genome
# Assume we have a population that just finished trading
population: list[GeneticBot] = [...]
# Evaluate and rank
ranked = sorted(population, key=evaluate_fitness, reverse=True)
print("Top 5 performers:")
for i, bot in enumerate(ranked[:5], 1):
fitness = evaluate_fitness(bot)
stats = bot.account
print(f"{i}. {bot.genome.id}: {fitness:+.2f}% ({stats.wins}W-{stats.n_settled-stats.wins}L)")
print("\nBottom 5:")
for i, bot in enumerate(ranked[-5:], 1):
fitness = evaluate_fitness(bot)
print(f"{i}. {bot.genome.id}: {fitness:+.2f}%")
# Evolve
print("\nEvolving...")
next_genomes = evolve(population)
print(f"\nNext generation: {len(next_genomes)} genomes")
print(f" Elite: {sum(1 for g in next_genomes if len(g.parent_ids) == 1)}")
print(f" Crossover: {sum(1 for g in next_genomes if len(g.parent_ids) == 2)}")
print(f" Immigration: {sum(1 for g in next_genomes if not g.parent_ids)}")
Genetic Diversity
The algorithm maintains diversity through:
- Tournament selection: Allows weaker genomes to reproduce
- Mutation: Introduces noise to prevent convergence
- Immigration: Injects fresh random genomes every generation
- Crossover: Mixes genes from different lineages
# Measure diversity in population
from genetic.genome import Genome
def genome_diversity(genomes: list[Genome]) -> dict:
"""Calculate diversity metrics."""
n = len(genomes)
gene_variances = {}
for gene_name in Genome.gene_names():
values = [getattr(g, gene_name) for g in genomes]
mean = sum(values) / n
variance = sum((v - mean) ** 2 for v in values) / n
gene_variances[gene_name] = variance
avg_variance = sum(gene_variances.values()) / len(gene_variances)
return {
"avg_variance": avg_variance,
"min_variance": min(gene_variances.values()),
"max_variance": max(gene_variances.values()),
"low_diversity_genes": [k for k, v in gene_variances.items() if v < 0.01]
}
# Check diversity after evolution
diversity = genome_diversity(next_genomes)
print(f"Average gene variance: {diversity['avg_variance']:.4f}")
if diversity['low_diversity_genes']:
print(f"Low diversity in: {diversity['low_diversity_genes']}")