clmm_lp_optimization/
objective.rs1use clmm_lp_domain::value_objects::simulation_result::SimulationResult;
2use rust_decimal::Decimal;
3use rust_decimal::prelude::FromPrimitive;
4use std::cmp::Ordering;
5
6pub trait ObjectiveFunction {
8 fn evaluate(&self, result: &SimulationResult) -> Decimal;
10
11 fn compare(&self, a: &SimulationResult, b: &SimulationResult) -> Ordering {
13 self.evaluate(a)
14 .partial_cmp(&self.evaluate(b))
15 .unwrap_or(Ordering::Equal)
16 }
17
18 fn name(&self) -> &'static str;
20}
21
22#[derive(Debug, Clone, Copy, Default)]
24pub struct MaximizeNetPnL;
25
26impl ObjectiveFunction for MaximizeNetPnL {
27 fn evaluate(&self, result: &SimulationResult) -> Decimal {
28 result.net_pnl
29 }
30
31 fn name(&self) -> &'static str {
32 "MaximizeNetPnL"
33 }
34}
35
36#[derive(Debug, Clone, Copy, Default)]
38pub struct MaximizeFees;
39
40impl ObjectiveFunction for MaximizeFees {
41 fn evaluate(&self, result: &SimulationResult) -> Decimal {
42 result.total_fees_earned
43 }
44
45 fn name(&self) -> &'static str {
46 "MaximizeFees"
47 }
48}
49
50#[derive(Debug, Clone, Copy)]
52pub struct MaximizeSharpeRatio {
53 pub risk_free_rate: Decimal,
55}
56
57impl Default for MaximizeSharpeRatio {
58 fn default() -> Self {
59 Self {
60 risk_free_rate: Decimal::ZERO,
61 }
62 }
63}
64
65impl MaximizeSharpeRatio {
66 #[must_use]
68 pub fn new(risk_free_rate: Decimal) -> Self {
69 Self { risk_free_rate }
70 }
71}
72
73impl ObjectiveFunction for MaximizeSharpeRatio {
74 fn evaluate(&self, result: &SimulationResult) -> Decimal {
75 if let Some(sharpe) = result.sharpe_ratio {
77 return sharpe;
78 }
79
80 if result.max_drawdown.is_zero() {
83 return result.net_pnl - self.risk_free_rate;
84 }
85
86 (result.net_pnl - self.risk_free_rate) / result.max_drawdown
87 }
88
89 fn name(&self) -> &'static str {
90 "MaximizeSharpeRatio"
91 }
92}
93
94#[derive(Debug, Clone, Copy)]
96pub struct MinimizeIL {
97 pub min_fees: Decimal,
99}
100
101impl Default for MinimizeIL {
102 fn default() -> Self {
103 Self {
104 min_fees: Decimal::ZERO,
105 }
106 }
107}
108
109impl MinimizeIL {
110 #[must_use]
112 pub fn new(min_fees: Decimal) -> Self {
113 Self { min_fees }
114 }
115}
116
117impl ObjectiveFunction for MinimizeIL {
118 fn evaluate(&self, result: &SimulationResult) -> Decimal {
119 if result.total_fees_earned < self.min_fees {
121 return Decimal::MIN;
122 }
123
124 -result.total_il
126 }
127
128 fn name(&self) -> &'static str {
129 "MinimizeIL"
130 }
131}
132
133#[derive(Debug, Clone, Copy, Default)]
135pub struct MaximizeTimeInRange;
136
137impl ObjectiveFunction for MaximizeTimeInRange {
138 fn evaluate(&self, result: &SimulationResult) -> Decimal {
139 result.time_in_range_percentage
140 }
141
142 fn name(&self) -> &'static str {
143 "MaximizeTimeInRange"
144 }
145}
146
147#[derive(Debug, Clone, Copy)]
149pub struct RiskAdjustedReturn {
150 pub risk_weight: Decimal,
152}
153
154impl Default for RiskAdjustedReturn {
155 fn default() -> Self {
156 Self {
157 risk_weight: Decimal::ONE,
158 }
159 }
160}
161
162impl RiskAdjustedReturn {
163 #[must_use]
165 pub fn new(risk_weight: Decimal) -> Self {
166 Self { risk_weight }
167 }
168}
169
170impl ObjectiveFunction for RiskAdjustedReturn {
171 fn evaluate(&self, result: &SimulationResult) -> Decimal {
172 result.net_pnl - (self.risk_weight * result.max_drawdown)
174 }
175
176 fn name(&self) -> &'static str {
177 "RiskAdjustedReturn"
178 }
179}
180
181#[derive(Debug, Clone)]
183pub struct CompositeObjective {
184 weights: CompositeWeights,
186}
187
188#[derive(Debug, Clone)]
190pub struct CompositeWeights {
191 pub pnl_weight: Decimal,
193 pub fees_weight: Decimal,
195 pub il_weight: Decimal,
197 pub time_in_range_weight: Decimal,
199 pub drawdown_weight: Decimal,
201}
202
203impl Default for CompositeWeights {
204 fn default() -> Self {
205 Self {
206 pnl_weight: Decimal::ONE,
207 fees_weight: Decimal::ZERO,
208 il_weight: Decimal::from_f64(-0.5).unwrap(),
209 time_in_range_weight: Decimal::ZERO,
210 drawdown_weight: Decimal::from_f64(-0.3).unwrap(),
211 }
212 }
213}
214
215impl CompositeObjective {
216 #[must_use]
218 pub fn new() -> Self {
219 Self {
220 weights: CompositeWeights::default(),
221 }
222 }
223
224 #[must_use]
226 pub fn with_weights(weights: CompositeWeights) -> Self {
227 Self { weights }
228 }
229}
230
231impl Default for CompositeObjective {
232 fn default() -> Self {
233 Self::new()
234 }
235}
236
237impl ObjectiveFunction for CompositeObjective {
238 fn evaluate(&self, result: &SimulationResult) -> Decimal {
239 let w = &self.weights;
240
241 w.pnl_weight * result.net_pnl
242 + w.fees_weight * result.total_fees_earned
243 + w.il_weight * result.total_il
244 + w.time_in_range_weight * result.time_in_range_percentage
245 + w.drawdown_weight * result.max_drawdown
246 }
247
248 fn name(&self) -> &'static str {
249 "CompositeObjective"
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn create_test_result() -> SimulationResult {
258 SimulationResult {
259 final_position_value: Decimal::from(1100),
260 total_fees_earned: Decimal::from(50),
261 total_il: Decimal::from(20),
262 net_pnl: Decimal::from(30),
263 max_drawdown: Decimal::from(10),
264 time_in_range_percentage: Decimal::from(75),
265 sharpe_ratio: Some(Decimal::from(2)),
266 }
267 }
268
269 #[test]
270 fn test_maximize_net_pnl() {
271 let obj = MaximizeNetPnL;
272 let result = create_test_result();
273 assert_eq!(obj.evaluate(&result), Decimal::from(30));
274 assert_eq!(obj.name(), "MaximizeNetPnL");
275 }
276
277 #[test]
278 fn test_maximize_fees() {
279 let obj = MaximizeFees;
280 let result = create_test_result();
281 assert_eq!(obj.evaluate(&result), Decimal::from(50));
282 }
283
284 #[test]
285 fn test_maximize_sharpe_ratio() {
286 let obj = MaximizeSharpeRatio::default();
287 let result = create_test_result();
288 assert_eq!(obj.evaluate(&result), Decimal::from(2));
290 }
291
292 #[test]
293 fn test_maximize_sharpe_ratio_fallback() {
294 let obj = MaximizeSharpeRatio::default();
295 let result = SimulationResult {
296 final_position_value: Decimal::from(1100),
297 total_fees_earned: Decimal::from(50),
298 total_il: Decimal::from(20),
299 net_pnl: Decimal::from(30),
300 max_drawdown: Decimal::from(10),
301 time_in_range_percentage: Decimal::from(75),
302 sharpe_ratio: None,
303 };
304 assert_eq!(obj.evaluate(&result), Decimal::from(3));
306 }
307
308 #[test]
309 fn test_minimize_il() {
310 let obj = MinimizeIL::new(Decimal::from(10));
311 let result = create_test_result();
312 assert_eq!(obj.evaluate(&result), Decimal::from(-20));
314 }
315
316 #[test]
317 fn test_minimize_il_penalty() {
318 let obj = MinimizeIL::new(Decimal::from(100));
319 let result = create_test_result();
320 assert_eq!(obj.evaluate(&result), Decimal::MIN);
322 }
323
324 #[test]
325 fn test_maximize_time_in_range() {
326 let obj = MaximizeTimeInRange;
327 let result = create_test_result();
328 assert_eq!(obj.evaluate(&result), Decimal::from(75));
329 }
330
331 #[test]
332 fn test_risk_adjusted_return() {
333 let obj = RiskAdjustedReturn::new(Decimal::from(2));
334 let result = create_test_result();
335 assert_eq!(obj.evaluate(&result), Decimal::from(10));
337 }
338
339 #[test]
340 fn test_composite_objective() {
341 let obj = CompositeObjective::with_weights(CompositeWeights {
342 pnl_weight: Decimal::ONE,
343 fees_weight: Decimal::ZERO,
344 il_weight: Decimal::ZERO,
345 time_in_range_weight: Decimal::ZERO,
346 drawdown_weight: Decimal::ZERO,
347 });
348 let result = create_test_result();
349 assert_eq!(obj.evaluate(&result), Decimal::from(30));
351 }
352
353 #[test]
354 fn test_objective_compare() {
355 let obj = MaximizeNetPnL;
356
357 let result_a = SimulationResult {
358 net_pnl: Decimal::from(30),
359 ..create_test_result()
360 };
361
362 let result_b = SimulationResult {
363 net_pnl: Decimal::from(20),
364 ..create_test_result()
365 };
366
367 assert_eq!(obj.compare(&result_a, &result_b), Ordering::Greater);
368 assert_eq!(obj.compare(&result_b, &result_a), Ordering::Less);
369 assert_eq!(obj.compare(&result_a, &result_a), Ordering::Equal);
370 }
371}