1use crate::constraints::RebalanceConstraints;
7use crate::objective::ObjectiveFunction;
8use crate::optimizer::OptimizationConfig;
9use rust_decimal::Decimal;
10use rust_decimal::prelude::{FromPrimitive, ToPrimitive};
11
12#[derive(Debug, Clone)]
14pub struct ThresholdParams {
15 pub price_threshold: Decimal,
17 pub il_threshold: Decimal,
19 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(), il_threshold: Decimal::from_f64(0.03).unwrap(), rebalance_on_out_of_range: true,
29 }
30 }
31}
32
33#[derive(Debug, Clone)]
35pub struct PeriodicParams {
36 pub interval: u64,
38 pub only_when_out_of_range: bool,
40}
41
42impl Default for PeriodicParams {
43 fn default() -> Self {
44 Self {
45 interval: 24, only_when_out_of_range: false,
47 }
48 }
49}
50
51#[derive(Debug, Clone)]
53pub struct ILLimitParams {
54 pub max_il: Decimal,
56 pub close_il: Option<Decimal>,
58 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(), close_il: Some(Decimal::from_f64(0.10).unwrap()), grace_period: 0,
68 }
69 }
70}
71
72#[derive(Debug, Clone)]
74pub struct ParameterOptimizationResult {
75 pub threshold_params: Option<ThresholdParams>,
77 pub periodic_params: Option<PeriodicParams>,
79 pub il_limit_params: Option<ILLimitParams>,
81 pub expected_fees: Decimal,
83 pub expected_il: Decimal,
85 pub expected_net_pnl: Decimal,
87 pub expected_rebalances: u32,
89 pub score: Decimal,
91}
92
93#[derive(Debug, Clone)]
95pub struct ParameterOptimizer {
96 pub constraints: RebalanceConstraints,
98 price_thresholds: Vec<Decimal>,
100 il_thresholds: Vec<Decimal>,
102 intervals: Vec<u64>,
104}
105
106impl Default for ParameterOptimizer {
107 fn default() -> Self {
108 Self::new()
109 }
110}
111
112impl ParameterOptimizer {
113 #[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], }
128 }
129
130 #[must_use]
132 pub fn with_price_thresholds(mut self, thresholds: Vec<Decimal>) -> Self {
133 self.price_thresholds = thresholds;
134 self
135 }
136
137 #[must_use]
139 pub fn with_il_thresholds(mut self, thresholds: Vec<Decimal>) -> Self {
140 self.il_thresholds = thresholds;
141 self
142 }
143
144 #[must_use]
146 pub fn with_intervals(mut self, intervals: Vec<u64>) -> Self {
147 self.intervals = intervals;
148 self
149 }
150
151 #[must_use]
153 pub fn with_constraints(mut self, constraints: RebalanceConstraints) -> Self {
154 self.constraints = constraints;
155 self
156 }
157
158 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(¶ms, 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 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 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(¶ms, 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 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 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(¶ms, 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 fn estimate_threshold_performance(
310 &self,
311 params: &ThresholdParams,
312 config: &OptimizationConfig,
313 range_width: Decimal,
314 ) -> (Decimal, Decimal, u32) {
315 let vol_dec = Decimal::from_f64(config.volatility).unwrap_or(Decimal::ZERO);
317 let steps = config.simulation_steps as u32;
318
319 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 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 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 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 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 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 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 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#[derive(Debug, Clone)]
400pub struct ThresholdCandidate {
401 pub params: ThresholdParams,
403 pub expected_fees: Decimal,
405 pub expected_il: Decimal,
407 pub expected_rebalances: u32,
409 pub score: Decimal,
411}
412
413#[derive(Debug, Clone)]
415pub struct PeriodicCandidate {
416 pub params: PeriodicParams,
418 pub expected_fees: Decimal,
420 pub expected_il: Decimal,
422 pub expected_rebalances: u32,
424 pub score: Decimal,
426}
427
428#[derive(Debug, Clone)]
430pub struct ILLimitCandidate {
431 pub params: ILLimitParams,
433 pub expected_fees: Decimal,
435 pub expected_il: Decimal,
437 pub expected_rebalances: u32,
439 pub score: Decimal,
441}
442
443fn 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 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}