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 = "posei_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;
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_eq!(result.unwrap(), 0.0);
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_eq!(result.unwrap(), 0.6666666666666666);
116 }
117
118 #[rstest]
119 fn test_with_zero() {
120 let profit_factor = ProfitFactor {};
121 let returns = create_returns(vec![10.0, 0.0, -20.0, -30.0]);
122 let result = profit_factor.calculate_from_returns(&returns);
123 assert!(result.is_some());
124 assert_eq!(result.unwrap(), 0.2);
126 }
127
128 #[rstest]
129 fn test_equal_positive_negative() {
130 let profit_factor = ProfitFactor {};
131 let returns = create_returns(vec![20.0, -20.0]);
132 let result = profit_factor.calculate_from_returns(&returns);
133 assert!(result.is_some());
134 assert_eq!(result.unwrap(), 1.0);
135 }
136
137 #[rstest]
138 fn test_name() {
139 let profit_factor = ProfitFactor {};
140 assert_eq!(profit_factor.name(), "ProfitFactor");
141 }
142}