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#[cfg(test)]
96mod tests {
97    use nautilus_model::{
98        identifiers::InstrumentId,
99        stubs::{stub_order_book_mbp, stub_order_book_mbp_appl_xnas},
100    };
101    use rstest::rstest;
102
103    use super::*;
104
105    #[rstest]
106    fn test_initialized() {
107        let imbalance = BookImbalanceRatio::new();
108        let display_str = format!("{imbalance}");
109        assert_eq!(display_str, "BookImbalanceRatio()");
110        assert_eq!(imbalance.value, 0.0);
111        assert_eq!(imbalance.count, 0);
112        assert!(!imbalance.has_inputs);
113        assert!(!imbalance.initialized);
114    }
115
116    #[rstest]
117    fn test_one_value_input_balanced() {
118        let mut imbalance = BookImbalanceRatio::new();
119        let book = stub_order_book_mbp_appl_xnas();
120        imbalance.handle_book(&book);
121
122        assert_eq!(imbalance.count, 1);
123        assert_eq!(imbalance.value, 1.0);
124        assert!(imbalance.initialized);
125        assert!(imbalance.has_inputs);
126    }
127
128    #[rstest]
129    fn test_reset() {
130        let mut imbalance = BookImbalanceRatio::new();
131        let book = stub_order_book_mbp_appl_xnas();
132        imbalance.handle_book(&book);
133        imbalance.reset();
134
135        assert_eq!(imbalance.count, 0);
136        assert_eq!(imbalance.value, 0.0);
137        assert!(!imbalance.initialized);
138        assert!(!imbalance.has_inputs);
139    }
140
141    #[rstest]
142    fn test_one_value_input_with_bid_imbalance() {
143        let mut imbalance = BookImbalanceRatio::new();
144        let book = stub_order_book_mbp(
145            InstrumentId::from("AAPL.XNAS"),
146            101.0,
147            100.0,
148            200.0, // <-- Larger bid side
149            100.0,
150            2,
151            0.01,
152            0,
153            100.0,
154            10,
155        );
156        imbalance.handle_book(&book);
157
158        assert_eq!(imbalance.count, 1);
159        assert_eq!(imbalance.value, 0.5);
160        assert!(imbalance.initialized);
161        assert!(imbalance.has_inputs);
162    }
163
164    #[rstest]
165    fn test_one_value_input_with_ask_imbalance() {
166        let mut imbalance = BookImbalanceRatio::new();
167        let book = stub_order_book_mbp(
168            InstrumentId::from("AAPL.XNAS"),
169            101.0,
170            100.0,
171            100.0,
172            200.0, // <-- Larger ask side
173            2,
174            0.01,
175            0,
176            100.0,
177            10,
178        );
179        imbalance.handle_book(&book);
180
181        assert_eq!(imbalance.count, 1);
182        assert_eq!(imbalance.value, 0.5);
183        assert!(imbalance.initialized);
184        assert!(imbalance.has_inputs);
185    }
186
187    #[rstest]
188    fn test_one_value_input_with_bid_imbalance_multiple_inputs() {
189        let mut imbalance = BookImbalanceRatio::new();
190        let book = stub_order_book_mbp(
191            InstrumentId::from("AAPL.XNAS"),
192            101.0,
193            100.0,
194            200.0, // <-- Larger bid side
195            100.0,
196            2,
197            0.01,
198            0,
199            100.0,
200            10,
201        );
202        imbalance.handle_book(&book);
203        imbalance.handle_book(&book);
204        imbalance.handle_book(&book);
205
206        assert_eq!(imbalance.count, 3);
207        assert_eq!(imbalance.value, 0.5);
208        assert!(imbalance.initialized);
209        assert!(imbalance.has_inputs);
210    }
211}