Skip to main content

clmm_lp_optimization/
objective.rs

1use clmm_lp_domain::value_objects::simulation_result::SimulationResult;
2use rust_decimal::Decimal;
3use rust_decimal::prelude::FromPrimitive;
4use std::cmp::Ordering;
5
6/// Trait for objective functions.
7pub trait ObjectiveFunction {
8    /// Evaluates the simulation result and returns a score.
9    fn evaluate(&self, result: &SimulationResult) -> Decimal;
10
11    /// Compares two simulation results.
12    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    /// Returns the name of the objective function.
19    fn name(&self) -> &'static str;
20}
21
22/// Objective function to maximize Net PnL.
23#[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/// Objective function to maximize Fees.
37#[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/// Objective function to maximize Sharpe Ratio.
51#[derive(Debug, Clone, Copy)]
52pub struct MaximizeSharpeRatio {
53    /// Risk-free rate.
54    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    /// Creates a new MaximizeSharpeRatio objective.
67    #[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        // Use sharpe_ratio if available, otherwise calculate from PnL/drawdown
76        if let Some(sharpe) = result.sharpe_ratio {
77            return sharpe;
78        }
79
80        // Simplified Sharpe: (Return - RiskFree) / Risk
81        // Use max_drawdown as risk proxy
82        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/// Objective function to minimize IL while maintaining minimum fees.
95#[derive(Debug, Clone, Copy)]
96pub struct MinimizeIL {
97    /// Minimum acceptable fees.
98    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    /// Creates a new MinimizeIL objective.
111    #[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        // Penalize if fees are below minimum
120        if result.total_fees_earned < self.min_fees {
121            return Decimal::MIN;
122        }
123
124        // Return negative IL (so minimizing IL = maximizing score)
125        -result.total_il
126    }
127
128    fn name(&self) -> &'static str {
129        "MinimizeIL"
130    }
131}
132
133/// Objective function to maximize time in range.
134#[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/// Risk-adjusted return objective (Sortino-like).
148#[derive(Debug, Clone, Copy)]
149pub struct RiskAdjustedReturn {
150    /// Weight for downside risk penalty.
151    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    /// Creates a new RiskAdjustedReturn objective.
164    #[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        // Score = PnL - (risk_weight * max_drawdown)
173        result.net_pnl - (self.risk_weight * result.max_drawdown)
174    }
175
176    fn name(&self) -> &'static str {
177        "RiskAdjustedReturn"
178    }
179}
180
181/// Composite objective that combines multiple objectives with weights.
182#[derive(Debug, Clone)]
183pub struct CompositeObjective {
184    /// Weights for each component.
185    weights: CompositeWeights,
186}
187
188/// Weights for composite objective.
189#[derive(Debug, Clone)]
190pub struct CompositeWeights {
191    /// Weight for net PnL.
192    pub pnl_weight: Decimal,
193    /// Weight for fees.
194    pub fees_weight: Decimal,
195    /// Weight for IL (negative = penalize).
196    pub il_weight: Decimal,
197    /// Weight for time in range.
198    pub time_in_range_weight: Decimal,
199    /// Weight for drawdown (negative = penalize).
200    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    /// Creates a new composite objective with default weights.
217    #[must_use]
218    pub fn new() -> Self {
219        Self {
220            weights: CompositeWeights::default(),
221        }
222    }
223
224    /// Creates a composite objective with custom weights.
225    #[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        // Should use the sharpe_ratio field
289        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        // Should calculate: 30 / 10 = 3
305        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        // Fees (50) >= min_fees (10), so return -IL = -20
313        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        // Fees (50) < min_fees (100), so return MIN
321        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        // 30 - (2 * 10) = 10
336        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        // Only PnL with weight 1
350        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}