Skip to main content

clmm_lp_optimization/
optimizer.rs

1//! Main optimization logic.
2//!
3//! This module provides the core optimization algorithms including
4//! grid search, and utilities for running optimization campaigns.
5
6use crate::constraints::OptimizationConstraints;
7use crate::objective::ObjectiveFunction;
8use rust_decimal::Decimal;
9use rust_decimal::prelude::FromPrimitive;
10use std::cmp::Ordering;
11
12/// Result of a single optimization candidate evaluation.
13#[derive(Debug, Clone)]
14pub struct CandidateResult {
15    /// The range width parameter.
16    pub range_width: Decimal,
17    /// Expected fees.
18    pub expected_fees: Decimal,
19    /// Expected IL.
20    pub expected_il: Decimal,
21    /// Net PnL.
22    pub net_pnl: Decimal,
23    /// Time in range percentage.
24    pub time_in_range: Decimal,
25    /// Objective score.
26    pub score: Decimal,
27}
28
29impl CandidateResult {
30    /// Creates a new candidate result.
31    #[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/// Grid search optimizer for finding optimal parameters.
52#[derive(Debug, Clone)]
53pub struct GridSearchOptimizer {
54    /// Grid of range widths to search.
55    pub range_widths: Vec<Decimal>,
56    /// Constraints to apply.
57    pub constraints: OptimizationConstraints,
58}
59
60impl Default for GridSearchOptimizer {
61    fn default() -> Self {
62        Self::new()
63    }
64}
65
66impl GridSearchOptimizer {
67    /// Creates a new grid search optimizer with default parameters.
68    #[must_use]
69    pub fn new() -> Self {
70        // Default grid: 1%, 2%, 5%, 10%, 15%, 20%, 30%, 50%
71        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    /// Creates a grid search optimizer with custom widths.
79    #[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    /// Sets the constraints.
88    #[must_use]
89    pub fn with_constraints(mut self, constraints: OptimizationConstraints) -> Self {
90        self.constraints = constraints;
91        self
92    }
93
94    /// Generates a fine-grained grid between min and max.
95    #[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    /// Filters candidates based on constraints.
108    #[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    /// Ranks candidates by score (descending).
118    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    /// Returns the top N candidates.
123    #[must_use]
124    pub fn top_n(candidates: &[CandidateResult], n: usize) -> Vec<CandidateResult> {
125        candidates.iter().take(n).cloned().collect()
126    }
127}
128
129/// Configuration for optimization runs.
130#[derive(Debug, Clone)]
131pub struct OptimizationConfig {
132    /// Number of Monte Carlo iterations.
133    pub mc_iterations: usize,
134    /// Number of simulation steps.
135    pub simulation_steps: usize,
136    /// Time step in years.
137    pub time_step_years: f64,
138    /// Volatility assumption.
139    pub volatility: f64,
140    /// Drift assumption.
141    pub drift: f64,
142    /// Current price.
143    pub current_price: Decimal,
144    /// Pool liquidity.
145    pub pool_liquidity: u128,
146    /// Fee rate.
147    pub fee_rate: Decimal,
148    /// Transaction cost per rebalance.
149    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, // 1 day
158            volatility: 0.5,              // 50% annual
159            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    /// Creates a new optimization config.
170    #[must_use]
171    pub fn new() -> Self {
172        Self::default()
173    }
174
175    /// Sets the Monte Carlo iterations.
176    #[must_use]
177    pub fn with_iterations(mut self, iterations: usize) -> Self {
178        self.mc_iterations = iterations;
179        self
180    }
181
182    /// Sets the simulation steps.
183    #[must_use]
184    pub fn with_steps(mut self, steps: usize) -> Self {
185        self.simulation_steps = steps;
186        self
187    }
188
189    /// Sets the volatility.
190    #[must_use]
191    pub fn with_volatility(mut self, volatility: f64) -> Self {
192        self.volatility = volatility;
193        self
194    }
195
196    /// Sets the current price.
197    #[must_use]
198    pub fn with_price(mut self, price: Decimal) -> Self {
199        self.current_price = price;
200        self
201    }
202
203    /// Sets the fee rate.
204    #[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
211/// Trait for optimization algorithms.
212pub trait Optimizer {
213    /// Runs the optimization and returns ranked candidates.
214    fn optimize<O: ObjectiveFunction>(
215        &self,
216        config: &OptimizationConfig,
217        objective: &O,
218    ) -> Vec<CandidateResult>;
219
220    /// Returns the best candidate.
221    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/// Simple analytical optimizer for quick estimates.
231///
232/// Uses simplified formulas instead of full Monte Carlo simulation.
233#[derive(Debug, Clone, Default)]
234pub struct AnalyticalOptimizer {
235    /// Grid of range widths to evaluate.
236    pub range_widths: Vec<Decimal>,
237    /// Constraints.
238    pub constraints: OptimizationConstraints,
239}
240
241impl AnalyticalOptimizer {
242    /// Creates a new analytical optimizer.
243    #[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    /// Estimates fees for a given range width.
253    ///
254    /// Narrower ranges earn more fees when in range but are in range less often.
255    #[must_use]
256    pub fn estimate_fees(
257        &self,
258        width: Decimal,
259        config: &OptimizationConfig,
260        time_in_range: Decimal,
261    ) -> Decimal {
262        // Fee estimation: fees ∝ (1/width) * time_in_range * volume * fee_rate
263        // Simplified: assume volume is proportional to pool_liquidity
264        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    /// Estimates IL for a given range width and volatility.
278    ///
279    /// Narrower ranges have higher IL when price moves out of range.
280    #[must_use]
281    pub fn estimate_il(&self, width: Decimal, volatility: f64) -> Decimal {
282        // IL estimation: IL ∝ volatility^2 / width
283        // This is a simplified approximation
284        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        // Scale IL by inverse of width (narrower = more IL)
292        vol_squared / width / Decimal::from(10)
293    }
294
295    /// Estimates time in range for a given width and volatility.
296    #[must_use]
297    pub fn estimate_time_in_range(&self, width: Decimal, volatility: f64) -> Decimal {
298        // Time in range estimation based on width and volatility
299        // Wider range = more time in range
300        // Higher volatility = less time in range
301        let vol_factor = Decimal::from_f64(1.0 - volatility.min(0.9)).unwrap_or(Decimal::ONE);
302        let width_factor = width * Decimal::from(2); // 10% width -> 20% factor
303
304        let base_time = Decimal::from(50); // 50% base
305        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                // Create a mock SimulationResult for the objective function
328                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        // All default widths should be valid with default constraints
379        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        // Candidates should be sorted by score descending
433        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}