Skip to content

Commit

Permalink
Add contract activation and expiration handling
Browse files Browse the repository at this point in the history
  • Loading branch information
cjdsellers committed Apr 24, 2024
1 parent 536d647 commit e39591a
Show file tree
Hide file tree
Showing 5 changed files with 264 additions and 3 deletions.
20 changes: 20 additions & 0 deletions nautilus_trader/backtest/matching_engine.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ from nautilus_trader.common.component cimport TestClock
from nautilus_trader.common.component cimport is_logging_initialized
from nautilus_trader.core.correctness cimport Condition
from nautilus_trader.core.data cimport Data
from nautilus_trader.core.datetime cimport format_iso8601
from nautilus_trader.core.datetime cimport unix_nanos_to_dt
from nautilus_trader.core.rust.model cimport AccountType
from nautilus_trader.core.rust.model cimport AggressorSide
from nautilus_trader.core.rust.model cimport BookType
Expand Down Expand Up @@ -81,6 +83,7 @@ from nautilus_trader.model.identifiers cimport StrategyId
from nautilus_trader.model.identifiers cimport TradeId
from nautilus_trader.model.identifiers cimport TraderId
from nautilus_trader.model.identifiers cimport VenueOrderId
from nautilus_trader.model.instruments.base cimport EXPIRING_INSTRUMENT_TYPES
from nautilus_trader.model.instruments.base cimport Instrument
from nautilus_trader.model.instruments.equity cimport Equity
from nautilus_trader.model.objects cimport Money
Expand Down Expand Up @@ -675,6 +678,23 @@ 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:
if now_ns < self.instrument.activation_ns:
self._generate_order_rejected(
order,
f"Contract {self.instrument.id} not yet active, "
f"activation {format_iso8601(unix_nanos_to_dt(self.instrument.activation_ns))}"
)
return
elif now_ns > self.instrument.expiration_ns:
self._generate_order_rejected(
order,
f"Contract {self.instrument.id} has expired, "
f"expiration {format_iso8601(unix_nanos_to_dt(self.instrument.expiration_ns))}"
)
return

cdef:
Order parent
Order contingenct_order
Expand Down
3 changes: 3 additions & 0 deletions nautilus_trader/model/instruments/base.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ from nautilus_trader.model.objects cimport Quantity
from nautilus_trader.model.tick_scheme.base cimport TickScheme


cdef set[InstrumentClass] EXPIRING_INSTRUMENT_TYPES


cdef class Instrument(Data):
cdef TickScheme _tick_scheme

Expand Down
8 changes: 8 additions & 0 deletions nautilus_trader/model/instruments/base.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ from nautilus_trader.model.tick_scheme.base cimport TICK_SCHEMES
from nautilus_trader.model.tick_scheme.base cimport get_tick_scheme


EXPIRING_INSTRUMENT_TYPES = {
InstrumentClass.FUTURE,
InstrumentClass.FUTURE_SPREAD,
InstrumentClass.OPTION,
InstrumentClass.OPTION_SPREAD,
}


cdef class Instrument(Data):
"""
The base class for all instruments.
Expand Down
6 changes: 3 additions & 3 deletions nautilus_trader/model/orders/base.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ from nautilus_trader.model.objects cimport Price
from nautilus_trader.model.objects cimport Quantity


cdef set STOP_ORDER_TYPES
cdef set LIMIT_ORDER_TYPES
cdef set LOCAL_ACTIVE_ORDER_STATUS
cdef set[OrderType] STOP_ORDER_TYPES
cdef set[OrderType] LIMIT_ORDER_TYPES
cdef set[OrderStatus] LOCAL_ACTIVE_ORDER_STATUS


cdef class Order:
Expand Down
230 changes: 230 additions & 0 deletions tests/unit_tests/backtest/test_exchange_glbx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# -------------------------------------------------------------------------------------------------
# Copyright (C) 2015-2024 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 decimal import Decimal

from nautilus_trader.backtest.exchange import SimulatedExchange
from nautilus_trader.backtest.execution_client import BacktestExecClient
from nautilus_trader.backtest.models import FillModel
from nautilus_trader.backtest.models import LatencyModel
from nautilus_trader.backtest.models import MakerTakerFeeModel
from nautilus_trader.common.component import MessageBus
from nautilus_trader.common.component import TestClock
from nautilus_trader.config import ExecEngineConfig
from nautilus_trader.config import RiskEngineConfig
from nautilus_trader.data.engine import DataEngine
from nautilus_trader.execution.engine import ExecutionEngine
from nautilus_trader.model.currencies import USD
from nautilus_trader.model.enums import AccountType
from nautilus_trader.model.enums import OmsType
from nautilus_trader.model.enums import OrderSide
from nautilus_trader.model.enums import OrderStatus
from nautilus_trader.model.identifiers import Venue
from nautilus_trader.model.objects import Money
from nautilus_trader.model.objects import Price
from nautilus_trader.model.objects import Quantity
from nautilus_trader.portfolio.portfolio import Portfolio
from nautilus_trader.risk.engine import RiskEngine
from nautilus_trader.test_kit.providers import TestInstrumentProvider
from nautilus_trader.test_kit.stubs.component import TestComponentStubs
from nautilus_trader.test_kit.stubs.data import TestDataStubs
from nautilus_trader.test_kit.stubs.identifiers import TestIdStubs
from nautilus_trader.trading import Strategy


_ESH4_GLBX = TestInstrumentProvider.es_future(2024, 3)


class TestSimulatedExchangeGlbx:
def setup(self) -> None:
# Fixture Setup
self.clock = TestClock()
self.trader_id = TestIdStubs.trader_id()

self.msgbus = MessageBus(
trader_id=self.trader_id,
clock=self.clock,
)

self.cache = TestComponentStubs.cache()

self.portfolio = Portfolio(
msgbus=self.msgbus,
cache=self.cache,
clock=self.clock,
)

self.data_engine = DataEngine(
msgbus=self.msgbus,
clock=self.clock,
cache=self.cache,
)

self.exec_engine = ExecutionEngine(
msgbus=self.msgbus,
cache=self.cache,
clock=self.clock,
config=ExecEngineConfig(debug=True),
)

self.risk_engine = RiskEngine(
portfolio=self.portfolio,
msgbus=self.msgbus,
cache=self.cache,
clock=self.clock,
config=RiskEngineConfig(debug=True),
)

self.exchange = SimulatedExchange(
venue=Venue("GLBX"),
oms_type=OmsType.HEDGING,
account_type=AccountType.MARGIN,
base_currency=USD,
starting_balances=[Money(1_000_000, USD)],
default_leverage=Decimal(10),
leverages={},
instruments=[_ESH4_GLBX],
modules=[],
fill_model=FillModel(),
fee_model=MakerTakerFeeModel(),
portfolio=self.portfolio,
msgbus=self.msgbus,
cache=self.cache,
clock=self.clock,
latency_model=LatencyModel(0),
)

self.exec_client = BacktestExecClient(
exchange=self.exchange,
msgbus=self.msgbus,
cache=self.cache,
clock=self.clock,
)

# Wire up components
self.exec_engine.register_client(self.exec_client)
self.exchange.register_client(self.exec_client)

self.cache.add_instrument(_ESH4_GLBX)

# Create mock strategy
self.strategy = Strategy()
self.strategy.register(
trader_id=self.trader_id,
portfolio=self.portfolio,
msgbus=self.msgbus,
cache=self.cache,
clock=self.clock,
)

# Start components
self.exchange.reset()
self.data_engine.start()
self.exec_engine.start()
self.strategy.start()

def test_repr(self) -> None:
# Arrange, Act, Assert
assert (
repr(self.exchange)
== "SimulatedExchange(id=GLBX, oms_type=HEDGING, account_type=MARGIN)"
)

def test_process_order_within_expiration_submits(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"),
)

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

# Assert
assert self.clock.timestamp_ns() == 1_630_704_600_000_000_001
assert order.status == OrderStatus.ACCEPTED

def test_process_order_prior_to_activation_rejects(self) -> None:
# Arrange: Prepare market
tick = TestDataStubs.quote_tick(
instrument=_ESH4_GLBX,
bid_price=4010.00,
ask_price=4011.00,
)
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"),
)

# Act
self.strategy.submit_order(order)
self.exchange.process(0)

# Assert
assert order.status == OrderStatus.REJECTED
assert (
order.last_event.reason
== "Contract ESH4.GLBX not yet active, activation 2021-09-03T21:30:00.000Z"
)

def test_process_order_after_expiration_rejects(self) -> None:
# Arrange: Prepare market
one_nano_past_expiration = _ESH4_GLBX.expiration_ns + 1

tick = TestDataStubs.quote_tick(
instrument=_ESH4_GLBX,
bid_price=4010.00,
ask_price=4011.00,
ts_init=one_nano_past_expiration,
)
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"),
)

# Act
self.strategy.submit_order(order)
self.exchange.process(one_nano_past_expiration)

# Assert
assert self.clock.timestamp_ns() == 1_710_513_000_000_000_001
assert order.status == OrderStatus.REJECTED
assert (
order.last_event.reason
== "Contract ESH4.GLBX has expired, expiration 2024-03-15T14:30:00.000Z"
)

0 comments on commit e39591a

Please sign in to comment.