Skip to content

Commit e39591a

Browse files
committed
Add contract activation and expiration handling
1 parent 536d647 commit e39591a

File tree

5 files changed

+264
-3
lines changed

5 files changed

+264
-3
lines changed

nautilus_trader/backtest/matching_engine.pyx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ from nautilus_trader.common.component cimport TestClock
3131
from nautilus_trader.common.component cimport is_logging_initialized
3232
from nautilus_trader.core.correctness cimport Condition
3333
from nautilus_trader.core.data cimport Data
34+
from nautilus_trader.core.datetime cimport format_iso8601
35+
from nautilus_trader.core.datetime cimport unix_nanos_to_dt
3436
from nautilus_trader.core.rust.model cimport AccountType
3537
from nautilus_trader.core.rust.model cimport AggressorSide
3638
from nautilus_trader.core.rust.model cimport BookType
@@ -81,6 +83,7 @@ from nautilus_trader.model.identifiers cimport StrategyId
8183
from nautilus_trader.model.identifiers cimport TradeId
8284
from nautilus_trader.model.identifiers cimport TraderId
8385
from nautilus_trader.model.identifiers cimport VenueOrderId
86+
from nautilus_trader.model.instruments.base cimport EXPIRING_INSTRUMENT_TYPES
8487
from nautilus_trader.model.instruments.base cimport Instrument
8588
from nautilus_trader.model.instruments.equity cimport Equity
8689
from nautilus_trader.model.objects cimport Money
@@ -675,6 +678,23 @@ cdef class OrderMatchingEngine:
675678
# Index identifiers
676679
self._account_ids[order.trader_id] = account_id
677680

681+
cdef uint64_t now_ns = self._clock.timestamp_ns()
682+
if self.instrument.instrument_class in EXPIRING_INSTRUMENT_TYPES:
683+
if now_ns < self.instrument.activation_ns:
684+
self._generate_order_rejected(
685+
order,
686+
f"Contract {self.instrument.id} not yet active, "
687+
f"activation {format_iso8601(unix_nanos_to_dt(self.instrument.activation_ns))}"
688+
)
689+
return
690+
elif now_ns > self.instrument.expiration_ns:
691+
self._generate_order_rejected(
692+
order,
693+
f"Contract {self.instrument.id} has expired, "
694+
f"expiration {format_iso8601(unix_nanos_to_dt(self.instrument.expiration_ns))}"
695+
)
696+
return
697+
678698
cdef:
679699
Order parent
680700
Order contingenct_order

nautilus_trader/model/instruments/base.pxd

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ from nautilus_trader.model.objects cimport Quantity
2727
from nautilus_trader.model.tick_scheme.base cimport TickScheme
2828

2929

30+
cdef set[InstrumentClass] EXPIRING_INSTRUMENT_TYPES
31+
32+
3033
cdef class Instrument(Data):
3134
cdef TickScheme _tick_scheme
3235

nautilus_trader/model/instruments/base.pyx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ from nautilus_trader.model.tick_scheme.base cimport TICK_SCHEMES
3737
from nautilus_trader.model.tick_scheme.base cimport get_tick_scheme
3838

3939

40+
EXPIRING_INSTRUMENT_TYPES = {
41+
InstrumentClass.FUTURE,
42+
InstrumentClass.FUTURE_SPREAD,
43+
InstrumentClass.OPTION,
44+
InstrumentClass.OPTION_SPREAD,
45+
}
46+
47+
4048
cdef class Instrument(Data):
4149
"""
4250
The base class for all instruments.

nautilus_trader/model/orders/base.pxd

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ from nautilus_trader.model.objects cimport Price
5050
from nautilus_trader.model.objects cimport Quantity
5151

5252

53-
cdef set STOP_ORDER_TYPES
54-
cdef set LIMIT_ORDER_TYPES
55-
cdef set LOCAL_ACTIVE_ORDER_STATUS
53+
cdef set[OrderType] STOP_ORDER_TYPES
54+
cdef set[OrderType] LIMIT_ORDER_TYPES
55+
cdef set[OrderStatus] LOCAL_ACTIVE_ORDER_STATUS
5656

5757

5858
cdef class Order:
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
# -------------------------------------------------------------------------------------------------
2+
# Copyright (C) 2015-2024 Nautech Systems Pty Ltd. All rights reserved.
3+
# https://nautechsystems.io
4+
#
5+
# Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6+
# You may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
# -------------------------------------------------------------------------------------------------
15+
16+
from decimal import Decimal
17+
18+
from nautilus_trader.backtest.exchange import SimulatedExchange
19+
from nautilus_trader.backtest.execution_client import BacktestExecClient
20+
from nautilus_trader.backtest.models import FillModel
21+
from nautilus_trader.backtest.models import LatencyModel
22+
from nautilus_trader.backtest.models import MakerTakerFeeModel
23+
from nautilus_trader.common.component import MessageBus
24+
from nautilus_trader.common.component import TestClock
25+
from nautilus_trader.config import ExecEngineConfig
26+
from nautilus_trader.config import RiskEngineConfig
27+
from nautilus_trader.data.engine import DataEngine
28+
from nautilus_trader.execution.engine import ExecutionEngine
29+
from nautilus_trader.model.currencies import USD
30+
from nautilus_trader.model.enums import AccountType
31+
from nautilus_trader.model.enums import OmsType
32+
from nautilus_trader.model.enums import OrderSide
33+
from nautilus_trader.model.enums import OrderStatus
34+
from nautilus_trader.model.identifiers import Venue
35+
from nautilus_trader.model.objects import Money
36+
from nautilus_trader.model.objects import Price
37+
from nautilus_trader.model.objects import Quantity
38+
from nautilus_trader.portfolio.portfolio import Portfolio
39+
from nautilus_trader.risk.engine import RiskEngine
40+
from nautilus_trader.test_kit.providers import TestInstrumentProvider
41+
from nautilus_trader.test_kit.stubs.component import TestComponentStubs
42+
from nautilus_trader.test_kit.stubs.data import TestDataStubs
43+
from nautilus_trader.test_kit.stubs.identifiers import TestIdStubs
44+
from nautilus_trader.trading import Strategy
45+
46+
47+
_ESH4_GLBX = TestInstrumentProvider.es_future(2024, 3)
48+
49+
50+
class TestSimulatedExchangeGlbx:
51+
def setup(self) -> None:
52+
# Fixture Setup
53+
self.clock = TestClock()
54+
self.trader_id = TestIdStubs.trader_id()
55+
56+
self.msgbus = MessageBus(
57+
trader_id=self.trader_id,
58+
clock=self.clock,
59+
)
60+
61+
self.cache = TestComponentStubs.cache()
62+
63+
self.portfolio = Portfolio(
64+
msgbus=self.msgbus,
65+
cache=self.cache,
66+
clock=self.clock,
67+
)
68+
69+
self.data_engine = DataEngine(
70+
msgbus=self.msgbus,
71+
clock=self.clock,
72+
cache=self.cache,
73+
)
74+
75+
self.exec_engine = ExecutionEngine(
76+
msgbus=self.msgbus,
77+
cache=self.cache,
78+
clock=self.clock,
79+
config=ExecEngineConfig(debug=True),
80+
)
81+
82+
self.risk_engine = RiskEngine(
83+
portfolio=self.portfolio,
84+
msgbus=self.msgbus,
85+
cache=self.cache,
86+
clock=self.clock,
87+
config=RiskEngineConfig(debug=True),
88+
)
89+
90+
self.exchange = SimulatedExchange(
91+
venue=Venue("GLBX"),
92+
oms_type=OmsType.HEDGING,
93+
account_type=AccountType.MARGIN,
94+
base_currency=USD,
95+
starting_balances=[Money(1_000_000, USD)],
96+
default_leverage=Decimal(10),
97+
leverages={},
98+
instruments=[_ESH4_GLBX],
99+
modules=[],
100+
fill_model=FillModel(),
101+
fee_model=MakerTakerFeeModel(),
102+
portfolio=self.portfolio,
103+
msgbus=self.msgbus,
104+
cache=self.cache,
105+
clock=self.clock,
106+
latency_model=LatencyModel(0),
107+
)
108+
109+
self.exec_client = BacktestExecClient(
110+
exchange=self.exchange,
111+
msgbus=self.msgbus,
112+
cache=self.cache,
113+
clock=self.clock,
114+
)
115+
116+
# Wire up components
117+
self.exec_engine.register_client(self.exec_client)
118+
self.exchange.register_client(self.exec_client)
119+
120+
self.cache.add_instrument(_ESH4_GLBX)
121+
122+
# Create mock strategy
123+
self.strategy = Strategy()
124+
self.strategy.register(
125+
trader_id=self.trader_id,
126+
portfolio=self.portfolio,
127+
msgbus=self.msgbus,
128+
cache=self.cache,
129+
clock=self.clock,
130+
)
131+
132+
# Start components
133+
self.exchange.reset()
134+
self.data_engine.start()
135+
self.exec_engine.start()
136+
self.strategy.start()
137+
138+
def test_repr(self) -> None:
139+
# Arrange, Act, Assert
140+
assert (
141+
repr(self.exchange)
142+
== "SimulatedExchange(id=GLBX, oms_type=HEDGING, account_type=MARGIN)"
143+
)
144+
145+
def test_process_order_within_expiration_submits(self) -> None:
146+
# Arrange: Prepare market
147+
one_nano_past_activation = _ESH4_GLBX.activation_ns + 1
148+
tick = TestDataStubs.quote_tick(
149+
instrument=_ESH4_GLBX,
150+
bid_price=4010.00,
151+
ask_price=4011.00,
152+
ts_init=one_nano_past_activation,
153+
)
154+
self.data_engine.process(tick)
155+
self.exchange.process_quote_tick(tick)
156+
157+
order = self.strategy.order_factory.limit(
158+
_ESH4_GLBX.id,
159+
OrderSide.BUY,
160+
Quantity.from_int(10),
161+
Price.from_str("4000.00"),
162+
)
163+
164+
# Act
165+
self.strategy.submit_order(order)
166+
self.exchange.process(one_nano_past_activation)
167+
168+
# Assert
169+
assert self.clock.timestamp_ns() == 1_630_704_600_000_000_001
170+
assert order.status == OrderStatus.ACCEPTED
171+
172+
def test_process_order_prior_to_activation_rejects(self) -> None:
173+
# Arrange: Prepare market
174+
tick = TestDataStubs.quote_tick(
175+
instrument=_ESH4_GLBX,
176+
bid_price=4010.00,
177+
ask_price=4011.00,
178+
)
179+
self.data_engine.process(tick)
180+
self.exchange.process_quote_tick(tick)
181+
182+
order = self.strategy.order_factory.limit(
183+
_ESH4_GLBX.id,
184+
OrderSide.BUY,
185+
Quantity.from_int(10),
186+
Price.from_str("4000.00"),
187+
)
188+
189+
# Act
190+
self.strategy.submit_order(order)
191+
self.exchange.process(0)
192+
193+
# Assert
194+
assert order.status == OrderStatus.REJECTED
195+
assert (
196+
order.last_event.reason
197+
== "Contract ESH4.GLBX not yet active, activation 2021-09-03T21:30:00.000Z"
198+
)
199+
200+
def test_process_order_after_expiration_rejects(self) -> None:
201+
# Arrange: Prepare market
202+
one_nano_past_expiration = _ESH4_GLBX.expiration_ns + 1
203+
204+
tick = TestDataStubs.quote_tick(
205+
instrument=_ESH4_GLBX,
206+
bid_price=4010.00,
207+
ask_price=4011.00,
208+
ts_init=one_nano_past_expiration,
209+
)
210+
self.data_engine.process(tick)
211+
self.exchange.process_quote_tick(tick)
212+
213+
order = self.strategy.order_factory.limit(
214+
_ESH4_GLBX.id,
215+
OrderSide.BUY,
216+
Quantity.from_int(10),
217+
Price.from_str("4000.00"),
218+
)
219+
220+
# Act
221+
self.strategy.submit_order(order)
222+
self.exchange.process(one_nano_past_expiration)
223+
224+
# Assert
225+
assert self.clock.timestamp_ns() == 1_710_513_000_000_000_001
226+
assert order.status == OrderStatus.REJECTED
227+
assert (
228+
order.last_event.reason
229+
== "Contract ESH4.GLBX has expired, expiration 2024-03-15T14:30:00.000Z"
230+
)

0 commit comments

Comments
 (0)