Skip to main content

clmm_lp_optimization/
constraints.rs

1//! Position constraints for optimization.
2//!
3//! This module defines constraints that limit the search space
4//! during optimization, ensuring valid and practical solutions.
5
6use rust_decimal::Decimal;
7use rust_decimal::prelude::FromPrimitive;
8
9/// Constraints for position optimization.
10#[derive(Debug, Clone)]
11pub struct PositionConstraints {
12    /// Minimum range width as a percentage (e.g., 0.01 = 1%).
13    pub min_range_width: Decimal,
14    /// Maximum range width as a percentage (e.g., 0.50 = 50%).
15    pub max_range_width: Decimal,
16    /// Minimum capital to deploy.
17    pub min_capital: Decimal,
18    /// Maximum capital to deploy.
19    pub max_capital: Decimal,
20    /// Maximum acceptable IL as a percentage.
21    pub max_il_threshold: Decimal,
22    /// Minimum time in range percentage.
23    pub min_time_in_range: Decimal,
24    /// Maximum transaction cost as percentage of capital.
25    pub max_tx_cost_ratio: Decimal,
26}
27
28impl Default for PositionConstraints {
29    fn default() -> Self {
30        Self {
31            min_range_width: Decimal::from_f64(0.01).unwrap(), // 1%
32            max_range_width: Decimal::from_f64(0.50).unwrap(), // 50%
33            min_capital: Decimal::from(100),                   // $100
34            max_capital: Decimal::from(1_000_000),             // $1M
35            max_il_threshold: Decimal::from_f64(0.10).unwrap(), // 10%
36            min_time_in_range: Decimal::from_f64(0.50).unwrap(), // 50%
37            max_tx_cost_ratio: Decimal::from_f64(0.01).unwrap(), // 1%
38        }
39    }
40}
41
42impl PositionConstraints {
43    /// Creates new constraints with custom values.
44    #[must_use]
45    pub fn new() -> Self {
46        Self::default()
47    }
48
49    /// Sets the minimum range width.
50    #[must_use]
51    pub fn with_min_range_width(mut self, width: Decimal) -> Self {
52        self.min_range_width = width;
53        self
54    }
55
56    /// Sets the maximum range width.
57    #[must_use]
58    pub fn with_max_range_width(mut self, width: Decimal) -> Self {
59        self.max_range_width = width;
60        self
61    }
62
63    /// Sets the capital bounds.
64    #[must_use]
65    pub fn with_capital_bounds(mut self, min: Decimal, max: Decimal) -> Self {
66        self.min_capital = min;
67        self.max_capital = max;
68        self
69    }
70
71    /// Sets the maximum IL threshold.
72    #[must_use]
73    pub fn with_max_il(mut self, max_il: Decimal) -> Self {
74        self.max_il_threshold = max_il;
75        self
76    }
77
78    /// Sets the minimum time in range.
79    #[must_use]
80    pub fn with_min_time_in_range(mut self, min_time: Decimal) -> Self {
81        self.min_time_in_range = min_time;
82        self
83    }
84
85    /// Checks if a range width is valid.
86    #[must_use]
87    pub fn is_valid_range_width(&self, width: Decimal) -> bool {
88        width >= self.min_range_width && width <= self.max_range_width
89    }
90
91    /// Checks if capital is within bounds.
92    #[must_use]
93    pub fn is_valid_capital(&self, capital: Decimal) -> bool {
94        capital >= self.min_capital && capital <= self.max_capital
95    }
96
97    /// Checks if IL is acceptable.
98    #[must_use]
99    pub fn is_acceptable_il(&self, il: Decimal) -> bool {
100        il.abs() <= self.max_il_threshold
101    }
102
103    /// Checks if time in range meets minimum.
104    #[must_use]
105    pub fn meets_time_in_range(&self, time_in_range: Decimal) -> bool {
106        time_in_range >= self.min_time_in_range
107    }
108}
109
110/// Constraints for rebalancing strategy optimization.
111#[derive(Debug, Clone)]
112pub struct RebalanceConstraints {
113    /// Minimum rebalance interval in steps.
114    pub min_rebalance_interval: u64,
115    /// Maximum rebalance interval in steps.
116    pub max_rebalance_interval: u64,
117    /// Minimum price deviation threshold for rebalancing.
118    pub min_price_threshold: Decimal,
119    /// Maximum price deviation threshold for rebalancing.
120    pub max_price_threshold: Decimal,
121    /// Minimum IL threshold for rebalancing.
122    pub min_il_threshold: Decimal,
123    /// Maximum IL threshold for rebalancing.
124    pub max_il_threshold: Decimal,
125    /// Maximum number of rebalances allowed.
126    pub max_rebalances: Option<u32>,
127}
128
129impl Default for RebalanceConstraints {
130    fn default() -> Self {
131        Self {
132            min_rebalance_interval: 1,
133            max_rebalance_interval: 168, // 1 week in hours
134            min_price_threshold: Decimal::from_f64(0.01).unwrap(), // 1%
135            max_price_threshold: Decimal::from_f64(0.20).unwrap(), // 20%
136            min_il_threshold: Decimal::from_f64(0.01).unwrap(), // 1%
137            max_il_threshold: Decimal::from_f64(0.15).unwrap(), // 15%
138            max_rebalances: None,
139        }
140    }
141}
142
143impl RebalanceConstraints {
144    /// Creates new rebalance constraints.
145    #[must_use]
146    pub fn new() -> Self {
147        Self::default()
148    }
149
150    /// Sets the rebalance interval bounds.
151    #[must_use]
152    pub fn with_interval_bounds(mut self, min: u64, max: u64) -> Self {
153        self.min_rebalance_interval = min;
154        self.max_rebalance_interval = max;
155        self
156    }
157
158    /// Sets the price threshold bounds.
159    #[must_use]
160    pub fn with_price_threshold_bounds(mut self, min: Decimal, max: Decimal) -> Self {
161        self.min_price_threshold = min;
162        self.max_price_threshold = max;
163        self
164    }
165
166    /// Sets the IL threshold bounds.
167    #[must_use]
168    pub fn with_il_threshold_bounds(mut self, min: Decimal, max: Decimal) -> Self {
169        self.min_il_threshold = min;
170        self.max_il_threshold = max;
171        self
172    }
173
174    /// Sets the maximum number of rebalances.
175    #[must_use]
176    pub fn with_max_rebalances(mut self, max: u32) -> Self {
177        self.max_rebalances = Some(max);
178        self
179    }
180
181    /// Checks if an interval is valid.
182    #[must_use]
183    pub fn is_valid_interval(&self, interval: u64) -> bool {
184        interval >= self.min_rebalance_interval && interval <= self.max_rebalance_interval
185    }
186
187    /// Checks if a price threshold is valid.
188    #[must_use]
189    pub fn is_valid_price_threshold(&self, threshold: Decimal) -> bool {
190        threshold >= self.min_price_threshold && threshold <= self.max_price_threshold
191    }
192
193    /// Checks if an IL threshold is valid.
194    #[must_use]
195    pub fn is_valid_il_threshold(&self, threshold: Decimal) -> bool {
196        threshold >= self.min_il_threshold && threshold <= self.max_il_threshold
197    }
198}
199
200/// Combined constraints for full optimization.
201#[derive(Debug, Clone, Default)]
202pub struct OptimizationConstraints {
203    /// Position constraints.
204    pub position: PositionConstraints,
205    /// Rebalance constraints.
206    pub rebalance: RebalanceConstraints,
207}
208
209impl OptimizationConstraints {
210    /// Creates new optimization constraints.
211    #[must_use]
212    pub fn new() -> Self {
213        Self::default()
214    }
215
216    /// Sets position constraints.
217    #[must_use]
218    pub fn with_position(mut self, constraints: PositionConstraints) -> Self {
219        self.position = constraints;
220        self
221    }
222
223    /// Sets rebalance constraints.
224    #[must_use]
225    pub fn with_rebalance(mut self, constraints: RebalanceConstraints) -> Self {
226        self.rebalance = constraints;
227        self
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    use super::*;
234
235    #[test]
236    fn test_position_constraints_default() {
237        let constraints = PositionConstraints::default();
238        assert_eq!(
239            constraints.min_range_width,
240            Decimal::from_f64(0.01).unwrap()
241        );
242        assert_eq!(
243            constraints.max_range_width,
244            Decimal::from_f64(0.50).unwrap()
245        );
246    }
247
248    #[test]
249    fn test_position_constraints_validation() {
250        let constraints = PositionConstraints::default();
251
252        assert!(constraints.is_valid_range_width(Decimal::from_f64(0.10).unwrap()));
253        assert!(!constraints.is_valid_range_width(Decimal::from_f64(0.005).unwrap()));
254        assert!(!constraints.is_valid_range_width(Decimal::from_f64(0.60).unwrap()));
255
256        assert!(constraints.is_valid_capital(Decimal::from(1000)));
257        assert!(!constraints.is_valid_capital(Decimal::from(50)));
258
259        assert!(constraints.is_acceptable_il(Decimal::from_f64(0.05).unwrap()));
260        assert!(!constraints.is_acceptable_il(Decimal::from_f64(0.15).unwrap()));
261    }
262
263    #[test]
264    fn test_rebalance_constraints_validation() {
265        let constraints = RebalanceConstraints::default();
266
267        assert!(constraints.is_valid_interval(24));
268        assert!(!constraints.is_valid_interval(0));
269        assert!(!constraints.is_valid_interval(200));
270
271        assert!(constraints.is_valid_price_threshold(Decimal::from_f64(0.05).unwrap()));
272        assert!(!constraints.is_valid_price_threshold(Decimal::from_f64(0.005).unwrap()));
273    }
274
275    #[test]
276    fn test_constraints_builder() {
277        let constraints = PositionConstraints::new()
278            .with_min_range_width(Decimal::from_f64(0.02).unwrap())
279            .with_max_il(Decimal::from_f64(0.05).unwrap());
280
281        assert_eq!(
282            constraints.min_range_width,
283            Decimal::from_f64(0.02).unwrap()
284        );
285        assert_eq!(
286            constraints.max_il_threshold,
287            Decimal::from_f64(0.05).unwrap()
288        );
289    }
290}