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