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