def test_factory_returns_unique_uuids(self): # Arrange factory = UUIDFactory() # Act result1 = factory.generate() result2 = factory.generate() result3 = factory.generate() assert type(result1) == UUID assert result1 != result2 assert result2 != result3
def test_factory_returns_unique_uuids(self): # Arrange factory = UUIDFactory() # Act result1 = factory.generate() result2 = factory.generate() result3 = factory.generate() assert isinstance(result1, UUID4) assert result1 != result2 assert result2 != result3
def test_factory_returns_unique_uuids(self): # Arrange factory = UUIDFactory() # Act result1 = factory.generate() result2 = factory.generate() result3 = factory.generate() self.assertEqual(UUID, type(result1)) self.assertNotEqual(result1, result2) self.assertNotEqual(result2, result3)
class TestRiskEngine: def setup(self): # Fixture Setup self.clock = TestClock() self.uuid_factory = UUIDFactory() self.logger = TestLogger(self.clock) self.trader_id = TraderId("TESTER", "000") self.account_id = TestStubs.account_id() self.venue = Venue("SIM") self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) self.portfolio.register_cache(DataCache(self.logger)) self.database = MockExecutionDatabase(trader_id=self.trader_id, logger=self.logger) self.exec_engine = ExecutionEngine( database=self.database, portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) self.exec_client = MockExecutionClient( self.venue, self.account_id, self.exec_engine, self.clock, self.logger, ) self.risk_engine = RiskEngine( exec_engine=self.exec_engine, portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={}, ) self.exec_engine.register_client(self.exec_client) self.exec_engine.register_risk_engine(self.risk_engine) def test_registered_clients_returns_expected_list(self): # Arrange # Act result = self.risk_engine.registered_clients # Assert assert result == [Venue('SIM')] def test_set_block_all_orders_changes_flag_value(self): # Arrange # Act self.risk_engine.set_block_all_orders() # Assert assert self.risk_engine.block_all_orders def test_given_random_command_logs_and_continues(self): # Arrange random = TradingCommand( self.venue, self.uuid_factory.generate(), self.clock.utc_now(), ) self.risk_engine.execute(random) def test_given_random_event_logs_and_continues(self): # Arrange random = Event( self.uuid_factory.generate(), self.clock.utc_now(), ) self.exec_engine.process(random) def test_submit_order_with_default_settings_sends_to_client(self): # 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.id, OrderSide.BUY, Quantity(100000), ) submit_order = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.utc_now(), ) # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_client.calls == ['connect', 'submit_order'] def test_submit_bracket_with_default_settings_sends_to_client(self): # 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) entry = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), ) bracket = strategy.order_factory.bracket( entry_order=entry, stop_loss=Price("1.00000"), take_profit=Price("1.00010"), ) submit_bracket = SubmitBracketOrder( self.venue, self.trader_id, self.account_id, strategy.id, bracket, self.uuid_factory.generate(), self.clock.utc_now(), ) # Act self.risk_engine.execute(submit_bracket) # Assert assert self.exec_client.calls == ['connect', 'submit_bracket_order'] def test_submit_order_when_block_all_orders_true_then_denies_order(self): # 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.id, OrderSide.BUY, Quantity(100000), ) submit_order = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.utc_now(), ) self.risk_engine.set_block_all_orders() # Act self.exec_engine.execute(submit_order) # Assert assert self.exec_client.calls == ['connect'] assert self.exec_engine.event_count == 1 def test_amend_order_with_default_settings_sends_to_client(self): # 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.id, OrderSide.BUY, Quantity(100000), ) submit = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.utc_now(), ) amend = AmendOrder( self.venue, self.trader_id, self.account_id, order.cl_ord_id, order.quantity, Price("1.00010"), self.uuid_factory.generate(), self.clock.utc_now(), ) self.risk_engine.execute(submit) # Act self.risk_engine.execute(amend) # Assert assert self.exec_client.calls == ['connect', 'submit_order', 'amend_order'] def test_cancel_order_with_default_settings_sends_to_client(self): # 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.id, OrderSide.BUY, Quantity(100000), ) submit = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.utc_now(), ) cancel = CancelOrder( self.venue, self.trader_id, self.account_id, order.cl_ord_id, order.id, self.uuid_factory.generate(), self.clock.utc_now(), ) self.risk_engine.execute(submit) # Act self.risk_engine.execute(cancel) # Assert assert self.exec_client.calls == ['connect', 'submit_order', 'cancel_order'] def test_submit_bracket_when_block_all_orders_true_then_denies_order(self): # 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) entry = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity(100000), ) bracket = strategy.order_factory.bracket( entry_order=entry, stop_loss=Price("1.00000"), take_profit=Price("1.00010"), ) submit_bracket = SubmitBracketOrder( self.venue, self.trader_id, self.account_id, strategy.id, bracket, self.uuid_factory.generate(), self.clock.utc_now(), ) self.risk_engine.set_block_all_orders() # Act self.exec_engine.execute(submit_bracket) # Assert assert self.exec_client.calls == ['connect'] assert self.exec_engine.event_count == 3
class TestMessageBus: def setup(self): # Fixture Setup self.clock = TestClock() self.uuid_factory = UUIDFactory() self.logger = Logger(self.clock) self.trader_id = TestIdStubs.trader_id() self.handler = [] self.msgbus = MessageBus( trader_id=self.trader_id, clock=self.clock, logger=self.logger, ) def test_instantiate_message_bus(self): # Arrange, Act, Assert assert self.msgbus.trader_id == self.trader_id assert self.msgbus.sent_count == 0 assert self.msgbus.req_count == 0 assert self.msgbus.res_count == 0 assert self.msgbus.pub_count == 0 def test_endpoints_with_none_registered_returns_empty_list(self): # Arrange, Act result = self.msgbus.endpoints() assert result == [] def test_topics_with_no_subscribers_returns_empty_list(self): # Arrange, Act result = self.msgbus.topics() assert result == [] def test_subscriptions_with_no_subscribers_returns_empty_list(self): # Arrange, Act result = self.msgbus.subscriptions() # Assert assert result == [] def test_has_subscribers_with_no_subscribers_returns_false(self): # Arrange, Act, Assert assert not self.msgbus.has_subscribers() def test_register_adds_endpoint(self): # Arrange endpoint = [] # Act self.msgbus.register("mailbox", endpoint.append) # Assert assert self.msgbus.endpoints() == ["mailbox"] def test_deregister_removes_endpoint(self): # Arrange endpoint = [] self.msgbus.register("mailbox", endpoint.append) # Act self.msgbus.deregister("mailbox", endpoint.append) # Assert assert self.msgbus.endpoints() == [] def test_send_when_no_endpoint_at_address_logs_error(self): # Arrange, Act endpoint = [] self.msgbus.send("mailbox", "message") # Assert assert "message" not in endpoint assert self.msgbus.sent_count == 0 def test_send_when_endpoint_at_address_sends_message_to_handler(self): # Arrange endpoint = [] self.msgbus.register("mailbox", endpoint.append) # Act self.msgbus.send("mailbox", "message") # Assert assert "message" in endpoint assert self.msgbus.sent_count == 1 def test_request_when_endpoint_not_registered_logs_error(self): # Arrange, Act handler = [] request = Request( callback=handler.append, request_id=self.uuid_factory.generate(), ts_init=self.clock.timestamp_ns(), ) self.msgbus.request(endpoint="mailbox", request=request) # Assert assert len(handler) == 0 assert self.msgbus.req_count == 0 def test_response_when_no_correlation_id_logs_error(self): # Arrange, Act handler = [] response = Response( correlation_id=self.uuid_factory.generate(), response_id=self.uuid_factory.generate(), ts_init=self.clock.timestamp_ns(), ) self.msgbus.response(response) # Assert assert response not in handler assert self.msgbus.res_count == 0 def test_request_response_when_correlation_id_registered_handles_response( self): # Arrange, Act endpoint = [] handler = [] self.msgbus.register(endpoint="mailbox", handler=endpoint.append) correlation_id = self.uuid_factory.generate() request = Request( callback=handler.append, request_id=correlation_id, ts_init=self.clock.timestamp_ns(), ) self.msgbus.request(endpoint="mailbox", request=request) response = Response( correlation_id=correlation_id, response_id=self.uuid_factory.generate(), ts_init=self.clock.timestamp_ns(), ) self.msgbus.response(response) # Assert assert request in endpoint assert response in handler assert self.msgbus.req_count == 1 assert self.msgbus.res_count == 1 def test_subscribe_then_returns_topics_list_including_topic(self): # Arrange handler = [].append # Act self.msgbus.subscribe(topic="*", handler=handler) self.msgbus.subscribe(topic="system", handler=handler) result = self.msgbus.topics() # Assert assert result == ["*", "system"] def test_has_subscribers_when_subscribers_returns_true(self): # Arrange, Act self.msgbus.subscribe(topic="*", handler=[].append) self.msgbus.subscribe(topic="system", handler=[].append) # Assert assert self.msgbus.has_subscribers() assert self.msgbus.has_subscribers(pattern="system") def test_subscribe_when_handler_already_subscribed_does_not_add_subscription( self): # Arrange handler = [].append self.msgbus.subscribe(topic="a", handler=handler) # Act self.msgbus.subscribe(topic="a", handler=handler) result = self.msgbus.topics() # Assert assert result == ["a"] def test_subscribe_then_subscriptions_list_includes_handler(self): # Arrange handler = [].append # Act self.msgbus.subscribe(topic="system", handler=handler) result = self.msgbus.subscriptions("system") # Assert assert len(result) == 1 assert result[0].handler == handler def test_subscribe_to_all_then_subscriptions_list_includes_handler(self): # Arrange handler = [].append # Act self.msgbus.subscribe(topic="*", handler=handler) result = self.msgbus.subscriptions("*") # Assert assert len(result) == 1 assert result[0].handler == handler def test_subscribe_all_when_handler_already_subscribed_does_not_add_subscription( self): # Arrange handler = [].append self.msgbus.subscribe(topic="a*", handler=handler) # Act self.msgbus.subscribe(topic="a*", handler=handler) result = self.msgbus.subscriptions("a*") # Assert assert len(result) == 1 assert result[0].handler == handler def test_unsubscribe_then_handler_not_in_subscriptions_list(self): # Arrange handler = [].append self.msgbus.subscribe(topic="events.order*", handler=handler) # Act self.msgbus.unsubscribe(topic="events.order*", handler=handler) result = self.msgbus.subscriptions("events.order*") # Assert assert result == [] def test_unsubscribe_when_no_subscription_does_nothing(self): # Arrange handler = [].append # Act self.msgbus.unsubscribe(topic="*", handler=handler) result = self.msgbus.subscriptions(pattern="*") # Assert assert result == [] def test_unsubscribe_from_all_returns_subscriptions_list_without_handler( self): # Arrange handler = [].append self.msgbus.subscribe(topic="*", handler=handler) # Act self.msgbus.unsubscribe(topic="*", handler=handler) result = self.msgbus.subscriptions("*") # Assert assert result == [] def test_unsubscribe_from_all_when_no_subscription_does_nothing(self): # Arrange handler = [].append # Act self.msgbus.unsubscribe(topic="*", handler=handler) result = self.msgbus.subscriptions("*") # Assert assert result == [] def test_publish_with_no_subscribers_does_nothing(self): # Arrange, Act self.msgbus.publish("*", "hello world") # Assert assert True # No exceptions raised def test_publish_with_subscriber_sends_to_handler(self): # Arrange subscriber = [] self.msgbus.subscribe(topic="system", handler=subscriber.append) # Act self.msgbus.publish("system", "hello world") # Assert assert "hello world" in subscriber assert self.msgbus.pub_count == 1 def test_publish_with_multiple_subscribers_sends_to_handlers(self): # Arrange subscriber1 = [] subscriber2 = [] subscriber3 = [] self.msgbus.subscribe(topic="system", handler=subscriber1.append) self.msgbus.subscribe(topic="system", handler=subscriber2.append) self.msgbus.subscribe(topic="system", handler=subscriber3.append) # Act self.msgbus.publish("system", "hello world") # Assert assert "hello world" in subscriber1 assert "hello world" in subscriber2 assert "hello world" in subscriber3 assert self.msgbus.pub_count == 1 def test_publish_with_header_sends_to_handler(self): # Arrange subscriber = [] self.msgbus.subscribe(topic="events.order*", handler=subscriber.append) # Act self.msgbus.publish("events.order.SCALPER-001", "ORDER") # Assert assert "ORDER" in subscriber assert self.msgbus.pub_count == 1 def test_publish_with_none_matching_header_then_filters_from_subscriber( self): # Arrange subscriber = [] self.msgbus.subscribe( topic="events.position*", handler=subscriber.append, ) # Act self.msgbus.publish("events.order*", "ORDER") # Assert assert "ORDER" not in subscriber assert self.msgbus.pub_count == 1 def test_publish_with_matching_subset_header_then_sends_to_subscriber( self): # Arrange subscriber = [] self.msgbus.subscribe( topic="events.order.*", handler=subscriber.append, ) # Act self.msgbus.publish("events.order.S-001", "ORDER") # Assert assert "ORDER" in subscriber assert self.msgbus.pub_count == 1 def test_publish_with_both_channel_and_all_sub_sends_to_subscribers(self): # Arrange subscriber1 = [] subscriber2 = [] self.msgbus.subscribe( topic="MyMessages", handler=subscriber1.append, ) self.msgbus.subscribe( topic="*", # <-- subscribe ALL handler=subscriber2.append, ) # Act self.msgbus.publish("MyMessages", "OK!") # Assert assert "OK!" in subscriber1 assert "OK!" in subscriber2 assert self.msgbus.pub_count == 1
class ExecutionClientTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = TestClock() self.uuid_factory = UUIDFactory() self.logger = TestLogger(self.clock) self.trader_id = TraderId("TESTER", "000") self.account_id = TestStubs.account_id() portfolio = Portfolio( clock=self.clock, logger=self.logger, ) portfolio.register_cache(DataCache(self.logger)) database = BypassExecutionDatabase(trader_id=self.trader_id, logger=self.logger) self.exec_engine = ExecutionEngine( database=database, portfolio=portfolio, clock=self.clock, logger=self.logger, ) self.venue = Venue("SIM") self.client = ExecutionClient( venue=self.venue, account_id=self.account_id, engine=self.exec_engine, clock=self.clock, logger=self.logger, ) self.order_factory = OrderFactory( trader_id=TraderId("TESTER", "000"), strategy_id=StrategyId("S", "001"), clock=TestClock(), ) def test_connect_when_not_implemented_raises_exception(self): self.assertRaises(NotImplementedError, self.client.connect) def test_disconnect_when_not_implemented_raises_exception(self): self.assertRaises(NotImplementedError, self.client.disconnect) def test_reset_when_not_implemented_raises_exception(self): self.assertRaises(NotImplementedError, self.client.reset) def test_dispose_when_not_implemented_raises_exception(self): self.assertRaises(NotImplementedError, self.client.dispose) def test_submit_order_raises_exception(self): order = self.order_factory.limit( AUDUSD_SIM.symbol, OrderSide.SELL, Quantity(100000), Price("1.00000"), ) command = SubmitOrder( self.venue, self.trader_id, self.account_id, StrategyId("SCALPER", "001"), PositionId.null(), order, self.uuid_factory.generate(), self.clock.utc_now(), ) self.assertRaises(NotImplementedError, self.client.submit_order, command) def test_submit_bracket_order_raises_not_implemented_error(self): entry_order = self.order_factory.stop_market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("0.99995"), ) # Act bracket_order = self.order_factory.bracket( entry_order, Price("0.99990"), Price("1.00010"), ) command = SubmitBracketOrder( self.venue, self.trader_id, self.account_id, StrategyId("SCALPER", "001"), bracket_order, self.uuid_factory.generate(), self.clock.utc_now(), ) self.assertRaises(NotImplementedError, self.client.submit_bracket_order, command) def test_amend_order_raises_not_implemented_error(self): # Arrange # Act command = AmendOrder( self.venue, self.trader_id, self.account_id, ClientOrderId("O-123456789"), Quantity(120000), Price("1.00000"), self.uuid_factory.generate(), self.clock.utc_now(), ) # Assert self.assertRaises(NotImplementedError, self.client.amend_order, command) def test_cancel_order_raises_not_implemented_error(self): # Arrange # Act command = CancelOrder( self.venue, self.trader_id, self.account_id, ClientOrderId("O-123456789"), OrderId("001"), self.uuid_factory.generate(), self.clock.utc_now(), ) # Assert self.assertRaises(NotImplementedError, self.client.cancel_order, command) def test_handle_event_sends_to_execution_engine(self): # Arrange order = self.order_factory.market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), ) fill = TestStubs.event_order_filled( order, AUDUSD_SIM, PositionId("P-123456"), StrategyId("S", "001"), Price("1.00001"), ) # Act self.client._handle_event_py(fill) # Accessing protected method # Assert self.assertEqual(1, self.exec_engine.event_count)
class TestLiveRiskEngine: 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.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.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.exec_client = MockExecutionClient( client_id=ClientId("SIM"), venue_type=VenueType.ECN, account_id=TestStubs.account_id(), account_type=AccountType.MARGIN, base_currency=USD, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) # Wire up components self.exec_engine.register_client(self.exec_client) @pytest.mark.asyncio async def test_start_when_loop_not_running_logs(self): # Arrange, Act self.risk_engine.start() # Assert assert True # No exceptions raised self.risk_engine.stop() @pytest.mark.asyncio async def test_get_event_loop_returns_expected_loop(self): # Arrange, Act loop = self.risk_engine.get_event_loop() # Assert assert loop == self.loop @pytest.mark.asyncio async def test_message_qsize_at_max_blocks_on_put_command(self): # Arrange self.msgbus.deregister("RiskEngine.execute", self.risk_engine.execute) self.risk_engine = LiveRiskEngine( loop=self.loop, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, config=LiveRiskEngineConfig(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.risk_engine.execute(submit_order) self.risk_engine.execute(submit_order) await asyncio.sleep(0.1) # Assert assert self.risk_engine.qsize() == 1 assert self.risk_engine.command_count == 0 @pytest.mark.asyncio async def test_message_qsize_at_max_blocks_on_put_event(self): # Arrange self.msgbus.deregister("RiskEngine.execute", self.risk_engine.execute) self.risk_engine = LiveRiskEngine( loop=self.loop, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, config=LiveRiskEngineConfig(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.risk_engine.execute(submit_order) self.risk_engine.process(event) # Add over max size await asyncio.sleep(0.1) # Assert assert self.risk_engine.qsize() == 1 assert self.risk_engine.event_count == 0 @pytest.mark.asyncio async def test_start(self): # Arrange, Act self.risk_engine.start() await asyncio.sleep(0.1) # Assert assert self.risk_engine.is_running # Tear Down self.risk_engine.stop() @pytest.mark.asyncio async def test_kill_when_running_and_no_messages_on_queues(self): # Arrange, Act self.risk_engine.start() await asyncio.sleep(0) self.risk_engine.kill() # Assert assert self.risk_engine.is_stopped @pytest.mark.asyncio async def test_kill_when_not_running_with_messages_on_queue(self): # Arrange, Act self.risk_engine.kill() # Assert assert self.risk_engine.qsize() == 0 @pytest.mark.asyncio async def test_execute_command_places_command_on_queue(self): # Arrange self.risk_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.risk_engine.execute(submit_order) await asyncio.sleep(0.1) # Assert assert self.risk_engine.qsize() == 0 assert self.risk_engine.command_count == 1 # Tear Down self.risk_engine.stop() await self.risk_engine.get_run_queue_task() @pytest.mark.asyncio async def test_handle_position_opening_with_position_id_none(self): # Arrange self.risk_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), ) event = TestStubs.event_order_submitted(order) # Act self.risk_engine.process(event) await asyncio.sleep(0.1) # Assert assert self.risk_engine.qsize() == 0 assert self.risk_engine.event_count == 1 # Tear Down self.risk_engine.stop() await self.risk_engine.get_run_queue_task()
class DataClientTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = TestClock() self.uuid_factory = UUIDFactory() self.logger = TestLogger(self.clock) self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) self.data_engine = DataEngine( portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) self.venue = Venue("SIM") self.client = DataClient( venue=self.venue, engine=self.data_engine, clock=self.clock, logger=self.logger, ) def test_connect_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.connect) def test_disconnect_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.disconnect) def test_reset_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.reset) def test_dispose_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.dispose) def test_subscribe_instrument_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.subscribe_instrument, AUDUSD_SIM.symbol) def test_subscribe_quote_ticks_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.subscribe_quote_ticks, AUDUSD_SIM.symbol) def test_subscribe_trade_ticks_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.subscribe_trade_ticks, AUDUSD_SIM.symbol) def test_subscribe_bars_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.subscribe_bars, TestStubs.bartype_gbpusd_1sec_mid()) def test_unsubscribe_instrument_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.unsubscribe_instrument, AUDUSD_SIM.symbol) def test_unsubscribe_quote_ticks_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.unsubscribe_quote_ticks, AUDUSD_SIM.symbol) def test_unsubscribe_trade_ticks_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.unsubscribe_trade_ticks, AUDUSD_SIM.symbol) def test_unsubscribe_bars_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.unsubscribe_bars, TestStubs.bartype_gbpusd_1sec_mid()) def test_request_instrument_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.request_instrument, None, None) def test_request_instruments_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.request_instruments, None) def test_request_quote_ticks_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.request_quote_ticks, None, None, None, 0, None) def test_request_trade_ticks_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.request_trade_ticks, None, None, None, 0, None) def test_request_bars_when_not_implemented_raises_exception(self): # Arrange # Act # Assert self.assertRaises(NotImplementedError, self.client.request_bars, None, None, None, 0, None) def test_handle_instrument_sends_to_data_engine(self): # Arrange # Act self.client._handle_instrument_py(AUDUSD_SIM) # Assert self.assertEqual(1, self.data_engine.data_count) def test_handle_quote_tick_sends_to_data_engine(self): # Arrange tick = QuoteTick( AUDUSD_SIM.symbol, Price("1.00050"), Price("1.00048"), Quantity(1), Quantity(1), UNIX_EPOCH, ) # Act self.client._handle_quote_tick_py(tick) # Assert self.assertEqual(1, self.data_engine.data_count) def test_handle_trade_tick_sends_to_data_engine(self): # Arrange tick = TradeTick( AUDUSD_SIM.symbol, Price("1.00050"), Quantity(1), OrderSide.BUY, TradeMatchId("123456"), UNIX_EPOCH, ) # Act self.client._handle_trade_tick_py(tick) # Assert self.assertEqual(1, self.data_engine.data_count) def test_handle_bar_sends_to_data_engine(self): # Arrange bar_type = TestStubs.bartype_gbpusd_1sec_mid() bar = Bar( Price("1.00001"), Price("1.00004"), Price("1.00002"), Price("1.00003"), Quantity(100000), UNIX_EPOCH, ) # Act self.client._handle_bar_py(bar_type, bar) # Assert self.assertEqual(1, self.data_engine.data_count) def test_handle_instruments_sends_to_data_engine(self): # Arrange # Act self.client._handle_instruments_py([], self.uuid_factory.generate()) # Assert self.assertEqual(1, self.data_engine.response_count) def test_handle_quote_ticks_sends_to_data_engine(self): # Arrange # Act self.client._handle_quote_ticks_py(AUDUSD_SIM.symbol, [], self.uuid_factory.generate()) # Assert self.assertEqual(1, self.data_engine.response_count) def test_handle_trade_ticks_sends_to_data_engine(self): # Arrange # Act self.client._handle_trade_ticks_py(AUDUSD_SIM.symbol, [], self.uuid_factory.generate()) # Assert self.assertEqual(1, self.data_engine.response_count) def test_handle_bars_sends_to_data_engine(self): # Arrange # Act self.client._handle_bars_py( TestStubs.bartype_gbpusd_1sec_mid(), [], None, self.uuid_factory.generate(), ) # Assert self.assertEqual(1, self.data_engine.response_count)
class ExecutionEngineTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = TestClock() 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=TestClock(), ) self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) self.portfolio.register_cache(DataCache(self.logger)) self.analyzer = PerformanceAnalyzer() database = BypassExecutionDatabase(trader_id=self.trader_id, logger=self.logger) self.exec_engine = ExecutionEngine( database=database, portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) self.cache = self.exec_engine.cache self.exec_engine.process(TestStubs.event_account_state()) 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 test_registered_venues_returns_expected(self): # Arrange # Act result = self.exec_engine.registered_venues # Assert self.assertEqual([Venue("SIM")], result) def test_deregister_client_removes_client(self): # Arrange # Act self.exec_engine.deregister_client(self.exec_client) # Assert self.assertEqual([], self.exec_engine.registered_venues) def test_register_strategy(self): # Arrange strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( self.trader_id, self.clock, self.logger, ) # Act self.exec_engine.register_strategy(strategy) # Assert self.assertIn(strategy.id, self.exec_engine.registered_strategies) def test_deregister_strategy(self): # Arrange strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.exec_engine.register_strategy(strategy) # Act self.exec_engine.deregister_strategy(strategy) # Assert self.assertNotIn(strategy.id, self.exec_engine.registered_strategies) def test_reset_retains_registered_strategies(self): # Arrange strategy = TradingStrategy(order_id_tag="001") strategy.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.exec_engine.register_strategy( strategy) # Also registers with portfolio # Act self.exec_engine.reset() # Assert self.assertIn(strategy.id, self.exec_engine.registered_strategies) def test_integrity_check_calls_check_on_cache(self): # Arrange # Act self.exec_engine.integrity_check() # Assert self.assertTrue(True) # No exceptions raised def test_submit_order(self): # 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( self.venue, 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) # Assert self.assertIn(submit_order, self.exec_client.commands) self.assertTrue(self.cache.order_exists(order.cl_ord_id)) def test_handle_order_fill_event(self): # 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( self.venue, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.utc_now(), ) self.exec_engine.execute(submit_order) # Act self.exec_engine.process(TestStubs.event_order_submitted(order)) self.exec_engine.process(TestStubs.event_order_accepted(order)) self.exec_engine.process( TestStubs.event_order_filled(order, AUDUSD_SIM)) expected_position_id = PositionId( "O-19700101-000000-000-001-1") # Stubbed from order id? # Assert self.assertTrue(self.cache.position_exists(expected_position_id)) self.assertTrue(self.cache.is_position_open(expected_position_id)) self.assertFalse(self.cache.is_position_closed(expected_position_id)) self.assertEqual(Position, type(self.cache.position(expected_position_id))) self.assertIn(expected_position_id, self.cache.position_ids()) self.assertNotIn( expected_position_id, self.cache.position_closed_ids(strategy_id=strategy.id)) self.assertNotIn(expected_position_id, self.cache.position_closed_ids()) self.assertIn(expected_position_id, self.cache.position_open_ids(strategy_id=strategy.id)) self.assertIn(expected_position_id, self.cache.position_open_ids()) self.assertEqual(1, self.cache.positions_total_count()) self.assertEqual(1, self.cache.positions_open_count()) self.assertEqual(0, self.cache.positions_closed_count()) def test_handle_position_opening_with_position_id_none(self): # 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( self.venue, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.utc_now(), ) self.exec_engine.execute(submit_order) # Act self.exec_engine.process(TestStubs.event_order_submitted(order)) self.exec_engine.process(TestStubs.event_order_accepted(order)) self.exec_engine.process( TestStubs.event_order_filled(order, AUDUSD_SIM)) expected_id = PositionId( "O-19700101-000000-000-001-1") # Stubbed from order id # Assert self.assertTrue(self.cache.position_exists(expected_id)) self.assertTrue(self.cache.is_position_open(expected_id)) self.assertFalse(self.cache.is_position_closed(expected_id)) self.assertEqual(Position, type(self.cache.position(expected_id))) self.assertIn(expected_id, self.cache.position_ids()) self.assertNotIn( expected_id, self.cache.position_closed_ids(strategy_id=strategy.id)) self.assertNotIn(expected_id, self.cache.position_closed_ids()) self.assertIn(expected_id, self.cache.position_open_ids(strategy_id=strategy.id)) self.assertIn(expected_id, self.cache.position_open_ids()) self.assertEqual(1, self.cache.positions_total_count()) self.assertEqual(1, self.cache.positions_open_count()) self.assertEqual(0, self.cache.positions_closed_count()) def test_add_to_existing_position_on_order_fill(self): # 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) order1 = strategy.order_factory.market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), ) order2 = strategy.order_factory.market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), ) submit_order1 = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy.id, PositionId.null(), order1, self.uuid_factory.generate(), self.clock.utc_now(), ) self.exec_engine.execute(submit_order1) self.exec_engine.process(TestStubs.event_order_submitted(order1)) self.exec_engine.process(TestStubs.event_order_accepted(order1)) self.exec_engine.process( TestStubs.event_order_filled(order1, AUDUSD_SIM)) expected_position_id = PositionId( "O-19700101-000000-000-001-1") # Stubbed from order id? submit_order2 = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy.id, expected_position_id, order2, self.uuid_factory.generate(), self.clock.utc_now(), ) # Act self.exec_engine.execute(submit_order2) self.exec_engine.process(TestStubs.event_order_submitted(order2)) self.exec_engine.process(TestStubs.event_order_accepted(order2)) self.exec_engine.process( TestStubs.event_order_filled(order2, AUDUSD_SIM, expected_position_id)) # Assert self.assertTrue( self.cache.position_exists( TestStubs.event_order_filled( order1, AUDUSD_SIM, ).position_id)) self.assertTrue(self.cache.is_position_open(expected_position_id)) self.assertFalse(self.cache.is_position_closed(expected_position_id)) self.assertEqual(Position, type(self.cache.position(expected_position_id))) self.assertEqual( 0, len(self.cache.positions_closed(strategy_id=strategy.id))) self.assertEqual(0, len(self.cache.positions_closed())) self.assertEqual( 1, len(self.cache.positions_open(strategy_id=strategy.id))) self.assertEqual(1, len(self.cache.positions_open())) self.assertEqual(1, self.cache.positions_total_count()) self.assertEqual(1, self.cache.positions_open_count()) self.assertEqual(0, self.cache.positions_closed_count()) def test_close_position_on_order_fill(self): # 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) order1 = strategy.order_factory.stop_market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("1.00000"), ) order2 = strategy.order_factory.stop_market( AUDUSD_SIM.symbol, OrderSide.SELL, Quantity(100000), Price("1.00000"), ) submit_order1 = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy.id, PositionId.null(), order1, self.uuid_factory.generate(), self.clock.utc_now(), ) position_id = PositionId("P-1") self.exec_engine.execute(submit_order1) self.exec_engine.process(TestStubs.event_order_submitted(order1)) self.exec_engine.process(TestStubs.event_order_accepted(order1)) self.exec_engine.process( TestStubs.event_order_filled(order1, AUDUSD_SIM, position_id)) submit_order2 = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy.id, position_id, order2, self.uuid_factory.generate(), self.clock.utc_now(), ) # Act self.exec_engine.execute(submit_order2) self.exec_engine.process(TestStubs.event_order_submitted(order2)) self.exec_engine.process(TestStubs.event_order_accepted(order2)) self.exec_engine.process( TestStubs.event_order_filled(order2, AUDUSD_SIM, position_id)) # # Assert self.assertTrue(self.cache.position_exists(position_id)) self.assertFalse(self.cache.is_position_open(position_id)) self.assertTrue(self.cache.is_position_closed(position_id)) self.assertEqual(position_id, self.cache.position(position_id).id) self.assertEqual(position_id, self.cache.positions(strategy_id=strategy.id)[0].id) self.assertEqual(position_id, self.cache.positions()[0].id) self.assertEqual( 0, len(self.cache.positions_open(strategy_id=strategy.id))) self.assertEqual(0, len(self.cache.positions_open())) self.assertEqual( position_id, self.cache.positions_closed(strategy_id=strategy.id)[0].id) self.assertEqual(position_id, self.cache.positions_closed()[0].id) self.assertNotIn(position_id, self.cache.position_open_ids(strategy_id=strategy.id)) self.assertNotIn(position_id, self.cache.position_open_ids()) self.assertEqual(1, self.cache.positions_total_count()) self.assertEqual(0, self.cache.positions_open_count()) self.assertEqual(1, self.cache.positions_closed_count()) def test_multiple_strategy_positions_opened(self): # Arrange self.exec_engine.start() strategy1 = TradingStrategy(order_id_tag="001") strategy1.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) strategy2 = TradingStrategy(order_id_tag="002") strategy2.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.exec_engine.register_strategy(strategy1) self.exec_engine.register_strategy(strategy2) order1 = strategy1.order_factory.stop_market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("1.00000"), ) order2 = strategy2.order_factory.stop_market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("1.00000"), ) submit_order1 = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy1.id, PositionId.null(), order1, self.uuid_factory.generate(), self.clock.utc_now(), ) submit_order2 = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy2.id, PositionId.null(), order2, self.uuid_factory.generate(), self.clock.utc_now(), ) position1_id = PositionId('P-1') position2_id = PositionId('P-2') # Act self.exec_engine.execute(submit_order1) self.exec_engine.execute(submit_order2) self.exec_engine.process(TestStubs.event_order_submitted(order1)) self.exec_engine.process(TestStubs.event_order_accepted(order1)) self.exec_engine.process( TestStubs.event_order_filled(order1, AUDUSD_SIM, position1_id)) self.exec_engine.process(TestStubs.event_order_submitted(order2)) self.exec_engine.process(TestStubs.event_order_accepted(order2)) self.exec_engine.process( TestStubs.event_order_filled(order2, AUDUSD_SIM, position2_id)) # Assert self.assertTrue(self.cache.position_exists(position1_id)) self.assertTrue(self.cache.position_exists(position2_id)) self.assertTrue(self.cache.is_position_open(position1_id)) self.assertTrue(self.cache.is_position_open(position2_id)) self.assertFalse(self.cache.is_position_closed(position1_id)) self.assertFalse(self.cache.is_position_closed(position2_id)) self.assertEqual(Position, type(self.cache.position(position1_id))) self.assertEqual(Position, type(self.cache.position(position2_id))) self.assertIn(position1_id, self.cache.position_ids(strategy_id=strategy1.id)) self.assertIn(position2_id, self.cache.position_ids(strategy_id=strategy2.id)) self.assertIn(position1_id, self.cache.position_ids()) self.assertIn(position2_id, self.cache.position_ids()) self.assertEqual(2, len(self.cache.position_open_ids())) self.assertEqual( 1, len(self.cache.positions_open(strategy_id=strategy1.id))) self.assertEqual( 1, len(self.cache.positions_open(strategy_id=strategy2.id))) self.assertEqual( 1, len(self.cache.positions_open(strategy_id=strategy2.id))) self.assertEqual(2, len(self.cache.positions_open())) self.assertEqual( 1, len(self.cache.positions_open(strategy_id=strategy1.id))) self.assertEqual( 1, len(self.cache.positions_open(strategy_id=strategy2.id))) self.assertIn(position1_id, self.cache.position_open_ids(strategy_id=strategy1.id)) self.assertIn(position2_id, self.cache.position_open_ids(strategy_id=strategy2.id)) self.assertIn(position1_id, self.cache.position_open_ids()) self.assertIn(position2_id, self.cache.position_open_ids()) self.assertNotIn( position1_id, self.cache.position_closed_ids(strategy_id=strategy1.id)) self.assertNotIn( position2_id, self.cache.position_closed_ids(strategy_id=strategy2.id)) self.assertNotIn(position1_id, self.cache.position_closed_ids()) self.assertNotIn(position2_id, self.cache.position_closed_ids()) self.assertEqual(2, self.cache.positions_total_count()) self.assertEqual(2, self.cache.positions_open_count()) self.assertEqual(0, self.cache.positions_closed_count()) def test_multiple_strategy_positions_one_active_one_closed(self): # Arrange self.exec_engine.start() strategy1 = TradingStrategy(order_id_tag="001") strategy1.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) strategy2 = TradingStrategy(order_id_tag="002") strategy2.register_trader( TraderId("TESTER", "000"), self.clock, self.logger, ) self.exec_engine.register_strategy(strategy1) self.exec_engine.register_strategy(strategy2) order1 = strategy1.order_factory.stop_market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("1.00000"), ) order2 = strategy1.order_factory.stop_market( AUDUSD_SIM.symbol, OrderSide.SELL, Quantity(100000), Price("1.00000"), ) order3 = strategy2.order_factory.stop_market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("1.00000"), ) submit_order1 = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy1.id, PositionId.null(), order1, self.uuid_factory.generate(), self.clock.utc_now(), ) position_id1 = PositionId('P-1') submit_order2 = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy1.id, position_id1, order2, self.uuid_factory.generate(), self.clock.utc_now(), ) submit_order3 = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy2.id, PositionId.null(), order3, self.uuid_factory.generate(), self.clock.utc_now(), ) position_id2 = PositionId('P-2') # Act self.exec_engine.execute(submit_order1) self.exec_engine.process(TestStubs.event_order_submitted(order1)) self.exec_engine.process(TestStubs.event_order_accepted(order1)) self.exec_engine.process( TestStubs.event_order_filled(order1, AUDUSD_SIM, position_id1)) self.exec_engine.execute(submit_order2) self.exec_engine.process(TestStubs.event_order_submitted(order2)) self.exec_engine.process(TestStubs.event_order_accepted(order2)) self.exec_engine.process( TestStubs.event_order_filled(order2, AUDUSD_SIM, position_id1)) self.exec_engine.execute(submit_order3) self.exec_engine.process(TestStubs.event_order_submitted(order3)) self.exec_engine.process(TestStubs.event_order_accepted(order3)) self.exec_engine.process( TestStubs.event_order_filled(order3, AUDUSD_SIM, position_id2)) # Assert # Already tested .is_position_active and .is_position_closed above self.assertTrue(self.cache.position_exists(position_id1)) self.assertTrue(self.cache.position_exists(position_id2)) self.assertIn(position_id1, self.cache.position_ids(strategy_id=strategy1.id)) self.assertIn(position_id2, self.cache.position_ids(strategy_id=strategy2.id)) self.assertIn(position_id1, self.cache.position_ids()) self.assertIn(position_id2, self.cache.position_ids()) self.assertEqual( 0, len(self.cache.positions_open(strategy_id=strategy1.id))) self.assertEqual( 1, len(self.cache.positions_open(strategy_id=strategy2.id))) self.assertEqual(1, len(self.cache.positions_open())) self.assertEqual(1, len(self.cache.positions_closed())) self.assertEqual(2, len(self.cache.positions())) self.assertNotIn( position_id1, self.cache.position_open_ids(strategy_id=strategy1.id)) self.assertIn(position_id2, self.cache.position_open_ids(strategy_id=strategy2.id)) self.assertNotIn(position_id1, self.cache.position_open_ids()) self.assertIn(position_id2, self.cache.position_open_ids()) self.assertIn(position_id1, self.cache.position_closed_ids(strategy_id=strategy1.id)) self.assertNotIn( position_id2, self.cache.position_closed_ids(strategy_id=strategy2.id)) self.assertIn(position_id1, self.cache.position_closed_ids()) self.assertNotIn(position_id2, self.cache.position_closed_ids()) self.assertEqual(2, self.cache.positions_total_count()) self.assertEqual(1, self.cache.positions_open_count()) self.assertEqual(1, self.cache.positions_closed_count()) def test_flip_position_on_opposite_filled_same_position_sell(self): # 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) order1 = strategy.order_factory.market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), ) order2 = strategy.order_factory.market( AUDUSD_SIM.symbol, OrderSide.SELL, Quantity(150000), ) submit_order1 = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy.id, PositionId.null(), order1, self.uuid_factory.generate(), self.clock.utc_now(), ) position_id = PositionId("P-000-AUD/USD.SIM-1") self.exec_engine.execute(submit_order1) self.exec_engine.process(TestStubs.event_order_submitted(order1)) self.exec_engine.process(TestStubs.event_order_accepted(order1)) self.exec_engine.process( TestStubs.event_order_filled(order1, AUDUSD_SIM, position_id)) submit_order2 = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy.id, position_id, order2, self.uuid_factory.generate(), self.clock.utc_now(), ) # Act self.exec_engine.execute(submit_order2) self.exec_engine.process(TestStubs.event_order_submitted(order2)) self.exec_engine.process(TestStubs.event_order_accepted(order2)) self.exec_engine.process( TestStubs.event_order_filled(order2, AUDUSD_SIM, position_id)) position_id_flipped = PositionId("P-000-AUD/USD.SIM-1F") # Assert position_flipped = self.cache.position(position_id_flipped) self.assertEqual(-50000, position_flipped.relative_quantity) self.assertTrue(self.cache.position_exists(position_id)) self.assertTrue(self.cache.position_exists(position_id_flipped)) self.assertTrue(self.cache.is_position_closed(position_id)) self.assertTrue(self.cache.is_position_open(position_id_flipped)) self.assertIn(position_id, self.cache.position_ids()) self.assertIn(position_id, self.cache.position_ids(strategy_id=strategy.id)) self.assertIn(position_id_flipped, self.cache.position_ids()) self.assertIn(position_id_flipped, self.cache.position_ids(strategy_id=strategy.id)) self.assertEqual(2, self.cache.positions_total_count()) self.assertEqual(1, self.cache.positions_open_count()) self.assertEqual(1, self.cache.positions_closed_count()) def test_flip_position_on_opposite_filled_same_position_buy(self): # 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) order1 = strategy.order_factory.market( AUDUSD_SIM.symbol, OrderSide.SELL, Quantity(100000), ) order2 = strategy.order_factory.market( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(150000), ) submit_order1 = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy.id, PositionId.null(), order1, self.uuid_factory.generate(), self.clock.utc_now(), ) position_id = PositionId("P-000-AUD/USD.SIM-1") self.exec_engine.execute(submit_order1) self.exec_engine.process(TestStubs.event_order_submitted(order1)) self.exec_engine.process(TestStubs.event_order_accepted(order1)) self.exec_engine.process( TestStubs.event_order_filled(order1, AUDUSD_SIM, position_id)) submit_order2 = SubmitOrder( self.venue, self.trader_id, self.account_id, strategy.id, position_id, order2, self.uuid_factory.generate(), self.clock.utc_now(), ) # Act self.exec_engine.execute(submit_order2) self.exec_engine.process(TestStubs.event_order_submitted(order2)) self.exec_engine.process(TestStubs.event_order_accepted(order2)) self.exec_engine.process( TestStubs.event_order_filled(order2, AUDUSD_SIM, position_id)) position_id_flipped = PositionId("P-000-AUD/USD.SIM-1F") # Assert position_flipped = self.cache.position(position_id_flipped) self.assertEqual(50000, position_flipped.relative_quantity) self.assertTrue(self.cache.position_exists(position_id)) self.assertTrue(self.cache.position_exists(position_id_flipped)) self.assertTrue(self.cache.is_position_closed(position_id)) self.assertTrue(self.cache.is_position_open(position_id_flipped)) self.assertIn(position_id, self.cache.position_ids()) self.assertIn(position_id, self.cache.position_ids(strategy_id=strategy.id)) self.assertIn(position_id_flipped, self.cache.position_ids()) self.assertIn(position_id_flipped, self.cache.position_ids(strategy_id=strategy.id)) self.assertEqual(2, self.cache.positions_total_count()) self.assertEqual(1, self.cache.positions_open_count()) self.assertEqual(1, self.cache.positions_closed_count())
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 TestBetfairExecutionClient: def setup(self): # Fixture Setup self.loop = asyncio.get_event_loop() self.loop.set_debug(True) self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.trader_id = TestStubs.trader_id() self.venue = BETFAIR_VENUE self.account_id = AccountId(self.venue.value, "001") # Setup logging self.logger = LiveLogger(loop=self.loop, clock=self.clock, level_stdout=LogLevel.DEBUG) self._log = LoggerAdapter("TestBetfairExecutionClient", self.logger) self.msgbus = MessageBus( trader_id=self.trader_id, clock=self.clock, logger=self.logger, ) self.cache = TestStubs.cache() self.cache.add_instrument(BetfairTestStubs.betting_instrument()) self.cache.add_account( TestStubs.betting_account(account_id=self.account_id)) self.portfolio = Portfolio( 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.betfair_client: BetfairClient = BetfairTestStubs.betfair_client( loop=self.loop, logger=self.logger) assert self.betfair_client.session_token self.instrument_provider = BetfairTestStubs.instrument_provider( betfair_client=self.betfair_client) self.client = BetfairExecutionClient( loop=asyncio.get_event_loop(), client=self.betfair_client, account_id=self.account_id, base_currency=GBP, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, instrument_provider=self.instrument_provider, market_filter={}, ) self.exec_engine.register_client(self.client) # Re-route exec engine messages through `handler` self.messages = [] def handler(func): def inner(x): self.messages.append(x) return func(x) return inner def listener(x): print(x) self.msgbus.subscribe("*", listener) self.msgbus.deregister(endpoint="ExecEngine.execute", handler=self.exec_engine.execute) self.msgbus.register(endpoint="ExecEngine.execute", handler=handler(self.exec_engine.execute)) self.msgbus.deregister(endpoint="ExecEngine.process", handler=self.exec_engine.process) self.msgbus.register(endpoint="ExecEngine.process", handler=handler(self.exec_engine.process)) self.msgbus.deregister(endpoint="Portfolio.update_account", handler=self.portfolio.update_account) self.msgbus.register(endpoint="Portfolio.update_account", handler=handler(self.portfolio.update_account)) def _prefill_venue_order_id_to_client_order_id(self, update): order_ids = [ update["id"] for market in update.get("oc", []) for order in market.get("orc", []) for update in order.get("uo", []) ] return { VenueOrderId(oid): ClientOrderId(str(i + 1)) for i, oid in enumerate(order_ids) } async def _setup_account(self): await self.client.connection_account_state() def _setup_exec_client_and_cache(self, update): """ Called before processing a test streaming update - ensure all orders are in the cache in `update`. """ venue_order_ids = self._prefill_venue_order_id_to_client_order_id( update) venue_order_id_to_client_order_id = {} for c_id, v_id in enumerate(venue_order_ids): client_order_id = ClientOrderId(str(c_id)) venue_order_id = VenueOrderId(str(v_id)) self._log.debug( f"Adding client_order_id=[{c_id}], venue_order_id=[{v_id}] ") order = BetfairTestStubs.make_accepted_order( venue_order_id=venue_order_id, client_order_id=client_order_id) self._log.debug(f"created order: {order}") venue_order_id_to_client_order_id[v_id] = order.client_order_id cache_order = self.cache.order( client_order_id=order.client_order_id) self._log.debug(f"Cached order: {order}") if cache_order is None: self._log.debug("Adding order to cache") self.cache.add_order(order, position_id=PositionId(v_id.value)) assert self.cache.order( client_order_id).venue_order_id == venue_order_id self.cache.update_order(order) self.client.venue_order_id_to_client_order_id = venue_order_id_to_client_order_id async def _account_state(self): account_details = await self.betfair_client.get_account_details() account_funds = await self.betfair_client.get_account_funds() timestamp = self.clock.timestamp_ns() account_state = betfair_account_to_account_state( account_detail=account_details, account_funds=account_funds, event_id=self.uuid_factory.generate(), ts_event=timestamp, ts_init=timestamp, ) return account_state @pytest.mark.asyncio async def test_submit_order_success(self): # Arrange command = BetfairTestStubs.submit_order_command() mock_betfair_request(self.betfair_client, BetfairResponses.betting_place_order_success()) # Act self.client.submit_order(command) await asyncio.sleep(0) # Assert submitted, accepted = self.messages assert isinstance(submitted, OrderSubmitted) assert isinstance(accepted, OrderAccepted) assert accepted.venue_order_id == VenueOrderId("228302937743") @pytest.mark.asyncio async def test_submit_order_error(self): # Arrange command = BetfairTestStubs.submit_order_command() mock_betfair_request(self.betfair_client, BetfairResponses.betting_place_order_error()) # Act self.client.submit_order(command) await asyncio.sleep(0) # Assert submitted, rejected = self.messages assert isinstance(submitted, OrderSubmitted) assert isinstance(rejected, OrderRejected) assert rejected.reason == "PERMISSION_DENIED: ERROR_IN_ORDER" @pytest.mark.asyncio async def test_modify_order_success(self): # Arrange venue_order_id = VenueOrderId("240808576108") order = BetfairTestStubs.make_accepted_order( venue_order_id=venue_order_id) command = BetfairTestStubs.modify_order_command( instrument_id=order.instrument_id, client_order_id=order.client_order_id, venue_order_id=venue_order_id, ) mock_betfair_request(self.betfair_client, BetfairResponses.betting_replace_orders_success()) # Act self.cache.add_order(order, PositionId("1")) self.client.modify_order(command) await asyncio.sleep(0) # Assert pending_update, updated = self.messages assert isinstance(pending_update, OrderPendingUpdate) assert isinstance(updated, OrderUpdated) assert updated.price == Price.from_str("0.02000") @pytest.mark.asyncio async def test_modify_order_error_order_doesnt_exist(self): # Arrange venue_order_id = VenueOrderId("229435133092") order = BetfairTestStubs.make_accepted_order( venue_order_id=venue_order_id) command = BetfairTestStubs.modify_order_command( instrument_id=order.instrument_id, client_order_id=order.client_order_id, venue_order_id=venue_order_id, ) mock_betfair_request(self.betfair_client, BetfairResponses.betting_replace_orders_success()) # Act self.client.modify_order(command) await asyncio.sleep(0) # Assert pending_update, rejected = self.messages assert isinstance(pending_update, OrderPendingUpdate) assert isinstance(rejected, OrderModifyRejected) assert rejected.reason == "ORDER NOT IN CACHE" @pytest.mark.asyncio async def test_modify_order_error_no_venue_id(self): # Arrange order = BetfairTestStubs.make_submitted_order() self.cache.add_order(order, position_id=BetfairTestStubs.position_id()) command = BetfairTestStubs.modify_order_command( instrument_id=order.instrument_id, client_order_id=order.client_order_id, venue_order_id="", ) mock_betfair_request(self.betfair_client, BetfairResponses.betting_replace_orders_success()) # Act self.client.modify_order(command) await asyncio.sleep(0) # Assert pending_update, rejected = self.messages assert isinstance(pending_update, OrderPendingUpdate) assert isinstance(rejected, OrderModifyRejected) assert rejected.reason == "ORDER MISSING VENUE_ORDER_ID" @pytest.mark.asyncio async def test_cancel_order_success(self): # Arrange order = BetfairTestStubs.make_submitted_order() self.cache.add_order(order, position_id=BetfairTestStubs.position_id()) command = BetfairTestStubs.cancel_order_command( instrument_id=order.instrument_id, client_order_id=order.client_order_id, venue_order_id=VenueOrderId("240564968665"), ) mock_betfair_request(self.betfair_client, BetfairResponses.betting_cancel_orders_success()) # Act self.client.cancel_order(command) await asyncio.sleep(0) # Assert pending_cancel, cancelled = self.messages assert isinstance(pending_cancel, OrderPendingCancel) assert isinstance(cancelled, OrderCanceled) @pytest.mark.asyncio async def test_cancel_order_fail(self): # Arrange order = BetfairTestStubs.make_submitted_order() self.cache.add_order(order, position_id=BetfairTestStubs.position_id()) command = BetfairTestStubs.cancel_order_command( instrument_id=order.instrument_id, client_order_id=order.client_order_id, venue_order_id=VenueOrderId("228302937743"), ) mock_betfair_request(self.betfair_client, BetfairResponses.betting_cancel_orders_error()) # Act self.client.cancel_order(command) await asyncio.sleep(0) # Assert pending_cancel, cancelled = self.messages assert isinstance(pending_cancel, OrderPendingCancel) assert isinstance(cancelled, OrderCancelRejected) @pytest.mark.asyncio async def test_order_multiple_fills(self): # Arrange self.exec_engine.start() client_order_id = ClientOrderId("1") venue_order_id = VenueOrderId("246938411724") submitted = BetfairTestStubs.make_submitted_order( client_order_id=client_order_id, quantity=Quantity.from_int(20)) self.cache.add_order(submitted, position_id=BetfairTestStubs.position_id()) self.client.venue_order_id_to_client_order_id[ venue_order_id] = client_order_id # Act for update in BetfairStreaming.ocm_multiple_fills(): await self.client._handle_order_stream_update(update) await asyncio.sleep(0.1) # Assert result = [fill.last_qty for fill in self.messages] expected = [ Quantity.from_str("16.1900"), Quantity.from_str("0.77"), Quantity.from_str("0.77"), ] assert result == expected @pytest.mark.asyncio async def test_connection_account_state(self): # Arrange, Act, Assert await self.client.connection_account_state() # Assert assert self.cache.account(self.account_id) @pytest.mark.asyncio async def test_check_account_currency(self): # Arrange, Act, Assert await self.client.check_account_currency() @pytest.mark.asyncio async def test_order_stream_full_image(self): # Arrange update = BetfairStreaming.ocm_FULL_IMAGE() await self._setup_account() self._setup_exec_client_and_cache(update=update) # Act await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert assert len(self.messages) == 7 @pytest.mark.asyncio async def test_order_stream_empty_image(self): # Arrange update = BetfairStreaming.ocm_EMPTY_IMAGE() await self._setup_account() self._setup_exec_client_and_cache(update=update) # Act await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert assert len(self.messages) == 1 @pytest.mark.asyncio async def test_order_stream_new_full_image(self): update = BetfairStreaming.ocm_NEW_FULL_IMAGE() await self._setup_account() self._setup_exec_client_and_cache(update=update) await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) assert len(self.messages) == 4 @pytest.mark.asyncio async def test_order_stream_sub_image(self): # Arrange update = BetfairStreaming.ocm_SUB_IMAGE() await self._setup_account() self._setup_exec_client_and_cache(update=update) # Act await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert assert len(self.messages) == 1 @pytest.mark.asyncio async def test_order_stream_update(self): # Arrange update = BetfairStreaming.ocm_UPDATE() await self._setup_account() self._setup_exec_client_and_cache(update=update) # Act await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert assert len(self.messages) == 2 @pytest.mark.asyncio async def test_order_stream_filled(self): # Arrange update = BetfairStreaming.ocm_FILLED() self._setup_exec_client_and_cache(update) await self._setup_account() # Act await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert assert len(self.messages) == 2 assert isinstance(self.messages[1], OrderFilled) assert self.messages[1].last_px == Price.from_str("0.9090909") @pytest.mark.asyncio async def test_order_stream_filled_multiple_prices(self): # Arrange await self._setup_account() update1 = BetfairStreaming.generate_order_update( price="1.50", size=20, side="B", status="E", sm=10, avp="1.60", ) self._setup_exec_client_and_cache(update1) await self.client._handle_order_stream_update(update=update1) await asyncio.sleep(0) order = self.cache.order(client_order_id=ClientOrderId("0")) event = self.messages[-1] order.apply(event) # Act update2 = BetfairStreaming.generate_order_update( price="1.50", size=20, side="B", status="EC", sm=20, avp="1.55", ) self._setup_exec_client_and_cache(update2) await self.client._handle_order_stream_update(update=update2) await asyncio.sleep(0) # Assert assert len(self.messages) == 3 assert isinstance(self.messages[1], OrderFilled) assert isinstance(self.messages[2], OrderFilled) assert self.messages[1].last_px == price_to_probability("1.60") assert self.messages[2].last_px == price_to_probability("1.50") @pytest.mark.asyncio async def test_order_stream_mixed(self): # Arrange update = BetfairStreaming.ocm_MIXED() self._setup_exec_client_and_cache(update) await self._setup_account() # Act await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert _, fill1, fill2, cancel = self.messages assert isinstance( fill1, OrderFilled) and fill1.venue_order_id.value == "229430281341" assert isinstance( fill2, OrderFilled) and fill2.venue_order_id.value == "229430281339" assert isinstance( cancel, OrderCanceled) and cancel.venue_order_id.value == "229430281339" @pytest.mark.asyncio @pytest.mark.skip(reason="Not implemented") async def test_generate_order_status_report(self): # Betfair client login orders = await self.betfair_client.list_current_orders() for order in orders: result = await self.client.generate_order_status_report(order=order ) assert result raise NotImplementedError() @pytest.mark.asyncio @pytest.mark.skip async def test_generate_trades_list(self): patch( "betfairlightweight.endpoints.betting.Betting.list_cleared_orders", return_value=BetfairDataProvider.list_cleared_orders( order_id="226125004209"), ) patch.object( self.client, "venue_order_id_to_client_order_id", {"226125004209": ClientOrderId("1")}, ) result = await generate_trades_list(self=self.client, venue_order_id="226125004209", symbol=None, since=None) assert result @pytest.mark.asyncio async def test_duplicate_execution_id(self): # Arrange await self._setup_account() for update in BetfairStreaming.ocm_DUPLICATE_EXECUTION(): self._setup_exec_client_and_cache(update) # # Load submitted orders # for client_order_id in (ClientOrderId('0'), ClientOrderId('1')): # order = BetfairTestStubs.make_order( # price=Price.from_str("0.5"), quantity=Quantity.from_int(10), client_order_id=client_order_id # ) # command = BetfairTestStubs.submit_order_command(order=order) # self.client.submit_order(command) # await asyncio.sleep(0) # Act for update in BetfairStreaming.ocm_DUPLICATE_EXECUTION(): self._setup_exec_client_and_cache(update=update) await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert _, fill1, cancel, fill2, fill3 = self.messages # First order example, partial fill followed by remainder canceled assert isinstance(fill1, OrderFilled) assert isinstance(cancel, OrderCanceled) # Second order example, partial fill followed by remainder filled assert (isinstance(fill2, OrderFilled) and fill2.execution_id.value == "4721ad7594e7a4a4dffb1bacb0cb45ccdec0747a") assert (isinstance(fill3, OrderFilled) and fill3.execution_id.value == "8b3e65be779968a3fdf2d72731c848c5153e88cd") @pytest.mark.asyncio async def test_betfair_order_reduces_balance(self): # Arrange self.client.stream = MagicMock() self.exec_engine.start() await asyncio.sleep(1) balance = self.cache.account_for_venue(self.venue).balances()[GBP] order = BetfairTestStubs.make_order(price=Price.from_str("0.5"), quantity=Quantity.from_int(10)) self.cache.add_order(order=order, position_id=None) mock_betfair_request(self.betfair_client, BetfairResponses.betting_place_order_success()) command = BetfairTestStubs.submit_order_command(order=order) self.client.submit_order(command) await asyncio.sleep(0.01) # Act balance_order = self.cache.account_for_venue( BETFAIR_VENUE).balances()[GBP] # Cancel the order, balance should return command = BetfairTestStubs.cancel_order_command( client_order_id=order.client_order_id, venue_order_id=order.venue_order_id) mock_betfair_request(self.betfair_client, BetfairResponses.betting_cancel_orders_success()) self.client.cancel_order(command) await asyncio.sleep(0.1) balance_cancel = self.cache.account_for_venue( BETFAIR_VENUE).balances()[GBP] # Assert assert balance.free == Money(1000.0, GBP) assert balance_order.free == Money(990.0, GBP) assert balance_cancel.free == Money(1000.0, GBP) self.exec_engine.kill() await asyncio.sleep(1) @pytest.mark.asyncio async def test_betfair_order_cancelled_no_timestamp(self): update = BetfairStreaming.ocm_error_fill() self._setup_exec_client_and_cache(update) for upd in update["oc"][0]["orc"][0]["uo"]: self.client._handle_stream_execution_complete_order_update( update=upd) await asyncio.sleep(1) @pytest.mark.asyncio @pytest.mark.parametrize( "price,size,side,status,updates", [ ("1.50", "50", "B", "EC", [{ "sm": 50 }]), ("1.50", "50", "B", "E", [{ "sm": 10 }, { "sm": 15 }]), ], ) async def test_various_betfair_order_fill_scenarios( self, price, size, side, status, updates): # Arrange update = BetfairStreaming.ocm_filled_different_price() self._setup_exec_client_and_cache(update) await self._setup_account() # Act for raw in updates: update = BetfairStreaming.generate_order_update(price=price, size=size, side=side, status=status, **raw) await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) # Assert assert len(self.messages) == 1 + len(updates) for msg, raw in zip(self.messages[1:], updates): assert isinstance(msg, OrderFilled) assert msg.last_qty == raw["sm"] @pytest.mark.asyncio async def test_order_filled_avp_update(self): # Arrange update = BetfairStreaming.ocm_filled_different_price() self._setup_exec_client_and_cache(update) await self._setup_account() # Act update = BetfairStreaming.generate_order_update(price="1.50", size=20, side="B", status="E", avp="1.50", sm=10) await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0) update = BetfairStreaming.generate_order_update(price="1.30", size=20, side="B", status="E", avp="1.50", sm=10) await self.client._handle_order_stream_update(update=update) await asyncio.sleep(0)
class TestRiskEngine: def setup(self): # Fixture Setup self.clock = TestClock() self.uuid_factory = UUIDFactory() self.logger = Logger( clock=self.clock, level_stdout=LogLevel.DEBUG, ) self.trader_id = TestIdStubs.trader_id() self.account_id = TestIdStubs.account_id() self.venue = Venue("SIM") 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, ) config = ExecEngineConfig() config.allow_cash_positions = True # Retain original behaviour for now self.exec_engine = ExecutionEngine( msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, config=config, ) self.risk_engine = RiskEngine( portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.exec_client = MockExecutionClient( client_id=ClientId(self.venue.value), venue=self.venue, account_type=AccountType.MARGIN, base_currency=USD, 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) # Prepare data self.cache.add_instrument(AUDUSD_SIM) def test_config_risk_engine(self): # Arrange self.msgbus.deregister("RiskEngine.execute", self.risk_engine.execute) config = RiskEngineConfig( bypass=True, # <-- bypassing pre-trade risk checks for backtest max_order_rate="5/00:00:01", max_notional_per_order={"GBP/USD.SIM": 2_000_000}, ) # Act risk_engine = RiskEngine( portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, config=config, ) # Assert assert risk_engine.max_order_rate() == (5, timedelta(seconds=1)) assert risk_engine.max_notionals_per_order() == {GBPUSD_SIM.id: Decimal("2000000")} assert risk_engine.max_notional_per_order(GBPUSD_SIM.id) == 2_000_000 def test_risk_engine_on_stop(self): # Arrange, Act self.risk_engine.start() self.risk_engine.stop() # Assert assert self.risk_engine.is_stopped def test_process_event_then_handles(self): # Arrange event = Event( event_id=self.uuid_factory.generate(), ts_event=self.clock.timestamp_ns(), ts_init=self.clock.timestamp_ns(), ) # Act self.risk_engine.process(event) # Assert assert self.risk_engine.event_count == 1 def test_trading_state_after_instantiation_returns_active(self): # Arrange, Act result = self.risk_engine.trading_state # Assert assert result == TradingState.ACTIVE def test_set_trading_state_when_no_change_logs_warning(self): # Arrange, Act self.risk_engine.set_trading_state(TradingState.ACTIVE) # Assert assert self.risk_engine.trading_state == TradingState.ACTIVE def test_set_trading_state_changes_value_and_publishes_event(self): # Arrange handler = [] self.msgbus.subscribe(topic="events.risk*", handler=handler.append) # Act self.risk_engine.set_trading_state(TradingState.HALTED) # Assert assert type(handler[0]) == TradingStateChanged assert self.risk_engine.trading_state == TradingState.HALTED def test_max_order_rate_when_no_risk_config_returns_100_per_second(self): # Arrange, Act result = self.risk_engine.max_order_rate() assert result == (100, timedelta(seconds=1)) def test_max_notionals_per_order_when_no_risk_config_returns_empty_dict(self): # Arrange, Act result = self.risk_engine.max_notionals_per_order() assert result == {} def test_max_notional_per_order_when_no_risk_config_returns_none(self): # Arrange, Act result = self.risk_engine.max_notional_per_order(AUDUSD_SIM.id) assert result is None def test_set_max_notional_per_order_changes_setting(self): # Arrange, Act self.risk_engine.set_max_notional_per_order(AUDUSD_SIM.id, 1_000_000) max_notionals = self.risk_engine.max_notionals_per_order() max_notional = self.risk_engine.max_notional_per_order(AUDUSD_SIM.id) # Assert assert max_notionals == {AUDUSD_SIM.id: Decimal("1000000")} assert max_notional == Decimal(1_000_000) def test_given_random_command_then_logs_and_continues(self): # Arrange random = TradingCommand( client_id=None, trader_id=self.trader_id, strategy_id=StrategyId("SCALPER-001"), instrument_id=AUDUSD_SIM.id, command_id=self.uuid_factory.generate(), ts_init=self.clock.timestamp_ns(), ) self.risk_engine.execute(random) def test_given_random_event_then_logs_and_continues(self): # Arrange random = Event( event_id=self.uuid_factory.generate(), ts_event=self.clock.timestamp_ns(), ts_init=self.clock.timestamp_ns(), ) self.risk_engine.process(random) # -- SUBMIT ORDER TESTS ------------------------------------------------------------------------ def test_submit_order_with_default_settings_then_sends_to_client(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.risk_engine.execute(submit_order) # Assert assert self.exec_engine.command_count == 1 assert self.exec_client.calls == ["_start", "submit_order"] def test_submit_order_when_duplicate_id_then_denies(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(), ) self.risk_engine.execute(submit_order) # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_engine.command_count == 1 assert self.exec_client.calls == ["_start", "submit_order"] def test_submit_order_when_risk_bypassed_sends_to_execution_engine(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( trader_id=self.trader_id, strategy_id=strategy.id, position_id=None, order=order, command_id=self.uuid_factory.generate(), ts_init=self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_engine.command_count == 1 # <-- initial account event assert self.exec_client.calls == ["_start", "submit_order"] def test_submit_order_when_position_already_closed_then_denies(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, ) order1 = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) order2 = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.SELL, Quantity.from_int(100000), ) order3 = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order1 = SubmitOrder( trader_id=self.trader_id, strategy_id=strategy.id, position_id=None, order=order1, command_id=self.uuid_factory.generate(), ts_init=self.clock.timestamp_ns(), ) self.risk_engine.execute(submit_order1) self.exec_engine.process(TestEventStubs.order_submitted(order1)) self.exec_engine.process(TestEventStubs.order_accepted(order1)) self.exec_engine.process(TestEventStubs.order_filled(order1, AUDUSD_SIM)) submit_order2 = SubmitOrder( trader_id=self.trader_id, strategy_id=strategy.id, position_id=PositionId("P-19700101-000000-000-000-1"), order=order2, command_id=self.uuid_factory.generate(), ts_init=self.clock.timestamp_ns(), ) self.risk_engine.execute(submit_order2) self.exec_engine.process(TestEventStubs.order_submitted(order2)) self.exec_engine.process(TestEventStubs.order_accepted(order2)) self.exec_engine.process(TestEventStubs.order_filled(order2, AUDUSD_SIM)) submit_order3 = SubmitOrder( trader_id=self.trader_id, strategy_id=strategy.id, position_id=PositionId("P-19700101-000000-000-000-1"), order=order3, command_id=self.uuid_factory.generate(), ts_init=self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_order3) # Assert assert self.exec_engine.command_count == 2 assert self.exec_client.calls == ["_start", "submit_order", "submit_order"] def test_submit_order_when_position_id_not_in_cache_then_denies(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, PositionId("009"), # <-- not in the cache order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_engine.command_count == 0 def test_submit_order_when_instrument_not_in_cache_then_denies(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( GBPUSD_SIM.id, # <-- not in the cache 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.risk_engine.execute(submit_order) # Assert assert self.exec_engine.command_count == 0 # <-- command never reaches engine def test_submit_order_when_invalid_price_precision_then_denies(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("0.9999999999999999"), # <- invalid price ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_engine.command_count == 0 # <-- command never reaches engine def test_submit_order_when_invalid_negative_price_and_not_option_then_denies(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.0"), # <- invalid price ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_engine.command_count == 0 # <-- command never reaches engine def test_submit_order_when_invalid_trigger_price_then_denies(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.stop_limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00000"), Price.from_str("0.999999999999999"), # <- invalid trigger ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_engine.command_count == 0 # <-- command never reaches engine def test_submit_order_when_invalid_quantity_precision_then_denies(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_str("1.111111111111111111"), # <- invalid quantity Price.from_str("1.00000"), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_engine.command_count == 0 # <-- command never reaches engine def test_submit_order_when_invalid_quantity_exceeds_maximum_then_denies(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(1_000_000_000), # <- invalid quantity fat finger! Price.from_str("1.00000"), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_engine.command_count == 0 # <-- command never reaches engine def test_submit_order_when_invalid_quantity_less_than_minimum_then_denies(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(1), # <- invalid quantity Price.from_str("1.00000"), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_engine.command_count == 0 # <-- command never reaches engine def test_submit_order_when_market_order_and_no_market_then_logs_warning(self): # Arrange self.risk_engine.set_max_notional_per_order(AUDUSD_SIM.id, 1_000_000) 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(10000000), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_engine.command_count == 1 # <-- command reaches engine with warning def test_submit_order_when_market_order_and_over_max_notional_then_denies(self): # Arrange self.risk_engine.set_max_notional_per_order(AUDUSD_SIM.id, 1_000_000) # Initialize market quote = TestDataStubs.quote_tick_5decimal(AUDUSD_SIM.id) self.cache.add_quote_tick(quote) 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(10000000), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_engine.command_count == 0 # <-- command never reaches engine def test_submit_order_when_reducing_and_buy_order_adds_then_denies(self): # Arrange self.risk_engine.set_max_notional_per_order(AUDUSD_SIM.id, 1_000_000) # Initialize market quote = TestDataStubs.quote_tick_5decimal(AUDUSD_SIM.id) self.cache.add_quote_tick(quote) 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, ) order1 = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order1 = SubmitOrder( self.trader_id, strategy.id, None, order1, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(submit_order1) self.risk_engine.set_trading_state(TradingState.REDUCING) # <-- allow reducing orders only order2 = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order2 = SubmitOrder( self.trader_id, strategy.id, None, order2, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.exec_engine.process(TestEventStubs.order_submitted(order1)) self.exec_engine.process(TestEventStubs.order_accepted(order1)) self.exec_engine.process(TestEventStubs.order_filled(order1, AUDUSD_SIM)) # Act self.risk_engine.execute(submit_order2) # Assert assert self.portfolio.is_net_long(AUDUSD_SIM.id) assert self.exec_engine.command_count == 1 # <-- command never reaches engine def test_submit_order_when_reducing_and_sell_order_adds_then_denies(self): # Arrange self.risk_engine.set_max_notional_per_order(AUDUSD_SIM.id, 1_000_000) # Initialize market quote = TestDataStubs.quote_tick_5decimal(AUDUSD_SIM.id) self.cache.add_quote_tick(quote) 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, ) order1 = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.SELL, Quantity.from_int(100000), ) submit_order1 = SubmitOrder( self.trader_id, strategy.id, None, order1, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(submit_order1) self.risk_engine.set_trading_state(TradingState.REDUCING) # <-- allow reducing orders only order2 = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.SELL, Quantity.from_int(100000), ) submit_order2 = SubmitOrder( self.trader_id, strategy.id, None, order2, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.exec_engine.process(TestEventStubs.order_submitted(order1)) self.exec_engine.process(TestEventStubs.order_accepted(order1)) self.exec_engine.process(TestEventStubs.order_filled(order1, AUDUSD_SIM)) # Act self.risk_engine.execute(submit_order2) # Assert assert self.portfolio.is_net_short(AUDUSD_SIM.id) assert self.exec_engine.command_count == 1 # <-- command never reaches engine def test_submit_order_when_trading_halted_then_denies_order(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(), ) # Halt trading self.risk_engine.set_trading_state(TradingState.HALTED) # Act self.risk_engine.execute(submit_order) # Assert assert self.risk_engine.command_count == 1 # <-- command never reaches engine def test_submit_order_list_when_trading_halted_then_denies_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, ) entry = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) stop_loss = strategy.order_factory.stop_market( # <-- duplicate AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00000"), ) take_profit = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.10000"), ) bracket = OrderList( list_id=OrderListId("1"), orders=[entry, stop_loss, take_profit], ) submit_bracket = SubmitOrderList( self.trader_id, strategy.id, bracket, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Halt trading self.risk_engine.set_trading_state(TradingState.HALTED) # Act self.risk_engine.execute(submit_bracket) # Assert assert self.risk_engine.command_count == 1 # <-- command never reaches engine # -- SUBMIT BRACKET ORDER TESTS ---------------------------------------------------------------- def test_submit_bracket_with_default_settings_sends_to_client(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, ) bracket = strategy.order_factory.bracket_market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), stop_loss=Price.from_str("1.00000"), take_profit=Price.from_str("1.00010"), ) submit_bracket = SubmitOrderList( self.trader_id, strategy.id, bracket, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_bracket) # Assert assert self.exec_engine.command_count == 1 assert self.exec_client.calls == ["_start", "submit_order_list"] def test_submit_bracket_order_with_duplicate_entry_id_then_denies(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, ) bracket = strategy.order_factory.bracket_market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), stop_loss=Price.from_str("1.00000"), take_profit=Price.from_str("1.00010"), ) submit_bracket = SubmitOrderList( self.trader_id, strategy.id, bracket, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(submit_bracket) # Act self.risk_engine.execute(submit_bracket) # Assert assert self.exec_engine.command_count == 1 # <-- command never reaches engine def test_submit_bracket_order_with_duplicate_stop_loss_id_then_denies(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, ) entry1 = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) stop_loss = strategy.order_factory.stop_market( # <-- duplicate AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00000"), ) take_profit1 = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.10000"), ) entry2 = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) take_profit2 = strategy.order_factory.limit( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.10000"), ) bracket1 = OrderList( list_id=OrderListId("1"), orders=[entry1, stop_loss, take_profit1], ) bracket2 = OrderList( list_id=OrderListId("1"), orders=[entry2, stop_loss, take_profit2], ) submit_bracket1 = SubmitOrderList( self.trader_id, strategy.id, bracket1, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) submit_bracket2 = SubmitOrderList( self.trader_id, strategy.id, bracket2, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(submit_bracket1) # Act self.risk_engine.execute(submit_bracket2) # Assert assert self.exec_engine.command_count == 1 # <-- command never reaches engine def test_submit_bracket_order_with_duplicate_take_profit_id_then_denies(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, ) entry1 = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) stop_loss1 = strategy.order_factory.stop_market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00000"), ) take_profit = strategy.order_factory.limit( # <-- duplicate AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.10000"), ) entry2 = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) stop_loss2 = strategy.order_factory.stop_market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00000"), ) bracket1 = OrderList( list_id=OrderListId("1"), orders=[entry1, stop_loss1, take_profit], ) bracket2 = OrderList( list_id=OrderListId("1"), orders=[entry2, stop_loss2, take_profit], ) submit_bracket1 = SubmitOrderList( self.trader_id, strategy.id, bracket1, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) submit_bracket2 = SubmitOrderList( self.trader_id, strategy.id, bracket2, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(submit_bracket1) # Act self.risk_engine.execute(submit_bracket2) # Assert assert self.exec_engine.command_count == 1 # <-- command never reaches engine def test_submit_bracket_order_when_instrument_not_in_cache_then_denies(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, ) bracket = strategy.order_factory.bracket_market( GBPUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), stop_loss=Price.from_str("1.00000"), take_profit=Price.from_str("1.00010"), ) submit_bracket = SubmitOrderList( self.trader_id, strategy.id, bracket, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_bracket) # Assert assert self.exec_engine.command_count == 0 # <-- command never reaches engine # -- UPDATE ORDER TESTS ------------------------------------------------------------------------ def test_update_order_when_no_order_found_denies(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, ) modify = ModifyOrder( self.trader_id, strategy.id, AUDUSD_SIM.id, ClientOrderId("invalid"), VenueOrderId("1"), Quantity.from_int(100000), Price.from_str("1.00010"), None, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(modify) # Assert assert self.exec_client.calls == ["_start"] assert self.risk_engine.command_count == 1 assert self.exec_engine.command_count == 0 def test_update_order_when_already_closed_then_denies(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.stop_market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00010"), ) submit = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(submit) self.exec_engine.process(TestEventStubs.order_submitted(order)) self.exec_engine.process(TestEventStubs.order_accepted(order)) self.exec_engine.process(TestEventStubs.order_filled(order, AUDUSD_SIM)) modify = ModifyOrder( self.trader_id, strategy.id, order.instrument_id, order.client_order_id, VenueOrderId("1"), order.quantity, Price.from_str("1.00010"), None, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(modify) # Assert assert self.exec_client.calls == ["_start", "submit_order"] assert self.risk_engine.command_count == 2 assert self.exec_engine.command_count == 1 def test_update_order_when_in_flight_then_denies(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.stop_market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00010"), ) submit = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(submit) self.exec_engine.process(TestEventStubs.order_submitted(order)) modify = ModifyOrder( self.trader_id, strategy.id, order.instrument_id, order.client_order_id, VenueOrderId("1"), order.quantity, Price.from_str("1.00010"), None, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(modify) # Assert assert self.exec_client.calls == ["_start", "submit_order"] assert self.risk_engine.command_count == 2 assert self.exec_engine.command_count == 1 def test_modify_order_with_default_settings_then_sends_to_client(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.stop_market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("1.00010"), ) submit = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) modify = ModifyOrder( self.trader_id, strategy.id, order.instrument_id, order.client_order_id, VenueOrderId("1"), order.quantity, Price.from_str("1.00010"), None, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(submit) # Act self.risk_engine.execute(modify) # Assert assert self.exec_client.calls == ["_start", "submit_order", "modify_order"] assert self.risk_engine.command_count == 2 assert self.exec_engine.command_count == 2 # -- CANCEL ORDER TESTS ------------------------------------------------------------------------ def test_cancel_order_when_order_does_not_exist_then_denies(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, ) cancel = CancelOrder( self.trader_id, strategy.id, AUDUSD_SIM.id, ClientOrderId("1"), VenueOrderId("1"), self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(cancel) # Assert assert self.exec_client.calls == ["_start"] assert self.risk_engine.command_count == 1 assert self.exec_engine.command_count == 0 def test_cancel_order_when_already_closed_then_denies(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 = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(submit) self.exec_engine.process(TestEventStubs.order_submitted(order)) self.exec_engine.process(TestEventStubs.order_rejected(order)) cancel = CancelOrder( self.trader_id, strategy.id, order.instrument_id, order.client_order_id, VenueOrderId("1"), self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(cancel) # Assert assert self.exec_client.calls == ["_start", "submit_order"] assert self.risk_engine.command_count == 2 assert self.exec_engine.command_count == 1 def test_cancel_order_when_already_pending_cancel_then_denies(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 = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) cancel = CancelOrder( self.trader_id, strategy.id, order.instrument_id, order.client_order_id, VenueOrderId("1"), self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(submit) self.exec_engine.process(TestEventStubs.order_submitted(order)) self.exec_engine.process(TestEventStubs.order_accepted(order)) self.risk_engine.execute(cancel) self.exec_engine.process(TestEventStubs.order_pending_cancel(order)) # Act self.risk_engine.execute(cancel) # Assert assert self.exec_client.calls == ["_start", "submit_order", "cancel_order"] assert self.risk_engine.command_count == 3 assert self.exec_engine.command_count == 2 def test_cancel_order_with_default_settings_then_sends_to_client(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 = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) cancel = CancelOrder( self.trader_id, strategy.id, order.instrument_id, order.client_order_id, VenueOrderId("1"), self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(submit) # Act self.risk_engine.execute(cancel) # Assert assert self.exec_client.calls == ["_start", "submit_order", "cancel_order"] assert self.risk_engine.command_count == 2 assert self.exec_engine.command_count == 2
class SimulatedExchangeTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = TestClock() self.uuid_factory = UUIDFactory() self.logger = TestLogger(self.clock) self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) self.data_engine = DataEngine( portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={'use_previous_close': False}, # To correctly reproduce historical data bars ) self.data_engine.cache.add_instrument(AUDUSD_SIM) self.data_engine.cache.add_instrument(USDJPY_SIM) self.portfolio.register_cache(self.data_engine.cache) self.analyzer = PerformanceAnalyzer() self.trader_id = TraderId("TESTER", "000") self.account_id = AccountId("SIM", "001") exec_db = BypassExecutionDatabase( trader_id=self.trader_id, logger=self.logger, ) self.exec_engine = ExecutionEngine( database=exec_db, portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) self.exchange = SimulatedExchange( venue=SIM, oms_type=OMSType.HEDGING, generate_position_ids=False, # Will force execution engine to generate ids is_frozen_account=False, starting_balances=[Money(1_000_000, USD)], instruments=[AUDUSD_SIM, USDJPY_SIM], modules=[], fill_model=FillModel(), exec_cache=self.exec_engine.cache, clock=self.clock, logger=self.logger, ) self.exec_client = BacktestExecClient( exchange=self.exchange, account_id=self.account_id, engine=self.exec_engine, clock=self.clock, logger=self.logger, ) self.exec_engine.register_client(self.exec_client) self.exchange.register_client(self.exec_client) self.strategy = MockStrategy(bar_type=TestStubs.bartype_usdjpy_1min_bid()) self.strategy.register_trader( self.trader_id, self.clock, self.logger, ) self.data_engine.register_strategy(self.strategy) self.exec_engine.register_strategy(self.strategy) self.data_engine.start() self.exec_engine.start() self.strategy.start() def test_repr(self): # Arrange # Act # Assert self.assertEqual("SimulatedExchange(SIM)", repr(self.exchange)) def test_check_residuals(self): # Arrange # Act self.exchange.check_residuals() # Assert self.assertTrue(True) # No exceptions raised def test_check_residuals_with_working_and_oco_orders(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) entry1 = self.strategy.order_factory.limit( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("90.000"), ) entry2 = self.strategy.order_factory.limit( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("89.900"), ) bracket1 = self.strategy.order_factory.bracket( entry_order=entry1, stop_loss=Price("89.900"), take_profit=Price("91.000"), ) bracket2 = self.strategy.order_factory.bracket( entry_order=entry2, stop_loss=Price("89.800"), ) self.strategy.submit_bracket_order(bracket1) self.strategy.submit_bracket_order(bracket2) tick2 = QuoteTick( USDJPY_SIM.symbol, Price("89.998"), Price("89.999"), Quantity(100000), Quantity(100000), UNIX_EPOCH, ) self.exchange.process_tick(tick2) # Act self.exchange.check_residuals() # Assert self.assertEqual(3, len(self.exchange.get_working_orders())) self.assertIn(bracket1.stop_loss, self.exchange.get_working_orders().values()) self.assertIn(bracket1.take_profit, self.exchange.get_working_orders().values()) self.assertIn(entry2, self.exchange.get_working_orders().values()) def test_get_working_orders_when_no_orders_returns_empty_dict(self): # Arrange # Act orders = self.exchange.get_working_orders() self.assertEqual({}, orders) def test_submit_order_with_no_market_rejects_order(self): # Arrange order = self.strategy.order_factory.stop_market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("80.000"), ) # Act self.strategy.submit_order(order) # Assert self.assertEqual(2, self.strategy.object_storer.count) self.assertTrue(isinstance(self.strategy.object_storer.get_store()[1], OrderRejected)) def test_submit_order_with_invalid_price_gets_rejected(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.exchange.process_tick(tick) self.portfolio.update_tick(tick) order = self.strategy.order_factory.stop_market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("80.000"), ) # Act self.strategy.submit_order(order) # Assert self.assertEqual(OrderState.REJECTED, order.state) def test_submit_market_order(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) # Create order order = self.strategy.order_factory.market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), ) # Act self.strategy.submit_order(order) # Assert self.assertEqual(OrderState.FILLED, order.state) self.assertEqual(Decimal("90.003"), order.avg_price) def test_submit_limit_order(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) order = self.strategy.order_factory.limit( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("80.000"), ) # Act self.strategy.submit_order(order) # Assert self.assertEqual(1, len(self.exchange.get_working_orders())) self.assertIn(order.cl_ord_id, self.exchange.get_working_orders()) def test_submit_bracket_market_order(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) entry_order = self.strategy.order_factory.market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), ) bracket_order = self.strategy.order_factory.bracket( entry_order, Price("80.000"), ) # Act self.strategy.submit_bracket_order(bracket_order) # Assert self.assertEqual(OrderState.FILLED, entry_order.state) def test_submit_bracket_stop_order(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) entry_order = self.strategy.order_factory.stop_market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("96.710"), ) bracket_order = self.strategy.order_factory.bracket( entry_order, Price("86.000"), Price("97.000"), ) # Act self.strategy.submit_bracket_order(bracket_order) # Assert self.assertEqual(1, len(self.exchange.get_working_orders())) self.assertIn(entry_order.cl_ord_id, self.exchange.get_working_orders()) def test_cancel_stop_order(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) order = self.strategy.order_factory.stop_market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("96.711"), ) self.strategy.submit_order(order) # Act self.strategy.cancel_order(order) # Assert self.assertEqual(0, len(self.exchange.get_working_orders())) def test_cancel_stop_order_when_order_does_not_exist_generates_cancel_reject(self): # Arrange command = CancelOrder( venue=SIM, trader_id=self.trader_id, account_id=self.account_id, cl_ord_id=ClientOrderId("O-123456"), order_id=OrderId("001"), command_id=self.uuid_factory.generate(), command_timestamp=UNIX_EPOCH, ) # Act self.exchange.handle_cancel_order(command) # Assert self.assertEqual(2, self.exec_engine.event_count) def test_modify_stop_order_when_order_does_not_exist(self): # Arrange command = AmendOrder( venue=SIM, trader_id=self.trader_id, account_id=self.account_id, cl_ord_id=ClientOrderId("O-123456"), quantity=Quantity(100000), price=Price("1.00000"), command_id=self.uuid_factory.generate(), command_timestamp=UNIX_EPOCH, ) # Act self.exchange.handle_amend_order(command) # Assert self.assertEqual(2, self.exec_engine.event_count) def test_modify_stop_order(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) order = self.strategy.order_factory.stop_market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("96.711"), ) self.strategy.submit_order(order) # Act self.strategy.amend_order(order, order.quantity, Price("96.714")) # Assert self.assertEqual(1, len(self.exchange.get_working_orders())) self.assertEqual(Price("96.714"), order.price) def test_expire_order(self): # Arrange # Prepare market tick1 = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick1) self.exchange.process_tick(tick1) order = self.strategy.order_factory.stop_market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("96.711"), time_in_force=TimeInForce.GTD, expire_time=UNIX_EPOCH + timedelta(minutes=1), ) self.strategy.submit_order(order) tick2 = QuoteTick( USDJPY_SIM.symbol, Price("96.709"), Price("96.710"), Quantity(100000), Quantity(100000), UNIX_EPOCH + timedelta(minutes=1), ) # Act self.exchange.process_tick(tick2) # Assert self.assertEqual(0, len(self.exchange.get_working_orders())) def test_modify_bracket_order_working_stop_loss(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) entry_order = self.strategy.order_factory.market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), ) bracket_order = self.strategy.order_factory.bracket( entry_order, stop_loss=Price("85.000"), ) self.strategy.submit_bracket_order(bracket_order) # Act self.strategy.amend_order(bracket_order.stop_loss, bracket_order.entry.quantity, Price("85.100")) # Assert self.assertEqual(Price("85.100"), bracket_order.stop_loss.price) def test_submit_market_order_with_slippage_fill_model_slips_order(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) fill_model = FillModel( prob_fill_at_limit=0.0, prob_fill_at_stop=1.0, prob_slippage=1.0, random_seed=None, ) self.exchange.set_fill_model(fill_model) order = self.strategy.order_factory.market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), ) # Act self.strategy.submit_order(order) # Assert self.assertEqual(Decimal("90.004"), order.avg_price) def test_order_fills_gets_commissioned(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) order = self.strategy.order_factory.market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), ) top_up_order = self.strategy.order_factory.market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), ) reduce_order = self.strategy.order_factory.market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(50000), ) # Act self.strategy.submit_order(order) position_id = PositionId("P-19700101-000000-000-001-1") # Generated by platform self.strategy.submit_order(top_up_order, position_id) self.strategy.submit_order(reduce_order, position_id) account_event1 = self.strategy.object_storer.get_store()[2] account_event2 = self.strategy.object_storer.get_store()[6] account_event3 = self.strategy.object_storer.get_store()[10] account = self.exec_engine.cache.account_for_venue(Venue("SIM")) # Assert self.assertEqual(Money(180.01, JPY), account_event1.commission) self.assertEqual(Money(180.01, JPY), account_event2.commission) self.assertEqual(Money(90.00, JPY), account_event3.commission) self.assertTrue(Money(999995.00, USD), account.balance()) def test_process_quote_tick_fills_buy_stop_order(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) order = self.strategy.order_factory.stop_market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("96.711"), ) self.strategy.submit_order(order) # Act tick2 = QuoteTick( AUDUSD_SIM.symbol, # Different market Price("80.010"), Price("80.011"), Quantity(200000), Quantity(200000), UNIX_EPOCH, ) tick3 = QuoteTick( USDJPY_SIM.symbol, Price("96.710"), Price("96.712"), Quantity(100000), Quantity(100000), UNIX_EPOCH, ) self.exchange.process_tick(tick2) self.exchange.process_tick(tick3) # Assert self.assertEqual(0, len(self.exchange.get_working_orders())) self.assertEqual(OrderState.FILLED, order.state) self.assertEqual(Price("96.711"), order.avg_price) def test_process_quote_tick_fills_buy_limit_order(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) order = self.strategy.order_factory.limit( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("90.001"), ) self.strategy.submit_order(order) # Act tick2 = QuoteTick( AUDUSD_SIM.symbol, # Different market Price("80.010"), Price("80.011"), Quantity(200000), Quantity(200000), UNIX_EPOCH, ) tick3 = QuoteTick( USDJPY_SIM.symbol, Price("89.998"), Price("89.999"), Quantity(100000), Quantity(100000), UNIX_EPOCH, ) self.exchange.process_tick(tick2) self.exchange.process_tick(tick3) # Assert self.assertEqual(0, len(self.exchange.get_working_orders())) self.assertEqual(OrderState.FILLED, order.state) self.assertEqual(Price("90.001"), order.avg_price) def test_process_quote_tick_fills_sell_stop_order(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) order = self.strategy.order_factory.stop_market( USDJPY_SIM.symbol, OrderSide.SELL, Quantity(100000), Price("90.000"), ) self.strategy.submit_order(order) # Act tick2 = QuoteTick( USDJPY_SIM.symbol, Price("89.997"), Price("89.999"), Quantity(100000), Quantity(100000), UNIX_EPOCH, ) self.exchange.process_tick(tick2) # Assert self.assertEqual(0, len(self.exchange.get_working_orders())) self.assertEqual(OrderState.FILLED, order.state) self.assertEqual(Price("90.000"), order.avg_price) def test_process_quote_tick_fills_sell_limit_order(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) order = self.strategy.order_factory.limit( USDJPY_SIM.symbol, OrderSide.SELL, Quantity(100000), Price("90.100"), ) self.strategy.submit_order(order) # Act tick2 = QuoteTick( USDJPY_SIM.symbol, Price("90.101"), Price("90.102"), Quantity(100000), Quantity(100000), UNIX_EPOCH, ) self.exchange.process_tick(tick2) # Assert self.assertEqual(0, len(self.exchange.get_working_orders())) self.assertEqual(OrderState.FILLED, order.state) self.assertEqual(Price("90.100"), order.avg_price) def test_process_quote_tick_fills_buy_limit_entry_with_bracket(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) entry = self.strategy.order_factory.limit( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("90.000"), ) bracket = self.strategy.order_factory.bracket( entry_order=entry, stop_loss=Price("89.900"), ) self.strategy.submit_bracket_order(bracket) # Act tick2 = QuoteTick( USDJPY_SIM.symbol, Price("89.998"), Price("89.999"), Quantity(100000), Quantity(100000), UNIX_EPOCH, ) self.exchange.process_tick(tick2) # Assert self.assertEqual(1, len(self.exchange.get_working_orders())) self.assertIn(bracket.stop_loss, self.exchange.get_working_orders().values()) def test_process_quote_tick_fills_sell_limit_entry_with_bracket(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) entry = self.strategy.order_factory.limit( USDJPY_SIM.symbol, OrderSide.SELL, Quantity(100000), Price("91.100"), ) bracket = self.strategy.order_factory.bracket( entry_order=entry, stop_loss=Price("91.200"), take_profit=Price("90.000"), ) self.strategy.submit_bracket_order(bracket) # Act tick2 = QuoteTick( USDJPY_SIM.symbol, Price("91.101"), Price("91.102"), Quantity(100000), Quantity(100000), UNIX_EPOCH, ) self.exchange.process_tick(tick2) # Assert self.assertEqual(2, len(self.exchange.get_working_orders())) # SL and TP self.assertIn(bracket.stop_loss, self.exchange.get_working_orders().values()) self.assertIn(bracket.take_profit, self.exchange.get_working_orders().values()) def test_process_trade_tick_fills_buy_limit_entry_with_bracket(self): # Arrange # Prepare market tick1 = TradeTick( AUDUSD_SIM.symbol, Price("1.00000"), Quantity(100000), OrderSide.SELL, TradeMatchId("123456789"), UNIX_EPOCH, ) tick2 = TradeTick( AUDUSD_SIM.symbol, Price("1.00001"), Quantity(100000), OrderSide.BUY, TradeMatchId("123456790"), UNIX_EPOCH, ) self.data_engine.process(tick1) self.data_engine.process(tick2) self.exchange.process_tick(tick1) self.exchange.process_tick(tick2) entry = self.strategy.order_factory.limit( AUDUSD_SIM.symbol, OrderSide.BUY, Quantity(100000), Price("0.99900"), ) bracket = self.strategy.order_factory.bracket( entry_order=entry, stop_loss=Price("0.99800"), take_profit=Price("1.100"), ) self.strategy.submit_bracket_order(bracket) # Act tick3 = TradeTick( AUDUSD_SIM.symbol, Price("0.99899"), Quantity(100000), OrderSide.BUY, # Lowers ask price TradeMatchId("123456789"), UNIX_EPOCH, ) self.exchange.process_tick(tick3) # Assert self.assertEqual(2, len(self.exchange.get_working_orders())) # SL and TP only self.assertIn(bracket.stop_loss, self.exchange.get_working_orders().values()) self.assertIn(bracket.take_profit, self.exchange.get_working_orders().values()) def test_filling_oco_sell_cancels_other_order(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) entry = self.strategy.order_factory.limit( USDJPY_SIM.symbol, OrderSide.SELL, Quantity(100000), Price("91.100"), ) bracket = self.strategy.order_factory.bracket( entry_order=entry, stop_loss=Price("91.200"), take_profit=Price("90.000"), ) self.strategy.submit_bracket_order(bracket) # Act tick2 = QuoteTick( USDJPY_SIM.symbol, Price("91.101"), Price("91.102"), Quantity(100000), Quantity(100000), UNIX_EPOCH, ) tick3 = QuoteTick( USDJPY_SIM.symbol, Price("91.201"), Price("91.203"), Quantity(100000), Quantity(100000), UNIX_EPOCH, ) self.exchange.process_tick(tick2) self.exchange.process_tick(tick3) # Assert self.assertEqual(0, len(self.exchange.get_working_orders())) def test_realized_pnl_contains_commission(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) order = self.strategy.order_factory.market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), ) # Act self.strategy.submit_order(order) position = self.exec_engine.cache.positions_open()[0] # Assert self.assertEqual(Money(-180.01, JPY), position.realized_pnl) self.assertEqual(Money(180.01, JPY), position.commission) self.assertEqual([Money(180.01, JPY)], position.commissions()) def test_unrealized_pnl(self): # Arrange # Prepare market tick = TestStubs.quote_tick_3decimal(USDJPY_SIM.symbol) self.data_engine.process(tick) self.exchange.process_tick(tick) order_open = self.strategy.order_factory.market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), ) # Act 1 self.strategy.submit_order(order_open) reduce_quote = QuoteTick( USDJPY_SIM.symbol, Price("100.003"), Price("100.003"), Quantity(100000), Quantity(100000), UNIX_EPOCH, ) self.exchange.process_tick(reduce_quote) self.portfolio.update_tick(reduce_quote) order_reduce = self.strategy.order_factory.market( USDJPY_SIM.symbol, OrderSide.SELL, Quantity(50000), ) position_id = PositionId("P-19700101-000000-000-001-1") # Generated by platform # Act 2 self.strategy.submit_order(order_reduce, position_id) # Assert position = self.exec_engine.cache.positions_open()[0] self.assertEqual(Money(500000.00, JPY), position.unrealized_pnl(Price("100.003"))) def test_position_flipped_when_reduce_order_exceeds_original_quantity(self): # Arrange # Prepare market open_quote = QuoteTick( USDJPY_SIM.symbol, Price("90.002"), Price("90.003"), Quantity(1), Quantity(1), UNIX_EPOCH, ) self.data_engine.process(open_quote) self.exchange.process_tick(open_quote) order_open = self.strategy.order_factory.market( USDJPY_SIM.symbol, OrderSide.BUY, Quantity(100000), ) # Act 1 self.strategy.submit_order(order_open) reduce_quote = QuoteTick( USDJPY_SIM.symbol, Price("100.003"), Price("100.003"), Quantity(1), Quantity(1), UNIX_EPOCH, ) self.exchange.process_tick(reduce_quote) self.portfolio.update_tick(reduce_quote) order_reduce = self.strategy.order_factory.market( USDJPY_SIM.symbol, OrderSide.SELL, Quantity(150000), ) # Act 2 self.strategy.submit_order(order_reduce, PositionId("P-19700101-000000-000-001-1")) # Generated by platform # Assert print(self.exec_engine.cache.positions()) position_open = self.exec_engine.cache.positions_open()[0] position_closed = self.exec_engine.cache.positions_closed()[0] self.assertEqual(PositionSide.SHORT, position_open.side) self.assertEqual(Quantity(50000), position_open.quantity) self.assertEqual(Money(999619.98, JPY), position_closed.realized_pnl) self.assertEqual([Money(380.02, JPY)], position_closed.commissions())
class TestBacktestExecClientTests: def setup(self): # Fixture Setup self.clock = TestClock() self.uuid_factory = UUIDFactory() self.logger = Logger(self.clock) self.trader_id = TraderId("TESTER-000") self.account_id = AccountId("BINANCE", "000") self.cache = TestStubs.cache() self.portfolio = Portfolio( cache=self.cache, clock=self.clock, logger=self.logger, ) self.exec_engine = ExecutionEngine( portfolio=self.portfolio, cache=self.cache, clock=self.clock, logger=self.logger, ) self.exchange = SimulatedExchange( venue=Venue("BINANCE"), venue_type=VenueType.EXCHANGE, oms_type=OMSType.NETTING, account_type=AccountType.CASH, base_currency=None, # Multi-currency account starting_balances=[Money(1_000_000, USDT)], is_frozen_account=False, instruments=[ETHUSDT_BINANCE], modules=[], cache=self.exec_engine.cache, fill_model=FillModel(), clock=self.clock, logger=self.logger, ) self.exec_client = BacktestExecClient( exchange=self.exchange, account_id=self.account_id, account_type=AccountType.CASH, base_currency=None, # Multi-currency account engine=self.exec_engine, clock=self.clock, logger=self.logger, ) self.order_factory = OrderFactory( trader_id=self.trader_id, strategy_id=StrategyId("SCALPER-001"), clock=self.clock, ) def test_is_connected_when_not_connected_returns_false(self): # Arrange # Act # Assert assert not self.exec_client.is_connected def test_connect(self): # Arrange # Act self.exec_client.connect() # Assert assert self.exec_client.is_connected def test_disconnect(self): # Arrange self.exec_client.connect() # Act self.exec_client.disconnect() # Assert assert not self.exec_client.is_connected def test_reset(self): # Arrange # Act self.exec_client.reset() # Assert assert not self.exec_client.is_connected def test_dispose(self): # Arrange # Act self.exec_client.dispose() # Assert assert not self.exec_client.is_connected def test_submit_order_when_not_connected_logs_and_does_not_send(self): # Arrange strategy = TradingStrategy("000") order = self.order_factory.market( ETHUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_int(100), ) command = SubmitOrder( self.trader_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.exec_client.submit_order(command) # Assert assert order.state == OrderState.INITIALIZED def test_submit_bracket_order_when_not_connected_logs_and_does_not_send( self): # Arrange strategy = TradingStrategy("000") entry = self.order_factory.market( ETHUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_int(100), ) bracket = self.order_factory.bracket( entry, Price.from_str("500.00000"), Price.from_str("600.00000"), ) command = SubmitBracketOrder( self.trader_id, strategy.id, bracket, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.exec_client.submit_bracket_order(command) # Assert assert entry.state == OrderState.INITIALIZED def test_cancel_order_when_not_connected_logs_and_does_not_send(self): # Arrange order = self.order_factory.market( ETHUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_int(100), ) command = CancelOrder( self.trader_id, self.order_factory.strategy_id, order.instrument_id, order.client_order_id, order.venue_order_id, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.exec_client.cancel_order(command) # Assert assert True # No exceptions raised def test_update_order_when_not_connected_logs_and_does_not_send(self): # Arrange order = self.order_factory.stop_market( ETHUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_int(100), Price.from_str("1000.00"), ) command = UpdateOrder( self.trader_id, order.strategy_id, order.instrument_id, order.client_order_id, order.venue_order_id, Quantity.from_int(100), Price.from_str("1010.00"), None, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.exec_client.update_order(command) # Assert assert True # No exceptions raised
class TestOandaDataClient: def setup(self): # Fixture Setup self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.trader_id = TraderId("TESTER-001") # Fresh isolated loop testing pattern self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.executor = concurrent.futures.ThreadPoolExecutor() self.loop.set_default_executor(self.executor) self.loop.set_debug(True) # Setup logging logger = LiveLogger( loop=self.loop, clock=self.clock, trader_id=self.trader_id, level_stdout=LogLevel.DEBUG, ) self.logger = LiveLogger( loop=self.loop, clock=self.clock, ) self.cache = TestStubs.cache() self.portfolio = Portfolio( cache=self.cache, clock=self.clock, logger=self.logger, ) self.data_engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, cache=self.cache, clock=self.clock, logger=self.logger, ) self.mock_oanda = MagicMock() self.client = OandaDataClient( client=self.mock_oanda, account_id="001", engine=self.data_engine, clock=self.clock, logger=logger, ) self.data_engine.register_client(self.client) with open(TEST_PATH + "instruments.json") as response: instruments = json.load(response) self.mock_oanda.request.return_value = instruments def teardown(self): self.executor.shutdown(wait=True) self.loop.stop() self.loop.close() # TODO: WIP - why is this failing?? # def test_connect(self): # async def run_test(): # # Arrange # # Act # self.data_engine.start() # Also connects client # self.client.connect() # await asyncio.sleep(1) # # # Assert # assert self.client.is_connected # # # Tear Down # self.data_engine.stop() # # self.loop.run_until_complete(run_test()) def test_disconnect(self): # Arrange self.client.connect() # Act self.client.disconnect() # Assert assert not self.client.is_connected def test_reset(self): # Arrange # Act self.client.reset() # Assert assert not self.client.is_connected def test_dispose(self): # Arrange # Act self.client.dispose() # Assert assert not self.client.is_connected def test_subscribe_instrument(self): # Arrange self.client.connect() # Act self.client.subscribe_instrument(AUDUSD) # Assert assert AUDUSD in self.client.subscribed_instruments def test_subscribe_quote_ticks(self): async def run_test(): # Arrange self.mock_oanda.request.return_value = {"type": {"HEARTBEAT": "0"}} self.data_engine.start() # Act self.client.subscribe_quote_ticks(AUDUSD) await asyncio.sleep(0.3) # Assert assert AUDUSD in self.client.subscribed_quote_ticks # Tear Down self.data_engine.stop() self.loop.run_until_complete(run_test()) def test_subscribe_bars(self): # Arrange bar_spec = BarSpecification(1, BarAggregation.MINUTE, PriceType.MID) bar_type = BarType(instrument_id=AUDUSD, bar_spec=bar_spec) # Act self.client.subscribe_bars(bar_type) # Assert assert True def test_unsubscribe_instrument(self): # Arrange self.client.connect() # Act self.client.unsubscribe_instrument(AUDUSD) # Assert assert True def test_unsubscribe_quote_ticks(self): async def run_test(): # Arrange self.mock_oanda.request.return_value = {"type": {"HEARTBEAT": "0"}} self.data_engine.start() self.client.subscribe_quote_ticks(AUDUSD) await asyncio.sleep(0.3) # # Act self.client.unsubscribe_quote_ticks(AUDUSD) await asyncio.sleep(0.3) # Assert assert AUDUSD not in self.client.subscribed_quote_ticks # Tear Down self.data_engine.stop() self.loop.run_until_complete(run_test()) def test_unsubscribe_bars(self): # Arrange bar_spec = BarSpecification(1, BarAggregation.MINUTE, PriceType.MID) bar_type = BarType(instrument_id=AUDUSD, bar_spec=bar_spec) # Act self.client.unsubscribe_bars(bar_type) # Assert assert True def test_request_instrument(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client # Act self.client.request_instrument(AUDUSD, uuid4()) await asyncio.sleep(1) # Assert # Instruments additionally requested on start assert self.data_engine.response_count == 1 # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_request_instruments(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.5) # Act self.client.request_instruments(uuid4()) await asyncio.sleep(1) # Assert # Instruments additionally requested on start assert self.data_engine.response_count == 1 # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_request_bars(self): async def run_test(): # Arrange with open(TEST_PATH + "instruments.json") as response: instruments = json.load(response) # Arrange with open(TEST_PATH + "bars.json") as response: bars = json.load(response) self.mock_oanda.request.side_effect = [instruments, bars] handler = ObjectStorer() self.data_engine.start() await asyncio.sleep(0.3) bar_spec = BarSpecification(1, BarAggregation.MINUTE, PriceType.MID) bar_type = BarType(instrument_id=AUDUSD, bar_spec=bar_spec) request = DataRequest( client_id=ClientId(OANDA.value), data_type=DataType( Bar, metadata={ "bar_type": bar_type, "from_datetime": None, "to_datetime": None, "limit": 1000, }, ), callback=handler.store, request_id=self.uuid_factory.generate(), timestamp_ns=self.clock.timestamp_ns(), ) # Act self.data_engine.send(request) # Allow time for request to be sent, processed and response returned await asyncio.sleep(1) # Assert assert self.data_engine.response_count == 1 assert handler.count == 1 # Final bar incomplete so becomes partial assert len(handler.get_store()[0]) == 99 # Tear Down self.data_engine.stop() self.data_engine.dispose() self.loop.run_until_complete(run_test())
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 TestRiskEngine: def setup(self): # Fixture Setup self.clock = TestClock() self.uuid_factory = UUIDFactory() self.logger = Logger(self.clock) self.trader_id = TraderId("TESTER-000") self.account_id = TestStubs.account_id() self.venue = Venue("SIM") self.portfolio = Portfolio( cache=TestStubs.cache(), clock=self.clock, logger=self.logger, ) self.exec_engine = ExecutionEngine( portfolio=self.portfolio, cache=TestStubs.cache(), clock=self.clock, logger=self.logger, ) self.risk_engine = RiskEngine( exec_engine=self.exec_engine, portfolio=self.portfolio, cache=TestStubs.cache(), clock=self.clock, logger=self.logger, config={}, ) self.exec_client = MockExecutionClient( client_id=ClientId(self.venue.value), venue_type=VenueType.ECN, account_id=self.account_id, account_type=AccountType.MARGIN, base_currency=USD, engine=self.exec_engine, clock=self.clock, logger=self.logger, ) # Wire up components self.exec_engine.register_risk_engine(self.risk_engine) self.exec_engine.register_client(self.exec_client) # Prepare data self.exec_engine.cache.add_instrument(AUDUSD_SIM) def test_set_block_all_orders_changes_flag_value(self): # Arrange # Act self.risk_engine.set_block_all_orders() # Assert assert self.risk_engine.block_all_orders def test_given_random_command_logs_and_continues(self): # Arrange random = TradingCommand( self.trader_id, StrategyId("SCALPER-001"), AUDUSD_SIM.id, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(random) def test_given_random_event_logs_and_continues(self): # Arrange random = Event( self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.exec_engine.process(random) def test_submit_order_with_default_settings_sends_to_client(self): # 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.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order = SubmitOrder( self.trader_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_client.calls == ["connect", "submit_order"] def test_submit_bracket_with_default_settings_sends_to_client(self): # 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) entry = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) bracket = strategy.order_factory.bracket( entry_order=entry, stop_loss=Price.from_str("1.00000"), take_profit=Price.from_str("1.00010"), ) submit_bracket = SubmitBracketOrder( self.trader_id, strategy.id, bracket, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.risk_engine.execute(submit_bracket) # Assert assert self.exec_client.calls == ["connect", "submit_bracket_order"] def test_submit_order_when_block_all_orders_true_then_denies_order(self): # 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.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order = SubmitOrder( self.trader_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.set_block_all_orders() # Act self.risk_engine.execute(submit_order) # Assert assert self.exec_client.calls == ["connect"] assert self.risk_engine.command_count == 1 def test_update_order_with_default_settings_sends_to_client(self): # 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.id, OrderSide.BUY, Quantity.from_int(100000), ) submit = SubmitOrder( self.trader_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) update = UpdateOrder( self.trader_id, strategy.id, order.instrument_id, order.client_order_id, order.venue_order_id, order.quantity, Price.from_str("1.00010"), self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(submit) # Act self.risk_engine.execute(update) # Assert assert self.exec_client.calls == [ "connect", "submit_order", "update_order" ] assert self.risk_engine.command_count == 2 assert self.exec_engine.command_count == 2 def test_cancel_order_with_default_settings_sends_to_client(self): # 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.id, OrderSide.BUY, Quantity.from_int(100000), ) submit = SubmitOrder( self.trader_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) cancel = CancelOrder( self.trader_id, strategy.id, order.instrument_id, order.client_order_id, order.venue_order_id, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.execute(submit) # Act self.risk_engine.execute(cancel) # Assert assert self.exec_client.calls == [ "connect", "submit_order", "cancel_order" ] assert self.risk_engine.command_count == 2 assert self.exec_engine.command_count == 2 def test_submit_bracket_when_block_all_orders_true_then_denies_order(self): # 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) entry = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) bracket = strategy.order_factory.bracket( entry_order=entry, stop_loss=Price.from_str("1.00000"), take_profit=Price.from_str("1.00010"), ) submit_bracket = SubmitBracketOrder( self.trader_id, strategy.id, bracket, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.risk_engine.set_block_all_orders() # Act self.risk_engine.execute(submit_bracket) # Assert assert self.exec_client.calls == ["connect"] assert self.risk_engine.command_count == 1 assert self.exec_engine.event_count == 3
class DataEngineTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = TestClock() self.uuid_factory = UUIDFactory() self.logger = TestLogger(self.clock) self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) self.data_engine = DataEngine( portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) self.portfolio.register_cache(self.data_engine.cache) self.binance_client = BacktestMarketDataClient( instruments=[BTCUSDT_BINANCE, ETHUSDT_BINANCE], name=BINANCE.value, engine=self.data_engine, clock=self.clock, logger=self.logger, ) self.bitmex_client = BacktestMarketDataClient( instruments=[XBTUSD_BITMEX], name=BITMEX.value, engine=self.data_engine, clock=self.clock, logger=self.logger, ) self.quandl = MockMarketDataClient( name="QUANDL", engine=self.data_engine, clock=self.clock, logger=self.logger, ) def test_registered_venues(self): # Arrange # Act # Assert self.assertEqual([], self.data_engine.registered_clients) def test_subscribed_instruments_when_nothing_subscribed_returns_empty_list(self): # Arrange # Act # Assert self.assertEqual([], self.data_engine.subscribed_instruments) def test_subscribed_quote_ticks_when_nothing_subscribed_returns_empty_list(self): # Arrange # Act # Assert self.assertEqual([], self.data_engine.subscribed_quote_ticks) def test_subscribed_trade_ticks_when_nothing_subscribed_returns_empty_list(self): # Arrange # Act # Assert self.assertEqual([], self.data_engine.subscribed_trade_ticks) def test_subscribed_bars_when_nothing_subscribed_returns_empty_list(self): # Arrange # Act # Assert self.assertEqual([], self.data_engine.subscribed_bars) def test_register_client_successfully_adds_client(self): # Arrange # Act self.data_engine.register_client(self.binance_client) # Assert self.assertIn(BINANCE.value, self.data_engine.registered_clients) def test_deregister_client_successfully_removes_client(self): # Arrange self.data_engine.register_client(self.binance_client) # Act self.data_engine.deregister_client(self.binance_client) # Assert self.assertNotIn(BINANCE.value, self.data_engine.registered_clients) def test_register_strategy_successfully_registered_with_strategy(self): # Arrange strategy = TradingStrategy("000") # Act strategy.register_data_engine(self.data_engine) # Assert self.assertEqual(self.data_engine.cache, strategy.data) def test_reset(self): # Arrange # Act self.data_engine.reset() # Assert self.assertEqual(0, self.data_engine.command_count) self.assertEqual(0, self.data_engine.data_count) self.assertEqual(0, self.data_engine.request_count) self.assertEqual(0, self.data_engine.response_count) def test_stop_and_resume(self): # Arrange self.data_engine.start() # Act self.data_engine.stop() self.data_engine.resume() self.data_engine.stop() self.data_engine.reset() # Assert self.assertEqual(0, self.data_engine.command_count) self.assertEqual(0, self.data_engine.data_count) self.assertEqual(0, self.data_engine.request_count) self.assertEqual(0, self.data_engine.response_count) def test_dispose(self): # Arrange self.data_engine.reset() # Act self.data_engine.dispose() # Assert self.assertEqual(0, self.data_engine.command_count) self.assertEqual(0, self.data_engine.data_count) self.assertEqual(0, self.data_engine.request_count) self.assertEqual(0, self.data_engine.response_count) def test_check_connected_when_client_disconnected_returns_false(self): # Arrange self.data_engine.register_client(self.binance_client) self.data_engine.register_client(self.bitmex_client) self.binance_client.disconnect() self.bitmex_client.disconnect() # Act result = self.data_engine.check_connected() # Assert self.assertFalse(result) def test_check_connected_when_client_connected_returns_true(self): # Arrange self.data_engine.register_client(self.binance_client) self.data_engine.register_client(self.bitmex_client) self.binance_client.connect() self.bitmex_client.connect() # Act result = self.data_engine.check_connected() # Assert self.assertTrue(result) def test_check_disconnected_when_client_disconnected_returns_true(self): # Arrange self.data_engine.register_client(self.binance_client) self.data_engine.register_client(self.bitmex_client) # Act result = self.data_engine.check_disconnected() # Assert self.assertTrue(result) def test_check_disconnected_when_client_connected_returns_false(self): # Arrange self.data_engine.register_client(self.binance_client) self.data_engine.register_client(self.bitmex_client) self.binance_client.connect() self.bitmex_client.connect() # Act result = self.data_engine.check_disconnected() # Assert self.assertFalse(result) def test_reset_when_already_disposed_raises_invalid_state_trigger(self): # Arrange self.data_engine.dispose() # Act # Assert self.assertRaises(InvalidStateTrigger, self.data_engine.reset) def test_dispose_when_already_disposed_raises_invalid_state_trigger(self): # Arrange self.data_engine.dispose() # Act # Assert self.assertRaises(InvalidStateTrigger, self.data_engine.dispose) def test_execute_unrecognized_message_logs_and_does_nothing(self): # Arrange self.data_engine.register_client(self.binance_client) # Bogus message command = DataCommand( provider=BINANCE.value, data_type=DataType(str), handler=[].append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(command) # Assert self.assertEqual(1, self.data_engine.command_count) def test_send_request_when_no_data_clients_registered_does_nothing(self): # Arrange handler = [] request = DataRequest( provider="RANDOM", data_type=DataType(QuoteTick, metadata={ "InstrumentId": InstrumentId(Symbol("SOMETHING"), Venue("RANDOM")), "FromDateTime": None, "ToDateTime": None, "Limit": 1000, }), callback=handler.append, request_id=self.uuid_factory.generate(), request_timestamp=self.clock.utc_now(), ) # Act self.data_engine.send(request) # Assert self.assertEqual(1, self.data_engine.request_count) def test_send_data_request_when_data_type_unrecognized_logs_and_does_nothing(self): # Arrange self.data_engine.register_client(self.binance_client) handler = [] request = DataRequest( provider=BINANCE.value, data_type=DataType(str, metadata={ # str data type is invalid "InstrumentId": InstrumentId(Symbol("SOMETHING"), Venue("RANDOM")), "FromDateTime": None, "ToDateTime": None, "Limit": 1000, }), callback=handler.append, request_id=self.uuid_factory.generate(), request_timestamp=self.clock.utc_now(), ) # Act self.data_engine.send(request) # Assert self.assertEqual(1, self.data_engine.request_count) def test_send_data_request_with_duplicate_ids_logs_and_does_not_handle_second(self): # Arrange self.data_engine.register_client(self.binance_client) self.data_engine.start() handler = [] uuid = self.uuid_factory.generate() # We'll use this as a duplicate request1 = DataRequest( provider=BINANCE.value, data_type=DataType(QuoteTick, metadata={ # str data type is invalid "InstrumentId": InstrumentId(Symbol("SOMETHING"), Venue("RANDOM")), "FromDateTime": None, "ToDateTime": None, "Limit": 1000, }), callback=handler.append, request_id=uuid, # Duplicate request_timestamp=self.clock.utc_now(), ) request2 = DataRequest( provider=BINANCE.value, data_type=DataType(QuoteTick, metadata={ # str data type is invalid "InstrumentId": InstrumentId(Symbol("SOMETHING"), Venue("RANDOM")), "FromDateTime": None, "ToDateTime": None, "Limit": 1000, }), callback=handler.append, request_id=uuid, # Duplicate request_timestamp=self.clock.utc_now(), ) # Act self.data_engine.send(request1) self.data_engine.send(request2) # Assert self.assertEqual(2, self.data_engine.request_count) def test_execute_subscribe_when_data_type_unrecognized_logs_and_does_nothing(self): # Arrange self.data_engine.register_client(self.binance_client) subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(str), # str data type is invalid handler=[].append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(subscribe) # Assert self.assertEqual(1, self.data_engine.command_count) def test_execute_subscribe_when_already_subscribed_does_not_add_and_logs(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=[].append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(subscribe) self.data_engine.execute(subscribe) # Assert self.assertEqual(2, self.data_engine.command_count) def test_execute_subscribe_custom_data(self): # Arrange self.data_engine.register_client(self.binance_client) self.data_engine.register_client(self.quandl) self.binance_client.connect() subscribe = Subscribe( provider="QUANDL", data_type=DataType(str, metadata={"Type": "news"}), handler=[].append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(subscribe) # Assert self.assertEqual(1, self.data_engine.command_count) self.assertEqual(["subscribe"], self.quandl.calls) def test_execute_unsubscribe_custom_data(self): # Arrange self.data_engine.register_client(self.binance_client) self.data_engine.register_client(self.quandl) self.binance_client.connect() handler = [] subscribe = Subscribe( provider="QUANDL", data_type=DataType(str, metadata={"Type": "news"}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe) unsubscribe = Unsubscribe( provider="QUANDL", data_type=DataType(str, metadata={"Type": "news"}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(unsubscribe) # Assert self.assertEqual(2, self.data_engine.command_count) self.assertEqual(["subscribe", "unsubscribe"], self.quandl.calls) def test_execute_unsubscribe_when_data_type_unrecognized_logs_and_does_nothing(self): # Arrange self.data_engine.register_client(self.binance_client) handler = [] unsubscribe = Unsubscribe( provider=BINANCE.value, data_type=DataType(type(str)), # str data type is invalid handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(unsubscribe) # Assert self.assertEqual(1, self.data_engine.command_count) def test_execute_unsubscribe_when_not_subscribed_logs_and_does_nothing(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler = [] unsubscribe = Unsubscribe( provider=BINANCE.value, data_type=DataType(type(QuoteTick), metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(unsubscribe) # Assert self.assertEqual(1, self.data_engine.command_count) def test_receive_response_when_no_data_clients_registered_does_nothing(self): # Arrange response = DataResponse( provider=BINANCE.value, data_type=DataType(QuoteTick), data=[], correlation_id=self.uuid_factory.generate(), response_id=self.uuid_factory.generate(), response_timestamp=self.clock.utc_now(), ) # Act self.data_engine.receive(response) # Assert self.assertEqual(1, self.data_engine.response_count) def test_process_unrecognized_data_type_logs_and_does_nothing(self): # Arrange # Act self.data_engine.process("DATA!") # Invalid # Assert self.assertEqual(1, self.data_engine.data_count) def test_process_data_places_data_on_queue(self): # Arrange tick = TestStubs.trade_tick_5decimal() # Act self.data_engine.process(tick) # Assert self.assertEqual(1, self.data_engine.data_count) def test_execute_subscribe_instrument_then_adds_handler(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(Instrument, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=[].append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(subscribe) # Assert self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_instruments) def test_execute_unsubscribe_instrument_then_removes_handler(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler = [] subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(Instrument, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe) unsubscribe = Unsubscribe( provider=BINANCE.value, data_type=DataType(Instrument, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(unsubscribe) # Assert self.assertEqual([], self.data_engine.subscribed_instruments) def test_process_instrument_when_subscriber_then_sends_to_registered_handler(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler = [] subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(Instrument, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe) # Act self.data_engine.process(ETHUSDT_BINANCE) # Assert self.assertEqual([ETHUSDT_BINANCE], handler) def test_process_instrument_when_subscribers_then_sends_to_registered_handlers(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler1 = [] subscribe1 = Subscribe( provider=BINANCE.value, data_type=DataType(Instrument, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler1.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) handler2 = [] subscribe2 = Subscribe( provider=BINANCE.value, data_type=DataType(Instrument, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler2.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe1) self.data_engine.execute(subscribe2) # Act self.data_engine.process(ETHUSDT_BINANCE) # Assert self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_instruments) self.assertEqual([ETHUSDT_BINANCE], handler1) self.assertEqual([ETHUSDT_BINANCE], handler2) def test_execute_subscribe_order_book_stream_then_adds_handler(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(OrderBook, metadata={ "InstrumentId": ETHUSDT_BINANCE.id, "Level": 2, "Depth": 10, "Interval": 0, }), handler=[].append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(subscribe) # Assert self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_order_books) def test_execute_subscribe_order_book_intervals_then_adds_handler(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(OrderBook, metadata={ "InstrumentId": ETHUSDT_BINANCE.id, "Level": 2, "Depth": 25, "Interval": 10, }), handler=[].append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(subscribe) # Assert self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_order_books) def test_execute_unsubscribe_order_book_stream_then_removes_handler(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler = [] subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(OrderBook, metadata={ "InstrumentId": ETHUSDT_BINANCE.id, "Level": 2, "Depth": 25, "Interval": 0, }), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe) unsubscribe = Unsubscribe( provider=BINANCE.value, data_type=DataType(OrderBook, metadata={ "InstrumentId": ETHUSDT_BINANCE.id, "Interval": 0, }), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(unsubscribe) # Assert self.assertEqual([], self.data_engine.subscribed_order_books) def test_execute_unsubscribe_order_book_interval_then_removes_handler(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler = [] subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(OrderBook, metadata={ "InstrumentId": ETHUSDT_BINANCE.id, "Level": 2, "Depth": 25, "Interval": 10, }), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe) unsubscribe = Unsubscribe( provider=BINANCE.value, data_type=DataType(OrderBook, metadata={ "InstrumentId": ETHUSDT_BINANCE.id, "Interval": 10, }), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(unsubscribe) # Assert self.assertEqual([], self.data_engine.subscribed_order_books) def test_process_order_book_when_one_subscriber_then_sends_to_registered_handler(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() order_book = OrderBook( instrument_id=ETHUSDT_BINANCE.id, level=2, depth=25, price_precision=2, size_precision=5, bids=[], asks=[], update_id=0, timestamp=0, ) handler = [] subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(OrderBook, { "InstrumentId": ETHUSDT_BINANCE.id, "Level": 2, "Depth": 25, "Interval": 0, # Streaming }), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe) # Act self.data_engine.process(order_book) # Assert self.assertEqual(order_book, handler[0]) def test_process_order_book_when_multiple_subscribers_then_sends_to_registered_handlers(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() order_book = OrderBook( instrument_id=ETHUSDT_BINANCE.id, level=2, depth=25, price_precision=2, size_precision=5, bids=[], asks=[], update_id=0, timestamp=0, ) handler1 = [] subscribe1 = Subscribe( provider=BINANCE.value, data_type=DataType(OrderBook, { "InstrumentId": ETHUSDT_BINANCE.id, "Level": 2, "Depth": 25, "Interval": 0, # Streaming }), handler=handler1.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) handler2 = [] subscribe2 = Subscribe( provider=BINANCE.value, data_type=DataType(OrderBook, { "InstrumentId": ETHUSDT_BINANCE.id, "Level": 2, "Depth": 25, "Interval": 0, # Streaming }), handler=handler2.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe1) self.data_engine.execute(subscribe2) # Act self.data_engine.process(order_book) # Assert self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_order_books) self.assertEqual(order_book, handler1[0]) self.assertEqual(order_book, handler2[0]) def test_execute_subscribe_for_quote_ticks(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler = [] subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe) # Assert self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_quote_ticks) def test_execute_unsubscribe_for_quote_ticks(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler = [] subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe) unsubscribe = Unsubscribe( provider=BINANCE.value, data_type=DataType(QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(unsubscribe) # Assert self.assertEqual([], self.data_engine.subscribed_quote_ticks) def test_process_quote_tick_when_subscriber_then_sends_to_registered_handler(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler = [] subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe) tick = QuoteTick( ETHUSDT_BINANCE.id, Price("100.003"), Price("100.003"), Quantity(1), Quantity(1), UNIX_EPOCH, ) # Act self.data_engine.process(tick) # Assert self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_quote_ticks) self.assertEqual([tick], handler) def test_process_quote_tick_when_subscribers_then_sends_to_registered_handlers(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler1 = [] subscribe1 = Subscribe( provider=BINANCE.value, data_type=DataType(QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler1.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) handler2 = [] subscribe2 = Subscribe( provider=BINANCE.value, data_type=DataType(QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler2.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe1) self.data_engine.execute(subscribe2) tick = QuoteTick( ETHUSDT_BINANCE.id, Price("100.003"), Price("100.003"), Quantity(1), Quantity(1), UNIX_EPOCH, ) # Act self.data_engine.process(tick) # Assert self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_quote_ticks) self.assertEqual([tick], handler1) self.assertEqual([tick], handler2) def test_subscribe_trade_tick_then_subscribes(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler = [] subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(TradeTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(subscribe) # Assert self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_trade_ticks) def test_unsubscribe_trade_tick_then_unsubscribes(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler = [] subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(TradeTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe) unsubscribe = Unsubscribe( provider=BINANCE.value, data_type=DataType(TradeTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(unsubscribe) # Assert self.assertEqual([], self.data_engine.subscribed_trade_ticks) def test_process_trade_tick_when_subscriber_then_sends_to_registered_handler(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler = [] subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(TradeTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe) tick = TradeTick( ETHUSDT_BINANCE.id, Price("1050.00000"), Quantity(100), OrderSide.BUY, TradeMatchId("123456789"), UNIX_EPOCH, ) # Act self.data_engine.process(tick) # Assert self.assertEqual([tick], handler) def test_process_trade_tick_when_subscribers_then_sends_to_registered_handlers(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() handler1 = [] subscribe1 = Subscribe( provider=BINANCE.value, data_type=DataType(TradeTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler1.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) handler2 = [] subscribe2 = Subscribe( provider=BINANCE.value, data_type=DataType(TradeTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}), handler=handler2.append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe1) self.data_engine.execute(subscribe2) tick = TradeTick( ETHUSDT_BINANCE.id, Price("1050.00000"), Quantity(100), OrderSide.BUY, TradeMatchId("123456789"), UNIX_EPOCH, ) # Act self.data_engine.process(tick) # Assert self.assertEqual([tick], handler1) self.assertEqual([tick], handler2) def test_subscribe_bar_type_then_subscribes(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID) bar_type = BarType(ETHUSDT_BINANCE.id, bar_spec, internal_aggregation=True) handler = ObjectStorer() subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(Bar, metadata={"BarType": bar_type}), handler=handler.store_2, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(subscribe) # Assert self.assertEqual([bar_type], self.data_engine.subscribed_bars) def test_unsubscribe_bar_type_then_unsubscribes(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID) bar_type = BarType(ETHUSDT_BINANCE.id, bar_spec, internal_aggregation=True) handler = ObjectStorer() subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(Bar, metadata={"BarType": bar_type}), handler=handler.store_2, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe) unsubscribe = Unsubscribe( provider=BINANCE.value, data_type=DataType(Bar, metadata={"BarType": bar_type}), handler=handler.store_2, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(unsubscribe) # Assert self.assertEqual([], self.data_engine.subscribed_bars) def test_process_bar_when_subscriber_then_sends_to_registered_handler(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID) bar_type = BarType(ETHUSDT_BINANCE.id, bar_spec, internal_aggregation=True) handler = ObjectStorer() subscribe = Subscribe( provider=BINANCE.value, data_type=DataType(Bar, metadata={"BarType": bar_type}), handler=handler.store_2, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe) bar = Bar( Price("1051.00000"), Price("1055.00000"), Price("1050.00000"), Price("1052.00000"), Quantity(100), UNIX_EPOCH, ) data = BarData(bar_type, bar) # Act self.data_engine.process(data) # Assert self.assertEqual([(bar_type, bar)], handler.get_store()) def test_process_bar_when_subscribers_then_sends_to_registered_handlers(self): # Arrange self.data_engine.register_client(self.binance_client) self.binance_client.connect() bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID) bar_type = BarType(ETHUSDT_BINANCE.id, bar_spec, internal_aggregation=True) handler1 = ObjectStorer() subscribe1 = Subscribe( provider=BINANCE.value, data_type=DataType(Bar, metadata={"BarType": bar_type}), handler=handler1.store_2, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) handler2 = ObjectStorer() subscribe2 = Subscribe( provider=BINANCE.value, data_type=DataType(Bar, metadata={"BarType": bar_type}), handler=handler2.store_2, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) self.data_engine.execute(subscribe1) self.data_engine.execute(subscribe2) bar = Bar( Price("1051.00000"), Price("1055.00000"), Price("1050.00000"), Price("1052.00000"), Quantity(100), UNIX_EPOCH, ) data = BarData(bar_type, bar) # Act self.data_engine.process(data) # Assert self.assertEqual([(bar_type, bar)], handler1.get_store()) self.assertEqual([(bar_type, bar)], handler2.get_store())
class DataMessageTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = TestClock() self.uuid_factory = UUIDFactory() def test_data_command_str_and_repr(self): # Arrange # Act handler = [].append command_id = self.uuid_factory.generate() command = Subscribe( provider=BINANCE.value, data_type=DataType(str, {"type": "newswire"}), # str data type is invalid handler=handler, command_id=command_id, command_timestamp=self.clock.utc_now(), ) # Assert self.assertEqual("Subscribe(<str> {'type': 'newswire'})", str(command)) self.assertEqual( f"Subscribe(" f"provider=BINANCE, " f"data_type=<str> {{'type': 'newswire'}}, " f"handler={repr(handler)}, " f"id={command_id}, " f"timestamp=1970-01-01 00:00:00+00:00)", repr(command), ) def test_data_request_message_str_and_repr(self): # Arrange # Act handler = [].append request_id = self.uuid_factory.generate() request = DataRequest( provider=BINANCE.value, data_type=DataType(str, metadata={ # str data type is invalid "InstrumentId": InstrumentId(Symbol("SOMETHING"), Venue("RANDOM")), "FromDateTime": None, "ToDateTime": None, "Limit": 1000, }), callback=handler, request_id=request_id, request_timestamp=self.clock.utc_now(), ) # Assert self.assertEqual( "DataRequest(" "<str> {'InstrumentId': InstrumentId('SOMETHING.RANDOM'), " "'FromDateTime': None, 'ToDateTime': None, 'Limit': 1000})", str(request), ) self.assertEqual( f"DataRequest(" f"provider=BINANCE, " f"data_type=<str> {{'InstrumentId': InstrumentId('SOMETHING.RANDOM'), " f"'FromDateTime': None, " f"'ToDateTime': None, " f"'Limit': 1000}}, " f"callback={repr(handler)}, " f"id={request_id}, " f"timestamp=1970-01-01 00:00:00+00:00)", repr(request), ) def test_data_response_message_str_and_repr(self): # Arrange # Act correlation_id = self.uuid_factory.generate() response_id = self.uuid_factory.generate() instrument_id = InstrumentId(Symbol("AUD/USD"), IDEALPRO) response = DataResponse( provider=BINANCE.value, data_type=DataType(QuoteTick, metadata={"InstrumentId": instrument_id}), data=[], correlation_id=correlation_id, response_id=response_id, response_timestamp=self.clock.utc_now(), ) # Assert self.assertEqual("DataResponse(<QuoteTick> {'InstrumentId': InstrumentId('AUD/USD.IDEALPRO')})", str(response)) self.assertEqual( f"DataResponse(" f"provider=BINANCE, " f"data_type=<QuoteTick> {{'InstrumentId': InstrumentId('AUD/USD.IDEALPRO')}}, " f"correlation_id={correlation_id}, " f"id={response_id}, " f"timestamp=1970-01-01 00:00:00+00:00)", repr(response), )
class OandaDataClientTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.trader_id = TraderId("TESTER", "001") # Fresh isolated loop testing pattern self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.executor = concurrent.futures.ThreadPoolExecutor() self.loop.set_default_executor(self.executor) self.loop.set_debug(True) # TODO: Development # Setup logging logger = LiveLogger( clock=self.clock, name=self.trader_id.value, level_console=LogLevel.DEBUG, level_file=LogLevel.DEBUG, level_store=LogLevel.WARNING, ) self.logger = LiveLogger(self.clock) self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) self.data_engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) self.mock_oanda = MagicMock() self.client = OandaDataClient( client=self.mock_oanda, account_id="001", engine=self.data_engine, clock=self.clock, logger=logger, ) self.data_engine.register_client(self.client) with open(TEST_PATH + "instruments.json") as response: instruments = json.load(response) self.mock_oanda.request.return_value = instruments def tearDown(self): self.executor.shutdown(wait=True) self.loop.stop() self.loop.close() # TODO: WIP # def test_connect(self): # async def run_test(): # # Arrange # # Act # self.data_engine.start() # Also connects client # await asyncio.sleep(0.3) # # # Assert # self.assertTrue(self.client.is_connected) # # # Tear Down # self.data_engine.stop() # # self.loop.run_until_complete(run_test()) def test_disconnect(self): # Arrange self.client.connect() # Act self.client.disconnect() # Assert self.assertFalse(self.client.is_connected) def test_reset(self): # Arrange # Act self.client.reset() # Assert self.assertFalse(self.client.is_connected) def test_dispose(self): # Arrange # Act self.client.dispose() # Assert self.assertFalse(self.client.is_connected) def test_subscribe_instrument(self): # Arrange self.client.connect() # Act self.client.subscribe_instrument(AUDUSD) # Assert self.assertIn(AUDUSD, self.client.subscribed_instruments) def test_subscribe_quote_ticks(self): async def run_test(): # Arrange self.mock_oanda.request.return_value = {"type": {"HEARTBEAT": "0"}} self.data_engine.start() # Act self.client.subscribe_quote_ticks(AUDUSD) await asyncio.sleep(0.3) # Assert self.assertIn(AUDUSD, self.client.subscribed_quote_ticks) # Tear Down self.data_engine.stop() self.loop.run_until_complete(run_test()) def test_subscribe_bars(self): # Arrange bar_spec = BarSpecification(1, BarAggregation.MINUTE, PriceType.MID) bar_type = BarType(instrument_id=AUDUSD, bar_spec=bar_spec) # Act self.client.subscribe_bars(bar_type) # Assert self.assertTrue(True) def test_unsubscribe_instrument(self): # Arrange self.client.connect() # Act self.client.unsubscribe_instrument(AUDUSD) # Assert self.assertTrue(True) def test_unsubscribe_quote_ticks(self): async def run_test(): # Arrange self.mock_oanda.request.return_value = {"type": {"HEARTBEAT": "0"}} self.data_engine.start() self.client.subscribe_quote_ticks(AUDUSD) await asyncio.sleep(0.3) # # Act self.client.unsubscribe_quote_ticks(AUDUSD) await asyncio.sleep(0.3) # Assert self.assertNotIn(AUDUSD, self.client.subscribed_quote_ticks) # Tear Down self.data_engine.stop() self.loop.run_until_complete(run_test()) def test_unsubscribe_bars(self): # Arrange bar_spec = BarSpecification(1, BarAggregation.MINUTE, PriceType.MID) bar_type = BarType(instrument_id=AUDUSD, bar_spec=bar_spec) # Act self.client.unsubscribe_bars(bar_type) # Assert self.assertTrue(True) def test_request_instrument(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.5) # Act self.client.request_instrument(AUDUSD, uuid4()) await asyncio.sleep(0.5) # Assert # Instruments additionally requested on start self.assertEqual(1, self.data_engine.response_count) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_request_instruments(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.5) # Act self.client.request_instruments(uuid4()) await asyncio.sleep(0.5) # Assert # Instruments additionally requested on start self.assertEqual(1, self.data_engine.response_count) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_request_bars(self): async def run_test(): # Arrange with open(TEST_PATH + "instruments.json") as response: instruments = json.load(response) # Arrange with open(TEST_PATH + "bars.json") as response: bars = json.load(response) self.mock_oanda.request.side_effect = [instruments, bars] handler = ObjectStorer() self.data_engine.start() await asyncio.sleep(0.3) bar_spec = BarSpecification(1, BarAggregation.MINUTE, PriceType.MID) bar_type = BarType(instrument_id=AUDUSD, bar_spec=bar_spec) request = DataRequest( provider=OANDA.value, data_type=DataType(Bar, metadata={ "BarType": bar_type, "FromDateTime": None, "ToDateTime": None, "Limit": 1000, }), callback=handler.store_2, request_id=self.uuid_factory.generate(), request_timestamp=self.clock.utc_now(), ) # Act self.data_engine.send(request) # Allow time for request to be sent, processed and response returned await asyncio.sleep(0.3) # Assert self.assertEqual(1, self.data_engine.response_count) self.assertEqual(1, handler.count) # Final bar incomplete so becomes partial self.assertEqual(99, len(handler.get_store()[0][1])) # Tear Down self.data_engine.stop() self.data_engine.dispose() 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 CCXTDataClientTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.trader_id = TraderId("TESTER", "001") # Fresh isolated loop testing pattern self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) # Setup logging self.logger = LiveLogger( loop=self.loop, clock=self.clock, ) self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) self.data_engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) # Setup mock CCXT exchange with open(TEST_PATH + "markets.json") as response: markets = json.load(response) with open(TEST_PATH + "currencies.json") as response: currencies = json.load(response) with open(TEST_PATH + "watch_order_book.json") as response: order_book = json.load(response) with open(TEST_PATH + "fetch_trades.json") as response: fetch_trades = json.load(response) with open(TEST_PATH + "watch_trades.json") as response: watch_trades = json.load(response) self.mock_ccxt = MagicMock() self.mock_ccxt.name = "Binance" self.mock_ccxt.precisionMode = 2 self.mock_ccxt.markets = markets self.mock_ccxt.currencies = currencies self.mock_ccxt.watch_order_book = order_book self.mock_ccxt.watch_trades = watch_trades self.mock_ccxt.fetch_trades = fetch_trades self.client = CCXTDataClient( client=self.mock_ccxt, engine=self.data_engine, clock=self.clock, logger=self.logger, ) self.data_engine.register_client(self.client) def tearDown(self): self.loop.stop() self.loop.close() def test_connect(self): async def run_test(): # Arrange # Act self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start # Assert self.assertTrue(self.client.is_connected) # Tear down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_disconnect(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start # Act self.client.disconnect() await asyncio.sleep(0.3) # Assert self.assertFalse(self.client.is_connected) # Tear down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_reset_when_not_connected_successfully_resets(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start self.data_engine.stop() await asyncio.sleep(0.3) # Allow engine message queue to stop # Act self.client.reset() # Assert self.assertFalse(self.client.is_connected) self.loop.run_until_complete(run_test()) def test_reset_when_connected_does_not_reset(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start # Act self.client.reset() # Assert self.assertTrue(self.client.is_connected) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_dispose_when_not_connected_does_not_dispose(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start # Act self.client.dispose() # Assert self.assertTrue(self.client.is_connected) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_subscribe_instrument(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start # Act self.client.subscribe_instrument(BTCUSDT) # Assert self.assertIn(BTCUSDT, self.client.subscribed_instruments) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_subscribe_quote_ticks(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start # Act self.client.subscribe_quote_ticks(ETHUSDT) await asyncio.sleep(0.3) # Assert self.assertIn(ETHUSDT, self.client.subscribed_quote_ticks) self.assertTrue(self.data_engine.cache.has_quote_ticks(ETHUSDT)) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_subscribe_trade_ticks(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start # Act self.client.subscribe_trade_ticks(ETHUSDT) await asyncio.sleep(0.3) # Assert self.assertIn(ETHUSDT, self.client.subscribed_trade_ticks) self.assertTrue(self.data_engine.cache.has_trade_ticks(ETHUSDT)) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_subscribe_bars(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.5) # Allow engine message queue to start bar_type = TestStubs.bartype_btcusdt_binance_100tick_last() # Act self.client.subscribe_bars(bar_type) # Assert self.assertIn(bar_type, self.client.subscribed_bars) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_unsubscribe_instrument(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start self.client.subscribe_instrument(BTCUSDT) # Act self.client.unsubscribe_instrument(BTCUSDT) # Assert self.assertNotIn(BTCUSDT, self.client.subscribed_instruments) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_unsubscribe_quote_ticks(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start self.client.subscribe_quote_ticks(ETHUSDT) await asyncio.sleep(0.3) # Act self.client.unsubscribe_quote_ticks(ETHUSDT) # Assert self.assertNotIn(ETHUSDT, self.client.subscribed_quote_ticks) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_unsubscribe_trade_ticks(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start self.client.subscribe_trade_ticks(ETHUSDT) # Act self.client.unsubscribe_trade_ticks(ETHUSDT) # Assert self.assertNotIn(ETHUSDT, self.client.subscribed_trade_ticks) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_unsubscribe_bars(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start bar_type = TestStubs.bartype_btcusdt_binance_100tick_last() self.client.subscribe_bars(bar_type) # Act self.client.unsubscribe_bars(bar_type) # Assert self.assertNotIn(bar_type, self.client.subscribed_bars) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_request_instrument(self): async def run_test(): # Arrange self.data_engine.start() await asyncio.sleep(0.5) # Allow engine message queue to start # Act self.client.request_instrument(BTCUSDT, uuid4()) await asyncio.sleep(0.5) # Assert # Instruments additionally requested on start self.assertEqual(1, self.data_engine.response_count) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_request_instruments(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.5) # Allow engine message queue to start # Act self.client.request_instruments(uuid4()) await asyncio.sleep(0.5) # Assert # Instruments additionally requested on start self.assertEqual(1, self.data_engine.response_count) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_request_quote_ticks(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start # Act self.client.request_quote_ticks(BTCUSDT, None, None, 0, uuid4()) # Assert self.assertTrue(True) # Logs warning # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_request_trade_ticks(self): async def run_test(): # Arrange self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start handler = ObjectStorer() request = DataRequest( client_name=BINANCE.value, data_type=DataType( TradeTick, metadata={ "InstrumentId": ETHUSDT, "FromDateTime": None, "ToDateTime": None, "Limit": 100, }, ), callback=handler.store, request_id=self.uuid_factory.generate(), timestamp_ns=self.clock.timestamp_ns(), ) # Act self.data_engine.send(request) await asyncio.sleep(1) # Assert self.assertEqual(1, self.data_engine.response_count) self.assertEqual(1, handler.count) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test()) def test_request_bars(self): async def run_test(): # Arrange with open(TEST_PATH + "fetch_ohlcv.json") as response: fetch_ohlcv = json.load(response) self.mock_ccxt.fetch_ohlcv = fetch_ohlcv self.data_engine.start() # Also starts client await asyncio.sleep(0.3) # Allow engine message queue to start handler = ObjectStorer() bar_spec = BarSpecification(1, BarAggregation.MINUTE, PriceType.LAST) bar_type = BarType(instrument_id=ETHUSDT, bar_spec=bar_spec) request = DataRequest( client_name=BINANCE.value, data_type=DataType( Bar, metadata={ "BarType": bar_type, "FromDateTime": None, "ToDateTime": None, "Limit": 100, }, ), callback=handler.store, request_id=self.uuid_factory.generate(), timestamp_ns=self.clock.timestamp_ns(), ) # Act self.data_engine.send(request) await asyncio.sleep(0.3) # Assert self.assertEqual(1, self.data_engine.response_count) self.assertEqual(1, handler.count) self.assertEqual(100, len(handler.get_store()[0])) # Tear Down self.data_engine.stop() await self.data_engine.get_run_queue_task() self.loop.run_until_complete(run_test())
class LiveDataEngineTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.logger = Logger(self.clock, level_stdout=LogLevel.DEBUG) self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) # Fresh isolated loop testing pattern self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) 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 self.assertTrue(True) # No exceptions raised self.engine.stop() def test_message_qsize_at_max_blocks_on_put_data_command(self): # Arrange self.engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={"qsize": 1}, ) subscribe = Subscribe( client_id=ClientId(BINANCE.value), data_type=DataType(QuoteTick), handler=[].append, command_id=self.uuid_factory.generate(), timestamp_ns=self.clock.timestamp_ns(), ) # Act self.engine.execute(subscribe) self.engine.execute(subscribe) # Assert self.assertEqual(1, self.engine.message_qsize()) self.assertEqual(0, self.engine.command_count) def test_message_qsize_at_max_blocks_on_send_request(self): # Arrange self.engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={"qsize": 1}, ) handler = [] request = DataRequest( client_id=ClientId("RANDOM"), data_type=DataType( QuoteTick, metadata={ "InstrumentId": InstrumentId(Symbol("SOMETHING"), Venue("RANDOM")), "FromDateTime": None, "ToDateTime": None, "Limit": 1000, }, ), callback=handler.append, request_id=self.uuid_factory.generate(), timestamp_ns=self.clock.timestamp_ns(), ) # Act self.engine.send(request) self.engine.send(request) # Assert self.assertEqual(1, self.engine.message_qsize()) self.assertEqual(0, self.engine.command_count) def test_message_qsize_at_max_blocks_on_receive_response(self): # Arrange self.engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={"qsize": 1}, ) response = DataResponse( client_id=ClientId("BINANCE"), data_type=DataType(QuoteTick), data=[], correlation_id=self.uuid_factory.generate(), response_id=self.uuid_factory.generate(), timestamp_ns=self.clock.timestamp_ns(), ) # Act self.engine.receive(response) self.engine.receive(response) # Add over max size # Assert self.assertEqual(1, self.engine.message_qsize()) self.assertEqual(0, self.engine.command_count) def test_data_qsize_at_max_blocks_on_put_data(self): # Arrange self.engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={"qsize": 1}, ) data = Data(1_000_000_000) # Act self.engine.process(data) self.engine.process(data) # Add over max size # Assert self.assertEqual(1, self.engine.data_qsize()) self.assertEqual(0, self.engine.data_count) def test_get_event_loop_returns_expected_loop(self): # Arrange # Act loop = self.engine.get_event_loop() # Assert self.assertEqual(self.loop, loop) def test_start(self): async def run_test(): # Arrange # Act self.engine.start() await asyncio.sleep(0.1) # Assert self.assertEqual(ComponentState.RUNNING, self.engine.state) # 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 self.assertEqual(ComponentState.STOPPED, self.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.engine.kill() # Assert self.assertEqual(0, self.engine.data_qsize()) self.loop.run_until_complete(run_test()) def test_execute_command_processes_message(self): async def run_test(): # Arrange self.engine.start() subscribe = Subscribe( client_id=ClientId(BINANCE.value), data_type=DataType(QuoteTick), handler=[].append, command_id=self.uuid_factory.generate(), timestamp_ns=self.clock.timestamp_ns(), ) # Act self.engine.execute(subscribe) await asyncio.sleep(0.1) # Assert self.assertEqual(0, self.engine.message_qsize()) self.assertEqual(1, self.engine.command_count) # Tear Down self.engine.stop() self.loop.run_until_complete(run_test()) def test_send_request_processes_message(self): async def run_test(): # Arrange self.engine.start() handler = [] request = DataRequest( client_id=ClientId("RANDOM"), data_type=DataType( QuoteTick, metadata={ "InstrumentId": InstrumentId(Symbol("SOMETHING"), Venue("RANDOM")), "FromDateTime": None, "ToDateTime": None, "Limit": 1000, }, ), callback=handler.append, request_id=self.uuid_factory.generate(), timestamp_ns=self.clock.timestamp_ns(), ) # Act self.engine.send(request) await asyncio.sleep(0.1) # Assert self.assertEqual(0, self.engine.message_qsize()) self.assertEqual(1, self.engine.request_count) # Tear Down self.engine.stop() self.loop.run_until_complete(run_test()) def test_receive_response_processes_message(self): async def run_test(): # Arrange self.engine.start() response = DataResponse( client_id=ClientId("BINANCE"), data_type=DataType(QuoteTick), data=[], correlation_id=self.uuid_factory.generate(), response_id=self.uuid_factory.generate(), timestamp_ns=self.clock.timestamp_ns(), ) # Act self.engine.receive(response) await asyncio.sleep(0.1) # Assert self.assertEqual(0, self.engine.message_qsize()) self.assertEqual(1, self.engine.response_count) # Tear Down self.engine.stop() self.loop.run_until_complete(run_test()) def test_process_data_processes_data(self): async def run_test(): # Arrange self.engine.start() # Act tick = TestStubs.trade_tick_5decimal() # Act self.engine.process(tick) await asyncio.sleep(0.1) # Assert self.assertEqual(0, self.engine.data_qsize()) self.assertEqual(1, self.engine.data_count) # Tear Down self.engine.stop() self.loop.run_until_complete(run_test())
class LiveDataEngineTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.logger = TestLogger(self.clock, level_console=LogLevel.DEBUG) self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) # Fresh isolated loop testing pattern self.loop = asyncio.new_event_loop() asyncio.set_event_loop(self.loop) self.data_engine = LiveDataEngine( loop=self.loop, portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) def tearDown(self): self.data_engine.dispose() self.loop.stop() self.loop.close() def test_message_qsize_at_max_blocks_on_put_data_command(self): self.data_engine = LiveDataEngine(loop=self.loop, portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={"qsize": 1}) subscribe = Subscribe( venue=BINANCE, data_type=QuoteTick, metadata={}, handler=[].append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(subscribe) self.data_engine.execute(subscribe) # Assert self.assertEqual(1, self.data_engine.message_qsize()) self.assertEqual(0, self.data_engine.command_count) def test_message_qsize_at_max_blocks_on_send_request(self): self.data_engine = LiveDataEngine(loop=self.loop, portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={"qsize": 1}) handler = [] request = DataRequest( venue=Venue("RANDOM"), data_type=QuoteTick, metadata={ "Symbol": Symbol("SOMETHING", Venue("RANDOM")), "FromDateTime": None, "ToDateTime": None, "Limit": 1000, }, callback=handler.append, request_id=self.uuid_factory.generate(), request_timestamp=self.clock.utc_now(), ) # Act self.data_engine.send(request) self.data_engine.send(request) # Assert self.assertEqual(1, self.data_engine.message_qsize()) self.assertEqual(0, self.data_engine.command_count) def test_message_qsize_at_max_blocks_on_receive_response(self): self.data_engine = LiveDataEngine(loop=self.loop, portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={"qsize": 1}) response = DataResponse( venue=Venue("BINANCE"), data_type=QuoteTick, metadata={}, data=[], correlation_id=self.uuid_factory.generate(), response_id=self.uuid_factory.generate(), response_timestamp=self.clock.utc_now(), ) # Act self.data_engine.receive(response) self.data_engine.receive(response) # Assert self.assertEqual(1, self.data_engine.message_qsize()) self.assertEqual(0, self.data_engine.command_count) def test_data_qsize_at_max_blocks_on_put_data(self): self.data_engine = LiveDataEngine(loop=self.loop, portfolio=self.portfolio, clock=self.clock, logger=self.logger, config={"qsize": 1}) # Act self.data_engine.process("some_data") self.data_engine.process("some_data") # Assert self.assertEqual(1, self.data_engine.data_qsize()) self.assertEqual(0, self.data_engine.data_count) def test_get_event_loop_returns_expected_loop(self): # Arrange # Act loop = self.data_engine.get_event_loop() # Assert self.assertEqual(self.loop, loop) def test_start(self): async def run_test(): # Arrange # Act self.data_engine.start() await asyncio.sleep(0.1) # Assert self.assertEqual(ComponentState.RUNNING, self.data_engine.state) # Tear Down self.data_engine.stop() self.loop.run_until_complete(run_test()) def test_kill(self): async def run_test(): # Arrange # Act self.data_engine.start() await asyncio.sleep(0) self.data_engine.kill() # Assert self.assertEqual(ComponentState.STOPPED, self.data_engine.state) self.loop.run_until_complete(run_test()) def test_execute_command_processes_message(self): async def run_test(): # Arrange self.data_engine.start() subscribe = Subscribe( venue=BINANCE, data_type=QuoteTick, metadata={}, handler=[].append, command_id=self.uuid_factory.generate(), command_timestamp=self.clock.utc_now(), ) # Act self.data_engine.execute(subscribe) await asyncio.sleep(0.1) # Assert self.assertEqual(0, self.data_engine.message_qsize()) self.assertEqual(1, self.data_engine.command_count) # Tear Down self.data_engine.stop() self.loop.run_until_complete(run_test()) def test_send_request_processes_message(self): async def run_test(): # Arrange self.data_engine.start() handler = [] request = DataRequest( venue=Venue("RANDOM"), data_type=QuoteTick, metadata={ "Symbol": Symbol("SOMETHING", Venue("RANDOM")), "FromDateTime": None, "ToDateTime": None, "Limit": 1000, }, callback=handler.append, request_id=self.uuid_factory.generate(), request_timestamp=self.clock.utc_now(), ) # Act self.data_engine.send(request) await asyncio.sleep(0.1) # Assert self.assertEqual(0, self.data_engine.message_qsize()) self.assertEqual(1, self.data_engine.request_count) # Tear Down self.data_engine.stop() self.loop.run_until_complete(run_test()) def test_receive_response_processes_message(self): async def run_test(): # Arrange self.data_engine.start() response = DataResponse( venue=Venue("BINANCE"), data_type=QuoteTick, metadata={}, data=[], correlation_id=self.uuid_factory.generate(), response_id=self.uuid_factory.generate(), response_timestamp=self.clock.utc_now(), ) # Act self.data_engine.receive(response) await asyncio.sleep(0.1) # Assert self.assertEqual(0, self.data_engine.message_qsize()) self.assertEqual(1, self.data_engine.response_count) # Tear Down self.data_engine.stop() self.loop.run_until_complete(run_test()) def test_process_data_processes_data(self): async def run_test(): # Arrange self.data_engine.start() # Act tick = TestStubs.trade_tick_5decimal() # Act self.data_engine.process(tick) await asyncio.sleep(0.1) # Assert self.assertEqual(0, self.data_engine.data_qsize()) self.assertEqual(1, self.data_engine.data_count) # Tear Down self.data_engine.stop() self.loop.run_until_complete(run_test())
class TradingNode: """ Provides an asynchronous network node for live trading. """ def __init__( self, strategies: List[TradingStrategy], config: Dict[str, object], ): """ Initialize a new instance of the TradingNode class. Parameters ---------- strategies : list[TradingStrategy] The list of strategies to run on the trading node. config : dict[str, object] The configuration for the trading node. Raises ------ ValueError If strategies is None or empty. ValueError If config is None or empty. """ PyCondition.not_none(strategies, "strategies") PyCondition.not_none(config, "config") PyCondition.not_empty(strategies, "strategies") PyCondition.not_empty(config, "config") self._config = config # Extract configs config_trader = config.get("trader", {}) config_system = config.get("system", {}) config_log = config.get("logging", {}) config_exec_db = config.get("exec_database", {}) config_risk = config.get("risk", {}) config_strategy = config.get("strategy", {}) # System config self._connection_timeout = config_system.get("connection_timeout", 5.0) self._disconnection_timeout = config_system.get( "disconnection_timeout", 5.0) self._check_residuals_delay = config_system.get( "check_residuals_delay", 5.0) self._load_strategy_state = config_strategy.get("load_state", True) self._save_strategy_state = config_strategy.get("save_state", True) # Setup loop self._loop = asyncio.get_event_loop() self._executor = concurrent.futures.ThreadPoolExecutor() self._loop.set_default_executor(self._executor) self._loop.set_debug(config_system.get("loop_debug", False)) # Components self._clock = LiveClock(loop=self._loop) self._uuid_factory = UUIDFactory() self.system_id = self._uuid_factory.generate() self.created_time = self._clock.utc_now() self._is_running = False # Setup identifiers self.trader_id = TraderId( name=config_trader["name"], tag=config_trader["id_tag"], ) # Setup logging level_stdout = LogLevelParser.from_str_py( config_log.get("level_stdout")) self._logger = LiveLogger( loop=self._loop, clock=self._clock, trader_id=self.trader_id, system_id=self.system_id, level_stdout=level_stdout, ) self._log = LoggerAdapter( component=self.__class__.__name__, logger=self._logger, ) self._log_header() self._log.info("Building...") if platform.system() != "Windows": # Requires the logger to be initialized # Windows does not support signal handling # https://stackoverflow.com/questions/45987985/asyncio-loops-add-signal-handler-in-windows self._setup_loop() # Build platform # ---------------------------------------------------------------------- self.portfolio = Portfolio( clock=self._clock, logger=self._logger, ) self._data_engine = LiveDataEngine( loop=self._loop, portfolio=self.portfolio, clock=self._clock, logger=self._logger, config={"qsize": 10000}, ) self.portfolio.register_cache(self._data_engine.cache) self.analyzer = PerformanceAnalyzer() if config_exec_db["type"] == "redis": exec_db = RedisExecutionDatabase( trader_id=self.trader_id, logger=self._logger, command_serializer=MsgPackCommandSerializer(), event_serializer=MsgPackEventSerializer(), config={ "host": config_exec_db["host"], "port": config_exec_db["port"], }, ) else: exec_db = BypassExecutionDatabase( trader_id=self.trader_id, logger=self._logger, ) self._exec_engine = LiveExecutionEngine( loop=self._loop, database=exec_db, portfolio=self.portfolio, clock=self._clock, logger=self._logger, config={"qsize": 10000}, ) self._risk_engine = LiveRiskEngine( loop=self._loop, exec_engine=self._exec_engine, portfolio=self.portfolio, clock=self._clock, logger=self._logger, config=config_risk, ) self._exec_engine.load_cache() self._exec_engine.register_risk_engine(self._risk_engine) self.trader = Trader( trader_id=self.trader_id, strategies=strategies, portfolio=self.portfolio, data_engine=self._data_engine, exec_engine=self._exec_engine, risk_engine=self._risk_engine, clock=self._clock, logger=self._logger, ) if self._load_strategy_state: self.trader.load() self._builder = TradingNodeBuilder( data_engine=self._data_engine, exec_engine=self._exec_engine, risk_engine=self._risk_engine, clock=self._clock, logger=self._logger, log=self._log, ) self._log.info("state=INITIALIZED.") self.time_to_initialize = self._clock.delta(self.created_time) self._log.info( f"Initialized in {self.time_to_initialize.total_seconds():.3f}s.") self._is_built = False @property def is_running(self) -> bool: """ If the trading node is running. Returns ------- bool True if running, else False. """ return self._is_running @property def is_built(self) -> bool: """ If the trading node clients are built. Returns ------- bool True if built, else False. """ return self._is_built def get_event_loop(self) -> asyncio.AbstractEventLoop: """ Return the event loop of the trading node. Returns ------- asyncio.AbstractEventLoop """ return self._loop def get_logger(self) -> LiveLogger: """ Return the logger for the trading node. Returns ------- LiveLogger """ return self._logger def add_data_client_factory(self, name, factory): """ Add the given data client factory to the node. Parameters ---------- name : str The name of the client factory. factory : LiveDataClientFactory or LiveExecutionClientFactory The factory to add. Raises ------ ValueError If name is not a valid string. KeyError If name has already been added. """ self._builder.add_data_client_factory(name, factory) def add_exec_client_factory(self, name, factory): """ Add the given execution client factory to the node. Parameters ---------- name : str The name of the client factory. factory : LiveDataClientFactory or LiveExecutionClientFactory The factory to add. Raises ------ ValueError If name is not a valid string. KeyError If name has already been added. """ self._builder.add_exec_client_factory(name, factory) def build(self) -> None: """ Build the nodes clients. """ if self._is_built: raise RuntimeError("The trading nodes clients are already built.") self._builder.build_data_clients(self._config.get("data_clients")) self._builder.build_exec_clients(self._config.get("exec_clients")) self._is_built = True def start(self) -> None: """ Start the trading node. """ if not self._is_built: raise RuntimeError( "The trading nodes clients have not been built. " "Please run `node.build()` prior to start.") try: if self._loop.is_running(): self._loop.create_task(self._run()) else: self._loop.run_until_complete(self._run()) except RuntimeError as ex: self._log.exception(ex) def stop(self) -> None: """ Stop the trading node gracefully. After a specified delay the internal `Trader` residuals will be checked. If save strategy is specified then strategy states will then be saved. """ try: if self._loop.is_running(): self._loop.create_task(self._stop()) else: self._loop.run_until_complete(self._stop()) except RuntimeError as ex: self._log.exception(ex) def dispose(self) -> None: """ Dispose of the trading node. Gracefully shuts down the executor and event loop. """ try: timeout = self._clock.utc_now() + timedelta(seconds=5) while self._is_running: time.sleep(0.1) if self._clock.utc_now() >= timeout: self._log.warning( "Timed out (5s) waiting for node to stop.") break self._log.info("state=DISPOSING...") self._log.debug(f"{self._data_engine.get_run_queue_task()}") self._log.debug(f"{self._exec_engine.get_run_queue_task()}") self._log.debug(f"{self._risk_engine.get_run_queue_task()}") self.trader.dispose() self._data_engine.dispose() self._exec_engine.dispose() self._risk_engine.dispose() self._log.info("Shutting down executor...") if sys.version_info >= (3, 9): # cancel_futures added in Python 3.9 self._executor.shutdown(wait=True, cancel_futures=True) else: self._executor.shutdown(wait=True) self._log.info("Stopping event loop...") self._cancel_all_tasks() self._logger.stop() self._loop.stop() except RuntimeError as ex: self._log.exception(ex) finally: if self._loop.is_running(): self._log.warning("Cannot close a running event loop.") else: self._log.info("Closing event loop...") self._loop.close() # Check and log if event loop is running if self._loop.is_running(): self._log.warning(f"loop.is_running={self._loop.is_running()}") else: self._log.info(f"loop.is_running={self._loop.is_running()}") # Check and log if event loop is closed if not self._loop.is_closed(): self._log.warning(f"loop.is_closed={self._loop.is_closed()}") else: self._log.info(f"loop.is_closed={self._loop.is_closed()}") self._log.info("state=DISPOSED.") def _log_header(self) -> None: nautilus_header(self._log) self._log.info(f"redis {redis.__version__}") self._log.info( f"msgpack {msgpack.version[0]}.{msgpack.version[1]}.{msgpack.version[2]}" ) if uvloop_version: self._log.info(f"uvloop {uvloop_version}") self._log.info( "=================================================================" ) def _setup_loop(self) -> None: if self._loop.is_closed(): self._log.error( "Cannot setup signal handling (event loop was closed).") return signal.signal(signal.SIGINT, signal.SIG_DFL) signals = (signal.SIGTERM, signal.SIGINT, signal.SIGABRT) for sig in signals: self._loop.add_signal_handler(sig, self._loop_sig_handler, sig) self._log.debug(f"Event loop {signals} handling setup.") def _loop_sig_handler(self, sig: signal.signal) -> None: self._loop.remove_signal_handler(signal.SIGTERM) self._loop.add_signal_handler(signal.SIGINT, lambda: None) self._log.warning(f"Received {sig!s}, shutting down...") self.stop() async def _run(self) -> None: try: self._log.info("state=STARTING...") self._is_running = True self._logger.start() self._data_engine.start() self._exec_engine.start() self._risk_engine.start() result: bool = await self._await_engines_connected() if not result: return result: bool = await self._exec_engine.reconcile_state() if not result: return self.trader.start() if self._loop.is_running(): self._log.info("state=RUNNING.") else: self._log.warning("Event loop is not running.") # Continue to run while engines are running... await self._data_engine.get_run_queue_task() await self._exec_engine.get_run_queue_task() await self._risk_engine.get_run_queue_task() except asyncio.CancelledError as ex: self._log.error(str(ex)) async def _await_engines_connected(self) -> bool: self._log.info(f"Waiting for engines to initialize " f"({self._connection_timeout}s timeout)...") # The data engine clients will be set as connected when all # instruments are received and updated with the data engine. # The execution engine clients will be set as connected when all # accounts are updated and the current order and position status is # reconciled. Thus any delay here will be due to blocking network IO. seconds = self._connection_timeout timeout: timedelta = self._clock.utc_now() + timedelta(seconds=seconds) while True: await asyncio.sleep(0) if self._clock.utc_now() >= timeout: self._log.error( f"Timed out ({seconds}s) waiting for engines to connect.") return False if not self._data_engine.check_connected(): continue if not self._exec_engine.check_connected(): continue break return True # Engines connected async def _stop(self) -> None: self._is_stopping = True self._log.info("state=STOPPING...") if self.trader.state == ComponentState.RUNNING: self.trader.stop() self._log.info( f"Awaiting residual state ({self._check_residuals_delay}s delay)..." ) await asyncio.sleep(self._check_residuals_delay) self.trader.check_residuals() if self._save_strategy_state: self.trader.save() if self._data_engine.state == ComponentState.RUNNING: self._data_engine.stop() if self._exec_engine.state == ComponentState.RUNNING: self._exec_engine.stop() if self._risk_engine.state == ComponentState.RUNNING: self._risk_engine.stop() await self._await_engines_disconnected() # Clean up remaining timers timer_names = self._clock.timer_names() self._clock.cancel_timers() for name in timer_names: self._log.info(f"Cancelled Timer(name={name}).") self._log.info("state=STOPPED.") self._is_running = False async def _await_engines_disconnected(self) -> None: self._log.info(f"Waiting for engines to disconnect " f"({self._disconnection_timeout}s timeout)...") seconds = self._disconnection_timeout timeout: timedelta = self._clock.utc_now() + timedelta(seconds=seconds) while True: await asyncio.sleep(0) if self._clock.utc_now() >= timeout: self._log.error( f"Timed out ({seconds}s) waiting for engines to disconnect." ) break if not self._data_engine.check_disconnected(): continue if not self._exec_engine.check_disconnected(): continue break def _cancel_all_tasks(self) -> None: to_cancel = asyncio.tasks.all_tasks(self._loop) if not to_cancel: self._log.info("All tasks finished.") return for task in to_cancel: self._log.warning(f"Cancelling pending task {task}") task.cancel() if self._loop.is_running(): self._log.warning( "Event loop still running during `cancel_all_tasks`.") return finish_all_tasks: asyncio.Future = asyncio.tasks.gather( *to_cancel, loop=self._loop, return_exceptions=True, ) self._loop.run_until_complete(finish_all_tasks) self._log.debug(f"{finish_all_tasks}") for task in to_cancel: if task.cancelled(): continue if task.exception() is not None: self._loop.call_exception_handler({ "message": "unhandled exception during asyncio.run() shutdown", "exception": task.exception(), "task": task, })
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 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 BacktestExecClientTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = TestClock() self.uuid_factory = UUIDFactory() self.logger = TestLogger(self.clock) self.trader_id = TraderId("TESTER", "000") self.account_id = AccountId("BINANCE", "000") self.portfolio = Portfolio( clock=self.clock, logger=self.logger, ) self.portfolio.register_cache(DataCache(self.logger)) self.analyzer = PerformanceAnalyzer() database = BypassExecutionDatabase( trader_id=self.trader_id, logger=self.logger, ) self.exec_engine = ExecutionEngine( database=database, portfolio=self.portfolio, clock=self.clock, logger=self.logger, ) self.exchange = SimulatedExchange( venue=Venue("BINANCE"), oms_type=OMSType.NETTING, generate_position_ids=True, is_frozen_account=False, starting_balances=[Money(1_000_000, USD)], instruments=[ETHUSDT_BINANCE], modules=[], exec_cache=self.exec_engine.cache, fill_model=FillModel(), clock=self.clock, logger=self.logger, ) self.exec_client = BacktestExecClient( exchange=self.exchange, account_id=self.account_id, engine=self.exec_engine, clock=self.clock, logger=self.logger, ) self.order_factory = OrderFactory( trader_id=self.trader_id, strategy_id=StrategyId("SCALPER", "000"), clock=self.clock, ) def test_is_connected_when_not_connected_returns_false(self): # Arrange # Act # Assert self.assertFalse(self.exec_client.is_connected()) def test_connect(self): # Arrange # Act self.exec_client.connect() # Assert self.assertTrue(self.exec_client.is_connected()) def test_disconnect(self): # Arrange self.exec_client.connect() # Act self.exec_client.disconnect() # Assert self.assertFalse(self.exec_client.is_connected()) def test_reset(self): # Arrange # Act self.exec_client.reset() # Assert self.assertFalse( self.exec_client.is_connected()) # No exceptions raised def test_dispose(self): # Arrange # Act self.exec_client.dispose() # Assert self.assertFalse( self.exec_client.is_connected()) # No exceptions raised def test_submit_order_when_not_connected_logs_and_does_not_send(self): # Arrange strategy = TradingStrategy("000") order = self.order_factory.market( ETHUSDT_BINANCE.symbol, OrderSide.BUY, Quantity(100), ) command = SubmitOrder( BINANCE, self.trader_id, self.account_id, strategy.id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.utc_now(), ) # Act self.exec_client.submit_order(command) # Assert self.assertEqual(OrderState.INITIALIZED, order.state) def test_submit_bracket_order_when_not_connected_logs_and_does_not_send( self): # Arrange strategy = TradingStrategy("000") entry = self.order_factory.market( ETHUSDT_BINANCE.symbol, OrderSide.BUY, Quantity(100), ) bracket = self.order_factory.bracket(entry, Price("500.00000")) command = SubmitBracketOrder( BINANCE, self.trader_id, self.account_id, strategy.id, bracket, self.uuid_factory.generate(), self.clock.utc_now(), ) # Act self.exec_client.submit_bracket_order(command) # Assert self.assertEqual(OrderState.INITIALIZED, entry.state) def test_cancel_order_when_not_connected_logs_and_does_not_send(self): # Arrange order = self.order_factory.market( ETHUSDT_BINANCE.symbol, OrderSide.BUY, Quantity(100), ) command = CancelOrder( BINANCE, self.trader_id, self.account_id, order.cl_ord_id, self.uuid_factory.generate(), self.clock.utc_now(), ) # Act self.exec_client.cancel_order(command) # Assert self.assertTrue(True) # No exceptions raised def test_modify_order_when_not_connected_logs_and_does_not_send(self): # Arrange order = self.order_factory.stop_market( ETHUSDT_BINANCE.symbol, OrderSide.BUY, Quantity(100), Price("1000.00"), ) command = ModifyOrder( BINANCE, self.trader_id, self.account_id, order.cl_ord_id, Quantity(100), Price("1010.00"), self.uuid_factory.generate(), self.clock.utc_now(), ) # Act self.exec_client.modify_order(command) # Assert self.assertTrue(True) # No exceptions raised
class ExecutionClientTests(unittest.TestCase): def setUp(self): # Fixture Setup self.clock = TestClock() self.uuid_factory = UUIDFactory() self.logger = Logger(self.clock) self.trader_id = TraderId("TESTER-000") self.account_id = TestStubs.account_id() self.cache = TestStubs.cache() self.portfolio = Portfolio( cache=self.cache, clock=self.clock, logger=self.logger, ) self.exec_engine = ExecutionEngine( portfolio=self.portfolio, cache=self.cache, clock=self.clock, logger=self.logger, ) self.venue = Venue("SIM") self.client = ExecutionClient( client_id=ClientId(self.venue.value), venue_type=VenueType.ECN, account_id=TestStubs.account_id(), account_type=AccountType.MARGIN, base_currency=USD, engine=self.exec_engine, clock=self.clock, logger=self.logger, ) self.order_factory = OrderFactory( trader_id=TraderId("TESTER-000"), strategy_id=StrategyId("S-001"), clock=TestClock(), ) def test_venue_when_brokerage_returns_client_id_value_as_venue(self): assert self.client.venue == self.venue def test_venue_when_brokerage_multi_venue_returns_none(self): # Arrange client = ExecutionClient( client_id=ClientId("IB"), venue_type=VenueType.BROKERAGE_MULTI_VENUE, account_id=AccountId("IB", "U1258001"), account_type=AccountType.MARGIN, base_currency=USD, engine=self.exec_engine, clock=self.clock, logger=self.logger, ) # Act, Assert assert client.venue is None def test_connect_when_not_implemented_raises_exception(self): self.assertRaises(NotImplementedError, self.client.connect) def test_disconnect_when_not_implemented_raises_exception(self): self.assertRaises(NotImplementedError, self.client.disconnect) def test_reset_when_not_implemented_raises_exception(self): self.assertRaises(NotImplementedError, self.client.reset) def test_dispose_when_not_implemented_raises_exception(self): self.assertRaises(NotImplementedError, self.client.dispose) def test_submit_order_raises_exception(self): order = self.order_factory.limit( AUDUSD_SIM.id, OrderSide.SELL, Quantity.from_int(100000), Price.from_str("1.00000"), ) command = SubmitOrder( self.trader_id, order.strategy_id, PositionId.null(), order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.assertRaises(NotImplementedError, self.client.submit_order, command) def test_submit_bracket_order_raises_not_implemented_error(self): entry_order = self.order_factory.stop_market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), Price.from_str("0.99995"), ) # Act bracket_order = self.order_factory.bracket( entry_order, Price.from_str("0.99990"), Price.from_str("1.00010"), ) command = SubmitBracketOrder( self.trader_id, entry_order.strategy_id, bracket_order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) self.assertRaises(NotImplementedError, self.client.submit_bracket_order, command) def test_update_order_raises_not_implemented_error(self): # Arrange # Act command = UpdateOrder( self.trader_id, StrategyId("SCALPER-001"), AUDUSD_SIM.id, ClientOrderId("O-123456789"), VenueOrderId("001"), Quantity.from_int(120000), Price.from_str("1.00000"), None, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Assert self.assertRaises(NotImplementedError, self.client.update_order, command) def test_cancel_order_raises_not_implemented_error(self): # Arrange # Act command = CancelOrder( self.trader_id, StrategyId("SCALPER-001"), AUDUSD_SIM.id, ClientOrderId("O-123456789"), VenueOrderId("001"), self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Assert self.assertRaises(NotImplementedError, self.client.cancel_order, command)