nautilus_analysis/statistics/
profit_factor.rs1use std::fmt::{self, Display};
17
18use nautilus_model::position::Position;
19
20use crate::{Returns, statistic::PortfolioStatistic};
21
22#[repr(C)]
40#[derive(Debug, Clone)]
41#[cfg_attr(
42 feature = "python",
43 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis")
44)]
45pub struct ProfitFactor {}
46
47impl Display for ProfitFactor {
48 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49 write!(f, "Profit Factor")
50 }
51}
52
53impl PortfolioStatistic for ProfitFactor {
54 type Item = f64;
55
56 fn name(&self) -> String {
57 self.to_string()
58 }
59
60 fn calculate_from_returns(&self, returns: &Returns) -> Option<Self::Item> {
61 if !self.check_valid_returns(returns) {
62 return Some(f64::NAN);
63 }
64
65 let (positive_returns_sum, negative_returns_sum) =
66 returns
67 .values()
68 .fold((0.0, 0.0), |(pos_sum, neg_sum), &pnl| {
69 if pnl >= 0.0 {
70 (pos_sum + pnl, neg_sum)
71 } else {
72 (pos_sum, neg_sum + pnl)
73 }
74 });
75
76 if negative_returns_sum == 0.0 {
77 return Some(f64::NAN);
78 }
79 Some((positive_returns_sum / negative_returns_sum).abs())
80 }
81 fn calculate_from_realized_pnls(&self, _realized_pnls: &[f64]) -> Option<Self::Item> {
82 None
83 }
84
85 fn calculate_from_positions(&self, _positions: &[Position]) -> Option<Self::Item> {
86 None
87 }
88}
89
90#[cfg(test)]
95mod profit_factor_tests {
96 use std::collections::BTreeMap;
97
98 use nautilus_core::{UnixNanos, approx_eq};
99 use rstest::rstest;
100
101 use super::*;
102
103 fn create_returns(values: Vec<f64>) -> Returns {
104 let mut new_return = BTreeMap::new();
105 for (i, value) in values.iter().enumerate() {
106 new_return.insert(UnixNanos::from(i as u64), *value);
107 }
108
109 new_return
110 }
111
112 #[rstest]
113 fn test_empty_returns() {
114 let profit_factor = ProfitFactor {};
115 let returns = create_returns(vec![]);
116 let result = profit_factor.calculate_from_returns(&returns);
117 assert!(result.is_some());
118 assert!(result.unwrap().is_nan());
119 }
120
121 #[rstest]
122 fn test_all_positive() {
123 let profit_factor = ProfitFactor {};
124 let returns = create_returns(vec![10.0, 20.0, 30.0]);
125 let result = profit_factor.calculate_from_returns(&returns);
126 assert!(result.is_some());
127 assert!(result.unwrap().is_nan());
128 }
129
130 #[rstest]
131 fn test_all_negative() {
132 let profit_factor = ProfitFactor {};
133 let returns = create_returns(vec![-10.0, -20.0, -30.0]);
134 let result = profit_factor.calculate_from_returns(&returns);
135 assert!(result.is_some());
136 assert!(approx_eq!(f64, result.unwrap(), 0.0, epsilon = 1e-9));
137 }
138
139 #[rstest]
140 fn test_mixed_returns() {
141 let profit_factor = ProfitFactor {};
142 let returns = create_returns(vec![10.0, -20.0, 30.0, -40.0]);
143 let result = profit_factor.calculate_from_returns(&returns);
144 assert!(result.is_some());
145 assert!(approx_eq!(
147 f64,
148 result.unwrap(),
149 0.6666666666666666,
150 epsilon = 1e-9
151 ));
152 }
153
154 #[rstest]
155 fn test_with_zero() {
156 let profit_factor = ProfitFactor {};
157 let returns = create_returns(vec![10.0, 0.0, -20.0, -30.0]);
158 let result = profit_factor.calculate_from_returns(&returns);
159 assert!(result.is_some());
160 assert!(approx_eq!(f64, result.unwrap(), 0.2, epsilon = 1e-9));
162 }
163
164 #[rstest]
165 fn test_equal_positive_negative() {
166 let profit_factor = ProfitFactor {};
167 let returns = create_returns(vec![20.0, -20.0]);
168 let result = profit_factor.calculate_from_returns(&returns);
169 assert!(result.is_some());
170 assert!(approx_eq!(f64, result.unwrap(), 1.0, epsilon = 1e-9));
171 }
172
173 #[rstest]
174 fn test_name() {
175 let profit_factor = ProfitFactor {};
176 assert_eq!(profit_factor.name(), "Profit Factor");
177 }
178}