nautilus_analysis/statistics/
profit_factor.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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::{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        // (10.0 + 30.0) / |-20.0 + -40.0| = 40 / 60 = 0.666...
108        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        // (10.0 + 0.0) / |-20.0 + -30.0| = 10 / 50 = 0.2
118        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}