Skip to content

Commit 1b6a416

Browse files
committed
Refine exit process for Strategy
1 parent cbd900f commit 1b6a416

File tree

7 files changed

+284
-1
lines changed

7 files changed

+284
-1
lines changed

nautilus_trader/trading/config.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ class StrategyConfig(NautilusConfig, kw_only=True, frozen=True):
6262
If commands should be logged by the strategy.
6363
log_rejected_due_post_only_as_warning : bool, default True
6464
If order rejected events where `due_post_only` is True should be logged as warnings.
65+
inflight_check_interval_ms : int, default 100
66+
The interval in milliseconds to check for in-flight orders and open positions
67+
during a market exit.
6568
6669
"""
6770

@@ -76,6 +79,7 @@ class StrategyConfig(NautilusConfig, kw_only=True, frozen=True):
7679
log_events: bool = True
7780
log_commands: bool = True
7881
log_rejected_due_post_only_as_warning: bool = True
82+
inflight_check_interval_ms: int = 100
7983

8084

8185
class ImportableStrategyConfig(NautilusConfig, frozen=True):

nautilus_trader/trading/controller.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from nautilus_trader.trading.config import StrategyFactory
3030
from nautilus_trader.trading.messages import CreateActor
3131
from nautilus_trader.trading.messages import CreateStrategy
32+
from nautilus_trader.trading.messages import MarketExitStrategy
3233
from nautilus_trader.trading.messages import RemoveActor
3334
from nautilus_trader.trading.messages import RemoveStrategy
3435
from nautilus_trader.trading.messages import StartActor
@@ -93,6 +94,8 @@ def execute(self, command: Command) -> None:
9394
self.start_strategy_from_id(command.strategy_id)
9495
elif isinstance(command, StopStrategy):
9596
self.stop_strategy_from_id(command.strategy_id)
97+
elif isinstance(command, MarketExitStrategy):
98+
self.market_exit_strategy_from_id(command.strategy_id)
9699
elif isinstance(command, RemoveStrategy):
97100
self.remove_strategy_from_id(command.strategy_id)
98101

@@ -210,6 +213,25 @@ def stop_strategy(self, strategy: Strategy) -> None:
210213
"""
211214
self._trader.stop_strategy(strategy.id)
212215

216+
def market_exit_strategy(self, strategy: Strategy) -> None:
217+
"""
218+
Market exit the given `strategy`.
219+
220+
Will log a warning if the strategy is not ``RUNNING``.
221+
222+
Parameters
223+
----------
224+
strategy : Strategy
225+
The strategy to market exit.
226+
227+
Raises
228+
------
229+
ValueError
230+
If `strategy` is not already registered with the trader.
231+
232+
"""
233+
self._trader.market_exit_strategy(strategy.id)
234+
213235
def remove_actor(self, actor: Actor) -> None:
214236
"""
215237
Remove the given `actor`.
@@ -383,6 +405,25 @@ def stop_strategy_from_id(self, strategy_id: StrategyId) -> None:
383405
"""
384406
self._trader.stop_strategy(strategy_id)
385407

408+
def market_exit_strategy_from_id(self, strategy_id: StrategyId) -> None:
409+
"""
410+
Market exit the strategy corresponding to `strategy_id`.
411+
412+
Will log a warning if the strategy is not ``RUNNING``.
413+
414+
Parameters
415+
----------
416+
strategy_id : StrategyId
417+
The ID of the strategy to market exit.
418+
419+
Raises
420+
------
421+
ValueError
422+
If `strategy` is not already registered with the trader.
423+
424+
"""
425+
self._trader.market_exit_strategy(strategy_id)
426+
386427
def remove_actor_from_id(self, actor_id: ComponentId) -> None:
387428
"""
388429
Remove the actor corresponding to `actor_id`.

nautilus_trader/trading/messages.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,3 +235,29 @@ def __init__(
235235
super().__init__(command_id or UUID4(), ts_init)
236236

237237
self.strategy_id = strategy_id
238+
239+
240+
class MarketExitStrategy(Command):
241+
"""
242+
Represents a command to exit the market for a strategy.
243+
244+
Parameters
245+
----------
246+
strategy_id : StrategyId
247+
The ID of the strategy to exit the market for.
248+
command_id : UUID4
249+
The command ID.
250+
ts_init : int
251+
UNIX timestamp (nanoseconds) when the object was initialized.
252+
253+
"""
254+
255+
def __init__(
256+
self,
257+
strategy_id: StrategyId,
258+
command_id: UUID4 | None = None,
259+
ts_init: int = 0,
260+
) -> None:
261+
super().__init__(command_id or UUID4(), ts_init)
262+
263+
self.strategy_id = strategy_id

nautilus_trader/trading/strategy.pxd

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ cdef class Strategy(Actor):
6767
cdef bint _log_events
6868
cdef bint _log_commands
6969
cdef bint _log_rejected_due_post_only_as_warning
70+
cdef bint _is_exiting
7071

7172
cdef readonly OrderFactory order_factory
7273
"""The order factory for the strategy.\n\n:returns: `OrderFactory`"""
@@ -97,6 +98,9 @@ cdef class Strategy(Actor):
9798
)
9899
cpdef void change_id(self, StrategyId strategy_id)
99100
cpdef void change_order_id_tag(self, str order_id_tag)
101+
cpdef void on_market_exit(self)
102+
cpdef void after_market_exit(self)
103+
cpdef void market_exit(self)
100104

101105
# -- ABSTRACT METHODS -----------------------------------------------------------------------------
102106

@@ -170,6 +174,7 @@ cdef class Strategy(Actor):
170174
cdef str _get_gtd_expiry_timer_name(self, ClientOrderId client_order_id)
171175
cdef void _set_gtd_expiry(self, Order order)
172176
cpdef void _expire_gtd_order(self, TimeEvent event)
177+
cpdef void _check_market_exit(self, TimeEvent event)
173178

174179
# -- EVENTS ---------------------------------------------------------------------------------------
175180

nautilus_trader/trading/strategy.pyx

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,18 @@ attempts to operate without a managing `Trader` instance.
2525
2626
"""
2727

28+
import pandas as pd
29+
2830
from nautilus_trader.trading.config import ImportableStrategyConfig
2931
from nautilus_trader.trading.config import StrategyConfig
32+
from nautilus_trader.trading.messages import MarketExitStrategy
3033

3134
from libc.stdint cimport uint64_t
3235

3336
from nautilus_trader.cache.base cimport CacheFacade
3437
from nautilus_trader.cache.cache cimport Cache
3538
from nautilus_trader.common.actor cimport Actor
39+
from nautilus_trader.common.component cimport CMD
3640
from nautilus_trader.common.component cimport EVT
3741
from nautilus_trader.common.component cimport RECV
3842
from nautilus_trader.common.component cimport Clock
@@ -163,6 +167,7 @@ cdef class Strategy(Actor):
163167
self.external_order_claims = self._parse_external_order_claims(config.external_order_claims)
164168
self.manage_contingent_orders = config.manage_contingent_orders
165169
self.manage_gtd_expiry = config.manage_gtd_expiry
170+
self._is_exiting = False
166171

167172
# Public components
168173
self.clock = self._clock
@@ -243,6 +248,29 @@ cdef class Strategy(Actor):
243248
"occur here, such as resetting indicators and other state"
244249
)
245250

251+
cpdef void on_market_exit(self):
252+
"""
253+
Actions to be performed when a market exit has been initiated.
254+
255+
Warnings
256+
--------
257+
Override this method in a subclass to implement custom market exit logic.
258+
259+
"""
260+
# Optionally override in subclass
261+
262+
cpdef void after_market_exit(self):
263+
"""
264+
Actions to be performed after a market exit has been completed.
265+
266+
Warnings
267+
--------
268+
Override this method in a subclass to implement custom logic after
269+
market exit.
270+
271+
"""
272+
# Optionally override in subclass
273+
246274
# -- REGISTRATION ---------------------------------------------------------------------------------
247275

248276
cpdef void register(
@@ -1664,7 +1692,77 @@ cdef class Strategy(Actor):
16641692
self._log.info(f"Expiring GTD order {order.client_order_id}", LogColor.BLUE)
16651693
self.cancel_order(order)
16661694

1667-
# -- HANDLERS -------------------------------------------------------------------------------------
1695+
cpdef void market_exit(self):
1696+
"""
1697+
Initiate an iterative market exit for the strategy.
1698+
1699+
Will cancel all open orders and close all open positions, and wait for
1700+
all in-flight orders to resolve and positions to close before stopping
1701+
the strategy.
1702+
"""
1703+
if self._is_exiting:
1704+
return
1705+
1706+
self._is_exiting = True
1707+
1708+
self._log.info("Initiating market exit...", LogColor.BLUE)
1709+
self.on_market_exit()
1710+
1711+
# Get all instruments the strategy has open orders or positions for
1712+
cdef list open_orders = self.cache.orders_open(None, None, self.id)
1713+
cdef list open_positions = self.cache.positions_open(None, None, self.id)
1714+
1715+
cdef set instruments = set()
1716+
cdef Order order
1717+
for order in open_orders:
1718+
instruments.add(order.instrument_id)
1719+
1720+
cdef Position position
1721+
for position in open_positions:
1722+
instruments.add(position.instrument_id)
1723+
1724+
cdef InstrumentId instrument_id
1725+
for instrument_id in instruments:
1726+
self.cancel_all_orders(instrument_id)
1727+
self.close_all_positions(instrument_id)
1728+
1729+
# Start iterative check
1730+
self._log.warning(f"Setting market exit timer for {self.id}")
1731+
self._clock.set_timer(
1732+
f"MARKET-EXIT-CHECK:{self.id}",
1733+
pd.Timedelta(milliseconds=self.config.inflight_check_interval_ms),
1734+
None,
1735+
None,
1736+
self._check_market_exit,
1737+
True,
1738+
False,
1739+
)
1740+
1741+
cpdef void _check_market_exit(self, TimeEvent event):
1742+
if self.state != ComponentState.RUNNING:
1743+
return
1744+
1745+
self._log.warning(f"Timer triggered: {event.name}")
1746+
cdef list open_orders = self.cache.orders_open(None, None, self.id)
1747+
cdef list inflight_orders = self.cache.orders_inflight(None, None, self.id)
1748+
1749+
if open_orders or inflight_orders:
1750+
return
1751+
1752+
cdef list open_positions = self.cache.positions_open(None, None, self.id)
1753+
if open_positions:
1754+
# If there are open positions but no orders, we should re-send close orders
1755+
for position in open_positions:
1756+
self.close_position(position)
1757+
1758+
return
1759+
1760+
# All clear
1761+
if f"MARKET-EXIT-CHECK:{self.id}" in self._clock.timer_names:
1762+
self._clock.cancel_timer(name=f"MARKET-EXIT-CHECK:{self.id}")
1763+
1764+
self.after_market_exit()
1765+
self.stop()
16681766

16691767
cpdef void handle_event(self, Event event):
16701768
"""

nautilus_trader/trading/trader.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,34 @@ def stop_strategy(self, strategy_id: StrategyId) -> None:
635635

636636
strategy.stop()
637637

638+
def market_exit_strategy(self, strategy_id: StrategyId) -> None:
639+
"""
640+
Market exit the strategy with the given `strategy_id`.
641+
642+
Parameters
643+
----------
644+
strategy_id : StrategyId
645+
The strategy ID to market exit.
646+
647+
Raises
648+
------
649+
ValueError
650+
If a strategy with the given `strategy_id` is not found.
651+
652+
"""
653+
PyCondition.not_none(strategy_id, "strategy_id")
654+
655+
strategy = self._strategies.get(strategy_id)
656+
657+
if strategy is None:
658+
raise ValueError(f"Cannot market exit strategy, {strategy_id} not found.")
659+
660+
if not strategy.is_running:
661+
self._log.warning(f"Strategy {strategy_id} not running")
662+
return
663+
664+
strategy.market_exit()
665+
638666
def remove_actor(self, actor_id: ComponentId) -> None:
639667
"""
640668
Remove the actor with the given `actor_id`.

0 commit comments

Comments
 (0)