Skip to main content

clmm_lp_optimization/
parameter_optimizer.rs

1//! Parameter optimization for rebalancing strategies.
2//!
3//! This module provides optimization for strategy parameters such as
4//! rebalancing thresholds, intervals, and IL limits.
5
6use crate::constraints::RebalanceConstraints;
7use crate::objective::ObjectiveFunction;
8use crate::optimizer::OptimizationConfig;
9use rust_decimal::Decimal;
10use rust_decimal::prelude::{FromPrimitive, ToPrimitive};
11
12/// Parameters for a threshold-based rebalancing strategy.
13#[derive(Debug, Clone)]
14pub struct ThresholdParams {
15    /// Price deviation threshold to trigger rebalance.
16    pub price_threshold: Decimal,
17    /// IL threshold to trigger rebalance.
18    pub il_threshold: Decimal,
19    /// Whether to rebalance when out of range.
20    pub rebalance_on_out_of_range: bool,
21}
22
23impl Default for ThresholdParams {
24    fn default() -> Self {
25        Self {
26            price_threshold: Decimal::from_f64(0.05).unwrap(), // 5%
27            il_threshold: Decimal::from_f64(0.03).unwrap(),    // 3%
28            rebalance_on_out_of_range: true,
29        }
30    }
31}
32
33/// Parameters for a periodic rebalancing strategy.
34#[derive(Debug, Clone)]
35pub struct PeriodicParams {
36    /// Interval between rebalances in steps.
37    pub interval: u64,
38    /// Whether to only rebalance when out of range.
39    pub only_when_out_of_range: bool,
40}
41
42impl Default for PeriodicParams {
43    fn default() -> Self {
44        Self {
45            interval: 24, // Daily
46            only_when_out_of_range: false,
47        }
48    }
49}
50
51/// Parameters for an IL-limit strategy.
52#[derive(Debug, Clone)]
53pub struct ILLimitParams {
54    /// Maximum IL before rebalancing.
55    pub max_il: Decimal,
56    /// Maximum IL before closing position.
57    pub close_il: Option<Decimal>,
58    /// Grace period in steps before acting.
59    pub grace_period: u64,
60}
61
62impl Default for ILLimitParams {
63    fn default() -> Self {
64        Self {
65            max_il: Decimal::from_f64(0.05).unwrap(),         // 5%
66            close_il: Some(Decimal::from_f64(0.10).unwrap()), // 10%
67            grace_period: 0,
68        }
69    }
70}
71
72/// Result of parameter optimization.
73#[derive(Debug, Clone)]
74pub struct ParameterOptimizationResult {
75    /// Best threshold parameters found.
76    pub threshold_params: Option<ThresholdParams>,
77    /// Best periodic parameters found.
78    pub periodic_params: Option<PeriodicParams>,
79    /// Best IL limit parameters found.
80    pub il_limit_params: Option<ILLimitParams>,
81    /// Expected performance metrics.
82    pub expected_fees: Decimal,
83    /// Expected IL.
84    pub expected_il: Decimal,
85    /// Expected net PnL.
86    pub expected_net_pnl: Decimal,
87    /// Number of expected rebalances.
88    pub expected_rebalances: u32,
89    /// Score from objective function.
90    pub score: Decimal,
91}
92
93/// Optimizer for rebalancing strategy parameters.
94#[derive(Debug, Clone)]
95pub struct ParameterOptimizer {
96    /// Constraints for parameter search.
97    pub constraints: RebalanceConstraints,
98    /// Grid of price thresholds to search.
99    price_thresholds: Vec<Decimal>,
100    /// Grid of IL thresholds to search.
101    il_thresholds: Vec<Decimal>,
102    /// Grid of intervals to search.
103    intervals: Vec<u64>,
104}
105
106impl Default for ParameterOptimizer {
107    fn default() -> Self {
108        Self::new()
109    }
110}
111
112impl ParameterOptimizer {
113    /// Creates a new parameter optimizer with default grids.
114    #[must_use]
115    pub fn new() -> Self {
116        Self {
117            constraints: RebalanceConstraints::default(),
118            price_thresholds: vec![0.02, 0.03, 0.05, 0.07, 0.10, 0.15]
119                .into_iter()
120                .filter_map(Decimal::from_f64)
121                .collect(),
122            il_thresholds: vec![0.01, 0.02, 0.03, 0.05, 0.07, 0.10]
123                .into_iter()
124                .filter_map(Decimal::from_f64)
125                .collect(),
126            intervals: vec![6, 12, 24, 48, 72, 168], // 6h to 1 week
127        }
128    }
129
130    /// Sets custom price threshold grid.
131    #[must_use]
132    pub fn with_price_thresholds(mut self, thresholds: Vec<Decimal>) -> Self {
133        self.price_thresholds = thresholds;
134        self
135    }
136
137    /// Sets custom IL threshold grid.
138    #[must_use]
139    pub fn with_il_thresholds(mut self, thresholds: Vec<Decimal>) -> Self {
140        self.il_thresholds = thresholds;
141        self
142    }
143
144    /// Sets custom interval grid.
145    #[must_use]
146    pub fn with_intervals(mut self, intervals: Vec<u64>) -> Self {
147        self.intervals = intervals;
148        self
149    }
150
151    /// Sets constraints.
152    #[must_use]
153    pub fn with_constraints(mut self, constraints: RebalanceConstraints) -> Self {
154        self.constraints = constraints;
155        self
156    }
157
158    /// Optimizes threshold strategy parameters.
159    pub fn optimize_threshold<O: ObjectiveFunction>(
160        &self,
161        config: &OptimizationConfig,
162        range_width: Decimal,
163        objective: &O,
164    ) -> Vec<ThresholdCandidate> {
165        let mut candidates = Vec::new();
166
167        for &price_threshold in &self.price_thresholds {
168            if !self.constraints.is_valid_price_threshold(price_threshold) {
169                continue;
170            }
171
172            for &il_threshold in &self.il_thresholds {
173                if !self.constraints.is_valid_il_threshold(il_threshold) {
174                    continue;
175                }
176
177                for rebalance_on_oor in [true, false] {
178                    let params = ThresholdParams {
179                        price_threshold,
180                        il_threshold,
181                        rebalance_on_out_of_range: rebalance_on_oor,
182                    };
183
184                    let result = self.estimate_threshold_performance(&params, config, range_width);
185
186                    let sim_result = create_sim_result(&result);
187                    let score = objective.evaluate(&sim_result);
188
189                    candidates.push(ThresholdCandidate {
190                        params,
191                        expected_fees: result.0,
192                        expected_il: result.1,
193                        expected_rebalances: result.2,
194                        score,
195                    });
196                }
197            }
198        }
199
200        // Sort by score descending
201        candidates.sort_by(|a, b| {
202            b.score
203                .partial_cmp(&a.score)
204                .unwrap_or(std::cmp::Ordering::Equal)
205        });
206
207        candidates
208    }
209
210    /// Optimizes periodic strategy parameters.
211    pub fn optimize_periodic<O: ObjectiveFunction>(
212        &self,
213        config: &OptimizationConfig,
214        range_width: Decimal,
215        objective: &O,
216    ) -> Vec<PeriodicCandidate> {
217        let mut candidates = Vec::new();
218
219        for &interval in &self.intervals {
220            if !self.constraints.is_valid_interval(interval) {
221                continue;
222            }
223
224            for only_oor in [true, false] {
225                let params = PeriodicParams {
226                    interval,
227                    only_when_out_of_range: only_oor,
228                };
229
230                let result = self.estimate_periodic_performance(&params, config, range_width);
231
232                let sim_result = create_sim_result(&result);
233                let score = objective.evaluate(&sim_result);
234
235                candidates.push(PeriodicCandidate {
236                    params,
237                    expected_fees: result.0,
238                    expected_il: result.1,
239                    expected_rebalances: result.2,
240                    score,
241                });
242            }
243        }
244
245        candidates.sort_by(|a, b| {
246            b.score
247                .partial_cmp(&a.score)
248                .unwrap_or(std::cmp::Ordering::Equal)
249        });
250
251        candidates
252    }
253
254    /// Optimizes IL limit strategy parameters.
255    pub fn optimize_il_limit<O: ObjectiveFunction>(
256        &self,
257        config: &OptimizationConfig,
258        range_width: Decimal,
259        objective: &O,
260    ) -> Vec<ILLimitCandidate> {
261        let mut candidates = Vec::new();
262
263        for &max_il in &self.il_thresholds {
264            if !self.constraints.is_valid_il_threshold(max_il) {
265                continue;
266            }
267
268            // Close IL options: None, 2x max_il, 3x max_il
269            let close_options = vec![
270                None,
271                Some(max_il * Decimal::from(2)),
272                Some(max_il * Decimal::from(3)),
273            ];
274
275            for close_il in close_options {
276                for grace_period in [0, 3, 6] {
277                    let params = ILLimitParams {
278                        max_il,
279                        close_il,
280                        grace_period,
281                    };
282
283                    let result = self.estimate_il_limit_performance(&params, config, range_width);
284
285                    let sim_result = create_sim_result(&result);
286                    let score = objective.evaluate(&sim_result);
287
288                    candidates.push(ILLimitCandidate {
289                        params,
290                        expected_fees: result.0,
291                        expected_il: result.1,
292                        expected_rebalances: result.2,
293                        score,
294                    });
295                }
296            }
297        }
298
299        candidates.sort_by(|a, b| {
300            b.score
301                .partial_cmp(&a.score)
302                .unwrap_or(std::cmp::Ordering::Equal)
303        });
304
305        candidates
306    }
307
308    /// Estimates performance for threshold strategy.
309    fn estimate_threshold_performance(
310        &self,
311        params: &ThresholdParams,
312        config: &OptimizationConfig,
313        range_width: Decimal,
314    ) -> (Decimal, Decimal, u32) {
315        // Estimate number of rebalances based on volatility and thresholds
316        let vol_dec = Decimal::from_f64(config.volatility).unwrap_or(Decimal::ZERO);
317        let steps = config.simulation_steps as u32;
318
319        // Higher volatility + lower threshold = more rebalances
320        let rebalance_rate = vol_dec / params.price_threshold;
321        let expected_rebalances = (Decimal::from(steps) * rebalance_rate / Decimal::from(10))
322            .to_u32()
323            .unwrap_or(0)
324            .min(steps);
325
326        // Fees: more rebalances = more time in optimal range = more fees
327        let time_in_range =
328            estimate_time_in_range(range_width, config.volatility, expected_rebalances);
329        let base_fees = estimate_base_fees(config, range_width, time_in_range);
330
331        // IL: rebalancing reduces IL but costs tx fees
332        let base_il = estimate_base_il(range_width, config.volatility);
333        let il_reduction = Decimal::from(expected_rebalances) * Decimal::from_f64(0.01).unwrap();
334        let effective_il = (base_il - il_reduction).max(Decimal::ZERO);
335
336        // Subtract tx costs
337        let tx_costs = Decimal::from(expected_rebalances) * config.tx_cost;
338        let net_fees = base_fees - tx_costs;
339
340        (net_fees, effective_il, expected_rebalances)
341    }
342
343    /// Estimates performance for periodic strategy.
344    fn estimate_periodic_performance(
345        &self,
346        params: &PeriodicParams,
347        config: &OptimizationConfig,
348        range_width: Decimal,
349    ) -> (Decimal, Decimal, u32) {
350        let steps = config.simulation_steps as u32;
351        let expected_rebalances = steps / params.interval as u32;
352
353        let time_in_range =
354            estimate_time_in_range(range_width, config.volatility, expected_rebalances);
355        let base_fees = estimate_base_fees(config, range_width, time_in_range);
356
357        let base_il = estimate_base_il(range_width, config.volatility);
358        let il_reduction = Decimal::from(expected_rebalances) * Decimal::from_f64(0.008).unwrap();
359        let effective_il = (base_il - il_reduction).max(Decimal::ZERO);
360
361        let tx_costs = Decimal::from(expected_rebalances) * config.tx_cost;
362        let net_fees = base_fees - tx_costs;
363
364        (net_fees, effective_il, expected_rebalances)
365    }
366
367    /// Estimates performance for IL limit strategy.
368    fn estimate_il_limit_performance(
369        &self,
370        params: &ILLimitParams,
371        config: &OptimizationConfig,
372        range_width: Decimal,
373    ) -> (Decimal, Decimal, u32) {
374        let vol_dec = Decimal::from_f64(config.volatility).unwrap_or(Decimal::ZERO);
375        let steps = config.simulation_steps as u32;
376
377        // Rebalances triggered when IL exceeds threshold
378        let il_trigger_rate = vol_dec * vol_dec / params.max_il;
379        let expected_rebalances = (Decimal::from(steps) * il_trigger_rate / Decimal::from(5))
380            .to_u32()
381            .unwrap_or(0)
382            .min(steps);
383
384        let time_in_range =
385            estimate_time_in_range(range_width, config.volatility, expected_rebalances);
386        let base_fees = estimate_base_fees(config, range_width, time_in_range);
387
388        // IL is capped at max_il due to rebalancing
389        let effective_il = params.max_il;
390
391        let tx_costs = Decimal::from(expected_rebalances) * config.tx_cost;
392        let net_fees = base_fees - tx_costs;
393
394        (net_fees, effective_il, expected_rebalances)
395    }
396}
397
398/// Candidate result for threshold optimization.
399#[derive(Debug, Clone)]
400pub struct ThresholdCandidate {
401    /// The parameters.
402    pub params: ThresholdParams,
403    /// Expected fees.
404    pub expected_fees: Decimal,
405    /// Expected IL.
406    pub expected_il: Decimal,
407    /// Expected number of rebalances.
408    pub expected_rebalances: u32,
409    /// Objective score.
410    pub score: Decimal,
411}
412
413/// Candidate result for periodic optimization.
414#[derive(Debug, Clone)]
415pub struct PeriodicCandidate {
416    /// The parameters.
417    pub params: PeriodicParams,
418    /// Expected fees.
419    pub expected_fees: Decimal,
420    /// Expected IL.
421    pub expected_il: Decimal,
422    /// Expected number of rebalances.
423    pub expected_rebalances: u32,
424    /// Objective score.
425    pub score: Decimal,
426}
427
428/// Candidate result for IL limit optimization.
429#[derive(Debug, Clone)]
430pub struct ILLimitCandidate {
431    /// The parameters.
432    pub params: ILLimitParams,
433    /// Expected fees.
434    pub expected_fees: Decimal,
435    /// Expected IL.
436    pub expected_il: Decimal,
437    /// Expected number of rebalances.
438    pub expected_rebalances: u32,
439    /// Objective score.
440    pub score: Decimal,
441}
442
443// Helper functions
444
445fn estimate_time_in_range(width: Decimal, volatility: f64, rebalances: u32) -> Decimal {
446    let vol_factor = Decimal::from_f64(1.0 - volatility.min(0.9)).unwrap_or(Decimal::ONE);
447    let width_factor = width * Decimal::from(2);
448    let rebalance_bonus = Decimal::from(rebalances) * Decimal::from_f64(0.5).unwrap();
449
450    let base_time = Decimal::from(50);
451    let time = base_time + (width_factor * Decimal::from(100) * vol_factor) + rebalance_bonus;
452
453    time.min(Decimal::from(100))
454}
455
456fn estimate_base_fees(
457    config: &OptimizationConfig,
458    width: Decimal,
459    time_in_range: Decimal,
460) -> Decimal {
461    let volume_factor = Decimal::from(config.pool_liquidity) / Decimal::from(1_000_000_000u64);
462    let width_factor = if width.is_zero() {
463        Decimal::ONE
464    } else {
465        Decimal::ONE / width
466    };
467
468    let steps = Decimal::from(config.simulation_steps);
469    let base_fees = config.fee_rate * volume_factor * steps;
470
471    base_fees * width_factor * time_in_range / Decimal::from(100)
472}
473
474fn estimate_base_il(width: Decimal, volatility: f64) -> Decimal {
475    let vol_dec = Decimal::from_f64(volatility).unwrap_or(Decimal::ZERO);
476    let vol_squared = vol_dec * vol_dec;
477
478    if width.is_zero() {
479        return vol_squared;
480    }
481
482    vol_squared / width / Decimal::from(10)
483}
484
485fn create_sim_result(
486    result: &(Decimal, Decimal, u32),
487) -> clmm_lp_domain::value_objects::simulation_result::SimulationResult {
488    clmm_lp_domain::value_objects::simulation_result::SimulationResult {
489        final_position_value: Decimal::ZERO,
490        total_fees_earned: result.0,
491        total_il: result.1,
492        net_pnl: result.0 - result.1,
493        max_drawdown: result.1,
494        time_in_range_percentage: Decimal::ZERO,
495        sharpe_ratio: None,
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502    use crate::objective::MaximizeNetPnL;
503
504    #[test]
505    fn test_parameter_optimizer_creation() {
506        let optimizer = ParameterOptimizer::new();
507        assert!(!optimizer.price_thresholds.is_empty());
508        assert!(!optimizer.il_thresholds.is_empty());
509        assert!(!optimizer.intervals.is_empty());
510    }
511
512    #[test]
513    fn test_optimize_threshold() {
514        let optimizer = ParameterOptimizer::new();
515        let config = OptimizationConfig::default();
516        let range_width = Decimal::from_f64(0.10).unwrap();
517
518        let candidates = optimizer.optimize_threshold(&config, range_width, &MaximizeNetPnL);
519
520        assert!(!candidates.is_empty());
521        // Should be sorted by score descending
522        for i in 1..candidates.len() {
523            assert!(candidates[i - 1].score >= candidates[i].score);
524        }
525    }
526
527    #[test]
528    fn test_optimize_periodic() {
529        let optimizer = ParameterOptimizer::new();
530        let config = OptimizationConfig::default();
531        let range_width = Decimal::from_f64(0.10).unwrap();
532
533        let candidates = optimizer.optimize_periodic(&config, range_width, &MaximizeNetPnL);
534
535        assert!(!candidates.is_empty());
536        for i in 1..candidates.len() {
537            assert!(candidates[i - 1].score >= candidates[i].score);
538        }
539    }
540
541    #[test]
542    fn test_optimize_il_limit() {
543        let optimizer = ParameterOptimizer::new();
544        let config = OptimizationConfig::default();
545        let range_width = Decimal::from_f64(0.10).unwrap();
546
547        let candidates = optimizer.optimize_il_limit(&config, range_width, &MaximizeNetPnL);
548
549        assert!(!candidates.is_empty());
550        for i in 1..candidates.len() {
551            assert!(candidates[i - 1].score >= candidates[i].score);
552        }
553    }
554
555    #[test]
556    fn test_threshold_params_default() {
557        let params = ThresholdParams::default();
558        assert_eq!(params.price_threshold, Decimal::from_f64(0.05).unwrap());
559        assert!(params.rebalance_on_out_of_range);
560    }
561
562    #[test]
563    fn test_periodic_params_default() {
564        let params = PeriodicParams::default();
565        assert_eq!(params.interval, 24);
566    }
567
568    #[test]
569    fn test_il_limit_params_default() {
570        let params = ILLimitParams::default();
571        assert_eq!(params.max_il, Decimal::from_f64(0.05).unwrap());
572        assert!(params.close_il.is_some());
573    }
574}