From d995981e4addeea4a496a0566271e7f62a7c558f Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 24 Oct 2022 15:20:39 +1100 Subject: [PATCH 01/38] Remove some indicators --- .../indicators/bid_ask_min_max.pxd | 48 ----- .../indicators/bid_ask_min_max.pyx | 168 ---------------- nautilus_trader/indicators/hilbert_period.pxd | 38 ---- nautilus_trader/indicators/hilbert_period.pyx | 160 --------------- nautilus_trader/indicators/hilbert_snr.pxd | 44 ----- nautilus_trader/indicators/hilbert_snr.pyx | 182 ------------------ .../indicators/hilbert_transform.pxd | 36 ---- .../indicators/hilbert_transform.pyx | 114 ----------- .../indicators/test_bid_ask_min_max.py | 180 ----------------- .../indicators/test_hilbert_period.py | 140 -------------- .../unit_tests/indicators/test_hilbert_snr.py | 141 -------------- .../indicators/test_hilbert_transform.py | 147 -------------- 12 files changed, 1398 deletions(-) delete mode 100644 nautilus_trader/indicators/bid_ask_min_max.pxd delete mode 100644 nautilus_trader/indicators/bid_ask_min_max.pyx delete mode 100644 nautilus_trader/indicators/hilbert_period.pxd delete mode 100644 nautilus_trader/indicators/hilbert_period.pyx delete mode 100644 nautilus_trader/indicators/hilbert_snr.pxd delete mode 100644 nautilus_trader/indicators/hilbert_snr.pyx delete mode 100644 nautilus_trader/indicators/hilbert_transform.pxd delete mode 100644 nautilus_trader/indicators/hilbert_transform.pyx delete mode 100644 tests/unit_tests/indicators/test_bid_ask_min_max.py delete mode 100644 tests/unit_tests/indicators/test_hilbert_period.py delete mode 100644 tests/unit_tests/indicators/test_hilbert_snr.py delete mode 100644 tests/unit_tests/indicators/test_hilbert_transform.py diff --git a/nautilus_trader/indicators/bid_ask_min_max.pxd b/nautilus_trader/indicators/bid_ask_min_max.pxd deleted file mode 100644 index f2406a3ae964..000000000000 --- a/nautilus_trader/indicators/bid_ask_min_max.pxd +++ /dev/null @@ -1,48 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# 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 cpython.datetime cimport datetime -from cpython.datetime cimport timedelta - -from nautilus_trader.indicators.base.indicator cimport Indicator -from nautilus_trader.model.identifiers cimport InstrumentId -from nautilus_trader.model.objects cimport Price - - -cdef class BidAskMinMax(Indicator): - cdef readonly InstrumentId instrument_id - """The instrument_id for inbound ticks.\n\n:returns: `InstrumentId`""" - cdef readonly timedelta lookback - """The look back duration in time.\n\n:returns: `timedelta`""" - cdef readonly WindowedMinMaxPrices bids - """The windowed min max prices.\n\n:returns: `WindowedMinMaxPrices`""" - cdef readonly WindowedMinMaxPrices asks - """The windowed min max prices.\n\n:returns: `WindowedMinMaxPrices`""" - - -cdef class WindowedMinMaxPrices: - cdef object _min_prices - cdef object _max_prices - - cdef readonly timedelta lookback - """The look back duration in time.\n\n:returns: `timedelta`""" - cdef readonly Price min_price - """The minimum price in the window.\n\n:returns: `Price`""" - cdef readonly Price max_price - """The maximum price in the window.\n\n:returns: `Price`""" - - cpdef void add_price(self, datetime ts, Price price) except * - cpdef void reset(self) except * - - cdef void _expire_stale_prices_by_cutoff(self, ts_prices, datetime cutoff) except * - cdef void _add_min_price(self, datetime ts, Price price) except * - cdef void _add_max_price(self, datetime ts, Price price) except * diff --git a/nautilus_trader/indicators/bid_ask_min_max.pyx b/nautilus_trader/indicators/bid_ask_min_max.pyx deleted file mode 100644 index 71aa43d74a38..000000000000 --- a/nautilus_trader/indicators/bid_ask_min_max.pyx +++ /dev/null @@ -1,168 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# 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 collections import deque - -import pandas as pd -from cpython.datetime cimport datetime -from cpython.datetime cimport timedelta - -from nautilus_trader.core.correctness cimport Condition -from nautilus_trader.core.datetime cimport is_datetime_utc -from nautilus_trader.indicators.base.indicator cimport Indicator -from nautilus_trader.model.data.tick cimport QuoteTick -from nautilus_trader.model.identifiers cimport InstrumentId -from nautilus_trader.model.objects cimport Price - - -cdef class BidAskMinMax(Indicator): - """ - Given a historic lookback window of bid/ask prices, keep a running - computation of the min/max values of the bid/ask prices within the window. - - Parameters - ---------- - instrument_id : InstrumentId - The instrument ID for inbound ticks. - lookback : timedelta - The look back duration in time. - """ - - def __init__(self, InstrumentId instrument_id not None, timedelta lookback not None): - super().__init__(params=[lookback]) - - self.instrument_id = instrument_id - self.lookback = lookback - - # Set up the bid/ask windows - self.bids = WindowedMinMaxPrices(lookback) - self.asks = WindowedMinMaxPrices(lookback) - - cpdef void handle_quote_tick(self, QuoteTick tick) except *: - """ - Update the indicator with the given tick. - - Parameters - ---------- - tick : QuoteTick - Incoming quote tick to process - - """ - self.bids.add_price(pd.Timestamp(tick.ts_init, tz="UTC"), tick.bid) - self.asks.add_price(pd.Timestamp(tick.ts_init, tz="UTC"), tick.ask) - - # Mark as having input and initialized - self._set_has_inputs(True) - self._set_initialized(True) - - cpdef void _reset(self) except *: - # Reset the windows - self.bids.reset() - self.asks.reset() - - -cdef class WindowedMinMaxPrices: - """ - Over the course of a defined lookback window, efficiently keep track - of the min/max values currently in the window. - - Parameters - ---------- - lookback : timedelta - The look back duration in time. - """ - - def __init__(self, timedelta lookback not None): - self.lookback = lookback - - # Initialize the dequeues - self._min_prices = deque() - self._max_prices = deque() - - # Set the min/max marks as None until we have data - self.min_price = None - self.max_price = None - - cpdef void add_price(self, datetime ts, Price price) except *: - """ - Given a price at a UTC timestamp, insert it into the structures and - update our running min/max values. - - Parameters - ---------- - ts : datetime - The timestamp for the price. - price : Price - The price to add. - - """ - Condition.true(is_datetime_utc(ts), "ts was not tz-aware UTC") - - # Expire old prices - cdef datetime cutoff = ts - self.lookback - self._expire_stale_prices_by_cutoff(self._min_prices, cutoff) - self._expire_stale_prices_by_cutoff(self._max_prices, cutoff) - - # Append to the min/max structures - self._add_min_price(ts, price) - self._add_max_price(ts, price) - - # Pull out the min/max - self.min_price = min([p[1] for p in self._min_prices]) - self.max_price = max([p[1] for p in self._max_prices]) - - cpdef void reset(self) except *: - """ - Reset the indicator. - - All stateful fields are reset to their initial value. - """ - # Set the min/max marks as None until we have data - self.min_price = None - self.max_price = None - - # Clear the dequeues - self._min_prices.clear() - self._max_prices.clear() - - cdef void _expire_stale_prices_by_cutoff( - self, - ts_prices, - datetime cutoff - ) except *: - """Drop items that are older than the cutoff""" - while ts_prices and ts_prices[0][0] < cutoff: - ts_prices.popleft() - - cdef void _add_min_price(self, datetime ts, Price price) except *: - """Handle appending to the min deque""" - # Pop front elements that are less than or equal (since we want the max ask) - while self._min_prices and self._min_prices[-1][1] <= price: - self._min_prices.pop() - - # Pop back elements that are less than or equal to the new ask - while self._min_prices and self._min_prices[0][1] <= price: - self._min_prices.popleft() - - self._min_prices.append((ts, price)) - - cdef void _add_max_price(self, datetime ts, Price price) except *: - """Handle appending to the max deque""" - # Pop front elements that are less than or equal (since we want the max bid) - while self._max_prices and self._max_prices[-1][1] <= price: - self._max_prices.pop() - - # Pop back elements that are less than or equal to the new bid - while self._max_prices and self._max_prices[0][1] <= price: - self._max_prices.popleft() - - self._max_prices.append((ts, price)) diff --git a/nautilus_trader/indicators/hilbert_period.pxd b/nautilus_trader/indicators/hilbert_period.pxd deleted file mode 100644 index aa167f94cb59..000000000000 --- a/nautilus_trader/indicators/hilbert_period.pxd +++ /dev/null @@ -1,38 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# Copyright (C) 2015-2022 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.indicators.base.indicator cimport Indicator -from nautilus_trader.model.data.bar cimport Bar - - -cdef class HilbertPeriod(Indicator): - cdef double _i_mult - cdef double _q_mult - cdef double _amplitude_floor - cdef object _inputs - cdef object _detrended_prices - cdef object _in_phase - cdef object _quadrature - cdef object _phase - cdef object _delta_phase - - cdef readonly int period - """The window period.\n\n:returns: `int`""" - cdef readonly double value - """The current value.\n\n:returns: `double`""" - - cpdef void handle_bar(self, Bar bar) except * - cpdef void update_raw(self, double high, double low) except * - cpdef void _calc_hilbert_transform(self) except * diff --git a/nautilus_trader/indicators/hilbert_period.pyx b/nautilus_trader/indicators/hilbert_period.pyx deleted file mode 100644 index ad210ec81181..000000000000 --- a/nautilus_trader/indicators/hilbert_period.pyx +++ /dev/null @@ -1,160 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# Copyright (C) 2015-2022 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 collections import deque - -import numpy as np - -from nautilus_trader.core.correctness cimport Condition -from nautilus_trader.indicators.base.indicator cimport Indicator -from nautilus_trader.model.data.bar cimport Bar - - -cdef class HilbertPeriod(Indicator): - """ - An indicator which calculates the instantaneous period of phase-change for a - market across a rolling window. One basic definition of a cycle is that the - phase has a constant rate of change, i.e. A 10 bar cycle changes phase at - the rate of 36 degrees per bar so that 360 degrees of phase is completed - (one full cycle) every 10 bars. - - Parameters - ---------- - period : int - The rolling window period for the indicator (> 0). - """ - - def __init__(self, int period=7): - Condition.positive_int(period, "period") - super().__init__(params=[period]) - - self.period = period - self._i_mult = 0.635 - self._q_mult = 0.338 - self._amplitude_floor = 0.001 - self._inputs = deque(maxlen=self.period) - self._detrended_prices = deque(maxlen=self.period) - self._in_phase =deque([0.0] * self.period, maxlen=self.period) - self._quadrature = deque([0.0] * self.period, maxlen=self.period) - self._phase = deque([0.0] * 2, maxlen=2) - self._delta_phase = [] - self.value = 0 # The last instantaneous period value - - cpdef void handle_bar(self, Bar bar) except *: - """ - Update the indicator with the given bar. - - Parameters - ---------- - bar : Bar - The update bar. - - """ - Condition.not_none(bar, "bar") - - self.update_raw(bar.high.as_double(), bar.low.as_double()) - - cpdef void update_raw(self, double high, double low) except *: - """ - Update the indicator with the given raw values. - - Parameters - ---------- - high : double - The high price. - low : double - The low price. - - """ - self._inputs.append((high + low) / 2) - - # Initialization logic (leave this here) - if not self.initialized: - self._set_has_inputs(True) - if len(self._inputs) >= self.period: - self._set_initialized(True) - else: - return - - # Update de-trended prices - self._detrended_prices.append(self._inputs[-1] - self._inputs[0]) - - # If insufficient de-trended prices to index feedback then return - if len(self._detrended_prices) < 5: - return - - self._calc_hilbert_transform() - - # Compute current phase - cdef double in_phase_value = self._in_phase[-1] + self._in_phase[-2] - cdef double quadrature_value = self._quadrature[-1] + self._quadrature[-2] - if abs(in_phase_value) > 0.0: - self._phase.append(np.arctan(abs(quadrature_value / in_phase_value))) - - # Resolve the arc tangent ambiguity - if self._in_phase[-1] < 0.0 < self._quadrature[-1]: - self._phase.append(180 - self._phase[-1]) - if self._in_phase[-1] < 0.0 and self._quadrature[-1] < 0.0: - self._phase.append(180 + self._phase[-1]) - if self._in_phase[-1] > 0.0 > self._quadrature[-1]: - self._phase.append(360 - self._phase[-1]) - - # Compute a differential phase, resolve wraparound, and limit delta-phase errors - self._delta_phase.append(self._phase[-1] - self._phase[-2]) - if self._phase[-2] < 90. and self._phase[-1] > 270.0: - self._delta_phase[-1] = 360 + self._phase[-2] - self._phase[-1] - if self._delta_phase[-1] < 1: - self._delta_phase[-1] = 1 - if self._delta_phase[-1] > 60: - self._delta_phase[-1] = 60 - - # Sum delta-phase to reach 360 degrees (sum loop count is the instantaneous period) - cdef int inst_period = 0 - cdef int cumulative_delta_phase = 0 - cdef int i - for i in range(min(len(self._delta_phase) - 1, 50)): - cumulative_delta_phase += self._delta_phase[-(1 + i)] - if cumulative_delta_phase > 360.0: - inst_period = i - break - - self.value = max(inst_period, self.period) - - cpdef void _calc_hilbert_transform(self) except *: - # Calculate the Hilbert Transform and update in-phase and quadrature values - # Calculate feedback - cdef double feedback1 = self._detrended_prices[-1] # V1 (last) - cdef double feedback2 = self._detrended_prices[-3] # V2 (2 elements back from the last) - cdef double feedback4 = self._detrended_prices[-5] # V4 (4 elements back from the last) - - cdef double in_phase3 = self._in_phase[-4] # (3 elements back from the last) - cdef double quadrature2 = self._quadrature[-3] # (2 elements back from the last) - - # Calculate in-phase - self._in_phase.append( - 1.25 * (feedback4 - (self._i_mult * feedback2) + (self._i_mult * in_phase3))) - - # Calculate quadrature - self._quadrature.append( - feedback2 - (self._q_mult * feedback1) + (self._q_mult * quadrature2)) - - cpdef void _reset(self) except *: - self._inputs.clear() - self._detrended_prices.clear() - self._in_phase = deque([0.0] * self.period, maxlen=self.period) - self._quadrature = deque([0.0] * self.period, maxlen=self.period) - self._phase = deque([0.0] * 2, maxlen=2) - self._delta_phase.clear() - self.value = 0.0 diff --git a/nautilus_trader/indicators/hilbert_snr.pxd b/nautilus_trader/indicators/hilbert_snr.pxd deleted file mode 100644 index 3bb5dee8a371..000000000000 --- a/nautilus_trader/indicators/hilbert_snr.pxd +++ /dev/null @@ -1,44 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# Copyright (C) 2015-2022 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.indicators.base.indicator cimport Indicator -from nautilus_trader.model.data.bar cimport Bar - - -cdef class HilbertSignalNoiseRatio(Indicator): - cdef double _i_mult - cdef double _q_mult - cdef double _range_floor - cdef double _amplitude_floor - cdef object _inputs - cdef object _detrended_prices - cdef object _in_phase - cdef object _quadrature - cdef double _previous_range - cdef double _previous_amplitude - cdef double _previous_value - cdef double _range - cdef double _amplitude - - cdef readonly int period - """The window period.\n\n:returns: `int`""" - cdef readonly double value - """The last amplitude value.\n\n:returns: `double`""" - - cpdef void handle_bar(self, Bar bar) except * - cpdef void update_raw(self, double high, double low) except * - cdef void _calc_hilbert_transform(self) except * - cdef double _calc_amplitude(self) - cdef double _calc_signal_noise_ratio(self) diff --git a/nautilus_trader/indicators/hilbert_snr.pyx b/nautilus_trader/indicators/hilbert_snr.pyx deleted file mode 100644 index 02e293e40a41..000000000000 --- a/nautilus_trader/indicators/hilbert_snr.pyx +++ /dev/null @@ -1,182 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# Copyright (C) 2015-2022 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 collections import deque - -import numpy as np - -from nautilus_trader.core.correctness cimport Condition -from nautilus_trader.indicators.base.indicator cimport Indicator - - -cdef class HilbertSignalNoiseRatio(Indicator): - """ - An indicator which calculates the amplitude of a signal. - - Parameters - ---------- - period : int - The rolling window period for the indicator (> 0). - range_floor : double - The floor value for range calculations. - amplitude_floor : double - The floor value for amplitude calculations (0.001 from paper). - """ - - def __init__( - self, - int period=7, - double range_floor=0.00001, - double amplitude_floor=0.001, - ): - Condition.positive_int(period, "period") - Condition.not_negative(range_floor, "range_floor") - Condition.not_negative(amplitude_floor, "amplitude_floor") - super().__init__(params=[period]) - - self.period = period - self._i_mult = 0.635 - self._q_mult = 0.338 - self._range_floor = range_floor - self._amplitude_floor = amplitude_floor - self._inputs = deque(maxlen=self.period) - self._detrended_prices = deque(maxlen=self.period) - self._in_phase = deque([0] * self.period, maxlen=self.period) - self._quadrature = deque([0] * self.period, maxlen=self.period) - self._previous_range = 0 - self._previous_amplitude = 0 - self._previous_value = 0 - self._range = 0 - self._amplitude = 0 - self.value = 0 # The last amplitude value (dB) - - cpdef void handle_bar(self, Bar bar) except *: - """ - Update the indicator with the given bar. - - Parameters - ---------- - bar : Bar - The update bar. - - """ - Condition.not_none(bar, "bar") - - self.update_raw(bar.high.as_double(), bar.low.as_double()) - - cpdef void update_raw(self, double high, double low) except *: - """ - Update the indicator with the given raw values. - - Parameters - ---------- - high : double - The high price. - low : double - The low price. - - """ - self._inputs.append((high + low) / 2.0) - - # Initialization logic - if not self.initialized: - # Do not initialize __has_inputs here - if len(self._inputs) >= self.period: - self._set_initialized(True) - else: - return - - # Update de-trended prices - self._detrended_prices.append(self._inputs[-1] - self._inputs[0]) - - cdef double last_range = high - low - # Must have a trading range to calculate - if last_range == 0: - return - - # If initial input then initialize ranges - if self._previous_range == 0: - self._previous_range = last_range - - # Compute noise as the average (smoothed) range - self._range = max((0.2 * last_range) + (0.8 * self._previous_range), self._range_floor) - self._previous_range = self._range - - # If insufficient de-trended prices to index feedback then return - if len(self._detrended_prices) < 5: - return - - self._calc_hilbert_transform() - - cdef double amplitude = self._calc_amplitude() - # If initial input then initialize amplitude - if self._previous_amplitude == 0: - self._previous_amplitude = amplitude - - # Calculate smoothed signal amplitude - self._amplitude = max((0.2 * amplitude) + (0.8 * self._previous_amplitude), self._amplitude_floor) - self._previous_amplitude = self._amplitude - - if self._range == 0.0: - return # Cannot calculate (guarding against divide by zero) - - # If initial input then initialize value - if not self.has_inputs: - self.value = self._calc_signal_noise_ratio() - self._previous_value = self.value - self._set_has_inputs(True) - - # Compute smoothed SNR in Decibels - self.value = (0.25 * self._calc_signal_noise_ratio()) + (0.75 * self._previous_value) - self._previous_value = self.value - - cdef void _calc_hilbert_transform(self) except *: - # Calculate the Hilbert Transform and update in-phase and quadrature values - # Calculate feedback - cdef double feedback1 = self._detrended_prices[-1] # V1 (last) - cdef double feedback2 = self._detrended_prices[-3] # V2 (2 elements back from the last) - cdef double feedback4 = self._detrended_prices[-5] # V4 (4 elements back from the last) - - cdef double in_phase3 = self._in_phase[-4] # (3 elements back from the last) - cdef double quadrature2 = self._quadrature[-3] # (2 elements back from the last) - - # Calculate in-phase - self._in_phase.append( - 1.25 * (feedback4 - (self._i_mult * feedback2) + (self._i_mult * in_phase3))) - - # Calculate quadrature - self._quadrature.append( - feedback2 - (self._q_mult * feedback1) + (self._q_mult * quadrature2)) - - cdef double _calc_amplitude(self): - # Calculate the signal amplitude - return (np.power(self._in_phase[-1], 2)) + (np.power(self._quadrature[-1], 2)) - - cdef double _calc_signal_noise_ratio(self): - # Calculate the signal to noise ratio - cdef double range_squared = np.power(self._range, 2) - return (10 * np.log(self._amplitude / range_squared)) / np.log(10) + 1.9 - - cpdef void _reset(self) except *: - self._inputs.clear() - self._detrended_prices.clear() - self._in_phase = deque([0] * self.period, maxlen=self.period) - self._quadrature = deque([0] * self.period, maxlen=self.period) - self._previous_range = 0 - self._previous_amplitude = 0 - self._previous_value = 0 - self._range = 0 - self._amplitude = 0 - self.value = 0 diff --git a/nautilus_trader/indicators/hilbert_transform.pxd b/nautilus_trader/indicators/hilbert_transform.pxd deleted file mode 100644 index f26b38106ebf..000000000000 --- a/nautilus_trader/indicators/hilbert_transform.pxd +++ /dev/null @@ -1,36 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# Copyright (C) 2015-2022 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.indicators.base.indicator cimport Indicator -from nautilus_trader.model.data.bar cimport Bar - - -cdef class HilbertTransform(Indicator): - cdef double _i_mult - cdef double _q_mult - cdef object _inputs - cdef object _detrended_prices - cdef object _in_phase - cdef object _quadrature - - cdef readonly int period - """The window period.\n\n:returns: `int`""" - cdef readonly double value_in_phase - """The last in-phase value (real part of complex number).\n\n:returns: `double`""" - cdef readonly double value_quad - """The last quadrature value (imaginary part of complex number).\n\n:returns: `double`""" - - cpdef void handle_bar(self, Bar bar) except * - cpdef void update_raw(self, double price) except * diff --git a/nautilus_trader/indicators/hilbert_transform.pyx b/nautilus_trader/indicators/hilbert_transform.pyx deleted file mode 100644 index 8a6a51477a30..000000000000 --- a/nautilus_trader/indicators/hilbert_transform.pyx +++ /dev/null @@ -1,114 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# Copyright (C) 2015-2022 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 collections import deque - -from nautilus_trader.core.correctness cimport Condition -from nautilus_trader.indicators.base.indicator cimport Indicator - - -cdef class HilbertTransform(Indicator): - """ - An indicator which calculates a Hilbert Transform across a rolling window. - The Hilbert Transform itself, is an all-pass filter used in digital signal - processing. By using present and prior price differences, and some feedback, - price values are split into their complex number components of real (in-phase) - and imaginary (quadrature) parts. - - Parameters - ---------- - period : int - The rolling window period for the indicator (> 0). - """ - - def __init__(self, int period=7): - Condition.positive_int(period, "period") - super().__init__(params=[period]) - - self.period = period - self._i_mult = 0.635 - self._q_mult = 0.338 - self._inputs = deque(maxlen=self.period) - self._detrended_prices = deque(maxlen=self.period) - self._in_phase = deque([0] * self.period, maxlen=self.period) - self._quadrature = deque([0] * self.period, maxlen=self.period) - self.value_in_phase = 0 - self.value_quad = 0 - - cpdef void handle_bar(self, Bar bar) except *: - """ - Update the indicator with the given bar. - - Parameters - ---------- - bar : Bar - The update bar. - - """ - Condition.not_none(bar, "bar") - - self.update_raw(bar.close.as_double()) - - cpdef void update_raw(self, double price) except *: - """ - Update the indicator with the given raw value. - - Parameters - ---------- - price : double - The price. - - """ - self._inputs.append(price) - - # Initialization logic - if not self.initialized: - self._set_has_inputs(True) - if len(self._inputs) >= self.period: - self._set_initialized(True) - else: - return - - # Update de-trended prices - self._detrended_prices.append(price - self._inputs[0]) - - # If insufficient de-trended prices to index feedback then return - if len(self._detrended_prices) < 5: - return - - # Calculate feedback - cdef double feedback1 = self._detrended_prices[-1] # V1 (last) - cdef double feedback2 = self._detrended_prices[-3] # V2 (2 elements back from the last) - cdef double feedback4 = self._detrended_prices[-5] # V4 (4 elements back from the last) - - cdef double in_phase3 = self._in_phase[-4] # (3 elements back from the last) - cdef double quadrature2 = self._quadrature[-3] # (2 elements back from the last) - - # Calculate in-phase - self._in_phase.append( - 1.25 * (feedback4 - (self._i_mult * feedback2) + (self._i_mult * in_phase3))) - - # Calculate quadrature - self._quadrature.append( - feedback2 - (self._q_mult * feedback1) + (self._q_mult * quadrature2)) - - self.value_in_phase = self._in_phase[-1] - self.value_quad = self._quadrature[-1] - - cpdef void _reset(self) except *: - self._inputs.clear() - self._detrended_prices.clear() - self._in_phase = deque([0] * self.period, maxlen=self.period) - self._quadrature = deque([0] * self.period, maxlen=self.period) diff --git a/tests/unit_tests/indicators/test_bid_ask_min_max.py b/tests/unit_tests/indicators/test_bid_ask_min_max.py deleted file mode 100644 index addfcef274b3..000000000000 --- a/tests/unit_tests/indicators/test_bid_ask_min_max.py +++ /dev/null @@ -1,180 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# 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 datetime import datetime -from datetime import timedelta - -import pytz - -from nautilus_trader.indicators.bid_ask_min_max import BidAskMinMax -from nautilus_trader.indicators.bid_ask_min_max import WindowedMinMaxPrices -from nautilus_trader.model.data.tick import QuoteTick -from nautilus_trader.model.identifiers import InstrumentId -from nautilus_trader.model.identifiers import Symbol -from nautilus_trader.model.identifiers import Venue -from nautilus_trader.model.objects import Price -from nautilus_trader.model.objects import Quantity - - -class TestBidAskMinMax: - instrument_id = InstrumentId(Symbol("SPY"), Venue("NYSE")) - - def test_instantiate(self): - # Arrange - indicator = BidAskMinMax(self.instrument_id, timedelta(minutes=5)) - - # Act, Assert - assert indicator.bids.min_price is None - assert indicator.bids.max_price is None - assert indicator.asks.min_price is None - assert indicator.asks.max_price is None - assert indicator.initialized is False - - def test_handle_quote_tick(self): - # Arrange - indicator = BidAskMinMax(self.instrument_id, timedelta(minutes=5)) - - # Act - indicator.handle_quote_tick( - QuoteTick( - instrument_id=self.instrument_id, - bid=Price.from_str("1.0"), - ask=Price.from_str("2.0"), - bid_size=Quantity.from_int(1), - ask_size=Quantity.from_int(1), - ts_event=0, - ts_init=0, - ) - ) - # 5 min later (still in the window) - indicator.handle_quote_tick( - QuoteTick( - instrument_id=self.instrument_id, - bid=Price.from_str("0.9"), - ask=Price.from_str("2.1"), - bid_size=Quantity.from_int(1), - ask_size=Quantity.from_int(1), - ts_event=3e11, - ts_init=3e11, - ) - ) - - # Assert - assert indicator.bids.min_price == Price.from_str("0.9") - assert indicator.bids.max_price == Price.from_str("1.0") - assert indicator.asks.min_price == Price.from_str("2.1") - assert indicator.asks.max_price == Price.from_str("2.1") - - def test_reset(self): - # Arrange - indicator = BidAskMinMax(self.instrument_id, timedelta(minutes=5)) - - indicator.handle_quote_tick( - QuoteTick( - instrument_id=self.instrument_id, - bid=Price.from_str("0.9"), - ask=Price.from_str("2.1"), - bid_size=Quantity.from_int(1), - ask_size=Quantity.from_int(1), - ts_event=0, - ts_init=0, - ) - ) - - # Act - indicator.reset() - - # Assert - assert indicator.bids.min_price is None - assert indicator.asks.min_price is None - - -class TestWindowedMinMaxPrices: - def test_instantiate(self): - # Arrange - instance = WindowedMinMaxPrices(timedelta(minutes=5)) - - # Act, Assert - assert instance.min_price is None - assert instance.max_price is None - - def test_add_price(self): - # Arrange - instance = WindowedMinMaxPrices(timedelta(minutes=5)) - - # Act - instance.add_price( - datetime(2020, 1, 1, 0, 0, 0, tzinfo=pytz.utc), - Price.from_str("1.0"), - ) - # Assert - assert instance.min_price == Price.from_str("1.0") - assert instance.max_price == Price.from_str("1.0") - - def test_add_multiple_prices(self): - # Arrange - instance = WindowedMinMaxPrices(timedelta(minutes=5)) - - # Act - instance.add_price( - datetime(2020, 1, 1, 0, 0, 0, tzinfo=pytz.utc), - Price.from_str("1.0"), - ) - # 5 min later (still in the window) - instance.add_price( - datetime(2020, 1, 1, 0, 5, 0, tzinfo=pytz.utc), - Price.from_str("0.9"), - ) - - # Assert - assert instance.min_price == Price.from_str("0.9") - assert instance.max_price == Price.from_str("1.0") - - def test_expire_items(self): - # Arrange - instance = WindowedMinMaxPrices(timedelta(minutes=5)) - - # Act - instance.add_price( - datetime(2020, 1, 1, 0, 0, 0, tzinfo=pytz.utc), - Price.from_str("1.0"), - ) - # 5 min later (still in the window) - instance.add_price( - datetime(2020, 1, 1, 0, 5, 0, tzinfo=pytz.utc), - Price.from_str("0.9"), - ) - # Allow the first item to expire out - # This also tests that the new tick is the new min/max - instance.add_price( - datetime(2020, 1, 1, 0, 5, 1, tzinfo=pytz.utc), - Price.from_str("0.95"), - ) - - # Assert - assert instance.min_price == Price.from_str("0.95") - assert instance.max_price == Price.from_str("0.95") - - def test_reset(self): - # Arrange - instance = WindowedMinMaxPrices(timedelta(minutes=5)) - - # Act - instance.add_price( - datetime(2020, 1, 1, 0, 0, 0, tzinfo=pytz.utc), - Price.from_str("1"), - ) - instance.reset() - - # Assert - assert instance.min_price is None - assert instance.max_price is None diff --git a/tests/unit_tests/indicators/test_hilbert_period.py b/tests/unit_tests/indicators/test_hilbert_period.py deleted file mode 100644 index 2e3e8e8e0a7b..000000000000 --- a/tests/unit_tests/indicators/test_hilbert_period.py +++ /dev/null @@ -1,140 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# Copyright (C) 2015-2022 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. -# ------------------------------------------------------------------------------------------------- - -import sys - -from nautilus_trader.backtest.data.providers import TestInstrumentProvider -from nautilus_trader.indicators.hilbert_period import HilbertPeriod -from tests.test_kit.stubs.data import TestDataStubs - - -AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy("AUD/USD") - - -class TestHilbertPeriod: - def setup(self): - # Fixture Setup - self.h_period = HilbertPeriod() - - def test_name_returns_expected_name(self): - # Arrange, Act, Assert - assert self.h_period.name == "HilbertPeriod" - - def test_str_returns_expected_string(self): - # Arrange, Act, Assert - assert str(self.h_period) == "HilbertPeriod(7)" - assert repr(self.h_period) == "HilbertPeriod(7)" - - def test_period_returns_expected_value(self): - # Arrange, Act, Assert - assert self.h_period.period == 7 - - def test_initialized_without_inputs_returns_false(self): - # Arrange, Act, Assert - assert self.h_period.initialized is False - - def test_initialized_with_required_inputs_returns_true(self): - # Arrange, Act - for _i in range(10): - self.h_period.update_raw(1.00010, 1.00000) - - # Assert - assert self.h_period.initialized is True - - def test_handle_bar_updates_indicator(self): - # Arrange - indicator = HilbertPeriod() - - bar = TestDataStubs.bar_5decimal() - - # Act - indicator.handle_bar(bar) - - # Assert - assert indicator.has_inputs - assert indicator.value == 0 - - def test_value_with_no_inputs_returns_none(self): - # Arrange, Act, Assert - assert self.h_period.value == 0 - - def test_value_with_epsilon_inputs_returns_expected_value(self): - # Arrange - for _i in range(100): - self.h_period.update_raw(sys.float_info.epsilon, sys.float_info.epsilon) - - # Act, Assert - assert self.h_period.value == 7 - - def test_value_with_ones_inputs_returns_expected_value(self): - # Arrange - for _i in range(100): - self.h_period.update_raw(1.00000, 1.00000) - - # Act, Assert - assert self.h_period.value == 7 - - def test_value_with_seven_inputs_returns_expected_value(self): - # Arrange - high = 1.00010 - low = 1.00000 - - # Act - for _i in range(9): - high += 0.00010 - low += 0.00010 - self.h_period.update_raw(high, low) - - # Assert - assert self.h_period.value == 0 - - def test_value_with_close_on_high_returns_expected_value(self): - # Arrange - high = 1.00010 - low = 1.00000 - - # Act - for _i in range(1000): - high += 0.00010 - low += 0.00010 - self.h_period.update_raw(high, low) - - # Assert - assert self.h_period.value == 7 - - def test_value_with_close_on_low_returns_expected_value(self): - # Arrange - high = 1.00010 - low = 1.00000 - - # Act - for _i in range(1000): - high -= 0.00010 - low -= 0.00010 - self.h_period.update_raw(high, low) - - # Assert - assert self.h_period.value == 7 - - def test_reset_successfully_returns_indicator_to_fresh_state(self): - # Arrange - for _i in range(1000): - self.h_period.update_raw(1.00000, 1.00000) - - # Act - self.h_period.reset() - - # Assert - assert self.h_period.value == 0 # No exceptions raised diff --git a/tests/unit_tests/indicators/test_hilbert_snr.py b/tests/unit_tests/indicators/test_hilbert_snr.py deleted file mode 100644 index 7ad959749dfd..000000000000 --- a/tests/unit_tests/indicators/test_hilbert_snr.py +++ /dev/null @@ -1,141 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# Copyright (C) 2015-2022 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. -# ------------------------------------------------------------------------------------------------- - -import sys - -import pytest - -from nautilus_trader.backtest.data.providers import TestInstrumentProvider -from nautilus_trader.indicators.hilbert_snr import HilbertSignalNoiseRatio -from tests.test_kit.stubs.data import TestDataStubs - - -AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy("AUD/USD") - - -class TestHilbertSignalNoiseRatio: - def setup(self): - # Fixture Setup - self.snr = HilbertSignalNoiseRatio() - - def test_name_returns_expected_name(self): - # Arrange, Act, Assert - assert self.snr.name == "HilbertSignalNoiseRatio" - - def test_str_returns_expected_string(self): - # Arrange, Act, Assert - assert str(self.snr) == "HilbertSignalNoiseRatio(7)" - assert repr(self.snr) == "HilbertSignalNoiseRatio(7)" - - def test_period_returns_expected_value(self): - # Arrange, Act, Assert - assert self.snr.period == 7 - - def test_initialized_without_inputs_returns_false(self): - # Arrange, Act, Assert - assert self.snr.initialized is False - - def test_initialized_with_required_inputs_returns_true(self): - # Arrange, Act - for _i in range(10): - self.snr.update_raw(1.00010, 1.00000) - - # Assert - assert self.snr.initialized is True - - def test_handle_bar_updates_indicator(self): - # Arrange - indicator = HilbertSignalNoiseRatio() - - bar = TestDataStubs.bar_5decimal() - - # Act - indicator.handle_bar(bar) - - # Assert - assert indicator.value == 0 - - def test_value_with_no_inputs_returns_none(self): - # Arrange, Act, Assert - assert self.snr.value == 0.0 - - def test_value_with_epsilon_inputs_returns_expected_value(self): - # Arrange - for _i in range(100): - self.snr.update_raw(sys.float_info.epsilon, sys.float_info.epsilon) - - # Act, Assert - assert self.snr.value == 0 - - def test_value_with_ones_inputs_returns_expected_value(self): - # Arrange - for _i in range(100): - self.snr.update_raw(1.00000, 1.00000) - - # Act, Assert - assert self.snr.value == 0 - - def test_value_with_seven_inputs_returns_expected_value(self): - # Arrange - high = 1.00010 - low = 1.00000 - - # Act - for _i in range(9): - high += 0.00010 - low += 0.00010 - self.snr.update_raw(high, low) - - # Assert - assert self.snr.value == 0 - - def test_value_with_close_on_high_returns_expected_value(self): - # Arrange - high = 1.00010 - low = 1.00000 - - # Act - for _i in range(1000): - high += 0.00010 - low += 0.00010 - self.snr.update_raw(high, low) - - # Assert - assert self.snr.value == pytest.approx(51.90) - - def test_value_with_close_on_low_returns_expected_value(self): - # Arrange - high = 1.00010 - low = 1.00000 - - # Act - for _i in range(1000): - high -= 0.00010 - low -= 0.00010 - self.snr.update_raw(high, low) - - # Assert - assert self.snr.value == pytest.approx(51.90) - - def test_reset_successfully_returns_indicator_to_fresh_state(self): - # Arrange - for _i in range(1000): - self.snr.update_raw(1.00000, 1.00000) - - # Act - self.snr.reset() - - # Assert - assert self.snr.value == 0.0 # No assertion errors. diff --git a/tests/unit_tests/indicators/test_hilbert_transform.py b/tests/unit_tests/indicators/test_hilbert_transform.py deleted file mode 100644 index 484b50f7b411..000000000000 --- a/tests/unit_tests/indicators/test_hilbert_transform.py +++ /dev/null @@ -1,147 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# Copyright (C) 2015-2022 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. -# ------------------------------------------------------------------------------------------------- - -import sys - -from nautilus_trader.backtest.data.providers import TestInstrumentProvider -from nautilus_trader.indicators.hilbert_transform import HilbertTransform -from tests.test_kit.stubs.data import TestDataStubs - - -AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy("AUD/USD") - - -class TestHilbertTransform: - def setup(self): - # Fixture Setup - self.ht = HilbertTransform() - - def test_name_returns_expected_name(self): - # Arrange, Act, Assert - assert self.ht.name == "HilbertTransform" - - def test_str_returns_expected_string(self): - # Arrange, Act, Assert - assert str(self.ht) == "HilbertTransform(7)" - assert repr(self.ht) == "HilbertTransform(7)" - - def test_period_returns_expected_value(self): - # Arrange, Act, Assert - assert self.ht.period == 7 - - def test_initialized_without_inputs_returns_false(self): - # Arrange, Act, Assert - assert self.ht.initialized is False - - def test_initialized_with_required_inputs_returns_true(self): - # Arrange, Act - for _i in range(10): - self.ht.update_raw(1.00000) - - # Assert - assert self.ht.initialized is True - - def test_handle_bar_updates_indicator(self): - # Arrange - indicator = HilbertTransform() - - bar = TestDataStubs.bar_5decimal() - - # Act - indicator.handle_bar(bar) - - # Assert - assert indicator.has_inputs - assert indicator.value_quad == 0 - - def test_value_with_no_inputs_returns_none(self): - # Arrange, Act, Assert - assert self.ht.value_in_phase == 0.0 - assert self.ht.value_quad == 0.0 - - def test_value_with_epsilon_inputs_returns_expected_value(self): - # Arrange - for _i in range(100): - self.ht.update_raw(sys.float_info.epsilon) - - # Act, Assert - assert self.ht.value_in_phase == 0.0 - assert self.ht.value_quad == 0.0 - - def test_value_with_ones_inputs_returns_expected_value(self): - # Arrange - for _i in range(100): - self.ht.update_raw(1.00000) - - # Act, Assert - assert self.ht.value_in_phase == 0.0 - assert self.ht.value_quad == 0.0 - - def test_value_with_seven_inputs_returns_expected_value(self): - # Arrange - high = 1.00010 - low = 1.00000 - - # Act - for _i in range(9): - high += 0.00010 - low += 0.00010 - self.ht.update_raw((high + low) / 2) - - # Assert - assert self.ht.value_in_phase == 0.0 - assert self.ht.value_quad == 0.0 - - def test_value_with_close_on_high_returns_expected_value(self): - # Arrange - high = 1.00010 - low = 1.00000 - - # Act - for _i in range(1000): - high += 0.00010 - low += 0.00010 - self.ht.update_raw((high + low) / 2) - - # Assert - assert self.ht.value_in_phase == 0.001327272727272581 - assert self.ht.value_quad == 0.0005999999999999338 - - def test_value_with_close_on_low_returns_expected_value(self): - # Arrange - high = 1.00010 - low = 1.00000 - - # Act - for _i in range(1000): - high -= 0.00010 - low -= 0.00010 - self.ht.update_raw((high + low) / 2) - - # Assert - assert self.ht.value_in_phase == -0.001327272727272581 - assert self.ht.value_quad == -0.0005999999999999338 - - def test_reset_successfully_returns_indicator_to_fresh_state(self): - # Arrange - for _i in range(1000): - self.ht.update_raw(1.00000) - - # Act - self.ht.reset() - - # Assert - assert self.ht.value_in_phase == 0.0 # No assertion errors. - assert self.ht.value_quad == 0.0 From 7fcb6867d645127d3956627fd4e9cc93e1c9a64b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 24 Oct 2022 15:26:02 +1100 Subject: [PATCH 02/38] Update dependencies and bump version --- RELEASES.md | 18 ++++++++++++++++++ docs/conf.py | 4 ++-- nautilus_core/Cargo.lock | 8 ++++---- poetry.lock | 6 +++--- pyproject.toml | 2 +- version.json | 2 +- 6 files changed, 29 insertions(+), 11 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index e4829c9016ee..4d92be5518b9 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,21 @@ +# NautilusTrader 1.158.0 Beta + +Released on TBD (UTC). + +### Breaking Changes +- Removed `BidAskMinMax` indicator (to reduce total package size) +- Removed `HilbertPeriod` indicator (to reduce total package size) +- Removed `HilbertSignalNoiseRatio` indicator (to reduce total package size) +- Removed `HilbertTransform` indicator (to reduce total package size) + +### Enhancements +None + +### Fixes +None + +--- + # NautilusTrader 1.157.0 Beta Released on 24th October (UTC). diff --git a/docs/conf.py b/docs/conf.py index 3cded4fe0415..0bebee7aeb0f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -112,8 +112,8 @@ "version_dropdown": True, "version_json": "_static/version.json", "version_info": { - "1.157.0 (develop)": "https://docs.nautilustrader.io", - "1.156.0 (latest)": "https://docs.nautilustrader.io/latest", + "1.158.0 (develop)": "https://docs.nautilustrader.io", + "1.157.0 (latest)": "https://docs.nautilustrader.io/latest", }, "table_classes": ["plain"], } diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 435aadb4b14f..7f974168f316 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -258,9 +258,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.22" +version = "3.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" +checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7" dependencies = [ "atty", "bitflags", @@ -1391,9 +1391,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.1" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "time" diff --git a/poetry.lock b/poetry.lock index f1e9566d94a2..bb975bc37461 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1442,7 +1442,7 @@ multidict = ">=4.0" [[package]] name = "zipp" -version = "3.9.0" +version = "3.10.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "dev" optional = false @@ -2882,6 +2882,6 @@ yarl = [ {file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"}, ] zipp = [ - {file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"}, - {file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"}, + {file = "zipp-3.10.0-py3-none-any.whl", hash = "sha256:4fcb6f278987a6605757302a6e40e896257570d11c51628968ccb2a47e80c6c1"}, + {file = "zipp-3.10.0.tar.gz", hash = "sha256:7a7262fd930bd3e36c50b9a64897aec3fafff3dfdeec9623ae22b40e93f99bb8"}, ] diff --git a/pyproject.toml b/pyproject.toml index 9074a0ff6bc2..149363135059 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nautilus_trader" -version = "1.157.0" +version = "1.158.0" description = "A high-performance algorithmic trading platform and event-driven backtester" authors = ["Nautech Systems "] license = "LGPL-3.0-or-later" diff --git a/version.json b/version.json index 34c3ad29848d..4a0299773677 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "label": "", - "message": "v1.157.0", + "message": "v1.158.0", "color": "orange" } From 225f0ba37bd3dc883692f6b4cc20584cb12d671b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 27 Oct 2022 17:53:30 +1100 Subject: [PATCH 03/38] Update dependencies --- nautilus_core/Cargo.lock | 12 +++--- poetry.lock | 89 ++++++++++++++++++++-------------------- pyproject.toml | 6 +-- 3 files changed, 53 insertions(+), 54 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 7f974168f316..09a1ad7986c6 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -258,9 +258,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.21" +version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ed5341b2301a26ab80be5cbdced622e80ed808483c52e45e3310a877d3b37d7" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "atty", "bitflags", @@ -786,9 +786,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.135" +version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] name = "link-cplusplus" @@ -1391,9 +1391,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.0" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "time" diff --git a/poetry.lock b/poetry.lock index bb975bc37461..35adc6607fc3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -188,11 +188,11 @@ python-versions = ">=3.6" [[package]] name = "colorama" -version = "0.4.5" +version = "0.4.6" description = "Cross-platform colored terminal text." category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" [[package]] name = "colorlog" @@ -279,6 +279,17 @@ python-versions = "*" [package.dependencies] numpy = "*" +[[package]] +name = "exceptiongroup" +version = "1.0.0rc9" +description = "Backport of PEP 654 (exception groups)" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +test = ["pytest (>=6)"] + [[package]] name = "execnet" version = "1.9.0" @@ -371,7 +382,7 @@ nest-asyncio = "*" [[package]] name = "identify" -version = "2.5.6" +version = "2.5.7" description = "File identification library for Python" category = "dev" optional = false @@ -756,8 +767,8 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "py-cpuinfo" -version = "8.0.0" -description = "Get CPU info with pure Python 2 & 3" +version = "9.0.0" +description = "Get CPU info with pure Python" category = "dev" optional = false python-versions = "*" @@ -834,7 +845,7 @@ diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" -version = "7.1.3" +version = "7.2.0" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -843,11 +854,11 @@ python-versions = ">=3.7" [package.dependencies] attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=0.12,<2.0" -py = ">=1.8.2" -tomli = ">=1.0.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] @@ -868,11 +879,11 @@ testing = ["coverage (==6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy [[package]] name = "pytest-benchmark" -version = "3.4.1" +version = "4.0.0" description = "A ``pytest`` fixture for benchmarking code. It will group the tests into rounds that are calibrated to the chosen timer." category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" [package.dependencies] py-cpuinfo = "*" @@ -898,18 +909,6 @@ pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] -[[package]] -name = "pytest-forked" -version = "1.4.0" -description = "run tests in isolated forked subprocesses" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -py = "*" -pytest = ">=3.10" - [[package]] name = "pytest-mock" version = "3.10.0" @@ -926,7 +925,7 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] [[package]] name = "pytest-xdist" -version = "2.5.0" +version = "3.0.2" description = "pytest xdist plugin for distributed testing and loop-on-failing modes" category = "dev" optional = false @@ -936,7 +935,6 @@ python-versions = ">=3.6" execnet = ">=1.1" psutil = {version = ">=3.0", optional = true, markers = "extra == \"psutil\""} pytest = ">=6.2.0" -pytest-forked = "*" [package.extras] psutil = ["psutil (>=3.0)"] @@ -1394,19 +1392,19 @@ test = ["Cython (>=0.29.32,<0.30.0)", "aiohttp", "flake8 (>=3.9.2,<3.10.0)", "my [[package]] name = "virtualenv" -version = "20.16.5" +version = "20.16.6" description = "Virtual Python Environment builder" category = "dev" optional = false python-versions = ">=3.6" [package.dependencies] -distlib = ">=0.3.5,<1" +distlib = ">=0.3.6,<1" filelock = ">=3.4.1,<4" platformdirs = ">=2.4,<3" [package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [[package]] @@ -1459,7 +1457,7 @@ redis = ["hiredis", "redis"] [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "f7a471e0e6f7879cb311c41c335c67344bb14cf797ed0a2c2ac8dc6293118627" +content-hash = "9fd4d7884b9f3e6fca16bd7d40b67a554426d6ce832cfd1664a5b30b5402d187" [metadata.files] aiodns = [ @@ -1693,8 +1691,8 @@ cloudpickle = [ {file = "cloudpickle-2.2.0.tar.gz", hash = "sha256:3f4219469c55453cfe4737e564b67c2a149109dabf7f242478948b895f61106f"}, ] colorama = [ - {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, - {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] colorlog = [ {file = "colorlog-6.7.0-py2.py3-none-any.whl", hash = "sha256:0d33ca236784a1ba3ff9c532d4964126d8a2c44f1f0cb1d2b0728196f512f662"}, @@ -1814,6 +1812,10 @@ eventkit = [ {file = "eventkit-1.0.0-py3-none-any.whl", hash = "sha256:c3c1ae6e15cda9970c3996b0aaeda48431fc6b8674c01e7a7ff77a13629cc021"}, {file = "eventkit-1.0.0.tar.gz", hash = "sha256:c9c4bb8a9685e4131e845882512a630d6a57acee148f38af286562a76873e4a9"}, ] +exceptiongroup = [ + {file = "exceptiongroup-1.0.0rc9-py3-none-any.whl", hash = "sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337"}, + {file = "exceptiongroup-1.0.0rc9.tar.gz", hash = "sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96"}, +] execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, {file = "execnet-1.9.0.tar.gz", hash = "sha256:8f694f3ba9cc92cab508b152dcfe322153975c29bda272e2fd7f3f00f36e47c5"}, @@ -1954,8 +1956,8 @@ ib-insync = [ {file = "ib_insync-0.9.71.tar.gz", hash = "sha256:c1be16a8f23640ddd239f776bd025e5e0a298491b138f05da5b3391f6a6f9bf5"}, ] identify = [ - {file = "identify-2.5.6-py2.py3-none-any.whl", hash = "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa"}, - {file = "identify-2.5.6.tar.gz", hash = "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245"}, + {file = "identify-2.5.7-py2.py3-none-any.whl", hash = "sha256:7a67b2a6208d390fd86fd04fb3def94a3a8b7f0bcbd1d1fcd6736f4defe26390"}, + {file = "identify-2.5.7.tar.gz", hash = "sha256:5b8fd1e843a6d4bf10685dd31f4520a7f1c7d0e14e9bc5d34b1d6f111cabc011"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, @@ -2368,7 +2370,8 @@ py = [ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] py-cpuinfo = [ - {file = "py-cpuinfo-8.0.0.tar.gz", hash = "sha256:5f269be0e08e33fd959de96b34cd4aeeeacac014dd8305f70eb28d06de2345c5"}, + {file = "py-cpuinfo-9.0.0.tar.gz", hash = "sha256:3cdbbf3fac90dc6f118bfd64384f309edeadd902d7c8fb17f02ffa1fc3f49690"}, + {file = "py_cpuinfo-9.0.0-py3-none-any.whl", hash = "sha256:859625bc251f64e21f077d099d4162689c762b5d6a4c3c97553d56241c9674d5"}, ] pyarrow = [ {file = "pyarrow-8.0.0-cp310-cp310-macosx_10_13_universal2.whl", hash = "sha256:d5ef4372559b191cafe7db8932801eee252bfc35e983304e7d60b6954576a071"}, @@ -2504,8 +2507,8 @@ pyparsing = [ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pytest = [ - {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, - {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, + {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, + {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] pytest-asyncio = [ {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, @@ -2513,24 +2516,20 @@ pytest-asyncio = [ {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, ] pytest-benchmark = [ - {file = "pytest-benchmark-3.4.1.tar.gz", hash = "sha256:40e263f912de5a81d891619032983557d62a3d85843f9a9f30b98baea0cd7b47"}, - {file = "pytest_benchmark-3.4.1-py2.py3-none-any.whl", hash = "sha256:36d2b08c4882f6f997fd3126a3d6dfd70f3249cde178ed8bbc0b73db7c20f809"}, + {file = "pytest-benchmark-4.0.0.tar.gz", hash = "sha256:fb0785b83efe599a6a956361c0691ae1dbb5318018561af10f3e915caa0048d1"}, + {file = "pytest_benchmark-4.0.0-py3-none-any.whl", hash = "sha256:fdb7db64e31c8b277dff9850d2a2556d8b60bcb0ea6524e36e28ffd7c87f71d6"}, ] pytest-cov = [ {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, ] -pytest-forked = [ - {file = "pytest-forked-1.4.0.tar.gz", hash = "sha256:8b67587c8f98cbbadfdd804539ed5455b6ed03802203485dd2f53c1422d7440e"}, - {file = "pytest_forked-1.4.0-py3-none-any.whl", hash = "sha256:bbbb6717efc886b9d64537b41fb1497cfaf3c9601276be8da2cccfea5a3c8ad8"}, -] pytest-mock = [ {file = "pytest-mock-3.10.0.tar.gz", hash = "sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"}, {file = "pytest_mock-3.10.0-py3-none-any.whl", hash = "sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b"}, ] pytest-xdist = [ - {file = "pytest-xdist-2.5.0.tar.gz", hash = "sha256:4580deca3ff04ddb2ac53eba39d76cb5dd5edeac050cb6fbc768b0dd712b4edf"}, - {file = "pytest_xdist-2.5.0-py3-none-any.whl", hash = "sha256:6fe5c74fec98906deb8f2d2b616b5c782022744978e7bd4695d39c8f42d0ce65"}, + {file = "pytest-xdist-3.0.2.tar.gz", hash = "sha256:688da9b814370e891ba5de650c9327d1a9d861721a524eb917e620eec3e90291"}, + {file = "pytest_xdist-3.0.2-py3-none-any.whl", hash = "sha256:9feb9a18e1790696ea23e1434fa73b325ed4998b0e9fcb221f16fd1945e6df1b"}, ] python-dateutil = [ {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, @@ -2747,8 +2746,8 @@ uvloop = [ {file = "uvloop-0.17.0.tar.gz", hash = "sha256:0ddf6baf9cf11a1a22c71487f39f15b2cf78eb5bde7e5b45fbb99e8a9d91b9e1"}, ] virtualenv = [ - {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, - {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, + {file = "virtualenv-20.16.6-py3-none-any.whl", hash = "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108"}, + {file = "virtualenv-20.16.6.tar.gz", hash = "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e"}, ] wheel = [ {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"}, diff --git a/pyproject.toml b/pyproject.toml index 149363135059..516d1cfd63ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -78,12 +78,12 @@ nox = "^2022.8.7" numpydoc = "^1.5.0" mypy = "^0.982" pre-commit = "^2.20.0" -pytest = "^7.1.3" +pytest = "^7.2.0" pytest-asyncio = "^0.18.3" # Pinned at 0.18.x due breaking changes for 0.19.x -pytest-benchmark = "^3.4.1" +pytest-benchmark = "^4.0.0" pytest-cov = "4.0.0" pytest-mock = "^3.10.0" -pytest-xdist = { version = "^2.5.0", extras = ["psutil"] } +pytest-xdist = { version = "^3.0.2", extras = ["psutil"] } linkify-it-py = "^2.0.0" myst-parser = "^0.18.1" sphinx_comments = "^0.0.3" From 92f134767dde3fd0e38f62455c6ba8ebccc111c7 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 27 Oct 2022 17:53:46 +1100 Subject: [PATCH 04/38] Separate value object tests --- tests/unit_tests/model/test_model_objects.py | 1479 ----------------- .../model/test_model_objects_money.py | 216 +++ .../model/test_model_objects_price.py | 662 ++++++++ .../model/test_model_objects_quantity.py | 628 +++++++ 4 files changed, 1506 insertions(+), 1479 deletions(-) delete mode 100644 tests/unit_tests/model/test_model_objects.py create mode 100644 tests/unit_tests/model/test_model_objects_money.py create mode 100644 tests/unit_tests/model/test_model_objects_price.py create mode 100644 tests/unit_tests/model/test_model_objects_quantity.py diff --git a/tests/unit_tests/model/test_model_objects.py b/tests/unit_tests/model/test_model_objects.py deleted file mode 100644 index 68d42742b5bc..000000000000 --- a/tests/unit_tests/model/test_model_objects.py +++ /dev/null @@ -1,1479 +0,0 @@ -# ------------------------------------------------------------------------------------------------- -# Copyright (C) 2015-2022 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. -# ------------------------------------------------------------------------------------------------- - -import pickle -from decimal import Decimal - -import pytest - -from nautilus_trader.model.currencies import AUD -from nautilus_trader.model.currencies import USD -from nautilus_trader.model.identifiers import InstrumentId -from nautilus_trader.model.identifiers import Symbol -from nautilus_trader.model.identifiers import Venue -from nautilus_trader.model.objects import AccountBalance -from nautilus_trader.model.objects import MarginBalance -from nautilus_trader.model.objects import Money -from nautilus_trader.model.objects import Price -from nautilus_trader.model.objects import Quantity - - -class TestQuantity: - def test_instantiate_with_none_value_raises_type_error(self): - # Arrange, Act, Assert - with pytest.raises(TypeError): - Quantity(None) - - def test_instantiate_with_negative_precision_raises_overflow_error(self): - # Arrange, Act, Assert - with pytest.raises(OverflowError): - Quantity(1.0, precision=-1) - - def test_instantiate_with_precision_over_maximum_raises_value_error(self): - # Arrange, Act, Assert - with pytest.raises(ValueError): - Quantity(1.0, precision=10) - - def test_instantiate_with_value_exceeding_limit_raises_value_error(self): - # Arrange, Act, Assert - with pytest.raises(ValueError): - Quantity(18_446_744_073 + 1, precision=0) - - def test_instantiate_with_value_exceeding_positive_limit_raises_value_error(self): - # Arrange, Act, Assert - with pytest.raises(ValueError): - Price(9_223_372_036 + 1, precision=0) - - def test_instantiate_with_value_exceeding_negative_limit_raises_value_error(self): - # Arrange, Act, Assert - with pytest.raises(ValueError): - Price(-9_223_372_036 - 1, precision=0) - - def test_instantiate_base_decimal_from_int(self): - # Arrange, Act - result = Quantity(1, precision=1) - - # Assert - assert str(result) == "1.0" - - def test_instantiate_base_decimal_from_float(self): - # Arrange, Act - result = Quantity(1.12300, precision=5) - - # Assert - assert str(result) == "1.12300" - - def test_instantiate_base_decimal_from_decimal(self): - # Arrange, Act - result = Quantity(Decimal("1.23"), precision=1) - - # Assert - assert str(result) == "1.2" - - def test_instantiate_base_decimal_from_str(self): - # Arrange, Act - result = Quantity.from_str("1.23") - - # Assert - assert str(result) == "1.23" - - @pytest.mark.parametrize( - "value, precision, expected", - [ - [Quantity(2.15, precision=2), 0, Decimal("2")], - [Quantity(2.15, precision=2), 1, Decimal("2.2")], - [Quantity(2.255, precision=3), 2, Decimal("2.26")], - ], - ) - def test_round_with_various_digits_returns_expected_decimal(self, value, precision, expected): - # Arrange, Act - result = round(value, precision) - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value, expected", - [ - [Quantity(-0, precision=0), Decimal("0")], - [Quantity(0, precision=0), Decimal("0")], - [Quantity(1, precision=0), Decimal("1")], - ], - ) - def test_abs_with_various_values_returns_expected_decimal(self, value, expected): - # Arrange, Act - result = abs(value) - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value, expected", - [ - [Quantity(1, precision=0), Decimal("-1")], - [Quantity(0, precision=0), Decimal("0")], - ], - ) - def test_neg_with_various_values_returns_expected_decimal(self, value, expected): - # Arrange, Act - result = -value - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value, expected", - [ - [0, Quantity(0, precision=0)], - [1, Quantity(1, precision=0)], - [Decimal(0), Quantity(0, precision=0)], - [Decimal("1.1"), Quantity(1.1, precision=1)], - [Quantity(0, precision=0), Quantity(0, precision=0)], - [Quantity(1.1, precision=1), Quantity(1.1, precision=1)], - ], - ) - def test_instantiate_with_various_valid_inputs_returns_expected_decimal(self, value, expected): - # Arrange, Act - decimal_object = Quantity(value, 2) - - # Assert - assert decimal_object == expected - - @pytest.mark.parametrize( - "value, precision, expected", - [ - [0.0, 0, Quantity(0, precision=0)], - [1.0, 0, Quantity(1, precision=0)], - [1.123, 3, Quantity(1.123, precision=3)], - [1.155, 2, Quantity(1.16, precision=2)], - ], - ) - def test_instantiate_with_various_precisions_returns_expected_decimal( - self, value, precision, expected - ): - # Arrange, Act - decimal_object = Quantity(value, precision) - - # Assert - assert decimal_object == expected - assert decimal_object.precision == precision - - @pytest.mark.parametrize( - "value1, value2, expected", - [ - [0, -0, True], - [-0, 0, True], - [1, 1, True], - [1.1, 1.1, True], - [0, 1, False], - [1, 2, False], - [1.1, 1.12, False], - ], - ) - def test_equality_with_various_values_returns_expected_result(self, value1, value2, expected): - # Arrange, Act - result = Quantity(value1, 2) == Quantity(value2, 2) - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value1, value2, expected", - [ - [0, -0, True], - [-0, 0, True], - [1, 1, True], - [0, 1, False], - [1, 2, False], - ], - ) - def test_equality_with_various_int_returns_expected_result(self, value1, value2, expected): - # Arrange, Act - result1 = Quantity(value1, 0) == value2 - result2 = value2 == Quantity(value1, 0) - - # Assert - assert result1 == expected - assert result2 == expected - - @pytest.mark.parametrize( - "value1, value2, expected", - [ - [Quantity(0, precision=0), Decimal(0), True], - [Quantity(0, precision=0), Decimal(-0), True], - [Quantity(1, precision=0), Decimal(0), False], - ], - ) - def test_equality_with_various_decimals_returns_expected_result(self, value1, value2, expected): - # Arrange, Act - result = value1 == value2 - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value1, value2, expected1, expected2, expected3, expected4", - [ - [0, 0, False, True, True, False], - [1, 0, True, True, False, False], - ], - ) - def test_comparisons_with_various_values_returns_expected_result( - self, - value1, - value2, - expected1, - expected2, - expected3, - expected4, - ): - # Arrange, Act, Assert - assert (Quantity(value1, precision=0) > Quantity(value2, precision=0)) == expected1 - assert (Quantity(value1, precision=0) >= Quantity(value2, precision=0)) == expected2 - assert (Quantity(value1, precision=0) <= Quantity(value2, precision=0)) == expected3 - assert (Quantity(value1, precision=0) < Quantity(value2, precision=0)) == expected4 - - @pytest.mark.parametrize( - "value1, value2, expected_type, expected_value", - [ - [Quantity(0, precision=0), Quantity(0, precision=0), Decimal, 0], - [ - Quantity(0, precision=0), - Quantity(1.1, precision=1), - Decimal, - Decimal("1.1"), - ], - [Quantity(0, precision=0), 0, Decimal, 0], - [Quantity(0, precision=0), 1, Decimal, 1], - [0, Quantity(0, precision=0), Decimal, 0], - [1, Quantity(0, precision=0), Decimal, 1], - [Quantity(0, precision=0), 0.1, float, 0.1], - [Quantity(0, precision=0), 1.1, float, 1.1], - [-1.1, Quantity(0, precision=0), float, -1.1], - [1.1, Quantity(0, precision=0), float, 1.1], - [ - Quantity(1, precision=0), - Quantity(1.1, precision=1), - Decimal, - Decimal("2.1"), - ], - [Quantity(1, precision=0), Decimal("1.1"), Decimal, Decimal("2.1")], - ], - ) - def test_addition_with_various_types_returns_expected_result( - self, - value1, - value2, - expected_type, - expected_value, - ): - # Arrange, Act - result = value1 + value2 - - # Assert - assert isinstance(result, expected_type) - assert result == expected_value - - @pytest.mark.parametrize( - "value1, value2, expected_type, expected_value", - [ - [Quantity(0, precision=0), Quantity(0, precision=0), Decimal, 0], - [ - Quantity(0, precision=0), - Quantity(1.1, precision=1), - Decimal, - Decimal("-1.1"), - ], - [Quantity(0, precision=0), 0, Decimal, 0], - [Quantity(0, precision=0), 1, Decimal, -1], - [0, Quantity(0, precision=0), Decimal, 0], - [1, Quantity(1, precision=0), Decimal, 0], - [Quantity(0, precision=0), 0.1, float, -0.1], - [Quantity(0, precision=0), 1.1, float, -1.1], - [0.1, Quantity(1, precision=0), float, -0.9], - [1.1, Quantity(1, precision=0), float, 0.10000000000000009], - [ - Quantity(1, precision=0), - Quantity(1.1, precision=1), - Decimal, - Decimal("-0.1"), - ], - [Quantity(1, precision=0), Decimal("1.1"), Decimal, Decimal("-0.1")], - ], - ) - def test_subtraction_with_various_types_returns_expected_result( - self, - value1, - value2, - expected_type, - expected_value, - ): - # Arrange, Act - result = value1 - value2 - - # Assert - assert isinstance(result, expected_type) - assert result == expected_value - - @pytest.mark.parametrize( - "value1, value2, expected_type, expected_value", - [ - [Quantity(0, 0), 0, Decimal, 0], - [Quantity(1, 0), 1, Decimal, 1], - [1, Quantity(1, 0), Decimal, 1], - [2, Quantity(3, 0), Decimal, 6], - [Quantity(2, 0), 1.0, float, 2], - [1.1, Quantity(2, 0), float, 2.2], - [Quantity(1.1, 1), Quantity(1.1, 1), Decimal, Decimal("1.21")], - [Quantity(1.1, 1), Decimal("1.1"), Decimal, Decimal("1.21")], - ], - ) - def test_multiplication_with_various_types_returns_expected_result( - self, - value1, - value2, - expected_type, - expected_value, - ): - # Arrange, Act - result = value1 * value2 - - # Assert - assert isinstance(result, expected_type) - assert result == expected_value - - @pytest.mark.parametrize( - "value1, value2, expected_type, expected_value", - [ - [1, Quantity(1, 0), Decimal, 1], - [1.1, Quantity(1.1, 1), float, 1], - [Quantity(0, 0), 1, Decimal, 0], - [Quantity(1, 0), 2, Decimal, Decimal("0.5")], - [2, Quantity(1, 0), Decimal, Decimal("2.0")], - [Quantity(2, 0), 1.1, float, 1.8181818181818181], - [1.1, Quantity(2, 0), float, 1.1 / 2], - [ - Quantity(1.1, 1), - Quantity(1.2, 1), - Decimal, - Decimal("0.9166666666666666666666666667"), - ], - [ - Quantity(1.1, 1), - Decimal("1.2"), - Decimal, - Decimal("0.9166666666666666666666666667"), - ], - ], - ) - def test_division_with_various_types_returns_expected_result( - self, - value1, - value2, - expected_type, - expected_value, - ): - # Arrange, Act - result = value1 / value2 - - # Assert - assert expected_type == type(result) - assert expected_value == result - - @pytest.mark.parametrize( - "value1, value2, expected_type, expected_value", - [ - [1, Quantity(1, 0), Decimal, 1], - [Quantity(0, 0), 1, Decimal, 0], - [Quantity(1, 0), 2, Decimal, Decimal(0)], - [2, Quantity(1, 0), Decimal, Decimal(2)], - [2.1, Quantity(1.1, 1), float, 1], - [4.4, Quantity(1.1, 1), float, 4], - [Quantity(2.1, 1), 1.1, float, 1], - [Quantity(4.4, 1), 1.1, float, 4], - [Quantity(1.1, 1), Quantity(1.2, 1), Decimal, Decimal(0)], - [Quantity(1.1, 1), Decimal("1.2"), Decimal, Decimal(0)], - ], - ) - def test_floor_division_with_various_types_returns_expected_result( - self, - value1, - value2, - expected_type, - expected_value, - ): - # Arrange, Act - result = value1 // value2 - - # Assert - assert expected_type == type(result) - assert expected_value == result - - @pytest.mark.parametrize( - "value1, value2, expected_type, expected_value", - [ - [Quantity(100, 0), 10, Decimal, 0], - [Quantity(23, 0), 2, Decimal, 1], - [2.1, Quantity(1.1, 1), float, 1.0], - [1.1, Quantity(2.1, 1), float, 1.1], - [Quantity(2.1, 1), 1.1, float, 1.0], - [Quantity(1.1, 1), 2.1, float, 1.1], - [Quantity(1.1, 1), Decimal("0.2"), Decimal, Decimal("0.1")], - ], - ) - def test_mod_with_various_types_returns_expected_result( - self, - value1, - value2, - expected_type, - expected_value, - ): - # Arrange, Act - result = value1 % value2 # noqa (not modulo formatting) - - # Assert - assert expected_type == type(result) - assert expected_value == result - - @pytest.mark.parametrize( - "value1, value2, expected", - [ - [Quantity(1, 0), Quantity(2, 0), Quantity(2, 0)], - [Quantity(1, 0), 2, 2], - [Quantity(1, 0), Decimal(2), Decimal(2)], - ], - ) - def test_max_with_various_types_returns_expected_result( - self, - value1, - value2, - expected, - ): - # Arrange, Act - result = max(value1, value2) - - # Assert - assert expected == result - - @pytest.mark.parametrize( - "value1, value2, expected", - [ - [Quantity(1, 0), Quantity(2, 0), Quantity(1, 0)], - [Quantity(1, 0), 2, Quantity(1, 0)], - [Quantity(2, 0), Decimal(1), Decimal(1)], - ], - ) - def test_min_with_various_types_returns_expected_result( - self, - value1, - value2, - expected, - ): - # Arrange, Act - result = min(value1, value2) - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value, expected", - [["1", 1], ["1.1", 1]], - ) - def test_int(self, value, expected): - # Arrange - decimal1 = Quantity.from_str(value) - - # Act, Assert - assert int(decimal1) == expected - - def test_hash(self): - # Arrange - decimal1 = Quantity(1.1, 1) - decimal2 = Quantity(1.1, 1) - - # Act, Assert - assert isinstance(hash(decimal2), int) - assert hash(decimal1) == hash(decimal2) - - @pytest.mark.parametrize( - "value, precision, expected", - [ - [0, 0, "0"], - [-0, 0, "0"], - [1, 0, "1"], - [1.1, 1, "1.1"], - ], - ) - def test_str_with_various_values_returns_expected_string( - self, - value, - precision, - expected, - ): - # Arrange, Act - decimal_object = Quantity(value, precision=precision) - - # Assert - assert str(decimal_object) == expected - - def test_repr(self): - # Arrange, Act - result = repr(Quantity(1.1, 1)) - - # Assert - assert "Quantity('1.1')" == result - - @pytest.mark.parametrize( - "value, precision, expected", - [ - [0, 0, Quantity(0, 0)], - [-0, 0, Quantity(0, 0)], - [1, 0, Quantity(1, 0)], - [1.1, 1, Quantity(1.1, 1)], - ], - ) - def test_as_decimal_with_various_values_returns_expected_value( - self, - value, - precision, - expected, - ): - # Arrange, Act - result = Quantity(value, precision=precision) - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value, expected", - [[0, 0], [-0, 0], [1, 1], [1.1, 1.1]], - ) - def test_as_double_with_various_values_returns_expected_value(self, value, expected): - # Arrange, Act - result = Quantity(value, 1).as_double() - - # Assert - assert result == expected - - def test_calling_new_returns_an_expected_zero_quantity(self): - # Arrange, Act - new_qty = Quantity.__new__(Quantity, 1, 1) - - # Assert - assert new_qty == 0 - - def test_from_raw_returns_expected_quantity(self): - # Arrange, Act - qty1 = Quantity.from_raw(1000000000000, 3) - qty2 = Quantity(1000, 3) - - # Assert - assert qty1 == qty2 - assert str(qty1) == "1000.000" - assert qty1.precision == 3 - - def test_zero_returns_zero_quantity(self): - # Arrange, Act - qty = Quantity.zero() - - # Assert - assert qty == 0 - assert str(qty) == "0" - assert qty.precision == 0 - - def test_from_int_returns_expected_value(self): - # Arrange, Act - qty = Quantity.from_int(1000) - - # Assert - assert qty == 1000 - assert str(qty) == "1000" - assert qty.precision == 0 - - def test_from_str_returns_expected_value(self): - # Arrange, Act - qty = Quantity.from_str("0.511") - - # Assert - assert qty == Quantity(0.511, precision=3) - assert str(qty) == "0.511" - assert qty.precision == 3 - - @pytest.mark.parametrize( - "value, expected", - [ - ["0", "0"], - ["10.05", "10.05"], - ["1000", "1_000"], - ["1112", "1_112"], - ["120100", "120_100"], - ["200000", "200_000"], - ["1000000", "1_000_000"], - ["2500000", "2_500_000"], - ["1111111", "1_111_111"], - ["2523000", "2_523_000"], - ["100000000", "100_000_000"], - ], - ) - def test_str_and_to_str(self, value, expected): - # Arrange, Act, Assert - assert Quantity.from_str(value).to_str() == expected - - def test_str_repr(self): - # Arrange - quantity = Quantity(2100.1666666, 6) - - # Act, Assert - assert "2100.166667" == str(quantity) - assert "Quantity('2100.166667')" == repr(quantity) - - def test_pickle_dumps_and_loads(self): - # Arrange - quantity = Quantity(1.2000, 2) - - # Act - pickled = pickle.dumps(quantity) - - # Assert - assert pickle.loads(pickled) == quantity # noqa (testing pickle) - - -class TestPrice: - def test_instantiate_with_none_value_raises_type_error(self): - # Arrange, Act, Assert - with pytest.raises(TypeError): - Price(None) - - def test_instantiate_with_negative_precision_raises_overflow_error(self): - # Arrange, Act, Assert - with pytest.raises(OverflowError): - Price(1.0, precision=-1) - - def test_instantiate_with_precision_over_maximum_raises_overflow_error(self): - # Arrange, Act, Assert - with pytest.raises(ValueError): - Price(1.0, precision=10) - - def test_instantiate_with_value_exceeding_positive_limit_raises_value_error(self): - # Arrange, Act, Assert - with pytest.raises(ValueError): - Price(9_223_372_036 + 1, precision=0) - - def test_instantiate_with_value_exceeding_negative_limit_raises_value_error(self): - # Arrange, Act, Assert - with pytest.raises(ValueError): - Price(-9_223_372_036 - 1, precision=0) - - def test_instantiate_base_decimal_from_int(self): - # Arrange, Act - result = Price(1, precision=1) - - # Assert - assert str(result) == "1.0" - - def test_instantiate_base_decimal_from_float(self): - # Arrange, Act - result = Price(1.12300, precision=5) - - # Assert - assert str(result) == "1.12300" - - def test_instantiate_base_decimal_from_decimal(self): - # Arrange, Act - result = Price(Decimal("1.23"), precision=1) - - # Assert - assert str(result) == "1.2" - - def test_instantiate_base_decimal_from_str(self): - # Arrange, Act - result = Price.from_str("1.23") - - # Assert - assert str(result) == "1.23" - - @pytest.mark.parametrize( - "value, precision, expected", - [ - [Price(2.15, precision=2), 0, Decimal("2")], - [Price(2.15, precision=2), 1, Decimal("2.2")], - [Price(2.255, precision=3), 2, Decimal("2.26")], - ], - ) - def test_round_with_various_digits_returns_expected_decimal(self, value, precision, expected): - # Arrange, Act - result = round(value, precision) - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value, expected", - [ - [Price(-0, precision=0), Decimal("0")], - [Price(0, precision=0), Decimal("0")], - [Price(1, precision=0), Decimal("1")], - [Price(-1, precision=0), Decimal("1")], - [Price(-1.1, precision=1), Decimal("1.1")], - ], - ) - def test_abs_with_various_values_returns_expected_decimal(self, value, expected): - # Arrange, Act - result = abs(value) - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value, expected", - [ - [ - Price(-1, precision=0), - Decimal("-1"), - ], # Matches built-in decimal.Decimal behaviour - [Price(0, 0), Decimal("0")], - ], - ) - def test_pos_with_various_values_returns_expected_decimal(self, value, expected): - # Arrange, Act - result = +value - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value, expected", - [ - [Price(1, precision=0), Decimal("-1")], - [Price(0, precision=0), Decimal("0")], - ], - ) - def test_neg_with_various_values_returns_expected_decimal(self, value, expected): - # Arrange, Act - result = -value - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value, expected", - [ - [0, Price(0, precision=0)], - [1, Price(1, precision=0)], - [-1, Price(-1, precision=0)], - [Decimal(0), Price(0, precision=0)], - [Decimal("1.1"), Price(1.1, precision=1)], - [Decimal("-1.1"), Price(-1.1, precision=1)], - [Price(0, precision=0), Price(0, precision=0)], - [Price(1.1, precision=1), Price(1.1, precision=1)], - [Price(-1.1, precision=1), Price(-1.1, precision=1)], - ], - ) - def test_instantiate_with_various_valid_inputs_returns_expected_decimal(self, value, expected): - # Arrange, Act - decimal_object = Price(value, 2) - - # Assert - assert decimal_object == expected - - @pytest.mark.parametrize( - "value, precision, expected", - [ - [0.0, 0, Price(0, precision=0)], - [1.0, 0, Price(1, precision=0)], - [-1.0, 0, Price(-1, precision=0)], - [1.123, 3, Price(1.123, precision=3)], - [-1.123, 3, Price(-1.123, precision=3)], - [1.155, 2, Price(1.16, precision=2)], - ], - ) - def test_instantiate_with_various_precisions_returns_expected_decimal( - self, value, precision, expected - ): - # Arrange, Act - decimal_object = Price(value, precision) - - # Assert - assert decimal_object == expected - assert decimal_object.precision == precision - - @pytest.mark.parametrize( - "value1, value2, expected", - [ - [0, -0, True], - [-0, 0, True], - [-1, -1, True], - [1, 1, True], - [1.1, 1.1, True], - [-1.1, -1.1, True], - [0, 1, False], - [-1, 0, False], - [-1, -2, False], - [1, 2, False], - [1.1, 1.12, False], - [-1.12, -1.1, False], - ], - ) - def test_equality_with_various_values_returns_expected_result(self, value1, value2, expected): - # Arrange, Act - result = Price(value1, 2) == Price(value2, 2) - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value1, value2, expected", - [ - [0, -0, True], - [-0, 0, True], - [-1, -1, True], - [1, 1, True], - [0, 1, False], - [-1, 0, False], - [-1, -2, False], - [1, 2, False], - ], - ) - def test_equality_with_various_int_returns_expected_result(self, value1, value2, expected): - # Arrange, Act - result1 = Price(value1, 0) == value2 - result2 = value2 == Price(value1, 0) - - # Assert - assert result1 == expected - assert result2 == expected - - @pytest.mark.parametrize( - "value1, value2, expected", - [ - [Price(0, precision=0), Decimal(0), True], - [Price(0, precision=0), Decimal(-0), True], - [Price(1, precision=0), Decimal(0), False], - ], - ) - def test_equality_with_various_decimals_returns_expected_result(self, value1, value2, expected): - # Arrange, Act - result = value1 == value2 - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value1, value2, expected1, expected2, expected3, expected4", - [ - [0, 0, False, True, True, False], - [1, 0, True, True, False, False], - [-1, 0, False, False, True, True], - ], - ) - def test_comparisons_with_various_values_returns_expected_result( - self, - value1, - value2, - expected1, - expected2, - expected3, - expected4, - ): - # Arrange, Act, Assert - assert (Price(value1, precision=0) > Price(value2, precision=0)) == expected1 - assert (Price(value1, precision=0) >= Price(value2, precision=0)) == expected2 - assert (Price(value1, precision=0) <= Price(value2, precision=0)) == expected3 - assert (Price(value1, precision=0) < Price(value2, precision=0)) == expected4 - - @pytest.mark.parametrize( - "value1, value2, expected_type, expected_value", - [ - [Price(0, precision=0), Price(0, precision=0), Decimal, 0], - [ - Price(0, precision=0), - Price(1.1, precision=1), - Decimal, - Decimal("1.1"), - ], - [Price(0, precision=0), 0, Decimal, 0], - [Price(0, precision=0), 1, Decimal, 1], - [0, Price(0, precision=0), Decimal, 0], - [1, Price(0, precision=0), Decimal, 1], - [Price(0, precision=0), 0.1, float, 0.1], - [Price(0, precision=0), 1.1, float, 1.1], - [-1.1, Price(0, precision=0), float, -1.1], - [1.1, Price(0, precision=0), float, 1.1], - [ - Price(1, precision=0), - Price(1.1, precision=1), - Decimal, - Decimal("2.1"), - ], - [Price(1, precision=0), Decimal("1.1"), Decimal, Decimal("2.1")], - ], - ) - def test_addition_with_various_types_returns_expected_result( - self, - value1, - value2, - expected_type, - expected_value, - ): - # Arrange, Act - result = value1 + value2 - - # Assert - assert isinstance(result, expected_type) - assert result == expected_value - - @pytest.mark.parametrize( - "value1, value2, expected_type, expected_value", - [ - [Price(0, precision=0), Price(0, precision=0), Decimal, 0], - [ - Price(0, precision=0), - Price(1.1, precision=1), - Decimal, - Decimal("-1.1"), - ], - [Price(0, precision=0), 0, Decimal, 0], - [Price(0, precision=0), 1, Decimal, -1], - [0, Price(0, precision=0), Decimal, 0], - [1, Price(1, precision=0), Decimal, 0], - [Price(0, precision=0), 0.1, float, -0.1], - [Price(0, precision=0), 1.1, float, -1.1], - [0.1, Price(1, precision=0), float, -0.9], - [1.1, Price(1, precision=0), float, 0.10000000000000009], - [ - Price(1, precision=0), - Price(1.1, precision=1), - Decimal, - Decimal("-0.1"), - ], - [Price(1, precision=0), Decimal("1.1"), Decimal, Decimal("-0.1")], - ], - ) - def test_subtraction_with_various_types_returns_expected_result( - self, - value1, - value2, - expected_type, - expected_value, - ): - # Arrange, Act - result = value1 - value2 - - # Assert - assert isinstance(result, expected_type) - assert result == expected_value - - @pytest.mark.parametrize( - "value1, value2, expected_type, expected_value", - [ - [Price(0, 0), 0, Decimal, 0], - [Price(1, 0), 1, Decimal, 1], - [1, Price(1, 0), Decimal, 1], - [2, Price(3, 0), Decimal, 6], - [Price(2, 0), 1.0, float, 2], - [1.1, Price(2, 0), float, 2.2], - [Price(1.1, 1), Price(1.1, 1), Decimal, Decimal("1.21")], - [Price(1.1, 1), Decimal("1.1"), Decimal, Decimal("1.21")], - ], - ) - def test_multiplication_with_various_types_returns_expected_result( - self, - value1, - value2, - expected_type, - expected_value, - ): - # Arrange, Act - result = value1 * value2 - - # Assert - assert isinstance(result, expected_type) - assert result == expected_value - - @pytest.mark.parametrize( - "value1, value2, expected_type, expected_value", - [ - [1, Price(1, 0), Decimal, 1], - [1.1, Price(1.1, 1), float, 1], - [Price(0, 0), 1, Decimal, 0], - [Price(1, 0), 2, Decimal, Decimal("0.5")], - [2, Price(1, 0), Decimal, Decimal("2.0")], - [Price(2, 0), 1.1, float, 1.8181818181818181], - [1.1, Price(2, 0), float, 1.1 / 2], - [ - Price(1.1, 1), - Price(1.2, 1), - Decimal, - Decimal("0.9166666666666666666666666667"), - ], - [ - Price(1.1, 1), - Decimal("1.2"), - Decimal, - Decimal("0.9166666666666666666666666667"), - ], - ], - ) - def test_division_with_various_types_returns_expected_result( - self, - value1, - value2, - expected_type, - expected_value, - ): - # Arrange, Act - result = value1 / value2 - - # Assert - assert expected_type == type(result) - assert expected_value == result - - @pytest.mark.parametrize( - "value1, value2, expected_type, expected_value", - [ - [1, Price(1, 0), Decimal, 1], - [Price(0, 0), 1, Decimal, 0], - [Price(1, 0), 2, Decimal, Decimal(0)], - [2, Price(1, 0), Decimal, Decimal(2)], - [2.1, Price(1.1, 1), float, 1], - [4.4, Price(1.1, 1), float, 4], - [Price(2.1, 1), 1.1, float, 1], - [Price(4.4, 1), 1.1, float, 4], - [Price(1.1, 1), Price(1.2, 1), Decimal, Decimal(0)], - [Price(1.1, 1), Decimal("1.2"), Decimal, Decimal(0)], - ], - ) - def test_floor_division_with_various_types_returns_expected_result( - self, - value1, - value2, - expected_type, - expected_value, - ): - # Arrange, Act - result = value1 // value2 - - # Assert - assert expected_type == type(result) - assert expected_value == result - - @pytest.mark.parametrize( - "value1, value2, expected_type, expected_value", - [ - [1, Price(1, 0), Decimal, 1], - [Price(100, 0), 10, Decimal, 0], - [Price(23, 0), 2, Decimal, 1], - [2.1, Price(1.1, 1), float, 1.0], - [1.1, Price(2.1, 1), float, 1.1], - [Price(2.1, 1), 1.1, float, 1.0], - [Price(1.1, 1), 2.1, float, 1.1], - [Price(1.1, 1), Price(0.2, 1), Decimal, Decimal("0.1")], - ], - ) - def test_mod_with_various_types_returns_expected_result( - self, - value1, - value2, - expected_type, - expected_value, - ): - # Arrange, Act - result = value1 % value2 # noqa (not modulo formatting) - - # Assert - assert expected_type == type(result) - assert expected_value == result - - @pytest.mark.parametrize( - "value1, value2, expected", - [ - [Price(1, 0), Price(2, 0), Price(2, 0)], - [Price(1, 0), 2, 2], - [Price(1, 0), Decimal(2), Decimal(2)], - ], - ) - def test_max_with_various_types_returns_expected_result( - self, - value1, - value2, - expected, - ): - # Arrange, Act - result = max(value1, value2) - - # Assert - assert expected == result - - @pytest.mark.parametrize( - "value1, value2, expected", - [ - [Price(1, 0), Price(2, 0), Price(1, 0)], - [Price(1, 0), 2, Price(1, 0)], - [Price(2, 0), Decimal(1), Decimal(1)], - ], - ) - def test_min_with_various_types_returns_expected_result( - self, - value1, - value2, - expected, - ): - # Arrange, Act - result = min(value1, value2) - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value, expected", - [["1", 1], ["1.1", 1]], - ) - def test_int(self, value, expected): - # Arrange - decimal1 = Price.from_str(value) - - # Act, Assert - assert int(decimal1) == expected - - def test_hash(self): - # Arrange - decimal1 = Price(1.1, 1) - decimal2 = Price(1.1, 1) - - # Act, Assert - assert isinstance(hash(decimal2), int) - assert hash(decimal1) == hash(decimal2) - - @pytest.mark.parametrize( - "value, precision, expected", - [ - [0, 0, "0"], - [-0, 0, "0"], - [-1, 0, "-1"], - [1, 0, "1"], - [1.1, 1, "1.1"], - [-1.1, 1, "-1.1"], - ], - ) - def test_str_with_various_values_returns_expected_string( - self, - value, - precision, - expected, - ): - # Arrange, Act - decimal_object = Price(value, precision=precision) - - # Assert - assert str(decimal_object) == expected - - def test_repr(self): - # Arrange, Act - result = repr(Price(1.1, 1)) - - # Assert - assert "Price('1.1')" == result - - @pytest.mark.parametrize( - "value, precision, expected", - [ - [0, 0, Price(0, 0)], - [-0, 0, Price(0, 0)], - [-1, 0, Price(-1, 0)], - [1, 0, Price(1, 0)], - [1.1, 1, Price(1.1, 1)], - [-1.1, 1, Price(-1.1, 1)], - ], - ) - def test_as_decimal_with_various_values_returns_expected_value( - self, - value, - precision, - expected, - ): - # Arrange, Act - result = Price(value, precision=precision) - - # Assert - assert result == expected - - @pytest.mark.parametrize( - "value, expected", - [[0, 0], [-0, 0], [-1, -1], [1, 1], [1.1, 1.1], [-1.1, -1.1]], - ) - def test_as_double_with_various_values_returns_expected_value(self, value, expected): - # Arrange, Act - result = Price(value, 1).as_double() - - # Assert - assert result == expected - - def test_calling_new_returns_an_expected_zero_price(self): - # Arrange, Act - new_price = Price.__new__(Price, 1, 1) - - # Assert - assert new_price == 0 - - def test_from_raw_returns_expected_price(self): - # Arrange, Act - price1 = Price.from_raw(1000000000000, 3) - price2 = Price(1000, 3) - - # Assert - assert price1 == price2 - assert str(price1) == "1000.000" - assert price1.precision == 3 - - def test_equality(self): - # Arrange, Act - price1 = Price(1.0, precision=1) - price2 = Price(1.5, precision=1) - - # Assert - assert price1 == price1 - assert price1 != price2 - assert price2 > price1 - - def test_from_int_returns_expected_value(self): - # Arrange, Act - price = Price.from_int(100) - - # Assert - assert str(price) == "100" - assert price.precision == 0 - - @pytest.mark.parametrize( - "value, string, precision", - [ - ["100.11", "100.11", 2], - ["1E7", "10000000", 0], - ["1E-7", "0.0000001", 7], - ["1e-2", "0.01", 2], - ], - ) - def test_from_str_returns_expected_value(self, value, string, precision): - # Arrange, Act - price = Price.from_str(value) - - # Assert - assert str(price) == string - assert price.precision == precision - - def test_str_repr(self): - # Arrange, Act - price = Price(1.00000, precision=5) - - # Assert - assert "1.00000" == str(price) - assert "Price('1.00000')" == repr(price) - - def test_pickle_dumps_and_loads(self): - # Arrange - price = Price(1.2000, 2) - - # Act - pickled = pickle.dumps(price) - - # Assert - assert pickle.loads(pickled) == price # noqa (testing pickle) - - -class TestMoney: - def test_instantiate_with_none_currency_raises_type_error(self): - # Arrange, Act, Assert - with pytest.raises(TypeError): - Money(1.0, None) - - def test_instantiate_with_value_exceeding_positive_limit_raises_value_error(self): - # Arrange, Act, Assert - with pytest.raises(ValueError): - Money(9_223_372_036 + 1, currency=USD) - - def test_instantiate_with_value_exceeding_negative_limit_raises_value_error(self): - # Arrange, Act, Assert - with pytest.raises(ValueError): - Money(-9_223_372_036 - 1, currency=USD) - - def test_instantiate_with_none_value_returns_money_with_zero_amount(self): - # Arrange, Act - money_zero = Money(None, currency=USD) - - # Assert - assert 0 == money_zero.as_decimal() - - @pytest.mark.parametrize( - "value, expected", - [ - [0, Money(0, USD)], - [1, Money(1, USD)], - [-1, Money(-1, USD)], - ["0", Money(0, USD)], - ["0.0", Money(0, USD)], - ["-0.0", Money(0, USD)], - ["1.0", Money(1, USD)], - ["-1.0", Money(-1, USD)], - [Decimal(0), Money(0, USD)], - [Decimal("1.1"), Money(1.1, USD)], - [Decimal("-1.1"), Money(-1.1, USD)], - ], - ) - def test_instantiate_with_various_valid_inputs_returns_expected_money(self, value, expected): - # Arrange, Act - money = Money(value, USD) - - # Assert - assert money == expected - - def test_pickling(self): - # Arrange - money = Money(1, USD) - - # Act - pickled = pickle.dumps(money) - unpickled = pickle.loads(pickled) # noqa S301 (pickle is safe here) - - # Assert - assert unpickled == money - - def test_as_double_returns_expected_result(self): - # Arrange, Act - money = Money(1, USD) - - # Assert - assert 1.0 == money.as_double() - assert "1.00" == str(money) - - def test_initialized_with_many_decimals_rounds_to_currency_precision(self): - # Arrange, Act - result1 = Money(1000.333, USD) - result2 = Money(5005.556666, USD) - - # Assert - assert "1_000.33 USD" == result1.to_str() - assert "5_005.56 USD" == result2.to_str() - - def test_hash(self): - # Arrange - money0 = Money(0, USD) - - # Act, Assert - assert isinstance(hash(money0), int) - assert hash(money0) == hash(money0) - - def test_str(self): - # Arrange - money0 = Money(0, USD) - money1 = Money(1, USD) - money2 = Money(1_000_000, USD) - - # Act, Assert - assert "0.00" == str(money0) - assert "1.00" == str(money1) - assert "1000000.00" == str(money2) - assert "1_000_000.00 USD" == money2.to_str() - - def test_repr(self): - # Arrange - money = Money(1.00, USD) - - # Act - result = repr(money) - - # Assert - assert "Money('1.00', USD)" == result - - def test_from_str_when_malformed_raises_value_error(self): - # Arrange - value = "@" - - # Act, Assert - with pytest.raises(ValueError): - Money.from_str(value) - - @pytest.mark.parametrize( - "value, expected", - [ - ["1.00 USD", Money(1.00, USD)], - ["1.001 AUD", Money(1.00, AUD)], - ], - ) - def test_from_str_given_valid_strings_returns_expected_result( - self, - value, - expected, - ): - # Arrange, Act - result = Money.from_str(value) - - # Assert - assert result == expected - - -class TestAccountBalance: - def test_instantiate_str_repr(self): - # Arrange, Act - balance = AccountBalance( - total=Money(1_525_000, USD), - locked=Money(25_000, USD), - free=Money(1_500_000, USD), - ) - - # Assert - assert ( - str(balance) - == "AccountBalance(total=1_525_000.00 USD, locked=25_000.00 USD, free=1_500_000.00 USD)" - ) - assert ( - repr(balance) - == "AccountBalance(total=1_525_000.00 USD, locked=25_000.00 USD, free=1_500_000.00 USD)" - ) - - -class TestMarginBalance: - def test_instantiate_str_repr_with_instrument_id(self): - # Arrange, Act - margin = MarginBalance( - initial=Money(5_000, USD), - maintenance=Money(25_000, USD), - instrument_id=InstrumentId(Symbol("AUD/USD"), Venue("IDEALPRO")), - ) - - # Assert - assert ( - str(margin) - == "MarginBalance(initial=5_000.00 USD, maintenance=25_000.00 USD, instrument_id=AUD/USD.IDEALPRO)" - ) - assert ( - repr(margin) - == "MarginBalance(initial=5_000.00 USD, maintenance=25_000.00 USD, instrument_id=AUD/USD.IDEALPRO)" - ) - - def test_instantiate_str_repr_without_instrument_id(self): - # Arrange, Act - margin = MarginBalance( - initial=Money(5_000, USD), - maintenance=Money(25_000, USD), - ) - - # Assert - assert ( - str(margin) - == "MarginBalance(initial=5_000.00 USD, maintenance=25_000.00 USD, instrument_id=None)" - ) - assert ( - repr(margin) - == "MarginBalance(initial=5_000.00 USD, maintenance=25_000.00 USD, instrument_id=None)" - ) diff --git a/tests/unit_tests/model/test_model_objects_money.py b/tests/unit_tests/model/test_model_objects_money.py new file mode 100644 index 000000000000..d29d3be9e3d2 --- /dev/null +++ b/tests/unit_tests/model/test_model_objects_money.py @@ -0,0 +1,216 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2022 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. +# ------------------------------------------------------------------------------------------------- + +import pickle +from decimal import Decimal + +import pytest + +from nautilus_trader.model.currencies import AUD +from nautilus_trader.model.currencies import USD +from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.identifiers import Symbol +from nautilus_trader.model.identifiers import Venue +from nautilus_trader.model.objects import AccountBalance +from nautilus_trader.model.objects import MarginBalance +from nautilus_trader.model.objects import Money + + +class TestMoney: + def test_instantiate_with_none_currency_raises_type_error(self): + # Arrange, Act, Assert + with pytest.raises(TypeError): + Money(1.0, None) + + def test_instantiate_with_value_exceeding_positive_limit_raises_value_error(self): + # Arrange, Act, Assert + with pytest.raises(ValueError): + Money(9_223_372_036 + 1, currency=USD) + + def test_instantiate_with_value_exceeding_negative_limit_raises_value_error(self): + # Arrange, Act, Assert + with pytest.raises(ValueError): + Money(-9_223_372_036 - 1, currency=USD) + + def test_instantiate_with_none_value_returns_money_with_zero_amount(self): + # Arrange, Act + money_zero = Money(None, currency=USD) + + # Assert + assert 0 == money_zero.as_decimal() + + @pytest.mark.parametrize( + "value, expected", + [ + [0, Money(0, USD)], + [1, Money(1, USD)], + [-1, Money(-1, USD)], + ["0", Money(0, USD)], + ["0.0", Money(0, USD)], + ["-0.0", Money(0, USD)], + ["1.0", Money(1, USD)], + ["-1.0", Money(-1, USD)], + [Decimal(0), Money(0, USD)], + [Decimal("1.1"), Money(1.1, USD)], + [Decimal("-1.1"), Money(-1.1, USD)], + ], + ) + def test_instantiate_with_various_valid_inputs_returns_expected_money(self, value, expected): + # Arrange, Act + money = Money(value, USD) + + # Assert + assert money == expected + + def test_pickling(self): + # Arrange + money = Money(1, USD) + + # Act + pickled = pickle.dumps(money) + unpickled = pickle.loads(pickled) # noqa S301 (pickle is safe here) + + # Assert + assert unpickled == money + + def test_as_double_returns_expected_result(self): + # Arrange, Act + money = Money(1, USD) + + # Assert + assert 1.0 == money.as_double() + assert "1.00" == str(money) + + def test_initialized_with_many_decimals_rounds_to_currency_precision(self): + # Arrange, Act + result1 = Money(1000.333, USD) + result2 = Money(5005.556666, USD) + + # Assert + assert "1_000.33 USD" == result1.to_str() + assert "5_005.56 USD" == result2.to_str() + + def test_hash(self): + # Arrange + money0 = Money(0, USD) + + # Act, Assert + assert isinstance(hash(money0), int) + assert hash(money0) == hash(money0) + + def test_str(self): + # Arrange + money0 = Money(0, USD) + money1 = Money(1, USD) + money2 = Money(1_000_000, USD) + + # Act, Assert + assert "0.00" == str(money0) + assert "1.00" == str(money1) + assert "1000000.00" == str(money2) + assert "1_000_000.00 USD" == money2.to_str() + + def test_repr(self): + # Arrange + money = Money(1.00, USD) + + # Act + result = repr(money) + + # Assert + assert "Money('1.00', USD)" == result + + def test_from_str_when_malformed_raises_value_error(self): + # Arrange + value = "@" + + # Act, Assert + with pytest.raises(ValueError): + Money.from_str(value) + + @pytest.mark.parametrize( + "value, expected", + [ + ["1.00 USD", Money(1.00, USD)], + ["1.001 AUD", Money(1.00, AUD)], + ], + ) + def test_from_str_given_valid_strings_returns_expected_result( + self, + value, + expected, + ): + # Arrange, Act + result = Money.from_str(value) + + # Assert + assert result == expected + + +class TestAccountBalance: + def test_instantiate_str_repr(self): + # Arrange, Act + balance = AccountBalance( + total=Money(1_525_000, USD), + locked=Money(25_000, USD), + free=Money(1_500_000, USD), + ) + + # Assert + assert ( + str(balance) + == "AccountBalance(total=1_525_000.00 USD, locked=25_000.00 USD, free=1_500_000.00 USD)" + ) + assert ( + repr(balance) + == "AccountBalance(total=1_525_000.00 USD, locked=25_000.00 USD, free=1_500_000.00 USD)" + ) + + +class TestMarginBalance: + def test_instantiate_str_repr_with_instrument_id(self): + # Arrange, Act + margin = MarginBalance( + initial=Money(5_000, USD), + maintenance=Money(25_000, USD), + instrument_id=InstrumentId(Symbol("AUD/USD"), Venue("IDEALPRO")), + ) + + # Assert + assert ( + str(margin) + == "MarginBalance(initial=5_000.00 USD, maintenance=25_000.00 USD, instrument_id=AUD/USD.IDEALPRO)" + ) + assert ( + repr(margin) + == "MarginBalance(initial=5_000.00 USD, maintenance=25_000.00 USD, instrument_id=AUD/USD.IDEALPRO)" + ) + + def test_instantiate_str_repr_without_instrument_id(self): + # Arrange, Act + margin = MarginBalance( + initial=Money(5_000, USD), + maintenance=Money(25_000, USD), + ) + + # Assert + assert ( + str(margin) + == "MarginBalance(initial=5_000.00 USD, maintenance=25_000.00 USD, instrument_id=None)" + ) + assert ( + repr(margin) + == "MarginBalance(initial=5_000.00 USD, maintenance=25_000.00 USD, instrument_id=None)" + ) diff --git a/tests/unit_tests/model/test_model_objects_price.py b/tests/unit_tests/model/test_model_objects_price.py new file mode 100644 index 000000000000..657e12573baa --- /dev/null +++ b/tests/unit_tests/model/test_model_objects_price.py @@ -0,0 +1,662 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2022 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. +# ------------------------------------------------------------------------------------------------- + +import pickle +from decimal import Decimal + +import pytest + +from nautilus_trader.model.objects import Price + + +class TestPrice: + def test_instantiate_with_none_value_raises_type_error(self): + # Arrange, Act, Assert + with pytest.raises(TypeError): + Price(None) + + def test_instantiate_with_negative_precision_raises_overflow_error(self): + # Arrange, Act, Assert + with pytest.raises(OverflowError): + Price(1.0, precision=-1) + + def test_instantiate_with_precision_over_maximum_raises_overflow_error(self): + # Arrange, Act, Assert + with pytest.raises(ValueError): + Price(1.0, precision=10) + + def test_instantiate_with_value_exceeding_positive_limit_raises_value_error(self): + # Arrange, Act, Assert + with pytest.raises(ValueError): + Price(9_223_372_036 + 1, precision=0) + + def test_instantiate_with_value_exceeding_negative_limit_raises_value_error(self): + # Arrange, Act, Assert + with pytest.raises(ValueError): + Price(-9_223_372_036 - 1, precision=0) + + def test_instantiate_base_decimal_from_int(self): + # Arrange, Act + result = Price(1, precision=1) + + # Assert + assert str(result) == "1.0" + + def test_instantiate_base_decimal_from_float(self): + # Arrange, Act + result = Price(1.12300, precision=5) + + # Assert + assert str(result) == "1.12300" + + def test_instantiate_base_decimal_from_decimal(self): + # Arrange, Act + result = Price(Decimal("1.23"), precision=1) + + # Assert + assert str(result) == "1.2" + + def test_instantiate_base_decimal_from_str(self): + # Arrange, Act + result = Price.from_str("1.23") + + # Assert + assert str(result) == "1.23" + + @pytest.mark.parametrize( + "value, precision, expected", + [ + [Price(2.15, precision=2), 0, Decimal("2")], + [Price(2.15, precision=2), 1, Decimal("2.2")], + [Price(2.255, precision=3), 2, Decimal("2.26")], + ], + ) + def test_round_with_various_digits_returns_expected_decimal(self, value, precision, expected): + # Arrange, Act + result = round(value, precision) + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value, expected", + [ + [Price(-0, precision=0), Decimal("0")], + [Price(0, precision=0), Decimal("0")], + [Price(1, precision=0), Decimal("1")], + [Price(-1, precision=0), Decimal("1")], + [Price(-1.1, precision=1), Decimal("1.1")], + ], + ) + def test_abs_with_various_values_returns_expected_decimal(self, value, expected): + # Arrange, Act + result = abs(value) + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value, expected", + [ + [ + Price(-1, precision=0), + Decimal("-1"), + ], # Matches built-in decimal.Decimal behaviour + [Price(0, 0), Decimal("0")], + ], + ) + def test_pos_with_various_values_returns_expected_decimal(self, value, expected): + # Arrange, Act + result = +value + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value, expected", + [ + [Price(1, precision=0), Decimal("-1")], + [Price(0, precision=0), Decimal("0")], + ], + ) + def test_neg_with_various_values_returns_expected_decimal(self, value, expected): + # Arrange, Act + result = -value + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value, expected", + [ + [0, Price(0, precision=0)], + [1, Price(1, precision=0)], + [-1, Price(-1, precision=0)], + [Decimal(0), Price(0, precision=0)], + [Decimal("1.1"), Price(1.1, precision=1)], + [Decimal("-1.1"), Price(-1.1, precision=1)], + [Price(0, precision=0), Price(0, precision=0)], + [Price(1.1, precision=1), Price(1.1, precision=1)], + [Price(-1.1, precision=1), Price(-1.1, precision=1)], + ], + ) + def test_instantiate_with_various_valid_inputs_returns_expected_decimal(self, value, expected): + # Arrange, Act + decimal_object = Price(value, 2) + + # Assert + assert decimal_object == expected + + @pytest.mark.parametrize( + "value, precision, expected", + [ + [0.0, 0, Price(0, precision=0)], + [1.0, 0, Price(1, precision=0)], + [-1.0, 0, Price(-1, precision=0)], + [1.123, 3, Price(1.123, precision=3)], + [-1.123, 3, Price(-1.123, precision=3)], + [1.155, 2, Price(1.16, precision=2)], + ], + ) + def test_instantiate_with_various_precisions_returns_expected_decimal( + self, value, precision, expected + ): + # Arrange, Act + decimal_object = Price(value, precision) + + # Assert + assert decimal_object == expected + assert decimal_object.precision == precision + + @pytest.mark.parametrize( + "value1, value2, expected", + [ + [0, -0, True], + [-0, 0, True], + [-1, -1, True], + [1, 1, True], + [1.1, 1.1, True], + [-1.1, -1.1, True], + [0, 1, False], + [-1, 0, False], + [-1, -2, False], + [1, 2, False], + [1.1, 1.12, False], + [-1.12, -1.1, False], + ], + ) + def test_equality_with_various_values_returns_expected_result(self, value1, value2, expected): + # Arrange, Act + result = Price(value1, 2) == Price(value2, 2) + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value1, value2, expected", + [ + [0, -0, True], + [-0, 0, True], + [-1, -1, True], + [1, 1, True], + [0, 1, False], + [-1, 0, False], + [-1, -2, False], + [1, 2, False], + ], + ) + def test_equality_with_various_int_returns_expected_result(self, value1, value2, expected): + # Arrange, Act + result1 = Price(value1, 0) == value2 + result2 = value2 == Price(value1, 0) + + # Assert + assert result1 == expected + assert result2 == expected + + @pytest.mark.parametrize( + "value1, value2, expected", + [ + [Price(0, precision=0), Decimal(0), True], + [Price(0, precision=0), Decimal(-0), True], + [Price(1, precision=0), Decimal(0), False], + ], + ) + def test_equality_with_various_decimals_returns_expected_result(self, value1, value2, expected): + # Arrange, Act + result = value1 == value2 + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value1, value2, expected1, expected2, expected3, expected4", + [ + [0, 0, False, True, True, False], + [1, 0, True, True, False, False], + [-1, 0, False, False, True, True], + ], + ) + def test_comparisons_with_various_values_returns_expected_result( + self, + value1, + value2, + expected1, + expected2, + expected3, + expected4, + ): + # Arrange, Act, Assert + assert (Price(value1, precision=0) > Price(value2, precision=0)) == expected1 + assert (Price(value1, precision=0) >= Price(value2, precision=0)) == expected2 + assert (Price(value1, precision=0) <= Price(value2, precision=0)) == expected3 + assert (Price(value1, precision=0) < Price(value2, precision=0)) == expected4 + + @pytest.mark.parametrize( + "value1, value2, expected_type, expected_value", + [ + [Price(0, precision=0), Price(0, precision=0), Decimal, 0], + [ + Price(0, precision=0), + Price(1.1, precision=1), + Decimal, + Decimal("1.1"), + ], + [Price(0, precision=0), 0, Decimal, 0], + [Price(0, precision=0), 1, Decimal, 1], + [0, Price(0, precision=0), Decimal, 0], + [1, Price(0, precision=0), Decimal, 1], + [Price(0, precision=0), 0.1, float, 0.1], + [Price(0, precision=0), 1.1, float, 1.1], + [-1.1, Price(0, precision=0), float, -1.1], + [1.1, Price(0, precision=0), float, 1.1], + [ + Price(1, precision=0), + Price(1.1, precision=1), + Decimal, + Decimal("2.1"), + ], + [Price(1, precision=0), Decimal("1.1"), Decimal, Decimal("2.1")], + ], + ) + def test_addition_with_various_types_returns_expected_result( + self, + value1, + value2, + expected_type, + expected_value, + ): + # Arrange, Act + result = value1 + value2 + + # Assert + assert isinstance(result, expected_type) + assert result == expected_value + + @pytest.mark.parametrize( + "value1, value2, expected_type, expected_value", + [ + [Price(0, precision=0), Price(0, precision=0), Decimal, 0], + [ + Price(0, precision=0), + Price(1.1, precision=1), + Decimal, + Decimal("-1.1"), + ], + [Price(0, precision=0), 0, Decimal, 0], + [Price(0, precision=0), 1, Decimal, -1], + [0, Price(0, precision=0), Decimal, 0], + [1, Price(1, precision=0), Decimal, 0], + [Price(0, precision=0), 0.1, float, -0.1], + [Price(0, precision=0), 1.1, float, -1.1], + [0.1, Price(1, precision=0), float, -0.9], + [1.1, Price(1, precision=0), float, 0.10000000000000009], + [ + Price(1, precision=0), + Price(1.1, precision=1), + Decimal, + Decimal("-0.1"), + ], + [Price(1, precision=0), Decimal("1.1"), Decimal, Decimal("-0.1")], + ], + ) + def test_subtraction_with_various_types_returns_expected_result( + self, + value1, + value2, + expected_type, + expected_value, + ): + # Arrange, Act + result = value1 - value2 + + # Assert + assert isinstance(result, expected_type) + assert result == expected_value + + @pytest.mark.parametrize( + "value1, value2, expected_type, expected_value", + [ + [Price(0, 0), 0, Decimal, 0], + [Price(1, 0), 1, Decimal, 1], + [1, Price(1, 0), Decimal, 1], + [2, Price(3, 0), Decimal, 6], + [Price(2, 0), 1.0, float, 2], + [1.1, Price(2, 0), float, 2.2], + [Price(1.1, 1), Price(1.1, 1), Decimal, Decimal("1.21")], + [Price(1.1, 1), Decimal("1.1"), Decimal, Decimal("1.21")], + ], + ) + def test_multiplication_with_various_types_returns_expected_result( + self, + value1, + value2, + expected_type, + expected_value, + ): + # Arrange, Act + result = value1 * value2 + + # Assert + assert isinstance(result, expected_type) + assert result == expected_value + + @pytest.mark.parametrize( + "value1, value2, expected_type, expected_value", + [ + [1, Price(1, 0), Decimal, 1], + [1.1, Price(1.1, 1), float, 1], + [Price(0, 0), 1, Decimal, 0], + [Price(1, 0), 2, Decimal, Decimal("0.5")], + [2, Price(1, 0), Decimal, Decimal("2.0")], + [Price(2, 0), 1.1, float, 1.8181818181818181], + [1.1, Price(2, 0), float, 1.1 / 2], + [ + Price(1.1, 1), + Price(1.2, 1), + Decimal, + Decimal("0.9166666666666666666666666667"), + ], + [ + Price(1.1, 1), + Decimal("1.2"), + Decimal, + Decimal("0.9166666666666666666666666667"), + ], + ], + ) + def test_division_with_various_types_returns_expected_result( + self, + value1, + value2, + expected_type, + expected_value, + ): + # Arrange, Act + result = value1 / value2 + + # Assert + assert expected_type == type(result) + assert expected_value == result + + @pytest.mark.parametrize( + "value1, value2, expected_type, expected_value", + [ + [1, Price(1, 0), Decimal, 1], + [Price(0, 0), 1, Decimal, 0], + [Price(1, 0), 2, Decimal, Decimal(0)], + [2, Price(1, 0), Decimal, Decimal(2)], + [2.1, Price(1.1, 1), float, 1], + [4.4, Price(1.1, 1), float, 4], + [Price(2.1, 1), 1.1, float, 1], + [Price(4.4, 1), 1.1, float, 4], + [Price(1.1, 1), Price(1.2, 1), Decimal, Decimal(0)], + [Price(1.1, 1), Decimal("1.2"), Decimal, Decimal(0)], + ], + ) + def test_floor_division_with_various_types_returns_expected_result( + self, + value1, + value2, + expected_type, + expected_value, + ): + # Arrange, Act + result = value1 // value2 + + # Assert + assert expected_type == type(result) + assert expected_value == result + + @pytest.mark.parametrize( + "value1, value2, expected_type, expected_value", + [ + [1, Price(1, 0), Decimal, 1], + [Price(100, 0), 10, Decimal, 0], + [Price(23, 0), 2, Decimal, 1], + [2.1, Price(1.1, 1), float, 1.0], + [1.1, Price(2.1, 1), float, 1.1], + [Price(2.1, 1), 1.1, float, 1.0], + [Price(1.1, 1), 2.1, float, 1.1], + [Price(1.1, 1), Price(0.2, 1), Decimal, Decimal("0.1")], + ], + ) + def test_mod_with_various_types_returns_expected_result( + self, + value1, + value2, + expected_type, + expected_value, + ): + # Arrange, Act + result = value1 % value2 # noqa (not modulo formatting) + + # Assert + assert expected_type == type(result) + assert expected_value == result + + @pytest.mark.parametrize( + "value1, value2, expected", + [ + [Price(1, 0), Price(2, 0), Price(2, 0)], + [Price(1, 0), 2, 2], + [Price(1, 0), Decimal(2), Decimal(2)], + ], + ) + def test_max_with_various_types_returns_expected_result( + self, + value1, + value2, + expected, + ): + # Arrange, Act + result = max(value1, value2) + + # Assert + assert expected == result + + @pytest.mark.parametrize( + "value1, value2, expected", + [ + [Price(1, 0), Price(2, 0), Price(1, 0)], + [Price(1, 0), 2, Price(1, 0)], + [Price(2, 0), Decimal(1), Decimal(1)], + ], + ) + def test_min_with_various_types_returns_expected_result( + self, + value1, + value2, + expected, + ): + # Arrange, Act + result = min(value1, value2) + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value, expected", + [["1", 1], ["1.1", 1]], + ) + def test_int(self, value, expected): + # Arrange + decimal1 = Price.from_str(value) + + # Act, Assert + assert int(decimal1) == expected + + def test_hash(self): + # Arrange + decimal1 = Price(1.1, 1) + decimal2 = Price(1.1, 1) + + # Act, Assert + assert isinstance(hash(decimal2), int) + assert hash(decimal1) == hash(decimal2) + + @pytest.mark.parametrize( + "value, precision, expected", + [ + [0, 0, "0"], + [-0, 0, "0"], + [-1, 0, "-1"], + [1, 0, "1"], + [1.1, 1, "1.1"], + [-1.1, 1, "-1.1"], + ], + ) + def test_str_with_various_values_returns_expected_string( + self, + value, + precision, + expected, + ): + # Arrange, Act + decimal_object = Price(value, precision=precision) + + # Assert + assert str(decimal_object) == expected + + def test_repr(self): + # Arrange, Act + result = repr(Price(1.1, 1)) + + # Assert + assert "Price('1.1')" == result + + @pytest.mark.parametrize( + "value, precision, expected", + [ + [0, 0, Price(0, 0)], + [-0, 0, Price(0, 0)], + [-1, 0, Price(-1, 0)], + [1, 0, Price(1, 0)], + [1.1, 1, Price(1.1, 1)], + [-1.1, 1, Price(-1.1, 1)], + ], + ) + def test_as_decimal_with_various_values_returns_expected_value( + self, + value, + precision, + expected, + ): + # Arrange, Act + result = Price(value, precision=precision) + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value, expected", + [[0, 0], [-0, 0], [-1, -1], [1, 1], [1.1, 1.1], [-1.1, -1.1]], + ) + def test_as_double_with_various_values_returns_expected_value(self, value, expected): + # Arrange, Act + result = Price(value, 1).as_double() + + # Assert + assert result == expected + + def test_calling_new_returns_an_expected_zero_price(self): + # Arrange, Act + new_price = Price.__new__(Price, 1, 1) + + # Assert + assert new_price == 0 + + def test_from_raw_returns_expected_price(self): + # Arrange, Act + price1 = Price.from_raw(1000000000000, 3) + price2 = Price(1000, 3) + + # Assert + assert price1 == price2 + assert str(price1) == "1000.000" + assert price1.precision == 3 + + def test_equality(self): + # Arrange, Act + price1 = Price(1.0, precision=1) + price2 = Price(1.5, precision=1) + + # Assert + assert price1 == price1 + assert price1 != price2 + assert price2 > price1 + + def test_from_int_returns_expected_value(self): + # Arrange, Act + price = Price.from_int(100) + + # Assert + assert str(price) == "100" + assert price.precision == 0 + + @pytest.mark.parametrize( + "value, string, precision", + [ + ["100.11", "100.11", 2], + ["1E7", "10000000", 0], + ["1E-7", "0.0000001", 7], + ["1e-2", "0.01", 2], + ], + ) + def test_from_str_returns_expected_value(self, value, string, precision): + # Arrange, Act + price = Price.from_str(value) + + # Assert + assert str(price) == string + assert price.precision == precision + + def test_str_repr(self): + # Arrange, Act + price = Price(1.00000, precision=5) + + # Assert + assert "1.00000" == str(price) + assert "Price('1.00000')" == repr(price) + + def test_pickle_dumps_and_loads(self): + # Arrange + price = Price(1.2000, 2) + + # Act + pickled = pickle.dumps(price) + + # Assert + assert pickle.loads(pickled) == price # noqa (testing pickle) diff --git a/tests/unit_tests/model/test_model_objects_quantity.py b/tests/unit_tests/model/test_model_objects_quantity.py new file mode 100644 index 000000000000..4c1433b6da0f --- /dev/null +++ b/tests/unit_tests/model/test_model_objects_quantity.py @@ -0,0 +1,628 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2022 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. +# ------------------------------------------------------------------------------------------------- + +import pickle +from decimal import Decimal + +import pytest + +from nautilus_trader.model.objects import Quantity + + +class TestQuantity: + def test_instantiate_with_none_value_raises_type_error(self): + # Arrange, Act, Assert + with pytest.raises(TypeError): + Quantity(None) + + def test_instantiate_with_negative_precision_raises_overflow_error(self): + # Arrange, Act, Assert + with pytest.raises(OverflowError): + Quantity(1.0, precision=-1) + + def test_instantiate_with_precision_over_maximum_raises_value_error(self): + # Arrange, Act, Assert + with pytest.raises(ValueError): + Quantity(1.0, precision=10) + + def test_instantiate_with_value_exceeding_limit_raises_value_error(self): + # Arrange, Act, Assert + with pytest.raises(ValueError): + Quantity(18_446_744_073 + 1, precision=0) + + def test_instantiate_base_decimal_from_int(self): + # Arrange, Act + Quantity(1, precision=1) + + def test_instantiate_base_decimal_from_float(self): + # Arrange, Act + result = Quantity(1.12300, precision=5) + + # Assert + assert str(result) == "1.12300" + + def test_instantiate_base_decimal_from_decimal(self): + # Arrange, Act + result = Quantity(Decimal("1.23"), precision=1) + + # Assert + assert str(result) == "1.2" + + def test_instantiate_base_decimal_from_str(self): + # Arrange, Act + result = Quantity.from_str("1.23") + + # Assert + assert str(result) == "1.23" + + @pytest.mark.parametrize( + "value, precision, expected", + [ + [Quantity(2.15, precision=2), 0, Decimal("2")], + [Quantity(2.15, precision=2), 1, Decimal("2.2")], + [Quantity(2.255, precision=3), 2, Decimal("2.26")], + ], + ) + def test_round_with_various_digits_returns_expected_decimal(self, value, precision, expected): + # Arrange, Act + result = round(value, precision) + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value, expected", + [ + [Quantity(-0, precision=0), Decimal("0")], + [Quantity(0, precision=0), Decimal("0")], + [Quantity(1, precision=0), Decimal("1")], + ], + ) + def test_abs_with_various_values_returns_expected_decimal(self, value, expected): + # Arrange, Act + result = abs(value) + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value, expected", + [ + [Quantity(1, precision=0), Decimal("-1")], + [Quantity(0, precision=0), Decimal("0")], + ], + ) + def test_neg_with_various_values_returns_expected_decimal(self, value, expected): + # Arrange, Act + result = -value + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value, expected", + [ + [0, Quantity(0, precision=0)], + [1, Quantity(1, precision=0)], + [Decimal(0), Quantity(0, precision=0)], + [Decimal("1.1"), Quantity(1.1, precision=1)], + [Quantity(0, precision=0), Quantity(0, precision=0)], + [Quantity(1.1, precision=1), Quantity(1.1, precision=1)], + ], + ) + def test_instantiate_with_various_valid_inputs_returns_expected_decimal(self, value, expected): + # Arrange, Act + decimal_object = Quantity(value, 2) + + # Assert + assert decimal_object == expected + + @pytest.mark.parametrize( + "value, precision, expected", + [ + [0.0, 0, Quantity(0, precision=0)], + [1.0, 0, Quantity(1, precision=0)], + [1.123, 3, Quantity(1.123, precision=3)], + [1.155, 2, Quantity(1.16, precision=2)], + ], + ) + def test_instantiate_with_various_precisions_returns_expected_decimal( + self, value, precision, expected + ): + # Arrange, Act + decimal_object = Quantity(value, precision) + + # Assert + assert decimal_object == expected + assert decimal_object.precision == precision + + @pytest.mark.parametrize( + "value1, value2, expected", + [ + [0, -0, True], + [-0, 0, True], + [1, 1, True], + [1.1, 1.1, True], + [0, 1, False], + [1, 2, False], + [1.1, 1.12, False], + ], + ) + def test_equality_with_various_values_returns_expected_result(self, value1, value2, expected): + # Arrange, Act + result = Quantity(value1, 2) == Quantity(value2, 2) + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value1, value2, expected", + [ + [0, -0, True], + [-0, 0, True], + [1, 1, True], + [0, 1, False], + [1, 2, False], + ], + ) + def test_equality_with_various_int_returns_expected_result(self, value1, value2, expected): + # Arrange, Act + result1 = Quantity(value1, 0) == value2 + result2 = value2 == Quantity(value1, 0) + + # Assert + assert result1 == expected + assert result2 == expected + + @pytest.mark.parametrize( + "value1, value2, expected", + [ + [Quantity(0, precision=0), Decimal(0), True], + [Quantity(0, precision=0), Decimal(-0), True], + [Quantity(1, precision=0), Decimal(0), False], + ], + ) + def test_equality_with_various_decimals_returns_expected_result(self, value1, value2, expected): + # Arrange, Act + result = value1 == value2 + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value1, value2, expected1, expected2, expected3, expected4", + [ + [0, 0, False, True, True, False], + [1, 0, True, True, False, False], + ], + ) + def test_comparisons_with_various_values_returns_expected_result( + self, + value1, + value2, + expected1, + expected2, + expected3, + expected4, + ): + # Arrange, Act, Assert + assert (Quantity(value1, precision=0) > Quantity(value2, precision=0)) == expected1 + assert (Quantity(value1, precision=0) >= Quantity(value2, precision=0)) == expected2 + assert (Quantity(value1, precision=0) <= Quantity(value2, precision=0)) == expected3 + assert (Quantity(value1, precision=0) < Quantity(value2, precision=0)) == expected4 + + @pytest.mark.parametrize( + "value1, value2, expected_type, expected_value", + [ + [Quantity(0, precision=0), Quantity(0, precision=0), Decimal, 0], + [ + Quantity(0, precision=0), + Quantity(1.1, precision=1), + Decimal, + Decimal("1.1"), + ], + [Quantity(0, precision=0), 0, Decimal, 0], + [Quantity(0, precision=0), 1, Decimal, 1], + [0, Quantity(0, precision=0), Decimal, 0], + [1, Quantity(0, precision=0), Decimal, 1], + [Quantity(0, precision=0), 0.1, float, 0.1], + [Quantity(0, precision=0), 1.1, float, 1.1], + [-1.1, Quantity(0, precision=0), float, -1.1], + [1.1, Quantity(0, precision=0), float, 1.1], + [ + Quantity(1, precision=0), + Quantity(1.1, precision=1), + Decimal, + Decimal("2.1"), + ], + [Quantity(1, precision=0), Decimal("1.1"), Decimal, Decimal("2.1")], + ], + ) + def test_addition_with_various_types_returns_expected_result( + self, + value1, + value2, + expected_type, + expected_value, + ): + # Arrange, Act + result = value1 + value2 + + # Assert + assert isinstance(result, expected_type) + assert result == expected_value + + @pytest.mark.parametrize( + "value1, value2, expected_type, expected_value", + [ + [Quantity(0, precision=0), Quantity(0, precision=0), Decimal, 0], + [ + Quantity(0, precision=0), + Quantity(1.1, precision=1), + Decimal, + Decimal("-1.1"), + ], + [Quantity(0, precision=0), 0, Decimal, 0], + [Quantity(0, precision=0), 1, Decimal, -1], + [0, Quantity(0, precision=0), Decimal, 0], + [1, Quantity(1, precision=0), Decimal, 0], + [Quantity(0, precision=0), 0.1, float, -0.1], + [Quantity(0, precision=0), 1.1, float, -1.1], + [0.1, Quantity(1, precision=0), float, -0.9], + [1.1, Quantity(1, precision=0), float, 0.10000000000000009], + [ + Quantity(1, precision=0), + Quantity(1.1, precision=1), + Decimal, + Decimal("-0.1"), + ], + [Quantity(1, precision=0), Decimal("1.1"), Decimal, Decimal("-0.1")], + ], + ) + def test_subtraction_with_various_types_returns_expected_result( + self, + value1, + value2, + expected_type, + expected_value, + ): + # Arrange, Act + result = value1 - value2 + + # Assert + assert isinstance(result, expected_type) + assert result == expected_value + + @pytest.mark.parametrize( + "value1, value2, expected_type, expected_value", + [ + [Quantity(0, 0), 0, Decimal, 0], + [Quantity(1, 0), 1, Decimal, 1], + [1, Quantity(1, 0), Decimal, 1], + [2, Quantity(3, 0), Decimal, 6], + [Quantity(2, 0), 1.0, float, 2], + [1.1, Quantity(2, 0), float, 2.2], + [Quantity(1.1, 1), Quantity(1.1, 1), Decimal, Decimal("1.21")], + [Quantity(1.1, 1), Decimal("1.1"), Decimal, Decimal("1.21")], + ], + ) + def test_multiplication_with_various_types_returns_expected_result( + self, + value1, + value2, + expected_type, + expected_value, + ): + # Arrange, Act + result = value1 * value2 + + # Assert + assert isinstance(result, expected_type) + assert result == expected_value + + @pytest.mark.parametrize( + "value1, value2, expected_type, expected_value", + [ + [1, Quantity(1, 0), Decimal, 1], + [1.1, Quantity(1.1, 1), float, 1], + [Quantity(0, 0), 1, Decimal, 0], + [Quantity(1, 0), 2, Decimal, Decimal("0.5")], + [2, Quantity(1, 0), Decimal, Decimal("2.0")], + [Quantity(2, 0), 1.1, float, 1.8181818181818181], + [1.1, Quantity(2, 0), float, 1.1 / 2], + [ + Quantity(1.1, 1), + Quantity(1.2, 1), + Decimal, + Decimal("0.9166666666666666666666666667"), + ], + [ + Quantity(1.1, 1), + Decimal("1.2"), + Decimal, + Decimal("0.9166666666666666666666666667"), + ], + ], + ) + def test_division_with_various_types_returns_expected_result( + self, + value1, + value2, + expected_type, + expected_value, + ): + # Arrange, Act + result = value1 / value2 + + # Assert + assert expected_type == type(result) + assert expected_value == result + + @pytest.mark.parametrize( + "value1, value2, expected_type, expected_value", + [ + [1, Quantity(1, 0), Decimal, 1], + [Quantity(0, 0), 1, Decimal, 0], + [Quantity(1, 0), 2, Decimal, Decimal(0)], + [2, Quantity(1, 0), Decimal, Decimal(2)], + [2.1, Quantity(1.1, 1), float, 1], + [4.4, Quantity(1.1, 1), float, 4], + [Quantity(2.1, 1), 1.1, float, 1], + [Quantity(4.4, 1), 1.1, float, 4], + [Quantity(1.1, 1), Quantity(1.2, 1), Decimal, Decimal(0)], + [Quantity(1.1, 1), Decimal("1.2"), Decimal, Decimal(0)], + ], + ) + def test_floor_division_with_various_types_returns_expected_result( + self, + value1, + value2, + expected_type, + expected_value, + ): + # Arrange, Act + result = value1 // value2 + + # Assert + assert expected_type == type(result) + assert expected_value == result + + @pytest.mark.parametrize( + "value1, value2, expected_type, expected_value", + [ + [Quantity(100, 0), 10, Decimal, 0], + [Quantity(23, 0), 2, Decimal, 1], + [2.1, Quantity(1.1, 1), float, 1.0], + [1.1, Quantity(2.1, 1), float, 1.1], + [Quantity(2.1, 1), 1.1, float, 1.0], + [Quantity(1.1, 1), 2.1, float, 1.1], + [Quantity(1.1, 1), Decimal("0.2"), Decimal, Decimal("0.1")], + ], + ) + def test_mod_with_various_types_returns_expected_result( + self, + value1, + value2, + expected_type, + expected_value, + ): + # Arrange, Act + result = value1 % value2 # noqa (not modulo formatting) + + # Assert + assert expected_type == type(result) + assert expected_value == result + + @pytest.mark.parametrize( + "value1, value2, expected", + [ + [Quantity(1, 0), Quantity(2, 0), Quantity(2, 0)], + [Quantity(1, 0), 2, 2], + [Quantity(1, 0), Decimal(2), Decimal(2)], + ], + ) + def test_max_with_various_types_returns_expected_result( + self, + value1, + value2, + expected, + ): + # Arrange, Act + result = max(value1, value2) + + # Assert + assert expected == result + + @pytest.mark.parametrize( + "value1, value2, expected", + [ + [Quantity(1, 0), Quantity(2, 0), Quantity(1, 0)], + [Quantity(1, 0), 2, Quantity(1, 0)], + [Quantity(2, 0), Decimal(1), Decimal(1)], + ], + ) + def test_min_with_various_types_returns_expected_result( + self, + value1, + value2, + expected, + ): + # Arrange, Act + result = min(value1, value2) + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value, expected", + [["1", 1], ["1.1", 1]], + ) + def test_int(self, value, expected): + # Arrange + decimal1 = Quantity.from_str(value) + + # Act, Assert + assert int(decimal1) == expected + + def test_hash(self): + # Arrange + decimal1 = Quantity(1.1, 1) + decimal2 = Quantity(1.1, 1) + + # Act, Assert + assert isinstance(hash(decimal2), int) + assert hash(decimal1) == hash(decimal2) + + @pytest.mark.parametrize( + "value, precision, expected", + [ + [0, 0, "0"], + [-0, 0, "0"], + [1, 0, "1"], + [1.1, 1, "1.1"], + ], + ) + def test_str_with_various_values_returns_expected_string( + self, + value, + precision, + expected, + ): + # Arrange, Act + decimal_object = Quantity(value, precision=precision) + + # Assert + assert str(decimal_object) == expected + + def test_repr(self): + # Arrange, Act + result = repr(Quantity(1.1, 1)) + + # Assert + assert "Quantity('1.1')" == result + + @pytest.mark.parametrize( + "value, precision, expected", + [ + [0, 0, Quantity(0, 0)], + [-0, 0, Quantity(0, 0)], + [1, 0, Quantity(1, 0)], + [1.1, 1, Quantity(1.1, 1)], + ], + ) + def test_as_decimal_with_various_values_returns_expected_value( + self, + value, + precision, + expected, + ): + # Arrange, Act + result = Quantity(value, precision=precision) + + # Assert + assert result == expected + + @pytest.mark.parametrize( + "value, expected", + [[0, 0], [-0, 0], [1, 1], [1.1, 1.1]], + ) + def test_as_double_with_various_values_returns_expected_value(self, value, expected): + # Arrange, Act + result = Quantity(value, 1).as_double() + + # Assert + assert result == expected + + def test_calling_new_returns_an_expected_zero_quantity(self): + # Arrange, Act + new_qty = Quantity.__new__(Quantity, 1, 1) + + # Assert + assert new_qty == 0 + + def test_from_raw_returns_expected_quantity(self): + # Arrange, Act + qty1 = Quantity.from_raw(1000000000000, 3) + qty2 = Quantity(1000, 3) + + # Assert + assert qty1 == qty2 + assert str(qty1) == "1000.000" + assert qty1.precision == 3 + + def test_zero_returns_zero_quantity(self): + # Arrange, Act + qty = Quantity.zero() + + # Assert + assert qty == 0 + assert str(qty) == "0" + assert qty.precision == 0 + + def test_from_int_returns_expected_value(self): + # Arrange, Act + qty = Quantity.from_int(1000) + + # Assert + assert qty == 1000 + assert str(qty) == "1000" + assert qty.precision == 0 + + def test_from_str_returns_expected_value(self): + # Arrange, Act + qty = Quantity.from_str("0.511") + + # Assert + assert qty == Quantity(0.511, precision=3) + assert str(qty) == "0.511" + assert qty.precision == 3 + + @pytest.mark.parametrize( + "value, expected", + [ + ["0", "0"], + ["10.05", "10.05"], + ["1000", "1_000"], + ["1112", "1_112"], + ["120100", "120_100"], + ["200000", "200_000"], + ["1000000", "1_000_000"], + ["2500000", "2_500_000"], + ["1111111", "1_111_111"], + ["2523000", "2_523_000"], + ["100000000", "100_000_000"], + ], + ) + def test_str_and_to_str(self, value, expected): + # Arrange, Act, Assert + assert Quantity.from_str(value).to_str() == expected + + def test_str_repr(self): + # Arrange + quantity = Quantity(2100.1666666, 6) + + # Act, Assert + assert "2100.166667" == str(quantity) + assert "Quantity('2100.166667')" == repr(quantity) + + def test_pickle_dumps_and_loads(self): + # Arrange + quantity = Quantity(1.2000, 2) + + # Act + pickled = pickle.dumps(quantity) + + # Assert + assert pickle.loads(pickled) == quantity # noqa (testing pickle) From d6ddbedf1ebd64e53779a6985d9b3acadc4d027a Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 27 Oct 2022 19:29:42 +1100 Subject: [PATCH 05/38] Extend instruments handling --- RELEASES.md | 5 +- nautilus_trader/backtest/data_client.pyx | 19 +++++- nautilus_trader/common/actor.pxd | 3 + nautilus_trader/common/actor.pyx | 64 +++++++++++++++++++- nautilus_trader/data/client.pxd | 4 ++ nautilus_trader/data/client.pyx | 56 ++++++++++++++++- nautilus_trader/data/engine.pyx | 14 +++-- tests/test_kit/mocks/actors.py | 4 ++ tests/unit_tests/common/test_common_actor.py | 38 ++++++++++++ tests/unit_tests/data/test_data_engine.py | 29 ++++++++- 10 files changed, 221 insertions(+), 15 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 4d92be5518b9..7a564eb5a8e8 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -7,9 +7,10 @@ Released on TBD (UTC). - Removed `HilbertPeriod` indicator (to reduce total package size) - Removed `HilbertSignalNoiseRatio` indicator (to reduce total package size) - Removed `HilbertTransform` indicator (to reduce total package size) - + ### Enhancements -None +- Added `Actor.request_instruments(...)` method +- Extended instrument(s) req/res handling for `DataClient` and `Actor ### Fixes None diff --git a/nautilus_trader/backtest/data_client.pyx b/nautilus_trader/backtest/data_client.pyx index 4a4416d25f65..3df04cb19e95 100644 --- a/nautilus_trader/backtest/data_client.pyx +++ b/nautilus_trader/backtest/data_client.pyx @@ -305,9 +305,22 @@ cdef class BacktestMarketDataClient(MarketDataClient): metadata={"instrument_id": instrument_id}, ) - self._handle_data_response( - data_type=data_type, - data=[instrument], # Data engine handles lists of instruments + self._handle_instrument( + instrument=instrument, + correlation_id=correlation_id, + ) + + cpdef void request_instruments(self, Venue venue, UUID4 correlation_id) except *: + Condition.not_none(correlation_id, "correlation_id") + + cdef list instruments = self._cache.instruments(venue) + if not instruments: + self._log.error(f"Cannot find instruments.") + return + + self._handle_instruments( + venue=venue, + instruments=instruments, correlation_id=correlation_id, ) diff --git a/nautilus_trader/common/actor.pxd b/nautilus_trader/common/actor.pxd index 73678174df57..644bccfa4d4e 100644 --- a/nautilus_trader/common/actor.pxd +++ b/nautilus_trader/common/actor.pxd @@ -143,6 +143,7 @@ cdef class Actor(Component): cpdef void request_data(self, ClientId client_id, DataType data_type) except * cpdef void request_instrument(self, InstrumentId instrument_id, ClientId client_id=*) except * + cpdef void request_instruments(self, Venue venue, ClientId client_id=*) except * cpdef void request_quote_ticks( self, InstrumentId instrument_id, @@ -168,6 +169,7 @@ cdef class Actor(Component): # -- HANDLERS ------------------------------------------------------------------------------------- cpdef void handle_instrument(self, Instrument instrument) except * + cpdef void handle_instruments(self, list instruments) except * cpdef void handle_order_book(self, OrderBook order_book) except * cpdef void handle_order_book_delta(self, OrderBookData data) except * cpdef void handle_ticker(self, Ticker ticker) except * @@ -186,6 +188,7 @@ cdef class Actor(Component): cpdef void _handle_data_response(self, DataResponse response) except * cpdef void _handle_instrument_response(self, DataResponse response) except * + cpdef void _handle_instruments_response(self, DataResponse response) except * cpdef void _handle_quote_ticks_response(self, DataResponse response) except * cpdef void _handle_trade_ticks_response(self, DataResponse response) except * cpdef void _handle_bars_response(self, DataResponse response) except * diff --git a/nautilus_trader/common/actor.pyx b/nautilus_trader/common/actor.pyx index 9cfe05027d75..1d96314f146b 100644 --- a/nautilus_trader/common/actor.pyx +++ b/nautilus_trader/common/actor.pyx @@ -1419,7 +1419,7 @@ cdef class Actor(Component): cpdef void request_instrument(self, InstrumentId instrument_id, ClientId client_id = None) except *: """ - Request a `Instrument` data for the given instrument ID. + Request `Instrument` data for the given instrument ID. Parameters ---------- @@ -1445,6 +1445,34 @@ cdef class Actor(Component): self._send_data_req(request) + cpdef void request_instruments(self, Venue venue, ClientId client_id = None) except *: + """ + Request all `Instrument` data for the given venue. + + Parameters + ---------- + venue : Venue + The venue for the request. + client_id : ClientId, optional + The specific client ID for the command. + If ``None`` then will be inferred from the venue in the instrument ID. + + """ + Condition.not_none(venue, "venue") + + cdef DataRequest request = DataRequest( + client_id=client_id, + venue=venue, + data_type=DataType(Instrument, metadata={ + "venue": venue, + }), + callback=self._handle_instruments_response, + request_id=UUID4(), + ts_init=self._clock.timestamp_ns(), + ) + + self._send_data_req(request) + cpdef void request_quote_ticks( self, InstrumentId instrument_id, @@ -1628,6 +1656,37 @@ cdef class Actor(Component): self._log.exception(f"Error on handling {repr(instrument)}", e) raise + @cython.boundscheck(False) + @cython.wraparound(False) + cpdef void handle_instruments(self, list instruments) except *: + """ + Handle the given instruments data by handling each instrument individually. + + Parameters + ---------- + instruments : list[Instrument] + The instruments received. + + Warnings + -------- + System method (not intended to be called by user code). + + """ + Condition.not_none(instruments, "instruments") # Could be empty + + cdef int length = len(instruments) + cdef Instrument first = instruments[0] if length > 0 else None + cdef InstrumentId instrument_id = first.id if first is not None else None + + if length > 0: + self._log.info(f"Received data for {instrument_id.venue}.") + else: + self._log.warning("Received data with no instruments.") + + cdef int i + for i in range(length): + self.handle_instrument(instruments[i]) + cpdef void handle_order_book_delta(self, OrderBookData delta) except *: """ Handle the given order book data. @@ -2031,6 +2090,9 @@ cdef class Actor(Component): cpdef void _handle_instrument_response(self, DataResponse response) except *: self.handle_instrument(response.data) + cpdef void _handle_instruments_response(self, DataResponse response) except *: + self.handle_instruments(response.data) + cpdef void _handle_quote_ticks_response(self, DataResponse response) except *: self.handle_quote_ticks(response.data) diff --git a/nautilus_trader/data/client.pxd b/nautilus_trader/data/client.pxd index b63968eb9c0a..65812b592dac 100644 --- a/nautilus_trader/data/client.pxd +++ b/nautilus_trader/data/client.pxd @@ -25,6 +25,7 @@ from nautilus_trader.model.data.bar cimport BarType from nautilus_trader.model.data.base cimport DataType from nautilus_trader.model.identifiers cimport InstrumentId from nautilus_trader.model.identifiers cimport Venue +from nautilus_trader.model.instruments.base cimport Instrument cdef class DataClient(Component): @@ -126,6 +127,7 @@ cdef class MarketDataClient(DataClient): # -- REQUEST HANDLERS ----------------------------------------------------------------------------- cpdef void request_instrument(self, InstrumentId instrument_id, UUID4 correlation_id) except * + cpdef void request_instruments(self, Venue venue, UUID4 correlation_id) except * cpdef void request_quote_ticks( self, InstrumentId instrument_id, @@ -153,6 +155,8 @@ cdef class MarketDataClient(DataClient): # -- DATA HANDLERS -------------------------------------------------------------------------------- + cpdef void _handle_instrument(self, Instrument instrument, UUID4 correlation_id) except * + cpdef void _handle_instruments(self, Venue venue, list instruments, UUID4 correlation_id) except * cpdef void _handle_quote_ticks(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id) except * cpdef void _handle_trade_ticks(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id) except * cpdef void _handle_bars(self, BarType bar_type, list bars, Bar partial, UUID4 correlation_id) except * diff --git a/nautilus_trader/data/client.pyx b/nautilus_trader/data/client.pyx index 084ba9172d57..f20efe881502 100644 --- a/nautilus_trader/data/client.pyx +++ b/nautilus_trader/data/client.pyx @@ -31,6 +31,7 @@ from nautilus_trader.model.data.tick cimport QuoteTick from nautilus_trader.model.data.tick cimport TradeTick from nautilus_trader.model.identifiers cimport ClientId from nautilus_trader.model.identifiers cimport InstrumentId +from nautilus_trader.model.instruments.base cimport Instrument from nautilus_trader.msgbus.bus cimport MessageBus @@ -789,6 +790,23 @@ cdef class MarketDataClient(DataClient): f"You can implement by overriding the `request_instrument` method for this client.", ) + cpdef void request_instruments(self, Venue venue, UUID4 correlation_id) except *: + """ + Request all `Instrument` data for the given venue. + + Parameters + ---------- + venue : Venue + The venue for the request. + correlation_id : UUID4 + The correlation ID for the request. + + """ + self._log.error( # pragma: no cover + f"Cannot request all `Instrument` data: not implemented. " + f"You can implement by overriding the `request_instruments` method for this client.", + ) + cpdef void request_quote_ticks( self, InstrumentId instrument_id, @@ -887,6 +905,12 @@ cdef class MarketDataClient(DataClient): # Convenient pure Python wrappers for the data handlers. Often Python methods # involving threads or the event loop don't work with `cpdef` methods. + def _handle_instrument_py(self, Instrument instrument, UUID4 correlation_id): + self._handle_instrument(instrument, correlation_id) + + def _handle_instruments_py(self, Venue venue, list instruments, UUID4 correlation_id): + self._handle_instruments(venue, instruments, correlation_id) + def _handle_quote_ticks_py(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id): self._handle_quote_ticks(instrument_id, ticks, correlation_id) @@ -898,10 +922,36 @@ cdef class MarketDataClient(DataClient): # -- DATA HANDLERS -------------------------------------------------------------------------------- + cpdef void _handle_instrument(self, Instrument instrument, UUID4 correlation_id) except *: + cdef DataResponse response = DataResponse( + client_id=self.id, + venue=instrument.venue, + data_type=DataType(Instrument, metadata={"instrument_id": instrument.id}), + data=instrument, + correlation_id=correlation_id, + response_id=UUID4(), + ts_init=self._clock.timestamp_ns(), + ) + + self._msgbus.send(endpoint="DataEngine.response", msg=response) + + cpdef void _handle_instruments(self, Venue venue, list instruments, UUID4 correlation_id) except *: + cdef DataResponse response = DataResponse( + client_id=self.id, + venue=venue, + data_type=DataType(Instrument, metadata={"venue": venue}), + data=instruments, + correlation_id=correlation_id, + response_id=UUID4(), + ts_init=self._clock.timestamp_ns(), + ) + + self._msgbus.send(endpoint="DataEngine.response", msg=response) + cpdef void _handle_quote_ticks(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id) except *: cdef DataResponse response = DataResponse( client_id=self.id, - venue=self.venue, + venue=instrument_id.venue, data_type=DataType(QuoteTick, metadata={"instrument_id": instrument_id}), data=ticks, correlation_id=correlation_id, @@ -914,7 +964,7 @@ cdef class MarketDataClient(DataClient): cpdef void _handle_trade_ticks(self, InstrumentId instrument_id, list ticks, UUID4 correlation_id) except *: cdef DataResponse response = DataResponse( client_id=self.id, - venue=self.venue, + venue=instrument_id.venue, data_type=DataType(TradeTick, metadata={"instrument_id": instrument_id}), data=ticks, correlation_id=correlation_id, @@ -927,7 +977,7 @@ cdef class MarketDataClient(DataClient): cpdef void _handle_bars(self, BarType bar_type, list bars, Bar partial, UUID4 correlation_id) except *: cdef DataResponse response = DataResponse( client_id=self.id, - venue=self.venue, + venue=bar_type.instrument_id.venue, data_type=DataType(Bar, metadata={"bar_type": bar_type, "Partial": partial}), data=bars, correlation_id=correlation_id, diff --git a/nautilus_trader/data/engine.pyx b/nautilus_trader/data/engine.pyx index f79b04259e18..cd7261577562 100644 --- a/nautilus_trader/data/engine.pyx +++ b/nautilus_trader/data/engine.pyx @@ -1030,10 +1030,11 @@ cdef class DataEngine(Component): if request.data_type.type == Instrument: Condition.true(isinstance(client, MarketDataClient), "client was not a MarketDataClient") - client.request_instrument( - request.data_type.metadata.get("instrument_id"), - request.id - ) + instrument_id = request.data_type.metadata.get("instrument_id") + if instrument_id is None: + client.request_instruments(request.data_type.metadata.get("venue"), request.id) + else: + client.request_instrument(instrument_id, request.id) elif request.data_type.type == QuoteTick: Condition.true(isinstance(client, MarketDataClient), "client was not a MarketDataClient") client.request_quote_ticks( @@ -1159,7 +1160,10 @@ cdef class DataEngine(Component): self.response_count += 1 if response.data_type.type == Instrument: - self._handle_instruments(response.data) + if isinstance(response.data, list): + self._handle_instruments(response.data) + else: + self._handle_instrument(response.data) elif response.data_type.type == QuoteTick: self._handle_quote_ticks(response.data) elif response.data_type.type == TradeTick: diff --git a/tests/test_kit/mocks/actors.py b/tests/test_kit/mocks/actors.py index a9e93f32c4d1..11ff2d121308 100644 --- a/tests/test_kit/mocks/actors.py +++ b/tests/test_kit/mocks/actors.py @@ -65,6 +65,10 @@ def on_instrument(self, instrument) -> None: self.calls.append(inspect.currentframe().f_code.co_name) self.object_storer.store(instrument) + def on_instruments(self, instruments) -> None: + self.calls.append(inspect.currentframe().f_code.co_name) + self.object_storer.store(instruments) + def on_ticker(self, ticker): self.calls.append(inspect.currentframe().f_code.co_name) self.object_storer.store(ticker) diff --git a/tests/unit_tests/common/test_common_actor.py b/tests/unit_tests/common/test_common_actor.py index 84d04fd45d75..26201385eabe 100644 --- a/tests/unit_tests/common/test_common_actor.py +++ b/tests/unit_tests/common/test_common_actor.py @@ -909,6 +909,44 @@ def test_handle_instrument_when_running_sends_to_on_instrument(self): assert actor.calls == ["on_start", "on_instrument"] assert actor.object_storer.get_store()[0] == AUDUSD_SIM + def test_handle_instruments_when_running_sends_to_on_instruments(self): + # Arrange + actor = MockActor() + actor.register_base( + trader_id=self.trader_id, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + + actor.start() + + # Act + actor.handle_instruments([AUDUSD_SIM]) + + # Assert + assert actor.calls == ["on_start", "on_instrument"] + assert actor.object_storer.get_store()[0] == AUDUSD_SIM + + def test_handle_instruments_when_not_running_does_not_send_to_on_instrument(self): + # Arrange + actor = MockActor() + actor.register_base( + trader_id=self.trader_id, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + + # Act + actor.handle_instruments([AUDUSD_SIM]) + + # Assert + assert actor.calls == [] + assert actor.object_storer.get_store() == [] + def test_handle_ticker_when_not_running_does_not_send_to_on_quote_tick(self): # Arrange actor = MockActor() diff --git a/tests/unit_tests/data/test_data_engine.py b/tests/unit_tests/data/test_data_engine.py index d106ad610f65..6b5b326f7fa3 100644 --- a/tests/unit_tests/data/test_data_engine.py +++ b/tests/unit_tests/data/test_data_engine.py @@ -1659,4 +1659,31 @@ def test_request_instrument_reaches_client(self): # Assert assert self.data_engine.request_count == 1 assert len(handler) == 1 - assert handler[0].data == [ETHUSDT_BINANCE] + assert handler[0].data == ETHUSDT_BINANCE + + def test_request_instruments_reaches_client(self): + # Arrange + self.data_engine.register_client(self.binance_client) + + handler = [] + request = DataRequest( + client_id=None, + venue=BINANCE, + data_type=DataType( + Instrument, + metadata={ # str data type is invalid + "venue": BINANCE, + }, + ), + callback=handler.append, + request_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + # Act + self.msgbus.request(endpoint="DataEngine.request", request=request) + + # Assert + assert self.data_engine.request_count == 1 + assert len(handler) == 1 + assert handler[0].data == [BTCUSDT_BINANCE, ETHUSDT_BINANCE] From 614b1879b29d2187cd22132f3e87a66a1a1394d1 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 27 Oct 2022 19:38:17 +1100 Subject: [PATCH 06/38] Fix LiveExecutionEngine in-flight orders logging --- nautilus_trader/live/execution_engine.pyx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nautilus_trader/live/execution_engine.pyx b/nautilus_trader/live/execution_engine.pyx index 2a6636bdf47d..17b00a879412 100644 --- a/nautilus_trader/live/execution_engine.pyx +++ b/nautilus_trader/live/execution_engine.pyx @@ -317,7 +317,8 @@ cdef class LiveExecutionEngine(ExecutionEngine): self._log.info("Checking in-flight orders status...") cdef list inflight_orders = self._cache.orders_inflight() - self._log.debug("Found {len(inflight_orders) orders in-flight.}") + cdef int inflight_len = len(inflight_orders) + self._log.debug(f"Found {inflight_len} order{'' if inflight_len == 1 else 's'} in-flight.") cdef: Order order QueryOrder query From acdcbeb07ff6bfa999df79c1a7cc5f24418f0411 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 27 Oct 2022 22:19:52 +1100 Subject: [PATCH 07/38] Update for poetry 1.2.x --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index 27c4bcceab4e..4323e0727bef 100644 --- a/README.md +++ b/README.md @@ -155,9 +155,6 @@ To install the latest binary wheel from PyPI: pip install -U nautilus_trader -To install `numpy` and `scipy` on ARM architectures such as MacBook Pro M1 / Apple Silicon, [this stackoverflow thread](https://stackoverflow.com/questions/65745683/how-to-install-scipy-on-apple-silicon-arm-m1) -is useful. - ### From Source Installation from source requires the `Python.h` header file, which is included in development releases such as `python-dev`. You'll also need the latest stable `rustc` and `cargo` to compile the Rust libraries. @@ -190,7 +187,7 @@ as specified in the `pyproject.toml`. However, we highly recommend installing us git clone https://github.com/nautechsystems/nautilus_trader cd nautilus_trader - poetry install --no-dev + poetry install --only main --extras "ib redis" Refer to the [Installation Guide](https://docs.nautilustrader.io/getting_started/installation.html) for other options and further details. From 7e4aa21d73921d90d6505321afc1af215cfbf50d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 29 Oct 2022 07:17:45 +1100 Subject: [PATCH 08/38] Add set_time flag to core clock --- nautilus_core/common/src/clock.rs | 32 ++++++++++++++++++------ nautilus_core/common/tests/test_clock.rs | 4 +-- nautilus_trader/common/clock.pyx | 2 +- nautilus_trader/core/includes/common.h | 4 ++- nautilus_trader/core/rust/common.pxd | 2 +- 5 files changed, 32 insertions(+), 12 deletions(-) diff --git a/nautilus_core/common/src/clock.rs b/nautilus_core/common/src/clock.rs index 7d55db6b2a3b..a005e5bee97f 100644 --- a/nautilus_core/common/src/clock.rs +++ b/nautilus_core/common/src/clock.rs @@ -14,6 +14,7 @@ // ------------------------------------------------------------------------------------------------- use std::collections::HashMap; +use std::mem; use std::ops::{Deref, DerefMut}; use std::ptr::null; @@ -93,14 +94,17 @@ impl TestClock { } #[inline] - pub fn advance_time(&mut self, to_time_ns: Timestamp) -> Vec { + pub fn advance_time(&mut self, to_time_ns: Timestamp, set_time: bool) -> Vec { // Time should increase monotonically assert!( to_time_ns >= self.time_ns, "`to_time_ns` was < `self._time_ns`" ); - self.time_ns = to_time_ns; + if set_time { + self.time_ns = to_time_ns; + } + self.timers .iter_mut() .filter(|(_, timer)| !timer.is_expired) @@ -333,11 +337,12 @@ pub unsafe extern "C" fn test_clock_set_timer_ns( } #[no_mangle] -pub extern "C" fn test_clock_advance_time( +pub unsafe extern "C" fn test_clock_advance_time( clock: &mut CTestClock, to_time_ns: u64, + set_time: u8, ) -> Vec_TimeEvent { - let events: Vec = clock.advance_time(to_time_ns); + let events: Vec = clock.advance_time(to_time_ns, mem::transmute(set_time)); let len = events.len(); let data = match events.is_empty() { true => null() as *const TimeEvent, @@ -399,20 +404,33 @@ mod tests { let mut clock = TestClock::new(); clock.set_timer_ns(String::from("TEST_TIME1"), 1, 1, Some(3), None); - clock.advance_time(2); + clock.advance_time(2, true); assert_eq!(clock.timer_names(), ["TEST_TIME1"]); assert_eq!(clock.timer_count(), 1); } #[test] - fn test_advance_time_to_stop_time() { + fn test_advance_time_to_stop_time_with_set_time_true() { + let mut clock = TestClock::new(); + clock.set_timer_ns(String::from("TEST_TIME1"), 2, 0, Some(3), None); + + clock.advance_time(3, true); + + assert_eq!(clock.timer_names().len(), 1); + assert_eq!(clock.timer_count(), 1); + assert_eq!(clock.time_ns, 3); + } + + #[test] + fn test_advance_time_to_stop_time_with_set_time_false() { let mut clock = TestClock::new(); clock.set_timer_ns(String::from("TEST_TIME1"), 2, 0, Some(3), None); - clock.advance_time(3); + clock.advance_time(3, false); assert_eq!(clock.timer_names().len(), 1); assert_eq!(clock.timer_count(), 1); + assert_eq!(clock.time_ns, 0); } } diff --git a/nautilus_core/common/tests/test_clock.rs b/nautilus_core/common/tests/test_clock.rs index 4451050ee9a1..4841e912397e 100644 --- a/nautilus_core/common/tests/test_clock.rs +++ b/nautilus_core/common/tests/test_clock.rs @@ -35,7 +35,7 @@ fn test_clock_advance() { assert_eq!(clock.timers.len(), 1); assert_eq!(clock.timers.keys().next().unwrap().as_str(), timer_name); - let events = clock.advance_time(3_000); + let events = clock.advance_time(3_000, true); assert_eq!(clock.timers.values().next().unwrap().is_expired, true); assert_eq!(events.len(), 1); @@ -58,6 +58,6 @@ fn test_clock_event_callback() { test_clock_set_time_alert_ns(&mut test_clock, name, 2_000); } - let events = test_clock.advance_time(3_000); + let events = test_clock.advance_time(3_000, true); assert_eq!(events.len(), 1); // TODO } diff --git a/nautilus_trader/common/clock.pyx b/nautilus_trader/common/clock.pyx index 94590cad1016..4819d8beb751 100644 --- a/nautilus_trader/common/clock.pyx +++ b/nautilus_trader/common/clock.pyx @@ -528,7 +528,7 @@ cdef class TestClock(Clock): # Ensure monotonic Condition.true(to_time_ns >= test_clock_time_ns(&self._mem), "to_time_ns was < time_ns") - cdef Vec_TimeEvent raw_events = test_clock_advance_time(&self._mem, to_time_ns) + cdef Vec_TimeEvent raw_events = test_clock_advance_time(&self._mem, to_time_ns, True) cdef list event_handlers = [] cdef: diff --git a/nautilus_trader/core/includes/common.h b/nautilus_trader/core/includes/common.h index 9d1f13d20c39..7a3dd6f6a751 100644 --- a/nautilus_trader/core/includes/common.h +++ b/nautilus_trader/core/includes/common.h @@ -109,7 +109,9 @@ void test_clock_set_timer_ns(struct CTestClock *clock, uint64_t start_time_ns, uint64_t stop_time_ns); -struct Vec_TimeEvent test_clock_advance_time(struct CTestClock *clock, uint64_t to_time_ns); +struct Vec_TimeEvent test_clock_advance_time(struct CTestClock *clock, + uint64_t to_time_ns, + uint8_t set_time); void vec_time_events_drop(struct Vec_TimeEvent v); diff --git a/nautilus_trader/core/rust/common.pxd b/nautilus_trader/core/rust/common.pxd index 215f5b890331..648b0aa11c9a 100644 --- a/nautilus_trader/core/rust/common.pxd +++ b/nautilus_trader/core/rust/common.pxd @@ -88,7 +88,7 @@ cdef extern from "../includes/common.h": uint64_t start_time_ns, uint64_t stop_time_ns); - Vec_TimeEvent test_clock_advance_time(CTestClock *clock, uint64_t to_time_ns); + Vec_TimeEvent test_clock_advance_time(CTestClock *clock, uint64_t to_time_ns, uint8_t set_time); void vec_time_events_drop(Vec_TimeEvent v); From 0e62d2124e088fe18980ea970e96ac7944df8e70 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 29 Oct 2022 08:02:33 +1100 Subject: [PATCH 09/38] Update dependencies --- nautilus_core/Cargo.lock | 8 ++--- poetry.lock | 67 ++++++++++++++++++++++------------------ pyproject.toml | 2 +- 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index 09a1ad7986c6..d36d8d758658 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -627,9 +627,9 @@ checksum = "71a816c97c42258aa5834d07590b718b4c9a598944cd39a52dc25b351185d678" [[package]] name = "iana-time-zone" -version = "0.1.51" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a6ef98976b22b3b7f2f3a806f858cb862044cfa66805aa3ad84cb3d3b785ed" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -926,9 +926,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "os_str_bytes" -version = "6.3.0" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" [[package]] name = "parking_lot" diff --git a/poetry.lock b/poetry.lock index 35adc6607fc3..c34db692d1a4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -281,7 +281,7 @@ numpy = "*" [[package]] name = "exceptiongroup" -version = "1.0.0rc9" +version = "1.0.0" description = "Backport of PEP 654 (exception groups)" category = "dev" optional = false @@ -382,7 +382,7 @@ nest-asyncio = "*" [[package]] name = "identify" -version = "2.5.7" +version = "2.5.8" description = "File identification library for Python" category = "dev" optional = false @@ -545,7 +545,7 @@ python-versions = ">=3.7" [[package]] name = "msgspec" -version = "0.9.0" +version = "0.9.1" description = "A fast and friendly JSON/MessagePack library, with optional schema validation" category = "main" optional = false @@ -1457,7 +1457,7 @@ redis = ["hiredis", "redis"] [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "9fd4d7884b9f3e6fca16bd7d40b67a554426d6ce832cfd1664a5b30b5402d187" +content-hash = "cbc14b2b9487d213cfda5a56274a41a6f47b84cf5e91c446084212079f821c87" [metadata.files] aiodns = [ @@ -1813,8 +1813,8 @@ eventkit = [ {file = "eventkit-1.0.0.tar.gz", hash = "sha256:c9c4bb8a9685e4131e845882512a630d6a57acee148f38af286562a76873e4a9"}, ] exceptiongroup = [ - {file = "exceptiongroup-1.0.0rc9-py3-none-any.whl", hash = "sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337"}, - {file = "exceptiongroup-1.0.0rc9.tar.gz", hash = "sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96"}, + {file = "exceptiongroup-1.0.0-py3-none-any.whl", hash = "sha256:2ac84b496be68464a2da60da518af3785fff8b7ec0d090a581604bc870bdee41"}, + {file = "exceptiongroup-1.0.0.tar.gz", hash = "sha256:affbabf13fb6e98988c38d9c5650e701569fe3c1de3233cfb61c5f33774690ad"}, ] execnet = [ {file = "execnet-1.9.0-py2.py3-none-any.whl", hash = "sha256:a295f7cc774947aac58dde7fdc85f4aa00c42adf5d8f5468fc630c1acf30a142"}, @@ -1956,8 +1956,8 @@ ib-insync = [ {file = "ib_insync-0.9.71.tar.gz", hash = "sha256:c1be16a8f23640ddd239f776bd025e5e0a298491b138f05da5b3391f6a6f9bf5"}, ] identify = [ - {file = "identify-2.5.7-py2.py3-none-any.whl", hash = "sha256:7a67b2a6208d390fd86fd04fb3def94a3a8b7f0bcbd1d1fcd6736f4defe26390"}, - {file = "identify-2.5.7.tar.gz", hash = "sha256:5b8fd1e843a6d4bf10685dd31f4520a7f1c7d0e14e9bc5d34b1d6f111cabc011"}, + {file = "identify-2.5.8-py2.py3-none-any.whl", hash = "sha256:48b7925fe122720088aeb7a6c34f17b27e706b72c61070f27fe3789094233440"}, + {file = "identify-2.5.8.tar.gz", hash = "sha256:7a214a10313b9489a0d61467db2856ae8d0b8306fc923e03a9effa53d8aedc58"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, @@ -2114,28 +2114,35 @@ mdurl = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] msgspec = [ - {file = "msgspec-0.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:993313a1e1097bb0b59abfe376cc8a47cdfb7bc1bb421aa0b743e8dccf2b993f"}, - {file = "msgspec-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37add8b096b88ced9349797c674e8b36a4be9c8fd3cb1694f0d93f4c757c8dd3"}, - {file = "msgspec-0.9.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe3add134d964132d24d95ddbba4373d01f2b8ea750334797d3333e9563212d8"}, - {file = "msgspec-0.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a427a75f8e704996624d3a5f64482665f0d5daec9f8d9569c10071b615374614"}, - {file = "msgspec-0.9.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5dd2f656ad972b55308ccc83b6f66ef05c059b0b7d02b29edb3408660fb4821b"}, - {file = "msgspec-0.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:15a7d24f72730cb67a227e4f19af21531f47b9c50a730ad6a5c49ef0d2ae448a"}, - {file = "msgspec-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:cd8ae3f54a8eb3a79e6a36f580c21dedaf64b9a0d9d193fa2348f73c535bbd5c"}, - {file = "msgspec-0.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f5712fa79a412f6a3b88fbc43495d65f6cd4eaab06d79c0cf2eb3662c5fc71b8"}, - {file = "msgspec-0.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7454466c418cc8faa96792367c56917899baeeda59524701b43392eeab738fbd"}, - {file = "msgspec-0.9.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7b27a3acb17266507f51f250e71e9d88adfa09d4694905324c55b286484673d"}, - {file = "msgspec-0.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1e4817dd66cefe38654adb576736ab57116b960a1ad40ab03c18f96373d6864"}, - {file = "msgspec-0.9.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a29c2a23e814b1a15ac91e400bea2b17e1cf86308a7a169f71607ebb4f97593b"}, - {file = "msgspec-0.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6306028e42e38158a2c3cc73588a1c7571bfa631a47c7f7896166a7e073509f1"}, - {file = "msgspec-0.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:bfccffb036901d97ee236262f46b0f152bc37e603180e748e08bee74d844a34f"}, - {file = "msgspec-0.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea3882f353ee7e7b59c9c64a329f9b912c9ac60bd0a8279418c7ae0ca1c671bf"}, - {file = "msgspec-0.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ba14c9c06a18e736c50b5184929b61b0ab8aece72519c08d01e9a350b2eb454e"}, - {file = "msgspec-0.9.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4ca552668d6a251aaf6f06aa85eef05c95fa4e7c6c4738048b25a260de5d075"}, - {file = "msgspec-0.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff9c8baee187a26383c267449da28799a0aa572f3d9b5aa4c1fed62b0fa8dc81"}, - {file = "msgspec-0.9.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8dbd0fee64d4412ad171f24a2e0cc054bc4f0a7c956aa2dad58a13eb3649ff03"}, - {file = "msgspec-0.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:42fae0339c330385ee894fa1524c1587677232fb8c124216e5e80eda083d6e9c"}, - {file = "msgspec-0.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:c0fcb9f390d8e0f000024c6946d383e05164e86a13fab439dbfc806b6e958422"}, - {file = "msgspec-0.9.0.tar.gz", hash = "sha256:64b00532f5f7aaeba589c36944c271ef7f6bddcc62519e8c03066e4f4d87471d"}, + {file = "msgspec-0.9.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22f9d68607a9c1d4c9770046f7c22f97c45c57e5a6fcc8d97715af94071f384c"}, + {file = "msgspec-0.9.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f4ad9182ae2597fe5507d7ac666cdf568fa2bb9774e03d03ffafed5f58503292"}, + {file = "msgspec-0.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1cc6d427a74ffd396d9f2a1a6a5a091337b77d0757a11157408e395cc7c5246"}, + {file = "msgspec-0.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3885b569eb78c7cee3c2ab6c312a477fdd2fdc1e395d7fecfa5db5c17d689df1"}, + {file = "msgspec-0.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b1d1fe0ca534cf60dc7b98d268588527c12ffd63d8c55c8bddaf35c43830590a"}, + {file = "msgspec-0.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:172e14a783d3c0580c88e6a4506b64a1ea397d8c614e1a169ef0461083ea97ed"}, + {file = "msgspec-0.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:851dc6d686f7c876fe895c4921aa9887aa1f303a593f16c4816811f9f99d56e2"}, + {file = "msgspec-0.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d550937a65a455de5b1fcb6e9eb60cee349cf421f31234195b51ca286e607a0d"}, + {file = "msgspec-0.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9516612c285535effdca6d9b74c2f23f1947711b86e93e4a6f99d307474de580"}, + {file = "msgspec-0.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b249dc07bd934339420fa422f674e2ea10794e21a8ce5b6c8bd5d8fa19481342"}, + {file = "msgspec-0.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb2d0b74b08115c7c4b06b5e373560970afcea06b810f47306e247a529d5ac25"}, + {file = "msgspec-0.9.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c737920ee52e3662321ccbcc00419e411608f5feb29a1007904b7885b26ab9a4"}, + {file = "msgspec-0.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:10c7d70564d35304f7f423ec6a894e5a029f90c9691b19ed18a3e10f0bf40fbc"}, + {file = "msgspec-0.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:0799a8b63be00c58c55325e9974effac3e76dff416d8f7e7b867db09ed2c978b"}, + {file = "msgspec-0.9.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:84b344ada028426bfcca9015aa9379f742435cd1633316fbaf0edde7199fdb8c"}, + {file = "msgspec-0.9.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c867df64eb80b723c8b9ee7bd5fc21664d31eeb64002af4747f46a64a71e5913"}, + {file = "msgspec-0.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90026c2fa34ddaa79d56dcde0d45ca0d22327730e2b130145096fe9c8f9e5a06"}, + {file = "msgspec-0.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2da67a1f4c7528054f1697b551289f9bd400704a78a1c4d53f4996dfdf79e7f"}, + {file = "msgspec-0.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6e652314394f027e34d0fd33aebefc3361ca2a49dfd3651d259703ef4965e1a0"}, + {file = "msgspec-0.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a115c09c10df17516198896877f3044a7fd773f825a4d9a07d2579a861f00356"}, + {file = "msgspec-0.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:c841a0b534898880e3e8c3b167670b962f35451693194d5414234904d5479816"}, + {file = "msgspec-0.9.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5fa772cf19b5f878554555b17c1e45830f6b0b0d838582d72953d2f24beb4d49"}, + {file = "msgspec-0.9.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7326e071720284ac5e0017697bf01d8f5e14309fbeef34def378207c53bcdc2"}, + {file = "msgspec-0.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:393f4eba17457882aa646b5c97a8ff6a7f9c85a77175edd4f927c4efd9663933"}, + {file = "msgspec-0.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5884f17d7cc8500616f0f0919b872b95fbefb90188ac3bc1b752f2d9dad20b05"}, + {file = "msgspec-0.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7972addcdd184c0560cc729182771d78e6f2bb7abc85368f632c6b69f90d5b6d"}, + {file = "msgspec-0.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:472d9931d09c92afa89b3637d6ca82f7fd513b428da1939a1ce3b51ad64b3f6d"}, + {file = "msgspec-0.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:deb1a5eb18f7d457b4d4ee8086dc89030ed511c7269e0cbb6550b5e80f3453d2"}, + {file = "msgspec-0.9.1.tar.gz", hash = "sha256:a792b0ca37b467be942675d3865847370f56022a83a42e4464e3505d276ab1cd"}, ] multidict = [ {file = "multidict-6.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0b9e95a740109c6047602f4db4da9949e6c5945cefbad34a1299775ddc9a62e2"}, diff --git a/pyproject.toml b/pyproject.toml index 516d1cfd63ea..0d366c7e54df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ click = "^8.1.3" cloudpickle = "^2.2.0" frozendict = "^2.3.2" fsspec = "^2022.10.0" -msgspec = "^0.9.0" +msgspec = "^0.9.1" numpy = "^1.23.3" pandas = "^1.5.1" psutil = "^5.9.3" From 6c065ef4cb63c9c409666e789f63c04ccc7e0f5d Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 29 Oct 2022 08:16:58 +1100 Subject: [PATCH 10/38] Fix clippy warnings (as errors) --- nautilus_core/common/src/clock.rs | 5 +++-- nautilus_trader/core/includes/common.h | 4 ++++ nautilus_trader/core/rust/common.pxd | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/nautilus_core/common/src/clock.rs b/nautilus_core/common/src/clock.rs index a005e5bee97f..a6f1469815ff 100644 --- a/nautilus_core/common/src/clock.rs +++ b/nautilus_core/common/src/clock.rs @@ -14,7 +14,6 @@ // ------------------------------------------------------------------------------------------------- use std::collections::HashMap; -use std::mem; use std::ops::{Deref, DerefMut}; use std::ptr::null; @@ -336,13 +335,15 @@ pub unsafe extern "C" fn test_clock_set_timer_ns( clock.set_timer_ns(name, interval_ns, start_time_ns, stop_time_ns, None); } +/// # Safety +/// - Assumes `set_time` is a correct `uint8_t` of either 0 or 1. #[no_mangle] pub unsafe extern "C" fn test_clock_advance_time( clock: &mut CTestClock, to_time_ns: u64, set_time: u8, ) -> Vec_TimeEvent { - let events: Vec = clock.advance_time(to_time_ns, mem::transmute(set_time)); + let events: Vec = clock.advance_time(to_time_ns, set_time != 0); let len = events.len(); let data = match events.is_empty() { true => null() as *const TimeEvent, diff --git a/nautilus_trader/core/includes/common.h b/nautilus_trader/core/includes/common.h index 7a3dd6f6a751..b69941ccbde2 100644 --- a/nautilus_trader/core/includes/common.h +++ b/nautilus_trader/core/includes/common.h @@ -109,6 +109,10 @@ void test_clock_set_timer_ns(struct CTestClock *clock, uint64_t start_time_ns, uint64_t stop_time_ns); +/** + * # Safety + * - Assumes `set_time` is a correct `uint8_t` of either 0 or 1. + */ struct Vec_TimeEvent test_clock_advance_time(struct CTestClock *clock, uint64_t to_time_ns, uint8_t set_time); diff --git a/nautilus_trader/core/rust/common.pxd b/nautilus_trader/core/rust/common.pxd index 648b0aa11c9a..8dfed2c7041b 100644 --- a/nautilus_trader/core/rust/common.pxd +++ b/nautilus_trader/core/rust/common.pxd @@ -88,6 +88,8 @@ cdef extern from "../includes/common.h": uint64_t start_time_ns, uint64_t stop_time_ns); + # # Safety + # - Assumes `set_time` is a correct `uint8_t` of either 0 or 1. Vec_TimeEvent test_clock_advance_time(CTestClock *clock, uint64_t to_time_ns, uint8_t set_time); void vec_time_events_drop(Vec_TimeEvent v); From 812eaedbf63ba9c520f6d430bc858fc869f47460 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 29 Oct 2022 10:17:27 +1100 Subject: [PATCH 11/38] Add `Symbol` and `Venue` tests --- .../model/test_model_identifiers.py | 59 +++++++++++++++++-- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/tests/unit_tests/model/test_model_identifiers.py b/tests/unit_tests/model/test_model_identifiers.py index 8604d0cd22e5..3745c9644e7c 100644 --- a/tests/unit_tests/model/test_model_identifiers.py +++ b/tests/unit_tests/model/test_model_identifiers.py @@ -113,19 +113,57 @@ def test_account_identifier(self): assert account_id1 == AccountId("SIM-02851908") +class TestSymbol: + def test_symbol_equality(self): + # Arrange + symbol1 = Symbol("AUD/USD") + symbol2 = Symbol("ETH/USD") + symbol3 = Symbol("AUD/USD") + + # Act, Assert + assert symbol1 == symbol1 + assert symbol1 != symbol2 + assert symbol1 == symbol3 + + def test_symbol_str(self): + # Arrange + symbol = Symbol("AUD/USD") + + # Act, Assert + assert str(symbol) == "AUD/USD" + + def test_symbol_repr(self): + # Arrange + symbol = Symbol("AUD/USD") + + # Act, Assert + assert repr(symbol) == "Symbol('AUD/USD')" + + def test_symbol_pickling(self): + # Arrange + symbol = Symbol("AUD/USD") + + # Act + pickled = pickle.dumps(symbol) + unpickled = pickle.loads(pickled) # noqa S301 (pickle is safe here) + + # Act, Assert + assert symbol == unpickled + + class TestVenue: def test_venue_equality(self): # Arrange - venue1 = InstrumentId(Symbol("AUD/USD"), Venue("SIM")) - venue2 = InstrumentId(Symbol("AUD/USD"), Venue("IDEALPRO")) - venue3 = InstrumentId(Symbol("GBP/USD"), Venue("SIM")) + venue1 = Venue("SIM") + venue2 = Venue("IDEALPRO") + venue3 = Venue("SIM") # Act, Assert assert venue1 == venue1 assert venue1 != venue2 - assert venue1 != venue3 + assert venue1 == venue3 - def test_instrument_id_str(self): + def test_venue_str(self): # Arrange venue = Venue("NYMEX") @@ -139,6 +177,17 @@ def test_venue_repr(self): # Act, Assert assert repr(venue) == "Venue('NYMEX')" + def test_venue_pickling(self): + # Arrange + venue = Venue("NYMEX") + + # Act + pickled = pickle.dumps(venue) + unpickled = pickle.loads(pickled) # noqa S301 (pickle is safe here) + + # Act, Assert + assert venue == unpickled + class TestInstrumentId: def test_instrument_id_equality(self): From 61de1a68cf47b5765728a26432e20fc20f8140d9 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 29 Oct 2022 10:18:25 +1100 Subject: [PATCH 12/38] Improve test separation --- tests/unit_tests/model/test_model_tick.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/unit_tests/model/test_model_tick.py b/tests/unit_tests/model/test_model_tick.py index 51297a47cb60..3be5be262ea8 100644 --- a/tests/unit_tests/model/test_model_tick.py +++ b/tests/unit_tests/model/test_model_tick.py @@ -22,7 +22,10 @@ from nautilus_trader.model.data.tick import TradeTick from nautilus_trader.model.enums import AggressorSide from nautilus_trader.model.enums import PriceType +from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.identifiers import Symbol from nautilus_trader.model.identifiers import TradeId +from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.objects import Price from nautilus_trader.model.objects import Quantity @@ -37,8 +40,10 @@ def test_fully_qualified_name(self): def test_tick_hash_str_and_repr(self): # Arrange + instrument_id = InstrumentId(Symbol("AUD/USD"), Venue("SIM")) + tick = QuoteTick( - instrument_id=AUDUSD_SIM.id, + instrument_id=instrument_id, bid=Price.from_str("1.00000"), ask=Price.from_str("1.00001"), bid_size=Quantity.from_int(1), From 0ef6c342d0f6259c178061ca7c8f8aad943b3f81 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 29 Oct 2022 10:22:52 +1100 Subject: [PATCH 13/38] Improve `Money` memory management --- nautilus_trader/model/objects.pxd | 4 +- nautilus_trader/model/objects.pyx | 40 +++++++++++-------- .../model/test_model_objects_money.py | 19 +++++++++ 3 files changed, 43 insertions(+), 20 deletions(-) diff --git a/nautilus_trader/model/objects.pxd b/nautilus_trader/model/objects.pxd index 266f41a9ee72..c246a0780763 100644 --- a/nautilus_trader/model/objects.pxd +++ b/nautilus_trader/model/objects.pxd @@ -115,9 +115,7 @@ cdef class Price: cdef class Money: cdef Money_t _mem - cdef readonly Currency currency - """The currency of the money.\n\n:returns: `Currency`""" - + cdef str currency_code_c(self) cdef bint is_zero(self) except * cdef bint is_negative(self) except * cdef bint is_positive(self) except * diff --git a/nautilus_trader/model/objects.pyx b/nautilus_trader/model/objects.pyx index cad778fd6452..9ae101b60d12 100644 --- a/nautilus_trader/model/objects.pyx +++ b/nautilus_trader/model/objects.pyx @@ -39,6 +39,8 @@ from nautilus_trader.core.rust.model cimport PRICE_MIN as RUST_PRICE_MIN from nautilus_trader.core.rust.model cimport QUANTITY_MAX as RUST_QUANTITY_MAX from nautilus_trader.core.rust.model cimport QUANTITY_MIN as RUST_QUANTITY_MIN from nautilus_trader.core.rust.model cimport Currency_t +from nautilus_trader.core.rust.model cimport currency_code_to_pystr +from nautilus_trader.core.rust.model cimport currency_eq from nautilus_trader.core.rust.model cimport money_free from nautilus_trader.core.rust.model cimport money_from_raw from nautilus_trader.core.rust.model cimport money_new @@ -839,37 +841,35 @@ cdef class Money: ) self._mem = money_new(value_f64, currency._mem) # borrows wrapped `currency` - self.currency = currency def __del__(self) -> None: money_free(self._mem) # `self._mem` moved to Rust (then dropped) def __getstate__(self): - return self._mem.raw, self.currency + return self._mem.raw, self.currency_code_c() def __setstate__(self, state): - cdef Currency currency = state[1] + cdef Currency currency = Currency.from_str_c(state[1]) self._mem = money_from_raw(state[0], currency._mem) - self.currency = currency def __eq__(self, Money other) -> bool: - Condition.equal(self.currency, other.currency, "currency", "other.currency") + Condition.true(currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency") return self._mem.raw == other.raw_int64_c() def __lt__(self, Money other) -> bool: - Condition.equal(self.currency, other.currency, "currency", "other.currency") + Condition.true(currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency") return self._mem.raw < other.raw_int64_c() def __le__(self, Money other) -> bool: - Condition.equal(self.currency, other.currency, "currency", "other.currency") + Condition.true(currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency") return self._mem.raw <= other.raw_int64_c() def __gt__(self, Money other) -> bool: - Condition.equal(self.currency, other.currency, "currency", "other.currency") + Condition.true(currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency") return self._mem.raw > other.raw_int64_c() def __ge__(self, Money other) -> bool: - Condition.equal(self.currency, other.currency, "currency", "other.currency") + Condition.true(currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency") return self._mem.raw >= other.raw_int64_c() def __add__(a, b) -> Union[decimal.Decimal, float]: @@ -951,13 +951,20 @@ cdef class Money: return int(self.as_f64_c()) def __hash__(self) -> int: - return hash((self._mem.raw, self.currency.code)) + return hash((self._mem.raw, self.currency_code_c())) def __str__(self) -> str: return f"{self._mem.raw / FIXED_SCALAR:.{self._mem.currency.precision}f}" def __repr__(self) -> str: - return f"{type(self).__name__}('{str(self)}', {self.currency.code})" + return f"{type(self).__name__}('{str(self)}', {self.currency_code_c()})" + + @property + def currency(self) -> Currency: + return Currency.from_str_c(self.currency_code_c()) + + cdef str currency_code_c(self): + return currency_code_to_pystr(&self._mem.currency) cdef bint is_zero(self) except *: return self._mem.raw == 0 @@ -969,21 +976,21 @@ cdef class Money: return self._mem.raw > 0 cdef Money add(self, Money other): - assert self.currency == other.currency, "other money currency was not equal" # design-time check + assert currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency" # design-time check cdef int64_t raw = self._mem.raw + other.raw_int64_c() return Money.from_raw_c(raw, self.currency) cdef Money sub(self, Money other): - assert self.currency == other.currency, "other money currency was not equal" # design-time check + assert currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency" # design-time check cdef int64_t raw = self._mem.raw - other.raw_int64_c() return Money.from_raw_c(raw, self.currency) cdef void add_assign(self, Money other) except *: - assert self.currency == other.currency, "other money currency was not equal" # design-time check + assert currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency" # design-time check self._mem.raw += other.raw_int64_c() cdef void sub_assign(self, Money other) except *: - assert self.currency == other.currency, "other money currency was not equal" # design-time check + assert currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency" # design-time check self._mem.raw -= other.raw_int64_c() cdef int64_t raw_int64_c(self): @@ -1004,7 +1011,6 @@ cdef class Money: cdef Money from_raw_c(uint64_t raw, Currency currency): cdef Money money = Money.__new__(Money) money._mem = money_from_raw(raw, currency._mem) - money.currency = currency return money @staticmethod @@ -1091,7 +1097,7 @@ cdef class Money: str """ - return f"{self.as_f64_c():,.{self._mem.currency.precision}f} {self.currency.code}".replace(",", "_") + return f"{self.as_f64_c():,.{self._mem.currency.precision}f} {self.currency_code_c()}".replace(",", "_") cdef class AccountBalance: diff --git a/tests/unit_tests/model/test_model_objects_money.py b/tests/unit_tests/model/test_model_objects_money.py index d29d3be9e3d2..349077a1792d 100644 --- a/tests/unit_tests/model/test_model_objects_money.py +++ b/tests/unit_tests/model/test_model_objects_money.py @@ -102,6 +102,25 @@ def test_initialized_with_many_decimals_rounds_to_currency_precision(self): assert "1_000.33 USD" == result1.to_str() assert "5_005.56 USD" == result2.to_str() + def test_equality_with_different_currencies_raises_value_error(self): + # Arrange + money1 = Money(1, USD) + money2 = Money(1, AUD) + + # Act, Assert + with pytest.raises(ValueError): + assert money1 != money2 + + def test_equality(self): + # Arrange + money1 = Money(1, USD) + money2 = Money(1, USD) + money3 = Money(2, USD) + + # Act, Assert + assert money1 == money2 + assert money1 != money3 + def test_hash(self): # Arrange money0 = Money(0, USD) From 99dab06106c4278d310712d7f403106198cabd53 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 29 Oct 2022 14:37:41 +1100 Subject: [PATCH 14/38] Upgrade Cython to 3.0.0a11 Comment `_free` functions which are currently not working --- .../adapters/binance/common/types.py | 1 + nautilus_trader/common/timer.pyx | 4 +- nautilus_trader/model/currency.pyx | 4 +- nautilus_trader/model/data/bar.pyx | 8 +- nautilus_trader/model/data/tick.pyx | 8 +- nautilus_trader/model/identifiers.pyx | 45 +++++----- nautilus_trader/model/objects.pyx | 10 +-- poetry.lock | 82 +++++++++---------- pyproject.toml | 4 +- 9 files changed, 91 insertions(+), 75 deletions(-) diff --git a/nautilus_trader/adapters/binance/common/types.py b/nautilus_trader/adapters/binance/common/types.py index cbd3284885fb..715c9b635c89 100644 --- a/nautilus_trader/adapters/binance/common/types.py +++ b/nautilus_trader/adapters/binance/common/types.py @@ -97,6 +97,7 @@ def __init__( self.taker_sell_quote_volume = self.quote_volume - self.taker_buy_quote_volume def __del__(self) -> None: + # TODO(cs): Investigate dealloc (not currently being freed) pass # Avoid double free (segmentation fault) def __getstate__(self): diff --git a/nautilus_trader/common/timer.pyx b/nautilus_trader/common/timer.pyx index 8849048d96f1..0cf398b456ef 100644 --- a/nautilus_trader/common/timer.pyx +++ b/nautilus_trader/common/timer.pyx @@ -64,7 +64,9 @@ cdef class TimeEvent(Event): ) def __del__(self) -> None: - time_event_free(self._mem) # `self._mem` moved to Rust (then dropped) + # TODO(cs): Investigate dealloc (not currently being freed) + # time_event_free(self._mem) # `self._mem` moved to Rust (then dropped) + pass cdef str to_str(self): return time_event_name(&self._mem) diff --git a/nautilus_trader/model/currency.pyx b/nautilus_trader/model/currency.pyx index fc332fa95432..f7885f628b67 100644 --- a/nautilus_trader/model/currency.pyx +++ b/nautilus_trader/model/currency.pyx @@ -82,7 +82,9 @@ cdef class Currency: ) def __del__(self) -> None: - currency_free(self._mem) # `self._mem` moved to Rust (then dropped) + # TODO(cs): Investigate dealloc (not currently being freed) + # currency_free(self._mem) # `self._mem` moved to Rust (then dropped) + pass def __getstate__(self): return ( diff --git a/nautilus_trader/model/data/bar.pyx b/nautilus_trader/model/data/bar.pyx index 3a612bfdbc57..c1a95886f608 100644 --- a/nautilus_trader/model/data/bar.pyx +++ b/nautilus_trader/model/data/bar.pyx @@ -433,7 +433,9 @@ cdef class BarType: ) def __del__(self) -> None: - bar_type_free(self._mem) # `self._mem` moved to Rust (then dropped) + # TODO(cs): Investigate dealloc (not currently being freed) + # bar_type_free(self._mem) # `self._mem` moved to Rust (then dropped) + pass cdef str to_str(self): return bar_type_to_pystr(&self._mem) @@ -681,7 +683,9 @@ cdef class Bar(Data): self.ts_init = state[14] def __del__(self) -> None: - bar_free(self._mem) # `self._mem` moved to Rust (then dropped) + # TODO(cs): Investigate dealloc (not currently being freed) + # bar_free(self._mem) # `self._mem` moved to Rust (then dropped) + pass def __eq__(self, Bar other) -> bool: return bar_eq(&self._mem, &other._mem) diff --git a/nautilus_trader/model/data/tick.pyx b/nautilus_trader/model/data/tick.pyx index 3d1b876f6d15..4b96187b78df 100644 --- a/nautilus_trader/model/data/tick.pyx +++ b/nautilus_trader/model/data/tick.pyx @@ -98,7 +98,9 @@ cdef class QuoteTick(Data): ) def __del__(self) -> None: - quote_tick_free(self._mem) # `self._mem` moved to Rust (then dropped) + # TODO(cs): Investigate dealloc (not currently being freed) + # quote_tick_free(self._mem) # `self._mem` moved to Rust (then dropped) + pass def __getstate__(self): return ( @@ -473,7 +475,9 @@ cdef class TradeTick(Data): ) def __del__(self) -> None: - trade_tick_free(self._mem) # `self._mem` moved to Rust (then dropped) + # TODO(cs): Investigate dealloc (not currently being freed) + # trade_tick_free(self._mem) # `self._mem` moved to Rust (then dropped) + pass def __getstate__(self): return ( diff --git a/nautilus_trader/model/identifiers.pyx b/nautilus_trader/model/identifiers.pyx index 61824a184fae..f86b96b7a0e0 100644 --- a/nautilus_trader/model/identifiers.pyx +++ b/nautilus_trader/model/identifiers.pyx @@ -133,11 +133,13 @@ cdef class Symbol(Identifier): https://en.wikipedia.org/wiki/Ticker_symbol """ - def __init__(self, str value): + def __init__(self, str value not None): self._mem = symbol_new(value) def __del__(self) -> None: - symbol_free(self._mem) # `self._mem` moved to Rust (then dropped) + # TODO(cs): Investigate dealloc (not currently being freed) + # symbol_free(self._mem) # `self._mem` moved to Rust (then dropped) + pass def __getstate__(self): return self.to_str() @@ -169,11 +171,13 @@ cdef class Venue(Identifier): - Panics at runtime if `value` is not a valid string. """ - def __init__(self, str name): + def __init__(self, str name not None): self._mem = venue_new(name) def __del__(self) -> None: - venue_free(self._mem) # `self._mem` moved to Rust (then dropped) + # TODO(cs): Investigate dealloc (not currently being freed) + # venue_free(self._mem) # `self._mem` moved to Rust (then dropped) + pass def __getstate__(self): return self.to_str() @@ -206,9 +210,6 @@ cdef class InstrumentId(Identifier): """ def __init__(self, Symbol symbol not None, Venue venue not None): - Condition.not_none(symbol, "symbol") - Condition.not_none(venue, "venue") - self._mem = instrument_id_new( symbol, venue, @@ -217,7 +218,9 @@ cdef class InstrumentId(Identifier): self.venue = venue def __del__(self) -> None: - instrument_id_free(self._mem) # `self._mem` moved to Rust (then dropped) + # TODO(cs): Investigate dealloc (not currently being freed) + # instrument_id_free(self._mem) # `self._mem` moved to Rust (then dropped) + pass def __getstate__(self): return ( @@ -316,11 +319,13 @@ cdef class ComponentId(Identifier): - Panics at runtime if `value` is not a valid string. """ - def __init__(self, str value): + def __init__(self, str value not None): self._mem = component_id_new(value) def __del__(self) -> None: - component_id_free(self._mem) # `self._mem` moved to Rust (then dropped) + # TODO(cs): Investigate dealloc (not currently being freed) + # component_id_free(self._mem) # `self._mem` moved to Rust (then dropped) + pass def __getstate__(self): return self.to_str() @@ -358,7 +363,7 @@ cdef class ClientId(ComponentId): - Panics at runtime if `value` is not a valid string. """ - def __init__(self, str value): + def __init__(self, str value not None): super().__init__(value) @@ -383,7 +388,7 @@ cdef class TraderId(ComponentId): - Panics at runtime if `value` is not a valid string containing a hyphen. """ - def __init__(self, str value): + def __init__(self, str value not None): super().__init__(value) cpdef str get_tag(self): @@ -476,7 +481,7 @@ cdef class AccountId(Identifier): - Panics at runtime if `value` is not a valid string containing a hyphen. """ - def __init__(self, str value): + def __init__(self, str value not None): self._mem = account_id_new(value) def __del__(self) -> None: @@ -524,7 +529,7 @@ cdef class ClientOrderId(Identifier): - Panics at runtime if `value` is not a valid string. """ - def __init__(self, str value): + def __init__(self, str value not None): self._mem = client_order_id_new(value) def __del__(self) -> None: @@ -560,7 +565,7 @@ cdef class VenueOrderId(Identifier): - Panics at runtime if `value` is not a valid string. """ - def __init__(self, str value): + def __init__(self, str value not None): self._mem = venue_order_id_new(value) def __del__(self) -> None: @@ -596,7 +601,7 @@ cdef class OrderListId(Identifier): - Panics at runtime if `value` is not a valid string. """ - def __init__(self, str value): + def __init__(self, str value not None): self._mem = order_list_id_new(value) def __del__(self) -> None: @@ -632,7 +637,7 @@ cdef class PositionId(Identifier): - Panics at runtime if `value` is not a valid string. """ - def __init__(self, str value): + def __init__(self, str value not None): self._mem = position_id_new(value) def __del__(self) -> None: @@ -686,11 +691,13 @@ cdef class TradeId(Identifier): https://www.onixs.biz/fix-dictionary/5.0/tagnum_1003.html """ - def __init__(self, str value): + def __init__(self, str value not None): self._mem = trade_id_new(value) def __del__(self) -> None: - trade_id_free(self._mem) # `self._mem` moved to Rust (then dropped) + # TODO(cs): Investigate dealloc (not currently being freed) + # trade_id_free(self._mem) # `self._mem` moved to Rust (then dropped) + pass def __getstate__(self): return self.to_str() diff --git a/nautilus_trader/model/objects.pyx b/nautilus_trader/model/objects.pyx index 9ae101b60d12..0db8cf518fcb 100644 --- a/nautilus_trader/model/objects.pyx +++ b/nautilus_trader/model/objects.pyx @@ -825,11 +825,7 @@ cdef class Money: """ def __init__(self, value, Currency currency not None): - cdef double value_f64 - if value is None: - value_f64 = 0.0 - else: - value_f64 = float(value) + cdef double value_f64 = 0.0 if value is None else float(value) if value_f64 > MONEY_MAX: raise ValueError( @@ -843,7 +839,9 @@ cdef class Money: self._mem = money_new(value_f64, currency._mem) # borrows wrapped `currency` def __del__(self) -> None: - money_free(self._mem) # `self._mem` moved to Rust (then dropped) + # TODO(cs): Investigate dealloc (not currently being freed) + # money_free(self._mem) # `self._mem` moved to Rust (then dropped) + pass def __getstate__(self): return self._mem.raw, self.currency_code_c() diff --git a/poetry.lock b/poetry.lock index c34db692d1a4..f95fe59804ba 100644 --- a/poetry.lock +++ b/poetry.lock @@ -232,8 +232,8 @@ python-versions = ">=3.6" [[package]] name = "cython" -version = "3.0.0a9" -description = "The Cython compiler for writing C extensions for the Python language." +version = "3.0.0a11" +description = "The Cython compiler for writing C extensions in the Python language." category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" @@ -1457,7 +1457,7 @@ redis = ["hiredis", "redis"] [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "cbc14b2b9487d213cfda5a56274a41a6f47b84cf5e91c446084212079f821c87" +content-hash = "072b0582f3f749e15c82f6486b8a1d93d27ff5801962650b6489b9af52c5c292" [metadata.files] aiodns = [ @@ -1756,45 +1756,43 @@ css-html-js-minify = [ {file = "css_html_js_minify-2.5.5-py3.6.egg", hash = "sha256:4704e04a0cd6dd56d61bbfa3bfffc630da6b2284be33519be0b456672e2a2438"}, ] cython = [ - {file = "Cython-3.0.0a9-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:48c8adf0006be76afcd30e73d75c4dfd356e97999b2ab79c6301a862a0b3a77b"}, - {file = "Cython-3.0.0a9-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:d8eaa098e2a72085256a3d653be56d649a1124cb41f38011e7c2482f5d940a4c"}, - {file = "Cython-3.0.0a9-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:8c4c08af0e3f6223aad6a04e82a73781718e820b1b1f75e4ddf49ade8b006851"}, - {file = "Cython-3.0.0a9-cp27-cp27m-win32.whl", hash = "sha256:4ac45cb2b19c72585803c39ee4ba0f7d890d6519a9a3e9d609dde22523410722"}, - {file = "Cython-3.0.0a9-cp27-cp27m-win_amd64.whl", hash = "sha256:52c22859eac6a35f3995eb595664d898fb35a29c82ed2123ec8e882250d4e490"}, - {file = "Cython-3.0.0a9-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:aaf3c90fc7bb99810026efc5b8cb72b8ea5eaa1ea703fa83cf7adc61b9a3cea7"}, - {file = "Cython-3.0.0a9-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e0f21b82eac4904d6881a5e4b9f005b6e2840efae744392b3b44051d19b910f7"}, - {file = "Cython-3.0.0a9-cp34-cp34m-win32.whl", hash = "sha256:971ddc0096ead71b727b5bbe5afdcabe897e188e2a91e4e702f1309d298c067d"}, - {file = "Cython-3.0.0a9-cp34-cp34m-win_amd64.whl", hash = "sha256:36b867cd86f7cd70d8bd5886fe26a610b89c26cfa7a620a7aa547a7709294bfd"}, - {file = "Cython-3.0.0a9-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:0fb13aed57cce88885da12f5544e8d232de289d63c046d05a4ad623580341155"}, - {file = "Cython-3.0.0a9-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:56c4e731825af64e807485f375ac11f2230f2577d06b0a74fafd2f3a84f9e38f"}, - {file = "Cython-3.0.0a9-cp35-cp35m-win32.whl", hash = "sha256:6337e187f20439ddeb0b80309d9ae31b9d7c501101781a5504fc744f2dbc20a8"}, - {file = "Cython-3.0.0a9-cp35-cp35m-win_amd64.whl", hash = "sha256:b3b4263e41c3006ad1a803c9e6d92782ea699f205923e77d21c2c3857f9d9201"}, - {file = "Cython-3.0.0a9-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:91511474c9b4601ba96f8f8b9760a30d0fa998a4f4bfd59357276fc1c08904ed"}, - {file = "Cython-3.0.0a9-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:c94161198402186f30e8b98cecf1968510632f72dbe41768aa3235d372786ee5"}, - {file = "Cython-3.0.0a9-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:635012bfb82aa0e3694b1c22bf0c85b416acf83000b80d6c305205d06dc91f90"}, - {file = "Cython-3.0.0a9-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bdee52750abd8dc1ca4afce798763ad3c955bd05d9855603d8abab552d807e2c"}, - {file = "Cython-3.0.0a9-cp36-cp36m-win32.whl", hash = "sha256:aa4e01e2ed8809c1c04032154aa3e3a03f6be5db9eb5edd30ee73cf23fcf50ca"}, - {file = "Cython-3.0.0a9-cp36-cp36m-win_amd64.whl", hash = "sha256:0460b40126103a49a84bf798d0faf9538f87f34dfc0cffce278678359b6db23f"}, - {file = "Cython-3.0.0a9-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:10aca15d4b337c693fdcec319f7c71c75562e435d38f4fea11ab4e2b7a59448d"}, - {file = "Cython-3.0.0a9-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6a677135a33240abe9040f5dbc1c79e162583b7f5e24eb668882de9a771836eb"}, - {file = "Cython-3.0.0a9-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:144380701f90833594a2e8d4e182dcfc1f90e125f2b8b1c50b4985fc9c3c234e"}, - {file = "Cython-3.0.0a9-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30903eb8a52c7ecaf3144c692372b5bab838a65941c520931c319c453dc0f802"}, - {file = "Cython-3.0.0a9-cp37-cp37m-win32.whl", hash = "sha256:1d84cca12ff2739ae6d788698de4e1b17e8fe6ffea637e6167cc59825e76aae6"}, - {file = "Cython-3.0.0a9-cp37-cp37m-win_amd64.whl", hash = "sha256:6e3a018fe09962c48415f1a713a3ae10a7b44d33bdbc9b09a831ef9d15ec745a"}, - {file = "Cython-3.0.0a9-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c425761ec961eba76935dfc1ba782919b975d6430018dfcbf7e44838e61eb1f5"}, - {file = "Cython-3.0.0a9-cp38-cp38-manylinux1_i686.whl", hash = "sha256:ff5c291b9ee97737476548c7be02bab39103b996bab96b5314edd347e1689534"}, - {file = "Cython-3.0.0a9-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:1c58c7154142c9ad084f6677427dd6dc063c49425229422642736a5312e0b6bb"}, - {file = "Cython-3.0.0a9-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d275b779510e0b9f48f39e8034b90e58bb1d747b1c9d2e020b5f3255855895f4"}, - {file = "Cython-3.0.0a9-cp38-cp38-win32.whl", hash = "sha256:6ba0384f8df54662d665bf03ce463c187393b1841948668850f73f033b0c8c3f"}, - {file = "Cython-3.0.0a9-cp38-cp38-win_amd64.whl", hash = "sha256:f57234e41735521fe406e4f0f944b15dfe66a3ba7e4aaafd7e2ee05aa32b8277"}, - {file = "Cython-3.0.0a9-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5c27a73c0419a34059e329f9ae8579d00b6b0d5b7c6dfc7a9a32a2fe882bfb1b"}, - {file = "Cython-3.0.0a9-cp39-cp39-manylinux1_i686.whl", hash = "sha256:48279cca752a28e942339dc7810307c30accca194d32ee08f6cb9a1f7ac1e192"}, - {file = "Cython-3.0.0a9-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:649e3b4667b509d1ef1916806f2a48b0db0542c2693c8cb81b191ff112f9f028"}, - {file = "Cython-3.0.0a9-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:068661d9df2d3db08ab4799b8a3314c67e6ce7e8d06640bf70aac09feb145f54"}, - {file = "Cython-3.0.0a9-cp39-cp39-win32.whl", hash = "sha256:880abf2abf11331d12f73063e1443dd78b27e1d7eb80bb1db3a6175ea041f034"}, - {file = "Cython-3.0.0a9-cp39-cp39-win_amd64.whl", hash = "sha256:07412b12a9adc5caebfd07c2b6fcaf4d8ea8db680cfb52a2d8f0be482955ed5b"}, - {file = "Cython-3.0.0a9-py2.py3-none-any.whl", hash = "sha256:d5a17108c5d765bacb7a7c16d339172e38379023746bb9126b9912086e7487e4"}, - {file = "Cython-3.0.0a9.tar.gz", hash = "sha256:23931c45877432097cef9de2db2dc66322cbc4fc3ebbb42c476bb2c768cecff0"}, + {file = "Cython-3.0.0a11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9299166daf8ec779077dba2c155fdd28403bf0a369d19bd2c101891fc925a2cf"}, + {file = "Cython-3.0.0a11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:363856a864ada94f91bfdf027b8d7a9f607c97f4f274b462109249b4887a0d66"}, + {file = "Cython-3.0.0a11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:35b37481e2be23dbcc93b9d3574aa1463890d67232b3dfcdd67c15b39631a327"}, + {file = "Cython-3.0.0a11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:52520b672a893d24ac4e1beb47a90b26938d0f0a81736d219b7a287f76845400"}, + {file = "Cython-3.0.0a11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:1e788a81dbf6aedf11e9b6d67dd160a59b349c70c5ca43ec10671c4d65ac9cb2"}, + {file = "Cython-3.0.0a11-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f80f7743775a4704b1b3fbf1b67ca40f125760b33c7e1296f3825cd2ecaec0bf"}, + {file = "Cython-3.0.0a11-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22fbb47470a60db656fee7a209d5c325de42efe9d5bc9d956654110bb3b2a48e"}, + {file = "Cython-3.0.0a11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:ef8c4101593b85892140806980df9fa38f4676aa7a46072e58f1b099cb4ece8e"}, + {file = "Cython-3.0.0a11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89617c03f6f1516ce3a90c6fc51da5f96b2f7a0e693ff0868d7fb633b888c274"}, + {file = "Cython-3.0.0a11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:191482c94a24d922082b93ea0f8a52bd2e90f573326302685eb0bd24ebddae68"}, + {file = "Cython-3.0.0a11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35ea5f761819d4123840656355617f9f0d50c71ca004a09075a574d480c6a942"}, + {file = "Cython-3.0.0a11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:6458695463db34f26c7fead8a1c0439898fc7c401fa959ac2490c6e0677b80f7"}, + {file = "Cython-3.0.0a11-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6421b82a8c305392e2eae85bc08920670f59f26a64a6c147c384f8d7f283f838"}, + {file = "Cython-3.0.0a11-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:07a7157a5f76d0c5e624c6e5f6276a70216c89c47436f8fae2c37ec0c9042484"}, + {file = "Cython-3.0.0a11-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:242047ce0151fe581b8be25845777c9547de4c82082aadbaabbe10c691b7c802"}, + {file = "Cython-3.0.0a11-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:375c351c8488d6de2bbb89939ddb3fc377e3fbfc38cc6fd6d4941a18741c8036"}, + {file = "Cython-3.0.0a11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:7e8a25c37fc844caba2102675714b77ea7bbabddd714c76d4ed146f2e91544bf"}, + {file = "Cython-3.0.0a11-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c757c2d8177a25e2b53bbc6feac0d92e236f4f4c39d4c3f7cf62d172d25ea2d"}, + {file = "Cython-3.0.0a11-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:a708cf45dd6184ea8fec884b498c420724291e1b8abd91c7ce9b8a7f37d5099f"}, + {file = "Cython-3.0.0a11-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:210a38dbe92c295bc074faf40734789d35b777713ea1a003e09130d8e36f2313"}, + {file = "Cython-3.0.0a11-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:14b6e5314a2d639eae9f343c92610a2bf9bae73a97593d5b4bce09bcab22bceb"}, + {file = "Cython-3.0.0a11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:c06f1f652bbb796513e8a3b447bbfb7580692e8d18f72cdff5fa739defe993fb"}, + {file = "Cython-3.0.0a11-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c02313dc6b5fbdeb4287a14689ba38e228f70bb9b65805a5ab57c4d3bf63ad7"}, + {file = "Cython-3.0.0a11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:54e99710b2201d137f4b9a349f4ade0c85a4ed8ee78146c0693e17ddc34b829a"}, + {file = "Cython-3.0.0a11-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a895430af454f098825ed0deb54432c90f2bd5371519c6e0eeb5a0770206021"}, + {file = "Cython-3.0.0a11-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:bc856c9389d4433d365f1c02b9861510ddce61faa4df1cd0d17960c59e77eb43"}, + {file = "Cython-3.0.0a11-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:89d6e70b96d567cdfea102b71c3749d60ce87e8ebb8199aa244c1e40f48bfd8c"}, + {file = "Cython-3.0.0a11-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a487c27e8c198520daec405d7a46ade2fb73307e9e588a864a6a94c41cbf68e7"}, + {file = "Cython-3.0.0a11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:501e4e86c68bedfd62dcff31c351e71f95723edc611e100e76d19070569abe4e"}, + {file = "Cython-3.0.0a11-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb67c94f81c8fe135b03fea25680944236e55d62673d2d236d1e6df7919c744f"}, + {file = "Cython-3.0.0a11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:aca2602c74572cd82d34d4897aca925323f46cfdc1f0463dcaa528ba948a82dc"}, + {file = "Cython-3.0.0a11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:67895d14d10f2846fcedff29090fcbf146146c7e1f36b2f88a533f03579d0ace"}, + {file = "Cython-3.0.0a11-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_24_i686.whl", hash = "sha256:b997abcc456e95fa8e44d8ba0bf051b54036c3bf211acfadda1dc883585ab2e7"}, + {file = "Cython-3.0.0a11-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b467d3c90ea68691ab89e45c21e2553933cae143bfb8de381517746cafb8b485"}, + {file = "Cython-3.0.0a11-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2a84f68960ae42fcd8399263fd813178bff6b2b2b8327d1fb71e02c5bbb7d73f"}, + {file = "Cython-3.0.0a11-py2.py3-none-any.whl", hash = "sha256:939d5d252b242445933c078f3e6a7661436574d6eb50fca08842eedc7167eefe"}, + {file = "Cython-3.0.0a11.tar.gz", hash = "sha256:e4672491fb31546b9abb63677f638e738085dc9321398170956ef6fbfc0e1726"}, ] deprecated = [ {file = "Deprecated-1.2.13-py2.py3-none-any.whl", hash = "sha256:64756e3e14c8c5eea9795d93c524551432a0be75629f8f29e67ab8caf076c76d"}, diff --git a/pyproject.toml b/pyproject.toml index 0d366c7e54df..6c368d6846b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,7 +36,7 @@ requires = [ "setuptools", "poetry-core>=1.3.2", "numpy>=1.23.4", - "Cython==3.0.0a9", + "Cython==3.0.0a11", ] build-backend = "poetry.core.masonry.api" @@ -46,7 +46,7 @@ generate-setup-file = false [tool.poetry.dependencies] python = ">=3.9,<3.11" -cython = "3.0.0a9" # Pinned at 3.0.0a9 +cython = "3.0.0a11" aiodns = "^3.0.0" aiohttp = "^3.8.3" click = "^8.1.3" From 406a9c788ebc9c302f457a7b4f8336c359222c72 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 29 Oct 2022 17:07:24 +1100 Subject: [PATCH 15/38] Improve condition check messages --- nautilus_trader/model/objects.pyx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nautilus_trader/model/objects.pyx b/nautilus_trader/model/objects.pyx index 0db8cf518fcb..e8f52ad27bdc 100644 --- a/nautilus_trader/model/objects.pyx +++ b/nautilus_trader/model/objects.pyx @@ -1129,10 +1129,10 @@ cdef class AccountBalance: ): Condition.equal(total.currency, locked.currency, "total.currency", "locked.currency") Condition.equal(total.currency, free.currency, "total.currency", "free.currency") - Condition.true(total.raw_int64_c() >= 0, "total was negative") - Condition.true(locked.raw_int64_c() >= 0, "locked was negative") - Condition.true(free.raw_int64_c() >= 0, "free was negative") - Condition.true(total.raw_int64_c() - locked.raw_int64_c() == free.raw_int64_c(), "total - locked != free") + Condition.true(total.raw_int64_c() >= 0, "`total` amount was negative") + Condition.true(locked.raw_int64_c() >= 0, "`locked` amount was negative") + Condition.true(free.raw_int64_c() >= 0, "`free` amount was negative") + Condition.true(total.raw_int64_c() - locked.raw_int64_c() == free.raw_int64_c(), "`total` - `locked` != `free` amount") self.total = total self.locked = locked From 2ed5ba14a172fc53002f21d96554c7ebfb782837 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 29 Oct 2022 17:08:34 +1100 Subject: [PATCH 16/38] Add `set_time` to `TestClock.advance_time` --- nautilus_trader/common/clock.pxd | 2 +- nautilus_trader/common/clock.pyx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/nautilus_trader/common/clock.pxd b/nautilus_trader/common/clock.pxd index dc7d985c937b..a202bcf9319f 100644 --- a/nautilus_trader/common/clock.pxd +++ b/nautilus_trader/common/clock.pxd @@ -73,7 +73,7 @@ cdef class TestClock(Clock): cdef CTestClock _mem cpdef void set_time(self, uint64_t to_time_ns) except * - cpdef list advance_time(self, uint64_t to_time_ns) + cpdef list advance_time(self, uint64_t to_time_ns, bint set_time=*) cdef class LiveClock(Clock): diff --git a/nautilus_trader/common/clock.pyx b/nautilus_trader/common/clock.pyx index 4819d8beb751..b420d8edc14e 100644 --- a/nautilus_trader/common/clock.pyx +++ b/nautilus_trader/common/clock.pyx @@ -505,7 +505,7 @@ cdef class TestClock(Clock): """ test_clock_set_time(&self._mem, to_time_ns) - cpdef list advance_time(self, uint64_t to_time_ns): + cpdef list advance_time(self, uint64_t to_time_ns, bint set_time=True): """ Advance the clocks time to the given `to_time_ns`. @@ -513,6 +513,8 @@ cdef class TestClock(Clock): ---------- to_time_ns : uint64_t The UNIX time (nanoseconds) to advance the clock to. + set_time : bool + If the clock should also be set to the given `to_time_ns`. Returns ------- @@ -528,7 +530,7 @@ cdef class TestClock(Clock): # Ensure monotonic Condition.true(to_time_ns >= test_clock_time_ns(&self._mem), "to_time_ns was < time_ns") - cdef Vec_TimeEvent raw_events = test_clock_advance_time(&self._mem, to_time_ns, True) + cdef Vec_TimeEvent raw_events = test_clock_advance_time(&self._mem, to_time_ns, set_time) cdef list event_handlers = [] cdef: From 8f560a772596ddac8f124c7273e472303a66834e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 29 Oct 2022 17:28:31 +1100 Subject: [PATCH 17/38] Improved accuracy of clocks for backtests --- RELEASES.md | 1 + nautilus_trader/backtest/engine.pxd | 2 +- nautilus_trader/backtest/engine.pyx | 36 +++++++++++++------ .../test_backtest_acceptance.py | 6 ++-- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 7a564eb5a8e8..1d09583dd3a9 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,6 +9,7 @@ Released on TBD (UTC). - Removed `HilbertTransform` indicator (to reduce total package size) ### Enhancements +- Improved accuracy of clocks for backtests (all clocks will now match a `TimeEvent`) - Added `Actor.request_instruments(...)` method - Extended instrument(s) req/res handling for `DataClient` and `Actor diff --git a/nautilus_trader/backtest/engine.pxd b/nautilus_trader/backtest/engine.pxd index 07529d532640..0fe3a9bbafe0 100644 --- a/nautilus_trader/backtest/engine.pxd +++ b/nautilus_trader/backtest/engine.pxd @@ -50,4 +50,4 @@ cdef class BacktestEngine: cpdef list list_strategies(self) cdef Data _next(self) - cdef list _advance_time(self, uint64_t now_ns) + cdef list _advance_time(self, uint64_t now_ns, list clocks) diff --git a/nautilus_trader/backtest/engine.pyx b/nautilus_trader/backtest/engine.pyx index 28c19f96f859..a46c33f481b3 100644 --- a/nautilus_trader/backtest/engine.pyx +++ b/nautilus_trader/backtest/engine.pyx @@ -43,6 +43,7 @@ from nautilus_trader.backtest.modules cimport SimulationModule from nautilus_trader.cache.base cimport CacheFacade from nautilus_trader.common.actor cimport Actor from nautilus_trader.common.clock cimport LiveClock +from nautilus_trader.common.clock cimport TestClock from nautilus_trader.common.logging cimport Logger from nautilus_trader.common.logging cimport LoggerAdapter from nautilus_trader.common.logging cimport LogLevelParser @@ -915,10 +916,16 @@ cdef class BacktestEngine: Condition.true(start_ns < end_ns, "start was >= end") Condition.not_empty(self._data, "data") - # Set clocks - self.kernel.clock.set_time(start_ns) + # Gather clocks + cdef list clocks = [self.kernel.clock] + cdef Actor actor for actor in self._kernel.trader.actors() + self._kernel.trader.strategies(): - actor.clock.set_time(start_ns) + clocks.append(actor.clock) + + # Set clocks + cdef TestClock clock + for clock in clocks: + clock.set_time(start_ns) cdef SimulatedExchange exchange if self._iteration == 0: @@ -956,7 +963,7 @@ cdef class BacktestEngine: while data is not None: if data.ts_init > end_ns: break - now_events = self._advance_time(data.ts_init) + now_events = self._advance_time(data.ts_init, clocks) if isinstance(data, OrderBookData): self._venues[data.instrument_id.venue].process_order_book(data) elif isinstance(data, QuoteTick): @@ -967,6 +974,8 @@ cdef class BacktestEngine: self._venues[data.bar_type.instrument_id.venue].process_bar(data) self._data_engine.process(data) for event_handler in now_events: + for clock in clocks: + clock.set_time(event_handler.event.ts_init) event_handler.handle() for exchange in self._venues.values(): exchange.process(data.ts_init) @@ -1005,26 +1014,33 @@ cdef class BacktestEngine: if cursor < self._data_len: return self._data[cursor] - cdef list _advance_time(self, uint64_t now_ns): + cdef list _advance_time(self, uint64_t now_ns, list clocks): cdef list all_events = [] # type: list[TimeEventHandler] cdef list now_events = [] # type: list[TimeEventHandler] cdef Actor actor for actor in self._kernel.trader.actors(): - all_events += actor.clock.advance_time(now_ns) + # Here we aren't setting the clock to the new time yet + all_events += actor.clock.advance_time(now_ns, False) cdef Strategy strategy for strategy in self._kernel.trader.strategies(): - all_events += strategy.clock.advance_time(now_ns) + # Here we aren't setting the clock to the new time yet + all_events += strategy.clock.advance_time(now_ns, False) - all_events += self.kernel.clock.advance_time(now_ns) + # Here we aren't setting the clock to the new time yet + all_events += self.kernel.clock.advance_time(now_ns, False) # Handle all events prior to the `now_ns` - cdef TimeEventHandler event_handler + cdef: + TimeEventHandler event_handler + TestClock clock for event_handler in sorted(all_events): - if event_handler.event.ts_event == now_ns: + if event_handler.event.ts_init == now_ns: now_events.append(event_handler) continue + for clock in clocks: + clock.set_time(event_handler.event.ts_init) event_handler.handle() # Return the remaining events to be handled diff --git a/tests/acceptance_tests/test_backtest_acceptance.py b/tests/acceptance_tests/test_backtest_acceptance.py index a49625d78384..adfdd9adc298 100644 --- a/tests/acceptance_tests/test_backtest_acceptance.py +++ b/tests/acceptance_tests/test_backtest_acceptance.py @@ -264,7 +264,7 @@ def test_run_ema_cross_stop_entry_trail_strategy_with_emulation(self): # Arrange config = EMACrossTrailingStopConfig( instrument_id=str(self.gbpusd.id), - bar_type="GBP/USD.SIM-5-MINUTE-BID-INTERNAL", + bar_type="GBP/USD.SIM-1-MINUTE-BID-INTERNAL", trade_size=Decimal(1_000_000), fast_ema_period=10, slow_ema_period=20, @@ -281,9 +281,9 @@ def test_run_ema_cross_stop_entry_trail_strategy_with_emulation(self): self.engine.run() # Assert - Should return expected PnL - assert strategy.fast_ema.count == 8353 + assert strategy.fast_ema.count == 41761 assert self.engine.iteration == 120468 - assert self.engine.portfolio.account(self.venue).balance_total(GBP) == Money(935316.79, GBP) + assert self.engine.portfolio.account(self.venue).balance_total(GBP) == Money(639016.69, GBP) class TestBacktestAcceptanceTestsGBPUSDBarsExternal: From 164555d6123a8a4160cada16e8875eb9dca2e4aa Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sat, 29 Oct 2022 19:01:49 +1100 Subject: [PATCH 18/38] Refine backtest clocks handling --- nautilus_trader/backtest/engine.pyx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/nautilus_trader/backtest/engine.pyx b/nautilus_trader/backtest/engine.pyx index a46c33f481b3..807cee96f581 100644 --- a/nautilus_trader/backtest/engine.pyx +++ b/nautilus_trader/backtest/engine.pyx @@ -974,8 +974,6 @@ cdef class BacktestEngine: self._venues[data.bar_type.instrument_id.venue].process_bar(data) self._data_engine.process(data) for event_handler in now_events: - for clock in clocks: - clock.set_time(event_handler.event.ts_init) event_handler.handle() for exchange in self._venues.values(): exchange.process(data.ts_init) @@ -1020,16 +1018,13 @@ cdef class BacktestEngine: cdef Actor actor for actor in self._kernel.trader.actors(): - # Here we aren't setting the clock to the new time yet - all_events += actor.clock.advance_time(now_ns, False) + all_events += actor.clock.advance_time(now_ns, set_time=False) cdef Strategy strategy for strategy in self._kernel.trader.strategies(): - # Here we aren't setting the clock to the new time yet - all_events += strategy.clock.advance_time(now_ns, False) + all_events += strategy.clock.advance_time(now_ns, set_time=False) - # Here we aren't setting the clock to the new time yet - all_events += self.kernel.clock.advance_time(now_ns, False) + all_events += self.kernel.clock.advance_time(now_ns, set_time=False) # Handle all events prior to the `now_ns` cdef: @@ -1043,7 +1038,11 @@ cdef class BacktestEngine: clock.set_time(event_handler.event.ts_init) event_handler.handle() - # Return the remaining events to be handled + # Set all clocks + for clock in clocks: + clock.set_time(now_ns) + + # Return all remaining events to be handled (at `now_ns`) return now_events def _log_pre_run(self): From 7d64e72bd27ec734215ba570bfa682a626037a4e Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 30 Oct 2022 08:19:56 +1100 Subject: [PATCH 19/38] Fix type hint --- nautilus_trader/live/node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nautilus_trader/live/node.py b/nautilus_trader/live/node.py index 6244a1d1a1af..6d1e976ff9a2 100644 --- a/nautilus_trader/live/node.py +++ b/nautilus_trader/live/node.py @@ -196,13 +196,13 @@ def is_built(self) -> bool: """ return self._is_built - def get_event_loop(self) -> asyncio.AbstractEventLoop: + def get_event_loop(self) -> Optional[asyncio.AbstractEventLoop]: """ Return the event loop of the trading node. Returns ------- - asyncio.AbstractEventLoop + asyncio.AbstractEventLoop or ``None`` """ return self.kernel.loop From d16ce6b9e0173ce01b5a118a447b4f210dc36921 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 30 Oct 2022 08:35:25 +1100 Subject: [PATCH 20/38] Update adapter templates --- nautilus_trader/adapters/_template/data.py | 5 +++++ nautilus_trader/adapters/_template/execution.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/nautilus_trader/adapters/_template/data.py b/nautilus_trader/adapters/_template/data.py index 49de7583eb75..c2b2839a375b 100644 --- a/nautilus_trader/adapters/_template/data.py +++ b/nautilus_trader/adapters/_template/data.py @@ -24,6 +24,7 @@ from nautilus_trader.model.data.base import DataType from nautilus_trader.model.enums import BookType from nautilus_trader.model.identifiers import InstrumentId +from nautilus_trader.model.identifiers import Venue # The 'pragma: no cover' comment excludes a method from test coverage. @@ -123,6 +124,7 @@ class TemplateLiveMarketDataClient(LiveMarketDataClient): | unsubscribe_instrument_close_prices | optional | +---------------------------------------+-------------+ | request_instrument | optional | + | request_instruments | optional | | request_quote_ticks | optional | | request_trade_ticks | optional | | request_bars | optional | @@ -227,6 +229,9 @@ def unsubscribe_instrument_close_prices(self, instrument_id: InstrumentId) -> No def request_instrument(self, instrument_id: InstrumentId, correlation_id: UUID4): raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + def request_instruments(self, venue: Venue, correlation_id: UUID4): + raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + def request_quote_ticks( self, instrument_id: InstrumentId, diff --git a/nautilus_trader/adapters/_template/execution.py b/nautilus_trader/adapters/_template/execution.py index aca3e36c206e..a0fae23d3ed6 100644 --- a/nautilus_trader/adapters/_template/execution.py +++ b/nautilus_trader/adapters/_template/execution.py @@ -19,6 +19,7 @@ from nautilus_trader.execution.messages import CancelAllOrders from nautilus_trader.execution.messages import CancelOrder from nautilus_trader.execution.messages import ModifyOrder +from nautilus_trader.execution.messages import QueryOrder from nautilus_trader.execution.messages import SubmitOrder from nautilus_trader.execution.messages import SubmitOrderList from nautilus_trader.execution.reports import OrderStatusReport @@ -58,6 +59,7 @@ class TemplateLiveExecutionClient(LiveExecutionClient): | modify_order | required | | cancel_order | required | | cancel_all_orders | required | + | query_order | required | | generate_order_status_report | required | | generate_order_status_reports | required | | generate_trade_reports | required | @@ -129,3 +131,6 @@ def cancel_order(self, command: CancelOrder) -> None: def cancel_all_orders(self, command: CancelAllOrders) -> None: raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + + def query_order(self, command: QueryOrder) -> None: + raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover From 52f0331166b1c71a2febd6d35e59df1290ba5600 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 30 Oct 2022 08:35:41 +1100 Subject: [PATCH 21/38] Fix fstring log --- nautilus_trader/live/execution_engine.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_trader/live/execution_engine.pyx b/nautilus_trader/live/execution_engine.pyx index 17b00a879412..7b67e8a8e2f9 100644 --- a/nautilus_trader/live/execution_engine.pyx +++ b/nautilus_trader/live/execution_engine.pyx @@ -323,7 +323,7 @@ cdef class LiveExecutionEngine(ExecutionEngine): Order order QueryOrder query for order in inflight_orders: - self._log.debug("Checking in-flight {order}...") + self._log.debug(f"Checking in-flight {order}...") if self._clock.timestamp_ns() > order.last_event_c().ts_event + self._inflight_check_threshold_ns: query = QueryOrder( trader_id=order.trader_id, From ac85669ba8bd905b696a0e822ab2df35d4019d3b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 30 Oct 2022 09:00:42 +1100 Subject: [PATCH 22/38] Fix matching engine order price bug --- nautilus_trader/backtest/matching_engine.pyx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/nautilus_trader/backtest/matching_engine.pyx b/nautilus_trader/backtest/matching_engine.pyx index 8146b9764da3..745299e00a5b 100644 --- a/nautilus_trader/backtest/matching_engine.pyx +++ b/nautilus_trader/backtest/matching_engine.pyx @@ -848,14 +848,16 @@ cdef class OrderMatchingEngine: self._update_trailing_stop_order(order) cpdef list _determine_limit_price_and_volume(self, Order order): + cdef Price price if self._bar_execution: + price = order.price if order.side == OrderSide.BUY: - self._core.set_bid(order.price._mem) + self._core.set_bid(price._mem) elif order.side == OrderSide.SELL: - self._core.set_ask(order.price._mem) + self._core.set_ask(price._mem) else: raise RuntimeError(f"invalid `OrderSide`, was {order.side}") # pragma: no cover (design-time error) - self._core.set_last(order.price._mem) + self._core.set_last(price._mem) return [(order.price, order.leaves_qty)] cdef OrderBookOrder submit_order = OrderBookOrder(price=order.price, size=order.leaves_qty, side=order.side) if order.side == OrderSide.BUY: From 7d5ea184627bc2d53b7000f30fd1b4b526f58959 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 30 Oct 2022 09:01:23 +1100 Subject: [PATCH 23/38] Improve inflight order status check and logging --- nautilus_trader/live/execution_engine.pyx | 9 ++- tests/test_kit/mocks/exec_clients.py | 14 +++++ .../live/test_live_execution_engine.py | 60 ++++++++++++++++++- 3 files changed, 80 insertions(+), 3 deletions(-) diff --git a/nautilus_trader/live/execution_engine.pyx b/nautilus_trader/live/execution_engine.pyx index 7b67e8a8e2f9..2152ab94ee50 100644 --- a/nautilus_trader/live/execution_engine.pyx +++ b/nautilus_trader/live/execution_engine.pyx @@ -322,9 +322,14 @@ cdef class LiveExecutionEngine(ExecutionEngine): cdef: Order order QueryOrder query + uint64_t now_ns + uint64_t ts_init_last for order in inflight_orders: - self._log.debug(f"Checking in-flight {order}...") - if self._clock.timestamp_ns() > order.last_event_c().ts_event + self._inflight_check_threshold_ns: + now_ns = self._clock.timestamp_ns() + ts_init_last = order.last_event_c().ts_event + self._log.debug(f"Checking in-flight order: {now_ns=}, {ts_init_last=}, {order=}...") + if now_ns > order.last_event_c().ts_event + self._inflight_check_threshold_ns: + self._log.debug(f"Querying {order} with exchange...") query = QueryOrder( trader_id=order.trader_id, strategy_id=order.strategy_id, diff --git a/tests/test_kit/mocks/exec_clients.py b/tests/test_kit/mocks/exec_clients.py index c25c3397213e..df26be2a34e0 100644 --- a/tests/test_kit/mocks/exec_clients.py +++ b/tests/test_kit/mocks/exec_clients.py @@ -187,6 +187,12 @@ def __init__( self.calls = [] self.commands = [] + def connect(self) -> None: + pass # Do nothing + + def disconnect(self) -> None: + pass # Do nothing + def add_order_status_report(self, report: OrderStatusReport) -> None: self._order_status_reports[report.venue_order_id] = report @@ -226,6 +232,14 @@ def cancel_order(self, command) -> None: self.calls.append(inspect.currentframe().f_code.co_name) self.commands.append(command) + def cancel_all_orders(self, command) -> None: + self.calls.append(inspect.currentframe().f_code.co_name) + self.commands.append(command) + + def query_order(self, command) -> None: + self.calls.append(inspect.currentframe().f_code.co_name) + self.commands.append(command) + # -- EXECUTION REPORTS ------------------------------------------------------------------------ async def generate_order_status_report( diff --git a/tests/unit_tests/live/test_live_execution_engine.py b/tests/unit_tests/live/test_live_execution_engine.py index 77366ec6ffe7..ad3f18c319b2 100644 --- a/tests/unit_tests/live/test_live_execution_engine.py +++ b/tests/unit_tests/live/test_live_execution_engine.py @@ -22,9 +22,11 @@ from nautilus_trader.common.clock import LiveClock from nautilus_trader.common.factories import OrderFactory from nautilus_trader.common.logging import Logger +from nautilus_trader.common.logging import LogLevel from nautilus_trader.common.providers import InstrumentProvider from nautilus_trader.config import LiveExecEngineConfig from nautilus_trader.core.uuid import UUID4 +from nautilus_trader.execution.emulator import OrderEmulator from nautilus_trader.execution.messages import SubmitOrder from nautilus_trader.execution.reports import ExecutionMassStatus from nautilus_trader.execution.reports import OrderStatusReport @@ -78,7 +80,10 @@ def setup(self): self.loop.set_debug(True) self.clock = LiveClock() - self.logger = Logger(self.clock) + self.logger = Logger( + clock=self.clock, + level_stdout=LogLevel.DEBUG, + ) self.trader_id = TestIdStubs.trader_id() @@ -123,6 +128,7 @@ def setup(self): cache=self.cache, clock=self.clock, logger=self.logger, + config=LiveExecEngineConfig(debug=True), ) self.risk_engine = LiveRiskEngine( @@ -134,12 +140,22 @@ def setup(self): logger=self.logger, ) + self.emulator = OrderEmulator( + trader_id=self.trader_id, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + self.instrument_provider = InstrumentProvider( venue=SIM, logger=self.logger, ) self.instrument_provider.add(AUDUSD_SIM) self.instrument_provider.add(GBPUSD_SIM) + self.cache.add_instrument(AUDUSD_SIM) + self.cache.add_instrument(GBPUSD_SIM) self.client = MockLiveExecutionClient( loop=self.loop, @@ -158,7 +174,28 @@ def setup(self): self.cache.add_instrument(AUDUSD_SIM) + self.strategy = Strategy() + self.strategy.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + + self.data_engine.start() + self.risk_engine.start() + self.exec_engine.start() + self.emulator.start() + self.strategy.start() + def teardown(self): + self.data_engine.stop() + self.risk_engine.stop() + self.exec_engine.stop() + self.emulator.stop() + self.strategy.stop() self.exec_engine.dispose() @pytest.mark.asyncio @@ -470,3 +507,24 @@ def test_execution_mass_status(self): # Assert assert self.exec_engine.report_count == 1 + + @pytest.mark.asyncio + async def test_check_inflight_order_status(self): + # Arrange + order = self.strategy.order_factory.limit( + instrument_id=AUDUSD_SIM.id, + order_side=OrderSide.BUY, + quantity=Quantity.from_int(100_000), + price=AUDUSD_SIM.make_price(0.70000), + ) + + self.strategy.submit_order(order) + self.exec_engine.process(TestEventStubs.order_submitted(order)) + + await asyncio.sleep(2.0) # Default threshold 1000ms + + # Act + await self.exec_engine._check_inflight_orders() + + # Assert + assert self.exec_engine.command_count == 2 From e619840410e4f2837eec9b7ec65dad1ed2c61c78 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 30 Oct 2022 17:08:11 +1100 Subject: [PATCH 24/38] Add execution algorithm params to `submit_order` --- nautilus_trader/execution/messages.pxd | 10 +++-- nautilus_trader/execution/messages.pyx | 37 ++++++++++++++++--- nautilus_trader/trading/strategy.pxd | 2 + nautilus_trader/trading/strategy.pyx | 15 ++++++++ .../execution/test_execution_messages.py | 37 ++++++++++++++++++- .../test_serialization_msgpack.py | 4 +- 6 files changed, 94 insertions(+), 11 deletions(-) diff --git a/nautilus_trader/execution/messages.pxd b/nautilus_trader/execution/messages.pxd index b4f21385176d..229346c3ea77 100644 --- a/nautilus_trader/execution/messages.pxd +++ b/nautilus_trader/execution/messages.pxd @@ -43,7 +43,11 @@ cdef class SubmitOrder(TradingCommand): cdef readonly Order order """The order for the command.\n\n:returns: `Order`""" cdef readonly PositionId position_id - """The position ID associated with the command.\n\n:returns: `PositionId` or ``None``""" + """The position ID to associate with the order.\n\n:returns: `PositionId` or ``None``""" + cdef readonly str exec_algorithm_id + """The execution algorithm ID for the order.\n\n:returns: `str` or ``None``""" + cdef readonly dict exec_algorithm_params + """The execution algorithm parameters for the order.\n\n:returns: `dict[str, Any]` or ``None``""" @staticmethod cdef SubmitOrder from_dict_c(dict values) @@ -108,9 +112,9 @@ cdef class CancelAllOrders(TradingCommand): cdef class QueryOrder(TradingCommand): cdef readonly ClientOrderId client_order_id - """The client order ID associated with the command.\n\n:returns: `ClientOrderId`""" + """The client order ID for the order to query.\n\n:returns: `ClientOrderId`""" cdef readonly VenueOrderId venue_order_id - """The venue order ID associated with the command.\n\n:returns: `VenueOrderId` or ``None``""" + """The venue order ID for the order to query.\n\n:returns: `VenueOrderId` or ``None``""" @staticmethod cdef QueryOrder from_dict_c(dict values) diff --git a/nautilus_trader/execution/messages.pyx b/nautilus_trader/execution/messages.pyx index d65de043bcc4..4bd4d12b0379 100644 --- a/nautilus_trader/execution/messages.pyx +++ b/nautilus_trader/execution/messages.pyx @@ -92,9 +92,20 @@ cdef class SubmitOrder(TradingCommand): The UNIX timestamp (nanoseconds) when the object was initialized. position_id : PositionId, optional The position ID for the command. + exec_algorithm_id : str, optional + The execution algorithm ID for the order. + exec_algorithm_params : dict[str, Any], optional + The execution algorithm parameters for the order (must be serializable primitives). client_id : ClientId, optional The execution client ID for the command. + Raises + ------ + ValueError + If `exec_algorithm_id` is not ``None`` and not a valid string. + ValueError + If `exec_algorithm_params` is not ``None`` and `exec_algorithm_id` is ``None``. + References ---------- https://www.onixs.biz/fix-dictionary/5.0.SP2/msgType_D_68.html @@ -108,8 +119,14 @@ cdef class SubmitOrder(TradingCommand): UUID4 command_id not None, uint64_t ts_init, PositionId position_id: Optional[PositionId] = None, + str exec_algorithm_id = None, + dict exec_algorithm_params = None, ClientId client_id = None, ): + if exec_algorithm_id is not None: + Condition.valid_string(exec_algorithm_id, "exec_algorithm_id") + if exec_algorithm_params is not None: + Condition.not_none(exec_algorithm_id, "exec_algorithm_id") super().__init__( client_id=client_id, trader_id=trader_id, @@ -121,6 +138,8 @@ cdef class SubmitOrder(TradingCommand): self.order = order self.position_id = position_id + self.exec_algorithm_id = exec_algorithm_id + self.exec_algorithm_params = exec_algorithm_params def __str__(self) -> str: return ( @@ -128,7 +147,9 @@ cdef class SubmitOrder(TradingCommand): f"instrument_id={self.instrument_id.to_str()}, " f"client_order_id={self.order.client_order_id.to_str()}, " f"order={self.order.info()}, " - f"position_id={self.position_id})" + f"position_id={self.position_id}, " # Can be None + f"exec_algorithm_id={self.exec_algorithm_id}, " # Can be None + f"exec_algorithm_params={self.exec_algorithm_params})" ) def __repr__(self) -> str: @@ -140,7 +161,9 @@ cdef class SubmitOrder(TradingCommand): f"instrument_id={self.instrument_id.to_str()}, " f"client_order_id={self.order.client_order_id.to_str()}, " f"order={self.order.info()}, " - f"position_id={self.position_id}, " + f"position_id={self.position_id}, " # Can be None + f"exec_algorithm_id={self.exec_algorithm_id}, " # Can be None + f"exec_algorithm_params={self.exec_algorithm_params}, " f"command_id={self.id.to_str()}, " f"ts_init={self.ts_init})" ) @@ -156,6 +179,8 @@ cdef class SubmitOrder(TradingCommand): strategy_id=StrategyId(values["strategy_id"]), order=OrderUnpacker.unpack_c(msgspec.json.decode(values["order"])), position_id=PositionId(p) if p is not None else None, + exec_algorithm_id=values["exec_algorithm_id"], + exec_algorithm_params=values["exec_algorithm_params"], command_id=UUID4(values["command_id"]), ts_init=values["ts_init"], ) @@ -170,6 +195,8 @@ cdef class SubmitOrder(TradingCommand): "strategy_id": obj.strategy_id.to_str(), "order": msgspec.json.encode(OrderInitialized.to_dict_c(obj.order.init_event_c())), "position_id": obj.position_id.to_str() if obj.position_id is not None else None, + "exec_algorithm_id": obj.exec_algorithm_id, + "exec_algorithm_params": obj.exec_algorithm_params, "command_id": obj.id.to_str(), "ts_init": obj.ts_init, } @@ -346,7 +373,7 @@ cdef class ModifyOrder(TradingCommand): The strategy ID for the command. instrument_id : InstrumentId The instrument ID for the command. - client_order_id : VenueOrderId + client_order_id : ClientOrderId The client order ID to update. venue_order_id : VenueOrderId, optional with no default so ``None`` must be passed explicitly The venue order ID (assigned by the venue) to update. @@ -762,9 +789,9 @@ cdef class QueryOrder(TradingCommand): instrument_id : InstrumentId The instrument ID for the command. client_order_id : ClientOrderId - The client order ID to cancel. + The client order ID for the order to query. venue_order_id : VenueOrderId, optional with no default so ``None`` must be passed explicitly - The venue order ID (assigned by the venue) to cancel. + The venue order ID (assigned by the venue) to query. command_id : UUID4 The command ID. ts_init : uint64_t diff --git a/nautilus_trader/trading/strategy.pxd b/nautilus_trader/trading/strategy.pxd index cd6acfbaa8cb..2f745ae34a4e 100644 --- a/nautilus_trader/trading/strategy.pxd +++ b/nautilus_trader/trading/strategy.pxd @@ -88,6 +88,8 @@ cdef class Strategy(Actor): self, Order order, PositionId position_id=*, + str exec_algorithm_id=*, + dict exec_algorithm_params=*, ClientId client_id=*, ) except * cpdef void submit_order_list(self, OrderList order_list, ClientId client_id=*) except * diff --git a/nautilus_trader/trading/strategy.pyx b/nautilus_trader/trading/strategy.pyx index cb1797acbc9b..a47557bed906 100644 --- a/nautilus_trader/trading/strategy.pyx +++ b/nautilus_trader/trading/strategy.pyx @@ -466,6 +466,8 @@ cdef class Strategy(Actor): self, Order order, PositionId position_id = None, + str exec_algorithm_id = None, + dict exec_algorithm_params = None, ClientId client_id = None, ) except *: """ @@ -480,10 +482,21 @@ cdef class Strategy(Actor): position_id : PositionId, optional The position ID to submit the order against. If a position does not yet exist, then any position opened will have this identifier assigned. + exec_algorithm_id : str, optional + The execution algorithm ID for the order. + exec_algorithm_params : dict[str, Any], optional + The execution algorithm parameters for the order (must be serializable primitives). client_id : ClientId, optional The specific client ID for the command. If ``None`` then will be inferred from the venue in the instrument ID. + Raises + ------ + ValueError + If `exec_algorithm_id` is not ``None`` and not a valid string. + ValueError + If `exec_algorithm_params` is not ``None`` and `exec_algorithm_id` is ``None``. + Warning ------- If a `position_id` is passed and a position does not yet exist, then any @@ -507,6 +520,8 @@ cdef class Strategy(Actor): command_id=UUID4(), ts_init=self.clock.timestamp_ns(), position_id=position_id, + exec_algorithm_id=exec_algorithm_id, + exec_algorithm_params=exec_algorithm_params, client_id=client_id, ) diff --git a/tests/unit_tests/execution/test_execution_messages.py b/tests/unit_tests/execution/test_execution_messages.py index 187b1e7f1291..54c479a8916c 100644 --- a/tests/unit_tests/execution/test_execution_messages.py +++ b/tests/unit_tests/execution/test_execution_messages.py @@ -74,11 +74,44 @@ def test_submit_order_command_to_from_dict_and_str_repr(self): assert SubmitOrder.from_dict(SubmitOrder.to_dict(command)) == command assert ( str(command) - == "SubmitOrder(instrument_id=AUD/USD.SIM, client_order_id=O-19700101-000000-000-001-1, order=BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, position_id=P-001)" # noqa + == "SubmitOrder(instrument_id=AUD/USD.SIM, client_order_id=O-19700101-000000-000-001-1, order=BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, position_id=P-001, exec_algorithm_id=None, exec_algorithm_params=None)" # noqa ) assert ( repr(command) - == f"SubmitOrder(client_id=None, trader_id=TRADER-001, strategy_id=S-001, instrument_id=AUD/USD.SIM, client_order_id=O-19700101-000000-000-001-1, order=BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, position_id=P-001, command_id={uuid}, ts_init=0)" # noqa + == f"SubmitOrder(client_id=None, trader_id=TRADER-001, strategy_id=S-001, instrument_id=AUD/USD.SIM, client_order_id=O-19700101-000000-000-001-1, order=BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, position_id=P-001, exec_algorithm_id=None, exec_algorithm_params=None, command_id={uuid}, ts_init=0)" # noqa + ) + + def test_submit_order_command_with_exec_algorithm_params_to_from_dict_and_str_repr(self): + # Arrange + uuid = UUID4() + + order = self.order_factory.limit( + AUDUSD_SIM.id, + OrderSide.BUY, + Quantity.from_int(100000), + Price.from_str("1.00000"), + ) + + command = SubmitOrder( + trader_id=TraderId("TRADER-001"), + strategy_id=StrategyId("S-001"), + order=order, + position_id=PositionId("P-001"), + exec_algorithm_id="TopChaser", + exec_algorithm_params={"parts": 4, "threshold": 1.0}, + command_id=uuid, + ts_init=self.clock.timestamp_ns(), + ) + + # Act, Assert + assert SubmitOrder.from_dict(SubmitOrder.to_dict(command)) == command + assert ( + str(command) + == "SubmitOrder(instrument_id=AUD/USD.SIM, client_order_id=O-19700101-000000-000-001-1, order=BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, position_id=P-001, exec_algorithm_id=TopChaser, exec_algorithm_params={'parts': 4, 'threshold': 1.0})" # noqa + ) + assert ( + repr(command) + == f"SubmitOrder(client_id=None, trader_id=TRADER-001, strategy_id=S-001, instrument_id=AUD/USD.SIM, client_order_id=O-19700101-000000-000-001-1, order=BUY 100_000 AUD/USD.SIM LIMIT @ 1.00000 GTC, position_id=P-001, exec_algorithm_id=TopChaser, exec_algorithm_params={{'parts': 4, 'threshold': 1.0}}, command_id={uuid}, ts_init=0)" # noqa ) def test_submit_bracket_order_command_to_from_dict_and_str_repr(self): diff --git a/tests/unit_tests/serialization/test_serialization_msgpack.py b/tests/unit_tests/serialization/test_serialization_msgpack.py index 887677726dd1..7c40a6747bc6 100644 --- a/tests/unit_tests/serialization/test_serialization_msgpack.py +++ b/tests/unit_tests/serialization/test_serialization_msgpack.py @@ -493,8 +493,10 @@ def test_serialize_and_deserialize_submit_order_commands(self): command = SubmitOrder( trader_id=self.trader_id, strategy_id=StrategyId("SCALPER-001"), - position_id=PositionId("P-123456"), order=order, + position_id=PositionId("P-123456"), + exec_algorithm_id="TopChaser", + exec_algorithm_params={"parts": 4, "threshold": 1.0}, command_id=UUID4(), ts_init=0, client_id=ClientId("SIM"), From 6a8399833f1dce2db0f8e9294726e4c023db11bc Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Sun, 30 Oct 2022 21:13:34 +1100 Subject: [PATCH 25/38] Add `SubmitOrder` command persistence --- RELEASES.md | 3 +- docs/user_guide/advanced/emulated_orders.md | 6 -- nautilus_trader/cache/cache.pxd | 5 + nautilus_trader/cache/cache.pyx | 78 +++++++++++++-- nautilus_trader/cache/database.pxd | 4 + nautilus_trader/cache/database.pyx | 12 +++ nautilus_trader/execution/emulator.pyx | 17 ++-- nautilus_trader/execution/engine.pyx | 3 + nautilus_trader/infrastructure/cache.pxd | 1 + nautilus_trader/infrastructure/cache.pyx | 96 +++++++++++++++++-- nautilus_trader/model/orders/limit.pyx | 1 + nautilus_trader/trading/strategy.pyx | 2 + .../infrastructure/test_cache_database.py | 72 +++++++++++++- tests/test_kit/mocks/cache_database.py | 22 ++++- .../unit_tests/cache/test_cache_execution.py | 59 ++++++++++++ 15 files changed, 344 insertions(+), 37 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 1d09583dd3a9..08c5127e4500 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -3,13 +3,14 @@ Released on TBD (UTC). ### Breaking Changes +- All Redis keys have changed to a lowercase convention - Removed `BidAskMinMax` indicator (to reduce total package size) - Removed `HilbertPeriod` indicator (to reduce total package size) - Removed `HilbertSignalNoiseRatio` indicator (to reduce total package size) - Removed `HilbertTransform` indicator (to reduce total package size) ### Enhancements -- Improved accuracy of clocks for backtests (all clocks will now match a `TimeEvent`) +- Improved accuracy of clocks for backtests (all clocks will now match generated `TimeEvent`s) - Added `Actor.request_instruments(...)` method - Extended instrument(s) req/res handling for `DataClient` and `Actor diff --git a/docs/user_guide/advanced/emulated_orders.md b/docs/user_guide/advanced/emulated_orders.md index 595972b36cfa..26a653bd0da3 100644 --- a/docs/user_guide/advanced/emulated_orders.md +++ b/docs/user_guide/advanced/emulated_orders.md @@ -10,12 +10,6 @@ There is no limitation on the number of emulated orders you can have per running Currently only individual orders can be emulated, so it is not possible to submit contingency order lists for emulation (this may be supported in a future version). -```{warning} -Emulated orders which have been persisted and reactivated from a subsequent start will not -remember any custom `position_id` or `client_id` routing instruction from the original `SubmitOrder` -command. -``` - ## Submitting for emulation The only requirement to emulate an order is to pass a `TriggerType` to the `emulation_trigger` parameter of an `Order` constructor, or `OrderFactory` creation method. The following diff --git a/nautilus_trader/cache/cache.pxd b/nautilus_trader/cache/cache.pxd index c291e27a2596..d5b068a21826 100644 --- a/nautilus_trader/cache/cache.pxd +++ b/nautilus_trader/cache/cache.pxd @@ -18,6 +18,7 @@ from nautilus_trader.accounting.calculators cimport ExchangeRateCalculator from nautilus_trader.cache.base cimport CacheFacade from nautilus_trader.cache.database cimport CacheDatabase from nautilus_trader.common.logging cimport LoggerAdapter +from nautilus_trader.execution.messages cimport SubmitOrder from nautilus_trader.model.c_enums.oms_type cimport OMSType from nautilus_trader.model.c_enums.order_side cimport OrderSide from nautilus_trader.model.c_enums.position_side cimport PositionSide @@ -56,6 +57,7 @@ cdef class Cache(CacheFacade): cdef dict _orders cdef dict _positions cdef dict _position_snapshots + cdef dict _submit_order_commands cdef dict _index_venue_account cdef dict _index_venue_orders @@ -89,6 +91,7 @@ cdef class Cache(CacheFacade): cpdef void cache_accounts(self) except * cpdef void cache_orders(self) except * cpdef void cache_positions(self) except * + cpdef void cache_commands(self) except * cpdef void build_index(self) except * cpdef bint check_integrity(self) except * cpdef bint check_residuals(self) except * @@ -112,6 +115,7 @@ cdef class Cache(CacheFacade): cpdef Order load_order(self, ClientOrderId order_id) cpdef Position load_position(self, PositionId position_id) cpdef void load_strategy(self, Strategy strategy) except * + cpdef SubmitOrder load_submit_order_command(self, ClientOrderId client_order_id) cpdef void add_order_book(self, OrderBook order_book) except * cpdef void add_ticker(self, Ticker ticker) except * @@ -128,6 +132,7 @@ cdef class Cache(CacheFacade): cpdef void add_position_id(self, PositionId position_id, Venue venue, ClientOrderId client_order_id, StrategyId strategy_id) except * cpdef void add_position(self, Position position, OMSType oms_type) except * cpdef void snapshot_position(self, Position position) except * + cpdef void add_submit_order_command(self, SubmitOrder command) except * cpdef void update_account(self, Account account) except * cpdef void update_order(self, Order order) except * diff --git a/nautilus_trader/cache/cache.pyx b/nautilus_trader/cache/cache.pyx index cafb916cc329..a50b848c3296 100644 --- a/nautilus_trader/cache/cache.pyx +++ b/nautilus_trader/cache/cache.pyx @@ -33,6 +33,7 @@ from nautilus_trader.common.logging cimport LoggerAdapter from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.rust.core cimport unix_timestamp from nautilus_trader.core.rust.core cimport unix_timestamp_us +from nautilus_trader.execution.messages cimport SubmitOrder from nautilus_trader.model.c_enums.oms_type cimport OMSType from nautilus_trader.model.c_enums.order_side cimport OrderSide from nautilus_trader.model.c_enums.position_side cimport PositionSide @@ -108,6 +109,7 @@ cdef class Cache(CacheFacade): self._orders = {} # type: dict[ClientOrderId, Order] self._positions = {} # type: dict[PositionId, Position] self._position_snapshots = {} # type: dict[PositionId, list[bytes]] + self._submit_order_commands = {} # type: dict[ClientOrderId, SubmitOrder] # Cache index self._index_venue_account = {} # type: dict[Venue, AccountId] @@ -138,7 +140,7 @@ cdef class Cache(CacheFacade): cpdef void cache_currencies(self) except *: """ - Clear the current currencies cache and load currencies from the execution + Clear the current currencies cache and load currencies from the cache database. """ self._log.debug(f"Loading currencies from database...") @@ -161,8 +163,8 @@ cdef class Cache(CacheFacade): cpdef void cache_instruments(self) except *: """ - Clear the current instruments cache and load instruments from the - execution database. + Clear the current instruments cache and load instruments from the cache + database. """ self._log.debug(f"Loading instruments from database...") @@ -179,7 +181,7 @@ cdef class Cache(CacheFacade): cpdef void cache_accounts(self) except *: """ - Clear the current accounts cache and load accounts from the execution + Clear the current accounts cache and load accounts from the cache database. """ self._log.debug(f"Loading accounts from database...") @@ -197,8 +199,7 @@ cdef class Cache(CacheFacade): cpdef void cache_orders(self) except *: """ - Clear the current orders cache and load orders from the execution - database. + Clear the current orders cache and load orders from the cache database. """ self._log.debug(f"Loading orders from database...") @@ -215,7 +216,7 @@ cdef class Cache(CacheFacade): cpdef void cache_positions(self) except *: """ - Clear the current positions cache and load positions from the execution + Clear the current positions cache and load positions from the cache database. """ self._log.debug(f"Loading positions from database...") @@ -231,6 +232,24 @@ cdef class Cache(CacheFacade): color=LogColor.BLUE if self._positions else LogColor.NORMAL ) + cpdef void cache_commands(self) except *: + """ + Clear the current submit order commands cache and load commands from the + cache database. + """ + self._log.debug(f"Loading commands from database...") + + if self._database is not None: + self._submit_order_commands = self._database.load_submit_order_commands() + else: + self._submit_order_commands = {} + + cdef int count = len(self._submit_order_commands) + self._log.info( + f"Cached {count} command{'' if count == 1 else 's'} from database.", + color=LogColor.BLUE if self._submit_order_commands else LogColor.NORMAL + ) + cpdef void build_index(self) except *: """ Clear the current cache index and re-build. @@ -565,6 +584,7 @@ cdef class Cache(CacheFacade): self._orders.clear() self._positions.clear() self._position_snapshots.clear() + self._submit_order_commands.clear() self._log.debug(f"Cleared cache.") @@ -836,6 +856,24 @@ cdef class Cache(CacheFacade): return self._positions.get(position_id) + cpdef SubmitOrder load_submit_order_command(self, ClientOrderId client_order_id): + """ + Load the command associated with the given client order ID (if found). + + Parameters + ---------- + client_order_id : ClientOrderId + The client order ID for the command to load. + + Returns + ------- + SubmitOrder or ``None`` + + """ + Condition.not_none(client_order_id, "client_order_id") + + return self._submit_order_commands.get(client_order_id) + cpdef void add_order_book(self, OrderBook order_book) except *: """ Add the given order book to the cache. @@ -1331,6 +1369,32 @@ cdef class Cache(CacheFacade): self._log.debug(f"Snapshot {repr(copied_position)}.") + cpdef void add_submit_order_command(self, SubmitOrder command) except *: + """ + Add the given command to the cache. + + Parameters + ---------- + command : SubmitOrder + The command to add to the cache. + + """ + Condition.not_none(command, "command") + Condition.not_in( + command.order.client_order_id, + self._submit_order_commands, + "command.order.client_order_id", + "self._submit_order_commands", + ) + + self._submit_order_commands[command.order.client_order_id] = command + + self._log.debug(f"Added command {command}") + + # Update database + if self._database is not None: + self._database.add_submit_order_command(command) + cpdef void update_account(self, Account account) except *: """ Update the given account in the cache. diff --git a/nautilus_trader/cache/database.pxd b/nautilus_trader/cache/database.pxd index 7ea7ad361bea..f2105bc16e17 100644 --- a/nautilus_trader/cache/database.pxd +++ b/nautilus_trader/cache/database.pxd @@ -15,6 +15,7 @@ from nautilus_trader.accounting.accounts.base cimport Account from nautilus_trader.common.logging cimport LoggerAdapter +from nautilus_trader.execution.messages cimport SubmitOrder from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.identifiers cimport AccountId from nautilus_trader.model.identifiers cimport ClientOrderId @@ -36,6 +37,7 @@ cdef class CacheDatabase: cpdef dict load_accounts(self) cpdef dict load_orders(self) cpdef dict load_positions(self) + cpdef dict load_submit_order_commands(self) cpdef Currency load_currency(self, str code) cpdef Instrument load_instrument(self, InstrumentId instrument_id) cpdef Account load_account(self, AccountId account_id) @@ -43,12 +45,14 @@ cdef class CacheDatabase: cpdef Position load_position(self, PositionId position_id) cpdef dict load_strategy(self, StrategyId strategy_id) cpdef void delete_strategy(self, StrategyId strategy_id) except * + cpdef SubmitOrder load_submit_order_command(self, ClientOrderId client_order_id) except * cpdef void add_currency(self, Currency currency) except * cpdef void add_instrument(self, Instrument instrument) except * cpdef void add_account(self, Account account) except * cpdef void add_order(self, Order order) except * cpdef void add_position(self, Position position) except * + cpdef void add_submit_order_command(self, SubmitOrder command) except * cpdef void update_account(self, Account account) except * cpdef void update_order(self, Order order) except * diff --git a/nautilus_trader/cache/database.pyx b/nautilus_trader/cache/database.pyx index 503b9de9b062..c11a87ab8d9f 100644 --- a/nautilus_trader/cache/database.pyx +++ b/nautilus_trader/cache/database.pyx @@ -69,6 +69,10 @@ cdef class CacheDatabase: """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + cpdef dict load_submit_order_commands(self): + """Abstract method (implement in subclass).""" + raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + cpdef Currency load_currency(self, str code): """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover @@ -97,6 +101,14 @@ cdef class CacheDatabase: """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + cpdef SubmitOrder load_submit_order_command(self, ClientOrderId client_order_id): + """Abstract method (implement in subclass).""" + raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + + cpdef void add_submit_order_command(self, SubmitOrder command) except *: + """Abstract method (implement in subclass).""" + raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover + cpdef void add_currency(self, Currency currency) except *: """Abstract method (implement in subclass).""" raise NotImplementedError("method must be implemented in the subclass") # pragma: no cover diff --git a/nautilus_trader/execution/emulator.pyx b/nautilus_trader/execution/emulator.pyx index 6cddfcc8ca66..e97c99095eef 100644 --- a/nautilus_trader/execution/emulator.pyx +++ b/nautilus_trader/execution/emulator.pyx @@ -98,6 +98,7 @@ cdef class OrderEmulator(Actor): cpdef void _start(self) except *: cdef list emulated_orders = self.cache.orders_emulated() if not emulated_orders: + self._log.info("No emulated orders to reactivate.") return cdef int emulated_count = len(emulated_orders) @@ -107,15 +108,13 @@ cdef class OrderEmulator(Actor): Order order SubmitOrder command for order in emulated_orders: - command = SubmitOrder( - trader_id=order.trader_id, - strategy_id=order.strategy_id, - order=order, - command_id=UUID4(), - ts_init=self.clock.timestamp_ns(), - position_id=None, # Custom position IDs not supported yet - client_id=None, # Custom routing not supported yet - ) + command = self.cache.load_submit_order_command(order.client_order_id) + if command is None: + self._log.error( + f"Cannot load `SubmitOrder` command for {repr(order.client_order_id)}: not found in cache." + ) + continue + self._log.info(f"Loaded {command}.", LogColor.BLUE) self._handle_submit_order(command) cpdef void _stop(self) except *: diff --git a/nautilus_trader/execution/engine.pyx b/nautilus_trader/execution/engine.pyx index 50b1b733cdc4..45a7913ac419 100644 --- a/nautilus_trader/execution/engine.pyx +++ b/nautilus_trader/execution/engine.pyx @@ -420,6 +420,9 @@ cdef class ExecutionEngine(Component): """ cdef uint64_t ts = unix_timestamp_ms() + # Cache commands first so that `SubmitOrder` commands don't revert orders + # back to their initialized state. + self._cache.cache_commands() self._cache.cache_currencies() self._cache.cache_instruments() self._cache.cache_accounts() diff --git a/nautilus_trader/infrastructure/cache.pxd b/nautilus_trader/infrastructure/cache.pxd index 835bc4d6aa1f..5dc7ead4daac 100644 --- a/nautilus_trader/infrastructure/cache.pxd +++ b/nautilus_trader/infrastructure/cache.pxd @@ -25,6 +25,7 @@ cdef class RedisCacheDatabase(CacheDatabase): cdef str _key_orders cdef str _key_positions cdef str _key_strategies + cdef str _key_commands cdef Serializer _serializer cdef object _redis diff --git a/nautilus_trader/infrastructure/cache.pyx b/nautilus_trader/infrastructure/cache.pyx index b9534262be74..6bb9974cd50d 100644 --- a/nautilus_trader/infrastructure/cache.pyx +++ b/nautilus_trader/infrastructure/cache.pyx @@ -23,7 +23,9 @@ from nautilus_trader.accounting.factory cimport AccountFactory from nautilus_trader.cache.database cimport CacheDatabase from nautilus_trader.common.logging cimport Logger from nautilus_trader.core.correctness cimport Condition +from nautilus_trader.execution.messages cimport SubmitOrder from nautilus_trader.model.c_enums.currency_type cimport CurrencyTypeParser +from nautilus_trader.model.c_enums.trigger_type cimport TriggerType from nautilus_trader.model.currency cimport Currency from nautilus_trader.model.events.order cimport OrderFilled from nautilus_trader.model.events.order cimport OrderInitialized @@ -47,14 +49,15 @@ except ImportError: # pragma: no cover redis = None -cdef str _UTF8 = 'utf-8' -cdef str _CURRENCIES = 'Currencies' -cdef str _INSTRUMENTS = 'Instruments' -cdef str _ACCOUNTS = 'Accounts' -cdef str _TRADER = 'Trader' -cdef str _ORDERS = 'Orders' -cdef str _POSITIONS = 'Positions' -cdef str _STRATEGIES = 'Strategies' +cdef str _UTF8 = "utf-8" +cdef str _CURRENCIES = "currencies" +cdef str _INSTRUMENTS = "instruments" +cdef str _ACCOUNTS = "accounts" +cdef str _TRADER = "trader" +cdef str _ORDERS = "orders" +cdef str _POSITIONS = "positions" +cdef str _STRATEGIES = "strategies" +cdef str _COMMANDS = "commands" cdef class RedisCacheDatabase(CacheDatabase): @@ -111,6 +114,7 @@ cdef class RedisCacheDatabase(CacheDatabase): self._key_orders = f"{self._key_trader}:{_ORDERS}:" # noqa self._key_positions = f"{self._key_trader}:{_POSITIONS}:" # noqa self._key_strategies = f"{self._key_trader}:{_STRATEGIES}:" # noqa + self._key_commands = f"{self._key_trader}:{_COMMANDS}:" # noqa # Serializers self._serializer = serializer @@ -272,6 +276,58 @@ cdef class RedisCacheDatabase(CacheDatabase): return positions + cpdef dict load_submit_order_commands(self): + """ + Load all commands from the database. + + Returns + ------- + dict[ClientOrderId, SubmitOrder] + + """ + cdef dict commands = {} + + cdef list command_keys = self._redis.keys(f"{self._key_commands}submit_order:*") + if not command_keys: + return commands + + cdef bytes key_bytes + cdef str key_str + cdef ClientOrderId client_order_id + cdef SubmitOrder command + for key_bytes in command_keys: + key_str = key_bytes.decode(_UTF8).rsplit(':', maxsplit=1)[1] + client_order_id = ClientOrderId(key_str) + command = self.load_submit_order_command(client_order_id) + + if command is not None: + commands[client_order_id] = command + + return commands + + cpdef SubmitOrder load_submit_order_command(self, ClientOrderId client_order_id): + """ + Load the command associated with the given client order ID (if found). + + Parameters + ---------- + client_order_id : ClientOrderId + The client order ID for the command to load. + + Returns + ------- + SubmitOrder or ``None`` + + """ + Condition.not_none(client_order_id, "client_order_id") + + cdef str key = f"{self._key_commands}submit_order:{client_order_id}" + cdef bytes command_bytes = self._redis.get(name=key) + if not command_bytes: + return None + + return self._serializer.deserialize(command_bytes) + cpdef Currency load_currency(self, str code): """ Load the currency associated with the given currency code (if found). @@ -588,6 +644,30 @@ cdef class RedisCacheDatabase(CacheDatabase): self._log.debug(f"Added Position(id={position.id.to_str()}).") + cpdef void add_submit_order_command(self, SubmitOrder command) except *: + """ + Add the given command to the database. + + Parameters + ---------- + command : SubmitOrder + The command to add. + + """ + Condition.not_none(command, "command") + + cdef str key = f"{self._key_commands}submit_order:{command.order.client_order_id.value}" + cdef bytes command_bytes = self._serializer.serialize(command) + cdef int reply = self._redis.set(key, command_bytes) + + # Check data integrity of reply + if reply > 1: # Reply = The length of the list after the push operation + self._log.warning( + f"The {repr(command)} already existed.", + ) + + self._log.debug(f"Added {command}.") + cpdef void update_strategy(self, Strategy strategy) except *: """ Update the given strategy state in the database. diff --git a/nautilus_trader/model/orders/limit.pyx b/nautilus_trader/model/orders/limit.pyx index a8652e922b52..9df84a850eb2 100644 --- a/nautilus_trader/model/orders/limit.pyx +++ b/nautilus_trader/model/orders/limit.pyx @@ -292,6 +292,7 @@ cdef class LimitOrder(Order): post_only=init.post_only, reduce_only=init.reduce_only, display_qty=Quantity.from_str_c(display_qty_str) if display_qty_str is not None else None, + emulation_trigger=init.emulation_trigger, contingency_type=init.contingency_type, order_list_id=init.order_list_id, linked_order_ids=init.linked_order_ids, diff --git a/nautilus_trader/trading/strategy.pyx b/nautilus_trader/trading/strategy.pyx index a47557bed906..a599e8eb3540 100644 --- a/nautilus_trader/trading/strategy.pyx +++ b/nautilus_trader/trading/strategy.pyx @@ -525,6 +525,8 @@ cdef class Strategy(Actor): client_id=client_id, ) + self.cache.add_submit_order_command(command) + self._send_risk_command(command) cpdef void submit_order_list(self, OrderList order_list, ClientId client_id = None) except *: diff --git a/tests/integration_tests/infrastructure/test_cache_database.py b/tests/integration_tests/infrastructure/test_cache_database.py index b5204c3c8e08..e5fe7ea59492 100644 --- a/tests/integration_tests/infrastructure/test_cache_database.py +++ b/tests/integration_tests/infrastructure/test_cache_database.py @@ -25,11 +25,14 @@ from nautilus_trader.backtest.engine import BacktestEngineConfig from nautilus_trader.common.clock import TestClock from nautilus_trader.common.logging import Logger +from nautilus_trader.common.logging import LogLevel from nautilus_trader.config import CacheDatabaseConfig +from nautilus_trader.core.uuid import UUID4 from nautilus_trader.data.engine import DataEngine from nautilus_trader.examples.strategies.ema_cross import EMACross from nautilus_trader.examples.strategies.ema_cross import EMACrossConfig from nautilus_trader.execution.engine import ExecutionEngine +from nautilus_trader.execution.messages import SubmitOrder from nautilus_trader.infrastructure.cache import RedisCacheDatabase from nautilus_trader.model.currencies import USD from nautilus_trader.model.currency import Currency @@ -37,7 +40,9 @@ from nautilus_trader.model.enums import CurrencyType from nautilus_trader.model.enums import OMSType from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.identifiers import ClientOrderId from nautilus_trader.model.identifiers import PositionId +from nautilus_trader.model.identifiers import StrategyId from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.objects import Money from nautilus_trader.model.objects import Price @@ -67,7 +72,10 @@ class TestRedisCacheDatabase: def setup(self): # Fixture Setup self.clock = TestClock() - self.logger = Logger(self.clock) + self.logger = Logger( + clock=self.clock, + level_stdout=LogLevel.DEBUG, + ) self.trader_id = TestIdStubs.trader_id() @@ -204,6 +212,32 @@ def test_add_position(self): # Assert assert self.database.load_position(position.id) == position + def test_add_submit_order_command(self): + # Arrange + order = self.strategy.order_factory.stop_market( + AUDUSD_SIM.id, + OrderSide.BUY, + Quantity.from_int(100000), + Price.from_str("1.00000"), + ) + + command = SubmitOrder( + trader_id=self.trader_id, + strategy_id=StrategyId("SCALPER-001"), + position_id=None, + order=order, + command_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + self.database.add_submit_order_command(command) + + # Act + result = self.database.load_submit_order_command(order.client_order_id) + + # Assert + assert command == result + def test_update_account(self): # Arrange account = TestExecStubs.cash_account() @@ -709,6 +743,40 @@ def test_delete_strategy(self): # Assert assert result == {} + def test_load_submit_order_command_when_not_in_database(self): + # Arrange, Act + result = self.cache.load_submit_order_command(ClientOrderId("O-123456789")) + + # Assert + assert result is None + + def test_load_submit_order_command(self): + # Arrange + order = self.strategy.order_factory.stop_market( + AUDUSD_SIM.id, + OrderSide.BUY, + Quantity.from_int(100000), + Price.from_str("1.00000"), + ) + + command = SubmitOrder( + trader_id=self.trader_id, + strategy_id=StrategyId("SCALPER-001"), + position_id=None, + order=order, + command_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + self.database.add_submit_order_command(command) + self.cache.add_submit_order_command(command) + + # Act + result = self.cache.load_submit_order_command(order.client_order_id) + + # Assert + assert command == result + def test_flush(self): # Arrange order1 = self.strategy.order_factory.market( @@ -754,7 +822,7 @@ def test_flush(self): assert self.database.load_position(position1.id) is None -class TestExecutionCacheWithRedisDatabaseTests: +class TestRedisCacheDatabaseIntegrity: def setup(self): # Fixture Setup config = BacktestEngineConfig( diff --git a/tests/test_kit/mocks/cache_database.py b/tests/test_kit/mocks/cache_database.py index 2e2681b976a3..9d82913e6378 100644 --- a/tests/test_kit/mocks/cache_database.py +++ b/tests/test_kit/mocks/cache_database.py @@ -13,9 +13,12 @@ # limitations under the License. # ------------------------------------------------------------------------------------------------- +from typing import Optional + from nautilus_trader.accounting.accounts.base import Account from nautilus_trader.cache.database import CacheDatabase from nautilus_trader.common.logging import Logger +from nautilus_trader.execution.messages import SubmitOrder from nautilus_trader.model.currency import Currency from nautilus_trader.model.identifiers import AccountId from nautilus_trader.model.identifiers import ClientOrderId @@ -46,11 +49,13 @@ def __init__(self, logger: Logger): self.accounts: dict[AccountId, Account] = {} self.orders: dict[ClientOrderId, Order] = {} self.positions: dict[PositionId, Position] = {} + self.submit_order_commands: dict[ClientOrderId, SubmitOrder] = {} def flush(self) -> None: self.accounts.clear() self.orders.clear() self.positions.clear() + self.submit_order_commands.clear() def load_currencies(self) -> dict: return self.currencies.copy() @@ -67,19 +72,22 @@ def load_orders(self) -> dict: def load_positions(self) -> dict: return self.positions.copy() + def load_submit_order_commands(self) -> dict: + return self.submit_order_commands.copy() + def load_currency(self, code: str) -> Currency: return self.currencies.get(code) - def load_instrument(self, instrument_id: InstrumentId) -> InstrumentId: + def load_instrument(self, instrument_id: InstrumentId) -> Optional[InstrumentId]: return self.instruments.get(instrument_id) - def load_account(self, account_id: AccountId) -> Account: + def load_account(self, account_id: AccountId) -> Optional[Account]: return self.accounts.get(account_id) - def load_order(self, client_order_id: ClientOrderId) -> Order: + def load_order(self, client_order_id: ClientOrderId) -> Optional[Order]: return self.orders.get(client_order_id) - def load_position(self, position_id: PositionId) -> Position: + def load_position(self, position_id: PositionId) -> Optional[Position]: return self.positions.get(position_id) def load_strategy(self, strategy_id: StrategyId) -> dict: @@ -88,6 +96,9 @@ def load_strategy(self, strategy_id: StrategyId) -> dict: def delete_strategy(self, strategy_id: StrategyId) -> None: pass + def load_submit_order_command(self, client_order_id: ClientOrderId) -> Optional[ClientOrderId]: + return self.submit_order_commands.get(client_order_id) + def add_currency(self, currency: Currency) -> None: self.currencies[currency.code] = currency @@ -103,6 +114,9 @@ def add_order(self, order: Order) -> None: def add_position(self, position: Position) -> None: self.positions[position.id] = position + def add_submit_order_command(self, command: SubmitOrder) -> None: + self.submit_order_commands[command.order.client_order_id] = command + def update_account(self, event: Account) -> None: pass # Would persist the event diff --git a/tests/unit_tests/cache/test_cache_execution.py b/tests/unit_tests/cache/test_cache_execution.py index 9f22f0f9875b..6704bbc10840 100644 --- a/tests/unit_tests/cache/test_cache_execution.py +++ b/tests/unit_tests/cache/test_cache_execution.py @@ -23,10 +23,12 @@ from nautilus_trader.cache.cache import Cache from nautilus_trader.common.clock import TestClock from nautilus_trader.common.logging import Logger +from nautilus_trader.core.uuid import UUID4 from nautilus_trader.data.engine import DataEngine from nautilus_trader.examples.strategies.ema_cross import EMACross from nautilus_trader.examples.strategies.ema_cross import EMACrossConfig from nautilus_trader.execution.engine import ExecutionEngine +from nautilus_trader.execution.messages import SubmitOrder from nautilus_trader.model.currencies import USD from nautilus_trader.model.currency import Currency from nautilus_trader.model.enums import AccountType @@ -159,6 +161,13 @@ def test_cache_positions_with_no_positions(self): # Assert assert True # No exception raised + def test_cache_commands_with_no_commands(self): + # Arrange, Act + self.cache.cache_commands() + + # Assert + assert True # No exception raised + def test_build_index_with_no_objects(self): # Arrange, Act self.cache.build_index() @@ -463,6 +472,56 @@ def test_load_position(self): # Assert assert result == position + def test_add_submit_order_command(self): + # Arrange + order = self.strategy.order_factory.stop_market( + AUDUSD_SIM.id, + OrderSide.BUY, + Quantity.from_int(100000), + Price.from_str("1.00000"), + ) + + command = SubmitOrder( + trader_id=self.trader_id, + strategy_id=StrategyId("SCALPER-001"), + position_id=None, + order=order, + command_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + # Act + self.cache.add_submit_order_command(command) + + # Assert + assert self.cache.load_submit_order_command(order.client_order_id) is not None + + def test_load_submit_order_command(self): + # Arrange + order = self.strategy.order_factory.stop_market( + AUDUSD_SIM.id, + OrderSide.BUY, + Quantity.from_int(100000), + Price.from_str("1.00000"), + ) + + command = SubmitOrder( + trader_id=self.trader_id, + strategy_id=StrategyId("SCALPER-001"), + position_id=None, + order=order, + command_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + self.cache.add_submit_order_command(command) + + # Act + result = self.cache.load_submit_order_command(order.client_order_id) + + # Assert + assert command == result + def test_update_order_for_submitted_order(self): # Arrange order = self.strategy.order_factory.stop_market( From 2b5ba071e7f3fc5299834f43443c3d1e530c01be Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 31 Oct 2022 17:41:01 +1100 Subject: [PATCH 26/38] Add `Order.would_reduce_only` convenience method --- RELEASES.md | 3 +- nautilus_trader/model/orders/base.pxd | 4 +- nautilus_trader/model/orders/base.pyx | 55 +++++++++++++++++---- tests/unit_tests/model/test_model_orders.py | 40 +++++++++++++++ 4 files changed, 91 insertions(+), 11 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 08c5127e4500..ffb37a73d4f3 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -12,7 +12,8 @@ Released on TBD (UTC). ### Enhancements - Improved accuracy of clocks for backtests (all clocks will now match generated `TimeEvent`s) - Added `Actor.request_instruments(...)` method -- Extended instrument(s) req/res handling for `DataClient` and `Actor +- Added `Order.would_reduce_only(...)` method +- Extended instrument(s) Req/Res handling for `DataClient` and `Actor ### Fixes None diff --git a/nautilus_trader/model/orders/base.pxd b/nautilus_trader/model/orders/base.pxd index d6c7b8928957..b77faa2117fb 100644 --- a/nautilus_trader/model/orders/base.pxd +++ b/nautilus_trader/model/orders/base.pxd @@ -147,7 +147,9 @@ cdef class Order: cdef OrderSide opposite_side_c(OrderSide side) except * @staticmethod - cdef OrderSide closing_side_c(PositionSide side) except * + cdef OrderSide closing_side_c(PositionSide position_side) except * + + cpdef bint would_reduce_only(self, PositionSide position_side, Quantity position_qty) except * cpdef void apply(self, OrderEvent event) except * diff --git a/nautilus_trader/model/orders/base.pyx b/nautilus_trader/model/orders/base.pyx index 0ccb5d42344b..2ca9313b42d2 100644 --- a/nautilus_trader/model/orders/base.pyx +++ b/nautilus_trader/model/orders/base.pyx @@ -24,6 +24,8 @@ from nautilus_trader.model.c_enums.order_status cimport OrderStatus from nautilus_trader.model.c_enums.order_status cimport OrderStatusParser from nautilus_trader.model.c_enums.order_type cimport OrderType from nautilus_trader.model.c_enums.order_type cimport OrderTypeParser +from nautilus_trader.model.c_enums.position_side cimport PositionSide +from nautilus_trader.model.c_enums.position_side cimport PositionSideParser from nautilus_trader.model.c_enums.time_in_force cimport TimeInForceParser from nautilus_trader.model.events.order cimport OrderAccepted from nautilus_trader.model.events.order cimport OrderCanceled @@ -635,18 +637,18 @@ cdef class Order: return OrderSide.BUY else: raise ValueError( # pragma: no cover (design-time error) - f"invalid `OrderSide`, was {side}", + f"invalid `OrderSide`, was {OrderSideParser.to_str(side)}", ) @staticmethod - cdef OrderSide closing_side_c(PositionSide side) except *: - if side == PositionSide.LONG: + cdef OrderSide closing_side_c(PositionSide position_side) except *: + if position_side == PositionSide.LONG: return OrderSide.SELL - elif side == PositionSide.SHORT: + elif position_side == PositionSide.SHORT: return OrderSide.BUY else: raise ValueError( # pragma: no cover (design-time error) - f"invalid `PositionSide`, was {side}", + f"invalid `PositionSide`, was {PositionSideParser.to_str(position_side)}", ) @staticmethod @@ -672,13 +674,13 @@ cdef class Order: return Order.opposite_side_c(side) @staticmethod - def closing_side(PositionSide side) -> OrderSide: + def closing_side(PositionSide position_side) -> OrderSide: """ Return the order side needed to close a position with the given side. Parameters ---------- - side : PositionSide {``LONG``, ``SHORT``} + position_side : PositionSide {``LONG``, ``SHORT``} The side of the position to close. Returns @@ -688,10 +690,45 @@ cdef class Order: Raises ------ ValueError - If `side` is ``FLAT`` or invalid. + If `position_side` is ``FLAT`` or invalid. """ - return Order.closing_side_c(side) + return Order.closing_side_c(position_side) + + cpdef bint would_reduce_only(self, PositionSide position_side, Quantity position_qty) except *: + """ + Whether the current order would only reduce the givien position if applied + in full. + + Parameters + ---------- + position_side : PositionSide {``FLAT``, ``LONG``, ``SHORT``} + The side of the position to check against. + position_qty : Quantity + The quantity of the position to check against. + + Returns + ------- + bool + + """ + Condition.not_none(position_qty, "position_qty") + + if position_side == PositionSide.FLAT: + return False # Would increase position + + if self.side == OrderSide.BUY: + if position_side == PositionSide.LONG: + return False # Would increase position + elif position_side == PositionSide.SHORT and self.leaves_qty._mem.raw > position_qty._mem.raw: + return False # Would increase position + elif self.side == OrderSide.SELL: + if position_side == PositionSide.SHORT: + return False # Would increase position + elif position_side == PositionSide.LONG and self.leaves_qty._mem.raw > position_qty._mem.raw: + return False # Would increase position + + return True # Would reduce only cpdef void apply(self, OrderEvent event) except *: """ diff --git a/tests/unit_tests/model/test_model_orders.py b/tests/unit_tests/model/test_model_orders.py index d2ac130b6317..977a2761d36c 100644 --- a/tests/unit_tests/model/test_model_orders.py +++ b/tests/unit_tests/model/test_model_orders.py @@ -112,6 +112,46 @@ def test_closing_side_returns_expected_sides(self, side, expected): # Assert assert result == expected + @pytest.mark.parametrize( + "order_side, position_side, position_qty, expected", + [ + [OrderSide.BUY, PositionSide.FLAT, Quantity.from_int(0), False], + [OrderSide.BUY, PositionSide.SHORT, Quantity.from_str("0.5"), False], + [OrderSide.BUY, PositionSide.SHORT, Quantity.from_int(1), True], + [OrderSide.BUY, PositionSide.SHORT, Quantity.from_int(2), True], + [OrderSide.BUY, PositionSide.LONG, Quantity.from_int(2), False], + [OrderSide.SELL, PositionSide.SHORT, Quantity.from_int(2), False], + [OrderSide.SELL, PositionSide.LONG, Quantity.from_int(2), True], + [OrderSide.SELL, PositionSide.LONG, Quantity.from_int(1), True], + [OrderSide.SELL, PositionSide.LONG, Quantity.from_str("0.5"), False], + [OrderSide.SELL, PositionSide.FLAT, Quantity.from_int(0), False], + ], + ) + def test_would_reduce_only_with_various_values_returns_expected( + self, + order_side, + position_side, + position_qty, + expected, + ): + # Arrange + order = MarketOrder( + self.trader_id, + self.strategy_id, + AUDUSD_SIM.id, + ClientOrderId("O-123456"), + order_side, + Quantity.from_int(1), + UUID4(), + 0, + ) + + # Act, Assert + assert ( + order.would_reduce_only(position_side=position_side, position_qty=position_qty) + == expected + ) + def test_market_order_with_quantity_zero_raises_value_error(self): # Arrange, Act, Assert with pytest.raises(ValueError): From ea7deecc90a91de366194d576d461d1aba02e268 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 31 Oct 2022 18:42:45 +1100 Subject: [PATCH 27/38] Fix exception clause when returning Python object --- nautilus_trader/cache/database.pxd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nautilus_trader/cache/database.pxd b/nautilus_trader/cache/database.pxd index f2105bc16e17..2815f461c1a8 100644 --- a/nautilus_trader/cache/database.pxd +++ b/nautilus_trader/cache/database.pxd @@ -45,7 +45,7 @@ cdef class CacheDatabase: cpdef Position load_position(self, PositionId position_id) cpdef dict load_strategy(self, StrategyId strategy_id) cpdef void delete_strategy(self, StrategyId strategy_id) except * - cpdef SubmitOrder load_submit_order_command(self, ClientOrderId client_order_id) except * + cpdef SubmitOrder load_submit_order_command(self, ClientOrderId client_order_id) cpdef void add_currency(self, Currency currency) except * cpdef void add_instrument(self, Instrument instrument) except * From de4028ae31f710b4fc3ea2db3b275b254bba26cc Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Mon, 31 Oct 2022 18:56:49 +1100 Subject: [PATCH 28/38] Improve `RiskEngine` handling of reduce_only orders --- RELEASES.md | 1 + nautilus_trader/risk/engine.pyx | 29 +++++---- .../backtest/test_backtest_exchange.py | 8 +-- tests/unit_tests/risk/test_risk_engine.py | 62 +++++++++++++++++++ 4 files changed, 81 insertions(+), 19 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index ffb37a73d4f3..ff4452328741 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -11,6 +11,7 @@ Released on TBD (UTC). ### Enhancements - Improved accuracy of clocks for backtests (all clocks will now match generated `TimeEvent`s) +- Improved risk engine checks for `reduce_only` orders - Added `Actor.request_instruments(...)` method - Added `Order.would_reduce_only(...)` method - Extended instrument(s) Req/Res handling for `DataClient` and `Actor diff --git a/nautilus_trader/risk/engine.pyx b/nautilus_trader/risk/engine.pyx index 8dbef129e379..c38dd45daba7 100644 --- a/nautilus_trader/risk/engine.pyx +++ b/nautilus_trader/risk/engine.pyx @@ -374,15 +374,17 @@ cdef class RiskEngine(Component): self._log.error(f"Cannot handle command: unrecognized {command}.") cdef void _handle_submit_order(self, SubmitOrder command) except *: + cdef Order order = command.order + # Check IDs for duplicate - if not self._check_order_id(command.order): + if not self._check_order_id(order): self._deny_command( command=command, - reason=f"Duplicate {repr(command.order.client_order_id)}") + reason=f"Duplicate {repr(order.client_order_id)}") return # Denied # Cache order - self._cache.add_order(command.order, command.position_id) + self._cache.add_order(order, command.position_id) if self.is_bypassed: # Perform no further risk checks or throttling @@ -395,16 +397,17 @@ cdef class RiskEngine(Component): # Check reduce only cdef Position position if command.position_id is not None: - position = self._cache.position(command.position_id) - if command.order.is_reduce_only and (position is None or position.is_closed_c()): - self._deny_command( - command=command, - reason=f"Order would increase position {repr(command.position_id)}", - ) - return # Denied + if order.is_reduce_only: + position = self._cache.position(command.position_id) + if position is None or not order.would_reduce_only(position.side, position.quantity): + self._deny_command( + command=command, + reason=f"Reduce only order would increase position {repr(command.position_id)}", + ) + return # Denied # Get instrument for order - cdef Instrument instrument = self._cache.instrument(command.order.instrument_id) + cdef Instrument instrument = self._cache.instrument(order.instrument_id) if instrument is None: self._deny_command( command=command, @@ -415,10 +418,10 @@ cdef class RiskEngine(Component): ######################################################################## # PRE-TRADE ORDER(S) CHECKS ######################################################################## - if not self._check_order(instrument, command.order): + if not self._check_order(instrument, order): return # Denied - if not self._check_orders_risk(instrument, [command.order]): + if not self._check_orders_risk(instrument, [order]): return # Denied if command.order.emulation_trigger == TriggerType.NONE: diff --git a/tests/unit_tests/backtest/test_backtest_exchange.py b/tests/unit_tests/backtest/test_backtest_exchange.py index 396c398bc295..87aae4cdf51f 100644 --- a/tests/unit_tests/backtest/test_backtest_exchange.py +++ b/tests/unit_tests/backtest/test_backtest_exchange.py @@ -2210,9 +2210,7 @@ def test_reduce_only_market_order_does_not_open_position_on_flip_scenario(self): self.exchange.process(0) # Assert - assert exit.status == OrderStatus.FILLED - assert exit.filled_qty == Quantity.from_int(200000) - assert exit.avg_px == Price.from_str("13.000") + assert exit.status == OrderStatus.DENIED def test_reduce_only_limit_order_does_not_open_position_on_flip_scenario(self): # Arrange: Prepare market @@ -2258,9 +2256,7 @@ def test_reduce_only_limit_order_does_not_open_position_on_flip_scenario(self): self.exchange.process_quote_tick(tick) # Assert - assert exit.status == OrderStatus.FILLED - assert exit.filled_qty == Quantity.from_int(200000) - assert exit.avg_px == Price.from_str("11.000") + assert exit.status == OrderStatus.DENIED def test_latency_model_submit_order(self): # Arrange diff --git a/tests/unit_tests/risk/test_risk_engine.py b/tests/unit_tests/risk/test_risk_engine.py index 3cc065db1799..8fe464979461 100644 --- a/tests/unit_tests/risk/test_risk_engine.py +++ b/tests/unit_tests/risk/test_risk_engine.py @@ -451,6 +451,68 @@ def test_submit_reduce_only_order_when_position_already_closed_then_denies(self) assert self.exec_engine.command_count == 2 assert self.exec_client.calls == ["_start", "submit_order", "submit_order"] + def test_submit_reduce_only_order_when_position_would_be_increased_then_denies(self): + # Arrange + self.exec_engine.start() + + strategy = Strategy() + strategy.register( + trader_id=self.trader_id, + portfolio=self.portfolio, + msgbus=self.msgbus, + cache=self.cache, + clock=self.clock, + logger=self.logger, + ) + + order1 = strategy.order_factory.market( + AUDUSD_SIM.id, + OrderSide.BUY, + Quantity.from_int(100000), + ) + + order2 = strategy.order_factory.market( + AUDUSD_SIM.id, + OrderSide.SELL, + Quantity.from_int(200000), + reduce_only=True, + ) + + submit_order1 = SubmitOrder( + trader_id=self.trader_id, + strategy_id=strategy.id, + position_id=None, + order=order1, + command_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + self.risk_engine.execute(submit_order1) + self.exec_engine.process(TestEventStubs.order_submitted(order1)) + self.exec_engine.process(TestEventStubs.order_accepted(order1)) + self.exec_engine.process(TestEventStubs.order_filled(order1, AUDUSD_SIM)) + + submit_order2 = SubmitOrder( + trader_id=self.trader_id, + strategy_id=strategy.id, + position_id=PositionId("P-19700101-000000-000-None-1"), + order=order2, + command_id=UUID4(), + ts_init=self.clock.timestamp_ns(), + ) + + # Act + self.risk_engine.execute(submit_order2) + self.exec_engine.process(TestEventStubs.order_submitted(order2)) + self.exec_engine.process(TestEventStubs.order_accepted(order2)) + self.exec_engine.process(TestEventStubs.order_filled(order2, AUDUSD_SIM)) + + # Assert + assert order1.status == OrderStatus.FILLED + assert order2.status == OrderStatus.DENIED + assert self.exec_engine.command_count == 1 + assert self.exec_client.calls == ["_start", "submit_order"] + def test_submit_order_reduce_only_order_with_custom_position_id_not_open_then_denies(self): # Arrange self.exec_engine.start() From 44156bc44621ed39b457f6ff4231ac9536c1572b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 1 Nov 2022 21:46:15 +1100 Subject: [PATCH 29/38] Improve core object memory management - All Rust structs now being freed --- nautilus_core/common/src/timer.rs | 27 ++++---- nautilus_core/core/src/uuid.rs | 13 +++- nautilus_core/model/src/data/bar.rs | 10 +++ .../model/src/identifiers/account_id.rs | 11 +++- .../model/src/identifiers/client_id.rs | 11 +++- .../model/src/identifiers/client_order_id.rs | 11 +++- .../model/src/identifiers/component_id.rs | 11 +++- .../model/src/identifiers/instrument_id.rs | 8 +++ .../model/src/identifiers/order_list_id.rs | 11 +++- .../model/src/identifiers/position_id.rs | 11 +++- .../model/src/identifiers/strategy_id.rs | 11 +++- nautilus_core/model/src/identifiers/symbol.rs | 11 +++- .../model/src/identifiers/trade_id.rs | 11 +++- .../model/src/identifiers/trader_id.rs | 11 +++- nautilus_core/model/src/identifiers/venue.rs | 11 +++- .../model/src/identifiers/venue_order_id.rs | 11 +++- nautilus_core/model/src/types/currency.rs | 19 ++++-- .../adapters/binance/common/types.py | 1 - nautilus_trader/backtest/matching_engine.pyx | 7 ++- nautilus_trader/common/timer.pyx | 4 +- nautilus_trader/core/includes/common.h | 10 +-- nautilus_trader/core/includes/core.h | 6 +- nautilus_trader/core/includes/model.h | 62 ++++++++++++++----- nautilus_trader/core/rust/common.pxd | 10 +-- nautilus_trader/core/rust/core.pxd | 6 +- nautilus_trader/core/rust/model.pxd | 62 ++++++++++++++----- nautilus_trader/core/uuid.pyx | 3 +- nautilus_trader/model/currency.pxd | 1 + nautilus_trader/model/currency.pyx | 6 +- nautilus_trader/model/data/bar.pyx | 26 ++++---- nautilus_trader/model/data/tick.pyx | 22 +++---- nautilus_trader/model/identifiers.pyx | 33 ++++------ nautilus_trader/model/objects.pxd | 1 + nautilus_trader/model/objects.pyx | 16 +++-- .../test_backtest_acceptance.py | 1 + tests/unit_tests/common/test_common_clock.py | 1 + 36 files changed, 338 insertions(+), 149 deletions(-) diff --git a/nautilus_core/common/src/timer.rs b/nautilus_core/common/src/timer.rs index 46327c9ece84..cb78c34816df 100644 --- a/nautilus_core/common/src/timer.rs +++ b/nautilus_core/common/src/timer.rs @@ -14,7 +14,7 @@ // ------------------------------------------------------------------------------------------------- use pyo3::ffi; -use pyo3::prelude::*; +use std::rc::Rc; use crate::enums::MessageCategory; use nautilus_core::string::{pystr_to_string, string_to_pystr}; @@ -22,12 +22,12 @@ use nautilus_core::time::{Timedelta, Timestamp}; use nautilus_core::uuid::UUID4; #[repr(C)] -#[pyclass] #[derive(Clone, Debug)] +#[allow(clippy::redundant_allocation)] // C ABI compatibility /// Represents a time event occurring at the event timestamp. pub struct TimeEvent { /// The event name. - pub name: Box, + pub name: Box>, /// The event ID. pub category: MessageCategory, // Only applicable to generic messages in the future /// The UNIX timestamp (nanoseconds) when the time event occurred. @@ -54,11 +54,6 @@ pub struct Vec_TimeEvent { //////////////////////////////////////////////////////////////////////////////// // C API //////////////////////////////////////////////////////////////////////////////// -#[no_mangle] -pub extern "C" fn time_event_free(event: TimeEvent) { - drop(event); // Memory freed here -} - /// # Safety /// - Assumes `name` is borrowed from a valid Python UTF-8 `str`. #[no_mangle] @@ -69,7 +64,7 @@ pub unsafe extern "C" fn time_event_new( ts_init: u64, ) -> TimeEvent { TimeEvent { - name: Box::new(pystr_to_string(name)), + name: Box::new(Rc::new(pystr_to_string(name))), category: MessageCategory::Event, event_id, ts_event, @@ -77,6 +72,16 @@ pub unsafe extern "C" fn time_event_new( } } +#[no_mangle] +pub extern "C" fn time_event_copy(event: &TimeEvent) -> TimeEvent { + event.clone() +} + +#[no_mangle] +pub extern "C" fn time_event_free(event: TimeEvent) { + drop(event); // Memory freed here +} + /// Returns a pointer to a valid Python UTF-8 string. /// /// # Safety @@ -145,7 +150,7 @@ impl TestTimer { pub fn pop_event(&self, event_id: UUID4, ts_init: Timestamp) -> TimeEvent { TimeEvent { - name: Box::new(self.name.clone()), + name: Box::new(Rc::new(self.name.clone())), category: MessageCategory::Event, event_id, ts_event: self.next_time_ns, @@ -177,7 +182,7 @@ impl Iterator for TestTimer { } else { let item = ( TimeEvent { - name: Box::new(self.name.clone()), + name: Box::new(Rc::new(self.name.clone())), category: MessageCategory::Event, event_id: UUID4::new(), ts_event: self.next_time_ns, diff --git a/nautilus_core/core/src/uuid.rs b/nautilus_core/core/src/uuid.rs index 94ba440db5d5..0a77b9b0fede 100644 --- a/nautilus_core/core/src/uuid.rs +++ b/nautilus_core/core/src/uuid.rs @@ -16,6 +16,7 @@ use std::collections::hash_map::DefaultHasher; use std::fmt::{Debug, Display, Formatter, Result}; use std::hash::{Hash, Hasher}; +use std::rc::Rc; use pyo3::ffi; @@ -25,15 +26,16 @@ use uuid::Uuid; #[repr(C)] #[derive(Clone, Hash, PartialEq, Eq, Debug)] #[allow(clippy::box_collection)] // C ABI compatibility +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct UUID4 { - value: Box, + value: Box>, } impl UUID4 { pub fn new() -> UUID4 { let uuid = Uuid::new_v4(); UUID4 { - value: Box::new(uuid.to_string()), + value: Box::new(Rc::new(uuid.to_string())), } } } @@ -42,7 +44,7 @@ impl From<&str> for UUID4 { fn from(s: &str) -> Self { let uuid = Uuid::try_parse(s).expect("invalid UUID string"); UUID4 { - value: Box::new(uuid.to_string()), + value: Box::new(Rc::new(uuid.to_string())), } } } @@ -67,6 +69,11 @@ pub extern "C" fn uuid4_new() -> UUID4 { UUID4::new() } +#[no_mangle] +pub extern "C" fn uuid4_copy(uuid4: &UUID4) -> UUID4 { + uuid4.clone() +} + #[no_mangle] pub extern "C" fn uuid4_free(uuid4: UUID4) { drop(uuid4); // Memory freed here diff --git a/nautilus_core/model/src/data/bar.rs b/nautilus_core/model/src/data/bar.rs index 184e388dd605..aa3028b8e63a 100644 --- a/nautilus_core/model/src/data/bar.rs +++ b/nautilus_core/model/src/data/bar.rs @@ -197,6 +197,11 @@ pub extern "C" fn bar_type_new( } } +#[no_mangle] +pub extern "C" fn bar_type_copy(bar_type: &BarType) -> BarType { + bar_type.clone() +} + #[no_mangle] pub extern "C" fn bar_type_eq(lhs: &BarType, rhs: &BarType) -> u8 { (lhs == rhs) as u8 @@ -329,6 +334,11 @@ pub unsafe extern "C" fn bar_to_pystr(bar: &Bar) -> *mut ffi::PyObject { string_to_pystr(bar.to_string().as_str()) } +#[no_mangle] +pub extern "C" fn bar_copy(bar: &Bar) -> Bar { + bar.clone() +} + #[no_mangle] pub extern "C" fn bar_free(bar: Bar) { drop(bar); // Memory freed here diff --git a/nautilus_core/model/src/identifiers/account_id.rs b/nautilus_core/model/src/identifiers/account_id.rs index 4d8497762900..44f1b87d99cc 100644 --- a/nautilus_core/model/src/identifiers/account_id.rs +++ b/nautilus_core/model/src/identifiers/account_id.rs @@ -16,6 +16,7 @@ use std::collections::hash_map::DefaultHasher; use std::fmt::{Debug, Display, Formatter, Result}; use std::hash::{Hash, Hasher}; +use std::rc::Rc; use pyo3::ffi; @@ -25,8 +26,9 @@ use nautilus_core::string::{pystr_to_string, string_to_pystr}; #[repr(C)] #[derive(Clone, Hash, PartialEq, Eq, Debug)] #[allow(clippy::box_collection)] // C ABI compatibility +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct AccountId { - value: Box, + value: Box>, } impl Display for AccountId { @@ -41,7 +43,7 @@ impl AccountId { correctness::string_contains(s, "-", "`TraderId` value"); AccountId { - value: Box::new(s.to_string()), + value: Box::new(Rc::new(s.to_string())), } } } @@ -59,6 +61,11 @@ pub unsafe extern "C" fn account_id_new(ptr: *mut ffi::PyObject) -> AccountId { AccountId::new(pystr_to_string(ptr).as_str()) } +#[no_mangle] +pub extern "C" fn account_id_copy(account_id: &AccountId) -> AccountId { + account_id.clone() +} + /// Frees the memory for the given `account_id` by dropping. #[no_mangle] pub extern "C" fn account_id_free(account_id: AccountId) { diff --git a/nautilus_core/model/src/identifiers/client_id.rs b/nautilus_core/model/src/identifiers/client_id.rs index 80d80c5afc71..f33163af4e17 100644 --- a/nautilus_core/model/src/identifiers/client_id.rs +++ b/nautilus_core/model/src/identifiers/client_id.rs @@ -16,6 +16,7 @@ use std::collections::hash_map::DefaultHasher; use std::fmt::{Debug, Display, Formatter, Result}; use std::hash::{Hash, Hasher}; +use std::rc::Rc; use pyo3::ffi; @@ -25,8 +26,9 @@ use nautilus_core::string::{pystr_to_string, string_to_pystr}; #[repr(C)] #[derive(Clone, Hash, PartialEq, Eq, Debug)] #[allow(clippy::box_collection)] // C ABI compatibility +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct ClientId { - value: Box, + value: Box>, } impl Display for ClientId { @@ -40,7 +42,7 @@ impl ClientId { correctness::valid_string(s, "`ClientId` value"); ClientId { - value: Box::new(s.to_string()), + value: Box::new(Rc::new(s.to_string())), } } } @@ -58,6 +60,11 @@ pub unsafe extern "C" fn client_id_new(ptr: *mut ffi::PyObject) -> ClientId { ClientId::new(pystr_to_string(ptr).as_str()) } +#[no_mangle] +pub extern "C" fn client_id_copy(client_id: &ClientId) -> ClientId { + client_id.clone() +} + /// Frees the memory for the given `client_id` by dropping. #[no_mangle] pub extern "C" fn client_id_free(client_id: ClientId) { diff --git a/nautilus_core/model/src/identifiers/client_order_id.rs b/nautilus_core/model/src/identifiers/client_order_id.rs index 1221689203d7..69c889451a83 100644 --- a/nautilus_core/model/src/identifiers/client_order_id.rs +++ b/nautilus_core/model/src/identifiers/client_order_id.rs @@ -16,6 +16,7 @@ use std::collections::hash_map::DefaultHasher; use std::fmt::{Debug, Display, Formatter, Result}; use std::hash::{Hash, Hasher}; +use std::rc::Rc; use pyo3::ffi; @@ -25,8 +26,9 @@ use nautilus_core::string::{pystr_to_string, string_to_pystr}; #[repr(C)] #[derive(Clone, Hash, PartialEq, Eq, Debug)] #[allow(clippy::box_collection)] // C ABI compatibility +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct ClientOrderId { - value: Box, + value: Box>, } impl Display for ClientOrderId { @@ -40,7 +42,7 @@ impl ClientOrderId { correctness::valid_string(s, "`ClientOrderId` value"); ClientOrderId { - value: Box::new(s.to_string()), + value: Box::new(Rc::new(s.to_string())), } } } @@ -58,6 +60,11 @@ pub unsafe extern "C" fn client_order_id_new(ptr: *mut ffi::PyObject) -> ClientO ClientOrderId::new(pystr_to_string(ptr).as_str()) } +#[no_mangle] +pub extern "C" fn client_order_id_copy(client_order_id: &ClientOrderId) -> ClientOrderId { + client_order_id.clone() +} + /// Frees the memory for the given `client_order_id` by dropping. #[no_mangle] pub extern "C" fn client_order_id_free(client_order_id: ClientOrderId) { diff --git a/nautilus_core/model/src/identifiers/component_id.rs b/nautilus_core/model/src/identifiers/component_id.rs index d1a04364e071..e3f5a5904ea7 100644 --- a/nautilus_core/model/src/identifiers/component_id.rs +++ b/nautilus_core/model/src/identifiers/component_id.rs @@ -16,6 +16,7 @@ use std::collections::hash_map::DefaultHasher; use std::fmt::{Debug, Display, Formatter, Result}; use std::hash::{Hash, Hasher}; +use std::rc::Rc; use pyo3::ffi; @@ -25,8 +26,9 @@ use nautilus_core::string::{pystr_to_string, string_to_pystr}; #[repr(C)] #[derive(Clone, Hash, PartialEq, Eq, Debug)] #[allow(clippy::box_collection)] // C ABI compatibility +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct ComponentId { - value: Box, + value: Box>, } impl Display for ComponentId { @@ -40,7 +42,7 @@ impl ComponentId { correctness::valid_string(s, "`ComponentId` value"); ComponentId { - value: Box::new(s.to_string()), + value: Box::new(Rc::new(s.to_string())), } } } @@ -58,6 +60,11 @@ pub unsafe extern "C" fn component_id_new(ptr: *mut ffi::PyObject) -> ComponentI ComponentId::new(pystr_to_string(ptr).as_str()) } +#[no_mangle] +pub extern "C" fn component_id_copy(component_id: &ComponentId) -> ComponentId { + component_id.clone() +} + /// Frees the memory for the given `component_id` by dropping. #[no_mangle] pub extern "C" fn component_id_free(component_id: ComponentId) { diff --git a/nautilus_core/model/src/identifiers/instrument_id.rs b/nautilus_core/model/src/identifiers/instrument_id.rs index dd4f13a36aa7..6e64bcf7faa5 100644 --- a/nautilus_core/model/src/identifiers/instrument_id.rs +++ b/nautilus_core/model/src/identifiers/instrument_id.rs @@ -84,6 +84,14 @@ pub unsafe extern "C" fn instrument_id_new( InstrumentId::new(symbol, venue) } +#[no_mangle] +pub extern "C" fn instrument_id_copy(instrument_id: &InstrumentId) -> InstrumentId { + InstrumentId { + symbol: instrument_id.symbol.clone(), + venue: instrument_id.venue.clone(), + } +} + /// Frees the memory for the given `instrument_id` by dropping. #[no_mangle] pub extern "C" fn instrument_id_free(instrument_id: InstrumentId) { diff --git a/nautilus_core/model/src/identifiers/order_list_id.rs b/nautilus_core/model/src/identifiers/order_list_id.rs index 6d16a0b47581..a01ff84acbda 100644 --- a/nautilus_core/model/src/identifiers/order_list_id.rs +++ b/nautilus_core/model/src/identifiers/order_list_id.rs @@ -16,6 +16,7 @@ use std::collections::hash_map::DefaultHasher; use std::fmt::{Debug, Display, Formatter, Result}; use std::hash::{Hash, Hasher}; +use std::rc::Rc; use pyo3::ffi; @@ -25,8 +26,9 @@ use nautilus_core::string::{pystr_to_string, string_to_pystr}; #[repr(C)] #[derive(Clone, Hash, PartialEq, Eq, Debug)] #[allow(clippy::box_collection)] // C ABI compatibility +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct OrderListId { - value: Box, + value: Box>, } impl Display for OrderListId { @@ -40,7 +42,7 @@ impl OrderListId { correctness::valid_string(s, "`OrderListId` value"); OrderListId { - value: Box::new(s.to_string()), + value: Box::new(Rc::new(s.to_string())), } } } @@ -58,6 +60,11 @@ pub unsafe extern "C" fn order_list_id_new(ptr: *mut ffi::PyObject) -> OrderList OrderListId::new(pystr_to_string(ptr).as_str()) } +#[no_mangle] +pub extern "C" fn order_list_id_copy(order_list_id: &OrderListId) -> OrderListId { + order_list_id.clone() +} + /// Frees the memory for the given `order_list_id` by dropping. #[no_mangle] pub extern "C" fn order_list_id_free(order_list_id: OrderListId) { diff --git a/nautilus_core/model/src/identifiers/position_id.rs b/nautilus_core/model/src/identifiers/position_id.rs index fd7eedc77685..434a1d595e3c 100644 --- a/nautilus_core/model/src/identifiers/position_id.rs +++ b/nautilus_core/model/src/identifiers/position_id.rs @@ -16,6 +16,7 @@ use std::collections::hash_map::DefaultHasher; use std::fmt::{Debug, Display, Formatter, Result}; use std::hash::{Hash, Hasher}; +use std::rc::Rc; use pyo3::ffi; @@ -25,8 +26,9 @@ use nautilus_core::string::{pystr_to_string, string_to_pystr}; #[repr(C)] #[derive(Clone, Hash, PartialEq, Eq, Debug)] #[allow(clippy::box_collection)] // C ABI compatibility +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct PositionId { - value: Box, + value: Box>, } impl Display for PositionId { @@ -40,7 +42,7 @@ impl PositionId { correctness::valid_string(s, "`PositionId` value"); PositionId { - value: Box::new(s.to_string()), + value: Box::new(Rc::new(s.to_string())), } } } @@ -58,6 +60,11 @@ pub unsafe extern "C" fn position_id_new(ptr: *mut ffi::PyObject) -> PositionId PositionId::new(pystr_to_string(ptr).as_str()) } +#[no_mangle] +pub extern "C" fn position_id_copy(position_id: &PositionId) -> PositionId { + position_id.clone() +} + /// Frees the memory for the given `position_id` by dropping. #[no_mangle] pub extern "C" fn position_id_free(position_id: PositionId) { diff --git a/nautilus_core/model/src/identifiers/strategy_id.rs b/nautilus_core/model/src/identifiers/strategy_id.rs index bbd81c85a9c5..5084273a77f7 100644 --- a/nautilus_core/model/src/identifiers/strategy_id.rs +++ b/nautilus_core/model/src/identifiers/strategy_id.rs @@ -14,6 +14,7 @@ // ------------------------------------------------------------------------------------------------- use std::fmt::{Debug, Display, Formatter, Result}; +use std::rc::Rc; use pyo3::ffi; @@ -23,8 +24,9 @@ use nautilus_core::string::pystr_to_string; #[repr(C)] #[derive(Clone, Hash, PartialEq, Eq, Debug)] #[allow(clippy::box_collection)] // C ABI compatibility +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct StrategyId { - value: Box, + value: Box>, } impl Display for StrategyId { @@ -41,7 +43,7 @@ impl StrategyId { } StrategyId { - value: Box::new(s.to_string()), + value: Box::new(Rc::new(s.to_string())), } } } @@ -59,6 +61,11 @@ pub unsafe extern "C" fn strategy_id_new(ptr: *mut ffi::PyObject) -> StrategyId StrategyId::new(pystr_to_string(ptr).as_str()) } +#[no_mangle] +pub extern "C" fn strategy_id_copy(strategy_id: &StrategyId) -> StrategyId { + strategy_id.clone() +} + /// Frees the memory for the given `strategy_id` by dropping. #[no_mangle] pub extern "C" fn strategy_id_free(strategy_id: StrategyId) { diff --git a/nautilus_core/model/src/identifiers/symbol.rs b/nautilus_core/model/src/identifiers/symbol.rs index d1b5504d4f1e..2ec269de2f64 100644 --- a/nautilus_core/model/src/identifiers/symbol.rs +++ b/nautilus_core/model/src/identifiers/symbol.rs @@ -16,6 +16,7 @@ use std::collections::hash_map::DefaultHasher; use std::fmt::{Debug, Display, Formatter, Result}; use std::hash::{Hash, Hasher}; +use std::rc::Rc; use pyo3::ffi; @@ -25,8 +26,9 @@ use nautilus_core::string::{pystr_to_string, string_to_pystr}; #[repr(C)] #[derive(Clone, Hash, PartialEq, Eq, Debug)] #[allow(clippy::box_collection)] // C ABI compatibility +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct Symbol { - value: Box, + pub value: Box>, } impl Display for Symbol { @@ -40,7 +42,7 @@ impl Symbol { correctness::valid_string(s, "`Symbol` value"); Symbol { - value: Box::new(s.to_string()), + value: Box::new(Rc::new(s.to_string())), } } } @@ -58,6 +60,11 @@ pub unsafe extern "C" fn symbol_new(ptr: *mut ffi::PyObject) -> Symbol { Symbol::new(pystr_to_string(ptr).as_str()) } +#[no_mangle] +pub extern "C" fn symbol_copy(symbol: &Symbol) -> Symbol { + symbol.clone() +} + /// Frees the memory for the given `symbol` by dropping. #[no_mangle] pub extern "C" fn symbol_free(symbol: Symbol) { diff --git a/nautilus_core/model/src/identifiers/trade_id.rs b/nautilus_core/model/src/identifiers/trade_id.rs index 001bdd1370e9..4e134b5ba7c0 100644 --- a/nautilus_core/model/src/identifiers/trade_id.rs +++ b/nautilus_core/model/src/identifiers/trade_id.rs @@ -16,6 +16,7 @@ use std::collections::hash_map::DefaultHasher; use std::fmt::{Debug, Display, Formatter, Result}; use std::hash::{Hash, Hasher}; +use std::rc::Rc; use pyo3::ffi; @@ -25,8 +26,9 @@ use nautilus_core::string::{pystr_to_string, string_to_pystr}; #[repr(C)] #[derive(Clone, Hash, PartialEq, Eq, Debug)] #[allow(clippy::box_collection)] // C ABI compatibility +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct TradeId { - pub value: Box, + pub value: Box>, } impl Display for TradeId { @@ -40,7 +42,7 @@ impl TradeId { correctness::valid_string(s, "`TradeId` value"); TradeId { - value: Box::new(s.to_string()), + value: Box::new(Rc::new(s.to_string())), } } } @@ -58,6 +60,11 @@ pub unsafe extern "C" fn trade_id_new(ptr: *mut ffi::PyObject) -> TradeId { TradeId::new(pystr_to_string(ptr).as_str()) } +#[no_mangle] +pub extern "C" fn trade_id_copy(trade_id: &TradeId) -> TradeId { + trade_id.clone() +} + /// Frees the memory for the given `trade_id` by dropping. #[no_mangle] pub extern "C" fn trade_id_free(trade_id: TradeId) { diff --git a/nautilus_core/model/src/identifiers/trader_id.rs b/nautilus_core/model/src/identifiers/trader_id.rs index f77964946544..dd8b8fd21254 100644 --- a/nautilus_core/model/src/identifiers/trader_id.rs +++ b/nautilus_core/model/src/identifiers/trader_id.rs @@ -14,6 +14,7 @@ // ------------------------------------------------------------------------------------------------- use std::fmt::{Debug, Display, Formatter, Result}; +use std::rc::Rc; use pyo3::ffi; @@ -23,8 +24,9 @@ use nautilus_core::string::pystr_to_string; #[repr(C)] #[derive(Clone, Hash, PartialEq, Eq, Debug)] #[allow(clippy::box_collection)] // C ABI compatibility +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct TraderId { - value: Box, + value: Box>, } impl Display for TraderId { @@ -39,7 +41,7 @@ impl TraderId { correctness::string_contains(s, "-", "`TraderId` value"); TraderId { - value: Box::new(s.to_string()), + value: Box::new(Rc::new(s.to_string())), } } } @@ -57,6 +59,11 @@ pub unsafe extern "C" fn trader_id_new(ptr: *mut ffi::PyObject) -> TraderId { TraderId::new(pystr_to_string(ptr).as_str()) } +#[no_mangle] +pub extern "C" fn trader_id_copy(trader_id: &TraderId) -> TraderId { + trader_id.clone() +} + /// Frees the memory for the given `trader_id` by dropping. #[no_mangle] pub extern "C" fn trader_id_free(trader_id: TraderId) { diff --git a/nautilus_core/model/src/identifiers/venue.rs b/nautilus_core/model/src/identifiers/venue.rs index 824581678477..4c82cacc351c 100644 --- a/nautilus_core/model/src/identifiers/venue.rs +++ b/nautilus_core/model/src/identifiers/venue.rs @@ -16,6 +16,7 @@ use std::collections::hash_map::DefaultHasher; use std::fmt::{Debug, Display, Formatter, Result}; use std::hash::{Hash, Hasher}; +use std::rc::Rc; use pyo3::ffi; @@ -25,8 +26,9 @@ use nautilus_core::string::{pystr_to_string, string_to_pystr}; #[repr(C)] #[derive(Clone, Hash, PartialEq, Eq, Debug)] #[allow(clippy::box_collection)] // C ABI compatibility +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct Venue { - value: Box, + pub value: Box>, } impl Display for Venue { @@ -40,7 +42,7 @@ impl Venue { correctness::valid_string(s, "`Venue` value"); Venue { - value: Box::new(s.to_string()), + value: Box::new(Rc::new(s.to_string())), } } } @@ -58,6 +60,11 @@ pub unsafe extern "C" fn venue_new(ptr: *mut ffi::PyObject) -> Venue { Venue::new(pystr_to_string(ptr).as_str()) } +#[no_mangle] +pub extern "C" fn venue_copy(venue: &Venue) -> Venue { + venue.clone() +} + /// Frees the memory for the given `venue` by dropping. #[no_mangle] pub extern "C" fn venue_free(venue: Venue) { diff --git a/nautilus_core/model/src/identifiers/venue_order_id.rs b/nautilus_core/model/src/identifiers/venue_order_id.rs index b46c36bfb0f9..1b184c3610f0 100644 --- a/nautilus_core/model/src/identifiers/venue_order_id.rs +++ b/nautilus_core/model/src/identifiers/venue_order_id.rs @@ -16,6 +16,7 @@ use std::collections::hash_map::DefaultHasher; use std::fmt::{Debug, Display, Formatter, Result}; use std::hash::{Hash, Hasher}; +use std::rc::Rc; use pyo3::ffi; @@ -25,8 +26,9 @@ use nautilus_core::string::{pystr_to_string, string_to_pystr}; #[repr(C)] #[derive(Clone, Hash, PartialEq, Eq, Debug)] #[allow(clippy::box_collection)] // C ABI compatibility +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct VenueOrderId { - value: Box, + value: Box>, } impl Display for VenueOrderId { @@ -40,7 +42,7 @@ impl VenueOrderId { correctness::valid_string(s, "`VenueOrderId` value"); VenueOrderId { - value: Box::new(s.to_string()), + value: Box::new(Rc::new(s.to_string())), } } } @@ -58,6 +60,11 @@ pub unsafe extern "C" fn venue_order_id_new(ptr: *mut ffi::PyObject) -> VenueOrd VenueOrderId::new(pystr_to_string(ptr).as_str()) } +#[no_mangle] +pub extern "C" fn venue_order_id_copy(venue_order_id: &VenueOrderId) -> VenueOrderId { + venue_order_id.clone() +} + /// Frees the memory for the given `venue_order_id` by dropping. #[no_mangle] pub extern "C" fn venue_order_id_free(venue_order_id: VenueOrderId) { diff --git a/nautilus_core/model/src/types/currency.rs b/nautilus_core/model/src/types/currency.rs index 4c74aedb7e67..fe6728eb96ee 100644 --- a/nautilus_core/model/src/types/currency.rs +++ b/nautilus_core/model/src/types/currency.rs @@ -15,6 +15,7 @@ use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; +use std::rc::Rc; use pyo3::ffi; @@ -24,11 +25,12 @@ use nautilus_core::string::{pystr_to_string, string_to_pystr}; #[repr(C)] #[derive(Eq, PartialEq, Clone, Hash, Debug)] +#[allow(clippy::redundant_allocation)] // C ABI compatibility pub struct Currency { - pub code: Box, + pub code: Box>, pub precision: u8, pub iso4217: u16, - pub name: Box, + pub name: Box>, pub currency_type: CurrencyType, } @@ -45,10 +47,10 @@ impl Currency { correctness::u8_in_range_inclusive(precision, 0, 9, "`Currency` precision"); Currency { - code: Box::new(code.to_string()), + code: Box::new(Rc::new(code.to_string())), precision, iso4217, - name: Box::new(name.to_string()), + name: Box::new(Rc::new(name.to_string())), currency_type, } } @@ -71,14 +73,19 @@ pub unsafe extern "C" fn currency_from_py( currency_type: CurrencyType, ) -> Currency { Currency { - code: Box::from(pystr_to_string(code_ptr)), + code: Box::from(Rc::new(pystr_to_string(code_ptr))), precision, iso4217, - name: Box::from(pystr_to_string(name_ptr)), + name: Box::from(Rc::new(pystr_to_string(name_ptr))), currency_type, } } +#[no_mangle] +pub extern "C" fn currency_copy(currency: &Currency) -> Currency { + currency.clone() +} + #[no_mangle] pub extern "C" fn currency_free(currency: Currency) { drop(currency); // Memory freed here diff --git a/nautilus_trader/adapters/binance/common/types.py b/nautilus_trader/adapters/binance/common/types.py index 715c9b635c89..cbd3284885fb 100644 --- a/nautilus_trader/adapters/binance/common/types.py +++ b/nautilus_trader/adapters/binance/common/types.py @@ -97,7 +97,6 @@ def __init__( self.taker_sell_quote_volume = self.quote_volume - self.taker_buy_quote_volume def __del__(self) -> None: - # TODO(cs): Investigate dealloc (not currently being freed) pass # Avoid double free (segmentation fault) def __getstate__(self): diff --git a/nautilus_trader/backtest/matching_engine.pyx b/nautilus_trader/backtest/matching_engine.pyx index 745299e00a5b..b8bdec0ec523 100644 --- a/nautilus_trader/backtest/matching_engine.pyx +++ b/nautilus_trader/backtest/matching_engine.pyx @@ -25,6 +25,7 @@ from nautilus_trader.common.clock cimport TestClock from nautilus_trader.common.logging cimport Logger from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.rust.model cimport price_new +from nautilus_trader.core.rust.model cimport trade_id_copy from nautilus_trader.core.uuid cimport UUID4 from nautilus_trader.execution.matching_core cimport MatchingCore from nautilus_trader.execution.trailing_calculator cimport TrailingStopCalculator @@ -401,7 +402,7 @@ cdef class OrderMatchingEngine: if bar._mem.high.raw > self._core.last_raw: # Direct memory comparison tick._mem.price = bar._mem.high # Direct memory assignment tick._mem.aggressor_side = AggressorSide.BUY # Direct memory assignment - tick._mem.trade_id = self._generate_trade_id()._mem + tick._mem.trade_id = trade_id_copy(&self._generate_trade_id()._mem) self._book.update_trade_tick(tick) self.iterate(tick.ts_init) self._core.set_last(bar._mem.high) @@ -410,7 +411,7 @@ cdef class OrderMatchingEngine: if bar._mem.low.raw < self._core.last_raw: # Direct memory comparison tick._mem.price = bar._mem.low # Direct memory assignment tick._mem.aggressor_side = AggressorSide.SELL - tick._mem.trade_id = self._generate_trade_id()._mem + tick._mem.trade_id = trade_id_copy(&self._generate_trade_id()._mem) self._book.update_trade_tick(tick) self.iterate(tick.ts_init) self._core.set_last(bar._mem.low) @@ -419,7 +420,7 @@ cdef class OrderMatchingEngine: if bar._mem.close.raw != self._core.last_raw: # Direct memory comparison tick._mem.price = bar._mem.close # Direct memory assignment tick._mem.aggressor_side = AggressorSide.BUY if bar._mem.close.raw > self._core.last_raw else AggressorSide.SELL - tick._mem.trade_id = self._generate_trade_id()._mem + tick._mem.trade_id = trade_id_copy(&self._generate_trade_id()._mem) self._book.update_trade_tick(tick) self.iterate(tick.ts_init) self._core.set_last(bar._mem.close) diff --git a/nautilus_trader/common/timer.pyx b/nautilus_trader/common/timer.pyx index 0cf398b456ef..8849048d96f1 100644 --- a/nautilus_trader/common/timer.pyx +++ b/nautilus_trader/common/timer.pyx @@ -64,9 +64,7 @@ cdef class TimeEvent(Event): ) def __del__(self) -> None: - # TODO(cs): Investigate dealloc (not currently being freed) - # time_event_free(self._mem) # `self._mem` moved to Rust (then dropped) - pass + time_event_free(self._mem) # `self._mem` moved to Rust (then dropped) cdef str to_str(self): return time_event_name(&self._mem) diff --git a/nautilus_trader/core/includes/common.h b/nautilus_trader/core/includes/common.h index b69941ccbde2..e74a61f7df9e 100644 --- a/nautilus_trader/core/includes/common.h +++ b/nautilus_trader/core/includes/common.h @@ -33,7 +33,7 @@ typedef enum MessageCategory { typedef struct Logger_t Logger_t; -typedef struct String String; +typedef struct Rc_String Rc_String; typedef struct TestClock TestClock; @@ -48,7 +48,7 @@ typedef struct TimeEvent_t { /** * The event name. */ - struct String *name; + struct Rc_String *name; /** * The event ID. */ @@ -189,8 +189,6 @@ void logger_log(struct CLogger *logger, PyObject *component_ptr, PyObject *msg_ptr); -void time_event_free(struct TimeEvent_t event); - /** * # Safety * - Assumes `name` is borrowed from a valid Python UTF-8 `str`. @@ -200,6 +198,10 @@ struct TimeEvent_t time_event_new(PyObject *name, uint64_t ts_event, uint64_t ts_init); +struct TimeEvent_t time_event_copy(const struct TimeEvent_t *event); + +void time_event_free(struct TimeEvent_t event); + /** * Returns a pointer to a valid Python UTF-8 string. * diff --git a/nautilus_trader/core/includes/core.h b/nautilus_trader/core/includes/core.h index 0e73e97246a4..a34fb562d1f9 100644 --- a/nautilus_trader/core/includes/core.h +++ b/nautilus_trader/core/includes/core.h @@ -5,7 +5,7 @@ #include #include -typedef struct String String; +typedef struct Rc_String Rc_String; /** * CVec is a C compatible struct that stores an opaque pointer to a block of @@ -32,7 +32,7 @@ typedef struct CVec { } CVec; typedef struct UUID4_t { - struct String *value; + struct Rc_String *value; } UUID4_t; void cvec_drop(struct CVec cvec); @@ -100,6 +100,8 @@ uint64_t unix_timestamp_ns(void); struct UUID4_t uuid4_new(void); +struct UUID4_t uuid4_copy(const struct UUID4_t *uuid4); + void uuid4_free(struct UUID4_t uuid4); /** diff --git a/nautilus_trader/core/includes/model.h b/nautilus_trader/core/includes/model.h index 4cca7f91fc7e..5dc367e16dab 100644 --- a/nautilus_trader/core/includes/model.h +++ b/nautilus_trader/core/includes/model.h @@ -73,7 +73,7 @@ typedef struct BTreeMap_BookPrice__Level BTreeMap_BookPrice__Level; typedef struct HashMap_u64__BookPrice HashMap_u64__BookPrice; -typedef struct String String; +typedef struct Rc_String Rc_String; typedef struct BarSpecification_t { uint64_t step; @@ -82,11 +82,11 @@ typedef struct BarSpecification_t { } BarSpecification_t; typedef struct Symbol_t { - struct String *value; + struct Rc_String *value; } Symbol_t; typedef struct Venue_t { - struct String *value; + struct Rc_String *value; } Venue_t; typedef struct InstrumentId_t { @@ -135,7 +135,7 @@ typedef struct QuoteTick_t { } QuoteTick_t; typedef struct TradeId_t { - struct String *value; + struct Rc_String *value; } TradeId_t; /** @@ -152,39 +152,39 @@ typedef struct TradeTick_t { } TradeTick_t; typedef struct AccountId_t { - struct String *value; + struct Rc_String *value; } AccountId_t; typedef struct ClientId_t { - struct String *value; + struct Rc_String *value; } ClientId_t; typedef struct ClientOrderId_t { - struct String *value; + struct Rc_String *value; } ClientOrderId_t; typedef struct ComponentId_t { - struct String *value; + struct Rc_String *value; } ComponentId_t; typedef struct OrderListId_t { - struct String *value; + struct Rc_String *value; } OrderListId_t; typedef struct PositionId_t { - struct String *value; + struct Rc_String *value; } PositionId_t; typedef struct StrategyId_t { - struct String *value; + struct Rc_String *value; } StrategyId_t; typedef struct TraderId_t { - struct String *value; + struct Rc_String *value; } TraderId_t; typedef struct VenueOrderId_t { - struct String *value; + struct Rc_String *value; } VenueOrderId_t; typedef struct Ladder { @@ -203,10 +203,10 @@ typedef struct OrderBook { } OrderBook; typedef struct Currency_t { - struct String *code; + struct Rc_String *code; uint8_t precision; uint16_t iso4217; - struct String *name; + struct Rc_String *name; enum CurrencyType currency_type; } Currency_t; @@ -253,6 +253,8 @@ struct BarType_t bar_type_new(struct InstrumentId_t instrument_id, struct BarSpecification_t spec, uint8_t aggregation_source); +struct BarType_t bar_type_copy(const struct BarType_t *bar_type); + uint8_t bar_type_eq(const struct BarType_t *lhs, const struct BarType_t *rhs); uint8_t bar_type_lt(const struct BarType_t *lhs, const struct BarType_t *rhs); @@ -309,6 +311,8 @@ struct Bar_t bar_new_from_raw(struct BarType_t bar_type, */ PyObject *bar_to_pystr(const struct Bar_t *bar); +struct Bar_t bar_copy(const struct Bar_t *bar); + void bar_free(struct Bar_t bar); uint8_t bar_eq(const struct Bar_t *lhs, const struct Bar_t *rhs); @@ -377,6 +381,8 @@ PyObject *trade_tick_to_pystr(const struct TradeTick_t *tick); */ struct AccountId_t account_id_new(PyObject *ptr); +struct AccountId_t account_id_copy(const struct AccountId_t *account_id); + /** * Frees the memory for the given `account_id` by dropping. */ @@ -404,6 +410,8 @@ uint64_t account_id_hash(const struct AccountId_t *account_id); */ struct ClientId_t client_id_new(PyObject *ptr); +struct ClientId_t client_id_copy(const struct ClientId_t *client_id); + /** * Frees the memory for the given `client_id` by dropping. */ @@ -431,6 +439,8 @@ uint64_t client_id_hash(const struct ClientId_t *client_id); */ struct ClientOrderId_t client_order_id_new(PyObject *ptr); +struct ClientOrderId_t client_order_id_copy(const struct ClientOrderId_t *client_order_id); + /** * Frees the memory for the given `client_order_id` by dropping. */ @@ -458,6 +468,8 @@ uint64_t client_order_id_hash(const struct ClientOrderId_t *client_order_id); */ struct ComponentId_t component_id_new(PyObject *ptr); +struct ComponentId_t component_id_copy(const struct ComponentId_t *component_id); + /** * Frees the memory for the given `component_id` by dropping. */ @@ -496,6 +508,8 @@ uint64_t component_id_hash(const struct ComponentId_t *component_id); */ struct InstrumentId_t instrument_id_new(PyObject *symbol_ptr, PyObject *venue_ptr); +struct InstrumentId_t instrument_id_copy(const struct InstrumentId_t *instrument_id); + /** * Frees the memory for the given `instrument_id` by dropping. */ @@ -523,6 +537,8 @@ uint64_t instrument_id_hash(const struct InstrumentId_t *instrument_id); */ struct OrderListId_t order_list_id_new(PyObject *ptr); +struct OrderListId_t order_list_id_copy(const struct OrderListId_t *order_list_id); + /** * Frees the memory for the given `order_list_id` by dropping. */ @@ -550,6 +566,8 @@ uint64_t order_list_id_hash(const struct OrderListId_t *order_list_id); */ struct PositionId_t position_id_new(PyObject *ptr); +struct PositionId_t position_id_copy(const struct PositionId_t *position_id); + /** * Frees the memory for the given `position_id` by dropping. */ @@ -577,6 +595,8 @@ uint64_t position_id_hash(const struct PositionId_t *position_id); */ struct StrategyId_t strategy_id_new(PyObject *ptr); +struct StrategyId_t strategy_id_copy(const struct StrategyId_t *strategy_id); + /** * Frees the memory for the given `strategy_id` by dropping. */ @@ -590,6 +610,8 @@ void strategy_id_free(struct StrategyId_t strategy_id); */ struct Symbol_t symbol_new(PyObject *ptr); +struct Symbol_t symbol_copy(const struct Symbol_t *symbol); + /** * Frees the memory for the given `symbol` by dropping. */ @@ -617,6 +639,8 @@ uint64_t symbol_hash(const struct Symbol_t *symbol); */ struct TradeId_t trade_id_new(PyObject *ptr); +struct TradeId_t trade_id_copy(const struct TradeId_t *trade_id); + /** * Frees the memory for the given `trade_id` by dropping. */ @@ -644,6 +668,8 @@ uint64_t trade_id_hash(const struct TradeId_t *trade_id); */ struct TraderId_t trader_id_new(PyObject *ptr); +struct TraderId_t trader_id_copy(const struct TraderId_t *trader_id); + /** * Frees the memory for the given `trader_id` by dropping. */ @@ -657,6 +683,8 @@ void trader_id_free(struct TraderId_t trader_id); */ struct Venue_t venue_new(PyObject *ptr); +struct Venue_t venue_copy(const struct Venue_t *venue); + /** * Frees the memory for the given `venue` by dropping. */ @@ -684,6 +712,8 @@ uint64_t venue_hash(const struct Venue_t *venue); */ struct VenueOrderId_t venue_order_id_new(PyObject *ptr); +struct VenueOrderId_t venue_order_id_copy(const struct VenueOrderId_t *venue_order_id); + /** * Frees the memory for the given `venue_order_id` by dropping. */ @@ -718,6 +748,8 @@ struct Currency_t currency_from_py(PyObject *code_ptr, PyObject *name_ptr, enum CurrencyType currency_type); +struct Currency_t currency_copy(const struct Currency_t *currency); + void currency_free(struct Currency_t currency); /** diff --git a/nautilus_trader/core/rust/common.pxd b/nautilus_trader/core/rust/common.pxd index 8dfed2c7041b..b2104176155d 100644 --- a/nautilus_trader/core/rust/common.pxd +++ b/nautilus_trader/core/rust/common.pxd @@ -32,7 +32,7 @@ cdef extern from "../includes/common.h": cdef struct Logger_t: pass - cdef struct String: + cdef struct Rc_String: pass cdef struct TestClock: @@ -44,7 +44,7 @@ cdef extern from "../includes/common.h": # Represents a time event occurring at the event timestamp. cdef struct TimeEvent_t: # The event name. - String *name; + Rc_String *name; # The event ID. MessageCategory category; # The UNIX timestamp (nanoseconds) when the time event occurred. @@ -152,8 +152,6 @@ cdef extern from "../includes/common.h": PyObject *component_ptr, PyObject *msg_ptr); - void time_event_free(TimeEvent_t event); - # # Safety # - Assumes `name` is borrowed from a valid Python UTF-8 `str`. TimeEvent_t time_event_new(PyObject *name, @@ -161,6 +159,10 @@ cdef extern from "../includes/common.h": uint64_t ts_event, uint64_t ts_init); + TimeEvent_t time_event_copy(const TimeEvent_t *event); + + void time_event_free(TimeEvent_t event); + # Returns a pointer to a valid Python UTF-8 string. # # # Safety diff --git a/nautilus_trader/core/rust/core.pxd b/nautilus_trader/core/rust/core.pxd index 0d6267be1d84..e768f1fa7223 100644 --- a/nautilus_trader/core/rust/core.pxd +++ b/nautilus_trader/core/rust/core.pxd @@ -5,7 +5,7 @@ from libc.stdint cimport uint8_t, uint64_t, uintptr_t cdef extern from "../includes/core.h": - cdef struct String: + cdef struct Rc_String: pass # CVec is a C compatible struct that stores an opaque pointer to a block of @@ -24,7 +24,7 @@ cdef extern from "../includes/core.h": uintptr_t cap; cdef struct UUID4_t: - String *value; + Rc_String *value; void cvec_drop(CVec cvec); @@ -69,6 +69,8 @@ cdef extern from "../includes/core.h": UUID4_t uuid4_new(); + UUID4_t uuid4_copy(const UUID4_t *uuid4); + void uuid4_free(UUID4_t uuid4); # Returns a `UUID4` from a valid Python object pointer. diff --git a/nautilus_trader/core/rust/model.pxd b/nautilus_trader/core/rust/model.pxd index 4f6c4de73f44..e081355d526f 100644 --- a/nautilus_trader/core/rust/model.pxd +++ b/nautilus_trader/core/rust/model.pxd @@ -69,7 +69,7 @@ cdef extern from "../includes/model.h": cdef struct HashMap_u64__BookPrice: pass - cdef struct String: + cdef struct Rc_String: pass cdef struct BarSpecification_t: @@ -78,10 +78,10 @@ cdef extern from "../includes/model.h": PriceType price_type; cdef struct Symbol_t: - String *value; + Rc_String *value; cdef struct Venue_t: - String *value; + Rc_String *value; cdef struct InstrumentId_t: Symbol_t symbol; @@ -121,7 +121,7 @@ cdef extern from "../includes/model.h": uint64_t ts_init; cdef struct TradeId_t: - String *value; + Rc_String *value; # Represents a single trade tick in a financial market. cdef struct TradeTick_t: @@ -134,31 +134,31 @@ cdef extern from "../includes/model.h": uint64_t ts_init; cdef struct AccountId_t: - String *value; + Rc_String *value; cdef struct ClientId_t: - String *value; + Rc_String *value; cdef struct ClientOrderId_t: - String *value; + Rc_String *value; cdef struct ComponentId_t: - String *value; + Rc_String *value; cdef struct OrderListId_t: - String *value; + Rc_String *value; cdef struct PositionId_t: - String *value; + Rc_String *value; cdef struct StrategyId_t: - String *value; + Rc_String *value; cdef struct TraderId_t: - String *value; + Rc_String *value; cdef struct VenueOrderId_t: - String *value; + Rc_String *value; cdef struct Ladder: OrderSide side; @@ -174,10 +174,10 @@ cdef extern from "../includes/model.h": uint64_t ts_last; cdef struct Currency_t: - String *code; + Rc_String *code; uint8_t precision; uint16_t iso4217; - String *name; + Rc_String *name; CurrencyType currency_type; cdef struct Money_t: @@ -215,6 +215,8 @@ cdef extern from "../includes/model.h": BarSpecification_t spec, uint8_t aggregation_source); + BarType_t bar_type_copy(const BarType_t *bar_type); + uint8_t bar_type_eq(const BarType_t *lhs, const BarType_t *rhs); uint8_t bar_type_lt(const BarType_t *lhs, const BarType_t *rhs); @@ -267,6 +269,8 @@ cdef extern from "../includes/model.h": # - Assumes you are immediately returning this pointer to Python. PyObject *bar_to_pystr(const Bar_t *bar); + Bar_t bar_copy(const Bar_t *bar); + void bar_free(Bar_t bar); uint8_t bar_eq(const Bar_t *lhs, const Bar_t *rhs); @@ -329,6 +333,8 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is borrowed from a valid Python UTF-8 `str`. AccountId_t account_id_new(PyObject *ptr); + AccountId_t account_id_copy(const AccountId_t *account_id); + # Frees the memory for the given `account_id` by dropping. void account_id_free(AccountId_t account_id); @@ -350,6 +356,8 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is borrowed from a valid Python UTF-8 `str`. ClientId_t client_id_new(PyObject *ptr); + ClientId_t client_id_copy(const ClientId_t *client_id); + # Frees the memory for the given `client_id` by dropping. void client_id_free(ClientId_t client_id); @@ -371,6 +379,8 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is borrowed from a valid Python UTF-8 `str`. ClientOrderId_t client_order_id_new(PyObject *ptr); + ClientOrderId_t client_order_id_copy(const ClientOrderId_t *client_order_id); + # Frees the memory for the given `client_order_id` by dropping. void client_order_id_free(ClientOrderId_t client_order_id); @@ -392,6 +402,8 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is borrowed from a valid Python UTF-8 `str`. ComponentId_t component_id_new(PyObject *ptr); + ComponentId_t component_id_copy(const ComponentId_t *component_id); + # Frees the memory for the given `component_id` by dropping. void component_id_free(ComponentId_t component_id); @@ -422,6 +434,8 @@ cdef extern from "../includes/model.h": # - Assumes `venue_ptr` is borrowed from a valid Python UTF-8 `str`. InstrumentId_t instrument_id_new(PyObject *symbol_ptr, PyObject *venue_ptr); + InstrumentId_t instrument_id_copy(const InstrumentId_t *instrument_id); + # Frees the memory for the given `instrument_id` by dropping. void instrument_id_free(InstrumentId_t instrument_id); @@ -443,6 +457,8 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is borrowed from a valid Python UTF-8 `str`. OrderListId_t order_list_id_new(PyObject *ptr); + OrderListId_t order_list_id_copy(const OrderListId_t *order_list_id); + # Frees the memory for the given `order_list_id` by dropping. void order_list_id_free(OrderListId_t order_list_id); @@ -464,6 +480,8 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is borrowed from a valid Python UTF-8 `str`. PositionId_t position_id_new(PyObject *ptr); + PositionId_t position_id_copy(const PositionId_t *position_id); + # Frees the memory for the given `position_id` by dropping. void position_id_free(PositionId_t position_id); @@ -485,6 +503,8 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is borrowed from a valid Python UTF-8 `str`. StrategyId_t strategy_id_new(PyObject *ptr); + StrategyId_t strategy_id_copy(const StrategyId_t *strategy_id); + # Frees the memory for the given `strategy_id` by dropping. void strategy_id_free(StrategyId_t strategy_id); @@ -494,6 +514,8 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is borrowed from a valid Python UTF-8 `str`. Symbol_t symbol_new(PyObject *ptr); + Symbol_t symbol_copy(const Symbol_t *symbol); + # Frees the memory for the given `symbol` by dropping. void symbol_free(Symbol_t symbol); @@ -515,6 +537,8 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is borrowed from a valid Python UTF-8 `str`. TradeId_t trade_id_new(PyObject *ptr); + TradeId_t trade_id_copy(const TradeId_t *trade_id); + # Frees the memory for the given `trade_id` by dropping. void trade_id_free(TradeId_t trade_id); @@ -536,6 +560,8 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is borrowed from a valid Python UTF-8 `str`. TraderId_t trader_id_new(PyObject *ptr); + TraderId_t trader_id_copy(const TraderId_t *trader_id); + # Frees the memory for the given `trader_id` by dropping. void trader_id_free(TraderId_t trader_id); @@ -545,6 +571,8 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is borrowed from a valid Python UTF-8 `str`. Venue_t venue_new(PyObject *ptr); + Venue_t venue_copy(const Venue_t *venue); + # Frees the memory for the given `venue` by dropping. void venue_free(Venue_t venue); @@ -566,6 +594,8 @@ cdef extern from "../includes/model.h": # - Assumes `ptr` is borrowed from a valid Python UTF-8 `str`. VenueOrderId_t venue_order_id_new(PyObject *ptr); + VenueOrderId_t venue_order_id_copy(const VenueOrderId_t *venue_order_id); + # Frees the memory for the given `venue_order_id` by dropping. void venue_order_id_free(VenueOrderId_t venue_order_id); @@ -594,6 +624,8 @@ cdef extern from "../includes/model.h": PyObject *name_ptr, CurrencyType currency_type); + Currency_t currency_copy(const Currency_t *currency); + void currency_free(Currency_t currency); # Returns a pointer to a valid Python UTF-8 string. diff --git a/nautilus_trader/core/uuid.pyx b/nautilus_trader/core/uuid.pyx index 33116c65794c..40a4fab6f666 100644 --- a/nautilus_trader/core/uuid.pyx +++ b/nautilus_trader/core/uuid.pyx @@ -16,6 +16,7 @@ from cpython.object cimport PyObject from nautilus_trader.core.rust.core cimport UUID4_t +from nautilus_trader.core.rust.core cimport uuid4_copy from nautilus_trader.core.rust.core cimport uuid4_eq from nautilus_trader.core.rust.core cimport uuid4_free from nautilus_trader.core.rust.core cimport uuid4_from_pystr @@ -84,5 +85,5 @@ cdef class UUID4: @staticmethod cdef UUID4 from_raw_c(UUID4_t raw): cdef UUID4 uuid4 = UUID4.__new__(UUID4) - uuid4._mem = raw + uuid4._mem = uuid4_copy(&raw) return uuid4 diff --git a/nautilus_trader/model/currency.pxd b/nautilus_trader/model/currency.pxd index 6ead988bab53..d8c42c677fbb 100644 --- a/nautilus_trader/model/currency.pxd +++ b/nautilus_trader/model/currency.pxd @@ -20,6 +20,7 @@ from nautilus_trader.core.rust.model cimport Currency_t cdef class Currency: cdef Currency_t _mem + cdef bint _init cdef uint8_t get_precision(self) diff --git a/nautilus_trader/model/currency.pyx b/nautilus_trader/model/currency.pyx index f7885f628b67..a23cc4e132a1 100644 --- a/nautilus_trader/model/currency.pyx +++ b/nautilus_trader/model/currency.pyx @@ -80,11 +80,11 @@ cdef class Currency: name, currency_type, ) + self._init = True def __del__(self) -> None: - # TODO(cs): Investigate dealloc (not currently being freed) - # currency_free(self._mem) # `self._mem` moved to Rust (then dropped) - pass + if self._init: + currency_free(self._mem) # `self._mem` moved to Rust (then dropped) def __getstate__(self): return ( diff --git a/nautilus_trader/model/data/bar.pyx b/nautilus_trader/model/data/bar.pyx index c1a95886f608..cd72caca6186 100644 --- a/nautilus_trader/model/data/bar.pyx +++ b/nautilus_trader/model/data/bar.pyx @@ -38,6 +38,7 @@ from nautilus_trader.core.rust.model cimport bar_specification_lt from nautilus_trader.core.rust.model cimport bar_specification_new from nautilus_trader.core.rust.model cimport bar_specification_to_pystr from nautilus_trader.core.rust.model cimport bar_to_pystr +from nautilus_trader.core.rust.model cimport bar_type_copy from nautilus_trader.core.rust.model cimport bar_type_eq from nautilus_trader.core.rust.model cimport bar_type_free from nautilus_trader.core.rust.model cimport bar_type_ge @@ -47,6 +48,7 @@ from nautilus_trader.core.rust.model cimport bar_type_le from nautilus_trader.core.rust.model cimport bar_type_lt from nautilus_trader.core.rust.model cimport bar_type_new from nautilus_trader.core.rust.model cimport bar_type_to_pystr +from nautilus_trader.core.rust.model cimport instrument_id_copy from nautilus_trader.core.rust.model cimport instrument_id_new from nautilus_trader.model.c_enums.aggregation_source cimport AggregationSource from nautilus_trader.model.c_enums.aggregation_source cimport AggregationSourceParser @@ -403,7 +405,7 @@ cdef class BarType: AggregationSource aggregation_source=AggregationSource.EXTERNAL, ): self._mem = bar_type_new( - instrument_id._mem, + instrument_id_copy(&instrument_id._mem), bar_spec._mem, aggregation_source ) @@ -433,9 +435,7 @@ cdef class BarType: ) def __del__(self) -> None: - # TODO(cs): Investigate dealloc (not currently being freed) - # bar_type_free(self._mem) # `self._mem` moved to Rust (then dropped) - pass + bar_type_free(self._mem) # `self._mem` moved to Rust (then dropped) cdef str to_str(self): return bar_type_to_pystr(&self._mem) @@ -467,7 +467,7 @@ cdef class BarType: @staticmethod cdef BarType from_raw_c(BarType_t raw): cdef BarType bar_type = BarType.__new__(BarType) - bar_type._mem = raw + bar_type._mem = bar_type_copy(&raw) return bar_type @staticmethod @@ -618,15 +618,10 @@ cdef class Bar(Data): uint64_t ts_event, uint64_t ts_init, ): - Condition.true(high._mem.raw >= open._mem.raw, "high was < open") - Condition.true(high._mem.raw >= low._mem.raw, "high was < low") - Condition.true(high._mem.raw >= close._mem.raw, "high was < close") - Condition.true(low._mem.raw <= close._mem.raw, "low was > close") - Condition.true(low._mem.raw <= open._mem.raw, "low was > open") super().__init__(ts_event, ts_init) self._mem = bar_new( - bar_type._mem, + bar_type_copy(&bar_type._mem), open._mem, high._mem, low._mem, @@ -635,6 +630,11 @@ cdef class Bar(Data): ts_event, ts_init, ) + Condition.true(high._mem.raw >= open._mem.raw, "high was < open") + Condition.true(high._mem.raw >= low._mem.raw, "high was < low") + Condition.true(high._mem.raw >= close._mem.raw, "high was < close") + Condition.true(low._mem.raw <= close._mem.raw, "low was > close") + Condition.true(low._mem.raw <= open._mem.raw, "low was > open") def __getstate__(self): return ( @@ -683,9 +683,7 @@ cdef class Bar(Data): self.ts_init = state[14] def __del__(self) -> None: - # TODO(cs): Investigate dealloc (not currently being freed) - # bar_free(self._mem) # `self._mem` moved to Rust (then dropped) - pass + bar_free(self._mem) # `self._mem` moved to Rust (then dropped) def __eq__(self, Bar other) -> bool: return bar_eq(&self._mem, &other._mem) diff --git a/nautilus_trader/model/data/tick.pyx b/nautilus_trader/model/data/tick.pyx index 4b96187b78df..9dd1a3de5e25 100644 --- a/nautilus_trader/model/data/tick.pyx +++ b/nautilus_trader/model/data/tick.pyx @@ -20,10 +20,12 @@ 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.model cimport instrument_id_copy from nautilus_trader.core.rust.model cimport instrument_id_new from nautilus_trader.core.rust.model cimport quote_tick_free from nautilus_trader.core.rust.model cimport quote_tick_from_raw from nautilus_trader.core.rust.model cimport quote_tick_to_pystr +from nautilus_trader.core.rust.model cimport trade_id_copy from nautilus_trader.core.rust.model cimport trade_id_new from nautilus_trader.core.rust.model cimport trade_tick_free from nautilus_trader.core.rust.model cimport trade_tick_from_raw @@ -84,7 +86,7 @@ cdef class QuoteTick(Data): super().__init__(ts_event, ts_init) self._mem = quote_tick_from_raw( - instrument_id._mem, + instrument_id_copy(&instrument_id._mem), bid._mem.raw, ask._mem.raw, bid._mem.precision, @@ -98,9 +100,7 @@ cdef class QuoteTick(Data): ) def __del__(self) -> None: - # TODO(cs): Investigate dealloc (not currently being freed) - # quote_tick_free(self._mem) # `self._mem` moved to Rust (then dropped) - pass + quote_tick_free(self._mem) # `self._mem` moved to Rust (then dropped) def __getstate__(self): return ( @@ -171,7 +171,7 @@ cdef class QuoteTick(Data): tick.ts_event = ts_event tick.ts_init = ts_init tick._mem = quote_tick_from_raw( - instrument_id._mem, + instrument_id_copy(&instrument_id._mem), raw_bid, raw_ask, bid_price_prec, @@ -463,21 +463,19 @@ cdef class TradeTick(Data): super().__init__(ts_event, ts_init) self._mem = trade_tick_from_raw( - instrument_id._mem, + instrument_id_copy(&instrument_id._mem), price._mem.raw, price._mem.precision, size._mem.raw, size._mem.precision, aggressor_side, - trade_id._mem, + trade_id_copy(&trade_id._mem), ts_event, ts_init, ) def __del__(self) -> None: - # TODO(cs): Investigate dealloc (not currently being freed) - # trade_tick_free(self._mem) # `self._mem` moved to Rust (then dropped) - pass + trade_tick_free(self._mem) # `self._mem` moved to Rust (then dropped) def __getstate__(self): return ( @@ -602,13 +600,13 @@ cdef class TradeTick(Data): tick.ts_event = ts_event tick.ts_init = ts_init tick._mem = trade_tick_from_raw( - instrument_id._mem, + instrument_id_copy(&instrument_id._mem), raw_price, price_prec, raw_size, size_prec, aggressor_side, - trade_id._mem, + trade_id_copy(&trade_id._mem), ts_event, ts_init, ) diff --git a/nautilus_trader/model/identifiers.pyx b/nautilus_trader/model/identifiers.pyx index f86b96b7a0e0..76549182f14a 100644 --- a/nautilus_trader/model/identifiers.pyx +++ b/nautilus_trader/model/identifiers.pyx @@ -15,7 +15,6 @@ from cpython.object cimport PyObject -from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.rust.model cimport account_id_eq from nautilus_trader.core.rust.model cimport account_id_free from nautilus_trader.core.rust.model cimport account_id_hash @@ -31,6 +30,7 @@ from nautilus_trader.core.rust.model cimport component_id_free from nautilus_trader.core.rust.model cimport component_id_hash from nautilus_trader.core.rust.model cimport component_id_new from nautilus_trader.core.rust.model cimport component_id_to_pystr +from nautilus_trader.core.rust.model cimport instrument_id_copy from nautilus_trader.core.rust.model cimport instrument_id_eq from nautilus_trader.core.rust.model cimport instrument_id_free from nautilus_trader.core.rust.model cimport instrument_id_hash @@ -46,16 +46,19 @@ from nautilus_trader.core.rust.model cimport position_id_free from nautilus_trader.core.rust.model cimport position_id_hash from nautilus_trader.core.rust.model cimport position_id_new from nautilus_trader.core.rust.model cimport position_id_to_pystr +from nautilus_trader.core.rust.model cimport symbol_copy from nautilus_trader.core.rust.model cimport symbol_eq from nautilus_trader.core.rust.model cimport symbol_free from nautilus_trader.core.rust.model cimport symbol_hash from nautilus_trader.core.rust.model cimport symbol_new from nautilus_trader.core.rust.model cimport symbol_to_pystr +from nautilus_trader.core.rust.model cimport trade_id_copy from nautilus_trader.core.rust.model cimport trade_id_eq from nautilus_trader.core.rust.model cimport trade_id_free from nautilus_trader.core.rust.model cimport trade_id_hash from nautilus_trader.core.rust.model cimport trade_id_new from nautilus_trader.core.rust.model cimport trade_id_to_pystr +from nautilus_trader.core.rust.model cimport venue_copy from nautilus_trader.core.rust.model cimport venue_eq from nautilus_trader.core.rust.model cimport venue_free from nautilus_trader.core.rust.model cimport venue_hash @@ -137,9 +140,7 @@ cdef class Symbol(Identifier): self._mem = symbol_new(value) def __del__(self) -> None: - # TODO(cs): Investigate dealloc (not currently being freed) - # symbol_free(self._mem) # `self._mem` moved to Rust (then dropped) - pass + symbol_free(self._mem) # `self._mem` moved to Rust (then dropped) def __getstate__(self): return self.to_str() @@ -175,9 +176,7 @@ cdef class Venue(Identifier): self._mem = venue_new(name) def __del__(self) -> None: - # TODO(cs): Investigate dealloc (not currently being freed) - # venue_free(self._mem) # `self._mem` moved to Rust (then dropped) - pass + venue_free(self._mem) # `self._mem` moved to Rust (then dropped) def __getstate__(self): return self.to_str() @@ -218,9 +217,7 @@ cdef class InstrumentId(Identifier): self.venue = venue def __del__(self) -> None: - # TODO(cs): Investigate dealloc (not currently being freed) - # instrument_id_free(self._mem) # `self._mem` moved to Rust (then dropped) - pass + instrument_id_free(self._mem) # `self._mem` moved to Rust (then dropped) def __getstate__(self): return ( @@ -248,13 +245,13 @@ cdef class InstrumentId(Identifier): @staticmethod cdef InstrumentId from_raw_c(InstrumentId_t raw): cdef Symbol symbol = Symbol.__new__(Symbol) - symbol._mem = raw.symbol + symbol._mem = symbol_copy(&raw.symbol) cdef Venue venue = Venue.__new__(Venue) - venue._mem = raw.venue + venue._mem = venue_copy(&raw.venue) cdef InstrumentId instrument_id = InstrumentId.__new__(InstrumentId) - instrument_id._mem = raw + instrument_id._mem = instrument_id_copy(&raw) instrument_id.symbol = symbol instrument_id.venue = venue @@ -323,9 +320,7 @@ cdef class ComponentId(Identifier): self._mem = component_id_new(value) def __del__(self) -> None: - # TODO(cs): Investigate dealloc (not currently being freed) - # component_id_free(self._mem) # `self._mem` moved to Rust (then dropped) - pass + component_id_free(self._mem) # `self._mem` moved to Rust (then dropped) def __getstate__(self): return self.to_str() @@ -695,9 +690,7 @@ cdef class TradeId(Identifier): self._mem = trade_id_new(value) def __del__(self) -> None: - # TODO(cs): Investigate dealloc (not currently being freed) - # trade_id_free(self._mem) # `self._mem` moved to Rust (then dropped) - pass + trade_id_free(self._mem) # `self._mem` moved to Rust (then dropped) def __getstate__(self): return self.to_str() @@ -717,5 +710,5 @@ cdef class TradeId(Identifier): @staticmethod cdef TradeId from_raw_c(TradeId_t raw): cdef TradeId trade_id = TradeId.__new__(TradeId) - trade_id._mem = raw + trade_id._mem = trade_id_copy(&raw) return trade_id diff --git a/nautilus_trader/model/objects.pxd b/nautilus_trader/model/objects.pxd index c246a0780763..14106674b287 100644 --- a/nautilus_trader/model/objects.pxd +++ b/nautilus_trader/model/objects.pxd @@ -114,6 +114,7 @@ cdef class Price: cdef class Money: cdef Money_t _mem + cdef bint _init cdef str currency_code_c(self) cdef bint is_zero(self) except * diff --git a/nautilus_trader/model/objects.pyx b/nautilus_trader/model/objects.pyx index e8f52ad27bdc..9affa8cd4882 100644 --- a/nautilus_trader/model/objects.pyx +++ b/nautilus_trader/model/objects.pyx @@ -40,6 +40,7 @@ from nautilus_trader.core.rust.model cimport QUANTITY_MAX as RUST_QUANTITY_MAX from nautilus_trader.core.rust.model cimport QUANTITY_MIN as RUST_QUANTITY_MIN from nautilus_trader.core.rust.model cimport Currency_t from nautilus_trader.core.rust.model cimport currency_code_to_pystr +from nautilus_trader.core.rust.model cimport currency_copy from nautilus_trader.core.rust.model cimport currency_eq from nautilus_trader.core.rust.model cimport money_free from nautilus_trader.core.rust.model cimport money_from_raw @@ -836,19 +837,21 @@ cdef class Money: f"invalid `value` less than `MONEY_MIN` {MONEY_MIN:_}, was {value:_}", ) - self._mem = money_new(value_f64, currency._mem) # borrows wrapped `currency` + cdef Currency_t currency_t = currency._mem + self._mem = money_new(value_f64, currency_copy(¤cy_t)) + self._init = True def __del__(self) -> None: - # TODO(cs): Investigate dealloc (not currently being freed) - # money_free(self._mem) # `self._mem` moved to Rust (then dropped) - pass + if self._init: + money_free(self._mem) # `self._mem` moved to Rust (then dropped) def __getstate__(self): return self._mem.raw, self.currency_code_c() def __setstate__(self, state): cdef Currency currency = Currency.from_str_c(state[1]) - self._mem = money_from_raw(state[0], currency._mem) + cdef Currency_t currency_t = currency._mem + self._mem = money_from_raw(state[0], currency_copy(¤cy_t)) def __eq__(self, Money other) -> bool: Condition.true(currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency") @@ -1008,7 +1011,8 @@ cdef class Money: @staticmethod cdef Money from_raw_c(uint64_t raw, Currency currency): cdef Money money = Money.__new__(Money) - money._mem = money_from_raw(raw, currency._mem) + cdef Currency_t currency_t = currency._mem + money._mem = money_from_raw(raw, currency_copy(¤cy_t)) return money @staticmethod diff --git a/tests/acceptance_tests/test_backtest_acceptance.py b/tests/acceptance_tests/test_backtest_acceptance.py index adfdd9adc298..f61dc6e84aa1 100644 --- a/tests/acceptance_tests/test_backtest_acceptance.py +++ b/tests/acceptance_tests/test_backtest_acceptance.py @@ -372,6 +372,7 @@ def setup(self): config = BacktestEngineConfig( bypass_logging=False, run_analysis=False, + log_level="DEBUG", exec_engine={"allow_cash_positions": False}, # <-- Normally True risk_engine={"bypass": True}, ) diff --git a/tests/unit_tests/common/test_common_clock.py b/tests/unit_tests/common/test_common_clock.py index 60746a848164..2e11d83cfe82 100644 --- a/tests/unit_tests/common/test_common_clock.py +++ b/tests/unit_tests/common/test_common_clock.py @@ -557,6 +557,7 @@ def test_advance_time_with_multiple_set_timers_triggers_events(self): assert clock.timer_count == 2 +@pytest.mark.skip(reason="Investigate threaded memory safety") @pytest.mark.skipif(sys.platform == "win32", reason="Randomly failing on Windows in CI") class TestLiveClockWithThreadTimer: def setup(self): From 6f68592923df41b8aea0cf7361b3a402aa0f25b4 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Tue, 1 Nov 2022 22:05:53 +1100 Subject: [PATCH 30/38] Refine matching engine trade ID generation --- nautilus_trader/backtest/matching_engine.pxd | 1 + nautilus_trader/backtest/matching_engine.pyx | 25 +++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/nautilus_trader/backtest/matching_engine.pxd b/nautilus_trader/backtest/matching_engine.pxd index 757d222a984c..26097ddd947e 100644 --- a/nautilus_trader/backtest/matching_engine.pxd +++ b/nautilus_trader/backtest/matching_engine.pxd @@ -161,6 +161,7 @@ cdef class OrderMatchingEngine: cdef PositionId _generate_venue_position_id(self) cdef VenueOrderId _generate_venue_order_id(self) cdef TradeId _generate_trade_id(self) + cdef str _generate_trade_id_str(self) # -- EVENT HANDLING ------------------------------------------------------------------------------- diff --git a/nautilus_trader/backtest/matching_engine.pyx b/nautilus_trader/backtest/matching_engine.pyx index b8bdec0ec523..e342621cce6c 100644 --- a/nautilus_trader/backtest/matching_engine.pyx +++ b/nautilus_trader/backtest/matching_engine.pyx @@ -15,6 +15,7 @@ from typing import Optional +from cpython.object cimport PyObject from libc.limits cimport INT_MAX from libc.limits cimport INT_MIN from libc.stdint cimport uint64_t @@ -25,7 +26,7 @@ from nautilus_trader.common.clock cimport TestClock from nautilus_trader.common.logging cimport Logger from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.rust.model cimport price_new -from nautilus_trader.core.rust.model cimport trade_id_copy +from nautilus_trader.core.rust.model cimport trade_id_new from nautilus_trader.core.uuid cimport UUID4 from nautilus_trader.execution.matching_core cimport MatchingCore from nautilus_trader.execution.trailing_calculator cimport TrailingStopCalculator @@ -398,11 +399,14 @@ cdef class OrderMatchingEngine: self.iterate(tick.ts_init) self._core.set_last(bar._mem.open) + cdef str trade_id_str # Assigned below + # High if bar._mem.high.raw > self._core.last_raw: # Direct memory comparison tick._mem.price = bar._mem.high # Direct memory assignment tick._mem.aggressor_side = AggressorSide.BUY # Direct memory assignment - tick._mem.trade_id = trade_id_copy(&self._generate_trade_id()._mem) + trade_id_str = self._generate_trade_id_str() + tick._mem.trade_id = trade_id_new(trade_id_str) self._book.update_trade_tick(tick) self.iterate(tick.ts_init) self._core.set_last(bar._mem.high) @@ -411,7 +415,8 @@ cdef class OrderMatchingEngine: if bar._mem.low.raw < self._core.last_raw: # Direct memory comparison tick._mem.price = bar._mem.low # Direct memory assignment tick._mem.aggressor_side = AggressorSide.SELL - tick._mem.trade_id = trade_id_copy(&self._generate_trade_id()._mem) + trade_id_str = self._generate_trade_id_str() + tick._mem.trade_id = trade_id_new(trade_id_str) self._book.update_trade_tick(tick) self.iterate(tick.ts_init) self._core.set_last(bar._mem.low) @@ -420,7 +425,8 @@ cdef class OrderMatchingEngine: if bar._mem.close.raw != self._core.last_raw: # Direct memory comparison tick._mem.price = bar._mem.close # Direct memory assignment tick._mem.aggressor_side = AggressorSide.BUY if bar._mem.close.raw > self._core.last_raw else AggressorSide.SELL - tick._mem.trade_id = trade_id_copy(&self._generate_trade_id()._mem) + trade_id_str = self._generate_trade_id_str() + tick._mem.trade_id = trade_id_new(trade_id_str) self._book.update_trade_tick(tick) self.iterate(tick.ts_init) self._core.set_last(bar._mem.close) @@ -1209,18 +1215,19 @@ cdef class OrderMatchingEngine: cdef PositionId _generate_venue_position_id(self): self._position_count += 1 return PositionId( - f"{self.venue.value}-{self.product_id}-{self._position_count:03d}") + f"{self.venue.to_str()}-{self.product_id}-{self._position_count:03d}") cdef VenueOrderId _generate_venue_order_id(self): self._order_count += 1 return VenueOrderId( - f"{self.venue.value}-{self.product_id}-{self._order_count:03d}") + f"{self.venue.to_str()}-{self.product_id}-{self._order_count:03d}") cdef TradeId _generate_trade_id(self): self._execution_count += 1 - return TradeId( - f"{self.venue.value}-{self.product_id}-{self._execution_count:03d}", - ) + return TradeId(self._generate_trade_id_str()) + + cdef str _generate_trade_id_str(self): + return f"{self.venue.to_str()}-{self.product_id}-{self._execution_count:03d}" # -- EVENT HANDLING ------------------------------------------------------------------------------- From 44e8cf60e076b5fe8e2577c00842132f9aeeb49b Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 2 Nov 2022 17:06:52 +1100 Subject: [PATCH 31/38] Add docker extra --- Makefile | 2 +- nautilus_core/Cargo.lock | 32 +++++++------- noxfile.py | 2 +- poetry.lock | 90 ++++++++++++++++++++++++++++++++++------ pyproject.toml | 10 ++--- 5 files changed, 100 insertions(+), 36 deletions(-) diff --git a/Makefile b/Makefile index 70634177faae..4d6564d6a9f2 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ REGISTRY?=ghcr.io/ IMAGE?=${REGISTRY}${PROJECT} GIT_TAG:=$(shell git rev-parse --abbrev-ref HEAD) IMAGE_FULL?=${IMAGE}:${GIT_TAG} -EXTRAS?="ib redis" +EXTRAS?="docker ib redis" .PHONY: install build clean docs format pre-commit .PHONY: cargo-update cargo-test cargo-test-arm64 .PHONY: update docker-build docker-build-force docker-push diff --git a/nautilus_core/Cargo.lock b/nautilus_core/Cargo.lock index d36d8d758658..ccb9fcd42633 100644 --- a/nautilus_core/Cargo.lock +++ b/nautilus_core/Cargo.lock @@ -159,9 +159,9 @@ checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "bytemuck" -version = "1.12.1" +version = "1.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" +checksum = "5aec14f5d4e6e3f927cd0c81f72e5710d95ee9019fbeb4b3021193867491bfd8" dependencies = [ "bytemuck_derive", ] @@ -204,9 +204,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" [[package]] name = "cfg-if" @@ -914,9 +914,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "oorandom" @@ -1043,9 +1043,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.17.2" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201b6887e5576bf2f945fe65172c1fcbf3fcf285b23e4d71eb171d9736e38d32" +checksum = "268be0c73583c183f2b14052337465768c07726936a260f480f0857cb95ba543" dependencies = [ "cfg-if", "indoc", @@ -1060,9 +1060,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.17.2" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0708c9ed01692635cbf056e286008e5a2927ab1a5e48cdd3aeb1ba5a6fef47" +checksum = "28fcd1e73f06ec85bf3280c48c67e731d8290ad3d730f8be9dc07946923005c8" dependencies = [ "once_cell", "target-lexicon", @@ -1070,9 +1070,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.17.2" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90352dea4f486932b72ddf776264d293f85b79a1d214de1d023927b41461132d" +checksum = "0f6cb136e222e49115b3c51c32792886defbfb0adead26a688142b346a0b9ffc" dependencies = [ "libc", "pyo3-build-config", @@ -1080,9 +1080,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.17.2" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eb24b804a2d9e88bfcc480a5a6dd76f006c1e3edaf064e8250423336e2cd79d" +checksum = "94144a1266e236b1c932682136dc35a9dee8d3589728f68130c7c3861ef96b28" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -1092,9 +1092,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.17.2" +version = "0.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f22bb49f6a7348c253d7ac67a6875f2dc65f36c2ae64a82c381d528972bea6d6" +checksum = "c8df9be978a2d2f0cdebabb03206ed73b11314701a5bfe71b0d753b81997777f" dependencies = [ "proc-macro2", "quote", diff --git a/noxfile.py b/noxfile.py index 6d6afe9e855c..55a8f1df0617 100644 --- a/noxfile.py +++ b/noxfile.py @@ -4,7 +4,7 @@ from nox.sessions import Session -ALL_EXTRAS = "ib redis" +ALL_EXTRAS = "docker ib redis" # Ensure everything runs within Poetry venvs diff --git a/poetry.lock b/poetry.lock index f95fe59804ba..ef2070a9bfd5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -83,7 +83,7 @@ tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy [[package]] name = "babel" -version = "2.10.3" +version = "2.11.0" description = "Internationalization utilities" category = "dev" optional = false @@ -133,7 +133,7 @@ uvloop = ["uvloop (>=0.15.2)"] name = "certifi" version = "2022.9.24" description = "Python package for providing Mozilla's CA Bundle." -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -260,6 +260,24 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "docker" +version = "6.0.0" +description = "A Python library for the Docker Engine API." +category = "main" +optional = true +python-versions = ">=3.7" + +[package.dependencies] +packaging = ">=14.0" +pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} +requests = ">=2.26.0" +urllib3 = ">=1.26.0" +websocket-client = ">=0.32.0" + +[package.extras] +ssh = ["paramiko (>=2.4.3)"] + [[package]] name = "docutils" version = "0.17.1" @@ -969,12 +987,20 @@ unidecode = ["Unidecode (>=1.1.1)"] [[package]] name = "pytz" -version = "2022.5" +version = "2022.6" description = "World timezone definitions, modern and historical" category = "main" optional = false python-versions = "*" +[[package]] +name = "pywin32" +version = "304" +description = "Python for Window Extensions" +category = "main" +optional = true +python-versions = "*" + [[package]] name = "pyyaml" version = "6.0" @@ -1004,7 +1030,7 @@ ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)" name = "requests" version = "2.28.1" description = "Python HTTP for Humans." -category = "dev" +category = "main" optional = false python-versions = ">=3.7, <4" @@ -1296,7 +1322,7 @@ telegram = ["requests"] [[package]] name = "types-pytz" -version = "2022.5.0.0" +version = "2022.6.0.1" description = "Typing stubs for pytz" category = "dev" optional = false @@ -1368,7 +1394,7 @@ python-versions = ">=3.5" name = "urllib3" version = "1.26.12" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" +category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" @@ -1407,6 +1433,19 @@ platformdirs = ">=2.4,<3" docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] +[[package]] +name = "websocket-client" +version = "1.4.1" +description = "WebSocket client for Python with low level API options" +category = "main" +optional = true +python-versions = ">=3.7" + +[package.extras] +docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"] +optional = ["python-socks", "wsaccel"] +test = ["websockets"] + [[package]] name = "wheel" version = "0.37.1" @@ -1451,13 +1490,14 @@ docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [extras] +docker = ["docker"] ib = ["ib_insync"] redis = ["hiredis", "redis"] [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "072b0582f3f749e15c82f6486b8a1d93d27ff5801962650b6489b9af52c5c292" +content-hash = "748fd442238ed6c7df31b2d8348ddd6439e501a110f199ea99e86b4ff5680731" [metadata.files] aiodns = [ @@ -1574,8 +1614,8 @@ attrs = [ {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] babel = [ - {file = "Babel-2.10.3-py3-none-any.whl", hash = "sha256:ff56f4892c1c4bf0d814575ea23471c230d544203c7748e8c68f0089478d48eb"}, - {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, + {file = "Babel-2.11.0-py3-none-any.whl", hash = "sha256:1ad3eca1c885218f6dce2ab67291178944f810a10a9b5f3cb8382a5a232b64fe"}, + {file = "Babel-2.11.0.tar.gz", hash = "sha256:5ef4b3226b0180dedded4229651c8b0e1a3a6a2837d45a073272f313e4cf97f6"}, ] beautifulsoup4 = [ {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, @@ -1802,6 +1842,10 @@ distlib = [ {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] +docker = [ + {file = "docker-6.0.0-py3-none-any.whl", hash = "sha256:6e06ee8eca46cd88733df09b6b80c24a1a556bc5cb1e1ae54b2c239886d245cf"}, + {file = "docker-6.0.0.tar.gz", hash = "sha256:19e330470af40167d293b0352578c1fa22d74b34d3edf5d4ff90ebc203bbb2f1"}, +] docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, @@ -2545,8 +2589,24 @@ python-slugify = [ {file = "python_slugify-6.1.2-py2.py3-none-any.whl", hash = "sha256:7b2c274c308b62f4269a9ba701aa69a797e9bca41aeee5b3a9e79e36b6656927"}, ] pytz = [ - {file = "pytz-2022.5-py2.py3-none-any.whl", hash = "sha256:335ab46900b1465e714b4fda4963d87363264eb662aab5e65da039c25f1f5b22"}, - {file = "pytz-2022.5.tar.gz", hash = "sha256:c4d88f472f54d615e9cd582a5004d1e5f624854a6a27a6211591c251f22a6914"}, + {file = "pytz-2022.6-py2.py3-none-any.whl", hash = "sha256:222439474e9c98fced559f1709d89e6c9cbf8d79c794ff3eb9f8800064291427"}, + {file = "pytz-2022.6.tar.gz", hash = "sha256:e89512406b793ca39f5971bc999cc538ce125c0e51c27941bef4568b460095e2"}, +] +pywin32 = [ + {file = "pywin32-304-cp310-cp310-win32.whl", hash = "sha256:3c7bacf5e24298c86314f03fa20e16558a4e4138fc34615d7de4070c23e65af3"}, + {file = "pywin32-304-cp310-cp310-win_amd64.whl", hash = "sha256:4f32145913a2447736dad62495199a8e280a77a0ca662daa2332acf849f0be48"}, + {file = "pywin32-304-cp310-cp310-win_arm64.whl", hash = "sha256:d3ee45adff48e0551d1aa60d2ec066fec006083b791f5c3527c40cd8aefac71f"}, + {file = "pywin32-304-cp311-cp311-win32.whl", hash = "sha256:30c53d6ce44c12a316a06c153ea74152d3b1342610f1b99d40ba2795e5af0269"}, + {file = "pywin32-304-cp311-cp311-win_amd64.whl", hash = "sha256:7ffa0c0fa4ae4077e8b8aa73800540ef8c24530057768c3ac57c609f99a14fd4"}, + {file = "pywin32-304-cp311-cp311-win_arm64.whl", hash = "sha256:cbbe34dad39bdbaa2889a424d28752f1b4971939b14b1bb48cbf0182a3bcfc43"}, + {file = "pywin32-304-cp36-cp36m-win32.whl", hash = "sha256:be253e7b14bc601718f014d2832e4c18a5b023cbe72db826da63df76b77507a1"}, + {file = "pywin32-304-cp36-cp36m-win_amd64.whl", hash = "sha256:de9827c23321dcf43d2f288f09f3b6d772fee11e809015bdae9e69fe13213988"}, + {file = "pywin32-304-cp37-cp37m-win32.whl", hash = "sha256:f64c0377cf01b61bd5e76c25e1480ca8ab3b73f0c4add50538d332afdf8f69c5"}, + {file = "pywin32-304-cp37-cp37m-win_amd64.whl", hash = "sha256:bb2ea2aa81e96eee6a6b79d87e1d1648d3f8b87f9a64499e0b92b30d141e76df"}, + {file = "pywin32-304-cp38-cp38-win32.whl", hash = "sha256:94037b5259701988954931333aafd39cf897e990852115656b014ce72e052e96"}, + {file = "pywin32-304-cp38-cp38-win_amd64.whl", hash = "sha256:ead865a2e179b30fb717831f73cf4373401fc62fbc3455a0889a7ddac848f83e"}, + {file = "pywin32-304-cp39-cp39-win32.whl", hash = "sha256:25746d841201fd9f96b648a248f731c1dec851c9a08b8e33da8b56148e4c65cc"}, + {file = "pywin32-304-cp39-cp39-win_amd64.whl", hash = "sha256:d24a3382f013b21aa24a5cfbfad5a2cd9926610c0affde3e8ab5b3d7dbcf4ac9"}, ] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, @@ -2683,8 +2743,8 @@ tqdm = [ {file = "tqdm-4.64.1.tar.gz", hash = "sha256:5f4f682a004951c1b450bc753c710e9280c5746ce6ffedee253ddbcbf54cf1e4"}, ] types-pytz = [ - {file = "types-pytz-2022.5.0.0.tar.gz", hash = "sha256:0c163b15d3e598e6cc7074a99ca9ec72b25dc1b446acc133b827667af0b7b09a"}, - {file = "types_pytz-2022.5.0.0-py3-none-any.whl", hash = "sha256:a8e1fe6a1b270fbfaf2553b20ad0f1316707cc320e596da903bb17d7373fed2d"}, + {file = "types-pytz-2022.6.0.1.tar.gz", hash = "sha256:d078196374d1277e9f9984d49373ea043cf2c64d5d5c491fbc86c258557bd46f"}, + {file = "types_pytz-2022.6.0.1-py3-none-any.whl", hash = "sha256:bea605ce5d5a5d52a8e1afd7656c9b42476e18a0f888de6be91587355313ddf4"}, ] types-redis = [ {file = "types-redis-4.3.21.3.tar.gz", hash = "sha256:2e1f184056188c8754ded0b5173dc01824d2bfe41975fe318068a68beedfb62c"}, @@ -2754,6 +2814,10 @@ virtualenv = [ {file = "virtualenv-20.16.6-py3-none-any.whl", hash = "sha256:186ca84254abcbde98180fd17092f9628c5fe742273c02724972a1d8a2035108"}, {file = "virtualenv-20.16.6.tar.gz", hash = "sha256:530b850b523c6449406dfba859d6345e48ef19b8439606c5d74d7d3c9e14d76e"}, ] +websocket-client = [ + {file = "websocket-client-1.4.1.tar.gz", hash = "sha256:f9611eb65c8241a67fb373bef040b3cf8ad377a9f6546a12b620b6511e8ea9ef"}, + {file = "websocket_client-1.4.1-py3-none-any.whl", hash = "sha256:398909eb7e261f44b8f4bd474785b6ec5f5b499d4953342fe9755e01ef624090"}, +] wheel = [ {file = "wheel-0.37.1-py2.py3-none-any.whl", hash = "sha256:4bdcd7d840138086126cd09254dc6195fb4fc6f01c050a1d7236f2630db1d22a"}, {file = "wheel-0.37.1.tar.gz", hash = "sha256:e9a504e793efbca1b8e0e9cb979a249cf4a0a7b5b8c9e8b65a5e39d49529c1c4"}, diff --git a/pyproject.toml b/pyproject.toml index 6c368d6846b0..d6800b649f71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ generate-setup-file = false [tool.poetry.dependencies] python = ">=3.9,<3.11" -cython = "3.0.0a11" +cython = "==3.0.0a11" aiodns = "^3.0.0" aiohttp = "^3.8.3" click = "^8.1.3" @@ -59,7 +59,7 @@ pandas = "^1.5.1" psutil = "^5.9.3" pyarrow = "^8.0.0" pydantic = "^1.10.2" -pytz = "^2022.2.5" +pytz = "^2022.2.6" tabulate = "^0.9.0" toml = "^0.10.2" tqdm = "^4.64.1" @@ -67,8 +67,7 @@ uvloop = { version = "^0.17.0", markers = "sys_platform != 'win32'" } hiredis = { version = "^2.0.0", optional = true } ib_insync = { version = "^0.9.70", optional = true } redis = { version = "^4.3.4", optional = true } -# Removed due to 3.10 windows build issue - https://github.com/docker/docker-py/issues/2902 -# docker = {version = "^5.0.3", optional = true } +docker = {version = "^6.0.0", optional = true } [tool.poetry.dev-dependencies] black = "^22.10.0" @@ -91,12 +90,13 @@ sphinx_copybutton = "^0.5.0" sphinx-external-toc = "^0.3.0" sphinx-material = "^0.0.35" sphinx_togglebutton = "^0.3.0" -types-pytz = "^2022.5.0" +types-pytz = "^2022.6.0" types-redis = "^4.3.21" types-requests = "^2.28.11" types-toml = "^0.10.8" [tool.poetry.extras] +docker = ["docker"] ib = ["ib_insync"] redis = ["hiredis", "redis"] From 358e956d143295d4390871ccf631ccee1ef446d7 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 2 Nov 2022 18:18:21 +1100 Subject: [PATCH 32/38] Improve reconciliation config --- nautilus_trader/config/live.py | 9 +++++---- nautilus_trader/live/execution_engine.pyx | 17 +++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/nautilus_trader/config/live.py b/nautilus_trader/config/live.py index 4bbbcf9afec5..aa303aa201e8 100644 --- a/nautilus_trader/config/live.py +++ b/nautilus_trader/config/live.py @@ -77,9 +77,10 @@ class LiveExecEngineConfig(ExecEngineConfig): ---------- reconciliation_auto : bool, default True If reconciliation should automatically generate events to align state. - reconciliation_lookback_mins : PositiveInt, optional - The maximum lookback minutes to reconcile state for. If ``None`` then - will use the maximum lookback available from the venues. + reconciliation_lookback_mins : NonNegativeInt, optional + The maximum lookback minutes to reconcile state for. + If 0 then will not performance any lookback or reconciliation. + If ``None`` then will use the maximum lookback available from the venues. inflight_check_interval_ms : NonNegativeInt, default 5000 The interval (milliseconds) between checking whether in-flight orders have exceeded their time-in-flight threshold. @@ -91,7 +92,7 @@ class LiveExecEngineConfig(ExecEngineConfig): """ reconciliation_auto: bool = True - reconciliation_lookback_mins: Optional[PositiveInt] = None + reconciliation_lookback_mins: Optional[NonNegativeInt] = None inflight_check_interval_ms: NonNegativeInt = 5000 inflight_check_threshold_ms: NonNegativeInt = 1000 qsize: PositiveInt = 10000 diff --git a/nautilus_trader/live/execution_engine.pyx b/nautilus_trader/live/execution_engine.pyx index 2152ab94ee50..7d4e108e1d24 100644 --- a/nautilus_trader/live/execution_engine.pyx +++ b/nautilus_trader/live/execution_engine.pyx @@ -121,10 +121,8 @@ cdef class LiveExecutionEngine(ExecutionEngine): self._queue = Queue(maxsize=config.qsize) # Settings - self.reconciliation_auto = config.reconciliation_auto if config else True - self.reconciliation_lookback_mins = 0 - if config and config.reconciliation_lookback_mins is not None: - self.reconciliation_lookback_mins = config.reconciliation_lookback_mins + self.reconciliation_auto = config.reconciliation_auto + self.reconciliation_lookback_mins = config.reconciliation_lookback_mins self.inflight_check_interval_ms = config.inflight_check_interval_ms self.inflight_check_threshold_ms = config.inflight_check_threshold_ms self._inflight_check_threshold_ns = millis_to_nanos(self.inflight_check_threshold_ms) @@ -363,15 +361,18 @@ cdef class LiveExecutionEngine(ExecutionEngine): """ Condition.positive(timeout_secs, "timeout_secs") + if self.reconciliation_lookback_mins == 0: + self._log.warning("No reconciliation.") + return True + + cdef list results = [] + # Request execution mass status report from clients - reconciliation_lookback_mins = self.reconciliation_lookback_mins if self.reconciliation_lookback_mins > 0 else None mass_status_coros = [ - c.generate_mass_status(reconciliation_lookback_mins) for c in self._clients.values() + c.generate_mass_status(self.reconciliation_lookback_mins) for c in self._clients.values() ] mass_status_all = await asyncio.gather(*mass_status_coros) - cdef list results = [] - # Reconcile each mass status with the execution engine for mass_status in mass_status_all: result = self._reconcile_mass_status(mass_status) From 7e30fbe80c88da7278f36ed21263d83c8b60b741 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 2 Nov 2022 18:40:01 +1100 Subject: [PATCH 33/38] Fix `TimeEvent` memory management --- nautilus_trader/common/timer.pyx | 3 ++- tests/unit_tests/common/test_common_clock.py | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nautilus_trader/common/timer.pyx b/nautilus_trader/common/timer.pyx index 8849048d96f1..091bddebe879 100644 --- a/nautilus_trader/common/timer.pyx +++ b/nautilus_trader/common/timer.pyx @@ -27,6 +27,7 @@ from nautilus_trader.core.rust.common cimport time_event_free from nautilus_trader.core.rust.common cimport time_event_name from nautilus_trader.core.rust.common cimport time_event_new from nautilus_trader.core.rust.core cimport nanos_to_secs +from nautilus_trader.core.rust.core cimport uuid4_copy from nautilus_trader.core.uuid cimport UUID4 @@ -58,7 +59,7 @@ cdef class TimeEvent(Event): self._mem = time_event_new( name, - event_id._mem, + uuid4_copy(&event_id._mem), ts_event, ts_init, ) diff --git a/tests/unit_tests/common/test_common_clock.py b/tests/unit_tests/common/test_common_clock.py index 2e11d83cfe82..60746a848164 100644 --- a/tests/unit_tests/common/test_common_clock.py +++ b/tests/unit_tests/common/test_common_clock.py @@ -557,7 +557,6 @@ def test_advance_time_with_multiple_set_timers_triggers_events(self): assert clock.timer_count == 2 -@pytest.mark.skip(reason="Investigate threaded memory safety") @pytest.mark.skipif(sys.platform == "win32", reason="Randomly failing on Windows in CI") class TestLiveClockWithThreadTimer: def setup(self): From 1356fca55046c4ea613d0289a5880d778fa9d166 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 2 Nov 2022 19:28:56 +1100 Subject: [PATCH 34/38] Add additional check for client order ID --- nautilus_trader/model/identifiers.pyx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nautilus_trader/model/identifiers.pyx b/nautilus_trader/model/identifiers.pyx index 76549182f14a..1683112dd283 100644 --- a/nautilus_trader/model/identifiers.pyx +++ b/nautilus_trader/model/identifiers.pyx @@ -15,6 +15,7 @@ from cpython.object cimport PyObject +from nautilus_trader.core.correctness cimport Condition from nautilus_trader.core.rust.model cimport account_id_eq from nautilus_trader.core.rust.model cimport account_id_free from nautilus_trader.core.rust.model cimport account_id_hash @@ -525,6 +526,8 @@ cdef class ClientOrderId(Identifier): """ def __init__(self, str value not None): + Condition.valid_string(value, "value") # TODO(cs): Temporary additional check + self._mem = client_order_id_new(value) def __del__(self) -> None: From 655d81d28cb46d36116887e22cb07d9b9396b880 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Wed, 2 Nov 2022 19:29:52 +1100 Subject: [PATCH 35/38] Refine live execution engine config --- RELEASES.md | 6 ++++-- examples/live/binance_futures_market_maker.py | 1 + examples/live/binance_futures_testnet_ema_cross.py | 1 + ...ce_futures_testnet_ema_cross_with_trailing_stop.py | 1 + examples/live/binance_futures_testnet_market_maker.py | 1 + examples/live/binance_spot_ema_cross.py | 1 + examples/live/binance_spot_market_maker.py | 1 + examples/live/binance_spot_testnet_ema_cross.py | 1 + examples/live/ftx_ema_cross.py | 1 + examples/live/ftx_ema_cross_with_trailing_stop.py | 1 + examples/live/ftx_market_maker.py | 1 + examples/live/ftx_stop_entry_with_trailing_stop.py | 1 + nautilus_trader/config/live.py | 9 ++++----- nautilus_trader/live/execution_engine.pxd | 4 ++-- nautilus_trader/live/execution_engine.pyx | 11 ++++++----- 15 files changed, 27 insertions(+), 14 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index ff4452328741..92f9a5b58f62 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -3,7 +3,9 @@ Released on TBD (UTC). ### Breaking Changes -- All Redis keys have changed to a lowercase convention +- Added `LiveExecEngineConfig.reconcilation` boolean flag to control if active +- Removed `LiveExecEngineConfig.reconciliation_auto` +- All Redis keys have changed to a lowercase convention (please either migrate or flush your Redis) - Removed `BidAskMinMax` indicator (to reduce total package size) - Removed `HilbertPeriod` indicator (to reduce total package size) - Removed `HilbertSignalNoiseRatio` indicator (to reduce total package size) @@ -17,7 +19,7 @@ Released on TBD (UTC). - Extended instrument(s) Req/Res handling for `DataClient` and `Actor ### Fixes -None +- Memory management for Rust backing structs (now properly being freed) --- diff --git a/examples/live/binance_futures_market_maker.py b/examples/live/binance_futures_market_maker.py index a6f4e7586757..96d382406bb7 100644 --- a/examples/live/binance_futures_market_maker.py +++ b/examples/live/binance_futures_market_maker.py @@ -40,6 +40,7 @@ trader_id="TESTER-001", log_level="INFO", exec_engine={ + "reconciliation": True, "reconciliation_lookback_mins": 1440, }, cache_database=CacheDatabaseConfig(type="in-memory"), diff --git a/examples/live/binance_futures_testnet_ema_cross.py b/examples/live/binance_futures_testnet_ema_cross.py index dbbca1bf1769..7feec92c62fb 100644 --- a/examples/live/binance_futures_testnet_ema_cross.py +++ b/examples/live/binance_futures_testnet_ema_cross.py @@ -40,6 +40,7 @@ trader_id="TESTER-001", log_level="INFO", exec_engine={ + "reconciliation": True, "reconciliation_lookback_mins": 1440, }, cache_database=CacheDatabaseConfig(type="in-memory"), diff --git a/examples/live/binance_futures_testnet_ema_cross_with_trailing_stop.py b/examples/live/binance_futures_testnet_ema_cross_with_trailing_stop.py index d5e35c169246..b17d0bede649 100644 --- a/examples/live/binance_futures_testnet_ema_cross_with_trailing_stop.py +++ b/examples/live/binance_futures_testnet_ema_cross_with_trailing_stop.py @@ -40,6 +40,7 @@ trader_id="TESTER-001", log_level="INFO", exec_engine={ + "reconciliation": True, "reconciliation_lookback_mins": 1440, }, cache_database=CacheDatabaseConfig(type="in-memory"), diff --git a/examples/live/binance_futures_testnet_market_maker.py b/examples/live/binance_futures_testnet_market_maker.py index aa6be45bc1a6..154ee98c0601 100644 --- a/examples/live/binance_futures_testnet_market_maker.py +++ b/examples/live/binance_futures_testnet_market_maker.py @@ -40,6 +40,7 @@ trader_id="TESTER-001", log_level="INFO", exec_engine={ + "reconciliation": True, "reconciliation_lookback_mins": 1440, }, cache_database=CacheDatabaseConfig(type="in-memory"), diff --git a/examples/live/binance_spot_ema_cross.py b/examples/live/binance_spot_ema_cross.py index 8f4fdfe9dc2b..6252214d4923 100644 --- a/examples/live/binance_spot_ema_cross.py +++ b/examples/live/binance_spot_ema_cross.py @@ -40,6 +40,7 @@ trader_id="TESTER-001", log_level="INFO", exec_engine={ + "reconciliation": True, "reconciliation_lookback_mins": 1440, }, cache_database=CacheDatabaseConfig(type="in-memory"), diff --git a/examples/live/binance_spot_market_maker.py b/examples/live/binance_spot_market_maker.py index c4b1edeaf01d..60e866ddfc3e 100644 --- a/examples/live/binance_spot_market_maker.py +++ b/examples/live/binance_spot_market_maker.py @@ -40,6 +40,7 @@ trader_id="TESTER-001", log_level="INFO", exec_engine={ + "reconciliation": True, "reconciliation_lookback_mins": 1440, }, cache_database=CacheDatabaseConfig(type="in-memory"), diff --git a/examples/live/binance_spot_testnet_ema_cross.py b/examples/live/binance_spot_testnet_ema_cross.py index 25cfa2f7f105..62d3bbe0c37e 100644 --- a/examples/live/binance_spot_testnet_ema_cross.py +++ b/examples/live/binance_spot_testnet_ema_cross.py @@ -40,6 +40,7 @@ trader_id="TESTER-001", log_level="INFO", exec_engine={ + "reconciliation": True, "reconciliation_lookback_mins": 1440, }, cache_database=CacheDatabaseConfig(type="in-memory"), diff --git a/examples/live/ftx_ema_cross.py b/examples/live/ftx_ema_cross.py index 99dbbedceb73..b55a8adecc10 100644 --- a/examples/live/ftx_ema_cross.py +++ b/examples/live/ftx_ema_cross.py @@ -39,6 +39,7 @@ trader_id="TESTER-001", log_level="INFO", exec_engine={ + "reconciliation": True, "reconciliation_lookback_mins": 1440, }, cache_database=CacheDatabaseConfig(type="in-memory"), diff --git a/examples/live/ftx_ema_cross_with_trailing_stop.py b/examples/live/ftx_ema_cross_with_trailing_stop.py index c0d1fff48930..d97462099633 100644 --- a/examples/live/ftx_ema_cross_with_trailing_stop.py +++ b/examples/live/ftx_ema_cross_with_trailing_stop.py @@ -39,6 +39,7 @@ trader_id="TESTER-001", log_level="INFO", exec_engine={ + "reconciliation": True, "reconciliation_lookback_mins": 1440, }, cache_database=CacheDatabaseConfig(type="in-memory"), diff --git a/examples/live/ftx_market_maker.py b/examples/live/ftx_market_maker.py index a41493c12b5b..996eaf312754 100644 --- a/examples/live/ftx_market_maker.py +++ b/examples/live/ftx_market_maker.py @@ -39,6 +39,7 @@ trader_id="TESTER-001", log_level="INFO", exec_engine={ + "reconciliation": True, "reconciliation_lookback_mins": 1440, }, cache_database=CacheDatabaseConfig(type="in-memory"), diff --git a/examples/live/ftx_stop_entry_with_trailing_stop.py b/examples/live/ftx_stop_entry_with_trailing_stop.py index 62e7f2be6bcd..b728b68372f4 100644 --- a/examples/live/ftx_stop_entry_with_trailing_stop.py +++ b/examples/live/ftx_stop_entry_with_trailing_stop.py @@ -39,6 +39,7 @@ trader_id="TESTER-001", log_level="INFO", exec_engine={ + "reconciliation": True, "reconciliation_lookback_mins": 1440, }, cache_database=CacheDatabaseConfig(type="in-memory"), diff --git a/nautilus_trader/config/live.py b/nautilus_trader/config/live.py index aa303aa201e8..38d009e5569c 100644 --- a/nautilus_trader/config/live.py +++ b/nautilus_trader/config/live.py @@ -75,12 +75,11 @@ class LiveExecEngineConfig(ExecEngineConfig): Parameters ---------- - reconciliation_auto : bool, default True - If reconciliation should automatically generate events to align state. + reconciliation : bool, default True + If reconciliation is active at start-up. reconciliation_lookback_mins : NonNegativeInt, optional The maximum lookback minutes to reconcile state for. - If 0 then will not performance any lookback or reconciliation. - If ``None`` then will use the maximum lookback available from the venues. + If ``None`` or 0 then will use the maximum lookback available from the venues. inflight_check_interval_ms : NonNegativeInt, default 5000 The interval (milliseconds) between checking whether in-flight orders have exceeded their time-in-flight threshold. @@ -91,7 +90,7 @@ class LiveExecEngineConfig(ExecEngineConfig): The queue size for the engines internal queue buffers. """ - reconciliation_auto: bool = True + reconciliation: bool = True reconciliation_lookback_mins: Optional[NonNegativeInt] = None inflight_check_interval_ms: NonNegativeInt = 5000 inflight_check_threshold_ms: NonNegativeInt = 1000 diff --git a/nautilus_trader/live/execution_engine.pxd b/nautilus_trader/live/execution_engine.pxd index 53492018868d..93d2990c7b24 100644 --- a/nautilus_trader/live/execution_engine.pxd +++ b/nautilus_trader/live/execution_engine.pxd @@ -37,8 +37,8 @@ cdef class LiveExecutionEngine(ExecutionEngine): cdef readonly bint is_running """If the execution engine is running.\n\n:returns: `bool`""" - cdef readonly bint reconciliation_auto - """If the execution engine will generate reconciliation events to align state.\n\n:returns: `bool`""" + cdef readonly bint reconciliation + """If the execution engine reconciliation is active at start-up.\n\n:returns: `bool`""" cdef readonly int reconciliation_lookback_mins """The lookback window for reconciliation on start-up (zero for max lookback).\n\n:returns: `int`""" cdef readonly int inflight_check_interval_ms diff --git a/nautilus_trader/live/execution_engine.pyx b/nautilus_trader/live/execution_engine.pyx index 7d4e108e1d24..30d54f538726 100644 --- a/nautilus_trader/live/execution_engine.pyx +++ b/nautilus_trader/live/execution_engine.pyx @@ -121,8 +121,8 @@ cdef class LiveExecutionEngine(ExecutionEngine): self._queue = Queue(maxsize=config.qsize) # Settings - self.reconciliation_auto = config.reconciliation_auto - self.reconciliation_lookback_mins = config.reconciliation_lookback_mins + self.reconciliation = config.reconciliation + self.reconciliation_lookback_mins = config.reconciliation_lookback_mins or 0 self.inflight_check_interval_ms = config.inflight_check_interval_ms self.inflight_check_threshold_ms = config.inflight_check_threshold_ms self._inflight_check_threshold_ns = millis_to_nanos(self.inflight_check_threshold_ms) @@ -361,15 +361,16 @@ cdef class LiveExecutionEngine(ExecutionEngine): """ Condition.positive(timeout_secs, "timeout_secs") - if self.reconciliation_lookback_mins == 0: - self._log.warning("No reconciliation.") + if not self.reconciliation: + self._log.warning("Reconciliation deactivated.") return True cdef list results = [] # Request execution mass status report from clients + reconciliation_lookback_mins = self.reconciliation_lookback_mins if self.reconciliation_lookback_mins > 0 else None mass_status_coros = [ - c.generate_mass_status(self.reconciliation_lookback_mins) for c in self._clients.values() + c.generate_mass_status(reconciliation_lookback_mins) for c in self._clients.values() ] mass_status_all = await asyncio.gather(*mass_status_coros) From b98904a36daba888c71423efbfe72a22935d6c33 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 3 Nov 2022 18:35:42 +1100 Subject: [PATCH 36/38] Refine core `Money` and `Currency` --- nautilus_core/model/src/types/currency.rs | 11 +++++- nautilus_trader/model/currency.pyx | 15 +++---- nautilus_trader/model/objects.pyx | 38 ++++++++---------- .../model/test_model_objects_money.py | 39 +++++++++++-------- 4 files changed, 57 insertions(+), 46 deletions(-) diff --git a/nautilus_core/model/src/types/currency.rs b/nautilus_core/model/src/types/currency.rs index fe6728eb96ee..7ab7e2543519 100644 --- a/nautilus_core/model/src/types/currency.rs +++ b/nautilus_core/model/src/types/currency.rs @@ -142,12 +142,21 @@ pub extern "C" fn currency_hash(currency: &Currency) -> u64 { #[cfg(test)] mod tests { use crate::enums::CurrencyType; - use crate::types::currency::Currency; + use crate::types::currency::{currency_eq, Currency}; + + #[test] + fn test_currency_equality() { + let currency1 = Currency::new("AUD", 2, 036, "Australian dollar", CurrencyType::Fiat); + let currency2 = Currency::new("AUD", 2, 036, "Australian dollar", CurrencyType::Fiat); + + assert!(currency_eq(¤cy1, ¤cy2) != 0); + } #[test] fn test_currency_new_for_fiat() { let currency = Currency::new("AUD", 2, 036, "Australian dollar", CurrencyType::Fiat); + assert!(currency_eq(¤cy, ¤cy) != 0); assert_eq!(currency, currency); assert_eq!(currency.code.as_str(), "AUD"); assert_eq!(currency.precision, 2); diff --git a/nautilus_trader/model/currency.pyx b/nautilus_trader/model/currency.pyx index a23cc4e132a1..c7b9a76ebed0 100644 --- a/nautilus_trader/model/currency.pyx +++ b/nautilus_trader/model/currency.pyx @@ -205,7 +205,7 @@ cdef class Currency: @staticmethod def register(Currency currency, bint overwrite=False): """ - Register the given currency. + Register the given `currency`. Will override the internal currency map. @@ -229,11 +229,12 @@ cdef class Currency: Parameters ---------- code : str - The code of the currency to get. + The code of the currency. strict : bool, default False - If strict mode is enabled. If not `strict` mode then it's very likely - a Cryptocurrency, so for robustness will then return a new - Cryptocurrency using the code and a default `precision` of 8. + If not `strict` mode then an unknown currency will very likely + be a Cryptocurrency, so for robustness will then return a new + `Currency` object using the given `code` with a default `precision` + of 8. Returns ------- @@ -261,7 +262,7 @@ cdef class Currency: @staticmethod def is_fiat(str code): """ - Return a value indicating whether a currency with the given code is ``FIAT``. + Return whether a currency with the given code is ``FIAT``. Parameters ---------- @@ -286,7 +287,7 @@ cdef class Currency: @staticmethod def is_crypto(str code): """ - Return a value indicating whether a currency with the given code is ``CRYPTO``. + Return whether a currency with the given code is ``CRYPTO``. Parameters ---------- diff --git a/nautilus_trader/model/objects.pyx b/nautilus_trader/model/objects.pyx index 9affa8cd4882..b2803ae6e091 100644 --- a/nautilus_trader/model/objects.pyx +++ b/nautilus_trader/model/objects.pyx @@ -268,20 +268,18 @@ cdef class Quantity: return self._mem.raw > 0 cdef Quantity add(self, Quantity other): - cdef int64_t raw = self._mem.raw + other.raw_int64_c() - return Quantity.from_raw_c(raw, self._mem.precision) + return Quantity.from_raw_c(self._mem.raw + other._mem.raw, self._mem.precision) cdef Quantity sub(self, Quantity other): - cdef int64_t raw = self._mem.raw - other.raw_int64_c() - return Quantity.from_raw_c(raw, self._mem.precision) + return Quantity.from_raw_c(self._mem.raw - other._mem.raw, self._mem.precision) cdef void add_assign(self, Quantity other) except *: - self._mem.raw += other.raw_uint64_c() + self._mem.raw += other._mem.raw if self._mem.precision == 0: self._mem.precision = other.precision cdef void sub_assign(self, Quantity other) except *: - self._mem.raw -= other.raw_uint64_c() + self._mem.raw -= other._mem.raw if self._mem.precision == 0: self._mem.precision = other.precision @@ -659,18 +657,16 @@ cdef class Price: return self._mem.raw > 0 cdef Price add(self, Price other): - cdef int64_t raw = self._mem.raw + other.raw_int64_c() - return Price.from_raw_c(raw, self._mem.precision) + return Price.from_raw_c(self._mem.raw + other._mem.raw, self._mem.precision) cdef Price sub(self, Price other): - cdef int64_t raw = self._mem.raw - other.raw_int64_c() - return Price.from_raw_c(raw, self._mem.precision) + return Price.from_raw_c(self._mem.raw - other._mem.raw, self._mem.precision) cdef void add_assign(self, Price other) except *: - self._mem.raw += other.raw_int64_c() + self._mem.raw += other._mem.raw cdef void sub_assign(self, Price other) except *: - self._mem.raw -= other.raw_int64_c() + self._mem.raw -= other._mem.raw @staticmethod def from_raw(int64_t raw, uint8_t precision): @@ -977,22 +973,20 @@ cdef class Money: return self._mem.raw > 0 cdef Money add(self, Money other): - assert currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency" # design-time check - cdef int64_t raw = self._mem.raw + other.raw_int64_c() - return Money.from_raw_c(raw, self.currency) + assert currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency" + return Money.from_raw_c(self._mem.raw + other._mem.raw, self.currency) cdef Money sub(self, Money other): - assert currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency" # design-time check - cdef int64_t raw = self._mem.raw - other.raw_int64_c() - return Money.from_raw_c(raw, self.currency) + assert currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency" + return Money.from_raw_c(self._mem.raw - other._mem.raw, self.currency) cdef void add_assign(self, Money other) except *: - assert currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency" # design-time check - self._mem.raw += other.raw_int64_c() + assert currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency" + self._mem.raw += other._mem.raw cdef void sub_assign(self, Money other) except *: - assert currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency" # design-time check - self._mem.raw -= other.raw_int64_c() + assert currency_eq(&self._mem.currency, &other._mem.currency), "currency != other.currency" + self._mem.raw -= other._mem.raw cdef int64_t raw_int64_c(self): return self._mem.raw diff --git a/tests/unit_tests/model/test_model_objects_money.py b/tests/unit_tests/model/test_model_objects_money.py index 349077a1792d..82bf93acc57e 100644 --- a/tests/unit_tests/model/test_model_objects_money.py +++ b/tests/unit_tests/model/test_model_objects_money.py @@ -20,6 +20,7 @@ from nautilus_trader.model.currencies import AUD from nautilus_trader.model.currencies import USD +from nautilus_trader.model.currencies import USDT from nautilus_trader.model.identifiers import InstrumentId from nautilus_trader.model.identifiers import Symbol from nautilus_trader.model.identifiers import Venue @@ -29,22 +30,23 @@ class TestMoney: - def test_instantiate_with_none_currency_raises_type_error(self): + def test_instantiate_with_none_currency_raises_type_error(self) -> None: # Arrange, Act, Assert with pytest.raises(TypeError): Money(1.0, None) - def test_instantiate_with_value_exceeding_positive_limit_raises_value_error(self): + def test_instantiate_with_value_exceeding_positive_limit_raises_value_error(self) -> None: # Arrange, Act, Assert with pytest.raises(ValueError): Money(9_223_372_036 + 1, currency=USD) - def test_instantiate_with_value_exceeding_negative_limit_raises_value_error(self): + def test_instantiate_with_value_exceeding_negative_limit_raises_value_error(self) -> None: # Arrange, Act, Assert with pytest.raises(ValueError): Money(-9_223_372_036 - 1, currency=USD) - def test_instantiate_with_none_value_returns_money_with_zero_amount(self): + def test_instantiate_with_none_value_returns_money_with_zero_amount(self) -> None: + # Arrange, Act money_zero = Money(None, currency=USD) @@ -67,7 +69,9 @@ def test_instantiate_with_none_value_returns_money_with_zero_amount(self): [Decimal("-1.1"), Money(-1.1, USD)], ], ) - def test_instantiate_with_various_valid_inputs_returns_expected_money(self, value, expected): + def test_instantiate_with_various_valid_inputs_returns_expected_money( + self, value, expected + ) -> None: # Arrange, Act money = Money(value, USD) @@ -85,7 +89,7 @@ def test_pickling(self): # Assert assert unpickled == money - def test_as_double_returns_expected_result(self): + def test_as_double_returns_expected_result(self) -> None: # Arrange, Act money = Money(1, USD) @@ -93,7 +97,7 @@ def test_as_double_returns_expected_result(self): assert 1.0 == money.as_double() assert "1.00" == str(money) - def test_initialized_with_many_decimals_rounds_to_currency_precision(self): + def test_initialized_with_many_decimals_rounds_to_currency_precision(self) -> None: # Arrange, Act result1 = Money(1000.333, USD) result2 = Money(5005.556666, USD) @@ -102,7 +106,7 @@ def test_initialized_with_many_decimals_rounds_to_currency_precision(self): assert "1_000.33 USD" == result1.to_str() assert "5_005.56 USD" == result2.to_str() - def test_equality_with_different_currencies_raises_value_error(self): + def test_equality_with_different_currencies_raises_value_error(self) -> None: # Arrange money1 = Money(1, USD) money2 = Money(1, AUD) @@ -111,7 +115,7 @@ def test_equality_with_different_currencies_raises_value_error(self): with pytest.raises(ValueError): assert money1 != money2 - def test_equality(self): + def test_equality(self) -> None: # Arrange money1 = Money(1, USD) money2 = Money(1, USD) @@ -121,7 +125,7 @@ def test_equality(self): assert money1 == money2 assert money1 != money3 - def test_hash(self): + def test_hash(self) -> None: # Arrange money0 = Money(0, USD) @@ -129,7 +133,7 @@ def test_hash(self): assert isinstance(hash(money0), int) assert hash(money0) == hash(money0) - def test_str(self): + def test_str(self) -> None: # Arrange money0 = Money(0, USD) money1 = Money(1, USD) @@ -141,7 +145,7 @@ def test_str(self): assert "1000000.00" == str(money2) assert "1_000_000.00 USD" == money2.to_str() - def test_repr(self): + def test_repr(self) -> None: # Arrange money = Money(1.00, USD) @@ -151,7 +155,7 @@ def test_repr(self): # Assert assert "Money('1.00', USD)" == result - def test_from_str_when_malformed_raises_value_error(self): + def test_from_str_when_malformed_raises_value_error(self) -> None: # Arrange value = "@" @@ -162,6 +166,7 @@ def test_from_str_when_malformed_raises_value_error(self): @pytest.mark.parametrize( "value, expected", [ + ["1.00 USDT", Money(1.00, USDT)], ["1.00 USD", Money(1.00, USD)], ["1.001 AUD", Money(1.00, AUD)], ], @@ -170,12 +175,14 @@ def test_from_str_given_valid_strings_returns_expected_result( self, value, expected, - ): + ) -> None: # Arrange, Act - result = Money.from_str(value) + result1 = Money.from_str(value) + result2 = Money.from_str(value) # Assert - assert result == expected + assert result1 == result2 + assert result1 == expected class TestAccountBalance: From 4ca679392948b8475359bfd1059ae0fb25ebe169 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 3 Nov 2022 20:16:06 +1100 Subject: [PATCH 37/38] Update dependencies --- nautilus_core/common/Cargo.toml | 4 ++-- nautilus_core/core/Cargo.toml | 6 +++--- nautilus_core/model/Cargo.toml | 2 +- nautilus_core/persistence/Cargo.toml | 4 ++-- poetry.lock | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/nautilus_core/common/Cargo.toml b/nautilus_core/common/Cargo.toml index f9d4cbb028ff..6ebf4169c3be 100644 --- a/nautilus_core/common/Cargo.toml +++ b/nautilus_core/common/Cargo.toml @@ -12,8 +12,8 @@ crate-type = ["rlib", "staticlib"] [dependencies] nautilus_core = { path = "../core" } nautilus_model = { path = "../model" } -pyo3 = { version = "0.17.2" } -chrono = "0.4.19" +chrono = "0.4.22" +pyo3 = { version = "0.17.3" } [features] extension-module = [ diff --git a/nautilus_core/core/Cargo.toml b/nautilus_core/core/Cargo.toml index ab28fa700c15..b730b23db612 100644 --- a/nautilus_core/core/Cargo.toml +++ b/nautilus_core/core/Cargo.toml @@ -9,10 +9,10 @@ name = "nautilus_core" crate-type = ["rlib", "staticlib"] [dependencies] -chrono = "0.4.19" -pyo3 = { version = "0.17.2" } -uuid = { version = "1.1.2", features = ["v4"] } +chrono = "0.4.22" lazy_static = "1.4.0" +pyo3 = { version = "0.17.3" } +uuid = { version = "1.1.2", features = ["v4"] } [features] extension-module = ["pyo3/extension-module"] diff --git a/nautilus_core/model/Cargo.toml b/nautilus_core/model/Cargo.toml index c65bc8feef58..7443c4b68e9f 100644 --- a/nautilus_core/model/Cargo.toml +++ b/nautilus_core/model/Cargo.toml @@ -10,7 +10,7 @@ crate-type = ["rlib", "staticlib"] [dependencies] nautilus_core = { path = "../core" } -pyo3 = { version = "0.17.2" } +pyo3 = { version = "0.17.3" } [features] extension-module = [ diff --git a/nautilus_core/persistence/Cargo.toml b/nautilus_core/persistence/Cargo.toml index 856a9b047403..db454c809a09 100644 --- a/nautilus_core/persistence/Cargo.toml +++ b/nautilus_core/persistence/Cargo.toml @@ -12,8 +12,8 @@ crate-type = ["rlib", "staticlib"] nautilus_core = { path = "../core" } nautilus_model = { path = "../model" } arrow2 = { version = "0.13.1", features = [ "io_parquet", "io_csv_read", "compute_comparison" ] } -chrono = "^0.4.19" -pyo3 = { version = "0.17.2" } +chrono = "^0.4.22" +pyo3 = { version = "0.17.3" } rand = "0.8.5" [features] diff --git a/poetry.lock b/poetry.lock index ef2070a9bfd5..c6c6ca21307a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -262,7 +262,7 @@ python-versions = "*" [[package]] name = "docker" -version = "6.0.0" +version = "6.0.1" description = "A Python library for the Docker Engine API." category = "main" optional = true @@ -1843,8 +1843,8 @@ distlib = [ {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, ] docker = [ - {file = "docker-6.0.0-py3-none-any.whl", hash = "sha256:6e06ee8eca46cd88733df09b6b80c24a1a556bc5cb1e1ae54b2c239886d245cf"}, - {file = "docker-6.0.0.tar.gz", hash = "sha256:19e330470af40167d293b0352578c1fa22d74b34d3edf5d4ff90ebc203bbb2f1"}, + {file = "docker-6.0.1-py3-none-any.whl", hash = "sha256:dbcb3bd2fa80dca0788ed908218bf43972772009b881ed1e20dfc29a65e49782"}, + {file = "docker-6.0.1.tar.gz", hash = "sha256:896c4282e5c7af5c45e8b683b0b0c33932974fe6e50fc6906a0a83616ab3da97"}, ] docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, From 216e6fb58c054a917aa5f05931a1c433e7bea755 Mon Sep 17 00:00:00 2001 From: Chris Sellers Date: Thu, 3 Nov 2022 20:39:53 +1100 Subject: [PATCH 38/38] Update release notes --- RELEASES.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 92f9a5b58f62..ef7c7263e63c 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,10 +1,10 @@ # NautilusTrader 1.158.0 Beta -Released on TBD (UTC). +Released on 3rd November (UTC). ### Breaking Changes -- Added `LiveExecEngineConfig.reconcilation` boolean flag to control if active -- Removed `LiveExecEngineConfig.reconciliation_auto` +- Added `LiveExecEngineConfig.reconcilation` boolean flag to control if reconciliation is active +- Removed `LiveExecEngineConfig.reconciliation_auto` (unclear naming and concept) - All Redis keys have changed to a lowercase convention (please either migrate or flush your Redis) - Removed `BidAskMinMax` indicator (to reduce total package size) - Removed `HilbertPeriod` indicator (to reduce total package size) @@ -19,7 +19,7 @@ Released on TBD (UTC). - Extended instrument(s) Req/Res handling for `DataClient` and `Actor ### Fixes -- Memory management for Rust backing structs (now properly being freed) +- Fixed memory management for Rust backing structs (now being properly freed) ---