hyperliquid_backtest/optimization/
mod.rs1use rand::RngCore;
2use std::{fmt, marker::PhantomData};
3
4pub trait Genome: Clone + Send + Sync + Sized {
6 fn random(rng: &mut dyn RngCore) -> Self;
8
9 fn mutate(&mut self, rng: &mut dyn RngCore);
11
12 fn crossover(&self, other: &Self, rng: &mut dyn RngCore) -> Self;
14}
15
16#[derive(Debug, Clone)]
18pub struct OptimizationOutcome<M> {
19 pub fitness: f64,
21 pub metrics: M,
23}
24
25#[derive(thiserror::Error, Debug)]
27pub enum OptimizationError {
28 #[error("population size must be greater than zero")]
30 EmptyPopulation,
31 #[error("elitism must be smaller than the population size")]
33 InvalidElitism,
34 #[error("tournament size must be greater than zero")]
36 InvalidTournamentSize,
37 #[error("candidate evaluation failed: {0}")]
39 EvaluationFailed(String),
40}
41
42#[derive(Debug, Clone)]
44pub struct OptimizationResult<G, M>
45where
46 G: Genome,
47 M: Clone + Send + Sync,
48{
49 pub best_candidate: G,
51 pub best_metrics: M,
53 pub best_fitness: f64,
55 pub generations: Vec<GenerationSummary<M>>,
57}
58
59#[derive(Debug, Clone)]
61pub struct GenerationSummary<M>
62where
63 M: Clone + Send + Sync,
64{
65 pub index: usize,
67 pub best_fitness: f64,
69 pub average_fitness: f64,
71 pub best_metrics: M,
73}
74
75#[derive(Debug, Clone, Copy)]
77pub struct GeneticOptimizerConfig {
78 pub population_size: usize,
80 pub elitism: usize,
82 pub generations: usize,
84 pub tournament_size: usize,
86}
87
88impl Default for GeneticOptimizerConfig {
89 fn default() -> Self {
90 Self {
91 population_size: 32,
92 elitism: 2,
93 generations: 20,
94 tournament_size: 3,
95 }
96 }
97}
98
99pub trait FitnessEvaluator<G>: Send + Sync
101where
102 G: Genome,
103{
104 type Metrics: Clone + Send + Sync;
106
107 fn evaluate(
109 &self,
110 candidate: &G,
111 ) -> Result<OptimizationOutcome<Self::Metrics>, Box<dyn std::error::Error + Send + Sync>>;
112}
113
114impl<G, M, F, E> FitnessEvaluator<G> for F
115where
116 G: Genome,
117 M: Clone + Send + Sync + 'static,
118 F: Fn(&G) -> Result<OptimizationOutcome<M>, E> + Send + Sync,
119 E: std::error::Error + Send + Sync + 'static,
120{
121 type Metrics = M;
122
123 fn evaluate(
124 &self,
125 candidate: &G,
126 ) -> Result<OptimizationOutcome<M>, Box<dyn std::error::Error + Send + Sync>> {
127 self(candidate).map_err(|err| Box::new(err) as _)
128 }
129}
130
131#[derive(Clone)]
132struct Individual<G, M>
133where
134 G: Genome,
135 M: Clone + Send + Sync,
136{
137 genome: G,
138 metrics: Option<M>,
139 fitness: f64,
140}
141
142impl<G, M> Individual<G, M>
143where
144 G: Genome,
145 M: Clone + Send + Sync,
146{
147 fn unevaluated(genome: G) -> Self {
148 Self {
149 genome,
150 metrics: None,
151 fitness: f64::NEG_INFINITY,
152 }
153 }
154}
155
156pub struct GeneticOptimizer<G, E>
158where
159 G: Genome,
160 E: FitnessEvaluator<G>,
161{
162 config: GeneticOptimizerConfig,
163 evaluator: E,
164 phantom: PhantomData<G>,
165}
166
167impl<G, E> GeneticOptimizer<G, E>
168where
169 G: Genome,
170 E: FitnessEvaluator<G>,
171{
172 pub fn new(config: GeneticOptimizerConfig, evaluator: E) -> Self {
174 Self {
175 config,
176 evaluator,
177 phantom: PhantomData,
178 }
179 }
180
181 pub fn run<R>(
183 &self,
184 rng: &mut R,
185 ) -> Result<OptimizationResult<G, E::Metrics>, OptimizationError>
186 where
187 R: RngCore,
188 {
189 if self.config.population_size == 0 {
190 return Err(OptimizationError::EmptyPopulation);
191 }
192
193 if self.config.elitism >= self.config.population_size {
194 return Err(OptimizationError::InvalidElitism);
195 }
196
197 if self.config.tournament_size == 0 {
198 return Err(OptimizationError::InvalidTournamentSize);
199 }
200
201 let mut population: Vec<Individual<G, E::Metrics>> = (0..self.config.population_size)
202 .map(|_| Individual::unevaluated(G::random(rng)))
203 .collect();
204
205 let mut generation_summaries = Vec::with_capacity(self.config.generations + 1);
206
207 self.evaluate_population(&mut population)?;
208 population.sort_by(|a, b| b.fitness.total_cmp(&a.fitness));
209 generation_summaries.push(Self::summarize_generation(0, &population));
210
211 for generation in 1..=self.config.generations {
212 let mut next_population: Vec<Individual<G, E::Metrics>> =
213 Vec::with_capacity(self.config.population_size);
214 next_population.extend(population.iter().take(self.config.elitism).cloned());
215
216 while next_population.len() < self.config.population_size {
217 let parent_a =
218 Self::tournament_select(&population, self.config.tournament_size, rng);
219 let parent_b =
220 Self::tournament_select(&population, self.config.tournament_size, rng);
221
222 let mut child_genome = parent_a.genome.crossover(&parent_b.genome, rng);
223 child_genome.mutate(rng);
224 next_population.push(Individual::unevaluated(child_genome));
225 }
226
227 population = next_population;
228 self.evaluate_population(&mut population)?;
229 population.sort_by(|a, b| b.fitness.total_cmp(&a.fitness));
230 generation_summaries.push(Self::summarize_generation(generation, &population));
231 }
232
233 let best = population
234 .first()
235 .expect("population cannot be empty after initialization");
236
237 Ok(OptimizationResult {
238 best_candidate: best.genome.clone(),
239 best_metrics: best
240 .metrics
241 .clone()
242 .expect("metrics must be present after evaluation"),
243 best_fitness: best.fitness,
244 generations: generation_summaries,
245 })
246 }
247
248 fn evaluate_population(
249 &self,
250 population: &mut [Individual<G, E::Metrics>],
251 ) -> Result<(), OptimizationError> {
252 for individual in population.iter_mut() {
253 if individual.metrics.is_some() {
254 continue;
255 }
256
257 let outcome = self
258 .evaluator
259 .evaluate(&individual.genome)
260 .map_err(|err| OptimizationError::EvaluationFailed(err.to_string()))?;
261
262 individual.fitness = if outcome.fitness.is_finite() {
263 outcome.fitness
264 } else {
265 f64::NEG_INFINITY
266 };
267 individual.metrics = Some(outcome.metrics);
268 }
269
270 Ok(())
271 }
272
273 fn tournament_select<'a, R>(
274 population: &'a [Individual<G, E::Metrics>],
275 tournament_size: usize,
276 rng: &mut R,
277 ) -> &'a Individual<G, E::Metrics>
278 where
279 R: RngCore,
280 {
281 let mut best_index = rng.next_u32() as usize % population.len();
282 let mut best_fitness = population[best_index].fitness;
283
284 for _ in 1..tournament_size {
285 let idx = rng.next_u32() as usize % population.len();
286 let fitness = population[idx].fitness;
287 if fitness > best_fitness {
288 best_index = idx;
289 best_fitness = fitness;
290 }
291 }
292
293 &population[best_index]
294 }
295
296 fn summarize_generation(
297 index: usize,
298 population: &[Individual<G, E::Metrics>],
299 ) -> GenerationSummary<E::Metrics> {
300 let mut total = 0.0;
301 let mut count = 0usize;
302 let mut best: Option<&Individual<G, E::Metrics>> = None;
303
304 for individual in population {
305 if best
306 .as_ref()
307 .map(|current| individual.fitness > current.fitness)
308 .unwrap_or(true)
309 {
310 best = Some(individual);
311 }
312
313 if individual.fitness.is_finite() {
314 total += individual.fitness;
315 count += 1;
316 }
317 }
318
319 let average = if count > 0 {
320 total / count as f64
321 } else {
322 f64::NEG_INFINITY
323 };
324
325 let best = best.expect("population must contain at least one individual");
326
327 GenerationSummary {
328 index,
329 best_fitness: best.fitness,
330 average_fitness: average,
331 best_metrics: best
332 .metrics
333 .clone()
334 .expect("metrics must exist after evaluation"),
335 }
336 }
337}
338
339impl<G, E> fmt::Debug for GeneticOptimizer<G, E>
340where
341 G: Genome,
342 E: FitnessEvaluator<G>,
343{
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 f.debug_struct("GeneticOptimizer")
346 .field("config", &self.config)
347 .finish()
348 }
349}