nautilus_analysis/statistics/
expectancy.rs1use super::{loser_avg::AvgLoser, winner_avg::AvgWinner};
17use crate::statistic::PortfolioStatistic;
18
19#[repr(C)]
25#[derive(Debug)]
26#[cfg_attr(
27 feature = "python",
28 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis")
29)]
30pub struct Expectancy {}
31
32impl PortfolioStatistic for Expectancy {
33 type Item = f64;
34
35 fn name(&self) -> String {
36 stringify!(Expectancy).to_string()
37 }
38
39 fn calculate_from_realized_pnls(&self, realized_pnls: &[f64]) -> Option<Self::Item> {
40 if realized_pnls.is_empty() {
41 return Some(0.0);
42 }
43
44 let avg_winner = AvgWinner {}
45 .calculate_from_realized_pnls(realized_pnls)
46 .unwrap_or(0.0);
47 let avg_loser = AvgLoser {}
48 .calculate_from_realized_pnls(realized_pnls)
49 .unwrap_or(0.0);
50
51 let (winners, losers): (Vec<f64>, Vec<f64>) =
52 realized_pnls.iter().partition(|&&pnl| pnl > 0.0);
53
54 let total_trades = winners.len() + losers.len();
55 let win_rate = winners.len() as f64 / total_trades.max(1) as f64;
56 let loss_rate = 1.0 - win_rate;
57
58 Some(avg_winner.mul_add(win_rate, avg_loser * loss_rate))
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use nautilus_core::approx_eq;
65 use rstest::rstest;
66
67 use super::*;
68
69 #[rstest]
70 fn test_empty_pnl_list() {
71 let expectancy = Expectancy {};
72 let result = expectancy.calculate_from_realized_pnls(&[]);
73 assert!(result.is_some());
74 assert!(approx_eq!(f64, result.unwrap(), 0.0, epsilon = 1e-9));
75 }
76
77 #[rstest]
78 fn test_all_winners() {
79 let expectancy = Expectancy {};
80 let pnls = vec![10.0, 20.0, 30.0];
81 let result = expectancy.calculate_from_realized_pnls(&pnls);
82
83 assert!(result.is_some());
84 assert!(approx_eq!(f64, result.unwrap(), 20.0, epsilon = 1e-9));
87 }
88
89 #[rstest]
90 fn test_all_losers() {
91 let expectancy = Expectancy {};
92 let pnls = vec![-10.0, -20.0, -30.0];
93 let result = expectancy.calculate_from_realized_pnls(&pnls);
94
95 assert!(result.is_some());
96 assert!(approx_eq!(f64, result.unwrap(), -20.0, epsilon = 1e-9));
99 }
100
101 #[rstest]
102 fn test_mixed_pnls() {
103 let expectancy = Expectancy {};
104 let pnls = vec![10.0, -5.0, 15.0, -10.0];
105 let result = expectancy.calculate_from_realized_pnls(&pnls);
106
107 assert!(result.is_some());
108 assert!(approx_eq!(f64, result.unwrap(), 2.5, epsilon = 1e-9));
115 }
116
117 #[rstest]
118 fn test_single_trade() {
119 let expectancy = Expectancy {};
120 let pnls = vec![10.0];
121 let result = expectancy.calculate_from_realized_pnls(&pnls);
122
123 assert!(result.is_some());
124 assert!(approx_eq!(f64, result.unwrap(), 10.0, epsilon = 1e-9));
127 }
128
129 #[rstest]
130 fn test_name() {
131 let expectancy = Expectancy {};
132 assert_eq!(expectancy.name(), "Expectancy");
133 }
134}