Skip to content

Commit

Permalink
Add contract expiration order and position closing
Browse files Browse the repository at this point in the history
  • Loading branch information
cjdsellers committed Apr 25, 2024
1 parent 2c27426 commit 319342d
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 3 deletions.
1 change: 1 addition & 0 deletions nautilus_trader/backtest/matching_engine.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ cdef class OrderMatchingEngine:
cdef FillModel _fill_model
cdef FeeModel _fee_model
# cdef object _auction_match_algo
cdef bint _instrument_has_expiration
cdef bint _bar_execution
cdef bint _reject_stop_orders
cdef bint _support_gtd_orders
Expand Down
31 changes: 29 additions & 2 deletions nautilus_trader/backtest/matching_engine.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ cdef class OrderMatchingEngine:
self.account_type = account_type
self.market_status = MarketStatus.OPEN

self._instrument_has_expiration = instrument.instrument_class in EXPIRING_INSTRUMENT_TYPES
self._bar_execution = bar_execution
self._reject_stop_orders = reject_stop_orders
self._support_gtd_orders = support_gtd_orders
Expand Down Expand Up @@ -678,8 +679,9 @@ cdef class OrderMatchingEngine:
# Index identifiers
self._account_ids[order.trader_id] = account_id

cdef uint64_t now_ns = self._clock.timestamp_ns()
if self.instrument.instrument_class in EXPIRING_INSTRUMENT_TYPES:
cdef uint64_t
if self._instrument_has_expiration:
now_ns = self._clock.timestamp_ns()
if now_ns < self.instrument.activation_ns:
self._generate_order_rejected(
order,
Expand Down Expand Up @@ -1311,6 +1313,31 @@ cdef class OrderMatchingEngine:
self._target_last = 0
self._has_targets = False

# Instrument expiration
if self._instrument_has_expiration and timestamp_ns >= self.instrument.expiration_ns:
self._log.info(f"{self.instrument.id} reached expiration")

# Cancel all open orders
for order in self.get_open_orders():
self.cancel_order(order)

# Close all open positions
for position in self.cache.positions(None, self.instrument.id):
order = MarketOrder(
trader_id=position.trader_id,
strategy_id=position.strategy_id,
instrument_id=position.instrument_id,
client_order_id=ClientOrderId(str(uuid.uuid4())),
order_side=Order.closing_side_c(position.side),
quantity=position.quantity,
init_id=UUID4(),
ts_init=self._clock.timestamp_ns(),
reduce_only=True,
tags=[f"EXPIRATION_{self.venue}_CLOSE"],
)
self.cache.add_order(order, position_id=position.id)
self.fill_market_order(order)

cpdef list determine_limit_price_and_volume(self, Order order):
"""
Return the projected fills for the given *limit* order filling passively
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def bypass_logging() -> None:
"""
init_logging(
level_stdout=LogLevel.WARNING,
level_stdout=LogLevel.DEBUG,
bypass=True, # Set this to False to see logging in tests
)

Expand Down
59 changes: 59 additions & 0 deletions tests/unit_tests/backtest/test_exchange_glbx.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,62 @@ def test_process_order_after_expiration_rejects(self) -> None:
order.last_event.reason
== "Contract ESH4.GLBX has expired, expiration 2024-03-15T14:30:00.000Z"
)

def test_process_exchange_past_instrument_expiration_cancels_open_order(self) -> None:
# Arrange: Prepare market
one_nano_past_activation = _ESH4_GLBX.activation_ns + 1
tick = TestDataStubs.quote_tick(
instrument=_ESH4_GLBX,
bid_price=4010.00,
ask_price=4011.00,
ts_init=one_nano_past_activation,
)
self.data_engine.process(tick)
self.exchange.process_quote_tick(tick)

order = self.strategy.order_factory.limit(
_ESH4_GLBX.id,
OrderSide.BUY,
Quantity.from_int(10),
Price.from_str("4000.00"),
)

self.strategy.submit_order(order)
self.exchange.process(one_nano_past_activation)

# Act
self.exchange.get_matching_engine(_ESH4_GLBX.id).iterate(_ESH4_GLBX.expiration_ns)

# Assert
assert self.clock.timestamp_ns() == _ESH4_GLBX.expiration_ns == 1_710_513_000_000_000_000
assert order.status == OrderStatus.CANCELED

def test_process_exchange_past_instrument_expiration_closed_open_position(self) -> None:
# Arrange: Prepare market
one_nano_past_activation = _ESH4_GLBX.activation_ns + 1
tick = TestDataStubs.quote_tick(
instrument=_ESH4_GLBX,
bid_price=4010.00,
ask_price=4011.00,
ts_init=one_nano_past_activation,
)
self.data_engine.process(tick)
self.exchange.process_quote_tick(tick)

order = self.strategy.order_factory.market(
_ESH4_GLBX.id,
OrderSide.BUY,
Quantity.from_int(10),
)

self.strategy.submit_order(order)
self.exchange.process(one_nano_past_activation)

# Act
self.exchange.get_matching_engine(_ESH4_GLBX.id).iterate(_ESH4_GLBX.expiration_ns)

# Assert
assert self.clock.timestamp_ns() == _ESH4_GLBX.expiration_ns == 1_710_513_000_000_000_000
assert order.status == OrderStatus.FILLED
position = self.cache.positions()[0]
assert position.is_closed

0 comments on commit 319342d

Please sign in to comment.