clmm_lp_optimization/
range_optimizer.rs1use crate::objective::ObjectiveFunction;
2use clmm_lp_domain::entities::position::Position;
3use clmm_lp_domain::value_objects::OptimizationResult;
4use clmm_lp_domain::value_objects::price::Price;
5use clmm_lp_domain::value_objects::price_range::PriceRange;
6use clmm_lp_domain::value_objects::simulation_result::SimulationResult;
7use clmm_lp_simulation::liquidity::ConstantLiquidity;
8use clmm_lp_simulation::monte_carlo::MonteCarloRunner;
9use clmm_lp_simulation::volume::ConstantVolume;
10use rust_decimal::Decimal;
11use rust_decimal::prelude::*;
12
13pub struct RangeOptimizer {
15 pub iterations: usize,
17 pub steps: usize,
19 pub time_step: f64,
21}
22
23impl RangeOptimizer {
24 pub fn new(iterations: usize, steps: usize, time_step: f64) -> Self {
26 Self {
27 iterations,
28 steps,
29 time_step,
30 }
31 }
32
33 #[allow(clippy::too_many_arguments)]
35 pub fn optimize<O: ObjectiveFunction>(
36 &self,
37 base_position: Position,
38 current_price: Decimal,
39 volatility: f64,
40 drift: f64,
41 volume: ConstantVolume,
42 pool_liquidity: u128,
43 fee_rate: Decimal,
44 objective: O,
45 ) -> OptimizationResult {
46 let widths = vec![0.01, 0.02, 0.05, 0.10, 0.20, 0.50];
48
49 let mut best_result: Option<(SimulationResult, PriceRange)> = None;
50 let mut best_score = Decimal::MIN;
51
52 let _capital = Decimal::from(1000);
54 let liquidity_model = ConstantLiquidity::new(pool_liquidity);
55
56 for width in widths {
57 let lower_mult = Decimal::from_f64(1.0 - width).unwrap();
58 let upper_mult = Decimal::from_f64(1.0 + width).unwrap();
59
60 let lower_price = current_price * lower_mult;
61 let upper_price = current_price * upper_mult;
62
63 let range = PriceRange::new(Price::new(lower_price), Price::new(upper_price));
64
65 let width_dec = Decimal::from_f64(width).unwrap();
71 let liquidity_proxy = (Decimal::from(1000) / width_dec).to_u128().unwrap_or(1000);
72
73 let mut candidate_position = base_position.clone();
74 candidate_position.range = Some(range.clone());
75 candidate_position.liquidity_amount = liquidity_proxy;
76
77 let mut runner = MonteCarloRunner {
78 position: candidate_position,
79 volume_model: volume.clone(),
80 liquidity_model: liquidity_model.clone(),
81 fee_rate,
82 initial_price: current_price,
83 drift,
84 volatility,
85 time_step: self.time_step,
86 steps: self.steps,
87 iterations: self.iterations,
88 };
89
90 let agg_result = runner.run();
91
92 let sim_result = SimulationResult {
93 final_position_value: Decimal::ZERO,
94 total_fees_earned: agg_result.mean_fees,
95 total_il: agg_result.mean_il,
96 net_pnl: agg_result.mean_net_pnl,
97 max_drawdown: Decimal::ZERO,
98 time_in_range_percentage: Decimal::ZERO,
99 sharpe_ratio: None,
100 };
101
102 let score = objective.evaluate(&sim_result);
103
104 if score > best_score {
105 best_score = score;
106 best_result = Some((sim_result, range));
107 }
108 }
109
110 let (best_sim, best_range) = best_result.expect("No candidates evaluated");
111
112 OptimizationResult {
113 recommended_range: best_range,
114 expected_pnl: best_sim.net_pnl,
115 expected_fees: best_sim.total_fees_earned,
116 expected_il: best_sim.total_il,
117 sharpe_ratio: best_sim.sharpe_ratio,
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::objective::MaximizeNetPnL;
126 use clmm_lp_domain::entities::position::{Position, PositionId};
127 use clmm_lp_domain::enums::PositionStatus;
128 use clmm_lp_domain::value_objects::amount::Amount;
129 use primitive_types::U256;
130 use uuid::Uuid;
131
132 fn create_dummy_position() -> Position {
133 Position {
134 id: PositionId(Uuid::new_v4()),
135 pool_address: "pool1".to_string(),
136 owner_address: "owner1".to_string(),
137 liquidity_amount: 0, deposited_amount_a: Amount::new(U256::zero(), 6),
139 deposited_amount_b: Amount::new(U256::zero(), 6),
140 current_amount_a: Amount::new(U256::zero(), 6),
141 current_amount_b: Amount::new(U256::zero(), 6),
142 unclaimed_fees_a: Amount::new(U256::zero(), 6),
143 unclaimed_fees_b: Amount::new(U256::zero(), 6),
144 range: None,
145 opened_at: 0,
146 status: PositionStatus::Open,
147 }
148 }
149
150 #[test]
151 fn test_optimization_recommends_range() {
152 let optimizer = RangeOptimizer::new(10, 5, 1.0 / 365.0);
153 let position = create_dummy_position();
154 let volume = ConstantVolume::from_amount(Amount::new(U256::from(1000000), 6));
155 let current_price = Decimal::from(100);
156 let pool_liquidity = 100_000_000;
157 let fee_rate = Decimal::from_f64(0.003).unwrap();
158
159 let result = optimizer.optimize(
163 position,
164 current_price,
165 0.1, 0.0,
167 volume,
168 pool_liquidity,
169 fee_rate,
170 MaximizeNetPnL,
171 );
172
173 assert!(result.expected_pnl > Decimal::MIN);
174 assert!(result.recommended_range.lower_price.value < current_price);
176 assert!(result.recommended_range.upper_price.value > current_price);
177 }
178}