use clmm_lp_domain::value_objects::simulation_result::SimulationResult;
use rust_decimal::Decimal;
use rust_decimal::prelude::FromPrimitive;
use std::cmp::Ordering;
pub trait ObjectiveFunction {
fn evaluate(&self, result: &SimulationResult) -> Decimal;
fn compare(&self, a: &SimulationResult, b: &SimulationResult) -> Ordering {
self.evaluate(a)
.partial_cmp(&self.evaluate(b))
.unwrap_or(Ordering::Equal)
}
fn name(&self) -> &'static str;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct MaximizeNetPnL;
impl ObjectiveFunction for MaximizeNetPnL {
fn evaluate(&self, result: &SimulationResult) -> Decimal {
result.net_pnl
}
fn name(&self) -> &'static str {
"MaximizeNetPnL"
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct MaximizeFees;
impl ObjectiveFunction for MaximizeFees {
fn evaluate(&self, result: &SimulationResult) -> Decimal {
result.total_fees_earned
}
fn name(&self) -> &'static str {
"MaximizeFees"
}
}
#[derive(Debug, Clone, Copy)]
pub struct MaximizeSharpeRatio {
pub risk_free_rate: Decimal,
}
impl Default for MaximizeSharpeRatio {
fn default() -> Self {
Self {
risk_free_rate: Decimal::ZERO,
}
}
}
impl MaximizeSharpeRatio {
#[must_use]
pub fn new(risk_free_rate: Decimal) -> Self {
Self { risk_free_rate }
}
}
impl ObjectiveFunction for MaximizeSharpeRatio {
fn evaluate(&self, result: &SimulationResult) -> Decimal {
if let Some(sharpe) = result.sharpe_ratio {
return sharpe;
}
if result.max_drawdown.is_zero() {
return result.net_pnl - self.risk_free_rate;
}
(result.net_pnl - self.risk_free_rate) / result.max_drawdown
}
fn name(&self) -> &'static str {
"MaximizeSharpeRatio"
}
}
#[derive(Debug, Clone, Copy)]
pub struct MinimizeIL {
pub min_fees: Decimal,
}
impl Default for MinimizeIL {
fn default() -> Self {
Self {
min_fees: Decimal::ZERO,
}
}
}
impl MinimizeIL {
#[must_use]
pub fn new(min_fees: Decimal) -> Self {
Self { min_fees }
}
}
impl ObjectiveFunction for MinimizeIL {
fn evaluate(&self, result: &SimulationResult) -> Decimal {
if result.total_fees_earned < self.min_fees {
return Decimal::MIN;
}
-result.total_il
}
fn name(&self) -> &'static str {
"MinimizeIL"
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct MaximizeTimeInRange;
impl ObjectiveFunction for MaximizeTimeInRange {
fn evaluate(&self, result: &SimulationResult) -> Decimal {
result.time_in_range_percentage
}
fn name(&self) -> &'static str {
"MaximizeTimeInRange"
}
}
#[derive(Debug, Clone, Copy)]
pub struct RiskAdjustedReturn {
pub risk_weight: Decimal,
}
impl Default for RiskAdjustedReturn {
fn default() -> Self {
Self {
risk_weight: Decimal::ONE,
}
}
}
impl RiskAdjustedReturn {
#[must_use]
pub fn new(risk_weight: Decimal) -> Self {
Self { risk_weight }
}
}
impl ObjectiveFunction for RiskAdjustedReturn {
fn evaluate(&self, result: &SimulationResult) -> Decimal {
result.net_pnl - (self.risk_weight * result.max_drawdown)
}
fn name(&self) -> &'static str {
"RiskAdjustedReturn"
}
}
#[derive(Debug, Clone)]
pub struct CompositeObjective {
weights: CompositeWeights,
}
#[derive(Debug, Clone)]
pub struct CompositeWeights {
pub pnl_weight: Decimal,
pub fees_weight: Decimal,
pub il_weight: Decimal,
pub time_in_range_weight: Decimal,
pub drawdown_weight: Decimal,
}
impl Default for CompositeWeights {
fn default() -> Self {
Self {
pnl_weight: Decimal::ONE,
fees_weight: Decimal::ZERO,
il_weight: Decimal::from_f64(-0.5).unwrap(),
time_in_range_weight: Decimal::ZERO,
drawdown_weight: Decimal::from_f64(-0.3).unwrap(),
}
}
}
impl CompositeObjective {
#[must_use]
pub fn new() -> Self {
Self {
weights: CompositeWeights::default(),
}
}
#[must_use]
pub fn with_weights(weights: CompositeWeights) -> Self {
Self { weights }
}
}
impl Default for CompositeObjective {
fn default() -> Self {
Self::new()
}
}
impl ObjectiveFunction for CompositeObjective {
fn evaluate(&self, result: &SimulationResult) -> Decimal {
let w = &self.weights;
w.pnl_weight * result.net_pnl
+ w.fees_weight * result.total_fees_earned
+ w.il_weight * result.total_il
+ w.time_in_range_weight * result.time_in_range_percentage
+ w.drawdown_weight * result.max_drawdown
}
fn name(&self) -> &'static str {
"CompositeObjective"
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_result() -> SimulationResult {
SimulationResult {
final_position_value: Decimal::from(1100),
total_fees_earned: Decimal::from(50),
total_il: Decimal::from(20),
net_pnl: Decimal::from(30),
max_drawdown: Decimal::from(10),
time_in_range_percentage: Decimal::from(75),
sharpe_ratio: Some(Decimal::from(2)),
}
}
#[test]
fn test_maximize_net_pnl() {
let obj = MaximizeNetPnL;
let result = create_test_result();
assert_eq!(obj.evaluate(&result), Decimal::from(30));
assert_eq!(obj.name(), "MaximizeNetPnL");
}
#[test]
fn test_maximize_fees() {
let obj = MaximizeFees;
let result = create_test_result();
assert_eq!(obj.evaluate(&result), Decimal::from(50));
}
#[test]
fn test_maximize_sharpe_ratio() {
let obj = MaximizeSharpeRatio::default();
let result = create_test_result();
assert_eq!(obj.evaluate(&result), Decimal::from(2));
}
#[test]
fn test_maximize_sharpe_ratio_fallback() {
let obj = MaximizeSharpeRatio::default();
let result = SimulationResult {
final_position_value: Decimal::from(1100),
total_fees_earned: Decimal::from(50),
total_il: Decimal::from(20),
net_pnl: Decimal::from(30),
max_drawdown: Decimal::from(10),
time_in_range_percentage: Decimal::from(75),
sharpe_ratio: None,
};
assert_eq!(obj.evaluate(&result), Decimal::from(3));
}
#[test]
fn test_minimize_il() {
let obj = MinimizeIL::new(Decimal::from(10));
let result = create_test_result();
assert_eq!(obj.evaluate(&result), Decimal::from(-20));
}
#[test]
fn test_minimize_il_penalty() {
let obj = MinimizeIL::new(Decimal::from(100));
let result = create_test_result();
assert_eq!(obj.evaluate(&result), Decimal::MIN);
}
#[test]
fn test_maximize_time_in_range() {
let obj = MaximizeTimeInRange;
let result = create_test_result();
assert_eq!(obj.evaluate(&result), Decimal::from(75));
}
#[test]
fn test_risk_adjusted_return() {
let obj = RiskAdjustedReturn::new(Decimal::from(2));
let result = create_test_result();
assert_eq!(obj.evaluate(&result), Decimal::from(10));
}
#[test]
fn test_composite_objective() {
let obj = CompositeObjective::with_weights(CompositeWeights {
pnl_weight: Decimal::ONE,
fees_weight: Decimal::ZERO,
il_weight: Decimal::ZERO,
time_in_range_weight: Decimal::ZERO,
drawdown_weight: Decimal::ZERO,
});
let result = create_test_result();
assert_eq!(obj.evaluate(&result), Decimal::from(30));
}
#[test]
fn test_objective_compare() {
let obj = MaximizeNetPnL;
let result_a = SimulationResult {
net_pnl: Decimal::from(30),
..create_test_result()
};
let result_b = SimulationResult {
net_pnl: Decimal::from(20),
..create_test_result()
};
assert_eq!(obj.compare(&result_a, &result_b), Ordering::Greater);
assert_eq!(obj.compare(&result_b, &result_a), Ordering::Less);
assert_eq!(obj.compare(&result_a, &result_a), Ordering::Equal);
}
}