nautilus_analysis/statistics/
profit_factor.rs1use crate::{statistic::PortfolioStatistic, Returns};
17
18#[repr(C)]
19#[derive(Debug)]
20#[cfg_attr(
21 feature = "python",
22 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis")
23)]
24pub struct ProfitFactor {}
25
26impl PortfolioStatistic for ProfitFactor {
27 type Item = f64;
28
29 fn name(&self) -> String {
30 stringify!(ProfitFactor).to_string()
31 }
32
33 fn calculate_from_returns(&self, returns: &Returns) -> Option<Self::Item> {
34 if !self.check_valid_returns(returns) {
35 return Some(f64::NAN);
36 }
37
38 let (positive_returns_sum, negative_returns_sum) =
39 returns
40 .values()
41 .fold((0.0, 0.0), |(pos_sum, neg_sum), &pnl| {
42 if pnl >= 0.0 {
43 (pos_sum + pnl, neg_sum)
44 } else {
45 (pos_sum, neg_sum + pnl)
46 }
47 });
48
49 if negative_returns_sum == 0.0 {
50 return Some(f64::NAN);
51 }
52 Some((positive_returns_sum / negative_returns_sum).abs())
53 }
54}
55
56#[cfg(test)]
57mod profit_factor_tests {
58 use std::collections::BTreeMap;
59
60 use nautilus_core::UnixNanos;
61
62 use super::*;
63
64 fn create_returns(values: Vec<f64>) -> Returns {
65 let mut new_return = BTreeMap::new();
66 for (i, value) in values.iter().enumerate() {
67 new_return.insert(UnixNanos::from(i as u64), *value);
68 }
69
70 new_return
71 }
72
73 #[test]
74 fn test_empty_returns() {
75 let profit_factor = ProfitFactor {};
76 let returns = create_returns(vec![]);
77 let result = profit_factor.calculate_from_returns(&returns);
78 assert!(result.is_some());
79 assert!(result.unwrap().is_nan());
80 }
81
82 #[test]
83 fn test_all_positive() {
84 let profit_factor = ProfitFactor {};
85 let returns = create_returns(vec![10.0, 20.0, 30.0]);
86 let result = profit_factor.calculate_from_returns(&returns);
87 assert!(result.is_some());
88 assert!(result.unwrap().is_nan());
89 }
90
91 #[test]
92 fn test_all_negative() {
93 let profit_factor = ProfitFactor {};
94 let returns = create_returns(vec![-10.0, -20.0, -30.0]);
95 let result = profit_factor.calculate_from_returns(&returns);
96 assert!(result.is_some());
97 assert_eq!(result.unwrap(), 0.0);
98 }
99
100 #[test]
101 fn test_mixed_returns() {
102 let profit_factor = ProfitFactor {};
103 let returns = create_returns(vec![10.0, -20.0, 30.0, -40.0]);
104 let result = profit_factor.calculate_from_returns(&returns);
105 assert!(result.is_some());
106 assert_eq!(result.unwrap(), 0.6666666666666666);
108 }
109
110 #[test]
111 fn test_with_zero() {
112 let profit_factor = ProfitFactor {};
113 let returns = create_returns(vec![10.0, 0.0, -20.0, -30.0]);
114 let result = profit_factor.calculate_from_returns(&returns);
115 assert!(result.is_some());
116 assert_eq!(result.unwrap(), 0.2);
118 }
119
120 #[test]
121 fn test_equal_positive_negative() {
122 let profit_factor = ProfitFactor {};
123 let returns = create_returns(vec![20.0, -20.0]);
124 let result = profit_factor.calculate_from_returns(&returns);
125 assert!(result.is_some());
126 assert_eq!(result.unwrap(), 1.0);
127 }
128
129 #[test]
130 fn test_name() {
131 let profit_factor = ProfitFactor {};
132 assert_eq!(profit_factor.name(), "ProfitFactor");
133 }
134}