1use crate::constraints::OptimizationConstraints;
7use crate::objective::ObjectiveFunction;
8use rust_decimal::Decimal;
9use rust_decimal::prelude::FromPrimitive;
10use std::cmp::Ordering;
11
12#[derive(Debug, Clone)]
14pub struct CandidateResult {
15 pub range_width: Decimal,
17 pub expected_fees: Decimal,
19 pub expected_il: Decimal,
21 pub net_pnl: Decimal,
23 pub time_in_range: Decimal,
25 pub score: Decimal,
27}
28
29impl CandidateResult {
30 #[must_use]
32 pub fn new(
33 range_width: Decimal,
34 expected_fees: Decimal,
35 expected_il: Decimal,
36 net_pnl: Decimal,
37 time_in_range: Decimal,
38 score: Decimal,
39 ) -> Self {
40 Self {
41 range_width,
42 expected_fees,
43 expected_il,
44 net_pnl,
45 time_in_range,
46 score,
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
53pub struct GridSearchOptimizer {
54 pub range_widths: Vec<Decimal>,
56 pub constraints: OptimizationConstraints,
58}
59
60impl Default for GridSearchOptimizer {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl GridSearchOptimizer {
67 #[must_use]
69 pub fn new() -> Self {
70 let widths = vec![0.01, 0.02, 0.05, 0.10, 0.15, 0.20, 0.30, 0.50];
72 Self {
73 range_widths: widths.into_iter().filter_map(Decimal::from_f64).collect(),
74 constraints: OptimizationConstraints::default(),
75 }
76 }
77
78 #[must_use]
80 pub fn with_widths(widths: Vec<Decimal>) -> Self {
81 Self {
82 range_widths: widths,
83 constraints: OptimizationConstraints::default(),
84 }
85 }
86
87 #[must_use]
89 pub fn with_constraints(mut self, constraints: OptimizationConstraints) -> Self {
90 self.constraints = constraints;
91 self
92 }
93
94 #[must_use]
96 pub fn generate_grid(min: Decimal, max: Decimal, steps: usize) -> Vec<Decimal> {
97 if steps < 2 {
98 return vec![min];
99 }
100
101 let step_size = (max - min) / Decimal::from(steps - 1);
102 (0..steps)
103 .map(|i| min + step_size * Decimal::from(i))
104 .collect()
105 }
106
107 #[must_use]
109 pub fn filter_valid_widths(&self) -> Vec<Decimal> {
110 self.range_widths
111 .iter()
112 .copied()
113 .filter(|w| self.constraints.position.is_valid_range_width(*w))
114 .collect()
115 }
116
117 pub fn rank_candidates(candidates: &mut [CandidateResult]) {
119 candidates.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(Ordering::Equal));
120 }
121
122 #[must_use]
124 pub fn top_n(candidates: &[CandidateResult], n: usize) -> Vec<CandidateResult> {
125 candidates.iter().take(n).cloned().collect()
126 }
127}
128
129#[derive(Debug, Clone)]
131pub struct OptimizationConfig {
132 pub mc_iterations: usize,
134 pub simulation_steps: usize,
136 pub time_step_years: f64,
138 pub volatility: f64,
140 pub drift: f64,
142 pub current_price: Decimal,
144 pub pool_liquidity: u128,
146 pub fee_rate: Decimal,
148 pub tx_cost: Decimal,
150}
151
152impl Default for OptimizationConfig {
153 fn default() -> Self {
154 Self {
155 mc_iterations: 100,
156 simulation_steps: 30,
157 time_step_years: 1.0 / 365.0, volatility: 0.5, drift: 0.0,
160 current_price: Decimal::from(100),
161 pool_liquidity: 1_000_000_000,
162 fee_rate: Decimal::from_f64(0.003).unwrap(),
163 tx_cost: Decimal::from_f64(0.001).unwrap(),
164 }
165 }
166}
167
168impl OptimizationConfig {
169 #[must_use]
171 pub fn new() -> Self {
172 Self::default()
173 }
174
175 #[must_use]
177 pub fn with_iterations(mut self, iterations: usize) -> Self {
178 self.mc_iterations = iterations;
179 self
180 }
181
182 #[must_use]
184 pub fn with_steps(mut self, steps: usize) -> Self {
185 self.simulation_steps = steps;
186 self
187 }
188
189 #[must_use]
191 pub fn with_volatility(mut self, volatility: f64) -> Self {
192 self.volatility = volatility;
193 self
194 }
195
196 #[must_use]
198 pub fn with_price(mut self, price: Decimal) -> Self {
199 self.current_price = price;
200 self
201 }
202
203 #[must_use]
205 pub fn with_fee_rate(mut self, fee_rate: Decimal) -> Self {
206 self.fee_rate = fee_rate;
207 self
208 }
209}
210
211pub trait Optimizer {
213 fn optimize<O: ObjectiveFunction>(
215 &self,
216 config: &OptimizationConfig,
217 objective: &O,
218 ) -> Vec<CandidateResult>;
219
220 fn best<O: ObjectiveFunction>(
222 &self,
223 config: &OptimizationConfig,
224 objective: &O,
225 ) -> Option<CandidateResult> {
226 self.optimize(config, objective).into_iter().next()
227 }
228}
229
230#[derive(Debug, Clone, Default)]
234pub struct AnalyticalOptimizer {
235 pub range_widths: Vec<Decimal>,
237 pub constraints: OptimizationConstraints,
239}
240
241impl AnalyticalOptimizer {
242 #[must_use]
244 pub fn new() -> Self {
245 let widths = vec![0.01, 0.02, 0.05, 0.10, 0.15, 0.20, 0.30, 0.50];
246 Self {
247 range_widths: widths.into_iter().filter_map(Decimal::from_f64).collect(),
248 constraints: OptimizationConstraints::default(),
249 }
250 }
251
252 #[must_use]
256 pub fn estimate_fees(
257 &self,
258 width: Decimal,
259 config: &OptimizationConfig,
260 time_in_range: Decimal,
261 ) -> Decimal {
262 let volume_factor = Decimal::from(config.pool_liquidity) / Decimal::from(1_000_000_000u64);
265 let width_factor = if width.is_zero() {
266 Decimal::ONE
267 } else {
268 Decimal::ONE / width
269 };
270
271 let steps = Decimal::from(config.simulation_steps);
272 let base_fees = config.fee_rate * volume_factor * steps;
273
274 base_fees * width_factor * time_in_range / Decimal::from(100)
275 }
276
277 #[must_use]
281 pub fn estimate_il(&self, width: Decimal, volatility: f64) -> Decimal {
282 let vol_dec = Decimal::from_f64(volatility).unwrap_or(Decimal::ZERO);
285 let vol_squared = vol_dec * vol_dec;
286
287 if width.is_zero() {
288 return vol_squared;
289 }
290
291 vol_squared / width / Decimal::from(10)
293 }
294
295 #[must_use]
297 pub fn estimate_time_in_range(&self, width: Decimal, volatility: f64) -> Decimal {
298 let vol_factor = Decimal::from_f64(1.0 - volatility.min(0.9)).unwrap_or(Decimal::ONE);
302 let width_factor = width * Decimal::from(2); let base_time = Decimal::from(50); let time_in_range = base_time + (width_factor * Decimal::from(100) * vol_factor);
306
307 time_in_range.min(Decimal::from(100))
308 }
309}
310
311impl Optimizer for AnalyticalOptimizer {
312 fn optimize<O: ObjectiveFunction>(
313 &self,
314 config: &OptimizationConfig,
315 objective: &O,
316 ) -> Vec<CandidateResult> {
317 let mut candidates: Vec<CandidateResult> = self
318 .range_widths
319 .iter()
320 .filter(|w| self.constraints.position.is_valid_range_width(**w))
321 .map(|&width| {
322 let time_in_range = self.estimate_time_in_range(width, config.volatility);
323 let fees = self.estimate_fees(width, config, time_in_range);
324 let il = self.estimate_il(width, config.volatility);
325 let net_pnl = fees - il;
326
327 let sim_result =
329 clmm_lp_domain::value_objects::simulation_result::SimulationResult {
330 final_position_value: Decimal::ZERO,
331 total_fees_earned: fees,
332 total_il: il,
333 net_pnl,
334 max_drawdown: il,
335 time_in_range_percentage: time_in_range,
336 sharpe_ratio: None,
337 };
338
339 let score = objective.evaluate(&sim_result);
340
341 CandidateResult::new(width, fees, il, net_pnl, time_in_range, score)
342 })
343 .collect();
344
345 GridSearchOptimizer::rank_candidates(&mut candidates);
346 candidates
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::objective::MaximizeNetPnL;
354
355 #[test]
356 fn test_grid_search_optimizer_creation() {
357 let optimizer = GridSearchOptimizer::new();
358 assert!(!optimizer.range_widths.is_empty());
359 }
360
361 #[test]
362 fn test_generate_grid() {
363 let grid = GridSearchOptimizer::generate_grid(
364 Decimal::from_f64(0.01).unwrap(),
365 Decimal::from_f64(0.10).unwrap(),
366 10,
367 );
368 assert_eq!(grid.len(), 10);
369 assert_eq!(grid[0], Decimal::from_f64(0.01).unwrap());
370 assert_eq!(grid[9], Decimal::from_f64(0.10).unwrap());
371 }
372
373 #[test]
374 fn test_filter_valid_widths() {
375 let optimizer = GridSearchOptimizer::new();
376 let valid = optimizer.filter_valid_widths();
377
378 assert!(!valid.is_empty());
380 for w in &valid {
381 assert!(optimizer.constraints.position.is_valid_range_width(*w));
382 }
383 }
384
385 #[test]
386 fn test_rank_candidates() {
387 let mut candidates = vec![
388 CandidateResult::new(
389 Decimal::from_f64(0.10).unwrap(),
390 Decimal::from(10),
391 Decimal::from(2),
392 Decimal::from(8),
393 Decimal::from(70),
394 Decimal::from(8),
395 ),
396 CandidateResult::new(
397 Decimal::from_f64(0.05).unwrap(),
398 Decimal::from(15),
399 Decimal::from(5),
400 Decimal::from(10),
401 Decimal::from(60),
402 Decimal::from(10),
403 ),
404 CandidateResult::new(
405 Decimal::from_f64(0.20).unwrap(),
406 Decimal::from(5),
407 Decimal::from(1),
408 Decimal::from(4),
409 Decimal::from(80),
410 Decimal::from(4),
411 ),
412 ];
413
414 GridSearchOptimizer::rank_candidates(&mut candidates);
415
416 assert_eq!(candidates[0].score, Decimal::from(10));
417 assert_eq!(candidates[1].score, Decimal::from(8));
418 assert_eq!(candidates[2].score, Decimal::from(4));
419 }
420
421 #[test]
422 fn test_analytical_optimizer() {
423 let optimizer = AnalyticalOptimizer::new();
424 let config = OptimizationConfig::new()
425 .with_iterations(10)
426 .with_steps(30)
427 .with_volatility(0.3);
428
429 let candidates = optimizer.optimize(&config, &MaximizeNetPnL);
430
431 assert!(!candidates.is_empty());
432 for i in 1..candidates.len() {
434 assert!(candidates[i - 1].score >= candidates[i].score);
435 }
436 }
437
438 #[test]
439 fn test_analytical_optimizer_best() {
440 let optimizer = AnalyticalOptimizer::new();
441 let config = OptimizationConfig::default();
442
443 let best = optimizer.best(&config, &MaximizeNetPnL);
444 assert!(best.is_some());
445 }
446
447 #[test]
448 fn test_optimization_config_builder() {
449 let config = OptimizationConfig::new()
450 .with_iterations(50)
451 .with_steps(60)
452 .with_volatility(0.4)
453 .with_price(Decimal::from(150));
454
455 assert_eq!(config.mc_iterations, 50);
456 assert_eq!(config.simulation_steps, 60);
457 assert_eq!(config.volatility, 0.4);
458 assert_eq!(config.current_price, Decimal::from(150));
459 }
460}