nautilus_analysis/statistics/
long_ratio.rs1use std::fmt::{self, Display};
17
18use nautilus_model::{enums::OrderSide, position::Position};
19
20use crate::{Returns, statistic::PortfolioStatistic};
21
22#[repr(C)]
23#[derive(Debug, Clone)]
24#[cfg_attr(
25 feature = "python",
26 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.analysis")
27)]
28pub struct LongRatio {
29 pub precision: usize,
30}
31
32impl LongRatio {
33 #[must_use]
35 pub fn new(precision: Option<usize>) -> Self {
36 Self {
37 precision: precision.unwrap_or(2),
38 }
39 }
40}
41
42impl Display for LongRatio {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 write!(f, "Long Ratio")
45 }
46}
47
48impl PortfolioStatistic for LongRatio {
49 type Item = f64;
50
51 fn name(&self) -> String {
52 self.to_string()
53 }
54
55 fn calculate_from_positions(&self, positions: &[Position]) -> Option<Self::Item> {
56 if positions.is_empty() {
57 return None;
58 }
59
60 let longs: Vec<&Position> = positions
61 .iter()
62 .filter(|p| matches!(p.entry, OrderSide::Buy))
63 .collect();
64
65 let value = longs.len() as f64 / positions.len() as f64;
66
67 let scale = 10f64.powi(self.precision as i32);
68 Some((value * scale).round() / scale)
69 }
70 fn calculate_from_returns(&self, _returns: &Returns) -> Option<Self::Item> {
71 None
72 }
73
74 fn calculate_from_realized_pnls(&self, _realized_pnls: &[f64]) -> Option<Self::Item> {
75 None
76 }
77}
78
79#[cfg(test)]
84mod tests {
85 use std::collections::HashMap;
86
87 use nautilus_core::{UnixNanos, approx_eq};
88 use nautilus_model::{
89 enums::OrderSide,
90 identifiers::{
91 AccountId, ClientOrderId, PositionId,
92 stubs::{instrument_id_aud_usd_sim, strategy_id_ema_cross, trader_id},
93 },
94 types::{Currency, Quantity},
95 };
96 use rstest::rstest;
97
98 use super::*;
99
100 fn create_test_position(side: OrderSide) -> Position {
101 Position {
102 events: Vec::new(),
103 trader_id: trader_id(),
104 strategy_id: strategy_id_ema_cross(),
105 instrument_id: instrument_id_aud_usd_sim(),
106 id: PositionId::new("test-position"),
107 account_id: AccountId::new("test-account"),
108 opening_order_id: ClientOrderId::default(),
109 closing_order_id: None,
110 entry: side,
111 side: nautilus_model::enums::PositionSide::NoPositionSide,
112 signed_qty: 0.0,
113 quantity: Quantity::default(),
114 peak_qty: Quantity::default(),
115 price_precision: 2,
116 size_precision: 2,
117 multiplier: Quantity::default(),
118 is_inverse: false,
119 base_currency: None,
120 quote_currency: Currency::USD(),
121 settlement_currency: Currency::USD(),
122 ts_init: UnixNanos::default(),
123 ts_opened: UnixNanos::default(),
124 ts_last: UnixNanos::default(),
125 ts_closed: None,
126 duration_ns: 2,
127 avg_px_open: 0.0,
128 avg_px_close: None,
129 realized_return: 0.0,
130 realized_pnl: None,
131 trade_ids: Vec::new(),
132 buy_qty: Quantity::default(),
133 sell_qty: Quantity::default(),
134 commissions: HashMap::new(),
135 }
136 }
137
138 #[rstest]
139 fn test_empty_positions() {
140 let long_ratio = LongRatio::new(None);
141 let result = long_ratio.calculate_from_positions(&[]);
142 assert!(result.is_none());
143 }
144
145 #[rstest]
146 fn test_all_long_positions() {
147 let long_ratio = LongRatio::new(None);
148 let positions = vec![
149 create_test_position(OrderSide::Buy),
150 create_test_position(OrderSide::Buy),
151 create_test_position(OrderSide::Buy),
152 ];
153
154 let result = long_ratio.calculate_from_positions(&positions);
155 assert!(result.is_some());
156 assert!(approx_eq!(f64, result.unwrap(), 1.00, epsilon = 1e-9));
157 }
158
159 #[rstest]
160 fn test_all_short_positions() {
161 let long_ratio = LongRatio::new(None);
162 let positions = vec![
163 create_test_position(OrderSide::Sell),
164 create_test_position(OrderSide::Sell),
165 create_test_position(OrderSide::Sell),
166 ];
167
168 let result = long_ratio.calculate_from_positions(&positions);
169 assert!(result.is_some());
170 assert!(approx_eq!(f64, result.unwrap(), 0.00, epsilon = 1e-9));
171 }
172
173 #[rstest]
174 fn test_mixed_positions() {
175 let long_ratio = LongRatio::new(None);
176 let positions = vec![
177 create_test_position(OrderSide::Buy),
178 create_test_position(OrderSide::Sell),
179 create_test_position(OrderSide::Buy),
180 create_test_position(OrderSide::Sell),
181 ];
182
183 let result = long_ratio.calculate_from_positions(&positions);
184 assert!(result.is_some());
185 assert!(approx_eq!(f64, result.unwrap(), 0.50, epsilon = 1e-9));
186 }
187
188 #[rstest]
189 fn test_custom_precision() {
190 let long_ratio = LongRatio::new(Some(3));
191 let positions = vec![
192 create_test_position(OrderSide::Buy),
193 create_test_position(OrderSide::Buy),
194 create_test_position(OrderSide::Sell),
195 ];
196
197 let result = long_ratio.calculate_from_positions(&positions);
198 assert!(result.is_some());
199 assert!(approx_eq!(f64, result.unwrap(), 0.667, epsilon = 1e-9));
200 }
201
202 #[rstest]
203 fn test_single_position_long() {
204 let long_ratio = LongRatio::new(None);
205 let positions = vec![create_test_position(OrderSide::Buy)];
206
207 let result = long_ratio.calculate_from_positions(&positions);
208 assert!(result.is_some());
209 assert!(approx_eq!(f64, result.unwrap(), 1.00, epsilon = 1e-9));
210 }
211
212 #[rstest]
213 fn test_single_position_short() {
214 let long_ratio = LongRatio::new(None);
215 let positions = vec![create_test_position(OrderSide::Sell)];
216
217 let result = long_ratio.calculate_from_positions(&positions);
218 assert!(result.is_some());
219 assert!(approx_eq!(f64, result.unwrap(), 0.00, epsilon = 1e-9));
220 }
221
222 #[rstest]
223 fn test_zero_precision() {
224 let long_ratio = LongRatio::new(Some(0));
225 let positions = vec![
226 create_test_position(OrderSide::Buy),
227 create_test_position(OrderSide::Buy),
228 create_test_position(OrderSide::Sell),
229 ];
230
231 let result = long_ratio.calculate_from_positions(&positions);
232 assert!(result.is_some());
233 assert!(approx_eq!(f64, result.unwrap(), 1.00, epsilon = 1e-9));
234 }
235
236 #[rstest]
237 fn test_name() {
238 let long_ratio = LongRatio::new(None);
239 assert_eq!(long_ratio.name(), "Long Ratio");
240 }
241}