Skip to main content

nautilus_indicators/book/
imbalance.rs

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