nautilus_analysis/statistics/
profit_factor.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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        // (10.0 + 30.0) / |-20.0 + -40.0| = 40 / 60 = 0.666...
107        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        // (10.0 + 0.0) / |-20.0 + -30.0| = 10 / 50 = 0.2
117        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}