nautilus_analysis/statistics/
expectancy.rs1use std::fmt::{self, 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 fmt::Formatter<'_>) -> 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(0.0);
61 }
62
63 let avg_winner = AvgWinner {}
64 .calculate_from_realized_pnls(realized_pnls)
65 .unwrap_or(0.0);
66 let avg_loser = AvgLoser {}
67 .calculate_from_realized_pnls(realized_pnls)
68 .unwrap_or(0.0);
69
70 let (winners, losers): (Vec<f64>, Vec<f64>) =
71 realized_pnls.iter().partition(|&&pnl| pnl > 0.0);
72
73 let total_trades = winners.len() + losers.len();
74 let win_rate = winners.len() as f64 / total_trades.max(1) as f64;
75 let loss_rate = 1.0 - win_rate;
76
77 Some(avg_winner.mul_add(win_rate, avg_loser * loss_rate))
78 }
79 fn calculate_from_returns(&self, _returns: &Returns) -> Option<Self::Item> {
80 None
81 }
82
83 fn calculate_from_positions(&self, _positions: &[Position]) -> Option<Self::Item> {
84 None
85 }
86}
87
88#[cfg(test)]
93mod tests {
94 use nautilus_core::approx_eq;
95 use rstest::rstest;
96
97 use super::*;
98
99 #[rstest]
100 fn test_empty_pnl_list() {
101 let expectancy = Expectancy {};
102 let result = expectancy.calculate_from_realized_pnls(&[]);
103 assert!(result.is_some());
104 assert!(approx_eq!(f64, result.unwrap(), 0.0, epsilon = 1e-9));
105 }
106
107 #[rstest]
108 fn test_all_winners() {
109 let expectancy = Expectancy {};
110 let pnls = vec![10.0, 20.0, 30.0];
111 let result = expectancy.calculate_from_realized_pnls(&pnls);
112
113 assert!(result.is_some());
114 assert!(approx_eq!(f64, result.unwrap(), 20.0, epsilon = 1e-9));
117 }
118
119 #[rstest]
120 fn test_all_losers() {
121 let expectancy = Expectancy {};
122 let pnls = vec![-10.0, -20.0, -30.0];
123 let result = expectancy.calculate_from_realized_pnls(&pnls);
124
125 assert!(result.is_some());
126 assert!(approx_eq!(f64, result.unwrap(), -20.0, epsilon = 1e-9));
129 }
130
131 #[rstest]
132 fn test_mixed_pnls() {
133 let expectancy = Expectancy {};
134 let pnls = vec![10.0, -5.0, 15.0, -10.0];
135 let result = expectancy.calculate_from_realized_pnls(&pnls);
136
137 assert!(result.is_some());
138 assert!(approx_eq!(f64, result.unwrap(), 2.5, epsilon = 1e-9));
145 }
146
147 #[rstest]
148 fn test_single_trade() {
149 let expectancy = Expectancy {};
150 let pnls = vec![10.0];
151 let result = expectancy.calculate_from_realized_pnls(&pnls);
152
153 assert!(result.is_some());
154 assert!(approx_eq!(f64, result.unwrap(), 10.0, epsilon = 1e-9));
157 }
158
159 #[rstest]
160 fn test_name() {
161 let expectancy = Expectancy {};
162 assert_eq!(expectancy.name(), "Expectancy");
163 }
164}