diff --git a/.github/workflows/test-tag-publish.yml b/.github/workflows/test-tag-publish.yml index fd2cb0eca2c4..0b2446a18232 100644 --- a/.github/workflows/test-tag-publish.yml +++ b/.github/workflows/test-tag-publish.yml @@ -130,6 +130,7 @@ jobs: release_name: ${{ env.RELEASE_NAME }} draft: false prerelease: false + body_path: RELEASE.md publish_sdist: diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 000000000000..54281b49d573 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,42 @@ +# NautilusTrader 1.107.0 Beta Release Notes + +The main thrust of this release is to refine some subtleties relating to order +matching and amendment behaviour for improved realism. This involved a fairly substantial refactoring +of `SimulatedExchange` to manage its complexity, and support extending the order types. + +The `post_only` flag for LIMIT orders now results in the expected behaviour regarding +when a marketable limit order will become a liquidity `TAKER` during order placement +and amendment. + +Test coverage was moderately increased. + +### Breaking Changes +None + +### Enhancements +- Refactored `SimulatedExchange` order matching and amendment logic. +- Add `risk` sub-package to group risk components. + +### Fixes +- `StopLimitOrder` triggering behaviour. +- All flake8 warnings. + +# NautilusTrader 1.106.0 Beta Release Notes + +The main thrust of this release is to introduce the Interactive Brokers +integration, and begin adding platform capabilities to support this effort. + +### Breaking Changes +- `from_serializable_string` methods changed to `from_serializable_str`. + +### Enhancements +- Scaffold Interactive Brokers integration in `adapters/ib`. +- Add the `Future` instrument type. +- Add the `StopLimitOrder` order type. +- Add the `Data` and `DataType` types to support custom data handling. +- Add the `Security` identifier types initial implementation to support extending the platforms capabilities. + +### Fixes +- `BracketOrder` correctness. +- CCXT precision parsing bug. +- Some log formatting. diff --git a/docs/source/api_reference/risk.rst b/docs/source/api_reference/risk.rst new file mode 100644 index 000000000000..3165e65a569a --- /dev/null +++ b/docs/source/api_reference/risk.rst @@ -0,0 +1,14 @@ +Risk +==== + +.. automodule:: nautilus_trader.risk + + +Sizing +------ + +.. automodule:: nautilus_trader.risk.sizing + :show-inheritance: + :inherited-members: + :members: + :member-order: bysource diff --git a/docs/source/api_reference/trading.rst b/docs/source/api_reference/trading.rst index 47626fb21099..f06fe27faa73 100644 --- a/docs/source/api_reference/trading.rst +++ b/docs/source/api_reference/trading.rst @@ -40,15 +40,6 @@ Portfolio :members: :member-order: bysource -Sizing ------- - -.. automodule:: nautilus_trader.trading.sizing - :show-inheritance: - :inherited-members: - :members: - :member-order: bysource - Strategy -------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 4870f8f274c5..f0399f93344e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -120,6 +120,7 @@ Index api_reference/live api_reference/model api_reference/redis + api_reference/risk api_reference/serialization api_reference/trading diff --git a/examples/strategies/ema_cross_cython.pyx b/examples/strategies/ema_cross_cython.pyx index b98d95899d37..f830da4eaa69 100644 --- a/examples/strategies/ema_cross_cython.pyx +++ b/examples/strategies/ema_cross_cython.pyx @@ -15,8 +15,8 @@ from decimal import Decimal -from nautilus_trader.data.base cimport Data from nautilus_trader.core.message cimport Event +from nautilus_trader.data.base cimport Data from nautilus_trader.indicators.average.ema cimport ExponentialMovingAverage from nautilus_trader.model.bar cimport Bar from nautilus_trader.model.bar cimport BarSpecification diff --git a/nautilus_trader/backtest/exchange.pxd b/nautilus_trader/backtest/exchange.pxd index a5d9660e5681..6b10255ce9a5 100644 --- a/nautilus_trader/backtest/exchange.pxd +++ b/nautilus_trader/backtest/exchange.pxd @@ -137,6 +137,10 @@ cdef class SimulatedExchange: cdef inline void _process_limit_order(self, LimitOrder order, Price bid, Price ask) except * cdef inline void _process_stop_market_order(self, StopMarketOrder order, Price bid, Price ask) except * cdef inline void _process_stop_limit_order(self, StopLimitOrder order, Price bid, Price ask) except * + cdef inline void _amend_limit_order(self, LimitOrder order, Quantity qty, Price price, Price bid, Price ask) except * + cdef inline void _amend_stop_market_order(self, StopMarketOrder order, Quantity qty, Price price, Price bid, Price ask) except * + cdef inline void _amend_stop_limit_order(self, StopLimitOrder order, Quantity qty, Price price, Price bid, Price ask) except * + cdef inline void _generate_order_amended(self, PassiveOrder order, Quantity qty, Price price) except * # -- ORDER MATCHING ENGINE ------------------------------------------------------------------------- @@ -148,8 +152,12 @@ cdef class SimulatedExchange: cdef inline bint _is_limit_matched(self, OrderSide side, Price order_price, Price bid, Price ask) except * cdef inline bint _is_stop_marketable(self, OrderSide side, Price order_price, Price bid, Price ask) except * cdef inline bint _is_stop_triggered(self, OrderSide side, Price order_price, Price bid, Price ask) except * - cdef inline Price _market_fill_price(self, Symbol symbol, OrderSide side, Price bid, Price ask) - cdef inline Price _stop_fill_price(self, Symbol symbol, OrderSide side, Price stop) + cdef inline Price _fill_price_maker(self, OrderSide side, Price bid, Price ask) + cdef inline Price _fill_price_taker(self, Symbol symbol, OrderSide side, Price bid, Price ask) + cdef inline Price _fill_price_stop(self, Symbol symbol, OrderSide side, Price stop) + +# -------------------------------------------------------------------------------------------------- + cdef inline void _fill_order(self, Order order, Price fill_price, LiquiditySide liquidity_side) except * cdef inline void _clean_up_child_orders(self, ClientOrderId cl_ord_id) except * cdef inline void _check_oco_order(self, ClientOrderId cl_ord_id) except * diff --git a/nautilus_trader/backtest/exchange.pyx b/nautilus_trader/backtest/exchange.pyx index 8b8cb72d36bd..ec6fa2633b5c 100644 --- a/nautilus_trader/backtest/exchange.pyx +++ b/nautilus_trader/backtest/exchange.pyx @@ -525,7 +525,7 @@ cdef class SimulatedExchange: "amend order", f"repr{cl_ord_id} not found", ) - return # Rejected the amend order request + return # Cannot amend order cdef Instrument instrument = self.instruments[order.symbol] @@ -540,79 +540,24 @@ cdef class SimulatedExchange: cdef Price bid = self._market_bids.get(order.symbol) cdef Price ask = self._market_asks.get(order.symbol) - cdef bint fill = False - # Check if request valid and if filled + # Check market exists + if bid is None or ask is None: # Market not initialized + self._cancel_reject( + cl_ord_id, + "amend order", + f"no market for {order.symbol}", + ) + return # Cannot amend order + if order.type == OrderType.LIMIT: - if self._is_limit_marketable(order.side, order.price, bid, ask): - if order.is_post_only: - self._cancel_reject( - order.cl_ord_id, - "amend order", - f"{OrderSideParser.to_str(order.side)} POST_ONLY LIMIT order " - f"px of {order.price} would have been TAKER: bid={bid}, ask={ask}", - ) - return # Rejected the amend order request - else: - fill = True + self._amend_limit_order(order, qty, price, bid, ask) elif order.type == OrderType.STOP_MARKET: - if self._is_stop_marketable(order.side, order.price, bid, ask): - self._cancel_reject( - order.cl_ord_id, - "amend order", - f"{OrderSideParser.to_str(order.side)} STOP order " - f"px of {order.price} was in the market: bid={bid}, ask={ask}", - ) - return # Rejected the amend order request + self._amend_stop_market_order(order, qty, price, bid, ask) elif order.type == OrderType.STOP_LIMIT: - if not order.is_triggered: - # Amending stop price - if self._is_stop_marketable(order.side, order.trigger, bid, ask): - self._cancel_reject( - order.cl_ord_id, - "amend order", - f"{OrderSideParser.to_str(order.side)} STOP_LIMIT order " - f"trigger px of {order.trigger} was in the market: bid={bid}, ask={ask}", - ) - return # Rejected the amend order request - else: - # Amending limit price - if self._is_limit_marketable(order.side, order.price, bid, ask): - if order.is_post_only: - self._cancel_reject( - order.cl_ord_id, - "amend order", - f"{OrderSideParser.to_str(order.side)} POST_ONLY LIMIT order " - f"limit px of {order.price} would have been TAKER: bid={bid}, ask={ask}", - ) - return # Rejected the amend order request - else: - fill = True + self._amend_stop_limit_order(order, qty, price, bid, ask) else: raise RuntimeError(f"Invalid order type") - # Generate event - cdef OrderAmended amended = OrderAmended( - order.account_id, - order.cl_ord_id, - order.id, - qty, - price, - self._clock.utc_now_c(), - self._uuid_factory.generate(), - self._clock.utc_now_c(), - ) - - self.exec_client.handle_event(amended) - - if fill: - fill_price = self._market_fill_price( - order.side, - order.symbol, - bid, - ask, - ) - self._fill_order(order, fill_price, LiquiditySide.TAKER) - cdef inline void _cancel_order(self, ClientOrderId cl_ord_id) except *: cdef PassiveOrder order = self._working_orders.pop(cl_ord_id, None) if order is None: @@ -749,7 +694,7 @@ cdef class SimulatedExchange: # Immediately fill marketable order self._fill_order( order, - self._market_fill_price(order.symbol, order.side, bid, ask), + self._fill_price_taker(order.symbol, order.side, bid, ask), LiquiditySide.TAKER, ) @@ -769,8 +714,8 @@ cdef class SimulatedExchange: # Check for immediate fill cdef Price fill_price - if self._is_limit_marketable(order.side, order.price, bid, ask): - fill_price = self._market_fill_price(order.symbol, order.side, bid, ask) + if not order.is_post_only and self._is_limit_marketable(order.side, order.price, bid, ask): + fill_price = self._fill_price_maker(order.side, bid, ask) self._fill_order(order, fill_price, LiquiditySide.TAKER) cdef inline void _process_stop_market_order(self, StopMarketOrder order, Price bid, Price ask) except *: @@ -799,6 +744,110 @@ cdef class SimulatedExchange: self._working_orders[order.cl_ord_id] = order self._accept_order(order) + cdef inline void _amend_limit_order( + self, + LimitOrder order, + Quantity qty, + Price price, + Price bid, + Price ask, + ) except *: + cdef Price fill_price + if self._is_limit_marketable(order.side, price, bid, ask): + if order.is_post_only: + self._cancel_reject( + order.cl_ord_id, + "amend order", + f"{OrderSideParser.to_str(order.side)} POST_ONLY LIMIT order " + f"amended limit px of {price} would have been TAKER: bid={bid}, ask={ask}", + ) + return # Cannot amend order + else: + # Immediate fill as TAKER + self._generate_order_amended(order, qty, price) + + fill_price = self._fill_price_taker(order.symbol, order.side, bid, ask) + self._fill_order(order, fill_price, LiquiditySide.TAKER) + return # Filled + + self._generate_order_amended(order, qty, price) + + cdef inline void _amend_stop_market_order( + self, + StopMarketOrder order, + Quantity qty, + Price price, + Price bid, + Price ask, + ) except *: + if self._is_stop_marketable(order.side, price, bid, ask): + self._cancel_reject( + order.cl_ord_id, + "amend order", + f"{OrderSideParser.to_str(order.side)} STOP order " + f"amended stop px of {price} was in the market: bid={bid}, ask={ask}", + ) + return # Cannot amend order + + self._generate_order_amended(order, qty, price) + + cdef inline void _amend_stop_limit_order( + self, + StopLimitOrder order, + Quantity qty, + Price price, + Price bid, + Price ask, + ) except *: + cdef Price fill_price + if not order.is_triggered: + # Amending stop price + if self._is_stop_marketable(order.side, price, bid, ask): + self._cancel_reject( + order.cl_ord_id, + "amend order", + f"{OrderSideParser.to_str(order.side)} STOP_LIMIT order " + f"amended stop px trigger of {price} was in the market: bid={bid}, ask={ask}", + ) + return # Cannot amend order + + self._generate_order_amended(order, qty, price) + else: + # Amending limit price + if self._is_limit_marketable(order.side, price, bid, ask): + if order.is_post_only: + self._cancel_reject( + order.cl_ord_id, + "amend order", + f"{OrderSideParser.to_str(order.side)} POST_ONLY LIMIT order " + f"amended limit px of {price} would have been TAKER: bid={bid}, ask={ask}", + ) + return # Cannot amend order + else: + # Immediate fill as TAKER + self._generate_order_amended(order, qty, price) + + fill_price = self._fill_price_taker(order.symbol, order.side, bid, ask) + self._fill_order(order, fill_price, LiquiditySide.TAKER) + return # Filled + + self._generate_order_amended(order, qty, price) + + cdef inline void _generate_order_amended(self, PassiveOrder order, Quantity qty, Price price) except *: + # Generate event + cdef OrderAmended amended = OrderAmended( + order.account_id, + order.cl_ord_id, + order.id, + qty, + price, + self._clock.utc_now_c(), + self._uuid_factory.generate(), + self._clock.utc_now_c(), + ) + + self.exec_client.handle_event(amended) + # -- ORDER MATCHING ENGINE ------------------------------------------------------------------------- cdef inline void _match_order(self, PassiveOrder order, Price bid, Price ask) except *: @@ -823,7 +872,7 @@ cdef class SimulatedExchange: if self._is_stop_triggered(order.side, order.price, bid, ask): self._fill_order( order, - self._stop_fill_price(order.symbol, order.side, order.price), + self._fill_price_stop(order.symbol, order.side, order.price), LiquiditySide.TAKER, ) @@ -840,7 +889,7 @@ cdef class SimulatedExchange: else: self._fill_order( order, - self._market_fill_price(order.symbol, order.side, bid, ask), + self._fill_price_taker(order.symbol, order.side, bid, ask), LiquiditySide.TAKER, # Immediate fill takes liquidity ) return # Triggered, rejected or filled @@ -854,9 +903,9 @@ cdef class SimulatedExchange: cdef inline bint _is_limit_marketable(self, OrderSide side, Price order_price, Price bid, Price ask) except *: if side == OrderSide.BUY: - return order_price >= ask + return order_price >= ask # Match with LIMIT sells else: # => OrderSide.SELL - return order_price <= bid + return order_price <= bid # Match with LIMIT buys cdef inline bint _is_limit_matched(self, OrderSide side, Price order_price, Price bid, Price ask) except *: if side == OrderSide.BUY: @@ -866,9 +915,9 @@ cdef class SimulatedExchange: cdef inline bint _is_stop_marketable(self, OrderSide side, Price order_price, Price bid, Price ask) except *: if side == OrderSide.BUY: - return order_price <= ask + return order_price <= ask # Match with LIMIT sells else: # => OrderSide.SELL - return order_price >= bid + return order_price >= bid # Match with LIMIT buys cdef inline bint _is_stop_triggered(self, OrderSide side, Price order_price, Price bid, Price ask) except *: if side == OrderSide.BUY: @@ -876,18 +925,29 @@ cdef class SimulatedExchange: else: # => OrderSide.SELL return order_price > bid or (order_price == bid and self.fill_model.is_stop_filled()) - cdef inline Price _market_fill_price(self, Symbol symbol, OrderSide side, Price bid, Price ask): + cdef inline Price _fill_price_maker(self, OrderSide side, Price bid, Price ask): + # LIMIT orders will always fill at the top of the book, + # (currently not simulating market impact). + if side == OrderSide.BUY: + return bid + else: # => OrderSide.SELL + return ask + + cdef inline Price _fill_price_taker(self, Symbol symbol, OrderSide side, Price bid, Price ask): + # Simulating potential slippage of one tick if side == OrderSide.BUY: return ask if not self.fill_model.is_slipped() else Price(ask + self._slippages[symbol]) else: # => OrderSide.SELL return bid if not self.fill_model.is_slipped() else Price(bid - self._slippages[symbol]) - cdef inline Price _stop_fill_price(self, Symbol symbol, OrderSide side, Price stop): + cdef inline Price _fill_price_stop(self, Symbol symbol, OrderSide side, Price stop): if side == OrderSide.BUY: return stop if not self.fill_model.is_slipped() else Price(stop + self._slippages[symbol]) else: # => OrderSide.SELL return stop if not self.fill_model.is_slipped() else Price(stop - self._slippages[symbol]) +# -------------------------------------------------------------------------------------------------- + cdef inline void _fill_order( self, Order order, diff --git a/nautilus_trader/backtest/models.pyx b/nautilus_trader/backtest/models.pyx index bdac6955bf8a..7698cce1d74b 100644 --- a/nautilus_trader/backtest/models.pyx +++ b/nautilus_trader/backtest/models.pyx @@ -30,7 +30,7 @@ cdef class FillModel: def __init__( self, - double prob_fill_at_limit=0.0, + double prob_fill_at_limit=1.0, double prob_fill_at_stop=1.0, double prob_slippage=0.0, random_seed=None, @@ -108,7 +108,5 @@ cdef class FillModel: # probability is the probability of the event occurring [0, 1]. if probability == 0: return False - elif probability == 1.: - return True else: return probability >= drand48() diff --git a/nautilus_trader/execution/cache.pyx b/nautilus_trader/execution/cache.pyx index f5fd41c9028e..b8e226e76b51 100644 --- a/nautilus_trader/execution/cache.pyx +++ b/nautilus_trader/execution/cache.pyx @@ -336,14 +336,14 @@ cdef class ExecutionCache(ExecutionCacheFacade): error_count += 1 # Finally - cdef long total_ns = round((time.time() - ts) * 1000000) + cdef long total_us = round((time.time() - ts) * 1000000) if error_count == 0: - self._log.info(f"Integrity check passed in {total_ns}μs.", LogColor.GREEN) + self._log.info(f"Integrity check passed in {total_us}μs.", LogColor.GREEN) return True else: self._log.error(f"Integrity check failed with " f"{error_count} error{'' if error_count == 1 else 's'} " - f"in {total_ns}μs.") + f"in {total_us}μs.") return False cpdef bint check_residuals(self) except *: diff --git a/nautilus_trader/execution/engine.pyx b/nautilus_trader/execution/engine.pyx index fe119ba39d3a..c74a42b014c5 100644 --- a/nautilus_trader/execution/engine.pyx +++ b/nautilus_trader/execution/engine.pyx @@ -383,8 +383,8 @@ cdef class ExecutionEngine(Component): self.cache.check_integrity() self._set_position_id_counts() - cdef long total_ns = round((self._clock.unix_time() - ts) * 1000000) - self._log.info(f"Loaded cache in {total_ns}μs.") + cdef long total_us = round((self._clock.unix_time() - ts) * 1000000) + self._log.info(f"Loaded cache in {total_us}μs.") # Update portfolio for account in self.cache.accounts(): diff --git a/nautilus_trader/model/order/base.pyx b/nautilus_trader/model/order/base.pyx index b9be08af248c..059d50e8b1ac 100644 --- a/nautilus_trader/model/order/base.pyx +++ b/nautilus_trader/model/order/base.pyx @@ -74,6 +74,9 @@ cdef dict _ORDER_STATE_TABLE = { (OrderState.PARTIALLY_FILLED, OrderState.FILLED): OrderState.FILLED, } +# Valid states to amend an order in +cdef tuple _AMENDING_STATES = (OrderState.ACCEPTED, OrderState.TRIGGERED) + cdef class Order: """ @@ -445,7 +448,7 @@ cdef class Order: self._fsm.trigger(OrderState.ACCEPTED) self._accepted(event) elif isinstance(event, OrderAmended): - Condition.true(self._fsm.state == OrderState.ACCEPTED, "state was != OrderState.ACCEPTED") + Condition.true(self._fsm.state in _AMENDING_STATES, "state was invalid for amending") self._amended(event) elif isinstance(event, OrderCancelled): # OrderId should have been assigned @@ -460,6 +463,7 @@ cdef class Order: elif isinstance(event, OrderTriggered): Condition.true(self.type == OrderType.STOP_LIMIT, "can only trigger a STOP_LIMIT order") self._fsm.trigger(OrderState.TRIGGERED) + self._triggered(event) elif isinstance(event, OrderFilled): if self.id.not_null(): Condition.equal(self.id, event.order_id, "id", "event.order_id") diff --git a/nautilus_trader/model/order/stop_limit.pyx b/nautilus_trader/model/order/stop_limit.pyx index e834c1c5209f..35af0a2c79a2 100644 --- a/nautilus_trader/model/order/stop_limit.pyx +++ b/nautilus_trader/model/order/stop_limit.pyx @@ -202,4 +202,4 @@ cdef class StopLimitOrder(PassiveOrder): self.trigger = event.price cdef void _triggered(self, OrderTriggered event) except *: - self._is_triggered = True + self.is_triggered = True diff --git a/nautilus_trader/risk/__init__.pxd b/nautilus_trader/risk/__init__.pxd new file mode 100644 index 000000000000..86147c8dc6d8 --- /dev/null +++ b/nautilus_trader/risk/__init__.pxd @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 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. +# ------------------------------------------------------------------------------------------------- diff --git a/nautilus_trader/risk/__init__.py b/nautilus_trader/risk/__init__.py new file mode 100644 index 000000000000..c7e63963d419 --- /dev/null +++ b/nautilus_trader/risk/__init__.py @@ -0,0 +1,21 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 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. +# ------------------------------------------------------------------------------------------------- + +""" +The `risk` sub-package groups all risk specific components and tooling. + +Base classes are defined which enable users to register `RiskModule` implementations +with an `ExecutionEngine`, and write `PositionSizer` components. +""" diff --git a/nautilus_trader/risk/module.pxd b/nautilus_trader/risk/module.pxd new file mode 100644 index 000000000000..d6bc53ce0dfc --- /dev/null +++ b/nautilus_trader/risk/module.pxd @@ -0,0 +1,21 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 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.model.order.base cimport Order + + +cdef class RiskModule: + + cpdef void approve(self, Order order) except * diff --git a/nautilus_trader/risk/module.pyx b/nautilus_trader/risk/module.pyx new file mode 100644 index 000000000000..f665888c2f92 --- /dev/null +++ b/nautilus_trader/risk/module.pyx @@ -0,0 +1,36 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 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.model.order.base cimport Order + + +cdef class RiskModule: + """ + The abstract base class for all risk modules. + + This class should not be used directly, but through its concrete subclasses. + """ + + def __init__(self): + """ + Initialize a new instance of the `RiskModule` class. + """ + + def __repr__(self) -> str: + return f"{type(self).__name__}" + + cpdef void approve(self, Order order) except *: + """Abstract method (implement in subclass).""" + raise NotImplementedError("method must be implemented in the subclass") diff --git a/nautilus_trader/trading/sizing.pxd b/nautilus_trader/risk/sizing.pxd similarity index 100% rename from nautilus_trader/trading/sizing.pxd rename to nautilus_trader/risk/sizing.pxd diff --git a/nautilus_trader/trading/sizing.pyx b/nautilus_trader/risk/sizing.pyx similarity index 100% rename from nautilus_trader/trading/sizing.pyx rename to nautilus_trader/risk/sizing.pyx diff --git a/poetry.lock b/poetry.lock index 2ba96e9553f8..3feeef878800 100644 --- a/poetry.lock +++ b/poetry.lock @@ -109,7 +109,7 @@ pytz = ">=2015.7" [[package]] name = "ccxt" -version = "1.42.40" +version = "1.42.59" description = "A JavaScript / Python / PHP cryptocurrency trading library with support for 130+ exchanges" category = "main" optional = false @@ -310,11 +310,11 @@ nest-asyncio = "*" [[package]] name = "identify" -version = "1.5.14" +version = "2.1.0" description = "File identification library for Python" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" +python-versions = ">=3.6.1" [package.extras] license = ["editdistance"] @@ -509,7 +509,7 @@ pyparsing = ">=2.0.2" [[package]] name = "pandas" -version = "1.2.2" +version = "1.2.3" description = "Powerful data structures for data analysis, time series, and statistics" category = "main" optional = false @@ -998,15 +998,15 @@ multidict = ">=4.0" [[package]] name = "zipp" -version = "3.4.0" +version = "3.4.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.6" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "jaraco.test (>=3.2.0)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] +docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] +testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pytest-cov", "pytest-enabler", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"] [extras] docs = ["numpydoc"] @@ -1014,7 +1014,7 @@ docs = ["numpydoc"] [metadata] lock-version = "1.1" python-versions = "^3.7.9" -content-hash = "f6196231cdab88e4eeda3ff4f11919b47109f4c92bf4ad71d2fbbe8cbfeb8cfb" +content-hash = "6c1afa5c7507134442b53d4aa2afbebd72d963e5321d3c733d0bfc0f9b930d5d" [metadata.files] aiodns = [ @@ -1093,8 +1093,8 @@ babel = [ {file = "Babel-2.9.0.tar.gz", hash = "sha256:da031ab54472314f210b0adcff1588ee5d1d1d0ba4dbd07b94dba82bde791e05"}, ] ccxt = [ - {file = "ccxt-1.42.40-py2.py3-none-any.whl", hash = "sha256:af30f60374455c8a4d1a8bf798c7091c164039602c94abebc0533bad4b937483"}, - {file = "ccxt-1.42.40.tar.gz", hash = "sha256:3c11845ed17b7ec009bebd76678763865bdf7d85e9bf1bf95cd4b5927395b94a"}, + {file = "ccxt-1.42.59-py2.py3-none-any.whl", hash = "sha256:b440b8a08f688a97dc2af907e6689bfe15f3e274e22a2928c94bb6ed409d864c"}, + {file = "ccxt-1.42.59.tar.gz", hash = "sha256:cab1421810be18606ecb8862962df24f4f16eea4d72886bb1cd15fc90466660e"}, ] certifi = [ {file = "certifi-2020.12.5-py2.py3-none-any.whl", hash = "sha256:719a74fb9e33b9bd44cc7f3a8d94bc35e4049deebe19ba7d8e108280cfd59830"}, @@ -1276,8 +1276,8 @@ ib-insync = [ {file = "ib_insync-0.9.65.tar.gz", hash = "sha256:7fe0bb294bb7a86414ebc0935a5f759ebb3b3abe903907b73ce0ba938a0c0510"}, ] identify = [ - {file = "identify-1.5.14-py2.py3-none-any.whl", hash = "sha256:e0dae57c0397629ce13c289f6ddde0204edf518f557bfdb1e56474aa143e77c3"}, - {file = "identify-1.5.14.tar.gz", hash = "sha256:de7129142a5c86d75a52b96f394d94d96d497881d2aaf8eafe320cdbe8ac4bcc"}, + {file = "identify-2.1.0-py2.py3-none-any.whl", hash = "sha256:2a5fdf2f5319cc357eda2550bea713a404392495961022cf2462624ce62f0f46"}, + {file = "identify-2.1.0.tar.gz", hash = "sha256:2179e7359471ab55729f201b3fdf7dc2778e221f868410fedcb0987b791ba552"}, ] idna = [ {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, @@ -1500,24 +1500,22 @@ packaging = [ {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, ] pandas = [ - {file = "pandas-1.2.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c76a108272a4de63189b8f64086bbaf8348841d7e610b52f50959fbbf401524f"}, - {file = "pandas-1.2.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:e61a089151f1ed78682aa77a3bcae0495cf8e585546c26924857d7e8a9960568"}, - {file = "pandas-1.2.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:fc351cd2df318674669481eb978a7799f24fd14ef26987a1aa75105b0531d1a1"}, - {file = "pandas-1.2.2-cp37-cp37m-win32.whl", hash = "sha256:05ca6bda50123158eb15e716789083ca4c3b874fd47688df1716daa72644ee1c"}, - {file = "pandas-1.2.2-cp37-cp37m-win_amd64.whl", hash = "sha256:08b6bbe74ae2b3e4741a744d2bce35ce0868a6b4189d8b84be26bb334f73da4c"}, - {file = "pandas-1.2.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:230de25bd9791748b2638c726a5f37d77a96a83854710110fadd068d1e2c2c9f"}, - {file = "pandas-1.2.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:a50cf3110a1914442e7b7b9cef394ef6bed0d801b8a34d56f4c4e927bbbcc7d0"}, - {file = "pandas-1.2.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:4d33537a375cfb2db4d388f9a929b6582a364137ea6c6b161b0166440d6ffe36"}, - {file = "pandas-1.2.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:8ac028cd9a6e1efe43f3dc36f708263838283535cc45430a98b9803f44f4c84b"}, - {file = "pandas-1.2.2-cp38-cp38-win32.whl", hash = "sha256:c43d1beb098a1da15934262009a7120aac8dafa20d042b31dab48c28868eb5a4"}, - {file = "pandas-1.2.2-cp38-cp38-win_amd64.whl", hash = "sha256:69a70d79a791fa1fd5f6e84b8b6dec2ec92369bde4ab2e18d43fc8a1825f51d1"}, - {file = "pandas-1.2.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cbad4155028b8ca66aa19a8b13f593ebbf51bfb6c3f2685fe64f04d695a81864"}, - {file = "pandas-1.2.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:fbddbb20f30308ba2546193d64e18c23b69f59d48cdef73676cbed803495c8dc"}, - {file = "pandas-1.2.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:214ae60b1f863844e97c87f758c29940ffad96c666257323a4bb2a33c58719c2"}, - {file = "pandas-1.2.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:26b4919eb3039a686a86cd4f4a74224f8f66e3a419767da26909dcdd3b37c31e"}, - {file = "pandas-1.2.2-cp39-cp39-win32.whl", hash = "sha256:e3c250faaf9979d0ec836d25e420428db37783fa5fed218da49c9fc06f80f51c"}, - {file = "pandas-1.2.2-cp39-cp39-win_amd64.whl", hash = "sha256:e9bbcc7b5c432600797981706f5b54611990c6a86b2e424329c995eea5f9c42b"}, - {file = "pandas-1.2.2.tar.gz", hash = "sha256:14ed84b463e9b84c8ff9308a79b04bf591ae3122a376ee0f62c68a1bd917a773"}, + {file = "pandas-1.2.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4d821b9b911fc1b7d428978d04ace33f0af32bb7549525c8a7b08444bce46b74"}, + {file = "pandas-1.2.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:9f5829e64507ad10e2561b60baf285c470f3c4454b007c860e77849b88865ae7"}, + {file = "pandas-1.2.3-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:97b1954533b2a74c7e20d1342c4f01311d3203b48f2ebf651891e6a6eaf01104"}, + {file = "pandas-1.2.3-cp37-cp37m-win32.whl", hash = "sha256:5e3c8c60541396110586bcbe6eccdc335a38e7de8c217060edaf4722260b158f"}, + {file = "pandas-1.2.3-cp37-cp37m-win_amd64.whl", hash = "sha256:8a051e957c5206f722e83f295f95a2cf053e890f9a1fba0065780a8c2d045f5d"}, + {file = "pandas-1.2.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a93e34f10f67d81de706ce00bf8bb3798403cabce4ccb2de10c61b5ae8786ab5"}, + {file = "pandas-1.2.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:46fc671c542a8392a4f4c13edc8527e3a10f6cb62912d856f82248feb747f06e"}, + {file = "pandas-1.2.3-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:43e00770552595c2250d8d712ec8b6e08ca73089ac823122344f023efa4abea3"}, + {file = "pandas-1.2.3-cp38-cp38-win32.whl", hash = "sha256:475b7772b6e18a93a43ea83517932deff33954a10d4fbae18d0c1aba4182310f"}, + {file = "pandas-1.2.3-cp38-cp38-win_amd64.whl", hash = "sha256:72ffcea00ae8ffcdbdefff800284311e155fbb5ed6758f1a6110fc1f8f8f0c1c"}, + {file = "pandas-1.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:621c044a1b5e535cf7dcb3ab39fca6f867095c3ef223a524f18f60c7fee028ea"}, + {file = "pandas-1.2.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:0f27fd1adfa256388dc34895ca5437eaf254832223812afd817a6f73127f969c"}, + {file = "pandas-1.2.3-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:dbb255975eb94143f2e6ec7dadda671d25147939047839cd6b8a4aff0379bb9b"}, + {file = "pandas-1.2.3-cp39-cp39-win32.whl", hash = "sha256:d59842a5aa89ca03c2099312163ffdd06f56486050e641a45d926a072f04d994"}, + {file = "pandas-1.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:09761bf5f8c741d47d4b8b9073288de1be39bbfccc281d70b889ade12b2aad29"}, + {file = "pandas-1.2.3.tar.gz", hash = "sha256:df6f10b85aef7a5bb25259ad651ad1cc1d6bb09000595cab47e718cbac250b1d"}, ] pandas-datareader = [ {file = "pandas-datareader-0.9.0.tar.gz", hash = "sha256:b2cbc1e16a6ab9ff1ed167ae2ea92839beab9a20823bd00bdfb78155fa04f891"}, @@ -1803,6 +1801,6 @@ yarl = [ {file = "yarl-1.1.0.tar.gz", hash = "sha256:6af895b45bd49254cc309ac0fe6e1595636a024953d710e01114257736184698"}, ] zipp = [ - {file = "zipp-3.4.0-py3-none-any.whl", hash = "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108"}, - {file = "zipp-3.4.0.tar.gz", hash = "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"}, + {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, + {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, ] diff --git a/pyproject.toml b/pyproject.toml index e905cc30244c..0973235d4eda 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "nautilus_trader" -version = "1.106.0" +version = "1.107.0" description = "A high-performance algorithmic trading platform and event-driven backtester" authors = ["Nautech Systems "] license = "LGPL-3.0-or-later" @@ -33,7 +33,7 @@ generate-setup-file = false [tool.poetry.dependencies] python = "^3.7.9" -ccxt = "^1.42.40" +ccxt = "^1.42.59" cython = "^3.0a6" empyrical = "^0.5.5" ib_insync = "^0.9.65" @@ -43,7 +43,7 @@ msgpack = "^1.0.2" numpy = "^1.20.1" numpydoc = { version = "^1.1.0", optional = true } oandapyV20 = "^0.6.3" -pandas = "^1.2.2" +pandas = "^1.2.3" psutil = "^5.8.0" pyarrow = "^3.0.0" pytz = "^2020.5" diff --git a/tests/performance_tests/test_perf_live_execution.py b/tests/performance_tests/test_perf_live_execution.py index 9f3800da198a..630a0cbca80b 100644 --- a/tests/performance_tests/test_perf_live_execution.py +++ b/tests/performance_tests/test_perf_live_execution.py @@ -78,7 +78,7 @@ def setUp(self): exec_client = MockExecutionClient( venue=Venue("BINANCE"), account_id=self.account_id, - exec_engine=self.exec_engine, + engine=self.exec_engine, clock=self.clock, logger=self.logger, ) diff --git a/tests/test_kit/mocks.py b/tests/test_kit/mocks.py index 8954d160928d..74192770e25c 100644 --- a/tests/test_kit/mocks.py +++ b/tests/test_kit/mocks.py @@ -33,7 +33,6 @@ from nautilus_trader.model.identifiers import StrategyId from nautilus_trader.model.identifiers import Symbol from nautilus_trader.model.identifiers import TraderId -from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.order.base import Order from nautilus_trader.model.position import Position from nautilus_trader.trading.account import Account @@ -393,7 +392,7 @@ def __init__( self, venue, account_id, - exec_engine, + engine, clock, logger, ): @@ -406,7 +405,7 @@ def __init__( The venue for the client. account_id : AccountId The account_id for the client. - exec_engine : ExecutionEngine + engine : ExecutionEngine The execution engine for the component. clock : Clock The clock for the component. @@ -417,7 +416,7 @@ def __init__( super().__init__( venue, account_id, - exec_engine, + engine, clock, logger, ) diff --git a/tests/test_kit/performance.py b/tests/test_kit/performance.py index 6cc7e9cdbbb9..dcf148c2b1a5 100644 --- a/tests/test_kit/performance.py +++ b/tests/test_kit/performance.py @@ -44,11 +44,20 @@ def profile_function(function, runs, iterations, print_output=True) -> float: print_output : bool If the output should be printed to the console. + Raises + ------ + ValueError + If runs is not positive (> 1). + ValueError + If iterations is not positive (> 1). + Returns ------- float """ + if runs < 1: + raise ValueError("runs cannot be less than 1") if iterations < 1: raise ValueError("iterations cannot be less than 1") diff --git a/tests/test_kit/providers.py b/tests/test_kit/providers.py index 81dd33e385c4..a373181acc41 100644 --- a/tests/test_kit/providers.py +++ b/tests/test_kit/providers.py @@ -96,6 +96,11 @@ class TestInstrumentProvider: def btcusdt_binance() -> Instrument: """ Return the Binance BTC/USDT instrument for backtesting. + + Returns + ------- + Instrument + """ return Instrument( symbol=Symbol("BTC/USDT", Venue("BINANCE")), @@ -129,6 +134,11 @@ def btcusdt_binance() -> Instrument: def ethusdt_binance() -> Instrument: """ Return the Binance ETH/USDT instrument for backtesting. + + Returns + ------- + Instrument + """ return Instrument( symbol=Symbol("ETH/USDT", Venue("BINANCE")), @@ -162,6 +172,16 @@ def ethusdt_binance() -> Instrument: def xbtusd_bitmex(leverage: Decimal=Decimal("1.0")) -> Instrument: """ Return the BitMEX XBT/USD perpetual contract for backtesting. + + Parameters + ---------- + leverage : Decimal + The margined leverage for the instrument. + + Returns + ------- + Instrument + """ return Instrument( symbol=Symbol("XBT/USD", Venue("BITMEX")), @@ -195,6 +215,16 @@ def xbtusd_bitmex(leverage: Decimal=Decimal("1.0")) -> Instrument: def ethusd_bitmex(leverage: Decimal=Decimal("1.0")) -> Instrument: """ Return the BitMEX ETH/USD perpetual contract for backtesting. + + Parameters + ---------- + leverage : Decimal + The margined leverage for the instrument. + + Returns + ------- + Instrument + """ return Instrument( symbol=Symbol("ETH/USD", Venue("BITMEX")), @@ -233,9 +263,13 @@ def default_fx_ccy(symbol: Symbol, leverage: Decimal=Decimal("50")) -> Instrumen ---------- symbol : Symbol The currency pair symbol. - leverage : Decimal, optional + leverage : Decimal The leverage for the instrument. + Returns + ------- + Instrument + Raises ------ ValueError diff --git a/tests/test_kit/stubs.py b/tests/test_kit/stubs.py index 98e8bd9b58ef..cfaceb22629e 100644 --- a/tests/test_kit/stubs.py +++ b/tests/test_kit/stubs.py @@ -163,32 +163,32 @@ def bar_3decimal() -> Bar: ) @staticmethod - def quote_tick_3decimal(symbol=None) -> QuoteTick: + def quote_tick_3decimal(symbol=None, bid=None, ask=None) -> QuoteTick: return QuoteTick( symbol if symbol is not None else TestStubs.symbol_usdjpy(), - Price("90.002"), - Price("90.003"), + bid if bid is not None else Price("90.002"), + ask if ask is not None else Price("90.005"), Quantity(1), Quantity(1), UNIX_EPOCH, ) @staticmethod - def quote_tick_5decimal(symbol=None) -> QuoteTick: + def quote_tick_5decimal(symbol=None, bid=None, ask=None) -> QuoteTick: return QuoteTick( symbol if symbol is not None else TestStubs.symbol_audusd(), - Price("1.00001"), - Price("1.00003"), + bid if bid is not None else Price("1.00001"), + ask if ask is not None else Price("1.00003"), Quantity(1), Quantity(1), UNIX_EPOCH, ) @staticmethod - def trade_tick_5decimal(symbol=None) -> TradeTick: + def trade_tick_5decimal(symbol=None, price=None) -> TradeTick: return TradeTick( symbol if symbol is not None else TestStubs.symbol_audusd(), - Price("1.00001"), + price if price is not None else Price("1.00001"), Quantity(100000), OrderSide.BUY, TradeMatchId("123456"), diff --git a/tests/unit_tests/backtest/test_backtest_exchange.py b/tests/unit_tests/backtest/test_backtest_exchange.py index d205bdc4882a..b69c8d5d2931 100644 --- a/tests/unit_tests/backtest/test_backtest_exchange.py +++ b/tests/unit_tests/backtest/test_backtest_exchange.py @@ -235,9 +235,12 @@ def test_submit_order_with_no_market_rejects_order(self): self.assertTrue(isinstance(self.strategy.object_storer.get_store()[1], OrderRejected)) def test_submit_order_with_invalid_price_gets_rejected(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.exchange.process_tick(tick) self.portfolio.update_tick(tick) @@ -245,7 +248,7 @@ def test_submit_order_with_invalid_price_gets_rejected(self): USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), - Price("80.000"), + Price("90.005"), # Price at ask ) # Act @@ -255,9 +258,12 @@ def test_submit_order_with_invalid_price_gets_rejected(self): self.assertEqual(OrderState.REJECTED, order.state) def test_submit_market_order(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -273,12 +279,15 @@ def test_submit_market_order(self): # Assert self.assertEqual(OrderState.FILLED, order.state) - self.assertEqual(Decimal("90.003"), order.avg_price) + self.assertEqual(Decimal("90.005"), order.avg_price) # No slippage def test_submit_limit_order(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -286,7 +295,7 @@ def test_submit_limit_order(self): USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), - Price("80.000"), + Price("90.001"), ) # Act @@ -297,9 +306,12 @@ def test_submit_limit_order(self): self.assertIn(order.cl_ord_id, self.exchange.get_working_orders()) def test_submit_stop_market_order(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -307,7 +319,7 @@ def test_submit_stop_market_order(self): USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), - Price("90.100"), + Price("90.010"), ) # Act @@ -318,9 +330,12 @@ def test_submit_stop_market_order(self): self.assertIn(order.cl_ord_id, self.exchange.get_working_orders()) def test_submit_stop_limit_order(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -328,8 +343,8 @@ def test_submit_stop_limit_order(self): USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), - price=Price("90.100"), - trigger=Price("90.150"), + price=Price("90.000"), + trigger=Price("90.010"), ) # Act @@ -340,9 +355,12 @@ def test_submit_stop_limit_order(self): self.assertIn(order.cl_ord_id, self.exchange.get_working_orders()) def test_submit_bracket_market_order(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -353,21 +371,29 @@ def test_submit_bracket_market_order(self): ) bracket_order = self.strategy.order_factory.bracket( - entry_order, - Price("80.000"), - Price("91.000"), + entry_order=entry_order, + stop_loss=Price("89.950"), + take_profit=Price("90.050"), ) # Act self.strategy.submit_bracket_order(bracket_order) # Assert + stop_loss_order = self.exec_engine.cache.order(ClientOrderId("O-19700101-000000-000-001-2")) + take_profit_order = self.exec_engine.cache.order(ClientOrderId("O-19700101-000000-000-001-3")) + self.assertEqual(OrderState.FILLED, entry_order.state) + self.assertEqual(OrderState.ACCEPTED, stop_loss_order.state) + self.assertEqual(OrderState.ACCEPTED, take_profit_order.state) def test_submit_stop_market_order_with_bracket(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -375,26 +401,35 @@ def test_submit_stop_market_order_with_bracket(self): USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), - Price("96.710"), + Price("90.020"), ) bracket_order = self.strategy.order_factory.bracket( - entry_order, - Price("86.000"), - Price("97.000"), + entry_order=entry_order, + stop_loss=Price("90.000"), + take_profit=Price("90.040"), ) # Act self.strategy.submit_bracket_order(bracket_order) # Assert + stop_loss_order = self.exec_engine.cache.order(ClientOrderId("O-19700101-000000-000-001-2")) + take_profit_order = self.exec_engine.cache.order(ClientOrderId("O-19700101-000000-000-001-3")) + + self.assertEqual(OrderState.ACCEPTED, entry_order.state) + self.assertEqual(OrderState.SUBMITTED, stop_loss_order.state) + self.assertEqual(OrderState.SUBMITTED, take_profit_order.state) self.assertEqual(1, len(self.exchange.get_working_orders())) self.assertIn(entry_order.cl_ord_id, self.exchange.get_working_orders()) def test_cancel_stop_order(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -402,7 +437,7 @@ def test_cancel_stop_order(self): USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), - Price("96.711"), + Price("90.010"), ) self.strategy.submit_order(order) @@ -431,7 +466,7 @@ def test_cancel_stop_order_when_order_does_not_exist_generates_cancel_reject(sel # Assert self.assertEqual(2, self.exec_engine.event_count) - def test_modify_stop_order_when_order_does_not_exist(self): + def test_amend_stop_order_when_order_does_not_exist(self): # Arrange command = AmendOrder( venue=SIM, @@ -450,10 +485,67 @@ def test_modify_stop_order_when_order_does_not_exist(self): # Assert self.assertEqual(2, self.exec_engine.event_count) - def test_modify_stop_order(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + def test_amend_post_only_limit_order_when_marketable_then_rejects_amendment(self): + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) + self.data_engine.process(tick) + self.exchange.process_tick(tick) + + order = self.strategy.order_factory.limit( + USDJPY_SIM.symbol, + OrderSide.BUY, + Quantity(100000), + Price("90.001"), + post_only=True, # Default value + ) + + self.strategy.submit_order(order) + + # Act: Amending BUY LIMIT order limit price to ask will become marketable + self.strategy.amend_order(order, order.quantity, Price("90.005")) + + # Assert + self.assertEqual(1, len(self.exchange.get_working_orders())) # Order still working + self.assertEqual(Price("90.001"), order.price) # Did not amend + + def test_amend_limit_order_when_marketable_then_fills_order(self): + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) + self.data_engine.process(tick) + self.exchange.process_tick(tick) + + order = self.strategy.order_factory.limit( + USDJPY_SIM.symbol, + OrderSide.BUY, + Quantity(100000), + Price("90.001"), + post_only=False, # Ensures marketable on amendment + ) + + self.strategy.submit_order(order) + + # Act: Amending BUY LIMIT order limit price to ask will become marketable + self.strategy.amend_order(order, order.quantity, Price("90.005")) + + # Assert + self.assertEqual(0, len(self.exchange.get_working_orders())) + self.assertEqual(Price("90.005"), order.avg_price) + + def test_amend_stop_market_order_when_price_inside_market_then_rejects_amendment(self): + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -461,55 +553,178 @@ def test_modify_stop_order(self): USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), - Price("96.711"), + Price("90.010"), ) self.strategy.submit_order(order) # Act - self.strategy.amend_order(order, order.quantity, Price("96.714")) + self.strategy.amend_order(order, order.quantity, Price("90.005")) # Assert self.assertEqual(1, len(self.exchange.get_working_orders())) - self.assertEqual(Price("96.714"), order.price) + self.assertEqual(Price("90.010"), order.price) - def test_expire_order(self): - # Arrange - # Prepare market - tick1 = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) - self.data_engine.process(tick1) - self.exchange.process_tick(tick1) + def test_amend_stop_market_order_when_price_valid_then_amends(self): + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) + self.data_engine.process(tick) + self.exchange.process_tick(tick) order = self.strategy.order_factory.stop_market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), - Price("96.711"), - time_in_force=TimeInForce.GTD, - expire_time=UNIX_EPOCH + timedelta(minutes=1), + Price("90.010"), ) self.strategy.submit_order(order) - tick2 = QuoteTick( + # Act + self.strategy.amend_order(order, order.quantity, Price("90.011")) + + # Assert + self.assertEqual(1, len(self.exchange.get_working_orders())) + self.assertEqual(Price("90.011"), order.price) + + def test_amend_untriggered_stop_limit_order_when_price_inside_market_then_rejects_amendment(self): + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) + self.data_engine.process(tick) + self.exchange.process_tick(tick) + + order = self.strategy.order_factory.stop_limit( USDJPY_SIM.symbol, - Price("96.709"), - Price("96.710"), + OrderSide.BUY, Quantity(100000), + price=Price("90.000"), + trigger=Price("90.010") + ) + + self.strategy.submit_order(order) + + # Act + self.strategy.amend_order(order, order.quantity, Price("90.005")) + + # Assert + self.assertEqual(1, len(self.exchange.get_working_orders())) + self.assertEqual(Price("90.010"), order.trigger) + + def test_amend_untriggered_stop_limit_order_when_price_valid_then_amends(self): + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) + self.data_engine.process(tick) + self.exchange.process_tick(tick) + + order = self.strategy.order_factory.stop_limit( + USDJPY_SIM.symbol, + OrderSide.BUY, Quantity(100000), - UNIX_EPOCH + timedelta(minutes=1), + price=Price("90.000"), + trigger=Price("90.010") + ) + + self.strategy.submit_order(order) + + # Act + self.strategy.amend_order(order, order.quantity, Price("90.011")) + + # Assert + self.assertEqual(1, len(self.exchange.get_working_orders())) + self.assertEqual(Price("90.011"), order.trigger) + + def test_amend_triggered_stop_limit_order_when_price_inside_market_then_rejects_amendment(self): + # Arrange: Prepare market + tick1 = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) + self.data_engine.process(tick1) + self.exchange.process_tick(tick1) + + order = self.strategy.order_factory.stop_limit( + USDJPY_SIM.symbol, + OrderSide.BUY, + Quantity(100000), + price=Price("90.000"), + trigger=Price("90.010") ) + self.strategy.submit_order(order) + + # Trigger order + tick2 = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.009"), + ask=Price("90.010"), + ) + self.data_engine.process(tick2) + self.exchange.process_tick(tick2) + # Act + self.strategy.amend_order(order, order.quantity, Price("90.010")) + + # Assert + self.assertTrue(order.is_triggered) + self.assertEqual(1, len(self.exchange.get_working_orders())) + self.assertEqual(Price("90.000"), order.price) + + def test_amend_triggered_stop_limit_order_when_price_valid_then_amends(self): + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) + self.data_engine.process(tick) + self.exchange.process_tick(tick) + + order = self.strategy.order_factory.stop_limit( + USDJPY_SIM.symbol, + OrderSide.BUY, + Quantity(100000), + price=Price("90.000"), + trigger=Price("90.010") + ) + + self.strategy.submit_order(order) + + # Trigger order + tick2 = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.009"), + ask=Price("90.010"), + ) + self.data_engine.process(tick2) self.exchange.process_tick(tick2) + # Act + self.strategy.amend_order(order, order.quantity, Price("90.005")) + # Assert - self.assertEqual(0, len(self.exchange.get_working_orders())) + self.assertEqual(1, len(self.exchange.get_working_orders())) + self.assertEqual(Price("90.005"), order.price) - def test_modify_bracket_order_working_stop_loss(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + def test_amend_bracket_orders_working_stop_loss(self): + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -534,9 +749,12 @@ def test_modify_bracket_order_working_stop_loss(self): self.assertEqual(Price("85.100"), bracket_order.stop_loss.price) def test_submit_market_order_with_slippage_fill_model_slips_order(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -559,12 +777,15 @@ def test_submit_market_order_with_slippage_fill_model_slips_order(self): self.strategy.submit_order(order) # Assert - self.assertEqual(Decimal("90.004"), order.avg_price) + self.assertEqual(Decimal("90.006"), order.avg_price) def test_order_fills_gets_commissioned(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -606,10 +827,49 @@ def test_order_fills_gets_commissioned(self): self.assertEqual(Money(90.00, JPY), account_event3.commission) self.assertTrue(Money(999995.00, USD), account.balance()) + def test_expire_order(self): + # Arrange: Prepare market + tick1 = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) + self.data_engine.process(tick1) + self.exchange.process_tick(tick1) + + order = self.strategy.order_factory.stop_market( + USDJPY_SIM.symbol, + OrderSide.BUY, + Quantity(100000), + Price("96.711"), + time_in_force=TimeInForce.GTD, + expire_time=UNIX_EPOCH + timedelta(minutes=1), + ) + + self.strategy.submit_order(order) + + tick2 = QuoteTick( + USDJPY_SIM.symbol, + Price("96.709"), + Price("96.710"), + Quantity(100000), + Quantity(100000), + UNIX_EPOCH + timedelta(minutes=1), + ) + + # Act + self.exchange.process_tick(tick2) + + # Assert + self.assertEqual(0, len(self.exchange.get_working_orders())) + def test_process_quote_tick_fills_buy_stop_order(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -650,9 +910,12 @@ def test_process_quote_tick_fills_buy_stop_order(self): self.assertEqual(Price("96.711"), order.avg_price) def test_process_quote_tick_triggers_buy_stop_limit_order(self): - # Arrange - # Prepare market - tick1 = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick1 = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick1) self.exchange.process_tick(tick1) @@ -683,11 +946,14 @@ def test_process_quote_tick_triggers_buy_stop_limit_order(self): self.assertEqual(OrderState.TRIGGERED, order.state) def test_process_quote_tick_fills_buy_limit_order(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) - self.data_engine.process(tick) - self.exchange.process_tick(tick) + # Arrange: Prepare market + tick1 = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) + self.data_engine.process(tick1) + self.exchange.process_tick(tick1) order = self.strategy.order_factory.limit( USDJPY_SIM.symbol, @@ -710,8 +976,8 @@ def test_process_quote_tick_fills_buy_limit_order(self): tick3 = QuoteTick( USDJPY_SIM.symbol, - Price("89.998"), - Price("89.999"), + Price("90.000"), + Price("90.001"), Quantity(100000), Quantity(100000), UNIX_EPOCH, @@ -726,9 +992,12 @@ def test_process_quote_tick_fills_buy_limit_order(self): self.assertEqual(Price("90.001"), order.avg_price) def test_process_quote_tick_fills_sell_stop_order(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -759,9 +1028,12 @@ def test_process_quote_tick_fills_sell_stop_order(self): self.assertEqual(Price("90.000"), order.avg_price) def test_process_quote_tick_fills_sell_limit_order(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -792,9 +1064,12 @@ def test_process_quote_tick_fills_sell_limit_order(self): self.assertEqual(Price("90.100"), order.avg_price) def test_process_quote_tick_fills_buy_limit_entry_with_bracket(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -830,9 +1105,12 @@ def test_process_quote_tick_fills_buy_limit_entry_with_bracket(self): self.assertIn(bracket.stop_loss, self.exchange.get_working_orders().values()) def test_process_quote_tick_fills_sell_limit_entry_with_bracket(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -869,8 +1147,7 @@ def test_process_quote_tick_fills_sell_limit_entry_with_bracket(self): self.assertIn(bracket.take_profit, self.exchange.get_working_orders().values()) def test_process_trade_tick_fills_buy_limit_entry_bracket(self): - # Arrange - # Prepare market + # Arrange: Prepare market tick1 = TradeTick( AUDUSD_SIM.symbol, Price("1.00000"), @@ -927,9 +1204,12 @@ def test_process_trade_tick_fills_buy_limit_entry_bracket(self): self.assertIn(bracket.take_profit, self.exchange.get_working_orders().values()) def test_filling_oco_sell_cancels_other_order(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -974,9 +1254,12 @@ def test_filling_oco_sell_cancels_other_order(self): self.assertEqual(0, len(self.exchange.get_working_orders())) def test_realized_pnl_contains_commission(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -996,9 +1279,12 @@ def test_realized_pnl_contains_commission(self): self.assertEqual([Money(180.01, JPY)], position.commissions()) def test_unrealized_pnl(self): - # Arrange - # Prepare market - tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) + # Arrange: Prepare market + tick = TestStubs.quote_tick_3decimal( + symbol=USDJPY_SIM.symbol, + bid=Price("90.002"), + ask=Price("90.005"), + ) self.data_engine.process(tick) self.exchange.process_tick(tick) @@ -1036,11 +1322,10 @@ def test_unrealized_pnl(self): # Assert position = self.exec_engine.cache.positions_open()[0] - self.assertEqual(Money(500000.00, JPY), position.unrealized_pnl(Price("100.003"))) + self.assertEqual(Money(499900.00, JPY), position.unrealized_pnl(Price("100.003"))) def test_position_flipped_when_reduce_order_exceeds_original_quantity(self): - # Arrange - # Prepare market + # Arrange: Prepare market open_quote = QuoteTick( USDJPY_SIM.symbol, Price("90.002"), diff --git a/tests/unit_tests/backtest/test_backtest_models.py b/tests/unit_tests/backtest/test_backtest_models.py index 29109eebf8be..8da045d5e95c 100644 --- a/tests/unit_tests/backtest/test_backtest_models.py +++ b/tests/unit_tests/backtest/test_backtest_models.py @@ -27,7 +27,7 @@ def test_instantiate_with_no_random_seed(self): # Act # Assert self.assertFalse(fill_model.is_slipped()) - self.assertFalse(fill_model.is_limit_filled()) + self.assertTrue(fill_model.is_limit_filled()) self.assertTrue(fill_model.is_stop_filled()) def test_instantiate_with_random_seed(self): @@ -37,14 +37,15 @@ def test_instantiate_with_random_seed(self): # Act # Assert self.assertFalse(fill_model.is_slipped()) - self.assertFalse(fill_model.is_limit_filled()) + self.assertTrue(fill_model.is_limit_filled()) self.assertTrue(fill_model.is_stop_filled()) def test_is_stop_filled_with_random_seed(self): # Arrange fill_model = FillModel( prob_fill_at_stop=0.5, - random_seed=42) + random_seed=42, + ) # Act # Assert @@ -54,7 +55,8 @@ def test_is_limit_filled_with_random_seed(self): # Arrange fill_model = FillModel( prob_fill_at_limit=0.5, - random_seed=42) + random_seed=42, + ) # Act # Assert @@ -64,7 +66,8 @@ def test_is_slipped_with_random_seed(self): # Arrange fill_model = FillModel( prob_slippage=0.5, - random_seed=42) + random_seed=42, + ) # Act # Assert diff --git a/tests/unit_tests/data/test_data_messages.py b/tests/unit_tests/data/test_data_messages.py index 1a696aaba836..61de387d8ad7 100644 --- a/tests/unit_tests/data/test_data_messages.py +++ b/tests/unit_tests/data/test_data_messages.py @@ -52,7 +52,15 @@ def test_data_command_str_and_repr(self): # Assert self.assertEqual("Subscribe( {'type': 'newswire'})", str(command)) - self.assertEqual(f"Subscribe(provider=BINANCE, data_type= {{'type': 'newswire'}}, handler={repr(handler)}, id={command_id}, timestamp=1970-01-01 00:00:00+00:00)", repr(command)) + self.assertEqual( + f"Subscribe(" + f"provider=BINANCE, " + f"data_type= {{'type': 'newswire'}}, " + f"handler={repr(handler)}, " + f"id={command_id}, " + f"timestamp=1970-01-01 00:00:00+00:00)", + repr(command), + ) def test_data_request_message_str_and_repr(self): # Arrange @@ -74,8 +82,24 @@ def test_data_request_message_str_and_repr(self): ) # Assert - self.assertEqual("DataRequest( {'Symbol': Symbol('SOMETHING.RANDOM'), 'FromDateTime': None, 'ToDateTime': None, 'Limit': 1000})", str(request)) - self.assertEqual(f"DataRequest(provider=BINANCE, data_type= {{'Symbol': Symbol('SOMETHING.RANDOM'), 'FromDateTime': None, 'ToDateTime': None, 'Limit': 1000}}, callback={repr(handler)}, id={request_id}, timestamp=1970-01-01 00:00:00+00:00)", repr(request)) + self.assertEqual( + "DataRequest(" + " {'Symbol': Symbol('SOMETHING.RANDOM'), " + "'FromDateTime': None, 'ToDateTime': None, 'Limit': 1000})", + str(request), + ) + self.assertEqual( + f"DataRequest(" + f"provider=BINANCE, " + f"data_type= {{'Symbol': Symbol('SOMETHING.RANDOM'), " + f"'FromDateTime': None, " + f"'ToDateTime': None, " + f"'Limit': 1000}}, " + f"callback={repr(handler)}, " + f"id={request_id}, " + f"timestamp=1970-01-01 00:00:00+00:00)", + repr(request), + ) def test_data_response_message_str_and_repr(self): # Arrange @@ -94,4 +118,12 @@ def test_data_response_message_str_and_repr(self): # Assert self.assertEqual("DataResponse( {'Symbol': Symbol('AUD/USD.IDEALPRO')})", str(response)) - self.assertEqual(f"DataResponse(provider=BINANCE, data_type= {{'Symbol': Symbol('AUD/USD.IDEALPRO')}}, correlation_id={correlation_id}, id={response_id}, timestamp=1970-01-01 00:00:00+00:00)", repr(response)) + self.assertEqual( + f"DataResponse(" + f"provider=BINANCE, " + f"data_type= {{'Symbol': Symbol('AUD/USD.IDEALPRO')}}, " + f"correlation_id={correlation_id}, " + f"id={response_id}, " + f"timestamp=1970-01-01 00:00:00+00:00)", + repr(response), + ) diff --git a/tests/unit_tests/indicators/test_hilbert_transform.py b/tests/unit_tests/indicators/test_hilbert_transform.py index 5b27418ea905..f360a77ff798 100644 --- a/tests/unit_tests/indicators/test_hilbert_transform.py +++ b/tests/unit_tests/indicators/test_hilbert_transform.py @@ -22,6 +22,7 @@ AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy(TestStubs.symbol_audusd()) + class HilbertTransformTests(unittest.TestCase): def setUp(self): diff --git a/tests/unit_tests/indicators/test_volatility_ratio.py b/tests/unit_tests/indicators/test_volatility_ratio.py index bf81042a26a8..78a7f441baf9 100644 --- a/tests/unit_tests/indicators/test_volatility_ratio.py +++ b/tests/unit_tests/indicators/test_volatility_ratio.py @@ -22,6 +22,7 @@ AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy(TestStubs.symbol_audusd()) + class VolatilityCompressionRatioTests(unittest.TestCase): def setUp(self): diff --git a/tests/unit_tests/indicators/test_vwap.py b/tests/unit_tests/indicators/test_vwap.py index 79303eaeaec9..c6d135c3eb4e 100644 --- a/tests/unit_tests/indicators/test_vwap.py +++ b/tests/unit_tests/indicators/test_vwap.py @@ -23,6 +23,7 @@ AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy(TestStubs.symbol_audusd()) + class VolumeWeightedAveragePriceTests(unittest.TestCase): def setUp(self): diff --git a/tests/unit_tests/live/test_live_data_engine.py b/tests/unit_tests/live/test_live_data_engine.py index 11debcad5f3a..89503d0edabb 100644 --- a/tests/unit_tests/live/test_live_data_engine.py +++ b/tests/unit_tests/live/test_live_data_engine.py @@ -57,7 +57,7 @@ def setUp(self): self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) - self.data_engine = LiveDataEngine( + self.engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, clock=self.clock, @@ -65,22 +65,22 @@ def setUp(self): ) def tearDown(self): - self.data_engine.dispose() + self.engine.dispose() self.loop.stop() self.loop.close() def test_start_when_loop_not_running_logs(self): # Arrange # Act - self.data_engine.start() + self.engine.start() # Assert self.assertTrue(True) # No exceptions raised - self.data_engine.stop() + self.engine.stop() def test_message_qsize_at_max_blocks_on_put_data_command(self): # Arrange - self.data_engine = LiveDataEngine( + self.engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, clock=self.clock, @@ -97,16 +97,16 @@ def test_message_qsize_at_max_blocks_on_put_data_command(self): ) # Act - self.data_engine.execute(subscribe) - self.data_engine.execute(subscribe) + self.engine.execute(subscribe) + self.engine.execute(subscribe) # Assert - self.assertEqual(1, self.data_engine.message_qsize()) - self.assertEqual(0, self.data_engine.command_count) + self.assertEqual(1, self.engine.message_qsize()) + self.assertEqual(0, self.engine.command_count) def test_message_qsize_at_max_blocks_on_send_request(self): # Arrange - self.data_engine = LiveDataEngine( + self.engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, clock=self.clock, @@ -129,16 +129,16 @@ def test_message_qsize_at_max_blocks_on_send_request(self): ) # Act - self.data_engine.send(request) - self.data_engine.send(request) + self.engine.send(request) + self.engine.send(request) # Assert - self.assertEqual(1, self.data_engine.message_qsize()) - self.assertEqual(0, self.data_engine.command_count) + self.assertEqual(1, self.engine.message_qsize()) + self.assertEqual(0, self.engine.command_count) def test_message_qsize_at_max_blocks_on_receive_response(self): # Arrange - self.data_engine = LiveDataEngine( + self.engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, clock=self.clock, @@ -156,16 +156,16 @@ def test_message_qsize_at_max_blocks_on_receive_response(self): ) # Act - self.data_engine.receive(response) - self.data_engine.receive(response) # Add over max size + self.engine.receive(response) + self.engine.receive(response) # Add over max size # Assert - self.assertEqual(1, self.data_engine.message_qsize()) - self.assertEqual(0, self.data_engine.command_count) + self.assertEqual(1, self.engine.message_qsize()) + self.assertEqual(0, self.engine.command_count) def test_data_qsize_at_max_blocks_on_put_data(self): # Arrange - self.data_engine = LiveDataEngine( + self.engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, clock=self.clock, @@ -174,17 +174,17 @@ def test_data_qsize_at_max_blocks_on_put_data(self): ) # Act - self.data_engine.process("some_data") - self.data_engine.process("some_data") # Add over max size + self.engine.process("some_data") + self.engine.process("some_data") # Add over max size # Assert - self.assertEqual(1, self.data_engine.data_qsize()) - self.assertEqual(0, self.data_engine.data_count) + self.assertEqual(1, self.engine.data_qsize()) + self.assertEqual(0, self.engine.data_count) def test_get_event_loop_returns_expected_loop(self): # Arrange # Act - loop = self.data_engine.get_event_loop() + loop = self.engine.get_event_loop() # Assert self.assertEqual(self.loop, loop) @@ -193,14 +193,14 @@ def test_start(self): async def run_test(): # Arrange # Act - self.data_engine.start() + self.engine.start() await asyncio.sleep(0.1) # Assert - self.assertEqual(ComponentState.RUNNING, self.data_engine.state) + self.assertEqual(ComponentState.RUNNING, self.engine.state) # Tear Down - self.data_engine.stop() + self.engine.stop() self.loop.run_until_complete(run_test()) @@ -208,12 +208,12 @@ def test_kill_when_running_and_no_messages_on_queues(self): async def run_test(): # Arrange # Act - self.data_engine.start() + self.engine.start() await asyncio.sleep(0) - self.data_engine.kill() + self.engine.kill() # Assert - self.assertEqual(ComponentState.STOPPED, self.data_engine.state) + self.assertEqual(ComponentState.STOPPED, self.engine.state) self.loop.run_until_complete(run_test()) @@ -221,17 +221,17 @@ def test_kill_when_not_running_with_messages_on_queue(self): async def run_test(): # Arrange # Act - self.data_engine.kill() + self.engine.kill() # Assert - self.assertEqual(0, self.data_engine.data_qsize()) + self.assertEqual(0, self.engine.data_qsize()) self.loop.run_until_complete(run_test()) def test_execute_command_processes_message(self): async def run_test(): # Arrange - self.data_engine.start() + self.engine.start() subscribe = Subscribe( provider=BINANCE.value, @@ -242,22 +242,22 @@ async def run_test(): ) # Act - self.data_engine.execute(subscribe) + self.engine.execute(subscribe) await asyncio.sleep(0.1) # Assert - self.assertEqual(0, self.data_engine.message_qsize()) - self.assertEqual(1, self.data_engine.command_count) + self.assertEqual(0, self.engine.message_qsize()) + self.assertEqual(1, self.engine.command_count) # Tear Down - self.data_engine.stop() + self.engine.stop() self.loop.run_until_complete(run_test()) def test_send_request_processes_message(self): async def run_test(): # Arrange - self.data_engine.start() + self.engine.start() handler = [] request = DataRequest( @@ -274,22 +274,22 @@ async def run_test(): ) # Act - self.data_engine.send(request) + self.engine.send(request) await asyncio.sleep(0.1) # Assert - self.assertEqual(0, self.data_engine.message_qsize()) - self.assertEqual(1, self.data_engine.request_count) + self.assertEqual(0, self.engine.message_qsize()) + self.assertEqual(1, self.engine.request_count) # Tear Down - self.data_engine.stop() + self.engine.stop() self.loop.run_until_complete(run_test()) def test_receive_response_processes_message(self): async def run_test(): # Arrange - self.data_engine.start() + self.engine.start() response = DataResponse( provider="BINANCE", @@ -301,35 +301,35 @@ async def run_test(): ) # Act - self.data_engine.receive(response) + self.engine.receive(response) await asyncio.sleep(0.1) # Assert - self.assertEqual(0, self.data_engine.message_qsize()) - self.assertEqual(1, self.data_engine.response_count) + self.assertEqual(0, self.engine.message_qsize()) + self.assertEqual(1, self.engine.response_count) # Tear Down - self.data_engine.stop() + self.engine.stop() self.loop.run_until_complete(run_test()) def test_process_data_processes_data(self): async def run_test(): # Arrange - self.data_engine.start() + self.engine.start() # Act tick = TestStubs.trade_tick_5decimal() # Act - self.data_engine.process(tick) + self.engine.process(tick) await asyncio.sleep(0.1) # Assert - self.assertEqual(0, self.data_engine.data_qsize()) - self.assertEqual(1, self.data_engine.data_count) + self.assertEqual(0, self.engine.data_qsize()) + self.assertEqual(1, self.engine.data_count) # Tear Down - self.data_engine.stop() + self.engine.stop() self.loop.run_until_complete(run_test()) diff --git a/tests/unit_tests/live/test_live_execution_engine.py b/tests/unit_tests/live/test_live_execution_engine.py index 70544797723b..9ec4091b373c 100644 --- a/tests/unit_tests/live/test_live_execution_engine.py +++ b/tests/unit_tests/live/test_live_execution_engine.py @@ -79,7 +79,7 @@ def setUp(self): asyncio.set_event_loop(self.loop) self.database = BypassExecutionDatabase(trader_id=self.trader_id, logger=self.logger) - self.exec_engine = LiveExecutionEngine( + self.engine = LiveExecutionEngine( loop=self.loop, database=self.database, portfolio=self.portfolio, @@ -88,33 +88,33 @@ def setUp(self): ) self.venue = Venue("SIM") - self.exec_client = MockExecutionClient( + self.client = MockExecutionClient( self.venue, self.account_id, - self.exec_engine, + self.engine, self.clock, self.logger, ) - self.exec_engine.register_client(self.exec_client) + self.engine.register_client(self.client) def tearDown(self): - self.exec_engine.dispose() + self.engine.dispose() self.loop.stop() self.loop.close() def test_start_when_loop_not_running_logs(self): # Arrange # Act - self.exec_engine.start() + self.engine.start() # Assert self.assertTrue(True) # No exceptions raised - self.exec_engine.stop() + self.engine.stop() def test_message_qsize_at_max_blocks_on_put_command(self): # Arrange - self.exec_engine = LiveExecutionEngine( + self.engine = LiveExecutionEngine( loop=self.loop, database=self.database, portfolio=self.portfolio, @@ -130,7 +130,7 @@ def test_message_qsize_at_max_blocks_on_put_command(self): self.logger, ) - self.exec_engine.register_strategy(strategy) + self.engine.register_strategy(strategy) order = strategy.order_factory.market( AUDUSD_SIM.symbol, @@ -150,16 +150,16 @@ def test_message_qsize_at_max_blocks_on_put_command(self): ) # Act - self.exec_engine.execute(submit_order) - self.exec_engine.execute(submit_order) + self.engine.execute(submit_order) + self.engine.execute(submit_order) # Assert - self.assertEqual(1, self.exec_engine.qsize()) - self.assertEqual(0, self.exec_engine.command_count) + self.assertEqual(1, self.engine.qsize()) + self.assertEqual(0, self.engine.command_count) def test_message_qsize_at_max_blocks_on_put_event(self): # Arrange - self.exec_engine = LiveExecutionEngine( + self.engine = LiveExecutionEngine( loop=self.loop, database=self.database, portfolio=self.portfolio, @@ -175,7 +175,7 @@ def test_message_qsize_at_max_blocks_on_put_event(self): self.logger, ) - self.exec_engine.register_strategy(strategy) + self.engine.register_strategy(strategy) order = strategy.order_factory.market( AUDUSD_SIM.symbol, @@ -197,17 +197,17 @@ def test_message_qsize_at_max_blocks_on_put_event(self): event = TestStubs.event_order_submitted(order) # Act - self.exec_engine.execute(submit_order) - self.exec_engine.process(event) # Add over max size + self.engine.execute(submit_order) + self.engine.process(event) # Add over max size # Assert - self.assertEqual(1, self.exec_engine.qsize()) - self.assertEqual(0, self.exec_engine.command_count) + self.assertEqual(1, self.engine.qsize()) + self.assertEqual(0, self.engine.command_count) def test_get_event_loop_returns_expected_loop(self): # Arrange # Act - loop = self.exec_engine.get_event_loop() + loop = self.engine.get_event_loop() # Assert self.assertEqual(self.loop, loop) @@ -216,14 +216,14 @@ def test_start(self): async def run_test(): # Arrange # Act - self.exec_engine.start() + self.engine.start() await asyncio.sleep(0.1) # Assert - self.assertEqual(ComponentState.RUNNING, self.exec_engine.state) + self.assertEqual(ComponentState.RUNNING, self.engine.state) # Tear Down - self.exec_engine.stop() + self.engine.stop() self.loop.run_until_complete(run_test()) @@ -231,12 +231,12 @@ def test_kill_when_running_and_no_messages_on_queues(self): async def run_test(): # Arrange # Act - self.exec_engine.start() + self.engine.start() await asyncio.sleep(0) - self.exec_engine.kill() + self.engine.kill() # Assert - self.assertEqual(ComponentState.STOPPED, self.exec_engine.state) + self.assertEqual(ComponentState.STOPPED, self.engine.state) self.loop.run_until_complete(run_test()) @@ -244,17 +244,17 @@ def test_kill_when_not_running_with_messages_on_queue(self): async def run_test(): # Arrange # Act - self.exec_engine.kill() + self.engine.kill() # Assert - self.assertEqual(0, self.exec_engine.qsize()) + self.assertEqual(0, self.engine.qsize()) self.loop.run_until_complete(run_test()) def test_execute_command_places_command_on_queue(self): async def run_test(): # Arrange - self.exec_engine.start() + self.engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( @@ -263,7 +263,7 @@ async def run_test(): self.logger, ) - self.exec_engine.register_strategy(strategy) + self.engine.register_strategy(strategy) order = strategy.order_factory.market( AUDUSD_SIM.symbol, @@ -283,22 +283,22 @@ async def run_test(): ) # Act - self.exec_engine.execute(submit_order) + self.engine.execute(submit_order) await asyncio.sleep(0.1) # Assert - self.assertEqual(0, self.exec_engine.qsize()) - self.assertEqual(1, self.exec_engine.command_count) + self.assertEqual(0, self.engine.qsize()) + self.assertEqual(1, self.engine.command_count) # Tear Down - self.exec_engine.stop() + self.engine.stop() self.loop.run_until_complete(run_test()) def test_handle_position_opening_with_position_id_none(self): async def run_test(): # Arrange - self.exec_engine.start() + self.engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( @@ -307,7 +307,7 @@ async def run_test(): self.logger, ) - self.exec_engine.register_strategy(strategy) + self.engine.register_strategy(strategy) order = strategy.order_factory.market( AUDUSD_SIM.symbol, @@ -318,15 +318,15 @@ async def run_test(): event = TestStubs.event_order_submitted(order) # Act - self.exec_engine.process(event) + self.engine.process(event) await asyncio.sleep(0.1) # Assert - self.assertEqual(0, self.exec_engine.qsize()) - self.assertEqual(1, self.exec_engine.event_count) + self.assertEqual(0, self.engine.qsize()) + self.assertEqual(1, self.engine.event_count) # Tear Down - self.exec_engine.stop() + self.engine.stop() self.loop.run_until_complete(run_test()) diff --git a/tests/unit_tests/model/test_model_events.py b/tests/unit_tests/model/test_model_events.py index cfdce896d0db..dd87dfda3946 100644 --- a/tests/unit_tests/model/test_model_events.py +++ b/tests/unit_tests/model/test_model_events.py @@ -36,7 +36,6 @@ from nautilus_trader.model.events import OrderSubmitted from nautilus_trader.model.identifiers import AccountId from nautilus_trader.model.identifiers import ClientOrderId -from nautilus_trader.model.identifiers import Exchange from nautilus_trader.model.identifiers import ExecutionId from nautilus_trader.model.identifiers import OrderId from nautilus_trader.model.identifiers import PositionId diff --git a/tests/unit_tests/risk/__init__.py b/tests/unit_tests/risk/__init__.py new file mode 100644 index 000000000000..86147c8dc6d8 --- /dev/null +++ b/tests/unit_tests/risk/__init__.py @@ -0,0 +1,14 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 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. +# ------------------------------------------------------------------------------------------------- diff --git a/tests/unit_tests/risk/test_risk_module.py b/tests/unit_tests/risk/test_risk_module.py new file mode 100644 index 000000000000..682dc96a8eef --- /dev/null +++ b/tests/unit_tests/risk/test_risk_module.py @@ -0,0 +1,74 @@ +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 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. +# ------------------------------------------------------------------------------------------------- + +# ------------------------------------------------------------------------------------------------- +# Copyright (C) 2015-2021 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 unittest + +from nautilus_trader.common.clock import TestClock +from nautilus_trader.common.factories import OrderFactory +from nautilus_trader.common.logging import TestLogger +from nautilus_trader.common.uuid import UUIDFactory +from nautilus_trader.model.enums import OrderSide +from nautilus_trader.model.identifiers import StrategyId +from nautilus_trader.model.identifiers import TraderId +from nautilus_trader.model.objects import Quantity +from nautilus_trader.risk.module import RiskModule +from tests.test_kit.providers import TestInstrumentProvider +from tests.test_kit.stubs import TestStubs + +AUDUSD_SIM = TestInstrumentProvider.default_fx_ccy(TestStubs.symbol_audusd()) + + +class RiskModuleBaseTests(unittest.TestCase): + + def setUp(self): + # Fixture Setup + self.clock = TestClock() + self.uuid_factory = UUIDFactory() + self.logger = TestLogger(self.clock) + + self.trader_id = TraderId("TESTER", "000") + self.account_id = TestStubs.account_id() + + self.order_factory = OrderFactory( + trader_id=self.trader_id, + strategy_id=StrategyId("S", "001"), + clock=TestClock(), + ) + + self.risk = RiskModule() + + def test_approve_when_not_implemented_raises_exception(self): + order = self.order_factory.market( + AUDUSD_SIM.symbol, + OrderSide.BUY, + Quantity(100000), + ) + + self.assertRaises(NotImplementedError, self.risk.approve, order) diff --git a/tests/unit_tests/trading/test_trading_sizing.py b/tests/unit_tests/risk/test_risk_sizing.py similarity index 98% rename from tests/unit_tests/trading/test_trading_sizing.py rename to tests/unit_tests/risk/test_risk_sizing.py index e8f74d55a974..b2184d95f158 100644 --- a/tests/unit_tests/trading/test_trading_sizing.py +++ b/tests/unit_tests/risk/test_risk_sizing.py @@ -20,8 +20,8 @@ from nautilus_trader.model.objects import Money from nautilus_trader.model.objects import Price from nautilus_trader.model.objects import Quantity -from nautilus_trader.trading.sizing import FixedRiskSizer -from nautilus_trader.trading.sizing import PositionSizer +from nautilus_trader.risk.sizing import FixedRiskSizer +from nautilus_trader.risk.sizing import PositionSizer from tests.test_kit.providers import TestInstrumentProvider from tests.test_kit.stubs import TestStubs diff --git a/tests/unit_tests/trading/test_trading_strategy.py b/tests/unit_tests/trading/test_trading_strategy.py index 00323e7af6c5..72a7e79604be 100644 --- a/tests/unit_tests/trading/test_trading_strategy.py +++ b/tests/unit_tests/trading/test_trading_strategy.py @@ -1757,7 +1757,7 @@ def test_cancel_order(self): USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), - Price("90.005"), + Price("90.006"), ) strategy.submit_order(order) @@ -1815,19 +1815,19 @@ def test_amend_order(self): USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), - Price("90.001"), + Price("90.000"), ) strategy.submit_order(order) # Act - strategy.amend_order(order, Quantity(110000), Price("90.002")) + strategy.amend_order(order, Quantity(110000), Price("90.001")) # Assert self.assertEqual(order, strategy.execution.orders()[0]) self.assertEqual(OrderState.ACCEPTED, strategy.execution.orders()[0].state) self.assertEqual(Quantity(110000), strategy.execution.orders()[0].quantity) - self.assertEqual(Price("90.002"), strategy.execution.orders()[0].price) + self.assertEqual(Price("90.001"), strategy.execution.orders()[0].price) self.assertTrue(strategy.execution.order_exists(order.cl_ord_id)) self.assertTrue(strategy.execution.is_order_working(order.cl_ord_id)) self.assertFalse(strategy.execution.is_order_completed(order.cl_ord_id)) @@ -1848,14 +1848,14 @@ def test_cancel_all_orders(self): USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), - Price("90.004"), + Price("90.007"), ) order2 = strategy.order_factory.stop_market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), - Price("90.005"), + Price("90.006"), ) strategy.submit_order(order1)