nautilus_analysis/statistics/
loser_min.rs1use std::fmt::{self, Display};
17
18use nautilus_model::position::Position;
19
20use crate::{Returns, statistic::PortfolioStatistic};
21
22#[repr(C)]
23#[derive(Debug, Clone)]
24#[cfg_attr(
25 feature = "python",
26 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis")
27)]
28pub struct MinLoser {}
29
30impl Display for MinLoser {
31 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32 write!(f, "Min Loser")
33 }
34}
35
36impl PortfolioStatistic for MinLoser {
37 type Item = f64;
38
39 fn name(&self) -> String {
40 self.to_string()
41 }
42
43 fn calculate_from_realized_pnls(&self, realized_pnls: &[f64]) -> Option<Self::Item> {
44 if realized_pnls.is_empty() {
45 return Some(0.0);
46 }
47
48 let losers: Vec<f64> = realized_pnls
50 .iter()
51 .filter(|&&pnl| pnl <= 0.0)
52 .copied()
53 .collect();
54
55 if losers.is_empty() {
56 return Some(0.0); }
58
59 losers
60 .iter()
61 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
62 .copied()
63 }
64
65 fn calculate_from_returns(&self, _returns: &Returns) -> Option<Self::Item> {
66 None
67 }
68
69 fn calculate_from_positions(&self, _positions: &[Position]) -> Option<Self::Item> {
70 None
71 }
72}
73
74#[cfg(test)]
79mod tests {
80 use nautilus_core::approx_eq;
81 use rstest::rstest;
82
83 use super::*;
84
85 #[rstest]
86 fn test_empty_pnls() {
87 let min_loser = MinLoser {};
88 let result = min_loser.calculate_from_realized_pnls(&[]);
89 assert!(result.is_some());
90 assert!(approx_eq!(f64, result.unwrap(), 0.0, epsilon = 1e-9));
91 }
92
93 #[rstest]
94 fn test_all_positive() {
95 let min_loser = MinLoser {};
96 let pnls = vec![10.0, 20.0, 30.0];
97 let result = min_loser.calculate_from_realized_pnls(&pnls);
98 assert!(result.is_some());
99 assert!(approx_eq!(f64, result.unwrap(), 0.0, epsilon = 1e-9));
101 }
102
103 #[rstest]
104 fn test_all_negative() {
105 let min_loser = MinLoser {};
106 let pnls = vec![-10.0, -20.0, -30.0];
107 let result = min_loser.calculate_from_realized_pnls(&pnls);
108 assert!(result.is_some());
109 assert!(approx_eq!(f64, result.unwrap(), -10.0, epsilon = 1e-9));
110 }
111
112 #[rstest]
113 fn test_mixed_pnls() {
114 let min_loser = MinLoser {};
115 let pnls = vec![10.0, -20.0, 30.0, -40.0];
116 let result = min_loser.calculate_from_realized_pnls(&pnls);
117 assert!(result.is_some());
118 assert!(approx_eq!(f64, result.unwrap(), -20.0, epsilon = 1e-9));
119 }
120
121 #[rstest]
122 fn test_with_zero() {
123 let min_loser = MinLoser {};
124 let pnls = vec![10.0, 0.0, -20.0, -30.0];
125 let result = min_loser.calculate_from_realized_pnls(&pnls);
126 assert!(result.is_some());
127 assert!(approx_eq!(f64, result.unwrap(), 0.0, epsilon = 1e-9));
129 }
130
131 #[rstest]
132 fn test_single_negative() {
133 let min_loser = MinLoser {};
134 let pnls = vec![-10.0];
135 let result = min_loser.calculate_from_realized_pnls(&pnls);
136 assert!(result.is_some());
137 assert!(approx_eq!(f64, result.unwrap(), -10.0, epsilon = 1e-9));
138 }
139
140 #[rstest]
141 fn test_name() {
142 let min_loser = MinLoser {};
143 assert_eq!(min_loser.name(), "Min Loser");
144 }
145}