Skip to main content

clmm_lp_optimization/
range_optimizer.rs

1use 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
13/// Optimizer for finding the best price range.
14pub struct RangeOptimizer {
15    /// Number of iterations for Monte Carlo.
16    pub iterations: usize,
17    /// Number of steps per iteration.
18    pub steps: usize,
19    /// Time step in years.
20    pub time_step: f64,
21}
22
23impl RangeOptimizer {
24    /// Creates a new RangeOptimizer.
25    pub fn new(iterations: usize, steps: usize, time_step: f64) -> Self {
26        Self {
27            iterations,
28            steps,
29            time_step,
30        }
31    }
32
33    /// Optimizes the price range for a given position.
34    #[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        // Candidate widths: 1%, 2%, 5%, 10%, 20%, 50%
47        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        // Assume 1000 USD capital for estimation
53        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            // Estimate Liquidity L for this range given Capital
66            // Narrower range -> Higher L
67            // Approximation: L = Capital / (Width_factor)
68            // For simplicity, let's use L = 1 / width (relative to 1000 base)
69            // Real calc is complex, this proxy ensures narrower ranges get higher fees.
70            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, // will be overwritten
138            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        // Low volatility, optimize for PnL
160        // Should prefer narrower range because fees > IL (with high volume/liquidity ratio assumption)
161        // But our fee model is simple.
162        let result = optimizer.optimize(
163            position,
164            current_price,
165            0.1, // 10% vol
166            0.0,
167            volume,
168            pool_liquidity,
169            fee_rate,
170            MaximizeNetPnL,
171        );
172
173        assert!(result.expected_pnl > Decimal::MIN);
174        // Check recommended range is valid
175        assert!(result.recommended_range.lower_price.value < current_price);
176        assert!(result.recommended_range.upper_price.value > current_price);
177    }
178}