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 = "posei_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 AccountId, ClientOrderId, PositionId,
73 stubs::{instrument_id_aud_usd_sim, strategy_id_ema_cross, trader_id},
74 },
75 types::{Currency, Quantity},
76 };
77 use rstest::rstest;
78
79 use super::*;
80
81 fn create_test_position(side: OrderSide) -> Position {
82 Position {
83 events: Vec::new(),
84 trader_id: trader_id(),
85 strategy_id: strategy_id_ema_cross(),
86 instrument_id: instrument_id_aud_usd_sim(),
87 id: PositionId::new("test-position"),
88 account_id: AccountId::new("test-account"),
89 opening_order_id: ClientOrderId::default(),
90 closing_order_id: None,
91 entry: side,
92 side: nautilus_model::enums::PositionSide::NoPositionSide,
93 signed_qty: 0.0,
94 quantity: Quantity::default(),
95 peak_qty: Quantity::default(),
96 price_precision: 2,
97 size_precision: 2,
98 multiplier: Quantity::default(),
99 is_inverse: false,
100 base_currency: None,
101 quote_currency: Currency::USD(),
102 settlement_currency: Currency::USD(),
103 ts_init: UnixNanos::default(),
104 ts_opened: UnixNanos::default(),
105 ts_last: UnixNanos::default(),
106 ts_closed: None,
107 duration_ns: 2,
108 avg_px_open: 0.0,
109 avg_px_close: None,
110 realized_return: 0.0,
111 realized_pnl: None,
112 trade_ids: Vec::new(),
113 buy_qty: Quantity::default(),
114 sell_qty: Quantity::default(),
115 commissions: HashMap::new(),
116 }
117 }
118
119 #[rstest]
120 fn test_empty_positions() {
121 let long_ratio = LongRatio::new(None);
122 let result = long_ratio.calculate_from_positions(&[]);
123 assert!(result.is_none());
124 }
125
126 #[rstest]
127 fn test_all_long_positions() {
128 let long_ratio = LongRatio::new(None);
129 let positions = vec![
130 create_test_position(OrderSide::Buy),
131 create_test_position(OrderSide::Buy),
132 create_test_position(OrderSide::Buy),
133 ];
134
135 let result = long_ratio.calculate_from_positions(&positions);
136 assert!(result.is_some());
137 assert_eq!(result.unwrap(), 1.00);
138 }
139
140 #[rstest]
141 fn test_all_short_positions() {
142 let long_ratio = LongRatio::new(None);
143 let positions = vec![
144 create_test_position(OrderSide::Sell),
145 create_test_position(OrderSide::Sell),
146 create_test_position(OrderSide::Sell),
147 ];
148
149 let result = long_ratio.calculate_from_positions(&positions);
150 assert!(result.is_some());
151 assert_eq!(result.unwrap(), 0.00);
152 }
153
154 #[rstest]
155 fn test_mixed_positions() {
156 let long_ratio = LongRatio::new(None);
157 let positions = vec![
158 create_test_position(OrderSide::Buy),
159 create_test_position(OrderSide::Sell),
160 create_test_position(OrderSide::Buy),
161 create_test_position(OrderSide::Sell),
162 ];
163
164 let result = long_ratio.calculate_from_positions(&positions);
165 assert!(result.is_some());
166 assert_eq!(result.unwrap(), 0.50);
167 }
168
169 #[rstest]
170 fn test_custom_precision() {
171 let long_ratio = LongRatio::new(Some(3));
172 let positions = vec![
173 create_test_position(OrderSide::Buy),
174 create_test_position(OrderSide::Buy),
175 create_test_position(OrderSide::Sell),
176 ];
177
178 let result = long_ratio.calculate_from_positions(&positions);
179 assert!(result.is_some());
180 assert_eq!(result.unwrap(), 0.667);
181 }
182
183 #[rstest]
184 fn test_single_position_long() {
185 let long_ratio = LongRatio::new(None);
186 let positions = vec![create_test_position(OrderSide::Buy)];
187
188 let result = long_ratio.calculate_from_positions(&positions);
189 assert!(result.is_some());
190 assert_eq!(result.unwrap(), 1.00);
191 }
192
193 #[rstest]
194 fn test_single_position_short() {
195 let long_ratio = LongRatio::new(None);
196 let positions = vec![create_test_position(OrderSide::Sell)];
197
198 let result = long_ratio.calculate_from_positions(&positions);
199 assert!(result.is_some());
200 assert_eq!(result.unwrap(), 0.00);
201 }
202
203 #[rstest]
204 fn test_zero_precision() {
205 let long_ratio = LongRatio::new(Some(0));
206 let positions = vec![
207 create_test_position(OrderSide::Buy),
208 create_test_position(OrderSide::Buy),
209 create_test_position(OrderSide::Sell),
210 ];
211
212 let result = long_ratio.calculate_from_positions(&positions);
213 assert!(result.is_some());
214 assert_eq!(result.unwrap(), 1.00);
215 }
216
217 #[rstest]
218 fn test_name() {
219 let long_ratio = LongRatio::new(None);
220 assert_eq!(long_ratio.name(), "LongRatio");
221 }
222}