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 long_count = positions
63 .iter()
64 .filter(|p| p.entry == OrderSide::Buy)
65 .count();
66
67 let value = long_count as f64 / positions.len() as f64;
68
69 let scale = 10f64.powi(self.precision as i32);
70 Some((value * scale).round() / scale)
71 }
72 fn calculate_from_returns(&self, _returns: &Returns) -> Option<Self::Item> {
73 None
74 }
75
76 fn calculate_from_realized_pnls(&self, _realized_pnls: &[f64]) -> Option<Self::Item> {
77 None
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use ahash::AHashMap;
84 use nautilus_core::{UnixNanos, approx_eq};
85 use nautilus_model::{
86 enums::{InstrumentClass, PositionSide},
87 identifiers::{
88 AccountId, ClientOrderId, PositionId,
89 stubs::{instrument_id_aud_usd_sim, strategy_id_ema_cross, trader_id},
90 },
91 types::{Currency, Quantity},
92 };
93 use rstest::rstest;
94
95 use super::*;
96
97 fn create_closed_position(entry: OrderSide) -> Position {
100 Position {
101 events: Vec::new(),
102 trader_id: trader_id(),
103 strategy_id: strategy_id_ema_cross(),
104 instrument_id: instrument_id_aud_usd_sim(),
105 id: PositionId::new("test-position"),
106 account_id: AccountId::new("test-account"),
107 opening_order_id: ClientOrderId::default(),
108 closing_order_id: None,
109 entry,
110 side: PositionSide::Flat, signed_qty: 0.0,
112 quantity: Quantity::default(),
113 peak_qty: Quantity::default(),
114 price_precision: 2,
115 size_precision: 2,
116 multiplier: Quantity::default(),
117 is_inverse: false,
118 base_currency: None,
119 quote_currency: Currency::USD(),
120 settlement_currency: Currency::USD(),
121 ts_init: UnixNanos::default(),
122 ts_opened: UnixNanos::default(),
123 ts_last: UnixNanos::default(),
124 ts_closed: Some(UnixNanos::from(1)), duration_ns: 2,
126 avg_px_open: 0.0,
127 avg_px_close: Some(0.0),
128 realized_return: 0.0,
129 realized_pnl: None,
130 trade_ids: Vec::new(),
131 buy_qty: Quantity::default(),
132 sell_qty: Quantity::default(),
133 commissions: AHashMap::new(),
134 adjustments: Vec::new(),
135 instrument_class: InstrumentClass::Spot,
136 is_currency_pair: true,
137 }
138 }
139
140 #[rstest]
141 fn test_empty_positions() {
142 let long_ratio = LongRatio::new(None);
143 let result = long_ratio.calculate_from_positions(&[]);
144 assert!(result.is_none());
145 }
146
147 #[rstest]
148 fn test_all_long_positions() {
149 let long_ratio = LongRatio::new(None);
150 let positions = vec![
151 create_closed_position(OrderSide::Buy),
152 create_closed_position(OrderSide::Buy),
153 create_closed_position(OrderSide::Buy),
154 ];
155
156 let result = long_ratio.calculate_from_positions(&positions);
157 assert!(result.is_some());
158 assert!(approx_eq!(f64, result.unwrap(), 1.00, epsilon = 1e-9));
159 }
160
161 #[rstest]
162 fn test_all_short_positions() {
163 let long_ratio = LongRatio::new(None);
164 let positions = vec![
165 create_closed_position(OrderSide::Sell),
166 create_closed_position(OrderSide::Sell),
167 create_closed_position(OrderSide::Sell),
168 ];
169
170 let result = long_ratio.calculate_from_positions(&positions);
171 assert!(result.is_some());
172 assert!(approx_eq!(f64, result.unwrap(), 0.00, epsilon = 1e-9));
173 }
174
175 #[rstest]
176 fn test_mixed_positions() {
177 let long_ratio = LongRatio::new(None);
178 let positions = vec![
179 create_closed_position(OrderSide::Buy),
180 create_closed_position(OrderSide::Sell),
181 create_closed_position(OrderSide::Buy),
182 create_closed_position(OrderSide::Sell),
183 ];
184
185 let result = long_ratio.calculate_from_positions(&positions);
186 assert!(result.is_some());
187 assert!(approx_eq!(f64, result.unwrap(), 0.50, epsilon = 1e-9));
188 }
189
190 #[rstest]
191 fn test_custom_precision() {
192 let long_ratio = LongRatio::new(Some(3));
193 let positions = vec![
194 create_closed_position(OrderSide::Buy),
195 create_closed_position(OrderSide::Buy),
196 create_closed_position(OrderSide::Sell),
197 ];
198
199 let result = long_ratio.calculate_from_positions(&positions);
200 assert!(result.is_some());
201 assert!(approx_eq!(f64, result.unwrap(), 0.667, epsilon = 1e-9));
202 }
203
204 #[rstest]
205 fn test_single_position_long() {
206 let long_ratio = LongRatio::new(None);
207 let positions = vec![create_closed_position(OrderSide::Buy)];
208
209 let result = long_ratio.calculate_from_positions(&positions);
210 assert!(result.is_some());
211 assert!(approx_eq!(f64, result.unwrap(), 1.00, epsilon = 1e-9));
212 }
213
214 #[rstest]
215 fn test_single_position_short() {
216 let long_ratio = LongRatio::new(None);
217 let positions = vec![create_closed_position(OrderSide::Sell)];
218
219 let result = long_ratio.calculate_from_positions(&positions);
220 assert!(result.is_some());
221 assert!(approx_eq!(f64, result.unwrap(), 0.00, epsilon = 1e-9));
222 }
223
224 #[rstest]
225 fn test_zero_precision() {
226 let long_ratio = LongRatio::new(Some(0));
227 let positions = vec![
228 create_closed_position(OrderSide::Buy),
229 create_closed_position(OrderSide::Buy),
230 create_closed_position(OrderSide::Sell),
231 ];
232
233 let result = long_ratio.calculate_from_positions(&positions);
234 assert!(result.is_some());
235 assert!(approx_eq!(f64, result.unwrap(), 1.00, epsilon = 1e-9));
236 }
237
238 #[rstest]
239 fn test_name() {
240 let long_ratio = LongRatio::new(None);
241 assert_eq!(long_ratio.name(), "Long Ratio");
242 }
243}