diff --git a/nautilus_core/adapters/src/databento/parsing.rs b/nautilus_core/adapters/src/databento/parsing.rs index 0062f6ed5f34..9f464499f2d0 100644 --- a/nautilus_core/adapters/src/databento/parsing.rs +++ b/nautilus_core/adapters/src/databento/parsing.rs @@ -359,6 +359,8 @@ pub fn parse_mbp10_msg( ) -> Result { let mut bids = Vec::with_capacity(DEPTH10_LEN); let mut asks = Vec::with_capacity(DEPTH10_LEN); + let mut bid_counts = Vec::with_capacity(DEPTH10_LEN); + let mut ask_counts = Vec::with_capacity(DEPTH10_LEN); for level in &record.levels { let bid_order = BookOrder::new( @@ -377,15 +379,21 @@ pub fn parse_mbp10_msg( bids.push(bid_order); asks.push(ask_order); + bid_counts.push(level.bid_ct); + ask_counts.push(level.ask_ct); } - let bids: [BookOrder; DEPTH10_LEN] = bids.try_into().expect("Bids `Vec` length mismatch"); - let asks: [BookOrder; DEPTH10_LEN] = asks.try_into().expect("Asks `Vec` length mismatch"); + let bids: [BookOrder; DEPTH10_LEN] = bids.try_into().expect("`bids` length != 10"); + let asks: [BookOrder; DEPTH10_LEN] = asks.try_into().expect("`asks` length != 10"); + let bid_counts: [u32; DEPTH10_LEN] = bid_counts.try_into().expect("`bid_counts` length != 10"); + let ask_counts: [u32; DEPTH10_LEN] = ask_counts.try_into().expect("`ask_counts` length != 10"); let depth = OrderBookDepth10::new( instrument_id, bids, asks, + bid_counts, + ask_counts, record.flags, record.sequence.into(), record.ts_recv, diff --git a/nautilus_core/model/cbindgen_cython.toml b/nautilus_core/model/cbindgen_cython.toml index d74b2001447a..b9ab372505d0 100644 --- a/nautilus_core/model/cbindgen_cython.toml +++ b/nautilus_core/model/cbindgen_cython.toml @@ -12,6 +12,7 @@ header = '"../includes/model.h"' "libc.stdint" = [ "uint8_t", "uint16_t", + "uint32_t", "uint64_t", "uintptr_t", "int64_t", diff --git a/nautilus_core/model/src/data/depth.rs b/nautilus_core/model/src/data/depth.rs index 7a3415c21f3a..d434f1738ed0 100644 --- a/nautilus_core/model/src/data/depth.rs +++ b/nautilus_core/model/src/data/depth.rs @@ -46,6 +46,10 @@ pub struct OrderBookDepth10 { pub bids: [BookOrder; DEPTH10_LEN], /// The ask orders for the depth update. pub asks: [BookOrder; DEPTH10_LEN], + /// The count of bid orders per level for the depth update. + pub bid_counts: [u32; DEPTH10_LEN], + /// The count of ask orders per level for the depth update. + pub ask_counts: [u32; DEPTH10_LEN], /// A combination of packet end with matching engine status. pub flags: u8, /// The message sequence number assigned at the venue. @@ -63,6 +67,8 @@ impl OrderBookDepth10 { instrument_id: InstrumentId, bids: [BookOrder; DEPTH10_LEN], asks: [BookOrder; DEPTH10_LEN], + bid_counts: [u32; DEPTH10_LEN], + ask_counts: [u32; DEPTH10_LEN], flags: u8, sequence: u64, ts_event: UnixNanos, @@ -72,6 +78,8 @@ impl OrderBookDepth10 { instrument_id, bids, asks, + bid_counts, + ask_counts, flags, sequence, ts_event, @@ -159,10 +167,15 @@ pub mod stubs { order_id += 1; } + let bid_counts: [u32; 10] = [1; 10]; + let ask_counts: [u32; 10] = [1; 10]; + OrderBookDepth10::new( instrument_id, bids, asks, + bid_counts, + ask_counts, flags, sequence, ts_event, @@ -196,6 +209,8 @@ mod tests { assert_eq!(depth.asks[0].price.as_f64(), 100.0); assert_eq!(depth.bids[0].price.as_f64(), 99.0); assert_eq!(depth.bids[9].price.as_f64(), 90.0); + assert_eq!(depth.bid_counts.len(), 10); + assert_eq!(depth.ask_counts.len(), 10); assert_eq!(depth.flags, flags); assert_eq!(depth.sequence, sequence); assert_eq!(depth.ts_event, ts_event); diff --git a/nautilus_core/model/src/ffi/data/depth.rs b/nautilus_core/model/src/ffi/data/depth.rs index fba2bacd3697..b5bcb017872a 100644 --- a/nautilus_core/model/src/ffi/data/depth.rs +++ b/nautilus_core/model/src/ffi/data/depth.rs @@ -31,12 +31,14 @@ use crate::{ /// # Safety /// /// - Assumes `bids` and `asks` are valid pointers to arrays of `BookOrder` of length 10. -/// - Assumes Rust now takes ownership of the memory for `bids` and `asks`. +/// - Assumes `bid_counts` and `ask_counts` are valid pointers to arrays of `u32` of length 10. #[no_mangle] pub unsafe extern "C" fn orderbook_depth10_new( instrument_id: InstrumentId, bids_ptr: *const BookOrder, asks_ptr: *const BookOrder, + bid_counts_ptr: *const u32, + ask_counts_ptr: *const u32, flags: u8, sequence: u64, ts_event: UnixNanos, @@ -46,16 +48,25 @@ pub unsafe extern "C" fn orderbook_depth10_new( // The caller must guarantee that they point to arrays of `BookOrder` of at least `DEPTH10_LEN` length. assert!(!bids_ptr.is_null()); assert!(!asks_ptr.is_null()); + assert!(!bid_counts_ptr.is_null()); + assert!(!ask_counts_ptr.is_null()); let bids_slice = std::slice::from_raw_parts(bids_ptr, DEPTH10_LEN); let asks_slice = std::slice::from_raw_parts(asks_ptr, DEPTH10_LEN); - let bids: [BookOrder; DEPTH10_LEN] = bids_slice.try_into().expect("Slice length mismatch"); - let asks: [BookOrder; DEPTH10_LEN] = asks_slice.try_into().expect("Slice length mismatch"); + let bids: [BookOrder; DEPTH10_LEN] = bids_slice.try_into().expect("Slice length != 10"); + let asks: [BookOrder; DEPTH10_LEN] = asks_slice.try_into().expect("Slice length != 10"); + + let bid_counts_slice = std::slice::from_raw_parts(bid_counts_ptr, DEPTH10_LEN); + let ask_counts_slice = std::slice::from_raw_parts(ask_counts_ptr, DEPTH10_LEN); + let bid_counts: [u32; DEPTH10_LEN] = bid_counts_slice.try_into().expect("Slice length != 10"); + let ask_counts: [u32; DEPTH10_LEN] = ask_counts_slice.try_into().expect("Slice length != 10"); OrderBookDepth10::new( instrument_id, bids, asks, + bid_counts, + ask_counts, flags, sequence, ts_event, @@ -84,3 +95,13 @@ pub extern "C" fn orderbook_depth10_bids_array(depth: &OrderBookDepth10) -> *con pub extern "C" fn orderbook_depth10_asks_array(depth: &OrderBookDepth10) -> *const BookOrder { depth.asks.as_ptr() } + +#[no_mangle] +pub extern "C" fn orderbook_depth10_bid_counts_array(depth: &OrderBookDepth10) -> *const u32 { + depth.bid_counts.as_ptr() +} + +#[no_mangle] +pub extern "C" fn orderbook_depth10_ask_counts_array(depth: &OrderBookDepth10) -> *const u32 { + depth.ask_counts.as_ptr() +} diff --git a/nautilus_core/model/src/python/data/depth.rs b/nautilus_core/model/src/python/data/depth.rs index 29c9ae1c2fee..99a4485a13ae 100644 --- a/nautilus_core/model/src/python/data/depth.rs +++ b/nautilus_core/model/src/python/data/depth.rs @@ -22,18 +22,24 @@ use nautilus_core::time::UnixNanos; use pyo3::{prelude::*, pyclass::CompareOp}; use crate::{ - data::{depth::OrderBookDepth10, order::BookOrder}, + data::{ + depth::{OrderBookDepth10, DEPTH10_LEN}, + order::BookOrder, + }, identifiers::instrument_id::InstrumentId, python::PY_MODULE_MODEL, }; #[pymethods] impl OrderBookDepth10 { + #[allow(clippy::too_many_arguments)] #[new] fn py_new( instrument_id: InstrumentId, - bids: [BookOrder; 10], - asks: [BookOrder; 10], + bids: [BookOrder; DEPTH10_LEN], + asks: [BookOrder; DEPTH10_LEN], + bid_counts: [u32; DEPTH10_LEN], + ask_counts: [u32; DEPTH10_LEN], flags: u8, sequence: u64, ts_event: UnixNanos, @@ -43,6 +49,8 @@ impl OrderBookDepth10 { instrument_id, bids, asks, + bid_counts, + ask_counts, flags, sequence, ts_event, @@ -78,15 +86,25 @@ impl OrderBookDepth10 { } #[getter] - fn bids(&self) -> [BookOrder; 10] { + fn bids(&self) -> [BookOrder; DEPTH10_LEN] { self.bids } #[getter] - fn asks(&self) -> [BookOrder; 10] { + fn asks(&self) -> [BookOrder; DEPTH10_LEN] { self.asks } + #[getter] + fn bid_counts(&self) -> [u32; DEPTH10_LEN] { + self.bid_counts + } + + #[getter] + fn ask_counts(&self) -> [u32; DEPTH10_LEN] { + self.ask_counts + } + #[getter] fn flags(&self) -> u8 { self.flags diff --git a/nautilus_trader/adapters/databento/parsing.py b/nautilus_trader/adapters/databento/parsing.py index 7782903bd28f..f6f8778eaba8 100644 --- a/nautilus_trader/adapters/databento/parsing.py +++ b/nautilus_trader/adapters/databento/parsing.py @@ -30,8 +30,9 @@ from nautilus_trader.model.data import Bar from nautilus_trader.model.data import BarSpecification from nautilus_trader.model.data import BarType +from nautilus_trader.model.data import BookOrder from nautilus_trader.model.data import OrderBookDelta -from nautilus_trader.model.data import OrderBookDeltas +from nautilus_trader.model.data import OrderBookDepth10 from nautilus_trader.model.data import QuoteTick from nautilus_trader.model.data import TradeTick from nautilus_trader.model.enums import AggregationSource @@ -305,45 +306,46 @@ def parse_mbp10_msg( record: databento.MBP10Msg, instrument_id: InstrumentId, ts_init: int, -) -> OrderBookDeltas: - bids: list[OrderBookDelta] = [] - asks: list[OrderBookDelta] = [] +) -> OrderBookDepth10: + bids: list[BookOrder] = [] + asks: list[BookOrder] = [] + bid_counts: list[int] = [] + ask_counts: list[int] = [] for level in record.levels: - bid = OrderBookDelta.from_raw( - instrument_id=instrument_id, - action=BookAction.ADD, + bid = BookOrder.from_raw( side=OrderSide.BUY, price_raw=level.bid_px, - price_prec=USD.precision, # TODO(per instrument precision) - size_raw=int(level.bid_sz * FIXED_SCALAR), # No fractional sizes + price_prec=USD.precision, + size_raw=int(level.bid_sz * FIXED_SCALAR), size_prec=0, # No fractional units order_id=0, # No order ID for MBP level - flags=record.flags, - sequence=record.sequence, - ts_event=record.ts_recv, # More accurate and reliable timestamp - ts_init=ts_init, ) bids.append(bid) + bid_counts.append(level.bid_ct) - ask = OrderBookDelta.from_raw( - instrument_id=instrument_id, - action=BookAction.ADD, + ask = BookOrder.from_raw( side=OrderSide.SELL, price_raw=level.ask_px, - price_prec=USD.precision, # TODO(per instrument precision) - size_raw=int(level.ask_sz * FIXED_SCALAR), # No fractional sizes + price_prec=USD.precision, + size_raw=int(level.ask_sz * FIXED_SCALAR), size_prec=0, # No fractional units order_id=0, # No order ID for MBP level - flags=record.flags, - sequence=record.sequence, - ts_event=record.ts_recv, # More accurate and reliable timestamp - ts_init=ts_init, ) asks.append(ask) + ask_counts.append(level.ask_ct) # Currently a typo in type stub - clear = [OrderBookDelta.clear(instrument_id, record.ts_recv, record.ts_recv, record.sequence)] - return OrderBookDeltas(instrument_id=instrument_id, deltas=clear + bids + asks) + return OrderBookDepth10( + instrument_id=instrument_id, + bids=bids, + asks=asks, + bid_counts=bid_counts, + ask_counts=ask_counts, + flags=record.flags, + sequence=record.sequence, + ts_event=record.ts_recv, + ts_init=ts_init, + ) def parse_trade_msg( diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index 9637f40451fe..9846263e6fc8 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -797,6 +797,14 @@ typedef struct OrderBookDepth10_t { * The ask orders for the depth update. */ struct BookOrder_t asks[DEPTH10_LEN]; + /** + * The count of bid orders per level for the depth update. + */ + uint32_t bid_counts[DEPTH10_LEN]; + /** + * The count of ask orders per level for the depth update. + */ + uint32_t ask_counts[DEPTH10_LEN]; /** * A combination of packet end with matching engine status. */ @@ -1390,11 +1398,13 @@ uint64_t orderbook_delta_hash(const struct OrderBookDelta_t *delta); * # Safety * * - Assumes `bids` and `asks` are valid pointers to arrays of `BookOrder` of length 10. - * - Assumes Rust now takes ownership of the memory for `bids` and `asks`. + * - Assumes `bid_counts` and `ask_counts` are valid pointers to arrays of `u32` of length 10. */ struct OrderBookDepth10_t orderbook_depth10_new(struct InstrumentId_t instrument_id, const struct BookOrder_t *bids_ptr, const struct BookOrder_t *asks_ptr, + const uint32_t *bid_counts_ptr, + const uint32_t *ask_counts_ptr, uint8_t flags, uint64_t sequence, uint64_t ts_event, @@ -1409,6 +1419,10 @@ const struct BookOrder_t *orderbook_depth10_bids_array(const struct OrderBookDep const struct BookOrder_t *orderbook_depth10_asks_array(const struct OrderBookDepth10_t *depth); +const uint32_t *orderbook_depth10_bid_counts_array(const struct OrderBookDepth10_t *depth); + +const uint32_t *orderbook_depth10_ask_counts_array(const struct OrderBookDepth10_t *depth); + struct BookOrder_t book_order_from_raw(enum OrderSide order_side, int64_t price_raw, uint8_t price_prec, diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index d9c5ed3b9490..9a39ca7da0d6 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -1,6 +1,6 @@ # Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ -from libc.stdint cimport uint8_t, uint16_t, uint64_t, uintptr_t, int64_t +from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t, uintptr_t, int64_t from nautilus_trader.core.rust.core cimport CVec, UUID4_t cdef extern from "../includes/model.h": @@ -435,6 +435,10 @@ cdef extern from "../includes/model.h": BookOrder_t bids[DEPTH10_LEN]; # The ask orders for the depth update. BookOrder_t asks[DEPTH10_LEN]; + # The count of bid orders per level for the depth update. + uint32_t bid_counts[DEPTH10_LEN]; + # The count of ask orders per level for the depth update. + uint32_t ask_counts[DEPTH10_LEN]; # A combination of packet end with matching engine status. uint8_t flags; # The message sequence number assigned at the venue. @@ -835,10 +839,12 @@ cdef extern from "../includes/model.h": # # Safety # # - Assumes `bids` and `asks` are valid pointers to arrays of `BookOrder` of length 10. - # - Assumes Rust now takes ownership of the memory for `bids` and `asks`. + # - Assumes `bid_counts` and `ask_counts` are valid pointers to arrays of `u32` of length 10. OrderBookDepth10_t orderbook_depth10_new(InstrumentId_t instrument_id, const BookOrder_t *bids_ptr, const BookOrder_t *asks_ptr, + const uint32_t *bid_counts_ptr, + const uint32_t *ask_counts_ptr, uint8_t flags, uint64_t sequence, uint64_t ts_event, @@ -852,6 +858,10 @@ cdef extern from "../includes/model.h": const BookOrder_t *orderbook_depth10_asks_array(const OrderBookDepth10_t *depth); + const uint32_t *orderbook_depth10_bid_counts_array(const OrderBookDepth10_t *depth); + + const uint32_t *orderbook_depth10_ask_counts_array(const OrderBookDepth10_t *depth); + BookOrder_t book_order_from_raw(OrderSide order_side, int64_t price_raw, uint8_t price_prec, diff --git a/nautilus_trader/model/data.pyx b/nautilus_trader/model/data.pyx index 5fb0510d56d6..ca87a0e54306 100644 --- a/nautilus_trader/model/data.pyx +++ b/nautilus_trader/model/data.pyx @@ -24,6 +24,7 @@ from cpython.pycapsule cimport PyCapsule_Destructor from cpython.pycapsule cimport PyCapsule_GetPointer from cpython.pycapsule cimport PyCapsule_New from libc.stdint cimport uint8_t +from libc.stdint cimport uint32_t from libc.stdint cimport uint64_t from nautilus_trader.core.correctness cimport Condition @@ -77,7 +78,9 @@ from nautilus_trader.core.rust.model cimport instrument_id_from_cstr from nautilus_trader.core.rust.model cimport orderbook_delta_eq from nautilus_trader.core.rust.model cimport orderbook_delta_hash from nautilus_trader.core.rust.model cimport orderbook_delta_new +from nautilus_trader.core.rust.model cimport orderbook_depth10_ask_counts_array from nautilus_trader.core.rust.model cimport orderbook_depth10_asks_array +from nautilus_trader.core.rust.model cimport orderbook_depth10_bid_counts_array from nautilus_trader.core.rust.model cimport orderbook_depth10_bids_array from nautilus_trader.core.rust.model cimport orderbook_depth10_eq from nautilus_trader.core.rust.model cimport orderbook_depth10_hash @@ -2057,6 +2060,10 @@ cdef class OrderBookDepth10(Data): The bid side orders for the update. asks : list[BookOrder] The ask side orders for the update. + bid_counts : list[uint32_t] + The count of bid orders per level for the update. Can be zeros if data not available. + ask_counts : list[uint32_t] + The count of ask orders per level for the update. Can be zeros if data not available. flags : uint8_t A combination of packet end with matching engine status. sequence : uint64_t @@ -2076,6 +2083,10 @@ cdef class OrderBookDepth10(Data): If `bids` length is not equal to 10. ValueError If `asks` length is not equal to 10. + ValueError + If `bid_counts` length is not equal to 10. + ValueError + If `ask_counts` length is not equal to 10. """ def __init__( @@ -2083,6 +2094,8 @@ cdef class OrderBookDepth10(Data): InstrumentId instrument_id not None, list bids not None, list asks not None, + list bid_counts not None, + list ask_counts not None, uint8_t flags, uint64_t sequence, uint64_t ts_event, @@ -2090,14 +2103,18 @@ cdef class OrderBookDepth10(Data): ): Condition.not_empty(bids, "bids") Condition.not_empty(asks, "asks") - Condition.true(len(bids) == DEPTH10_LEN, f"bids length != 10, was {len(bids)}") - Condition.true(len(asks) == DEPTH10_LEN, f"asks length != 10, was {len(asks)}") + Condition.true(len(bids) == DEPTH10_LEN, f"`bids` length != 10, was {len(bids)}") + Condition.true(len(asks) == DEPTH10_LEN, f"`asks` length != 10, was {len(asks)}") + Condition.true(len(bid_counts) == DEPTH10_LEN, f"`bid_counts` length != 10, was {len(bid_counts)}") + Condition.true(len(ask_counts) == DEPTH10_LEN, f"`ask_counts` length != 10, was {len(ask_counts)}") # Create temporary arrays to copy data to Rust cdef BookOrder_t *bids_array = PyMem_Malloc(DEPTH10_LEN * sizeof(BookOrder_t)) cdef BookOrder_t *asks_array = PyMem_Malloc(DEPTH10_LEN * sizeof(BookOrder_t)) - if bids_array == NULL or asks_array == NULL: - raise MemoryError("Failed to allocate memory for `bids` or `asks`") + cdef uint32_t *bid_counts_array = PyMem_Malloc(DEPTH10_LEN * sizeof(uint32_t)) + cdef uint32_t *ask_counts_array = PyMem_Malloc(DEPTH10_LEN * sizeof(uint32_t)) + if bids_array == NULL or asks_array == NULL or bid_counts_array == NULL or ask_counts_array == NULL: + raise MemoryError("Failed to allocate memory for data transfer array") cdef uint64_t i cdef BookOrder order @@ -2105,13 +2122,17 @@ cdef class OrderBookDepth10(Data): for i in range(DEPTH10_LEN): order = bids[i] bids_array[i] = order._mem + bid_counts_array[i] = bid_counts[i] order = asks[i] asks_array[i] = order._mem + ask_counts_array[i] = ask_counts[i] self._mem = orderbook_depth10_new( instrument_id._mem, bids_array, asks_array, + bid_counts_array, + ask_counts_array, flags, sequence, ts_event, @@ -2121,12 +2142,16 @@ cdef class OrderBookDepth10(Data): # Deallocate temporary data transfer arrays PyMem_Free(bids_array) PyMem_Free(asks_array) + PyMem_Free(bid_counts_array) + PyMem_Free(ask_counts_array) def __getstate__(self): return ( self.instrument_id.value, pickle.dumps(self.bids), pickle.dumps(self.asks), + pickle.dumps(self.bid_counts), + pickle.dumps(self.ask_counts), self._mem.flags, self._mem.sequence, self._mem.ts_event, @@ -2139,11 +2164,15 @@ cdef class OrderBookDepth10(Data): # Create temporary arrays to copy data to Rust cdef BookOrder_t *bids_array = PyMem_Malloc(DEPTH10_LEN * sizeof(BookOrder_t)) cdef BookOrder_t *asks_array = PyMem_Malloc(DEPTH10_LEN * sizeof(BookOrder_t)) - if bids_array == NULL or asks_array == NULL: - raise MemoryError("Failed to allocate memory for `bids` or `asks`") + cdef uint32_t *bid_counts_array = PyMem_Malloc(DEPTH10_LEN * sizeof(uint32_t)) + cdef uint32_t *ask_counts_array = PyMem_Malloc(DEPTH10_LEN * sizeof(uint32_t)) + if bids_array == NULL or asks_array == NULL or bid_counts_array == NULL or ask_counts_array == NULL: + raise MemoryError("Failed to allocate memory for data transfer array") cdef list[BookOrder] bids = pickle.loads(state[1]) cdef list[BookOrder] asks = pickle.loads(state[2]) + cdef list[uint32_t] bid_counts = pickle.loads(state[3]) + cdef list[uint32_t] ask_counts = pickle.loads(state[4]) cdef uint64_t i cdef BookOrder order @@ -2151,22 +2180,28 @@ cdef class OrderBookDepth10(Data): for i in range(DEPTH10_LEN): order = bids[i] bids_array[i] = order._mem + bid_counts_array[i] = bid_counts[i] order = asks[i] asks_array[i] = order._mem + ask_counts_array[i] = ask_counts[i] self._mem = orderbook_depth10_new( instrument_id._mem, bids_array, asks_array, - state[3], - state[4], + bid_counts_array, + ask_counts_array, state[5], state[6], + state[7], + state[8], ) finally: # Deallocate temporary data transfer arrays PyMem_Free(bids_array) PyMem_Free(asks_array) + PyMem_Free(bid_counts_array) + PyMem_Free(ask_counts_array) def __eq__(self, OrderBookDepth10 other) -> bool: return orderbook_depth10_eq(&self._mem, &other._mem) @@ -2180,6 +2215,8 @@ cdef class OrderBookDepth10(Data): f"instrument_id={self.instrument_id}, " f"bids={self.bids}, " f"asks={self.asks}, " + f"bid_counts={self.bid_counts}, " + f"ask_counts={self.ask_counts}, " f"flags={self.flags}, " f"sequence={self.sequence}, " f"ts_event={self.ts_event}, " @@ -2238,6 +2275,44 @@ cdef class OrderBookDepth10(Data): return asks + @property + def bid_counts(self) -> list[uint32_t]: + """ + Return the count of bid orders per level for the update. + + Returns + ------- + list[uint32_t] + + """ + cdef const uint32_t* bid_counts_array = orderbook_depth10_bid_counts_array(&self._mem) + cdef list[uint32_t] bid_counts = []; + + cdef uint64_t i + for i in range(DEPTH10_LEN): + bid_counts.append(bid_counts_array[i]) + + return bid_counts + + @property + def ask_counts(self) -> list[uint32_t]: + """ + Return the count of ask orders level for the update. + + Returns + ------- + list[uint32_t] + + """ + cdef const uint32_t* ask_counts_array = orderbook_depth10_ask_counts_array(&self._mem) + cdef list[uint32_t] ask_counts = []; + + cdef uint64_t i + for i in range(DEPTH10_LEN): + ask_counts.append(ask_counts_array[i]) + + return ask_counts + @property def flags(self) -> uint8_t: """ @@ -2299,6 +2374,8 @@ cdef class OrderBookDepth10(Data): instrument_id=InstrumentId.from_str_c(values["instrument_id"]), bids=[BookOrder.from_dict_c(o) for o in values["bids"]], asks=[BookOrder.from_dict_c(o) for o in values["asks"]], + bid_counts=values["bid_counts"], + ask_counts=values["ask_counts"], flags=values["flags"], sequence=values["sequence"], ts_event=values["ts_event"], @@ -2313,6 +2390,8 @@ cdef class OrderBookDepth10(Data): "instrument_id": obj.instrument_id.value, "bids": [BookOrder.to_dict_c(o) for o in obj.bids], "asks": [BookOrder.to_dict_c(o) for o in obj.asks], + "bid_counts": obj.bid_counts, + "ask_counts": obj.ask_counts, "flags": obj.flags, "sequence": obj.sequence, "ts_event": obj.ts_event, diff --git a/nautilus_trader/test_kit/stubs/data.py b/nautilus_trader/test_kit/stubs/data.py index d5bea8071380..8058642d3973 100644 --- a/nautilus_trader/test_kit/stubs/data.py +++ b/nautilus_trader/test_kit/stubs/data.py @@ -376,10 +376,15 @@ def order_book_depth10( quantity += 100.0 order_id += 1 + bid_counts = [1] * 10 + ask_counts = [1] * 10 + return OrderBookDepth10( instrument_id=instrument_id or TestIdStubs.aapl_xnas_id(), bids=bids, asks=asks, + bid_counts=bid_counts, + ask_counts=ask_counts, flags=flags, sequence=sequence, ts_event=ts_event, diff --git a/tests/integration_tests/adapters/databento/test_loaders.py b/tests/integration_tests/adapters/databento/test_loaders.py index 95e9b080199d..6e2f2b964c60 100644 --- a/tests/integration_tests/adapters/databento/test_loaders.py +++ b/tests/integration_tests/adapters/databento/test_loaders.py @@ -22,7 +22,7 @@ from nautilus_trader.model.data import Bar from nautilus_trader.model.data import BarType from nautilus_trader.model.data import OrderBookDelta -from nautilus_trader.model.data import OrderBookDeltas +from nautilus_trader.model.data import OrderBookDepth10 from nautilus_trader.model.data import QuoteTick from nautilus_trader.model.data import TradeTick from nautilus_trader.model.enums import AggressorSide @@ -321,11 +321,28 @@ def test_loader_with_mbp_10() -> None: # Assert assert len(data) == 2 - assert isinstance(data[0], OrderBookDeltas) - assert isinstance(data[1], OrderBookDeltas) - deltas = data[0] - assert deltas.instrument_id == InstrumentId.from_str("ESH1.GLBX") - assert len(deltas.deltas) == 21 + assert isinstance(data[0], OrderBookDepth10) + assert isinstance(data[1], OrderBookDepth10) + depth = data[0] + assert depth.instrument_id == InstrumentId.from_str("ESH1.GLBX") + assert len(depth.bids) == 10 + assert len(depth.asks) == 10 + assert depth.bids[0].price == Price.from_str("3720.25") + assert depth.bids[0].size == Quantity.from_int(24) + assert depth.asks[0].price == Price.from_str("3720.50") + assert depth.asks[0].size == Quantity.from_int(10) + assert depth.bid_counts == [15, 18, 23, 26, 35, 28, 35, 39, 32, 39] + assert depth.ask_counts == [8, 24, 25, 17, 19, 33, 40, 38, 35, 26] + depth = data[1] + assert depth.instrument_id == InstrumentId.from_str("ESH1.GLBX") + assert len(depth.bids) == 10 + assert len(depth.asks) == 10 + assert depth.bids[0].price == Price.from_str("3720.25") + assert depth.bids[0].size == Quantity.from_int(24) + assert depth.asks[0].price == Price.from_str("3720.50") + assert depth.asks[0].size == Quantity.from_int(10) + assert depth.bid_counts == [15, 17, 23, 26, 35, 28, 35, 39, 32, 39] + assert depth.ask_counts == [8, 24, 25, 17, 19, 33, 40, 38, 35, 26] def test_loader_with_tbbo() -> None: diff --git a/tests/unit_tests/model/test_orderbook_data.py b/tests/unit_tests/model/test_orderbook_data.py index f0667e0ba15c..83a9172e2527 100644 --- a/tests/unit_tests/model/test_orderbook_data.py +++ b/tests/unit_tests/model/test_orderbook_data.py @@ -606,11 +606,11 @@ def test_depth10_hash_str_repr() -> None: assert isinstance(hash(depth), int) assert ( str(depth) - == "OrderBookDepth10(instrument_id=AAPL.XNAS, bids=[BookOrder { side: Buy, price: 99.00, size: 100, order_id: 1 }, BookOrder { side: Buy, price: 98.00, size: 200, order_id: 2 }, BookOrder { side: Buy, price: 97.00, size: 300, order_id: 3 }, BookOrder { side: Buy, price: 96.00, size: 400, order_id: 4 }, BookOrder { side: Buy, price: 95.00, size: 500, order_id: 5 }, BookOrder { side: Buy, price: 94.00, size: 600, order_id: 6 }, BookOrder { side: Buy, price: 93.00, size: 700, order_id: 7 }, BookOrder { side: Buy, price: 92.00, size: 800, order_id: 8 }, BookOrder { side: Buy, price: 91.00, size: 900, order_id: 9 }, BookOrder { side: Buy, price: 90.00, size: 1000, order_id: 10 }], asks=[BookOrder { side: Sell, price: 100.00, size: 100, order_id: 11 }, BookOrder { side: Sell, price: 101.00, size: 200, order_id: 12 }, BookOrder { side: Sell, price: 102.00, size: 300, order_id: 13 }, BookOrder { side: Sell, price: 103.00, size: 400, order_id: 14 }, BookOrder { side: Sell, price: 104.00, size: 500, order_id: 15 }, BookOrder { side: Sell, price: 105.00, size: 600, order_id: 16 }, BookOrder { side: Sell, price: 106.00, size: 700, order_id: 17 }, BookOrder { side: Sell, price: 107.00, size: 800, order_id: 18 }, BookOrder { side: Sell, price: 108.00, size: 900, order_id: 19 }, BookOrder { side: Sell, price: 109.00, size: 1000, order_id: 20 }], flags=0, sequence=1, ts_event=2, ts_init=3)" # noqa + == "OrderBookDepth10(instrument_id=AAPL.XNAS, bids=[BookOrder { side: Buy, price: 99.00, size: 100, order_id: 1 }, BookOrder { side: Buy, price: 98.00, size: 200, order_id: 2 }, BookOrder { side: Buy, price: 97.00, size: 300, order_id: 3 }, BookOrder { side: Buy, price: 96.00, size: 400, order_id: 4 }, BookOrder { side: Buy, price: 95.00, size: 500, order_id: 5 }, BookOrder { side: Buy, price: 94.00, size: 600, order_id: 6 }, BookOrder { side: Buy, price: 93.00, size: 700, order_id: 7 }, BookOrder { side: Buy, price: 92.00, size: 800, order_id: 8 }, BookOrder { side: Buy, price: 91.00, size: 900, order_id: 9 }, BookOrder { side: Buy, price: 90.00, size: 1000, order_id: 10 }], asks=[BookOrder { side: Sell, price: 100.00, size: 100, order_id: 11 }, BookOrder { side: Sell, price: 101.00, size: 200, order_id: 12 }, BookOrder { side: Sell, price: 102.00, size: 300, order_id: 13 }, BookOrder { side: Sell, price: 103.00, size: 400, order_id: 14 }, BookOrder { side: Sell, price: 104.00, size: 500, order_id: 15 }, BookOrder { side: Sell, price: 105.00, size: 600, order_id: 16 }, BookOrder { side: Sell, price: 106.00, size: 700, order_id: 17 }, BookOrder { side: Sell, price: 107.00, size: 800, order_id: 18 }, BookOrder { side: Sell, price: 108.00, size: 900, order_id: 19 }, BookOrder { side: Sell, price: 109.00, size: 1000, order_id: 20 }], bid_counts=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ask_counts=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], flags=0, sequence=1, ts_event=2, ts_init=3)" # noqa ) assert ( repr(depth) - == "OrderBookDepth10(instrument_id=AAPL.XNAS, bids=[BookOrder { side: Buy, price: 99.00, size: 100, order_id: 1 }, BookOrder { side: Buy, price: 98.00, size: 200, order_id: 2 }, BookOrder { side: Buy, price: 97.00, size: 300, order_id: 3 }, BookOrder { side: Buy, price: 96.00, size: 400, order_id: 4 }, BookOrder { side: Buy, price: 95.00, size: 500, order_id: 5 }, BookOrder { side: Buy, price: 94.00, size: 600, order_id: 6 }, BookOrder { side: Buy, price: 93.00, size: 700, order_id: 7 }, BookOrder { side: Buy, price: 92.00, size: 800, order_id: 8 }, BookOrder { side: Buy, price: 91.00, size: 900, order_id: 9 }, BookOrder { side: Buy, price: 90.00, size: 1000, order_id: 10 }], asks=[BookOrder { side: Sell, price: 100.00, size: 100, order_id: 11 }, BookOrder { side: Sell, price: 101.00, size: 200, order_id: 12 }, BookOrder { side: Sell, price: 102.00, size: 300, order_id: 13 }, BookOrder { side: Sell, price: 103.00, size: 400, order_id: 14 }, BookOrder { side: Sell, price: 104.00, size: 500, order_id: 15 }, BookOrder { side: Sell, price: 105.00, size: 600, order_id: 16 }, BookOrder { side: Sell, price: 106.00, size: 700, order_id: 17 }, BookOrder { side: Sell, price: 107.00, size: 800, order_id: 18 }, BookOrder { side: Sell, price: 108.00, size: 900, order_id: 19 }, BookOrder { side: Sell, price: 109.00, size: 1000, order_id: 20 }], flags=0, sequence=1, ts_event=2, ts_init=3)" # noqa + == "OrderBookDepth10(instrument_id=AAPL.XNAS, bids=[BookOrder { side: Buy, price: 99.00, size: 100, order_id: 1 }, BookOrder { side: Buy, price: 98.00, size: 200, order_id: 2 }, BookOrder { side: Buy, price: 97.00, size: 300, order_id: 3 }, BookOrder { side: Buy, price: 96.00, size: 400, order_id: 4 }, BookOrder { side: Buy, price: 95.00, size: 500, order_id: 5 }, BookOrder { side: Buy, price: 94.00, size: 600, order_id: 6 }, BookOrder { side: Buy, price: 93.00, size: 700, order_id: 7 }, BookOrder { side: Buy, price: 92.00, size: 800, order_id: 8 }, BookOrder { side: Buy, price: 91.00, size: 900, order_id: 9 }, BookOrder { side: Buy, price: 90.00, size: 1000, order_id: 10 }], asks=[BookOrder { side: Sell, price: 100.00, size: 100, order_id: 11 }, BookOrder { side: Sell, price: 101.00, size: 200, order_id: 12 }, BookOrder { side: Sell, price: 102.00, size: 300, order_id: 13 }, BookOrder { side: Sell, price: 103.00, size: 400, order_id: 14 }, BookOrder { side: Sell, price: 104.00, size: 500, order_id: 15 }, BookOrder { side: Sell, price: 105.00, size: 600, order_id: 16 }, BookOrder { side: Sell, price: 106.00, size: 700, order_id: 17 }, BookOrder { side: Sell, price: 107.00, size: 800, order_id: 18 }, BookOrder { side: Sell, price: 108.00, size: 900, order_id: 19 }, BookOrder { side: Sell, price: 109.00, size: 1000, order_id: 20 }], bid_counts=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], ask_counts=[1, 1, 1, 1, 1, 1, 1, 1, 1, 1], flags=0, sequence=1, ts_event=2, ts_init=3)" # noqa ) @@ -661,6 +661,8 @@ def test_depth10_to_dict_from_dict_round_trip() -> None: "order_id": 20, }, ], + "bid_counts": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "ask_counts": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "flags": 0, "sequence": 1, "ts_event": 2,