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