Skip to main content

nautilus_model/ffi/orderbook/
book.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::ffi::c_char;
17
18use nautilus_core::ffi::{abort_on_panic, cvec::CVec, string::str_to_cstr};
19
20use crate::{
21    data::{OrderBookDeltas, QuoteTick, TradeTick},
22    enums::{BookType, OrderSide},
23    ffi::{
24        data::{delta::OrderBookDeltaFfi, depth::OrderBookDepth10Ffi, order::BookOrderFfi},
25        enums::OrderSideOptional,
26    },
27    identifiers::InstrumentId,
28    orderbook::{BookLevel, OrderBook, analysis::book_check_integrity, ladder::BookPrice},
29    types::{ERROR_PRICE, Price, Quantity, price::PriceRaw},
30};
31
32/// Returns an owning pointer to the heap-allocated `OrderBook` which the caller must
33/// eventually pass to [`orderbook_drop`].
34#[unsafe(no_mangle)]
35pub extern "C" fn orderbook_new(
36    instrument_id: InstrumentId,
37    book_type: BookType,
38) -> *mut OrderBook {
39    Box::into_raw(Box::new(OrderBook::new(instrument_id, book_type)))
40}
41
42/// # Safety
43///
44/// `book` must be a live owning pointer returned by [`orderbook_new`], and must not
45/// be used after this call.
46///
47/// # Panics
48///
49/// Panics if `book` is null.
50#[unsafe(no_mangle)]
51pub unsafe extern "C" fn orderbook_drop(book: *mut OrderBook) {
52    abort_on_panic(|| {
53        assert!(!book.is_null(), "`book` was NULL");
54        // SAFETY: Caller guarantees `book` was allocated by `orderbook_new`
55        drop(unsafe { Box::from_raw(book) }); // Memory freed here
56    });
57}
58
59#[unsafe(no_mangle)]
60pub extern "C" fn orderbook_reset(book: &mut OrderBook) {
61    book.reset();
62}
63
64#[unsafe(no_mangle)]
65pub extern "C" fn orderbook_instrument_id(book: &OrderBook) -> InstrumentId {
66    book.instrument_id
67}
68
69#[unsafe(no_mangle)]
70pub extern "C" fn orderbook_book_type(book: &OrderBook) -> BookType {
71    book.book_type
72}
73
74#[unsafe(no_mangle)]
75pub extern "C" fn orderbook_sequence(book: &OrderBook) -> u64 {
76    book.sequence
77}
78
79#[unsafe(no_mangle)]
80pub extern "C" fn orderbook_ts_last(book: &OrderBook) -> u64 {
81    book.ts_last.into()
82}
83
84#[unsafe(no_mangle)]
85pub extern "C" fn orderbook_update_count(book: &OrderBook) -> u64 {
86    book.update_count
87}
88
89#[unsafe(no_mangle)]
90#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
91pub extern "C" fn orderbook_add(
92    book: &mut OrderBook,
93    order: BookOrderFfi,
94    flags: u8,
95    sequence: u64,
96    ts_event: u64,
97) {
98    book.add(order.into(), flags, sequence, ts_event.into());
99}
100
101#[unsafe(no_mangle)]
102#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
103pub extern "C" fn orderbook_update(
104    book: &mut OrderBook,
105    order: BookOrderFfi,
106    flags: u8,
107    sequence: u64,
108    ts_event: u64,
109) {
110    book.update(order.into(), flags, sequence, ts_event.into());
111}
112
113#[unsafe(no_mangle)]
114#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
115pub extern "C" fn orderbook_delete(
116    book: &mut OrderBook,
117    order: BookOrderFfi,
118    flags: u8,
119    sequence: u64,
120    ts_event: u64,
121) {
122    book.delete(order.into(), flags, sequence, ts_event.into());
123}
124
125#[unsafe(no_mangle)]
126pub extern "C" fn orderbook_clear(book: &mut OrderBook, sequence: u64, ts_event: u64) {
127    book.clear(sequence, ts_event.into());
128}
129
130#[unsafe(no_mangle)]
131pub extern "C" fn orderbook_clear_bids(book: &mut OrderBook, sequence: u64, ts_event: u64) {
132    book.clear_bids(sequence, ts_event.into());
133}
134
135#[unsafe(no_mangle)]
136pub extern "C" fn orderbook_clear_asks(book: &mut OrderBook, sequence: u64, ts_event: u64) {
137    book.clear_asks(sequence, ts_event.into());
138}
139
140#[unsafe(no_mangle)]
141pub extern "C" fn orderbook_apply_delta(book: &mut OrderBook, delta: &OrderBookDeltaFfi) {
142    if let Err(e) = book.apply_delta_unchecked(&(*delta).into()) {
143        log::error!("Failed to apply order book delta: {e}");
144    }
145}
146
147#[unsafe(no_mangle)]
148pub extern "C" fn orderbook_apply_deltas(book: &mut OrderBook, deltas: &OrderBookDeltas) {
149    // Clone will actually copy the contents of the `deltas` vec
150    if let Err(e) = book.apply_deltas_unchecked(deltas) {
151        log::error!("Failed to apply order book deltas: {e}");
152    }
153}
154
155/// Creates an `OrderBookDeltas` snapshot from the current order book state.
156///
157/// This is the reverse operation of `orderbook_apply_deltas`: it converts the current book state
158/// back into a snapshot format with a `Clear` delta followed by `Add` deltas for all orders.
159///
160/// # Parameters
161///
162/// - `book` - The order book to convert.
163/// - `ts_event` - UNIX timestamp (nanoseconds) when the book event occurred.
164/// - `ts_init` - UNIX timestamp (nanoseconds) when the instance was created.
165///
166/// # Returns
167///
168/// An owning pointer to an `OrderBookDeltas` containing a snapshot of the current order book
169/// state, which the caller must eventually pass to [`crate::ffi::data::deltas::orderbook_deltas_drop`].
170#[unsafe(no_mangle)]
171pub extern "C" fn orderbook_to_snapshot_deltas(
172    book: &OrderBook,
173    ts_event: u64,
174    ts_init: u64,
175) -> *mut OrderBookDeltas {
176    use nautilus_core::UnixNanos;
177    Box::into_raw(Box::new(
178        book.to_deltas(UnixNanos::from(ts_event), UnixNanos::from(ts_init)),
179    ))
180}
181
182#[unsafe(no_mangle)]
183pub extern "C" fn orderbook_apply_depth(book: &mut OrderBook, depth: &OrderBookDepth10Ffi) {
184    if let Err(e) = book.apply_depth_unchecked(&(*depth).into()) {
185        log::error!("Failed to apply order book depth: {e}");
186    }
187}
188
189#[unsafe(no_mangle)]
190pub extern "C" fn orderbook_bids(book: &mut OrderBook) -> CVec {
191    book.bids
192        .levels
193        .values()
194        .map(|level| Box::into_raw(Box::new(level.clone())))
195        .collect::<Vec<*mut BookLevel>>()
196        .into()
197}
198
199#[unsafe(no_mangle)]
200pub extern "C" fn orderbook_asks(book: &mut OrderBook) -> CVec {
201    book.asks
202        .levels
203        .values()
204        .map(|level| Box::into_raw(Box::new(level.clone())))
205        .collect::<Vec<*mut BookLevel>>()
206        .into()
207}
208
209#[unsafe(no_mangle)]
210#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
211pub extern "C" fn orderbook_bids_down_to(
212    book: &mut OrderBook,
213    price_raw: PriceRaw,
214    price_prec: u8,
215) -> CVec {
216    let price = Price::from_raw(price_raw, price_prec);
217    let bound = BookPrice::new(price, OrderSide::Buy);
218    book.bids
219        .levels
220        .range(..=bound)
221        .map(|(_, level)| Box::into_raw(Box::new(level.clone())))
222        .collect::<Vec<*mut BookLevel>>()
223        .into()
224}
225
226#[unsafe(no_mangle)]
227#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
228pub extern "C" fn orderbook_asks_up_to(
229    book: &mut OrderBook,
230    price_raw: PriceRaw,
231    price_prec: u8,
232) -> CVec {
233    let price = Price::from_raw(price_raw, price_prec);
234    let bound = BookPrice::new(price, OrderSide::Sell);
235    book.asks
236        .levels
237        .range(..=bound)
238        .map(|(_, level)| Box::into_raw(Box::new(level.clone())))
239        .collect::<Vec<*mut BookLevel>>()
240        .into()
241}
242
243#[unsafe(no_mangle)]
244pub extern "C" fn orderbook_has_bid(book: &mut OrderBook) -> u8 {
245    u8::from(book.has_bid())
246}
247
248#[unsafe(no_mangle)]
249pub extern "C" fn orderbook_has_ask(book: &mut OrderBook) -> u8 {
250    u8::from(book.has_ask())
251}
252
253/// # Panics
254///
255/// Panics if there are no bid orders for best bid price.
256#[unsafe(no_mangle)]
257#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
258pub extern "C" fn orderbook_best_bid_price(book: &mut OrderBook) -> Price {
259    abort_on_panic(|| {
260        book.best_bid_price()
261            .expect("Error: No bid orders for best bid price")
262    })
263}
264
265/// # Panics
266///
267/// Panics if there are no ask orders for best ask price.
268#[unsafe(no_mangle)]
269#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
270pub extern "C" fn orderbook_best_ask_price(book: &mut OrderBook) -> Price {
271    abort_on_panic(|| {
272        book.best_ask_price()
273            .expect("Error: No ask orders for best ask price")
274    })
275}
276
277/// # Panics
278///
279/// Panics if there are no bid orders for best bid size.
280#[unsafe(no_mangle)]
281#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
282pub extern "C" fn orderbook_best_bid_size(book: &mut OrderBook) -> Quantity {
283    abort_on_panic(|| {
284        book.best_bid_size()
285            .expect("Error: No bid orders for best bid size")
286    })
287}
288
289/// # Panics
290///
291/// Panics if there are no ask orders for best ask size.
292#[unsafe(no_mangle)]
293#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
294pub extern "C" fn orderbook_best_ask_size(book: &mut OrderBook) -> Quantity {
295    abort_on_panic(|| {
296        book.best_ask_size()
297            .expect("Error: No ask orders for best ask size")
298    })
299}
300
301/// # Panics
302///
303/// Panics if unable to calculate spread (requires at least one bid and one ask).
304#[unsafe(no_mangle)]
305pub extern "C" fn orderbook_spread(book: &mut OrderBook) -> f64 {
306    abort_on_panic(|| {
307        book.spread()
308            .expect("Error: Unable to calculate `spread` (no bid or ask)")
309    })
310}
311
312/// # Panics
313///
314/// Panics if unable to calculate midpoint (requires at least one bid and one ask).
315#[unsafe(no_mangle)]
316pub extern "C" fn orderbook_midpoint(book: &mut OrderBook) -> f64 {
317    abort_on_panic(|| {
318        book.midpoint()
319            .expect("Error: Unable to calculate `midpoint` (no bid or ask)")
320    })
321}
322
323/// # Panics
324///
325/// Panics if `order_side` is `NoOrderSide`.
326#[unsafe(no_mangle)]
327#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
328pub extern "C" fn orderbook_get_avg_px_for_quantity(
329    book: &mut OrderBook,
330    qty: Quantity,
331    order_side: OrderSideOptional,
332) -> f64 {
333    book.get_avg_px_for_quantity(
334        qty,
335        order_side
336            .as_option()
337            .expect("Order side must be Buy or Sell"),
338    )
339}
340
341/// # Panics
342///
343/// Panics if `order_side` is `NoOrderSide`.
344#[unsafe(no_mangle)]
345#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
346pub extern "C" fn orderbook_get_worst_px_for_quantity(
347    book: &mut OrderBook,
348    qty: Quantity,
349    order_side: OrderSideOptional,
350) -> Price {
351    book.get_worst_px_for_quantity(
352        qty,
353        order_side
354            .as_option()
355            .expect("Order side must be Buy or Sell"),
356    )
357    .unwrap_or(ERROR_PRICE)
358}
359
360/// # Panics
361///
362/// Panics if `order_side` is `NoOrderSide`.
363#[unsafe(no_mangle)]
364#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
365pub extern "C" fn orderbook_get_quantity_for_price(
366    book: &mut OrderBook,
367    price: Price,
368    order_side: OrderSideOptional,
369) -> f64 {
370    book.get_quantity_for_price(
371        price,
372        order_side
373            .as_option()
374            .expect("Order side must be Buy or Sell"),
375    )
376}
377
378/// # Panics
379///
380/// Panics if `order_side` is `NoOrderSide`.
381#[unsafe(no_mangle)]
382#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
383pub extern "C" fn orderbook_get_quantity_at_level(
384    book: &OrderBook,
385    price: Price,
386    order_side: OrderSideOptional,
387    size_precision: u8,
388) -> Quantity {
389    book.get_quantity_at_level(
390        price,
391        order_side
392            .as_option()
393            .expect("Order side must be Buy or Sell"),
394        size_precision,
395    )
396}
397
398/// Updates the order book with a quote tick.
399///
400/// # Panics
401///
402/// Panics if book type is not `L1_MBP`.
403#[unsafe(no_mangle)]
404pub extern "C" fn orderbook_update_quote_tick(book: &mut OrderBook, quote: &QuoteTick) {
405    book.update_quote_tick(quote).unwrap();
406}
407
408/// Updates the order book with a trade tick.
409///
410/// # Panics
411///
412/// Panics if book type is not `L1_MBP`.
413#[unsafe(no_mangle)]
414pub extern "C" fn orderbook_update_trade_tick(book: &mut OrderBook, trade: &TradeTick) {
415    book.update_trade_tick(trade).unwrap();
416}
417
418/// # Panics
419///
420/// Panics if `order.side` is `NoOrderSide`.
421#[unsafe(no_mangle)]
422#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
423pub extern "C" fn orderbook_simulate_fills(book: &OrderBook, order: BookOrderFfi) -> CVec {
424    book.simulate_fills(&order.into()).into()
425}
426
427/// # Panics
428///
429/// Panics if `order_side` is `NoOrderSide`.
430#[unsafe(no_mangle)]
431#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
432pub extern "C" fn orderbook_get_all_crossed_levels(
433    book: &OrderBook,
434    order_side: OrderSideOptional,
435    price: Price,
436    size_precision: u8,
437) -> CVec {
438    book.get_all_crossed_levels(
439        order_side
440            .as_option()
441            .expect("Order side must be Buy or Sell"),
442        price,
443        size_precision,
444    )
445    .into()
446}
447
448#[unsafe(no_mangle)]
449pub extern "C" fn orderbook_check_integrity(book: &OrderBook) -> u8 {
450    u8::from(book_check_integrity(book).is_ok())
451}
452
453/// # Safety
454///
455/// `v` must uniquely own a valid `Vec<(Price, Quantity)>` allocation transferred from Rust.
456#[unsafe(no_mangle)]
457pub unsafe extern "C" fn vec_drop_fills(v: CVec) {
458    let data = unsafe { v.into_vec::<(Price, Quantity)>() };
459    drop(data); // Memory freed here
460}
461
462/// Returns a pretty printed `OrderBook` number of levels per side, as a C string pointer.
463#[unsafe(no_mangle)]
464pub extern "C" fn orderbook_pprint_to_cstr(book: &OrderBook, num_levels: usize) -> *const c_char {
465    str_to_cstr(&book.pprint(num_levels, None))
466}
467
468#[cfg(test)]
469mod cvec_tests {
470    use rstest::rstest;
471
472    use super::*;
473
474    #[rstest]
475    fn test_empty_fills_drop_returns_without_panic() {
476        unsafe { vec_drop_fills(CVec::empty()) };
477    }
478}