nautilus_indicators/book/
imbalance.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 std::fmt::Display;
17
18use nautilus_model::{orderbook::OrderBook, types::Quantity};
19
20use crate::indicator::Indicator;
21
22#[repr(C)]
23#[derive(Debug, Default)]
24#[cfg_attr(
25    feature = "python",
26    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")
27)]
28pub struct BookImbalanceRatio {
29    pub value: f64,
30    pub count: usize,
31    pub initialized: bool,
32    has_inputs: bool,
33}
34
35impl Display for BookImbalanceRatio {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(f, "{}()", self.name())
38    }
39}
40
41impl Indicator for BookImbalanceRatio {
42    fn name(&self) -> String {
43        stringify!(BookImbalanceRatio).to_string()
44    }
45
46    fn has_inputs(&self) -> bool {
47        self.has_inputs
48    }
49
50    fn initialized(&self) -> bool {
51        self.initialized
52    }
53
54    fn handle_book(&mut self, book: &OrderBook) {
55        self.update(book.best_bid_size(), book.best_ask_size());
56    }
57
58    fn reset(&mut self) {
59        self.value = 0.0;
60        self.count = 0;
61        self.has_inputs = false;
62        self.initialized = false;
63    }
64}
65
66impl BookImbalanceRatio {
67    /// Creates a new [`BookImbalanceRatio`] instance.
68    #[must_use]
69    pub const fn new() -> Self {
70        Self {
71            value: 0.0,
72            count: 0,
73            has_inputs: false,
74            initialized: false,
75        }
76    }
77
78    pub fn update(&mut self, best_bid: Option<Quantity>, best_ask: Option<Quantity>) {
79        self.has_inputs = true;
80        self.count += 1;
81
82        if let (Some(best_bid), Some(best_ask)) = (best_bid, best_ask) {
83            let smaller = std::cmp::min(best_bid, best_ask);
84            let larger = std::cmp::max(best_bid, best_ask);
85
86            let ratio = smaller.as_f64() / larger.as_f64();
87            self.value = ratio;
88
89            self.initialized = true;
90        }
91        // No market yet
92    }
93}
94
95////////////////////////////////////////////////////////////////////////////////
96// Tests
97////////////////////////////////////////////////////////////////////////////////
98#[cfg(test)]
99mod tests {
100    use nautilus_model::{
101        identifiers::InstrumentId,
102        stubs::{stub_order_book_mbp, stub_order_book_mbp_appl_xnas},
103    };
104    use rstest::rstest;
105
106    use super::*;
107
108    #[rstest]
109    fn test_initialized() {
110        let imbalance = BookImbalanceRatio::new();
111        let display_str = format!("{imbalance}");
112        assert_eq!(display_str, "BookImbalanceRatio()");
113        assert_eq!(imbalance.value, 0.0);
114        assert_eq!(imbalance.count, 0);
115        assert!(!imbalance.has_inputs);
116        assert!(!imbalance.initialized);
117    }
118
119    #[rstest]
120    fn test_one_value_input_balanced() {
121        let mut imbalance = BookImbalanceRatio::new();
122        let book = stub_order_book_mbp_appl_xnas();
123        imbalance.handle_book(&book);
124
125        assert_eq!(imbalance.count, 1);
126        assert_eq!(imbalance.value, 1.0);
127        assert!(imbalance.initialized);
128        assert!(imbalance.has_inputs);
129    }
130
131    #[rstest]
132    fn test_reset() {
133        let mut imbalance = BookImbalanceRatio::new();
134        let book = stub_order_book_mbp_appl_xnas();
135        imbalance.handle_book(&book);
136        imbalance.reset();
137
138        assert_eq!(imbalance.count, 0);
139        assert_eq!(imbalance.value, 0.0);
140        assert!(!imbalance.initialized);
141        assert!(!imbalance.has_inputs);
142    }
143
144    #[rstest]
145    fn test_one_value_input_with_bid_imbalance() {
146        let mut imbalance = BookImbalanceRatio::new();
147        let book = stub_order_book_mbp(
148            InstrumentId::from("AAPL.XNAS"),
149            101.0,
150            100.0,
151            200.0, // <-- Larger bid side
152            100.0,
153            2,
154            0.01,
155            0,
156            100.0,
157            10,
158        );
159        imbalance.handle_book(&book);
160
161        assert_eq!(imbalance.count, 1);
162        assert_eq!(imbalance.value, 0.5);
163        assert!(imbalance.initialized);
164        assert!(imbalance.has_inputs);
165    }
166
167    #[rstest]
168    fn test_one_value_input_with_ask_imbalance() {
169        let mut imbalance = BookImbalanceRatio::new();
170        let book = stub_order_book_mbp(
171            InstrumentId::from("AAPL.XNAS"),
172            101.0,
173            100.0,
174            100.0,
175            200.0, // <-- Larger ask side
176            2,
177            0.01,
178            0,
179            100.0,
180            10,
181        );
182        imbalance.handle_book(&book);
183
184        assert_eq!(imbalance.count, 1);
185        assert_eq!(imbalance.value, 0.5);
186        assert!(imbalance.initialized);
187        assert!(imbalance.has_inputs);
188    }
189
190    #[rstest]
191    fn test_one_value_input_with_bid_imbalance_multiple_inputs() {
192        let mut imbalance = BookImbalanceRatio::new();
193        let book = stub_order_book_mbp(
194            InstrumentId::from("AAPL.XNAS"),
195            101.0,
196            100.0,
197            200.0, // <-- Larger bid side
198            100.0,
199            2,
200            0.01,
201            0,
202            100.0,
203            10,
204        );
205        imbalance.handle_book(&book);
206        imbalance.handle_book(&book);
207        imbalance.handle_book(&book);
208
209        assert_eq!(imbalance.count, 3);
210        assert_eq!(imbalance.value, 0.5);
211        assert!(imbalance.initialized);
212        assert!(imbalance.has_inputs);
213    }
214}