Skip to content

Commit 61221de

Browse files
authored
Merge pull request #13 from xsa-dev/codex/add-ga-optimizer-example-for-backtesting-fdxplt
Add reusable genetic optimizer framework
2 parents 1402fb4 + a8aa29b commit 61221de

5 files changed

Lines changed: 523 additions & 0 deletions

File tree

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,15 @@ rustdoc-args = ["--cfg", "docsrs"]
4343
[dependencies]
4444
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
4545
thiserror = "1.0"
46+
rand = { version = "0.8", default-features = false, features = ["std"] }
4647

4748
[dev-dependencies]
4849
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
50+
anyhow = "1"
51+
rayon = "1.10"
52+
rand = "0.8"
53+
tokio = { version = "1", features = ["full"] }
54+
hyperliquid_rust_sdk = { git = "https://github.com/hyperliquid-dex/hyperliquid-rust-sdk" }
4955

5056
[[example]]
5157
name = "mode_reporting_example"

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ A comprehensive Rust library that integrates Hyperliquid trading data with the r
2424
- 🎯 **Risk Management**: Advanced risk controls and position management
2525
- 📊 **Unified Data Interface**: Consistent API across different trading modes
2626
- 🔔 **Alert System**: Configurable alerts for market conditions and performance metrics
27+
- 🧬 **Genetic Optimization**: Built-in GA framework for tuning strategy hyperparameters
2728

2829
## 📦 Installation
2930

@@ -154,6 +155,23 @@ async fn main() -> Result<(), HyperliquidBacktestError> {
154155
}
155156
```
156157

158+
### Genetic Hyperparameter Optimization
159+
160+
The crate ships with a reusable genetic algorithm engine that can search strategy
161+
parameters for you. Implement the [`Genome`](https://docs.rs/hyperliquid-backtest/latest/hyperliquid_backtest/optimization/trait.Genome.html)
162+
trait for your configuration, provide an evaluation function, and let the
163+
optimizer explore the space. The bundled example downloads real candles via the
164+
official SDK and tunes the SMA crossover strategy end-to-end:
165+
166+
```bash
167+
cargo run --example ga_optimize
168+
```
169+
170+
The optimizer reports the best candidate per generation together with the
171+
metrics returned by your evaluator. This makes it easy to compare fitness
172+
scores, inspect Sharpe/return/drawdown trade-offs, or integrate a custom
173+
scoring function.
174+
157175
### Real-Time Monitoring Example
158176

159177
```rust

examples/ga_optimize.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//! Genetic algorithm example built on the reusable optimization framework.
2+
//!
3+
//! This example demonstrates how strategies can express their parameters via the
4+
//! [`Genome`](hyperliquid_backtest::optimization::Genome) trait and plug into the
5+
//! [`GeneticOptimizer`](hyperliquid_backtest::optimization::GeneticOptimizer).
6+
//! Instead of running a full backtest we rely on a synthetic scoring function to
7+
//! keep the example lightweight and deterministic.
8+
9+
use anyhow::Result;
10+
use hyperliquid_backtest::optimization::{
11+
FitnessEvaluator, GeneticOptimizer, GeneticOptimizerConfig, Genome, OptimizationOutcome,
12+
};
13+
use rand::rngs::StdRng;
14+
use rand::{Rng, SeedableRng};
15+
16+
/// Strategy parameters (our genome).
17+
#[derive(Clone, Debug)]
18+
struct SmaParams {
19+
fast: u32,
20+
slow: u32,
21+
risk: f64,
22+
}
23+
24+
impl Genome for SmaParams {
25+
fn random(rng: &mut dyn rand::RngCore) -> Self {
26+
let mut fast = rng.gen_range(5..=40);
27+
let mut slow = rng.gen_range(20..=160);
28+
if slow <= fast {
29+
slow = fast + 5;
30+
}
31+
let risk = rng.gen_range(0.2..=2.0);
32+
Self { fast, slow, risk }
33+
}
34+
35+
fn mutate(&mut self, rng: &mut dyn rand::RngCore) {
36+
if rng.gen_bool(0.4) {
37+
let delta: i32 = rng.gen_range(-3..=3);
38+
let new_fast = (self.fast as i32 + delta).clamp(5, 60);
39+
self.fast = new_fast as u32;
40+
}
41+
if rng.gen_bool(0.4) {
42+
let delta: i32 = rng.gen_range(-8..=8);
43+
let new_slow = (self.slow as i32 + delta).clamp(10, 200);
44+
self.slow = new_slow as u32;
45+
}
46+
if self.slow <= self.fast {
47+
self.slow = self.fast + 5;
48+
}
49+
if rng.gen_bool(0.3) {
50+
let delta = rng.gen_range(-0.2..=0.2);
51+
self.risk = (self.risk + delta).clamp(0.1, 3.0);
52+
}
53+
}
54+
55+
fn crossover(&self, other: &Self, rng: &mut dyn rand::RngCore) -> Self {
56+
let fast = if rng.gen_bool(0.5) {
57+
self.fast
58+
} else {
59+
other.fast
60+
};
61+
let slow = if rng.gen_bool(0.5) {
62+
self.slow
63+
} else {
64+
other.slow
65+
};
66+
let risk = if rng.gen_bool(0.5) {
67+
self.risk
68+
} else {
69+
other.risk
70+
};
71+
let mut child = Self { fast, slow, risk };
72+
if child.slow <= child.fast {
73+
child.slow = child.fast + 5;
74+
}
75+
child
76+
}
77+
}
78+
79+
/// Synthetic metrics returned by the evaluator.
80+
#[derive(Clone, Debug)]
81+
struct StrategyMetrics {
82+
total_return: f64,
83+
sharpe_ratio: f64,
84+
max_drawdown: f64,
85+
}
86+
87+
/// Deterministic evaluator that mimics a backtest result.
88+
struct SyntheticEvaluator;
89+
90+
impl FitnessEvaluator<SmaParams> for SyntheticEvaluator {
91+
type Metrics = StrategyMetrics;
92+
93+
fn evaluate(
94+
&self,
95+
candidate: &SmaParams,
96+
) -> Result<OptimizationOutcome<Self::Metrics>, Box<dyn std::error::Error + Send + Sync>> {
97+
let fast = candidate.fast as f64;
98+
let slow = candidate.slow as f64;
99+
let ratio = fast / slow;
100+
101+
// Synthetic objective components.
102+
let total_return = 0.05 + 0.6 * (-(fast - 18.0).powi(2) / 600.0).exp();
103+
let sharpe = 1.0 + 0.8 * (-(slow - 90.0).powi(2) / 8000.0).exp();
104+
let drawdown_penalty = 0.12 + 0.5 * (ratio - 0.25).abs();
105+
let risk_penalty = (candidate.risk - 1.2).abs() * 0.1;
106+
107+
let fitness = total_return * 0.7 + sharpe * 0.4 - drawdown_penalty * 0.8 - risk_penalty;
108+
109+
Ok(OptimizationOutcome {
110+
fitness,
111+
metrics: StrategyMetrics {
112+
total_return,
113+
sharpe_ratio: sharpe,
114+
max_drawdown: drawdown_penalty,
115+
},
116+
})
117+
}
118+
}
119+
120+
fn main() -> Result<()> {
121+
let config = GeneticOptimizerConfig {
122+
population_size: 48,
123+
elitism: 4,
124+
generations: 20,
125+
tournament_size: 3,
126+
};
127+
128+
let optimizer = GeneticOptimizer::new(config, SyntheticEvaluator);
129+
let mut rng = StdRng::seed_from_u64(42);
130+
let result = optimizer.run(&mut rng)?;
131+
132+
println!("Best candidate: {:?}", result.best_candidate);
133+
println!(
134+
"Metrics: return={:.4}, sharpe={:.4}, max_dd={:.4}",
135+
result.best_metrics.total_return,
136+
result.best_metrics.sharpe_ratio,
137+
result.best_metrics.max_drawdown
138+
);
139+
println!("Fitness: {:.4}", result.best_fitness);
140+
141+
for generation in result.generations {
142+
println!(
143+
"Generation {:>2}: best={:.4}, avg={:.4}",
144+
generation.index, generation.best_fitness, generation.average_fitness
145+
);
146+
}
147+
148+
Ok(())
149+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
//! quickly and remain easy to understand.
88
99
pub mod backtest;
10+
pub mod optimization;
1011
pub mod risk_manager;
1112
pub mod unified_data;
1213

0 commit comments

Comments
 (0)