nautilus_analysis/statistics/
profit_factor.rs1use crate::{Returns, statistic::PortfolioStatistic};
17
18#[repr(C)]
19#[derive(Debug)]
20#[cfg_attr(
21 feature = "python",
22 pyo3::pyclass(module = "posei_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 use rstest::rstest;
62
63 use super::*;
64
65 fn create_returns(values: Vec<f64>) -> Returns {
66 let mut new_return = BTreeMap::new();
67 for (i, value) in values.iter().enumerate() {
68 new_return.insert(UnixNanos::from(i as u64), *value);
69 }
70
71 new_return
72 }
73
74 #[rstest]
75 fn test_empty_returns() {
76 let profit_factor = ProfitFactor {};
77 let returns = create_returns(vec![]);
78 let result = profit_factor.calculate_from_returns(&returns);
79 assert!(result.is_some());
80 assert!(result.unwrap().is_nan());
81 }
82
83 #[rstest]
84 fn test_all_positive() {
85 let profit_factor = ProfitFactor {};
86 let returns = create_returns(vec![10.0, 20.0, 30.0]);
87 let result = profit_factor.calculate_from_returns(&returns);
88 assert!(result.is_some());
89 assert!(result.unwrap().is_nan());
90 }
91
92 #[rstest]
93 fn test_all_negative() {
94 let profit_factor = ProfitFactor {};
95 let returns = create_returns(vec![-10.0, -20.0, -30.0]);
96 let result = profit_factor.calculate_from_returns(&returns);
97 assert!(result.is_some());
98 assert_eq!(result.unwrap(), 0.0);
99 }
100
101 #[rstest]
102 fn test_mixed_returns() {
103 let profit_factor = ProfitFactor {};
104 let returns = create_returns(vec![10.0, -20.0, 30.0, -40.0]);
105 let result = profit_factor.calculate_from_returns(&returns);
106 assert!(result.is_some());
107 assert_eq!(result.unwrap(), 0.6666666666666666);
109 }
110
111 #[rstest]
112 fn test_with_zero() {
113 let profit_factor = ProfitFactor {};
114 let returns = create_returns(vec![10.0, 0.0, -20.0, -30.0]);
115 let result = profit_factor.calculate_from_returns(&returns);
116 assert!(result.is_some());
117 assert_eq!(result.unwrap(), 0.2);
119 }
120
121 #[rstest]
122 fn test_equal_positive_negative() {
123 let profit_factor = ProfitFactor {};
124 let returns = create_returns(vec![20.0, -20.0]);
125 let result = profit_factor.calculate_from_returns(&returns);
126 assert!(result.is_some());
127 assert_eq!(result.unwrap(), 1.0);
128 }
129
130 #[rstest]
131 fn test_name() {
132 let profit_factor = ProfitFactor {};
133 assert_eq!(profit_factor.name(), "ProfitFactor");
134 }
135}