nautilus_analysis/statistics/
long_ratio.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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 = "nautilus_trader.core.nautilus_pyo3.analysis")
25)]
26pub struct LongRatio {
27    precision: usize,
28}
29
30impl LongRatio {
31    /// Creates a new [`LongRatio`] instance.
32    #[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            stubs::{instrument_id_aud_usd_sim, strategy_id_ema_cross, trader_id},
73            AccountId, ClientOrderId, PositionId,
74        },
75        types::{Currency, Quantity},
76    };
77
78    use super::*;
79
80    fn create_test_position(side: OrderSide) -> Position {
81        Position {
82            events: Vec::new(),
83            trader_id: trader_id(),
84            strategy_id: strategy_id_ema_cross(),
85            instrument_id: instrument_id_aud_usd_sim(),
86            id: PositionId::new("test-position"),
87            account_id: AccountId::new("test-account"),
88            opening_order_id: ClientOrderId::default(),
89            closing_order_id: None,
90            entry: side,
91            side: nautilus_model::enums::PositionSide::NoPositionSide,
92            signed_qty: 0.0,
93            quantity: Quantity::default(),
94            peak_qty: Quantity::default(),
95            price_precision: 2,
96            size_precision: 2,
97            multiplier: Quantity::default(),
98            is_inverse: false,
99            base_currency: None,
100            quote_currency: Currency::USD(),
101            settlement_currency: Currency::USD(),
102            ts_init: UnixNanos::default(),
103            ts_opened: UnixNanos::default(),
104            ts_last: UnixNanos::default(),
105            ts_closed: None,
106            duration_ns: 2,
107            avg_px_open: 0.0,
108            avg_px_close: None,
109            realized_return: 0.0,
110            realized_pnl: None,
111            trade_ids: Vec::new(),
112            buy_qty: Quantity::default(),
113            sell_qty: Quantity::default(),
114            commissions: HashMap::new(),
115        }
116    }
117
118    #[test]
119    fn test_empty_positions() {
120        let long_ratio = LongRatio::new(None);
121        let result = long_ratio.calculate_from_positions(&[]);
122        assert!(result.is_none());
123    }
124
125    #[test]
126    fn test_all_long_positions() {
127        let long_ratio = LongRatio::new(None);
128        let positions = vec![
129            create_test_position(OrderSide::Buy),
130            create_test_position(OrderSide::Buy),
131            create_test_position(OrderSide::Buy),
132        ];
133
134        let result = long_ratio.calculate_from_positions(&positions);
135        assert!(result.is_some());
136        assert_eq!(result.unwrap(), 1.00);
137    }
138
139    #[test]
140    fn test_all_short_positions() {
141        let long_ratio = LongRatio::new(None);
142        let positions = vec![
143            create_test_position(OrderSide::Sell),
144            create_test_position(OrderSide::Sell),
145            create_test_position(OrderSide::Sell),
146        ];
147
148        let result = long_ratio.calculate_from_positions(&positions);
149        assert!(result.is_some());
150        assert_eq!(result.unwrap(), 0.00);
151    }
152
153    #[test]
154    fn test_mixed_positions() {
155        let long_ratio = LongRatio::new(None);
156        let positions = vec![
157            create_test_position(OrderSide::Buy),
158            create_test_position(OrderSide::Sell),
159            create_test_position(OrderSide::Buy),
160            create_test_position(OrderSide::Sell),
161        ];
162
163        let result = long_ratio.calculate_from_positions(&positions);
164        assert!(result.is_some());
165        assert_eq!(result.unwrap(), 0.50);
166    }
167
168    #[test]
169    fn test_custom_precision() {
170        let long_ratio = LongRatio::new(Some(3));
171        let positions = vec![
172            create_test_position(OrderSide::Buy),
173            create_test_position(OrderSide::Buy),
174            create_test_position(OrderSide::Sell),
175        ];
176
177        let result = long_ratio.calculate_from_positions(&positions);
178        assert!(result.is_some());
179        assert_eq!(result.unwrap(), 0.667);
180    }
181
182    #[test]
183    fn test_single_position_long() {
184        let long_ratio = LongRatio::new(None);
185        let positions = vec![create_test_position(OrderSide::Buy)];
186
187        let result = long_ratio.calculate_from_positions(&positions);
188        assert!(result.is_some());
189        assert_eq!(result.unwrap(), 1.00);
190    }
191
192    #[test]
193    fn test_single_position_short() {
194        let long_ratio = LongRatio::new(None);
195        let positions = vec![create_test_position(OrderSide::Sell)];
196
197        let result = long_ratio.calculate_from_positions(&positions);
198        assert!(result.is_some());
199        assert_eq!(result.unwrap(), 0.00);
200    }
201
202    #[test]
203    fn test_zero_precision() {
204        let long_ratio = LongRatio::new(Some(0));
205        let positions = vec![
206            create_test_position(OrderSide::Buy),
207            create_test_position(OrderSide::Buy),
208            create_test_position(OrderSide::Sell),
209        ];
210
211        let result = long_ratio.calculate_from_positions(&positions);
212        assert!(result.is_some());
213        assert_eq!(result.unwrap(), 1.00);
214    }
215
216    #[test]
217    fn test_name() {
218        let long_ratio = LongRatio::new(None);
219        assert_eq!(long_ratio.name(), "LongRatio");
220    }
221}