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