From c18dd85436134c1fbc0655183a568f094aeffaa3 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 1 Jan 2024 18:01:56 +1100 Subject: [PATCH] Add OrderBookDepth10 type for Python --- nautilus_core/model/cbindgen.toml | 1 + nautilus_core/model/cbindgen_cython.toml | 1 + nautilus_core/model/src/enums.rs | 6 +- nautilus_trader/core/includes/model.h | 30 +- nautilus_trader/core/rust/model.pxd | 26 +- nautilus_trader/model/data.pxd | 22 ++ nautilus_trader/model/data.pyx | 296 +++++++++++++++++- nautilus_trader/test_kit/stubs/data.py | 60 ++++ nautilus_trader/test_kit/stubs/identifiers.py | 4 + .../tracemalloc_orderbook_depth.py | 27 ++ .../mem_leak_tests/tracemalloc_quote_ticks.py | 27 ++ .../mem_leak_tests/tracemalloc_trade_ticks.py | 29 +- tests/unit_tests/model/test_orderbook_data.py | 107 ++++++- 13 files changed, 576 insertions(+), 60 deletions(-) create mode 100644 tests/mem_leak_tests/tracemalloc_orderbook_depth.py create mode 100644 tests/mem_leak_tests/tracemalloc_quote_ticks.py diff --git a/nautilus_core/model/cbindgen.toml b/nautilus_core/model/cbindgen.toml index f92a0dd880d0..adeba901c586 100644 --- a/nautilus_core/model/cbindgen.toml +++ b/nautilus_core/model/cbindgen.toml @@ -35,6 +35,7 @@ exclude = [ "OrderId" = "uint64_t" "OrderBookDelta" = "OrderBookDelta_t" "OrderBookDeltas" = "OrderBookDeltas_t" +"OrderBookDepth10" = "OrderBookDepth10_t" "OrderInitialized" = "OrderInitialized_t" "OrderDenied" = "OrderDenied_t" "OrderEmulated" = "OrderEmulated_t" diff --git a/nautilus_core/model/cbindgen_cython.toml b/nautilus_core/model/cbindgen_cython.toml index 614332556412..d74b2001447a 100644 --- a/nautilus_core/model/cbindgen_cython.toml +++ b/nautilus_core/model/cbindgen_cython.toml @@ -51,6 +51,7 @@ exclude = [ "OrderId" = "uint64_t" "OrderBookDelta" = "OrderBookDelta_t" "OrderBookDeltas" = "OrderBookDeltas_t" +"OrderBookDepth10" = "OrderBookDepth10_t" "OrderInitialized" = "OrderInitialized_t" "OrderDenied" = "OrderDenied_t" "OrderEmulated" = "OrderEmulated_t" diff --git a/nautilus_core/model/src/enums.rs b/nautilus_core/model/src/enums.rs index c3145ec58e8c..44d397c08230 100644 --- a/nautilus_core/model/src/enums.rs +++ b/nautilus_core/model/src/enums.rs @@ -121,7 +121,7 @@ pub enum AggregationSource { )] pub enum AggressorSide { /// There was no specific aggressor for the trade. - NoAggressor = 0, // Will be replaced by `Option` + NoAggressor = 0, /// The BUY order was the aggressor for the trade. #[pyo3(name = "BUYER")] Buyer = 1, @@ -726,8 +726,8 @@ pub enum OptionKind { pyclass(module = "nautilus_trader.core.nautilus_pyo3.model.enums") )] pub enum OrderSide { - /// No order side is specified (only valid in the context of a filter for actions involving orders). - NoOrderSide = 0, // Will be replaced by `Option` + /// No order side is specified. + NoOrderSide = 0, /// The order is a BUY. #[pyo3(name = "BUY")] Buy = 1, diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index ae2c2b77997a..74f76317c93a 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -354,7 +354,7 @@ typedef enum OptionKind { */ typedef enum OrderSide { /** - * No order side is specified (only valid in the context of a filter for actions involving orders). + * No order side is specified. */ NO_ORDER_SIDE = 0, /** @@ -784,7 +784,7 @@ typedef struct OrderBookDelta_t { * Note: This type is not compatible with `OrderBookDelta` or `OrderBookDeltas` due to * its specialized structure and limited depth use case. */ -typedef struct OrderBookDepth10 { +typedef struct OrderBookDepth10_t { /** * The instrument ID for the book. */ @@ -813,7 +813,7 @@ typedef struct OrderBookDepth10 { * The UNIX timestamp (nanoseconds) when the data object was initialized. */ uint64_t ts_init; -} OrderBookDepth10; +} OrderBookDepth10_t; /** * Represents a single quote tick in a financial market. @@ -989,7 +989,7 @@ typedef struct Data_t { struct OrderBookDelta_t delta; }; struct { - struct OrderBookDepth10 depth10; + struct OrderBookDepth10_t depth10; }; struct { struct QuoteTick_t quote; @@ -1392,18 +1392,18 @@ uint64_t orderbook_delta_hash(const struct OrderBookDelta_t *delta); * - 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`. */ -struct OrderBookDepth10 orderbook_depth10_new(struct InstrumentId_t instrument_id, - const struct BookOrder_t *bids_ptr, - const struct BookOrder_t *asks_ptr, - uint8_t flags, - uint64_t sequence, - uint64_t ts_event, - uint64_t ts_init); +struct OrderBookDepth10_t orderbook_depth10_new(struct InstrumentId_t instrument_id, + const struct BookOrder_t *bids_ptr, + const struct BookOrder_t *asks_ptr, + uint8_t flags, + uint64_t sequence, + uint64_t ts_event, + uint64_t ts_init); -uint8_t orderbook_depth10_eq(const struct OrderBookDepth10 *lhs, - const struct OrderBookDepth10 *rhs); +uint8_t orderbook_depth10_eq(const struct OrderBookDepth10_t *lhs, + const struct OrderBookDepth10_t *rhs); -uint64_t orderbook_depth10_hash(const struct OrderBookDepth10 *delta); +uint64_t orderbook_depth10_hash(const struct OrderBookDepth10_t *delta); struct BookOrder_t book_order_from_raw(enum OrderSide order_side, int64_t price_raw, @@ -2085,7 +2085,7 @@ void orderbook_clear_asks(struct OrderBook_API *book, uint64_t ts_event, uint64_ void orderbook_apply_delta(struct OrderBook_API *book, struct OrderBookDelta_t delta); -void orderbook_apply_depth(struct OrderBook_API *book, struct OrderBookDepth10 depth); +void orderbook_apply_depth(struct OrderBook_API *book, struct OrderBookDepth10_t depth); CVec orderbook_bids(struct OrderBook_API *book); diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index 604597372c1c..5f4cd3d2c245 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -190,7 +190,7 @@ cdef extern from "../includes/model.h": # The order side for a specific order, or action related to orders. cpdef enum OrderSide: - # No order side is specified (only valid in the context of a filter for actions involving orders). + # No order side is specified. NO_ORDER_SIDE # = 0, # The order is a BUY. BUY # = 1, @@ -428,7 +428,7 @@ cdef extern from "../includes/model.h": # # Note: This type is not compatible with `OrderBookDelta` or `OrderBookDeltas` due to # its specialized structure and limited depth use case. - cdef struct OrderBookDepth10: + cdef struct OrderBookDepth10_t: # The instrument ID for the book. InstrumentId_t instrument_id; # The bid orders for the depth update. @@ -537,7 +537,7 @@ cdef extern from "../includes/model.h": cdef struct Data_t: Data_t_Tag tag; OrderBookDelta_t delta; - OrderBookDepth10 depth10; + OrderBookDepth10_t depth10; QuoteTick_t quote; TradeTick_t trade; Bar_t bar; @@ -836,17 +836,17 @@ cdef extern from "../includes/model.h": # # - 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`. - OrderBookDepth10 orderbook_depth10_new(InstrumentId_t instrument_id, - const BookOrder_t *bids_ptr, - const BookOrder_t *asks_ptr, - uint8_t flags, - uint64_t sequence, - uint64_t ts_event, - uint64_t ts_init); + OrderBookDepth10_t orderbook_depth10_new(InstrumentId_t instrument_id, + const BookOrder_t *bids_ptr, + const BookOrder_t *asks_ptr, + uint8_t flags, + uint64_t sequence, + uint64_t ts_event, + uint64_t ts_init); - uint8_t orderbook_depth10_eq(const OrderBookDepth10 *lhs, const OrderBookDepth10 *rhs); + uint8_t orderbook_depth10_eq(const OrderBookDepth10_t *lhs, const OrderBookDepth10_t *rhs); - uint64_t orderbook_depth10_hash(const OrderBookDepth10 *delta); + uint64_t orderbook_depth10_hash(const OrderBookDepth10_t *delta); BookOrder_t book_order_from_raw(OrderSide order_side, int64_t price_raw, @@ -1425,7 +1425,7 @@ cdef extern from "../includes/model.h": void orderbook_apply_delta(OrderBook_API *book, OrderBookDelta_t delta); - void orderbook_apply_depth(OrderBook_API *book, OrderBookDepth10 depth); + void orderbook_apply_depth(OrderBook_API *book, OrderBookDepth10_t depth); CVec orderbook_bids(OrderBook_API *book); diff --git a/nautilus_trader/model/data.pxd b/nautilus_trader/model/data.pxd index d9bcef3eddc0..671a3724e50e 100644 --- a/nautilus_trader/model/data.pxd +++ b/nautilus_trader/model/data.pxd @@ -32,6 +32,7 @@ from nautilus_trader.core.rust.model cimport HaltReason from nautilus_trader.core.rust.model cimport InstrumentCloseType from nautilus_trader.core.rust.model cimport MarketStatus from nautilus_trader.core.rust.model cimport OrderBookDelta_t +from nautilus_trader.core.rust.model cimport OrderBookDepth10_t from nautilus_trader.core.rust.model cimport OrderSide from nautilus_trader.core.rust.model cimport PriceType from nautilus_trader.core.rust.model cimport QuoteTick_t @@ -247,6 +248,27 @@ cdef class OrderBookDeltas(Data): cdef dict to_dict_c(OrderBookDeltas obj) +cdef class OrderBookDepth10(Data): + cdef OrderBookDepth10_t _mem + cdef list[BookOrder] _bids + cdef list[BookOrder] _asks + + @staticmethod + cdef OrderBookDepth10 from_mem_c(OrderBookDepth10_t mem) + + @staticmethod + cdef OrderBookDepth10 from_dict_c(dict values) + + @staticmethod + cdef dict to_dict_c(OrderBookDepth10 obj) + + @staticmethod + cdef list capsule_to_list_c(capsule) + + @staticmethod + cdef object list_to_capsule_c(list items) + + cdef class VenueStatus(Data): cdef readonly Venue venue """The venue.\n\n:returns: `Venue`""" diff --git a/nautilus_trader/model/data.pyx b/nautilus_trader/model/data.pyx index 0ae74b49d147..2cf3f23d00ee 100644 --- a/nautilus_trader/model/data.pyx +++ b/nautilus_trader/model/data.pyx @@ -27,12 +27,14 @@ from libc.stdint cimport uint64_t from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.data cimport Data from nautilus_trader.core.rust.core cimport CVec +from nautilus_trader.core.rust.model cimport DEPTH_10_LEN from nautilus_trader.core.rust.model cimport AggregationSource from nautilus_trader.core.rust.model cimport AggressorSide from nautilus_trader.core.rust.model cimport Bar_t from nautilus_trader.core.rust.model cimport BarSpecification_t from nautilus_trader.core.rust.model cimport BarType_t from nautilus_trader.core.rust.model cimport BookAction +from nautilus_trader.core.rust.model cimport BookOrder_t from nautilus_trader.core.rust.model cimport Data_t from nautilus_trader.core.rust.model cimport Data_t_Tag from nautilus_trader.core.rust.model cimport HaltReason @@ -73,6 +75,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_eq +from nautilus_trader.core.rust.model cimport orderbook_depth10_hash +from nautilus_trader.core.rust.model cimport orderbook_depth10_new from nautilus_trader.core.rust.model cimport quote_tick_eq from nautilus_trader.core.rust.model cimport quote_tick_hash from nautilus_trader.core.rust.model cimport quote_tick_new @@ -122,6 +127,8 @@ cpdef list capsule_to_list(capsule): for i in range(0, data.len): if ptr[i].tag == Data_t_Tag.DELTA: objects.append(OrderBookDelta.from_mem_c(ptr[i].delta)) + elif ptr[i].tag == Data_t_Tag.DEPTH10: + objects.append(OrderBookDepth10.from_mem_c(ptr[i].depth10)) elif ptr[i].tag == Data_t_Tag.QUOTE: objects.append(QuoteTick.from_mem_c(ptr[i].quote)) elif ptr[i].tag == Data_t_Tag.TRADE: @@ -1775,7 +1782,7 @@ cdef class OrderBookDelta(Data): return PyCapsule_New(cvec, NULL, capsule_destructor) @staticmethod - def list_from_capsule(capsule) -> list[QuoteTick]: + def list_from_capsule(capsule) -> list[OrderBookDelta]: return OrderBookDelta.capsule_to_list_c(capsule) @staticmethod @@ -2034,6 +2041,293 @@ cdef class OrderBookDeltas(Data): return OrderBookDeltas.to_dict_c(obj) +cdef class OrderBookDepth10(Data): + """ + Represents a self-contained order book update with a fixed depth of 10 levels per side. + + Parameters + ---------- + instrument_id : InstrumentId + The instrument ID for the book. + bids : list[BookOrder] + The bid side orders for the update. + asks : list[BookOrder] + The ask side orders for the update. + flags : uint8_t + A combination of packet end with matching engine status. + sequence : uint64_t + The unique sequence number for the update. + ts_event : uint64_t + The UNIX timestamp (nanoseconds) when the tick event occurred. + ts_init : uint64_t + The UNIX timestamp (nanoseconds) when the data object was initialized. + + Raises + ------ + ValueError + If `bids` is empty. + ValueError + If `asks` is empty. + ValueError + If `bids` length is not equal to 10. + ValueError + If `asks` length is not equal to 10. + """ + + def __init__( + self, + InstrumentId instrument_id not None, + list bids not None, + list asks not None, + uint8_t flags, + uint64_t sequence, + uint64_t ts_event, + uint64_t ts_init, + ): + Condition.not_empty(bids, "bids") + Condition.not_empty(asks, "asks") + Condition.true(len(bids) == DEPTH_10_LEN, f"bids length != 10, was {len(bids)}") + Condition.true(len(asks) == DEPTH_10_LEN, f"asks length != 10, was {len(asks)}") + + # Retain the original lists on the Python side + self._bids = bids + self._asks = asks + + # Create temporary arrays to copy data to Rust + cdef BookOrder_t *bids_array = PyMem_Malloc(DEPTH_10_LEN * sizeof(BookOrder_t)) + cdef BookOrder_t *asks_array = PyMem_Malloc(DEPTH_10_LEN * sizeof(BookOrder_t)) + if bids_array == NULL or asks_array == NULL: + raise MemoryError("Failed to allocate memory for `bids` or `asks`") + + cdef uint64_t i + cdef BookOrder order + try: + for i in range(DEPTH_10_LEN): + order = bids[i] + bids_array[i] = order._mem + order = asks[i] + asks_array[i] = order._mem + + self._mem = orderbook_depth10_new( + instrument_id._mem, + bids_array, + asks_array, + flags, + sequence, + ts_event, + ts_init, + ) + finally: + PyMem_Free(bids_array) + PyMem_Free(asks_array) + + def __eq__(self, OrderBookDepth10 other) -> bool: + return orderbook_depth10_eq(&self._mem, &other._mem) + + def __hash__(self) -> int: + return orderbook_depth10_hash(&self._mem) + + def __repr__(self) -> str: + return ( + f"{type(self).__name__}(" + f"instrument_id={self.instrument_id}, " + f"bids={self.bids}, " + f"asks={self.asks}, " + f"flags={self.flags}, " + f"sequence={self.sequence}, " + f"ts_event={self.ts_event}, " + f"ts_init={self.ts_init})" + ) + + @property + def instrument_id(self) -> InstrumentId: + """ + Return the depth updates book instrument ID. + + Returns + ------- + InstrumentId + + """ + return InstrumentId.from_mem_c(self._mem.instrument_id) + + @property + def bids(self) -> list[BookOrder]: + """ + Return the bid orders for the update. + + Returns + ------- + list[BookOrder] + + """ + return self._bids + + @property + def asks(self) -> list[BookOrder]: + """ + Return the ask orders for the update. + + Returns + ------- + list[BookOrder] + + """ + return self._asks + + @property + def flags(self) -> uint8_t: + """ + Return the flags for the depth update. + + Returns + ------- + uint8_t + + """ + return self._mem.flags + + @property + def sequence(self) -> uint64_t: + """ + Return the sequence number for the depth update. + + Returns + ------- + uint64_t + + """ + return self._mem.sequence + + @property + def ts_event(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the data event occurred. + + Returns + ------- + int + + """ + return self._mem.ts_event + + @property + def ts_init(self) -> int: + """ + The UNIX timestamp (nanoseconds) when the object was initialized. + + Returns + ------- + int + + """ + return self._mem.ts_init + + @staticmethod + cdef OrderBookDepth10 from_mem_c(OrderBookDepth10_t mem): + cdef OrderBookDepth10 depth = OrderBookDepth10.__new__(OrderBookDepth10) + depth._mem = mem + return depth + + @staticmethod + cdef OrderBookDepth10 from_dict_c(dict values): + Condition.not_none(values, "values") + return OrderBookDepth10( + 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"]], + flags=values["flags"], + sequence=values["sequence"], + ts_event=values["ts_event"], + ts_init=values["ts_init"], + ) + + @staticmethod + cdef dict to_dict_c(OrderBookDepth10 obj): + Condition.not_none(obj, "obj") + return { + "type": obj.__class__.__name__, + "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], + "flags": obj.flags, + "sequence": obj.sequence, + "ts_event": obj.ts_event, + "ts_init": obj.ts_init, + } + + # SAFETY: Do NOT deallocate the capsule here + # It is supposed to be deallocated by the creator + @staticmethod + cdef inline list capsule_to_list_c(object capsule): + cdef CVec* data = PyCapsule_GetPointer(capsule, NULL) + cdef OrderBookDepth10_t* ptr = data.ptr + cdef list depths = [] + + cdef uint64_t i + for i in range(0, data.len): + depths.append(OrderBookDepth10.from_mem_c(ptr[i])) + + return depths + + @staticmethod + cdef inline list_to_capsule_c(list items): + # Create a C struct buffer + cdef uint64_t len_ = len(items) + cdef OrderBookDepth10_t * data = PyMem_Malloc(len_ * sizeof(OrderBookDepth10_t)) + cdef uint64_t i + for i in range(len_): + data[i] = (items[i])._mem + if not data: + raise MemoryError() + + # Create CVec + cdef CVec * cvec = PyMem_Malloc(1 * sizeof(CVec)) + cvec.ptr = data + cvec.len = len_ + cvec.cap = len_ + + # Create PyCapsule + return PyCapsule_New(cvec, NULL, capsule_destructor) + + @staticmethod + def list_from_capsule(capsule) -> list[OrderBookDepth10]: + return OrderBookDepth10.capsule_to_list_c(capsule) + + @staticmethod + def capsule_from_list(list items): + return OrderBookDepth10.list_to_capsule_c(items) + + @staticmethod + def from_dict(dict values) -> OrderBookDepth10: + """ + Return order book depth from the given dict values. + + Parameters + ---------- + values : dict[str, object] + The values for initialization. + + Returns + ------- + OrderBookDepth10 + + """ + return OrderBookDepth10.from_dict_c(values) + + @staticmethod + def to_dict(OrderBookDepth10 obj): + """ + Return a dictionary representation of this object. + + Returns + ------- + dict[str, object] + + """ + return OrderBookDepth10.to_dict_c(obj) + + cdef class VenueStatus(Data): """ Represents an update that indicates a change in a Venue status. diff --git a/nautilus_trader/test_kit/stubs/data.py b/nautilus_trader/test_kit/stubs/data.py index fb4a7eb9c301..d5bea8071380 100644 --- a/nautilus_trader/test_kit/stubs/data.py +++ b/nautilus_trader/test_kit/stubs/data.py @@ -33,6 +33,7 @@ from nautilus_trader.model.data import InstrumentStatus 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 Ticker from nautilus_trader.model.data import TradeTick @@ -326,6 +327,65 @@ def order_book_delta( ts_init=ts_init, ) + @staticmethod + def order_book_depth10( + instrument_id: InstrumentId | None = None, + flags: int = 0, + sequence: int = 0, + ts_event: int = 0, + ts_init: int = 0, + ) -> OrderBookDepth10: + bids: list[BookOrder] = [] + asks: list[BookOrder] = [] + + # Create bids + price = 99.00 + quantity = 100.0 + order_id = 1 + + for _ in range(10): + order = BookOrder( + OrderSide.BUY, + Price(price, 2), + Quantity(quantity, 0), + order_id, + ) + + bids.append(order) + + price -= 1.0 + quantity += 100.0 + order_id += 1 + + # Create asks + price = 100.00 + quantity = 100.0 + order_id = 11 + + for _ in range(10): + order = BookOrder( + OrderSide.SELL, + Price(price, 2), + Quantity(quantity, 0), + order_id, + ) + + asks.append(order) + + price += 1.0 + quantity += 100.0 + order_id += 1 + + return OrderBookDepth10( + instrument_id=instrument_id or TestIdStubs.aapl_xnas_id(), + bids=bids, + asks=asks, + flags=flags, + sequence=sequence, + ts_event=ts_event, + ts_init=ts_init, + ) + @staticmethod def order_book_delta_clear( instrument_id: InstrumentId | None = None, diff --git a/nautilus_trader/test_kit/stubs/identifiers.py b/nautilus_trader/test_kit/stubs/identifiers.py index a74ab1a311ad..b137749258b2 100644 --- a/nautilus_trader/test_kit/stubs/identifiers.py +++ b/nautilus_trader/test_kit/stubs/identifiers.py @@ -76,6 +76,10 @@ def usdjpy_id() -> InstrumentId: def audusd_idealpro_id() -> InstrumentId: return InstrumentId(Symbol("AUD/USD"), Venue("IDEALPRO")) + @staticmethod + def aapl_xnas_id() -> InstrumentId: + return InstrumentId(Symbol("AAPL"), Venue("XNAS")) + @staticmethod def msft_xnas_id() -> InstrumentId: return InstrumentId(Symbol("MSFT"), Venue("XNAS")) diff --git a/tests/mem_leak_tests/tracemalloc_orderbook_depth.py b/tests/mem_leak_tests/tracemalloc_orderbook_depth.py new file mode 100644 index 000000000000..be394e463e68 --- /dev/null +++ b/tests/mem_leak_tests/tracemalloc_orderbook_depth.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2023 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from nautilus_trader.test_kit.fixtures.memory import snapshot_memory +from nautilus_trader.test_kit.stubs.data import TestDataStubs + + +@snapshot_memory(4000) +def run(*args, **kwargs): + _ = TestDataStubs.order_book_depth10() + + +if __name__ == "__main__": + run() diff --git a/tests/mem_leak_tests/tracemalloc_quote_ticks.py b/tests/mem_leak_tests/tracemalloc_quote_ticks.py new file mode 100644 index 000000000000..fd798e89bbce --- /dev/null +++ b/tests/mem_leak_tests/tracemalloc_quote_ticks.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2023 Nautech Systems Pty Ltd. All rights reserved. +# https://nautechsystems.io +# +# Licensed under the GNU Lesser General Public License Version 3.0 (the "License"); +# You may not use this file except in compliance with the License. +# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ------------------------------------------------------------------------------------------------- + +from nautilus_trader.test_kit.fixtures.memory import snapshot_memory +from nautilus_trader.test_kit.stubs.data import TestDataStubs + + +@snapshot_memory(4000) +def run(*args, **kwargs): + _ = TestDataStubs.quote_tick() + + +if __name__ == "__main__": + run() diff --git a/tests/mem_leak_tests/tracemalloc_trade_ticks.py b/tests/mem_leak_tests/tracemalloc_trade_ticks.py index b0938c333ff9..57d7706d87a6 100644 --- a/tests/mem_leak_tests/tracemalloc_trade_ticks.py +++ b/tests/mem_leak_tests/tracemalloc_trade_ticks.py @@ -14,36 +14,13 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- -from nautilus_trader.model.data import TradeTick -from nautilus_trader.model.enums import AggressorSide -from nautilus_trader.model.identifiers import TradeId -from nautilus_trader.model.objects import Price -from nautilus_trader.model.objects import Quantity - -# from nautilus_trader.persistence.wranglers import TradeTickDataWrangler from nautilus_trader.test_kit.fixtures.memory import snapshot_memory - -# from nautilus_trader.test_kit.providers import TestDataProvider -from nautilus_trader.test_kit.providers import TestInstrumentProvider - - -ETHUSDT_BINANCE = TestInstrumentProvider.ethusdt_binance() +from nautilus_trader.test_kit.stubs.data import TestDataStubs -@snapshot_memory(1000) +@snapshot_memory(4000) def run(*args, **kwargs): - # provider = TestDataProvider() - # wrangler = TradeTickDataWrangler(instrument=ETHUSDT_BINANCE) - # _ = wrangler.process(provider.read_csv_ticks("binance/binance-ethusdt-trades.csv")) - _ = TradeTick( - ETHUSDT_BINANCE.id, - Price.from_str("1.00000"), - Quantity.from_str("1"), - AggressorSide.BUYER, - TradeId("123456789"), - 1, - 2, - ) + _ = TestDataStubs.trade_tick() if __name__ == "__main__": diff --git a/tests/unit_tests/model/test_orderbook_data.py b/tests/unit_tests/model/test_orderbook_data.py index 9328ec8e3c1e..394f8240dcc3 100644 --- a/tests/unit_tests/model/test_orderbook_data.py +++ b/tests/unit_tests/model/test_orderbook_data.py @@ -21,10 +21,12 @@ 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.enums import BookAction from nautilus_trader.model.enums import OrderSide from nautilus_trader.model.objects import Price from nautilus_trader.model.objects import Quantity +from nautilus_trader.test_kit.stubs.data import TestDataStubs from nautilus_trader.test_kit.stubs.identifiers import TestIdStubs @@ -427,7 +429,7 @@ def test_deltas_hash_str_and_repr() -> None: ) -def test_deltas_to_dict() -> None: +def test_deltas_to_dict_from_dict_round_trip() -> None: # Arrange order1 = BookOrder( side=OrderSide.BUY, @@ -472,7 +474,7 @@ def test_deltas_to_dict() -> None: result = OrderBookDeltas.to_dict(deltas) # Assert - assert result + assert OrderBookDeltas.from_dict(result) == deltas assert result == { "type": "OrderBookDeltas", "instrument_id": "AUD/USD.SIM", @@ -547,3 +549,104 @@ def test_deltas_from_dict_returns_expected_dict() -> None: # Assert assert result == deltas + + +def test_depth10_fully_qualified_name() -> None: + # Arrange, Act, Assert + assert OrderBookDepth10.fully_qualified_name() == "nautilus_trader.model.data:OrderBookDepth10" + + +def test_depth10_new() -> None: + # Arrange, Act + instrument_id = TestIdStubs.aapl_xnas_id() + depth = TestDataStubs.order_book_depth10( + instrument_id=instrument_id, + flags=0, + sequence=1, + ts_event=2, + ts_init=3, + ) + + # Assert + assert depth.instrument_id == instrument_id + assert len(depth.bids) == 10 + assert len(depth.asks) == 10 + assert depth.flags == 0 + assert depth.sequence == 1 + assert depth.ts_event == 2 + assert depth.ts_init == 3 + + +def test_depth10_hash_str_repr() -> None: + # Arrange + depth = TestDataStubs.order_book_depth10( + flags=0, + sequence=1, + ts_event=2, + ts_init=3, + ) + + # Act, Assert + 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 + ) + 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 + ) + + +def test_depth10_to_dict_from_dict_round_trip() -> None: + # Arrange + depth = TestDataStubs.order_book_depth10( + flags=0, + sequence=1, + ts_event=2, + ts_init=3, + ) + + # Act + result = OrderBookDepth10.to_dict(depth) + + # Assert + assert OrderBookDepth10.from_dict(result) == depth + assert result == { + "type": "OrderBookDepth10", + "instrument_id": "AAPL.XNAS", + "bids": [ + {"type": "BookOrder", "side": "BUY", "price": "99.00", "size": "100", "order_id": 1}, + {"type": "BookOrder", "side": "BUY", "price": "98.00", "size": "200", "order_id": 2}, + {"type": "BookOrder", "side": "BUY", "price": "97.00", "size": "300", "order_id": 3}, + {"type": "BookOrder", "side": "BUY", "price": "96.00", "size": "400", "order_id": 4}, + {"type": "BookOrder", "side": "BUY", "price": "95.00", "size": "500", "order_id": 5}, + {"type": "BookOrder", "side": "BUY", "price": "94.00", "size": "600", "order_id": 6}, + {"type": "BookOrder", "side": "BUY", "price": "93.00", "size": "700", "order_id": 7}, + {"type": "BookOrder", "side": "BUY", "price": "92.00", "size": "800", "order_id": 8}, + {"type": "BookOrder", "side": "BUY", "price": "91.00", "size": "900", "order_id": 9}, + {"type": "BookOrder", "side": "BUY", "price": "90.00", "size": "1000", "order_id": 10}, + ], + "asks": [ + {"type": "BookOrder", "side": "SELL", "price": "100.00", "size": "100", "order_id": 11}, + {"type": "BookOrder", "side": "SELL", "price": "101.00", "size": "200", "order_id": 12}, + {"type": "BookOrder", "side": "SELL", "price": "102.00", "size": "300", "order_id": 13}, + {"type": "BookOrder", "side": "SELL", "price": "103.00", "size": "400", "order_id": 14}, + {"type": "BookOrder", "side": "SELL", "price": "104.00", "size": "500", "order_id": 15}, + {"type": "BookOrder", "side": "SELL", "price": "105.00", "size": "600", "order_id": 16}, + {"type": "BookOrder", "side": "SELL", "price": "106.00", "size": "700", "order_id": 17}, + {"type": "BookOrder", "side": "SELL", "price": "107.00", "size": "800", "order_id": 18}, + {"type": "BookOrder", "side": "SELL", "price": "108.00", "size": "900", "order_id": 19}, + { + "type": "BookOrder", + "side": "SELL", + "price": "109.00", + "size": "1000", + "order_id": 20, + }, + ], + "flags": 0, + "sequence": 1, + "ts_event": 2, + "ts_init": 3, + }