diff --git a/nautilus_trader/config/common.py b/nautilus_trader/config/common.py index 667bb82363c3..c2335ab72017 100644 --- a/nautilus_trader/config/common.py +++ b/nautilus_trader/config/common.py @@ -440,6 +440,8 @@ class StrategyConfig(NautilusConfig, kw_only=True, frozen=True): external_order_claims : list[str], optional The external order claim instrument IDs. External orders for matching instrument IDs will be associated with (claimed by) the strategy. + manage_contingencies : bool, default False + If OUO and OCO **open** contingency orders should be managed automatically by the strategy. manage_gtd_expiry : bool, default False If all order GTD time in force expirations should be managed by the strategy. If True then will ensure open orders have their GTD timers re-activated on start. @@ -450,6 +452,7 @@ class StrategyConfig(NautilusConfig, kw_only=True, frozen=True): order_id_tag: str | None = None oms_type: str | None = None external_order_claims: list[str] | None = None + manage_contingencies: bool = False manage_gtd_expiry: bool = False diff --git a/nautilus_trader/execution/emulator.pxd b/nautilus_trader/execution/emulator.pxd index 5bcfd002e42c..a7bed5e25f12 100644 --- a/nautilus_trader/execution/emulator.pxd +++ b/nautilus_trader/execution/emulator.pxd @@ -65,6 +65,7 @@ cdef class OrderEmulator(Actor): cpdef void _check_monitoring(self, StrategyId strategy_id, PositionId position_id) cpdef void _cancel_order(self, Order order) + cpdef void _update_order(self, Order order, Quantity new_quantity) # ------------------------------------------------------------------------------------------------- diff --git a/nautilus_trader/execution/emulator.pyx b/nautilus_trader/execution/emulator.pyx index d350326eba28..31af6284ae06 100644 --- a/nautilus_trader/execution/emulator.pyx +++ b/nautilus_trader/execution/emulator.pyx @@ -74,7 +74,7 @@ from nautilus_trader.model.orders.market cimport MarketOrder from nautilus_trader.msgbus.bus cimport MessageBus -cdef tuple SUPPORTED_TRIGGERS = (TriggerType.DEFAULT, TriggerType.BID_ASK, TriggerType.LAST_TRADE) +cdef set SUPPORTED_TRIGGERS = {TriggerType.DEFAULT, TriggerType.BID_ASK, TriggerType.LAST_TRADE} cdef class OrderEmulator(Actor): @@ -127,6 +127,7 @@ cdef class OrderEmulator(Actor): component_name=type(self).__name__, submit_order_handler=self._handle_submit_order, cancel_order_handler=self._cancel_order, + modify_order_handler=self._update_order, debug=config.debug, ) @@ -598,6 +599,57 @@ cdef class OrderEmulator(Actor): if matching_core is not None: matching_core.delete_order(order) + self.cache.update_order_pending_cancel_local(order) + + # Generate event + cdef uint64_t ts_now = self._clock.timestamp_ns() + cdef OrderCanceled event = OrderCanceled( + trader_id=order.trader_id, + strategy_id=order.strategy_id, + instrument_id=order.instrument_id, + client_order_id=order.client_order_id, + venue_order_id=order.venue_order_id, # Probably None + account_id=order.account_id, # Probably None + event_id=UUID4(), + ts_event=ts_now, + ts_init=ts_now, + ) + self._manager.send_exec_event(event) + + cpdef void _update_order(self, Order order, Quantity new_quantity): + if order is None: + self._log.error( + f"Cannot update order: order for {repr(order.client_order_id)} not found.", + ) + return + + if self.debug: + self._log.info( + f"Updating order {order.client_order_id} quantity to {new_quantity}.", + LogColor.MAGENTA, + ) + + # Generate event + cdef uint64_t ts_now = self._clock.timestamp_ns() + cdef OrderUpdated event = OrderUpdated( + trader_id=order.trader_id, + strategy_id=order.strategy_id, + instrument_id=order.instrument_id, + client_order_id=order.client_order_id, + venue_order_id=None, # Not yet assigned by any venue + account_id=order.account_id, # Probably None + quantity=new_quantity, + price=None, + trigger_price=None, + event_id=UUID4(), + ts_event=ts_now, + ts_init=ts_now, + ) + order.apply(event) + self.cache.update_order(order) + + self._manager.send_risk_event(event) + # ------------------------------------------------------------------------------------------------- cpdef void _trigger_stop_order(self, Order order): diff --git a/nautilus_trader/execution/manager.pxd b/nautilus_trader/execution/manager.pxd index 459c40399818..a4e9731bbb04 100644 --- a/nautilus_trader/execution/manager.pxd +++ b/nautilus_trader/execution/manager.pxd @@ -54,6 +54,7 @@ cdef class OrderManager: cdef dict _submit_order_commands cdef object _submit_order_handler cdef object _cancel_order_handler + cdef object _modify_order_handler cpdef dict get_submit_order_commands(self) cpdef void cache_submit_order_command(self, SubmitOrder command) @@ -63,6 +64,7 @@ cdef class OrderManager: # -- COMMAND HANDLERS ----------------------------------------------------------------------------- cpdef void cancel_order(self, Order order) + cpdef void modify_order_quantity(self, Order order, Quantity new_quantity) cpdef void create_new_submit_order(self, Order order, PositionId position_id=*, ClientId client_id=*) # -- EVENT HANDLERS ------------------------------------------------------------------------------- @@ -75,7 +77,6 @@ cdef class OrderManager: cpdef void handle_order_filled(self, OrderFilled filled) cpdef void handle_contingencies(self, Order order) cpdef void handle_contingencies_update(self, Order order) - cpdef void update_order_quantity(self, Order order, Quantity new_quantity) # -- EGRESS --------------------------------------------------------------------------------------- diff --git a/nautilus_trader/execution/manager.pyx b/nautilus_trader/execution/manager.pyx index b72bb6810bc4..5cd8edee3d52 100644 --- a/nautilus_trader/execution/manager.pyx +++ b/nautilus_trader/execution/manager.pyx @@ -75,6 +75,8 @@ cdef class OrderManager: The handler to call when submitting orders. cancel_order_handler : Callable[[Order], None], optional The handler to call when canceling orders. + modify_order_handler : Callable[[Order], None], optional + The handler to call when modifying orders. debug : bool, default False If debug mode is active (will provide extra debug logging). @@ -95,6 +97,7 @@ cdef class OrderManager: str component_name not None, submit_order_handler: Optional[Callable[[SubmitOrder], None]] = None, cancel_order_handler: Optional[Callable[[Order], None]] = None, + modify_order_handler: Optional[Callable[[Order, Quantity], None]] = None, bint debug = False, ): Condition.valid_string(component_name, "component_name") @@ -107,8 +110,9 @@ cdef class OrderManager: self._cache = cache self.debug = debug - self._submit_order_handler: Callable[[SubmitOrder], None] = submit_order_handler - self._cancel_order_handler: Callable[[Order], None] = cancel_order_handler + self._submit_order_handler = submit_order_handler + self._cancel_order_handler = cancel_order_handler + self._modify_order_handler = modify_order_handler self._submit_order_commands: dict[ClientOrderId, SubmitOrder] = {} @@ -181,8 +185,6 @@ cdef class OrderManager: self._log.warning("Cannot cancel order: already closed.") return - self._cache.update_order_pending_cancel_local(order) - if self.debug: self._log.info(f"Cancelling order {order}.", LogColor.MAGENTA) @@ -191,20 +193,21 @@ cdef class OrderManager: if self._cancel_order_handler is not None: self._cancel_order_handler(order) - # Generate event - cdef uint64_t ts_now = self._clock.timestamp_ns() - cdef OrderCanceled event = OrderCanceled( - trader_id=order.trader_id, - strategy_id=order.strategy_id, - instrument_id=order.instrument_id, - client_order_id=order.client_order_id, - venue_order_id=order.venue_order_id, # Probably None - account_id=order.account_id, # Probably None - event_id=UUID4(), - ts_event=ts_now, - ts_init=ts_now, - ) - self.send_exec_event(event) + cpdef void modify_order_quantity(self, Order order, Quantity new_quantity): + """ + Modify the given `order` with the manager. + + Parameters + ---------- + order : Order + The order to modify. + + """ + Condition.not_none(order, "order") + Condition.not_none(new_quantity, "new_quantity") + + if self._modify_order_handler is not None: + self._modify_order_handler(order, new_quantity) cpdef void create_new_submit_order( self, @@ -366,9 +369,9 @@ cdef class OrderManager: child_order.position_id = position_id if parent_filled_qty._mem.raw != child_order.leaves_qty._mem.raw: - self.update_order_quantity(child_order, parent_filled_qty) + self.modify_order_quantity(child_order, parent_filled_qty) - if child_order.status_c() not in (OrderStatus.INITIALIZED, OrderStatus.EMULATED) or self._submit_order_handler is None: + if not child_order.is_active_local_c() or self._submit_order_handler is None: return # Order does not need to be released if not child_order.client_order_id in self._submit_order_commands: @@ -435,7 +438,7 @@ cdef class OrderManager: if order.is_closed_c() and filled_qty._mem.raw == 0 and (order.exec_spawn_id is None or not is_spawn_active): self.cancel_order(contingent_order) elif filled_qty._mem.raw > 0 and filled_qty._mem.raw != contingent_order.quantity._mem.raw: - self.update_order_quantity(contingent_order, filled_qty) + self.modify_order_quantity(contingent_order, filled_qty) elif order.contingency_type == ContingencyType.OCO: if self.debug: self._log.info(f"Processing OCO contingent order {client_order_id}.", LogColor.MAGENTA) @@ -449,7 +452,7 @@ cdef class OrderManager: elif order.is_closed_c() and (order.exec_spawn_id is None or not is_spawn_active): self.cancel_order(contingent_order) elif leaves_qty._mem.raw != contingent_order.leaves_qty._mem.raw: - self.update_order_quantity(contingent_order, leaves_qty) + self.modify_order_quantity(contingent_order, leaves_qty) cpdef void handle_contingencies_update(self, Order order): Condition.not_none(order, "order") @@ -482,38 +485,10 @@ cdef class OrderManager: if order.contingency_type == ContingencyType.OTO: if quantity._mem.raw != contingent_order.quantity._mem.raw: - self.update_order_quantity(contingent_order, quantity) + self.modify_order_quantity(contingent_order, quantity) elif order.contingency_type == ContingencyType.OUO: if quantity._mem.raw != contingent_order.quantity._mem.raw: - self.update_order_quantity(contingent_order, quantity) - - cpdef void update_order_quantity(self, Order order, Quantity new_quantity): - if self.debug: - self._log.info( - f"Update contingency order {order.client_order_id} quantity to {new_quantity}.", - LogColor.MAGENTA, - ) - - # Generate event - cdef uint64_t ts_now = self._clock.timestamp_ns() - cdef OrderUpdated event = OrderUpdated( - trader_id=order.trader_id, - strategy_id=order.strategy_id, - instrument_id=order.instrument_id, - client_order_id=order.client_order_id, - venue_order_id=None, # Not yet assigned by any venue - account_id=order.account_id, # Probably None - quantity=new_quantity, - price=None, - trigger_price=None, - event_id=UUID4(), - ts_event=ts_now, - ts_init=ts_now, - ) - order.apply(event) - self._cache.update_order(order) - - self.send_risk_event(event) + self.modify_order_quantity(contingent_order, quantity) # -- EGRESS --------------------------------------------------------------------------------------- diff --git a/nautilus_trader/trading/strategy.pxd b/nautilus_trader/trading/strategy.pxd index 9045f8099b7a..f18ad56d5177 100644 --- a/nautilus_trader/trading/strategy.pxd +++ b/nautilus_trader/trading/strategy.pxd @@ -78,8 +78,10 @@ cdef class Strategy(Actor): """The order management system for the strategy.\n\n:returns: `OmsType`""" cdef readonly list external_order_claims """The external order claims instrument IDs for the strategy.\n\n:returns: `list[InstrumentId]`""" + cdef readonly bint manage_contingencies + """If contingency orders should be managed automatically by the strategy.\n\n:returns: `bool`""" cdef readonly bint manage_gtd_expiry - """If all order GTD time in force expirations should be managed by the strategy.\n\n:returns: `bool`""" + """If all order GTD time in force expirations should be managed automatically by the strategy.\n\n:returns: `bool`""" # -- REGISTRATION --------------------------------------------------------------------------------- diff --git a/nautilus_trader/trading/strategy.pyx b/nautilus_trader/trading/strategy.pyx index c7a0153401a0..e8b1ed48e5d0 100644 --- a/nautilus_trader/trading/strategy.pyx +++ b/nautilus_trader/trading/strategy.pyx @@ -151,6 +151,7 @@ cdef class Strategy(Actor): self.config = config self.oms_type = oms_type_from_str(str(config.oms_type).upper()) if config.oms_type else OmsType.UNSPECIFIED self.external_order_claims = self._parse_external_order_claims(config.external_order_claims) + self.manage_contingencies = config.manage_contingencies self.manage_gtd_expiry = config.manage_gtd_expiry # Public components diff --git a/tests/unit_tests/trading/test_strategy.py b/tests/unit_tests/trading/test_strategy.py index 36477ed7eb68..9914ddbd76bb 100644 --- a/tests/unit_tests/trading/test_strategy.py +++ b/tests/unit_tests/trading/test_strategy.py @@ -201,6 +201,7 @@ def test_strategy_to_importable_config_with_no_specific_config(self): "order_id_tag": None, "strategy_id": None, "external_order_claims": None, + "manage_contingencies": False, "manage_gtd_expiry": False, } @@ -210,6 +211,7 @@ def test_strategy_to_importable_config(self): order_id_tag="001", strategy_id="ALPHA-01", external_order_claims=["ETHUSDT-PERP.DYDX"], + manage_contingencies=True, manage_gtd_expiry=True, ) @@ -227,6 +229,7 @@ def test_strategy_to_importable_config(self): "order_id_tag": "001", "strategy_id": "ALPHA-01", "external_order_claims": ["ETHUSDT-PERP.DYDX"], + "manage_contingencies": True, "manage_gtd_expiry": True, }