1use rust_decimal::Decimal;
6
7use crate::MetricsError;
8
9pub fn win_rate(trade_pnls: &[Decimal]) -> Result<Decimal, MetricsError> {
28 if trade_pnls.is_empty() {
29 return Err(MetricsError::InsufficientData {
30 required: 1,
31 actual: 0,
32 });
33 }
34
35 let wins = trade_pnls
36 .iter()
37 .filter(|&&pnl| pnl > Decimal::ZERO)
38 .count();
39 let total = trade_pnls.len();
40
41 Ok(Decimal::from(wins as u64) / Decimal::from(total as u64) * Decimal::from(100))
42}
43
44pub fn profit_factor(trade_pnls: &[Decimal]) -> Result<Decimal, MetricsError> {
56 if trade_pnls.is_empty() {
57 return Err(MetricsError::InsufficientData {
58 required: 1,
59 actual: 0,
60 });
61 }
62
63 let gross_profit: Decimal = trade_pnls.iter().filter(|&&pnl| pnl > Decimal::ZERO).sum();
64
65 let gross_loss: Decimal = trade_pnls
66 .iter()
67 .filter(|&&pnl| pnl < Decimal::ZERO)
68 .map(|pnl| pnl.abs())
69 .sum();
70
71 if gross_loss == Decimal::ZERO {
72 if gross_profit == Decimal::ZERO {
73 return Ok(Decimal::ONE); }
75 return Err(MetricsError::DivisionByZero {
77 context: "no losing trades",
78 });
79 }
80
81 Ok(gross_profit / gross_loss)
82}
83
84pub fn avg_win(trade_pnls: &[Decimal]) -> Result<Decimal, MetricsError> {
92 let wins: Vec<Decimal> = trade_pnls
93 .iter()
94 .filter(|&&pnl| pnl > Decimal::ZERO)
95 .copied()
96 .collect();
97
98 if wins.is_empty() {
99 return Err(MetricsError::InsufficientData {
100 required: 1,
101 actual: 0,
102 });
103 }
104
105 let sum: Decimal = wins.iter().sum();
106 Ok(sum / Decimal::from(wins.len() as u64))
107}
108
109pub fn avg_loss(trade_pnls: &[Decimal]) -> Result<Decimal, MetricsError> {
117 let losses: Vec<Decimal> = trade_pnls
118 .iter()
119 .filter(|&&pnl| pnl < Decimal::ZERO)
120 .copied()
121 .collect();
122
123 if losses.is_empty() {
124 return Err(MetricsError::InsufficientData {
125 required: 1,
126 actual: 0,
127 });
128 }
129
130 let sum: Decimal = losses.iter().sum();
131 Ok(sum / Decimal::from(losses.len() as u64))
132}
133
134pub fn expectancy(trade_pnls: &[Decimal]) -> Result<Decimal, MetricsError> {
144 if trade_pnls.is_empty() {
145 return Err(MetricsError::InsufficientData {
146 required: 1,
147 actual: 0,
148 });
149 }
150
151 let wins: Vec<Decimal> = trade_pnls
152 .iter()
153 .filter(|&&pnl| pnl > Decimal::ZERO)
154 .copied()
155 .collect();
156
157 let losses: Vec<Decimal> = trade_pnls
158 .iter()
159 .filter(|&&pnl| pnl < Decimal::ZERO)
160 .copied()
161 .collect();
162
163 let total = trade_pnls.len() as u64;
164 let win_pct = Decimal::from(wins.len() as u64) / Decimal::from(total);
165 let loss_pct = Decimal::ONE - win_pct;
166
167 let avg_w = if wins.is_empty() {
168 Decimal::ZERO
169 } else {
170 wins.iter().sum::<Decimal>() / Decimal::from(wins.len() as u64)
171 };
172
173 let avg_l = if losses.is_empty() {
174 Decimal::ZERO
175 } else {
176 losses.iter().sum::<Decimal>() / Decimal::from(losses.len() as u64)
177 };
178
179 Ok((win_pct * avg_w) + (loss_pct * avg_l))
180}
181
182#[cfg(test)]
183#[path = "trading_tests.rs"]
184mod tests;