class TestLiveExecutionEngine: def setup(self): # Fixture Setup self.loop = asyncio.get_event_loop() self.loop.set_debug(True) self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.logger = Logger(self.clock) self.trader_id = TestStubs.trader_id() self.order_factory = OrderFactory( trader_id=self.trader_id, strategy_id=StrategyId("S-001"), clock=self.clock, ) self.random_order_factory = OrderFactory( trader_id=TraderId("RANDOM-042"), strategy_id=StrategyId("S-042"), clock=self.clock, ) self.msgbus = MessageBus( trader_id=self.trader_id, clock=self.clock, logger=self.logger, ) self.cache = TestStubs.cache() self.portfolio = Portfolio( msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.data_engine = LiveDataEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.risk_engine = LiveRiskEngine( loop=self.loop, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.instrument_provider = InstrumentProvider() self.instrument_provider.add(AUDUSD_SIM) self.instrument_provider.add(GBPUSD_SIM) self.client = MockLiveExecutionClient( loop=self.loop, client_id=ClientId(SIM.value), venue_type=VenueType.ECN, account_id=TestStubs.account_id(), account_type=AccountType.CASH, base_currency=USD, instrument_provider=self.instrument_provider, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.portfolio.update_account(TestStubs.event_cash_account_state()) self.exec_engine.register_client(self.client) self.cache.add_instrument(AUDUSD_SIM) def teardown(self): self.exec_engine.dispose() @pytest.mark.asyncio async def test_start_when_loop_not_running_logs(self): # Arrange, Act self.exec_engine.start() # Assert assert True # No exceptions raised self.exec_engine.stop() @pytest.mark.asyncio async def test_message_qsize_at_max_blocks_on_put_command(self): # Arrange # Deregister test fixture ExecutionEngine from msgbus) self.msgbus.deregister(endpoint="ExecEngine.execute", handler=self.exec_engine.execute) self.msgbus.deregister(endpoint="ExecEngine.process", handler=self.exec_engine.process) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, config=LiveExecEngineConfig(qsize=1), ) strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.exec_engine.execute(submit_order) self.exec_engine.execute(submit_order) await asyncio.sleep(0.1) # Assert assert self.exec_engine.qsize() == 1 assert self.exec_engine.command_count == 0 @pytest.mark.asyncio async def test_message_qsize_at_max_blocks_on_put_event(self): # Arrange # Deregister test fixture ExecutionEngine from msgbus) self.msgbus.deregister(endpoint="ExecEngine.execute", handler=self.exec_engine.execute) self.msgbus.deregister(endpoint="ExecEngine.process", handler=self.exec_engine.process) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, config=LiveExecEngineConfig(qsize=1), ) strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) event = TestStubs.event_order_submitted(order) # Act self.exec_engine.execute(submit_order) self.exec_engine.process(event) # Add over max size await asyncio.sleep(0.1) # Assert assert self.exec_engine.qsize() == 1 assert self.exec_engine.command_count == 0 @pytest.mark.asyncio async def test_start(self): # Arrange, Act self.exec_engine.start() await asyncio.sleep(0.1) # Assert assert self.exec_engine.is_running # Tear Down self.exec_engine.stop() @pytest.mark.asyncio async def test_kill_when_running_and_no_messages_on_queues(self): # Arrange, Act self.exec_engine.start() await asyncio.sleep(0) self.exec_engine.kill() # Assert assert self.exec_engine.is_stopped @pytest.mark.asyncio async def test_kill_when_not_running_with_messages_on_queue(self): # Arrange, Act self.exec_engine.kill() # Assert assert self.exec_engine.qsize() == 0 @pytest.mark.asyncio async def test_execute_command_places_command_on_queue(self): # Arrange self.exec_engine.start() strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.exec_engine.execute(submit_order) await asyncio.sleep(0.1) # Assert assert self.exec_engine.qsize() == 0 assert self.exec_engine.command_count == 1 # Tear Down self.exec_engine.stop() @pytest.mark.asyncio async def test_reconcile_state_with_no_active_orders(self): # Arrange self.exec_engine.start() strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) # Act await self.exec_engine.reconcile_state(timeout_secs=10) self.exec_engine.stop() await asyncio.sleep(0.1) # Assert assert True # No exceptions raised @pytest.mark.asyncio async def test_reconcile_state_when_report_agrees_reconciles(self): # Arrange self.exec_engine.start() strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00000"), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.exec_engine.execute(submit_order) self.exec_engine.process(TestStubs.event_order_submitted(order)) self.exec_engine.process(TestStubs.event_order_accepted(order)) report = OrderStatusReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), # <-- from stub event order_status=OrderStatus.ACCEPTED, filled_qty=Quantity.zero(), ts_init=0, ) self.client.add_order_status_report(report) await asyncio.sleep(0.1) # Allow processing time # Act result = await self.exec_engine.reconcile_state(timeout_secs=10) self.exec_engine.stop() # Assert assert result @pytest.mark.asyncio async def test_reconcile_state_when_canceled_reconciles(self): # Arrange self.exec_engine.start() strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00000"), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.exec_engine.execute(submit_order) self.exec_engine.process(TestStubs.event_order_submitted(order)) self.exec_engine.process(TestStubs.event_order_accepted(order)) report = OrderStatusReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), # <-- from stub event order_status=OrderStatus.CANCELED, filled_qty=Quantity.zero(), ts_init=0, ) self.client.add_order_status_report(report) await asyncio.sleep(0.1) # Allow processing time # Act result = await self.exec_engine.reconcile_state(timeout_secs=10) self.exec_engine.stop() # Assert assert result @pytest.mark.asyncio async def test_reconcile_state_when_expired_reconciles(self): # Arrange self.exec_engine.start() strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00000"), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.exec_engine.execute(submit_order) self.exec_engine.process(TestStubs.event_order_submitted(order)) self.exec_engine.process(TestStubs.event_order_accepted(order)) report = OrderStatusReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), # <-- from stub event order_status=OrderStatus.EXPIRED, filled_qty=Quantity.zero(), ts_init=0, ) self.client.add_order_status_report(report) await asyncio.sleep(0.1) # Allow processing time # Act result = await self.exec_engine.reconcile_state(timeout_secs=10) self.exec_engine.stop() # Assert assert result @pytest.mark.skip(reason="reimplement reconciliation") @pytest.mark.asyncio async def test_reconcile_state_when_partially_filled_reconciles(self): # Arrange self.exec_engine.start() strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00000"), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.exec_engine.execute(submit_order) self.exec_engine.process(TestStubs.event_order_submitted(order)) self.exec_engine.process(TestStubs.event_order_accepted(order)) report = OrderStatusReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), # <-- from stub event order_status=OrderStatus.PARTIALLY_FILLED, filled_qty=Quantity.from_int(70000), ts_init=0, ) trade1 = ExecutionReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), venue_position_id=None, execution_id=ExecutionId("1"), last_qty=Quantity.from_int(50000), last_px=Price.from_str("1.00000"), commission=Money(5.00, USD), liquidity_side=LiquiditySide.MAKER, ts_event=0, ts_init=0, ) trade2 = ExecutionReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), venue_position_id=None, execution_id=ExecutionId("2"), last_qty=Quantity.from_int(20000), last_px=Price.from_str("1.00000"), commission=Money(2.00, USD), liquidity_side=LiquiditySide.MAKER, ts_event=0, ts_init=0, ) self.client.add_order_status_report(report) self.client.add_trades_list(VenueOrderId("1"), [trade1, trade2]) await asyncio.sleep(0.1) # Allow processing time # Act result = await self.exec_engine.reconcile_state(timeout_secs=10) self.exec_engine.stop() # Assert assert result @pytest.mark.skip(reason="reimplement reconciliation") @pytest.mark.asyncio async def test_reconcile_state_when_filled_reconciles(self): # Arrange self.exec_engine.start() strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00000"), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.exec_engine.execute(submit_order) self.exec_engine.process(TestStubs.event_order_submitted(order)) self.exec_engine.process(TestStubs.event_order_accepted(order)) report = OrderStatusReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), # <-- from stub event order_status=OrderStatus.FILLED, filled_qty=Quantity.from_int(100000), ts_init=0, ) trade1 = ExecutionReport( execution_id=ExecutionId("1"), client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), venue_position_id=None, last_qty=Quantity.from_int(50000), last_px=Price.from_str("1.00000"), commission=Money(5.00, USD), liquidity_side=LiquiditySide.MAKER, ts_event=0, ts_init=0, ) trade2 = ExecutionReport( execution_id=ExecutionId("2"), client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), venue_position_id=None, last_qty=Quantity.from_int(50000), last_px=Price.from_str("1.00000"), commission=Money(2.00, USD), liquidity_side=LiquiditySide.MAKER, ts_event=0, ts_init=0, ) self.client.add_order_status_report(report) self.client.add_trades_list(VenueOrderId("1"), [trade1, trade2]) await asyncio.sleep(0.1) # Allow processing time # Act result = await self.exec_engine.reconcile_state(timeout_secs=10) self.exec_engine.stop() # Assert assert result
class LiveExecutionPerformanceTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.logger = TestLogger(self.clock, bypass_logging=True) self.trader_id = TraderId("TESTER", "000") self.account_id = AccountId("BINANCE", "001") self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) self.portfolio.register_cache(DataCache(self.logger)) self.analyzer = PerformanceAnalyzer() # Fresh isolated loop testing pattern self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) database = BypassExecutionDatabase(trader_id=self.trader_id, logger=self.logger) self.exec_engine = LiveExecutionEngine( loop=self.loop, database=database, portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) exec_client = MockExecutionClient( venue=Venue("BINANCE"), account_id=self.account_id, exec_engine=self.exec_engine, clock=self.clock, logger=self.logger, ) self.exec_engine.register_client(exec_client) self.exec_engine.process(TestStubs.event_account_state(self.account_id)) self.strategy = TradingStrategy(order_id_tag="001") self.strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.exec_engine.register_strategy(self.strategy) def submit_order(self): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.symbol, OrderSide.BUY, Quantity("1.00000000"), ) self.strategy.submit_order(order) def test_execute_command(self): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.symbol, OrderSide.BUY, Quantity("1.00000000"), ) command = SubmitOrder( order.symbol.venue, self.trader_id, self.account_id, self.strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.utc_now(), ) def execute_command(): self.exec_engine.execute(command) PerformanceHarness.profile_function(execute_command, 10000, 1) # ~0.0ms / ~0.3μs / 253ns minimum of 10,000 runs @ 1 iteration each run. def test_submit_order(self): self.exec_engine.start() time.sleep(0.1) async def run_test(): def submit_order(): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.symbol, OrderSide.BUY, Quantity("1.00000000"), ) self.strategy.submit_order(order) PerformanceHarness.profile_function(submit_order, 10000, 1) self.loop.run_until_complete(run_test()) # ~0.0ms / ~24.5μs / 24455ns minimum of 10,000 runs @ 1 iteration each run. def test_submit_order_end_to_end(self): self.exec_engine.start() time.sleep(0.1) async def run_test(): for _ in range(10000): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.symbol, OrderSide.BUY, Quantity("1.00000000"), ) self.strategy.submit_order(order) stats_file = "perf_live_execution.prof" cProfile.runctx("self.loop.run_until_complete(run_test())", globals(), locals(), stats_file) s = pstats.Stats(stats_file) s.strip_dirs().sort_stats("time").print_stats()
class LiveExecutionEngineTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.logger = TestLogger(self.clock) self.trader_id = TraderId("TESTER", "000") self.account_id = TestStubs.account_id() self.order_factory = OrderFactory( trader_id=self.trader_id, strategy_id=StrategyId("S", "001"), clock=self.clock, ) self.random_order_factory = OrderFactory( trader_id=TraderId("RANDOM", "042"), strategy_id=StrategyId("S", "042"), clock=self.clock, ) self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) self.portfolio.register_cache(DataCache(self.logger)) self.analyzer = PerformanceAnalyzer() # Fresh isolated loop testing pattern self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.database = BypassExecutionDatabase(trader_id=self.trader_id, logger=self.logger) self.exec_engine = LiveExecutionEngine( loop=self.loop, database=self.database, portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) self.venue = Venue("SIM") self.exec_client = MockExecutionClient( self.venue, self.account_id, self.exec_engine, self.clock, self.logger, ) self.exec_engine.register_client(self.exec_client) def tearDown(self): self.exec_engine.dispose() self.loop.stop() self.loop.close() def test_start_when_loop_not_running_logs(self): # Arrange # Act self.exec_engine.start() # Assert self.assertTrue(True) # No exceptions raised self.exec_engine.stop() def test_message_qsize_at_max_blocks_on_put_command(self): # Arrange self.exec_engine = LiveExecutionEngine(loop=self.loop, database=self.database, portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={"qsize": 1}) strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.exec_engine.register_strategy(strategy) order = strategy.order_factory.market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), ) submit_order = SubmitOrder( Venue("SIM"), self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.utc_now(), ) # Act self.exec_engine.execute(submit_order) self.exec_engine.execute(submit_order) # Assert self.assertEqual(1, self.exec_engine.qsize()) self.assertEqual(0, self.exec_engine.command_count) def test_message_qsize_at_max_blocks_on_put_event(self): # Arrange self.exec_engine = LiveExecutionEngine(loop=self.loop, database=self.database, portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={"qsize": 1}) strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.exec_engine.register_strategy(strategy) order = strategy.order_factory.market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), ) submit_order = SubmitOrder( Venue("SIM"), self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.utc_now(), ) event = TestStubs.event_order_submitted(order) # Act self.exec_engine.execute(submit_order) self.exec_engine.process(event) # Add over max size # Assert self.assertEqual(1, self.exec_engine.qsize()) self.assertEqual(0, self.exec_engine.command_count) def test_get_event_loop_returns_expected_loop(self): # Arrange # Act loop = self.exec_engine.get_event_loop() # Assert self.assertEqual(self.loop, loop) def test_start(self): async def run_test(): # Arrange # Act self.exec_engine.start() await asyncio.sleep(0.1) # Assert self.assertEqual(ComponentState.RUNNING, self.exec_engine.state) # Tear Down self.exec_engine.stop() self.loop.run_until_complete(run_test()) def test_kill_when_running_and_no_messages_on_queues(self): async def run_test(): # Arrange # Act self.exec_engine.start() await asyncio.sleep(0) self.exec_engine.kill() # Assert self.assertEqual(ComponentState.STOPPED, self.exec_engine.state) self.loop.run_until_complete(run_test()) def test_kill_when_not_running_with_messages_on_queue(self): async def run_test(): # Arrange # Act self.exec_engine.kill() # Assert self.assertEqual(0, self.exec_engine.qsize()) self.loop.run_until_complete(run_test()) def test_execute_command_places_command_on_queue(self): async def run_test(): # Arrange self.exec_engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.exec_engine.register_strategy(strategy) order = strategy.order_factory.market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), ) submit_order = SubmitOrder( Venue("SIM"), self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.utc_now(), ) # Act self.exec_engine.execute(submit_order) await asyncio.sleep(0.1) # Assert self.assertEqual(0, self.exec_engine.qsize()) self.assertEqual(1, self.exec_engine.command_count) # Tear Down self.exec_engine.stop() self.loop.run_until_complete(run_test()) def test_handle_position_opening_with_position_id_none(self): async def run_test(): # Arrange self.exec_engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.exec_engine.register_strategy(strategy) order = strategy.order_factory.market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), ) event = TestStubs.event_order_submitted(order) # Act self.exec_engine.process(event) await asyncio.sleep(0.1) # Assert self.assertEqual(0, self.exec_engine.qsize()) self.assertEqual(1, self.exec_engine.event_count) # Tear Down self.exec_engine.stop() self.loop.run_until_complete(run_test())
class TestLiveExecutionEngine: def setup(self): # Fixture Setup self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.logger = Logger(self.clock) self.trader_id = TraderId("TESTER", "000") self.account_id = TestStubs.account_id() self.order_factory = OrderFactory( trader_id=self.trader_id, strategy_id=StrategyId("S", "001"), clock=self.clock, ) self.random_order_factory = OrderFactory( trader_id=TraderId("RANDOM", "042"), strategy_id=StrategyId("S", "042"), clock=self.clock, ) self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) self.portfolio.register_cache(DataCache(self.logger)) self.analyzer = PerformanceAnalyzer() # Fresh isolated loop testing pattern self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.database = BypassExecutionDatabase(trader_id=self.trader_id, logger=self.logger) self.engine = LiveExecutionEngine( loop=self.loop, database=self.database, portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) self.instrument_provider = InstrumentProvider() self.instrument_provider.add(AUDUSD_SIM) self.instrument_provider.add(GBPUSD_SIM) self.client = MockLiveExecutionClient( name=SIM.value, account_id=self.account_id, engine=self.engine, instrument_provider=self.instrument_provider, clock=self.clock, logger=self.logger, ) self.engine.register_client(self.client) def teardown(self): self.engine.dispose() self.loop.stop() self.loop.close() def test_start_when_loop_not_running_logs(self): # Arrange # Act self.engine.start() # Assert assert True # No exceptions raised self.engine.stop() def test_get_event_loop_returns_expected_loop(self): # Arrange # Act loop = self.engine.get_event_loop() # Assert assert loop == self.loop def test_message_qsize_at_max_blocks_on_put_command(self): # Arrange self.engine = LiveExecutionEngine( loop=self.loop, database=self.database, portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={"qsize": 1}, ) strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.engine.register_strategy(strategy) order = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), ) submit_order = SubmitOrder( order.instrument_id, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.engine.execute(submit_order) self.engine.execute(submit_order) # Assert assert self.engine.qsize() == 1 assert self.engine.command_count == 0 def test_message_qsize_at_max_blocks_on_put_event(self): # Arrange self.engine = LiveExecutionEngine( loop=self.loop, database=self.database, portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={"qsize": 1}, ) strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.engine.register_strategy(strategy) order = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), ) submit_order = SubmitOrder( order.instrument_id, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) event = TestStubs.event_order_submitted(order) # Act self.engine.execute(submit_order) self.engine.process(event) # Add over max size # Assert assert self.engine.qsize() == 1 assert self.engine.command_count == 0 def test_start(self): async def run_test(): # Arrange # Act self.engine.start() await asyncio.sleep(0.1) # Assert assert self.engine.state == ComponentState.RUNNING # Tear Down self.engine.stop() self.loop.run_until_complete(run_test()) def test_kill_when_running_and_no_messages_on_queues(self): async def run_test(): # Arrange # Act self.engine.start() await asyncio.sleep(0) self.engine.kill() # Assert assert self.engine.state == ComponentState.STOPPED self.loop.run_until_complete(run_test()) def test_kill_when_not_running_with_messages_on_queue(self): async def run_test(): # Arrange # Act self.engine.kill() # Assert assert self.engine.qsize() == 0 self.loop.run_until_complete(run_test()) def test_execute_command_places_command_on_queue(self): async def run_test(): # Arrange self.engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.engine.register_strategy(strategy) order = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), ) submit_order = SubmitOrder( order.instrument_id, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.engine.execute(submit_order) await asyncio.sleep(0.1) # Assert assert self.engine.qsize() == 0 assert self.engine.command_count == 1 # Tear Down self.engine.stop() self.loop.run_until_complete(run_test()) def test_handle_position_opening_with_position_id_none(self): async def run_test(): # Arrange self.engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.engine.register_strategy(strategy) order = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), ) event = TestStubs.event_order_submitted(order) # Act self.engine.process(event) await asyncio.sleep(0.1) # Assert assert self.engine.qsize() == 0 assert self.engine.event_count == 1 # Tear Down self.engine.stop() self.loop.run_until_complete(run_test()) def test_reconcile_state_with_no_active_orders(self): async def run_test(): # Arrange self.engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.engine.register_strategy(strategy) # Act await self.engine.reconcile_state() self.engine.stop() # Assert assert True # No exceptions raised self.loop.run_until_complete(run_test()) def test_reconcile_state_when_report_agrees_reconciles(self): async def run_test(): # Arrange self.engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.engine.register_strategy(strategy) order = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), Price("1.00000"), ) submit_order = SubmitOrder( AUDUSD_SIM.id, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.engine.execute(submit_order) self.engine.process(TestStubs.event_order_submitted(order)) self.engine.process(TestStubs.event_order_accepted(order)) report = OrderStatusReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), # <-- from stub event order_state=OrderState.ACCEPTED, filled_qty=Quantity(0), timestamp_ns=0, ) self.client.add_order_status_report(report) await asyncio.sleep(0.1) # Allow processing time # Act result = await self.engine.reconcile_state() self.engine.stop() # Assert assert result self.loop.run_until_complete(run_test()) def test_reconcile_state_when_cancelled_reconciles(self): async def run_test(): # Arrange self.engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.engine.register_strategy(strategy) order = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), Price("1.00000"), ) submit_order = SubmitOrder( AUDUSD_SIM.id, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.engine.execute(submit_order) self.engine.process(TestStubs.event_order_submitted(order)) self.engine.process(TestStubs.event_order_accepted(order)) report = OrderStatusReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), # <-- from stub event order_state=OrderState.CANCELLED, filled_qty=Quantity(0), timestamp_ns=0, ) self.client.add_order_status_report(report) await asyncio.sleep(0.1) # Allow processing time # Act result = await self.engine.reconcile_state() self.engine.stop() # Assert assert result self.loop.run_until_complete(run_test()) def test_reconcile_state_when_expired_reconciles(self): async def run_test(): # Arrange self.engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.engine.register_strategy(strategy) order = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), Price("1.00000"), ) submit_order = SubmitOrder( AUDUSD_SIM.id, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.engine.execute(submit_order) self.engine.process(TestStubs.event_order_submitted(order)) self.engine.process(TestStubs.event_order_accepted(order)) report = OrderStatusReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), # <-- from stub event order_state=OrderState.EXPIRED, filled_qty=Quantity(0), timestamp_ns=0, ) self.client.add_order_status_report(report) await asyncio.sleep(0.01) # Act result = await self.engine.reconcile_state() self.engine.stop() # Assert assert result self.loop.run_until_complete(run_test()) def test_reconcile_state_when_partially_filled_reconciles(self): async def run_test(): # Arrange self.engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.engine.register_strategy(strategy) order = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), Price("1.00000"), ) submit_order = SubmitOrder( AUDUSD_SIM.id, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.engine.execute(submit_order) self.engine.process(TestStubs.event_order_submitted(order)) self.engine.process(TestStubs.event_order_accepted(order)) report = OrderStatusReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), # <-- from stub event order_state=OrderState.PARTIALLY_FILLED, filled_qty=Quantity(70000), timestamp_ns=0, ) trade1 = ExecutionReport( execution_id=ExecutionId("1"), client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), last_qty=Decimal(50000), last_px=Decimal("1.00000"), commission_amount=Decimal("5.0"), commission_currency="USD", liquidity_side=LiquiditySide.MAKER, execution_ns=0, timestamp_ns=0, ) trade2 = ExecutionReport( execution_id=ExecutionId("2"), client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), last_qty=Decimal(20000), last_px=Decimal("1.00000"), commission_amount=Decimal("2.0"), commission_currency="USD", liquidity_side=LiquiditySide.MAKER, execution_ns=0, timestamp_ns=0, ) self.client.add_order_status_report(report) self.client.add_trades_list(VenueOrderId("1"), [trade1, trade2]) await asyncio.sleep(0.01) # Act result = await self.engine.reconcile_state() self.engine.stop() # Assert assert result self.loop.run_until_complete(run_test()) def test_reconcile_state_when_filled_reconciles(self): async def run_test(): # Arrange self.engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.engine.register_strategy(strategy) order = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), Price("1.00000"), ) submit_order = SubmitOrder( AUDUSD_SIM.id, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.engine.execute(submit_order) self.engine.process(TestStubs.event_order_submitted(order)) self.engine.process(TestStubs.event_order_accepted(order)) report = OrderStatusReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), # <-- from stub event order_state=OrderState.FILLED, filled_qty=Quantity(100000), timestamp_ns=0, ) trade1 = ExecutionReport( execution_id=ExecutionId("1"), client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), last_qty=Decimal(50000), last_px=Decimal("1.00000"), commission_amount=Decimal("5.0"), commission_currency="USD", liquidity_side=LiquiditySide.MAKER, execution_ns=0, timestamp_ns=0, ) trade2 = ExecutionReport( execution_id=ExecutionId("2"), client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), last_qty=Decimal(50000), last_px=Decimal("1.00000"), commission_amount=Decimal("2.0"), commission_currency="USD", liquidity_side=LiquiditySide.MAKER, execution_ns=0, timestamp_ns=0, ) self.client.add_order_status_report(report) self.client.add_trades_list(VenueOrderId("1"), [trade1, trade2]) await asyncio.sleep(0.01) # Act result = await self.engine.reconcile_state() self.engine.stop() # Assert assert result self.loop.run_until_complete(run_test())
class TestLiveExecutionClient: def setup(self): # Fixture Setup # Fresh isolated loop testing pattern self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.logger = Logger(self.clock) self.trader_id = TraderId("TESTER", "000") self.account_id = TestStubs.account_id() self.order_factory = OrderFactory( trader_id=self.trader_id, strategy_id=StrategyId("S", "001"), clock=self.clock, ) self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) self.portfolio.register_cache(DataCache(self.logger)) self.analyzer = PerformanceAnalyzer() # Fresh isolated loop testing pattern self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) database = BypassExecutionDatabase(trader_id=self.trader_id, logger=self.logger) self.engine = LiveExecutionEngine( loop=self.loop, database=database, portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) self.client = MockLiveExecutionClient( name=SIM.value, account_id=self.account_id, engine=self.engine, instrument_provider=InstrumentProvider(), clock=self.clock, logger=self.logger, ) self.engine.register_client(self.client) def teardown(self): self.client.dispose() def test_reconcile_state_given_no_order_and_not_in_cache_returns_false( self): async def run_test(): # Arrange report = OrderStatusReport( client_order_id=ClientOrderId("O-123456"), venue_order_id=VenueOrderId("1"), order_state=OrderState.FILLED, filled_qty=Quantity(100000), timestamp_ns=0, ) # Act result = await self.client.reconcile_state( report, order=None) # <- order won't be in cache # Assert assert not result self.loop.run_until_complete(run_test()) def test_reconcile_state_when_order_completed_returns_true_with_warning1( self): async def run_test(): # Arrange self.engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.engine.register_strategy(strategy) order = strategy.order_factory.stop_market( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), Price("1.00000"), ) submit_order = SubmitOrder( AUDUSD_SIM.id, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.engine.execute(submit_order) self.engine.process(TestStubs.event_order_submitted(order)) await asyncio.sleep(0) # Process queue self.engine.process(TestStubs.event_order_accepted(order)) await asyncio.sleep(0) # Process queue self.engine.process(TestStubs.event_order_cancelled(order)) await asyncio.sleep(0) # Process queue report = OrderStatusReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), # <-- from stub event order_state=OrderState.CANCELLED, filled_qty=Quantity(0), timestamp_ns=0, ) # Act result = await self.client.reconcile_state(report, order) # Assert assert result self.loop.run_until_complete(run_test()) def test_reconcile_state_when_order_completed_returns_true_with_warning2( self): async def run_test(): # Arrange self.engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.engine.register_strategy(strategy) order = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), Price("1.00000"), ) submit_order = SubmitOrder( AUDUSD_SIM.id, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.engine.execute(submit_order) self.engine.process(TestStubs.event_order_submitted(order)) await asyncio.sleep(0) # Process queue self.engine.process(TestStubs.event_order_accepted(order)) await asyncio.sleep(0) # Process queue self.engine.process(TestStubs.event_order_filled( order, AUDUSD_SIM)) await asyncio.sleep(0) # Process queue report = OrderStatusReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), # <-- from stub event order_state=OrderState.FILLED, filled_qty=Quantity(100000), timestamp_ns=0, ) # Act result = await self.client.reconcile_state(report, order) # Assert assert result self.loop.run_until_complete(run_test()) def test_reconcile_state_with_filled_order_when_trades_not_given_returns_false( self, ): async def run_test(): # Arrange self.engine.start() strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.engine.register_strategy(strategy) order = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), Price("1.00000"), ) submit_order = SubmitOrder( AUDUSD_SIM.id, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.engine.execute(submit_order) self.engine.process(TestStubs.event_order_submitted(order)) await asyncio.sleep(0) # Process queue self.engine.process(TestStubs.event_order_accepted(order)) await asyncio.sleep(0) # Process queue report = OrderStatusReport( client_order_id=order.client_order_id, venue_order_id=VenueOrderId("1"), # <-- from stub event order_state=OrderState.FILLED, filled_qty=Quantity(100000), timestamp_ns=0, ) # Act result = await self.client.reconcile_state(report, order) # Assert assert not result self.loop.run_until_complete(run_test())
class TestLiveExecutionPerformance(PerformanceHarness): def setup(self): # Fixture Setup self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.trader_id = TraderId("TESTER", "000") self.logger = Logger(self.clock, bypass_logging=True) self.account_id = AccountId("BINANCE", "001") self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) self.portfolio.register_cache(DataCache(self.logger)) self.analyzer = PerformanceAnalyzer() # Fresh isolated loop testing pattern self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) database = BypassExecutionDatabase(trader_id=self.trader_id, logger=self.logger) self.exec_engine = LiveExecutionEngine( loop=self.loop, database=database, portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) exec_client = MockExecutionClient( client_id=ClientId("BINANCE"), account_id=self.account_id, engine=self.exec_engine, clock=self.clock, logger=self.logger, ) self.exec_engine.register_client(exec_client) self.exec_engine.process(TestStubs.event_account_state(self.account_id)) self.strategy = TradingStrategy(order_id_tag="001") self.strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.exec_engine.register_strategy(self.strategy) @pytest.fixture(autouse=True) @pytest.mark.benchmark(disable_gc=True, warmup=True) def setup_benchmark(self, benchmark): self.benchmark = benchmark def submit_order(self): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity("1.00000000"), ) self.strategy.submit_order(order) def test_execute_command(self): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity("1.00000000"), ) command = SubmitOrder( order.instrument_id.venue.client_id, self.trader_id, self.account_id, self.strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) def execute_command(): self.exec_engine.execute(command) self.benchmark.pedantic(execute_command, iterations=10_000, rounds=1) # ~0.0ms / ~0.2μs / 218ns minimum of 10,000 runs @ 1 iteration each run. def test_submit_order(self): self.exec_engine.start() time.sleep(0.1) async def run_test(): def submit_order(): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity("1.00000000"), ) self.strategy.submit_order(order) self.benchmark.pedantic(submit_order, iterations=10_000, rounds=1) self.loop.run_until_complete(run_test()) # ~0.0ms / ~25.3μs / 25326ns minimum of 10,000 runs @ 1 iteration each run. def test_submit_order_end_to_end(self): self.exec_engine.start() time.sleep(0.1) async def run_test(): for _ in range(10000): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity("1.00000000"), ) self.strategy.submit_order(order) stats_file = "perf_live_execution.prof" cProfile.runctx( "self.loop.run_until_complete(run_test())", globals(), locals(), stats_file ) s = pstats.Stats(stats_file) s.strip_dirs().sort_stats("time").print_stats()
class TestLiveExecutionPerformance(PerformanceHarness): def setup(self): # Fixture Setup self.loop = asyncio.get_event_loop() self.loop.set_debug(True) self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.logger = Logger(self.clock, bypass=True) self.trader_id = TestIdStubs.trader_id() self.account_id = AccountId(BINANCE.value, "001") self.msgbus = MessageBus( trader_id=self.trader_id, clock=self.clock, logger=self.logger, ) self.cache = TestComponentStubs.cache() self.portfolio = Portfolio( msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.data_engine = LiveDataEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.risk_engine = LiveRiskEngine( loop=self.loop, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.exec_client = MockExecutionClient( client_id=ClientId("BINANCE"), venue=BINANCE, account_type=AccountType.CASH, base_currency=None, # Multi-currency account msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.portfolio.update_account(TestEventStubs.margin_account_state()) self.exec_engine.register_client(self.exec_client) self.strategy = TradingStrategy() self.strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) @pytest.fixture(autouse=True) @pytest.mark.benchmark(disable_gc=True, warmup=True) def setup_benchmark(self, benchmark): self.benchmark = benchmark def submit_order(self): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("1.00000000"), ) self.strategy.submit_order(order) @pytest.mark.skip(reason="For development only, event loop issue") def test_execute_command(self): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("1.00000000"), ) command = SubmitOrder( None, self.trader_id, self.strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) def execute_command(): self.exec_engine.execute(command) self.benchmark.pedantic(execute_command, iterations=100, rounds=100, warmup_rounds=5) # ~0.0ms / ~0.2μs / 218ns minimum of 10,000 runs @ 1 iteration each run. @pytest.mark.asyncio async def test_submit_order(self): self.exec_engine.start() await asyncio.sleep(1) def submit_order(): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("1.00000000"), ) self.strategy.submit_order(order) self.benchmark.pedantic(submit_order, iterations=100, rounds=100, warmup_rounds=5) # ~0.0ms / ~25.3μs / 25326ns minimum of 10,000 runs @ 1 iteration each run. @pytest.mark.asyncio async def test_submit_order_end_to_end(self): self.exec_engine.start() await asyncio.sleep(1) def run(): for _ in range(1000): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("1.00000000"), ) self.strategy.submit_order(order) self.benchmark.pedantic(run, rounds=10, warmup_rounds=5)
class TestLiveExecutionEngine: def setup(self): # Fixture Setup self.loop = asyncio.get_event_loop() self.loop.set_debug(True) self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.logger = Logger(self.clock) self.trader_id = TestIdStubs.trader_id() self.order_factory = OrderFactory( trader_id=self.trader_id, strategy_id=StrategyId("S-001"), clock=self.clock, ) self.random_order_factory = OrderFactory( trader_id=TraderId("RANDOM-042"), strategy_id=StrategyId("S-042"), clock=self.clock, ) self.msgbus = MessageBus( trader_id=self.trader_id, clock=self.clock, logger=self.logger, ) self.cache = TestComponentStubs.cache() self.portfolio = Portfolio( msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.data_engine = LiveDataEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.risk_engine = LiveRiskEngine( loop=self.loop, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.instrument_provider = InstrumentProvider( venue=SIM, logger=self.logger, ) self.instrument_provider.add(AUDUSD_SIM) self.instrument_provider.add(GBPUSD_SIM) self.client = MockLiveExecutionClient( loop=self.loop, client_id=ClientId(SIM.value), venue=SIM, account_type=AccountType.CASH, base_currency=USD, instrument_provider=self.instrument_provider, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.portfolio.update_account(TestEventStubs.cash_account_state()) self.exec_engine.register_client(self.client) self.cache.add_instrument(AUDUSD_SIM) def teardown(self): self.exec_engine.dispose() @pytest.mark.asyncio async def test_start_when_loop_not_running_logs(self): # Arrange, Act self.exec_engine.start() # Assert assert True # No exceptions raised self.exec_engine.stop() @pytest.mark.asyncio async def test_message_qsize_at_max_blocks_on_put_command(self): # Arrange # Deregister test fixture ExecutionEngine from msgbus) self.msgbus.deregister( endpoint="ExecEngine.execute", handler=self.exec_engine.execute, ) self.msgbus.deregister( endpoint="ExecEngine.process", handler=self.exec_engine.process, ) self.msgbus.deregister( endpoint="ExecEngine.reconcile_report", handler=self.exec_engine.reconcile_report, ) self.msgbus.deregister( endpoint="ExecEngine.reconcile_mass_status", handler=self.exec_engine.reconcile_mass_status, ) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, config=LiveExecEngineConfig(qsize=1), ) strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.exec_engine.execute(submit_order) self.exec_engine.execute(submit_order) await asyncio.sleep(0.1) # Assert assert self.exec_engine.qsize() == 1 assert self.exec_engine.command_count == 0 @pytest.mark.asyncio async def test_message_qsize_at_max_blocks_on_put_event(self): # Arrange # Deregister test fixture ExecutionEngine from msgbus) self.msgbus.deregister( endpoint="ExecEngine.execute", handler=self.exec_engine.execute, ) self.msgbus.deregister( endpoint="ExecEngine.process", handler=self.exec_engine.process, ) self.msgbus.deregister( endpoint="ExecEngine.reconcile_report", handler=self.exec_engine.reconcile_report, ) self.msgbus.deregister( endpoint="ExecEngine.reconcile_mass_status", handler=self.exec_engine.reconcile_mass_status, ) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, config=LiveExecEngineConfig(qsize=1), ) strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) event = TestEventStubs.order_submitted(order) # Act self.exec_engine.execute(submit_order) self.exec_engine.process(event) # Add over max size await asyncio.sleep(0.1) # Assert assert self.exec_engine.qsize() == 1 assert self.exec_engine.command_count == 0 @pytest.mark.asyncio async def test_start(self): # Arrange, Act self.exec_engine.start() await asyncio.sleep(0.1) # Assert assert self.exec_engine.is_running # Tear Down self.exec_engine.stop() @pytest.mark.asyncio async def test_kill_when_running_and_no_messages_on_queues(self): # Arrange, Act self.exec_engine.start() await asyncio.sleep(0) self.exec_engine.kill() # Assert assert self.exec_engine.is_stopped @pytest.mark.asyncio async def test_kill_when_not_running_with_messages_on_queue(self): # Arrange, Act self.exec_engine.kill() # Assert assert self.exec_engine.qsize() == 0 @pytest.mark.asyncio async def test_execute_command_places_command_on_queue(self): # Arrange self.exec_engine.start() strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.exec_engine.execute(submit_order) await asyncio.sleep(0.1) # Assert assert self.exec_engine.qsize() == 0 assert self.exec_engine.command_count == 1 # Tear Down self.exec_engine.stop() def test_handle_order_status_report(self): # Arrange order_report = OrderStatusReport( account_id=AccountId("SIM", "001"), instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456"), order_list_id=OrderListId("1"), venue_order_id=VenueOrderId("2"), order_side=OrderSide.SELL, order_type=OrderType.STOP_LIMIT, contingency_type=ContingencyType.OCO, time_in_force=TimeInForce.DAY, expire_time=None, order_status=OrderStatus.REJECTED, price=Price.from_str("0.90090"), trigger_price=Price.from_str("0.90100"), trigger_type=TriggerType.DEFAULT, limit_offset=None, trailing_offset=Decimal("0.00010"), offset_type=TrailingOffsetType.PRICE, quantity=Quantity.from_int(1_000_000), filled_qty=Quantity.from_int(0), display_qty=None, avg_px=None, post_only=True, reduce_only=False, cancel_reason="SOME_REASON", report_id=UUID4(), ts_accepted=1_000_000, ts_triggered=1_500_000, ts_last=2_000_000, ts_init=3_000_000, ) # Act self.exec_engine.reconcile_report(order_report) # Assert assert self.exec_engine.report_count == 1 def test_handle_trade_report(self): # Arrange trade_report = TradeReport( account_id=AccountId("SIM", "001"), instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456789"), venue_order_id=VenueOrderId("1"), venue_position_id=PositionId("2"), trade_id=TradeId("3"), order_side=OrderSide.BUY, last_qty=Quantity.from_int(100), last_px=Price.from_str("100.50"), commission=Money("4.50", USD), liquidity_side=LiquiditySide.TAKER, report_id=UUID4(), ts_event=0, ts_init=0, ) # Act self.exec_engine.reconcile_report(trade_report) # Assert assert self.exec_engine.report_count == 1 def test_handle_position_status_report(self): # Arrange position_report = PositionStatusReport( account_id=AccountId("SIM", "001"), instrument_id=AUDUSD_SIM.id, venue_position_id=PositionId("1"), position_side=PositionSide.LONG, quantity=Quantity.from_int(1_000_000), report_id=UUID4(), ts_last=0, ts_init=0, ) # Act self.exec_engine.reconcile_report(position_report) # Assert assert self.exec_engine.report_count == 1 def test_execution_mass_status(self): # Arrange mass_status = ExecutionMassStatus( client_id=ClientId("SIM"), account_id=TestIdStubs.account_id(), venue=Venue("SIM"), report_id=UUID4(), ts_init=0, ) # Act self.exec_engine.reconcile_mass_status(mass_status) # Assert assert self.exec_engine.report_count == 1