nautilus_analysis/statistics/
long_ratio.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.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 = "posei_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            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}