Skip to main content

nautilus_model/ffi/data/
depth.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::{
17    collections::hash_map::DefaultHasher,
18    hash::{Hash, Hasher},
19};
20
21use nautilus_core::{UnixNanos, ffi::abort_on_panic};
22
23use crate::{
24    data::depth::{DEPTH10_LEN, OrderBookDepth},
25    ffi::data::order::BookOrderFfi,
26    identifiers::InstrumentId,
27};
28
29/// The stable C representation of an [`OrderBookDepth`].
30#[repr(C)]
31#[derive(Clone, Copy, Debug)]
32pub struct OrderBookDepth10Ffi {
33    pub instrument_id: InstrumentId,
34    pub bids: [BookOrderFfi; DEPTH10_LEN],
35    pub asks: [BookOrderFfi; DEPTH10_LEN],
36    pub bid_counts: [u32; DEPTH10_LEN],
37    pub ask_counts: [u32; DEPTH10_LEN],
38    pub flags: u8,
39    pub sequence: u64,
40    pub ts_event: UnixNanos,
41    pub ts_init: UnixNanos,
42}
43
44impl From<OrderBookDepth10Ffi> for OrderBookDepth {
45    fn from(value: OrderBookDepth10Ffi) -> Self {
46        Self {
47            instrument_id: value.instrument_id,
48            bids: value.bids.map(Into::into).into(),
49            asks: value.asks.map(Into::into).into(),
50            bid_counts: value.bid_counts.into(),
51            ask_counts: value.ask_counts.into(),
52            flags: value.flags,
53            sequence: value.sequence,
54            ts_event: value.ts_event,
55            ts_init: value.ts_init,
56        }
57    }
58}
59
60impl TryFrom<OrderBookDepth> for OrderBookDepth10Ffi {
61    type Error = anyhow::Error;
62
63    fn try_from(value: OrderBookDepth) -> Result<Self, Self::Error> {
64        anyhow::ensure!(
65            [
66                value.bids.len(),
67                value.asks.len(),
68                value.bid_counts.len(),
69                value.ask_counts.len()
70            ]
71            .into_iter()
72            .all(|len| len == DEPTH10_LEN),
73            "The legacy depth FFI requires exactly ten levels per side"
74        );
75        Ok(Self {
76            instrument_id: value.instrument_id,
77            bids: std::array::from_fn(|i| value.bids[i].into()),
78            asks: std::array::from_fn(|i| value.asks[i].into()),
79            bid_counts: std::array::from_fn(|i| value.bid_counts[i]),
80            ask_counts: std::array::from_fn(|i| value.ask_counts[i]),
81            flags: value.flags,
82            sequence: value.sequence,
83            ts_event: value.ts_event,
84            ts_init: value.ts_init,
85        })
86    }
87}
88
89/// # Safety
90///
91/// This function assumes:
92/// - `bids` and `asks` are valid pointers to arrays of `BookOrderFfi` of length 10.
93/// - `bid_counts` and `ask_counts` are valid pointers to arrays of `u32` of length 10.
94///
95/// # Panics
96///
97/// Panics if any input pointer is null or if slice conversion for bids or asks fails.
98#[unsafe(no_mangle)]
99#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
100pub unsafe extern "C" fn orderbook_depth10_new(
101    instrument_id: InstrumentId,
102    bids_ptr: *const BookOrderFfi,
103    asks_ptr: *const BookOrderFfi,
104    bid_counts_ptr: *const u32,
105    ask_counts_ptr: *const u32,
106    flags: u8,
107    sequence: u64,
108    ts_event: UnixNanos,
109    ts_init: UnixNanos,
110) -> OrderBookDepth10Ffi {
111    abort_on_panic(|| {
112        // SAFETY: Null checks run before slice construction. The caller still
113        // guarantees each pointer refers to `DEPTH10_LEN` initialized elements.
114        assert!(!bids_ptr.is_null());
115        assert!(!asks_ptr.is_null());
116        assert!(!bid_counts_ptr.is_null());
117        assert!(!ask_counts_ptr.is_null());
118
119        let bids_slice = unsafe { std::slice::from_raw_parts(bids_ptr, DEPTH10_LEN) };
120        let asks_slice = unsafe { std::slice::from_raw_parts(asks_ptr, DEPTH10_LEN) };
121        let bids: [BookOrderFfi; DEPTH10_LEN] = bids_slice.try_into().expect("Slice length != 10");
122        let asks: [BookOrderFfi; DEPTH10_LEN] = asks_slice.try_into().expect("Slice length != 10");
123
124        let bid_counts_slice = unsafe { std::slice::from_raw_parts(bid_counts_ptr, DEPTH10_LEN) };
125        let ask_counts_slice = unsafe { std::slice::from_raw_parts(ask_counts_ptr, DEPTH10_LEN) };
126        let bid_counts: [u32; DEPTH10_LEN] =
127            bid_counts_slice.try_into().expect("Slice length != 10");
128        let ask_counts: [u32; DEPTH10_LEN] =
129            ask_counts_slice.try_into().expect("Slice length != 10");
130
131        OrderBookDepth10Ffi {
132            instrument_id,
133            bids,
134            asks,
135            bid_counts,
136            ask_counts,
137            flags,
138            sequence,
139            ts_event,
140            ts_init,
141        }
142    })
143}
144
145#[unsafe(no_mangle)]
146#[cfg_attr(feature = "high-precision", allow(improper_ctypes_definitions))]
147pub extern "C" fn orderbook_depth10_clone(depth: &OrderBookDepth10Ffi) -> OrderBookDepth10Ffi {
148    *depth
149}
150
151#[unsafe(no_mangle)]
152pub extern "C" fn orderbook_depth10_eq(lhs: &OrderBookDepth10Ffi, rhs: &OrderBookDepth10Ffi) -> u8 {
153    u8::from(OrderBookDepth::from(*lhs) == OrderBookDepth::from(*rhs))
154}
155
156#[unsafe(no_mangle)]
157pub extern "C" fn orderbook_depth10_hash(delta: &OrderBookDepth10Ffi) -> u64 {
158    let mut hasher = DefaultHasher::new();
159    OrderBookDepth::from(*delta).hash(&mut hasher);
160    hasher.finish()
161}
162
163#[unsafe(no_mangle)]
164pub extern "C" fn orderbook_depth10_bids_array(depth: &OrderBookDepth10Ffi) -> *const BookOrderFfi {
165    depth.bids.as_ptr()
166}
167
168#[unsafe(no_mangle)]
169pub extern "C" fn orderbook_depth10_asks_array(depth: &OrderBookDepth10Ffi) -> *const BookOrderFfi {
170    depth.asks.as_ptr()
171}
172
173#[unsafe(no_mangle)]
174pub extern "C" fn orderbook_depth10_bid_counts_array(depth: &OrderBookDepth10Ffi) -> *const u32 {
175    depth.bid_counts.as_ptr()
176}
177
178#[unsafe(no_mangle)]
179pub extern "C" fn orderbook_depth10_ask_counts_array(depth: &OrderBookDepth10Ffi) -> *const u32 {
180    depth.ask_counts.as_ptr()
181}
182
183#[cfg(test)]
184mod tests {
185    use rstest::rstest;
186
187    use super::*;
188    use crate::data::{BookOrder, stubs::stub_depth10};
189
190    #[rstest]
191    #[case::populated(10, 10)]
192    #[case::padded(3, 6)]
193    #[case::empty(0, 0)]
194    fn legacy_constructor_preserves_slots_and_metadata(
195        #[case] bid_levels: usize,
196        #[case] ask_levels: usize,
197    ) {
198        let depth = stub_depth10();
199        let bids: [BookOrderFfi; DEPTH10_LEN] = std::array::from_fn(|i| {
200            if i < bid_levels {
201                depth.bids[i].into()
202            } else {
203                BookOrder::default().into()
204            }
205        });
206        let asks: [BookOrderFfi; DEPTH10_LEN] = std::array::from_fn(|i| {
207            if i < ask_levels {
208                depth.asks[i].into()
209            } else {
210                BookOrder::default().into()
211            }
212        });
213        let bid_counts = std::array::from_fn::<_, DEPTH10_LEN, _>(|i| i as u32 + 11);
214        let ask_counts = std::array::from_fn::<_, DEPTH10_LEN, _>(|i| i as u32 + 31);
215
216        // SAFETY: All pointers refer to live arrays with exactly DEPTH10_LEN initialized slots
217        let actual = unsafe {
218            orderbook_depth10_new(
219                depth.instrument_id,
220                bids.as_ptr(),
221                asks.as_ptr(),
222                bid_counts.as_ptr(),
223                ask_counts.as_ptr(),
224                17,
225                23,
226                UnixNanos::from(41),
227                UnixNanos::from(43),
228            )
229        };
230
231        assert_eq!(actual.instrument_id, depth.instrument_id);
232        for (actual, expected) in actual
233            .bids
234            .into_iter()
235            .chain(actual.asks)
236            .zip(bids.into_iter().chain(asks))
237        {
238            assert_eq!(
239                (
240                    actual.side,
241                    actual.price.raw,
242                    actual.price.precision,
243                    actual.size.raw,
244                    actual.size.precision,
245                    actual.order_id
246                ),
247                (
248                    expected.side,
249                    expected.price.raw,
250                    expected.price.precision,
251                    expected.size.raw,
252                    expected.size.precision,
253                    expected.order_id
254                ),
255            );
256        }
257        assert_eq!(actual.bid_counts, bid_counts);
258        assert_eq!(actual.ask_counts, ask_counts);
259        assert_eq!(
260            (
261                actual.flags,
262                actual.sequence,
263                actual.ts_event,
264                actual.ts_init
265            ),
266            (17, 23, UnixNanos::from(41), UnixNanos::from(43))
267        );
268    }
269
270    #[rstest]
271    fn legacy_depth_conversion_preserves_all_fields() {
272        let mut depth = stub_depth10();
273        depth.flags = 31;
274        depth.sequence = 23;
275        depth.ts_event = UnixNanos::from(41);
276        depth.ts_init = UnixNanos::from(43);
277        depth.bid_counts = (1..=10).collect();
278        depth.ask_counts = (11..=20).collect();
279        let ffi = OrderBookDepth10Ffi::try_from(depth.clone()).unwrap();
280        assert_eq!(ffi.instrument_id, depth.instrument_id);
281        assert_eq!(
282            ffi.bids.map(BookOrder::from).as_slice(),
283            depth.bids.as_slice()
284        );
285        assert_eq!(
286            ffi.asks.map(BookOrder::from).as_slice(),
287            depth.asks.as_slice()
288        );
289        assert_eq!(ffi.bid_counts.as_slice(), depth.bid_counts.as_slice());
290        assert_eq!(ffi.ask_counts.as_slice(), depth.ask_counts.as_slice());
291        assert_eq!(
292            (ffi.flags, ffi.sequence, ffi.ts_event, ffi.ts_init),
293            (31, 23, UnixNanos::from(41), UnixNanos::from(43))
294        );
295        assert_eq!(OrderBookDepth::from(ffi), depth);
296    }
297
298    #[rstest]
299    #[case(0)]
300    #[case(9)]
301    #[case(11)]
302    fn legacy_depth_conversion_rejects_other_depths(#[case] levels: usize) {
303        let mut depth = stub_depth10();
304        depth.bids.resize(levels, depth.bids[0]);
305        depth.bid_counts.resize(levels, 1);
306        let error = OrderBookDepth10Ffi::try_from(depth).unwrap_err();
307        assert_eq!(
308            error.to_string(),
309            "The legacy depth FFI requires exactly ten levels per side"
310        );
311    }
312}