nautilus_analysis/statistics/
long_ratio.rs1use std::fmt::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 std::fmt::Formatter<'_>) -> std::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::{InstrumentClass, 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 adjustments: Vec::new(),
136 instrument_class: InstrumentClass::Spot,
137 is_currency_pair: true,
138 }
139 }
140
141 #[rstest]
142 fn test_empty_positions() {
143 let long_ratio = LongRatio::new(None);
144 let result = long_ratio.calculate_from_positions(&[]);
145 assert!(result.is_none());
146 }
147
148 #[rstest]
149 fn test_all_long_positions() {
150 let long_ratio = LongRatio::new(None);
151 let positions = vec![
152 create_test_position(OrderSide::Buy),
153 create_test_position(OrderSide::Buy),
154 create_test_position(OrderSide::Buy),
155 ];
156
157 let result = long_ratio.calculate_from_positions(&positions);
158 assert!(result.is_some());
159 assert!(approx_eq!(f64, result.unwrap(), 1.00, epsilon = 1e-9));
160 }
161
162 #[rstest]
163 fn test_all_short_positions() {
164 let long_ratio = LongRatio::new(None);
165 let positions = vec![
166 create_test_position(OrderSide::Sell),
167 create_test_position(OrderSide::Sell),
168 create_test_position(OrderSide::Sell),
169 ];
170
171 let result = long_ratio.calculate_from_positions(&positions);
172 assert!(result.is_some());
173 assert!(approx_eq!(f64, result.unwrap(), 0.00, epsilon = 1e-9));
174 }
175
176 #[rstest]
177 fn test_mixed_positions() {
178 let long_ratio = LongRatio::new(None);
179 let positions = vec![
180 create_test_position(OrderSide::Buy),
181 create_test_position(OrderSide::Sell),
182 create_test_position(OrderSide::Buy),
183 create_test_position(OrderSide::Sell),
184 ];
185
186 let result = long_ratio.calculate_from_positions(&positions);
187 assert!(result.is_some());
188 assert!(approx_eq!(f64, result.unwrap(), 0.50, epsilon = 1e-9));
189 }
190
191 #[rstest]
192 fn test_custom_precision() {
193 let long_ratio = LongRatio::new(Some(3));
194 let positions = vec![
195 create_test_position(OrderSide::Buy),
196 create_test_position(OrderSide::Buy),
197 create_test_position(OrderSide::Sell),
198 ];
199
200 let result = long_ratio.calculate_from_positions(&positions);
201 assert!(result.is_some());
202 assert!(approx_eq!(f64, result.unwrap(), 0.667, epsilon = 1e-9));
203 }
204
205 #[rstest]
206 fn test_single_position_long() {
207 let long_ratio = LongRatio::new(None);
208 let positions = vec![create_test_position(OrderSide::Buy)];
209
210 let result = long_ratio.calculate_from_positions(&positions);
211 assert!(result.is_some());
212 assert!(approx_eq!(f64, result.unwrap(), 1.00, epsilon = 1e-9));
213 }
214
215 #[rstest]
216 fn test_single_position_short() {
217 let long_ratio = LongRatio::new(None);
218 let positions = vec![create_test_position(OrderSide::Sell)];
219
220 let result = long_ratio.calculate_from_positions(&positions);
221 assert!(result.is_some());
222 assert!(approx_eq!(f64, result.unwrap(), 0.00, epsilon = 1e-9));
223 }
224
225 #[rstest]
226 fn test_zero_precision() {
227 let long_ratio = LongRatio::new(Some(0));
228 let positions = vec![
229 create_test_position(OrderSide::Buy),
230 create_test_position(OrderSide::Buy),
231 create_test_position(OrderSide::Sell),
232 ];
233
234 let result = long_ratio.calculate_from_positions(&positions);
235 assert!(result.is_some());
236 assert!(approx_eq!(f64, result.unwrap(), 1.00, epsilon = 1e-9));
237 }
238
239 #[rstest]
240 fn test_name() {
241 let long_ratio = LongRatio::new(None);
242 assert_eq!(long_ratio.name(), "Long Ratio");
243 }
244}