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