nautilus_analysis/statistics/
expectancy.rs1use std::fmt::Display;
17
18use nautilus_model::position::Position;
19
20use super::{loser_avg::AvgLoser, winner_avg::AvgWinner};
21use crate::{Returns, statistic::PortfolioStatistic};
22
23#[repr(C)]
38#[derive(Debug, Clone)]
39#[cfg_attr(
40 feature = "python",
41 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis")
42)]
43pub struct Expectancy {}
44
45impl Display for Expectancy {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(f, "Expectancy")
48 }
49}
50
51impl PortfolioStatistic for Expectancy {
52 type Item = f64;
53
54 fn name(&self) -> String {
55 self.to_string()
56 }
57
58 fn calculate_from_realized_pnls(&self, realized_pnls: &[f64]) -> Option<Self::Item> {
59 if realized_pnls.is_empty() {
60 return Some(f64::NAN);
61 }
62
63 let avg_winner = AvgWinner {}
65 .calculate_from_realized_pnls(realized_pnls)
66 .map_or(0.0, |v| if v.is_nan() { 0.0 } else { v });
67 let avg_loser = AvgLoser {}
68 .calculate_from_realized_pnls(realized_pnls)
69 .map_or(0.0, |v| if v.is_nan() { 0.0 } else { v });
70
71 let winners: Vec<f64> = realized_pnls
73 .iter()
74 .filter(|&&pnl| pnl > 0.0)
75 .copied()
76 .collect();
77 let losers: Vec<f64> = realized_pnls
78 .iter()
79 .filter(|&&pnl| pnl < 0.0)
80 .copied()
81 .collect();
82
83 let total_trades = winners.len() + losers.len();
84 if total_trades == 0 {
85 return Some(0.0);
86 }
87
88 let win_rate = winners.len() as f64 / total_trades as f64;
89 let loss_rate = losers.len() as f64 / total_trades as f64;
90
91 Some(avg_winner.mul_add(win_rate, avg_loser * loss_rate))
92 }
93 fn calculate_from_returns(&self, _returns: &Returns) -> Option<Self::Item> {
94 None
95 }
96
97 fn calculate_from_positions(&self, _positions: &[Position]) -> Option<Self::Item> {
98 None
99 }
100}
101
102#[cfg(test)]
103mod tests {
104 use nautilus_core::approx_eq;
105 use rstest::rstest;
106
107 use super::*;
108
109 #[rstest]
110 fn test_empty_pnl_list() {
111 let expectancy = Expectancy {};
112 let result = expectancy.calculate_from_realized_pnls(&[]);
113 assert!(result.is_some());
114 assert!(result.unwrap().is_nan());
115 }
116
117 #[rstest]
118 fn test_all_winners() {
119 let expectancy = Expectancy {};
120 let pnls = vec![10.0, 20.0, 30.0];
121 let result = expectancy.calculate_from_realized_pnls(&pnls);
122
123 assert!(result.is_some());
124 assert!(approx_eq!(f64, result.unwrap(), 20.0, epsilon = 1e-9));
127 }
128
129 #[rstest]
130 fn test_all_losers() {
131 let expectancy = Expectancy {};
132 let pnls = vec![-10.0, -20.0, -30.0];
133 let result = expectancy.calculate_from_realized_pnls(&pnls);
134
135 assert!(result.is_some());
136 assert!(approx_eq!(f64, result.unwrap(), -20.0, epsilon = 1e-9));
139 }
140
141 #[rstest]
142 fn test_mixed_pnls() {
143 let expectancy = Expectancy {};
144 let pnls = vec![10.0, -5.0, 15.0, -10.0];
145 let result = expectancy.calculate_from_realized_pnls(&pnls);
146
147 assert!(result.is_some());
148 assert!(approx_eq!(f64, result.unwrap(), 2.5, epsilon = 1e-9));
155 }
156
157 #[rstest]
158 fn test_single_trade() {
159 let expectancy = Expectancy {};
160 let pnls = vec![10.0];
161 let result = expectancy.calculate_from_realized_pnls(&pnls);
162
163 assert!(result.is_some());
164 assert!(approx_eq!(f64, result.unwrap(), 10.0, epsilon = 1e-9));
167 }
168
169 #[rstest]
170 fn test_zeros_excluded_from_win_loss_rates() {
171 let expectancy = Expectancy {};
172 let pnls = vec![10.0, 0.0, -10.0];
173 let result = expectancy.calculate_from_realized_pnls(&pnls);
174
175 assert!(result.is_some());
176 assert!(approx_eq!(f64, result.unwrap(), 0.0, epsilon = 1e-9));
181 }
182
183 #[rstest]
184 fn test_only_zeros() {
185 let expectancy = Expectancy {};
186 let pnls = vec![0.0, 0.0, 0.0];
187 let result = expectancy.calculate_from_realized_pnls(&pnls);
188
189 assert!(result.is_some());
190 assert!(approx_eq!(f64, result.unwrap(), 0.0, epsilon = 1e-9));
192 }
193
194 #[rstest]
195 fn test_name() {
196 let expectancy = Expectancy {};
197 assert_eq!(expectancy.name(), "Expectancy");
198 }
199}