class TestLiveExecutionReconciliation: 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 = LiveLogger(self.loop, self.clock) self.account_id = TestIdStubs.account_id() self.trader_id = TestIdStubs.trader_id() self.order_factory = OrderFactory( trader_id=self.trader_id, strategy_id=StrategyId("S-001"), clock=self.clock, ) self.msgbus = MessageBus( trader_id=self.trader_id, clock=self.clock, logger=self.logger, ) self.cache = TestComponentStubs.cache() self.portfolio = Portfolio( msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.data_engine = LiveDataEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.risk_engine = LiveRiskEngine( loop=self.loop, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.client = MockLiveExecutionClient( loop=self.loop, client_id=ClientId(SIM.value), venue=SIM, account_type=AccountType.CASH, base_currency=USD, instrument_provider=InstrumentProvider( venue=SIM, logger=self.logger, ), msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.portfolio.update_account(TestEventStubs.cash_account_state()) self.exec_engine.register_client(self.client) # Prepare components self.cache.add_instrument(AUDUSD_SIM) def teardown(self): self.client.dispose() @pytest.mark.asyncio async def test_reconcile_state_no_cached_with_rejected_order(self): # Arrange report = OrderStatusReport( account_id=self.account_id, instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456"), venue_order_id=VenueOrderId("1"), order_side=OrderSide.BUY, order_type=OrderType.LIMIT, time_in_force=TimeInForce.GTC, order_status=OrderStatus.REJECTED, price=Price.from_str("1.00000"), quantity=Quantity.from_int(10_000), filled_qty=Quantity.from_int(0), post_only=True, cancel_reason="SOME_REASON", report_id=UUID4(), ts_accepted=0, ts_last=0, ts_init=0, ) self.client.add_order_status_report(report) # Act result = await self.exec_engine.reconcile_state() # Assert assert result assert len(self.cache.orders()) == 1 assert self.cache.orders()[0].status == OrderStatus.REJECTED @pytest.mark.asyncio async def test_reconcile_state_no_cached_with_accepted_order(self): # Arrange report = OrderStatusReport( account_id=self.account_id, instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456"), venue_order_id=VenueOrderId("1"), order_side=OrderSide.BUY, order_type=OrderType.LIMIT, time_in_force=TimeInForce.GTC, order_status=OrderStatus.ACCEPTED, price=Price.from_str("1.00000"), quantity=Quantity.from_int(10_000), filled_qty=Quantity.from_int(0), post_only=True, report_id=UUID4(), ts_accepted=0, ts_last=0, ts_init=0, ) self.client.add_order_status_report(report) # Act result = await self.exec_engine.reconcile_state() # Assert assert result assert len(self.cache.orders()) == 1 assert len(self.cache.orders_open()) == 1 assert self.cache.orders()[0].status == OrderStatus.ACCEPTED @pytest.mark.asyncio async def test_reconcile_state_no_cached_with_canceled_order(self): # Arrange report = OrderStatusReport( account_id=self.account_id, instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456"), venue_order_id=VenueOrderId("1"), order_side=OrderSide.BUY, order_type=OrderType.LIMIT, time_in_force=TimeInForce.GTC, order_status=OrderStatus.CANCELED, price=Price.from_str("1.00000"), quantity=Quantity.from_int(10_000), filled_qty=Quantity.from_int(0), post_only=True, report_id=UUID4(), ts_accepted=0, ts_triggered=0, ts_last=0, ts_init=0, ) self.client.add_order_status_report(report) # Act result = await self.exec_engine.reconcile_state() # Assert assert result assert len(self.cache.orders()) == 1 assert self.cache.orders()[0].status == OrderStatus.CANCELED @pytest.mark.asyncio async def test_reconcile_state_no_cached_with_expired_order(self): # Arrange report = OrderStatusReport( account_id=self.account_id, instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456"), venue_order_id=VenueOrderId("1"), order_side=OrderSide.BUY, order_type=OrderType.STOP_LIMIT, time_in_force=TimeInForce.GTD, expire_time=pd.Timestamp("1970-01-01T00:10:00", tz="UTC"), order_status=OrderStatus.EXPIRED, price=Price.from_str("0.99500"), trigger_price=Price.from_str("1.00000"), trigger_type=TriggerType.BID_ASK, offset_type=TrailingOffsetType.PRICE, quantity=Quantity.from_int(10_000), filled_qty=Quantity.from_int(0), post_only=True, report_id=UUID4(), ts_accepted=0, ts_last=0, ts_init=0, ) self.client.add_order_status_report(report) # Act result = await self.exec_engine.reconcile_state() # Assert assert result assert len(self.cache.orders()) == 1 assert self.cache.orders()[0].status == OrderStatus.EXPIRED @pytest.mark.asyncio async def test_reconcile_state_no_cached_with_triggered_order(self): # Arrange report = OrderStatusReport( account_id=self.account_id, instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456"), venue_order_id=VenueOrderId("1"), order_side=OrderSide.BUY, order_type=OrderType.STOP_LIMIT, time_in_force=TimeInForce.GTC, order_status=OrderStatus.TRIGGERED, price=Price.from_str("0.99500"), trigger_price=Price.from_str("1.00000"), trigger_type=TriggerType.BID_ASK, offset_type=TrailingOffsetType.PRICE, quantity=Quantity.from_int(10_000), filled_qty=Quantity.from_int(0), post_only=True, report_id=UUID4(), ts_accepted=1_000_000_000, ts_triggered=2_000_000_000, ts_last=2_000_000_000, ts_init=3_000_000_000, ) self.client.add_order_status_report(report) # Act result = await self.exec_engine.reconcile_state() # Assert assert result assert len(self.cache.orders()) == 1 assert len(self.cache.orders_open()) == 1 assert self.cache.orders()[0].status == OrderStatus.TRIGGERED @pytest.mark.asyncio async def test_reconcile_state_no_cached_with_filled_order_and_no_trades( self): # Arrange report = OrderStatusReport( account_id=self.account_id, instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456"), venue_order_id=VenueOrderId("1"), order_side=OrderSide.BUY, order_type=OrderType.LIMIT, time_in_force=TimeInForce.GTC, order_status=OrderStatus.FILLED, price=Price.from_str("1.00000"), quantity=Quantity.from_int(10_000), filled_qty=Quantity.from_int(10_000), avg_px=Decimal("1.00000"), post_only=True, report_id=UUID4(), ts_accepted=0, ts_triggered=0, ts_last=0, ts_init=0, ) self.client.add_order_status_report(report) # Act result = await self.exec_engine.reconcile_state() # Assert assert result assert len(self.cache.orders()) == 1 assert self.cache.orders()[0].status == OrderStatus.FILLED @pytest.mark.asyncio async def test_reconcile_state_no_cached_with_filled_order_and_trade(self): # Arrange venue_order_id = VenueOrderId("1") order_report = OrderStatusReport( account_id=self.account_id, instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456"), venue_order_id=venue_order_id, order_side=OrderSide.BUY, order_type=OrderType.LIMIT, time_in_force=TimeInForce.GTC, order_status=OrderStatus.FILLED, price=Price.from_str("1.00000"), quantity=Quantity.from_int(10_000), filled_qty=Quantity.from_int(10_000), avg_px=Decimal("1.00000"), post_only=True, reduce_only=False, report_id=UUID4(), ts_accepted=0, ts_triggered=0, ts_last=0, ts_init=0, ) trade_report = TradeReport( account_id=self.account_id, instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456"), venue_order_id=venue_order_id, trade_id=TradeId("1"), order_side=OrderSide.BUY, last_qty=Quantity.from_int(10_000), last_px=Price.from_str("1.00000"), commission=Money(0, USD), liquidity_side=LiquiditySide.MAKER, report_id=UUID4(), ts_event=0, ts_init=0, ) self.client.add_order_status_report(order_report) self.client.add_trade_reports(venue_order_id, [trade_report]) # Act result = await self.exec_engine.reconcile_state() # Assert assert result assert len(self.cache.orders()) == 1 assert self.cache.orders()[0].status == OrderStatus.FILLED @pytest.mark.asyncio async def test_reconcile_state_no_cached_with_partially_filled_order_and_trade( self): # Arrange venue_order_id = VenueOrderId("1") order_report = OrderStatusReport( account_id=self.account_id, instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456"), venue_order_id=venue_order_id, order_side=OrderSide.BUY, order_type=OrderType.LIMIT, time_in_force=TimeInForce.GTC, order_status=OrderStatus.PARTIALLY_FILLED, price=Price.from_str("1.00000"), quantity=Quantity.from_int(10_000), filled_qty=Quantity.from_int(5_000), avg_px=Decimal("1.00000"), post_only=True, report_id=UUID4(), ts_accepted=0, ts_triggered=0, ts_last=0, ts_init=0, ) trade_report = TradeReport( account_id=self.account_id, instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456"), venue_order_id=venue_order_id, venue_position_id=None, trade_id=TradeId("1"), order_side=OrderSide.BUY, last_qty=Quantity.from_int(5_000), last_px=Price.from_str("1.00000"), commission=Money(0, USD), liquidity_side=LiquiditySide.MAKER, report_id=UUID4(), ts_event=0, ts_init=0, ) self.client.add_order_status_report(order_report) self.client.add_trade_reports(venue_order_id, [trade_report]) # Act result = await self.exec_engine.reconcile_state() # Assert assert result assert len(self.cache.orders()) == 1 assert self.cache.orders()[0].status == OrderStatus.PARTIALLY_FILLED @pytest.mark.asyncio async def test_reconcile_state_no_cached_with_partially_filled_order_and_no_trade( self): # Arrange venue_order_id = VenueOrderId("1") order_report = OrderStatusReport( account_id=self.account_id, instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456"), venue_order_id=venue_order_id, order_side=OrderSide.BUY, order_type=OrderType.LIMIT, time_in_force=TimeInForce.GTC, order_status=OrderStatus.PARTIALLY_FILLED, price=Price.from_str("1.00000"), quantity=Quantity.from_int(10_000), filled_qty=Quantity.from_int(5_000), avg_px=Decimal("1.00000"), post_only=True, report_id=UUID4(), ts_accepted=0, ts_triggered=0, ts_last=0, ts_init=0, ) self.client.add_order_status_report(order_report) # Act result = await self.exec_engine.reconcile_state() # Assert assert result assert len(self.cache.orders()) == 1 assert self.cache.orders()[0].status == OrderStatus.PARTIALLY_FILLED
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 TestPortfolio: def setup(self): # Fixture Setup self.clock = TestClock() 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=TestClock(), ) 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.exec_engine = ExecutionEngine( msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) # Prepare components self.cache.add_instrument(AUDUSD_SIM) self.cache.add_instrument(GBPUSD_SIM) self.cache.add_instrument(BTCUSDT_BINANCE) self.cache.add_instrument(BTCUSD_BITMEX) self.cache.add_instrument(ETHUSD_BITMEX) self.cache.add_instrument(BETTING_INSTRUMENT) def test_account_when_no_account_returns_none(self): # Arrange, Act, Assert assert self.portfolio.account(SIM) is None def test_account_when_account_returns_the_account_facade(self): # Arrange state = AccountState( account_id=AccountId("BINANCE", "1513111"), account_type=AccountType.CASH, base_currency=None, reported=True, balances=[ AccountBalance( BTC, Money(10.00000000, BTC), Money(0.00000000, BTC), Money(10.00000000, BTC), ) ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) self.portfolio.update_account(state) # Act result = self.portfolio.account(BINANCE) # Assert assert result.id.issuer == "BINANCE" def test_balances_locked_when_no_account_for_venue_returns_none(self): # Arrange, Act, Assert assert self.portfolio.balances_locked(SIM) is None def test_margins_init_when_no_account_for_venue_returns_none(self): # Arrange, Act, Assert assert self.portfolio.margins_init(SIM) is None def test_margins_maint_when_no_account_for_venue_returns_none(self): # Arrange, Act, Assert assert self.portfolio.margins_maint(SIM) is None def test_unrealized_pnl_for_instrument_when_no_instrument_returns_none( self): # Arrange, Act, Assert assert self.portfolio.unrealized_pnl(USDJPY_SIM.id) is None def test_unrealized_pnl_for_venue_when_no_account_returns_empty_dict(self): # Arrange, Act, Assert assert self.portfolio.unrealized_pnls(SIM) == {} def test_net_position_when_no_positions_returns_zero(self): # Arrange, Act, Assert assert self.portfolio.net_position(AUDUSD_SIM.id) == Decimal(0) def test_net_exposures_when_no_positions_returns_none(self): # Arrange, Act, Assert assert self.portfolio.net_exposures(SIM) is None def test_is_net_long_when_no_positions_returns_false(self): # Arrange, Act, Assert assert self.portfolio.is_net_long(AUDUSD_SIM.id) is False def test_is_net_short_when_no_positions_returns_false(self): # Arrange, Act, Assert assert self.portfolio.is_net_short(AUDUSD_SIM.id) is False def test_is_flat_when_no_positions_returns_true(self): # Arrange, Act, Assert assert self.portfolio.is_flat(AUDUSD_SIM.id) is True def test_is_completely_flat_when_no_positions_returns_true(self): # Arrange, Act, Assert assert self.portfolio.is_flat(AUDUSD_SIM.id) is True def test_open_value_when_no_account_returns_none(self): # Arrange, Act, Assert assert self.portfolio.net_exposures(SIM) is None def test_update_tick(self): # Arrange tick = TestStubs.quote_tick_5decimal(GBPUSD_SIM.id) # Act self.portfolio.update_tick(tick) # Assert assert self.portfolio.unrealized_pnl(GBPUSD_SIM.id) is None def test_update_orders_working_cash_account(self): # Arrange AccountFactory.register_calculated_account("BINANCE") account_id = AccountId("BINANCE", "000") state = AccountState( account_id=account_id, account_type=AccountType.CASH, base_currency=None, # Multi-currency account reported=True, balances=[ AccountBalance( BTC, Money(10.00000000, BTC), Money(0.00000000, BTC), Money(10.00000000, BTC), ), AccountBalance( USDT, Money(100000.00000000, USDT), Money(0.00000000, USDT), Money(100000.00000000, USDT), ), ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) self.portfolio.update_account(state) # Create two working orders order = self.order_factory.limit( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("1.0"), Price.from_str("50000.00"), ) self.cache.add_order(order, position_id=None) # Act: push order state to ACCEPTED self.exec_engine.process( TestStubs.event_order_submitted(order, account_id=account_id)) self.exec_engine.process( TestStubs.event_order_accepted(order, account_id=account_id)) # Assert assert self.portfolio.balances_locked( BINANCE)[USDT].as_decimal() == 50100 def test_update_orders_working_margin_account(self): # Arrange AccountFactory.register_calculated_account("BINANCE") account_id = AccountId("BINANCE", "01234") state = AccountState( account_id=account_id, account_type=AccountType.MARGIN, base_currency=None, # Multi-currency account reported=True, balances=[ AccountBalance( BTC, Money(10.00000000, BTC), Money(0.00000000, BTC), Money(10.00000000, BTC), ), AccountBalance( ETH, Money(20.00000000, ETH), Money(0.00000000, ETH), Money(20.00000000, ETH), ), AccountBalance( USDT, Money(100000.00000000, USDT), Money(0.00000000, USDT), Money(100000.00000000, USDT), ), ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) self.portfolio.update_account(state) # Create two working orders order1 = self.order_factory.stop_market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("10.5"), Price.from_str("25000.00"), ) order2 = self.order_factory.stop_market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("10.5"), Price.from_str("25000.00"), ) self.cache.add_order(order1, position_id=None) self.cache.add_order(order2, position_id=None) # Push states to ACCEPTED order1.apply(TestStubs.event_order_submitted(order1)) self.cache.update_order(order1) order1.apply(TestStubs.event_order_accepted(order1)) self.cache.update_order(order1) filled1 = TestStubs.event_order_filled( order1, instrument=BTCUSDT_BINANCE, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-1"), last_px=Price.from_str("25000.00"), ) self.exec_engine.process(filled1) # Update the last quote last = QuoteTick( instrument_id=BTCUSDT_BINANCE.id, bid=Price.from_str("25001.00"), ask=Price.from_str("25002.00"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, ts_init=0, ) # Act self.portfolio.update_tick(last) self.portfolio.initialize_orders() # Assert assert self.portfolio.margins_init(BINANCE) == {} def test_order_accept_updates_margin_init(self): # Arrange AccountFactory.register_calculated_account("BINANCE") state = AccountState( account_id=AccountId("BETFAIR", "01234"), account_type=AccountType.MARGIN, base_currency=GBP, reported=True, balances=[ AccountBalance( currency=GBP, total=Money(1000, GBP), free=Money(1000, GBP), locked=Money(0, GBP), ), ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) AccountFactory.register_calculated_account("BETFAIR") self.portfolio.update_account(state) # Create a passive order order1 = self.order_factory.limit( BETTING_INSTRUMENT.id, OrderSide.BUY, Quantity.from_str("100"), Price.from_str("0.5"), ) self.cache.add_order(order1, position_id=None) # Push states to ACCEPTED order1.apply(TestStubs.event_order_submitted(order1)) self.cache.update_order(order1) order1.apply( TestStubs.event_order_accepted(order1, venue_order_id=VenueOrderId("1"))) self.cache.update_order(order1) # Act self.portfolio.initialize_orders() # Assert assert self.portfolio.margins_init(BETFAIR)[ BETTING_INSTRUMENT.id] == Money(200, GBP) def test_update_positions(self): # Arrange AccountFactory.register_calculated_account("BINANCE") account_id = AccountId("BINANCE", "01234") state = AccountState( account_id=account_id, account_type=AccountType.CASH, base_currency=None, # Multi-currency account reported=True, balances=[ AccountBalance( BTC, Money(10.00000000, BTC), Money(0.00000000, BTC), Money(10.00000000, BTC), ), AccountBalance( ETH, Money(20.00000000, ETH), Money(0.00000000, ETH), Money(20.00000000, ETH), ), ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) self.portfolio.update_account(state) # Create a closed position order1 = self.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("10.50000000"), ) order2 = self.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.SELL, Quantity.from_str("10.50000000"), ) self.cache.add_order(order1, position_id=None) self.cache.add_order(order2, position_id=None) # Push states to ACCEPTED order1.apply(TestStubs.event_order_submitted(order1)) self.cache.update_order(order1) order1.apply(TestStubs.event_order_accepted(order1)) self.cache.update_order(order1) fill1 = TestStubs.event_order_filled( order1, instrument=BTCUSDT_BINANCE, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-1"), last_px=Price.from_str("25000.00"), ) fill2 = TestStubs.event_order_filled( order2, instrument=BTCUSDT_BINANCE, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-1"), last_px=Price.from_str("25000.00"), ) position1 = Position(instrument=BTCUSDT_BINANCE, fill=fill1) position1.apply(fill2) order3 = self.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("10.00000000"), ) fill3 = TestStubs.event_order_filled( order3, instrument=BTCUSDT_BINANCE, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-2"), last_px=Price.from_str("25000.00"), ) position2 = Position(instrument=BTCUSDT_BINANCE, fill=fill3) # Update the last quote last = QuoteTick( instrument_id=BTCUSDT_BINANCE.id, bid=Price.from_str("25001.00"), ask=Price.from_str("25002.00"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, ts_init=0, ) # Act self.cache.add_position(position1, OMSType.HEDGING) self.cache.add_position(position2, OMSType.HEDGING) self.portfolio.initialize_positions() self.portfolio.update_tick(last) # Assert assert self.portfolio.is_net_long(BTCUSDT_BINANCE.id) def test_opening_one_long_position_updates_portfolio(self): # Arrange AccountFactory.register_calculated_account("BINANCE") account_id = AccountId("BINANCE", "01234") state = AccountState( account_id=account_id, account_type=AccountType.MARGIN, base_currency=None, # Multi-currency account reported=True, balances=[ AccountBalance( BTC, Money(10.00000000, BTC), Money(0.00000000, BTC), Money(10.00000000, BTC), ), AccountBalance( ETH, Money(20.00000000, ETH), Money(0.00000000, ETH), Money(20.00000000, ETH), ), AccountBalance( USDT, Money(100000.00000000, USDT), Money(0.00000000, USDT), Money(100000.00000000, USDT), ), ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) self.portfolio.update_account(state) order = self.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("10.000000"), ) fill = TestStubs.event_order_filled( order=order, instrument=BTCUSDT_BINANCE, strategy_id=StrategyId("S-001"), account_id=account_id, position_id=PositionId("P-123456"), last_px=Price.from_str("10500.00"), ) last = QuoteTick( instrument_id=BTCUSDT_BINANCE.id, bid=Price.from_str("10510.00"), ask=Price.from_str("10511.00"), bid_size=Quantity.from_str("1.000000"), ask_size=Quantity.from_str("1.000000"), ts_event=0, ts_init=0, ) self.cache.add_quote_tick(last) self.portfolio.update_tick(last) position = Position(instrument=BTCUSDT_BINANCE, fill=fill) # Act self.cache.add_position(position, OMSType.HEDGING) self.portfolio.update_position( TestStubs.event_position_opened(position)) # Assert assert self.portfolio.net_exposures(BINANCE) == { USDT: Money(105100.00000000, USDT) } assert self.portfolio.unrealized_pnls(BINANCE) == { USDT: Money(100.00000000, USDT) } assert self.portfolio.margins_maint(BINANCE) == { BTCUSDT_BINANCE.id: Money(105.00000000, USDT) } assert self.portfolio.net_exposure(BTCUSDT_BINANCE.id) == Money( 105100.00000000, USDT) assert self.portfolio.unrealized_pnl(BTCUSDT_BINANCE.id) == Money( 100.00000000, USDT) assert self.portfolio.net_position( order.instrument_id) == Decimal("10.00000000") assert self.portfolio.is_net_long(order.instrument_id) assert not self.portfolio.is_net_short(order.instrument_id) assert not self.portfolio.is_flat(order.instrument_id) assert not self.portfolio.is_completely_flat() def test_opening_one_short_position_updates_portfolio(self): # Arrange AccountFactory.register_calculated_account("BINANCE") account_id = AccountId("BINANCE", "01234") state = AccountState( account_id=account_id, account_type=AccountType.MARGIN, base_currency=None, # Multi-currency account reported=True, balances=[ AccountBalance( BTC, Money(10.00000000, BTC), Money(0.00000000, BTC), Money(10.00000000, BTC), ), AccountBalance( ETH, Money(20.00000000, ETH), Money(0.00000000, ETH), Money(20.00000000, ETH), ), AccountBalance( USDT, Money(100000.00000000, USDT), Money(0.00000000, USDT), Money(100000.00000000, USDT), ), ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) self.portfolio.update_account(state) order = self.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.SELL, Quantity.from_str("0.515"), ) fill = TestStubs.event_order_filled( order=order, instrument=BTCUSDT_BINANCE, strategy_id=StrategyId("S-001"), account_id=account_id, position_id=PositionId("P-123456"), last_px=Price.from_str("15000.00"), ) last = QuoteTick( instrument_id=BTCUSDT_BINANCE.id, bid=Price.from_str("15510.15"), ask=Price.from_str("15510.25"), bid_size=Quantity.from_str("12.62"), ask_size=Quantity.from_str("3.1"), ts_event=0, ts_init=0, ) self.cache.add_quote_tick(last) self.portfolio.update_tick(last) position = Position(instrument=BTCUSDT_BINANCE, fill=fill) # Act self.cache.add_position(position, OMSType.HEDGING) self.portfolio.update_position( TestStubs.event_position_opened(position)) # Assert assert self.portfolio.net_exposures(BINANCE) == { USDT: Money(7987.77875000, USDT) } assert self.portfolio.unrealized_pnls(BINANCE) == { USDT: Money(-262.77875000, USDT) } assert self.portfolio.margins_maint(BINANCE) == { BTCUSDT_BINANCE.id: Money(7.72500000, USDT) } assert self.portfolio.net_exposure(BTCUSDT_BINANCE.id) == Money( 7987.77875000, USDT) assert self.portfolio.unrealized_pnl(BTCUSDT_BINANCE.id) == Money( -262.77875000, USDT) assert self.portfolio.net_position( order.instrument_id) == Decimal("-0.515") assert not self.portfolio.is_net_long(order.instrument_id) assert self.portfolio.is_net_short(order.instrument_id) assert not self.portfolio.is_flat(order.instrument_id) assert not self.portfolio.is_completely_flat() def test_opening_positions_with_multi_asset_account(self): # Arrange AccountFactory.register_calculated_account("BITMEX") account_id = AccountId("BITMEX", "01234") state = AccountState( account_id=account_id, account_type=AccountType.MARGIN, base_currency=None, # Multi-currency account reported=True, balances=[ AccountBalance( BTC, Money(10.00000000, BTC), Money(0.00000000, BTC), Money(10.00000000, BTC), ), AccountBalance( ETH, Money(20.00000000, ETH), Money(0.00000000, ETH), Money(20.00000000, ETH), ), ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) self.portfolio.update_account(state) last_ethusd = QuoteTick( instrument_id=ETHUSD_BITMEX.id, bid=Price.from_str("376.05"), ask=Price.from_str("377.10"), bid_size=Quantity.from_str("16"), ask_size=Quantity.from_str("25"), ts_event=0, ts_init=0, ) last_btcusd = QuoteTick( instrument_id=BTCUSD_BITMEX.id, bid=Price.from_str("10500.05"), ask=Price.from_str("10501.51"), bid_size=Quantity.from_str("2.54"), ask_size=Quantity.from_str("0.91"), ts_event=0, ts_init=0, ) self.cache.add_quote_tick(last_ethusd) self.cache.add_quote_tick(last_btcusd) self.portfolio.update_tick(last_ethusd) self.portfolio.update_tick(last_btcusd) order = self.order_factory.market( ETHUSD_BITMEX.id, OrderSide.BUY, Quantity.from_int(10000), ) fill = TestStubs.event_order_filled( order=order, instrument=ETHUSD_BITMEX, strategy_id=StrategyId("S-001"), account_id=account_id, position_id=PositionId("P-123456"), last_px=Price.from_str("376.05"), ) position = Position(instrument=ETHUSD_BITMEX, fill=fill) # Act self.cache.add_position(position, OMSType.HEDGING) self.portfolio.update_position( TestStubs.event_position_opened(position)) # Assert assert self.portfolio.net_exposures(BITMEX) == { ETH: Money(26.59220848, ETH) } assert self.portfolio.margins_maint(BITMEX) == { ETHUSD_BITMEX.id: Money(0.20608962, ETH) } assert self.portfolio.net_exposure(ETHUSD_BITMEX.id) == Money( 26.59220848, ETH) assert self.portfolio.unrealized_pnl(ETHUSD_BITMEX.id) == Money( 0.00000000, ETH) def test_unrealized_pnl_when_insufficient_data_for_xrate_returns_none( self): # Arrange AccountFactory.register_calculated_account("BITMEX") state = AccountState( account_id=AccountId("BITMEX", "01234"), account_type=AccountType.MARGIN, base_currency=BTC, reported=True, balances=[ AccountBalance( BTC, Money(10.00000000, BTC), Money(0.00000000, BTC), Money(10.00000000, BTC), ), AccountBalance( ETH, Money(20.00000000, ETH), Money(0.00000000, ETH), Money(20.00000000, ETH), ), ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) self.portfolio.update_account(state) order = self.order_factory.market( ETHUSD_BITMEX.id, OrderSide.BUY, Quantity.from_int(100), ) self.cache.add_order(order, position_id=None) self.exec_engine.process(TestStubs.event_order_submitted(order)) self.exec_engine.process(TestStubs.event_order_accepted(order)) fill = TestStubs.event_order_filled( order=order, instrument=ETHUSD_BITMEX, strategy_id=StrategyId("S-1"), position_id=PositionId("P-123456"), last_px=Price.from_str("376.05"), ) self.exec_engine.process(fill) position = Position(instrument=ETHUSD_BITMEX, fill=fill) self.portfolio.update_position( TestStubs.event_position_opened(position)) # Act result = self.portfolio.unrealized_pnls(BITMEX) # # Assert assert result == {} def test_market_value_when_insufficient_data_for_xrate_returns_none(self): # Arrange AccountFactory.register_calculated_account("BITMEX") account_id = AccountId("BITMEX", "01234") state = AccountState( account_id=account_id, account_type=AccountType.MARGIN, base_currency=BTC, reported=True, balances=[ AccountBalance( BTC, Money(10.00000000, BTC), Money(0.00000000, BTC), Money(10.00000000, BTC), ), ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) self.portfolio.update_account(state) order = self.order_factory.market( ETHUSD_BITMEX.id, OrderSide.BUY, Quantity.from_int(100), ) fill = TestStubs.event_order_filled( order=order, instrument=ETHUSD_BITMEX, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-123456"), last_px=Price.from_str("376.05"), ) last_ethusd = QuoteTick( instrument_id=ETHUSD_BITMEX.id, bid=Price.from_str("376.05"), ask=Price.from_str("377.10"), bid_size=Quantity.from_str("16"), ask_size=Quantity.from_str("25"), ts_event=0, ts_init=0, ) last_xbtusd = QuoteTick( instrument_id=BTCUSD_BITMEX.id, bid=Price.from_str("50000.00"), ask=Price.from_str("50000.00"), bid_size=Quantity.from_str("1"), ask_size=Quantity.from_str("1"), ts_event=0, ts_init=0, ) position = Position(instrument=ETHUSD_BITMEX, fill=fill) self.portfolio.update_position( TestStubs.event_position_opened(position)) self.cache.add_position(position, OMSType.HEDGING) self.cache.add_quote_tick(last_ethusd) self.cache.add_quote_tick(last_xbtusd) self.portfolio.update_tick(last_ethusd) self.portfolio.update_tick(last_xbtusd) # Act result = self.portfolio.net_exposures(BITMEX) # Assert assert result == {BTC: Money(0.00200000, BTC)} def test_opening_several_positions_updates_portfolio(self): # Arrange AccountFactory.register_calculated_account("SIM") account_id = AccountId("SIM", "01234") state = AccountState( account_id=account_id, account_type=AccountType.MARGIN, base_currency=USD, reported=True, balances=[ AccountBalance( USD, Money(1_000_000, USD), Money(0, USD), Money(1_000_000, USD), ), ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) self.portfolio.update_account(state) last_audusd = QuoteTick( instrument_id=AUDUSD_SIM.id, bid=Price.from_str("0.80501"), ask=Price.from_str("0.80505"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, ts_init=0, ) last_gbpusd = QuoteTick( instrument_id=GBPUSD_SIM.id, bid=Price.from_str("1.30315"), ask=Price.from_str("1.30317"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, ts_init=0, ) self.cache.add_quote_tick(last_audusd) self.cache.add_quote_tick(last_gbpusd) self.portfolio.update_tick(last_audusd) self.portfolio.update_tick(last_gbpusd) order1 = self.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) order2 = self.order_factory.market( GBPUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) self.cache.add_order(order1, position_id=None) self.cache.add_order(order2, position_id=None) fill1 = TestStubs.event_order_filled( order1, instrument=AUDUSD_SIM, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-1"), last_px=Price.from_str("1.00000"), ) fill2 = TestStubs.event_order_filled( order2, instrument=GBPUSD_SIM, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-2"), last_px=Price.from_str("1.00000"), ) self.cache.update_order(order1) self.cache.update_order(order2) position1 = Position(instrument=AUDUSD_SIM, fill=fill1) position2 = Position(instrument=GBPUSD_SIM, fill=fill2) position_opened1 = TestStubs.event_position_opened(position1) position_opened2 = TestStubs.event_position_opened(position2) # Act self.cache.add_position(position1, OMSType.HEDGING) self.cache.add_position(position2, OMSType.HEDGING) self.portfolio.update_position(position_opened1) self.portfolio.update_position(position_opened2) # Assert assert self.portfolio.net_exposures(SIM) == { USD: Money(210816.00, USD) } assert self.portfolio.unrealized_pnls(SIM) == { USD: Money(10816.00, USD) } assert self.portfolio.margins_maint(SIM) == { AUDUSD_SIM.id: Money(3002.00, USD), GBPUSD_SIM.id: Money(3002.00, USD), } assert self.portfolio.net_exposure(AUDUSD_SIM.id) == Money( 80501.00, USD) assert self.portfolio.net_exposure(GBPUSD_SIM.id) == Money( 130315.00, USD) assert self.portfolio.unrealized_pnl(AUDUSD_SIM.id) == Money( -19499.00, USD) assert self.portfolio.unrealized_pnl(GBPUSD_SIM.id) == Money( 30315.00, USD) assert self.portfolio.net_position(AUDUSD_SIM.id) == Decimal(100000) assert self.portfolio.net_position(GBPUSD_SIM.id) == Decimal(100000) assert self.portfolio.is_net_long(AUDUSD_SIM.id) assert not self.portfolio.is_net_short(AUDUSD_SIM.id) assert not self.portfolio.is_flat(AUDUSD_SIM.id) assert not self.portfolio.is_completely_flat() def test_modifying_position_updates_portfolio(self): # Arrange AccountFactory.register_calculated_account("SIM") account_id = AccountId("SIM", "01234") state = AccountState( account_id=account_id, account_type=AccountType.MARGIN, base_currency=USD, reported=True, balances=[ AccountBalance( USD, Money(1_000_000, USD), Money(0, USD), Money(1_000_000, USD), ), ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) self.portfolio.update_account(state) last_audusd = QuoteTick( instrument_id=AUDUSD_SIM.id, bid=Price.from_str("0.80501"), ask=Price.from_str("0.80505"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, ts_init=0, ) self.cache.add_quote_tick(last_audusd) self.portfolio.update_tick(last_audusd) order1 = self.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) fill1 = TestStubs.event_order_filled( order1, instrument=AUDUSD_SIM, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-123456"), last_px=Price.from_str("1.00000"), ) position = Position(instrument=AUDUSD_SIM, fill=fill1) self.cache.add_position(position, OMSType.HEDGING) self.portfolio.update_position( TestStubs.event_position_opened(position)) order2 = self.order_factory.market( AUDUSD_SIM.id, OrderSide.SELL, Quantity.from_int(50000), ) order2_filled = TestStubs.event_order_filled( order2, instrument=AUDUSD_SIM, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-123456"), last_px=Price.from_str("1.00000"), ) position.apply(order2_filled) # Act self.portfolio.update_position( TestStubs.event_position_changed(position)) # Assert assert self.portfolio.net_exposures(SIM) == {USD: Money(40250.50, USD)} assert self.portfolio.unrealized_pnls(SIM) == { USD: Money(-9749.50, USD) } assert self.portfolio.margins_maint(SIM) == { AUDUSD_SIM.id: Money(1501.00, USD) } assert self.portfolio.net_exposure(AUDUSD_SIM.id) == Money( 40250.50, USD) assert self.portfolio.unrealized_pnl(AUDUSD_SIM.id) == Money( -9749.50, USD) assert self.portfolio.net_position(AUDUSD_SIM.id) == Decimal(50000) assert self.portfolio.is_net_long(AUDUSD_SIM.id) assert not self.portfolio.is_net_short(AUDUSD_SIM.id) assert not self.portfolio.is_flat(AUDUSD_SIM.id) assert not self.portfolio.is_completely_flat() assert self.portfolio.unrealized_pnls(BINANCE) == {} assert self.portfolio.net_exposures(BINANCE) is None def test_closing_position_updates_portfolio(self): # Arrange AccountFactory.register_calculated_account("SIM") account_id = AccountId("SIM", "01234") state = AccountState( account_id=account_id, account_type=AccountType.MARGIN, base_currency=USD, reported=True, balances=[ AccountBalance( USD, Money(1_000_000, USD), Money(0, USD), Money(1_000_000, USD), ), ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) self.portfolio.update_account(state) order1 = self.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) fill1 = TestStubs.event_order_filled( order1, instrument=AUDUSD_SIM, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-123456"), last_px=Price.from_str("1.00000"), ) position = Position(instrument=AUDUSD_SIM, fill=fill1) self.cache.add_position(position, OMSType.HEDGING) self.portfolio.update_position( TestStubs.event_position_opened(position)) order2 = self.order_factory.market( AUDUSD_SIM.id, OrderSide.SELL, Quantity.from_int(100000), ) order2_filled = TestStubs.event_order_filled( order2, instrument=AUDUSD_SIM, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-123456"), last_px=Price.from_str("1.00010"), ) position.apply(order2_filled) self.cache.update_position(position) # Act self.portfolio.update_position( TestStubs.event_position_closed(position)) # Assert assert self.portfolio.net_exposures(SIM) == {} assert self.portfolio.unrealized_pnls(SIM) == {} assert self.portfolio.margins_maint(SIM) == {} assert self.portfolio.net_exposure(AUDUSD_SIM.id) == Money(0, USD) assert self.portfolio.unrealized_pnl(AUDUSD_SIM.id) == Money(0, USD) assert self.portfolio.net_position(AUDUSD_SIM.id) == Decimal(0) assert not self.portfolio.is_net_long(AUDUSD_SIM.id) assert not self.portfolio.is_net_short(AUDUSD_SIM.id) assert self.portfolio.is_flat(AUDUSD_SIM.id) assert self.portfolio.is_completely_flat() def test_several_positions_with_different_instruments_updates_portfolio( self): # Arrange account_id = AccountId("SIM", "01234") state = AccountState( account_id=account_id, account_type=AccountType.MARGIN, base_currency=USD, reported=True, balances=[ AccountBalance( USD, Money(1_000_000, USD), Money(0, USD), Money(1_000_000, USD), ), ], info={}, event_id=UUID4(), ts_event=0, ts_init=0, ) self.portfolio.update_account(state) order1 = self.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) order2 = self.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) order3 = self.order_factory.market( GBPUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) order4 = self.order_factory.market( GBPUSD_SIM.id, OrderSide.SELL, Quantity.from_int(100000), ) fill1 = TestStubs.event_order_filled( order1, instrument=AUDUSD_SIM, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-1"), last_px=Price.from_str("1.00000"), ) fill2 = TestStubs.event_order_filled( order2, instrument=AUDUSD_SIM, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-2"), last_px=Price.from_str("1.00000"), ) fill3 = TestStubs.event_order_filled( order3, instrument=GBPUSD_SIM, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-3"), last_px=Price.from_str("1.00000"), ) fill4 = TestStubs.event_order_filled( order4, instrument=GBPUSD_SIM, strategy_id=StrategyId("S-1"), account_id=account_id, position_id=PositionId("P-3"), last_px=Price.from_str("1.00100"), ) position1 = Position(instrument=AUDUSD_SIM, fill=fill1) position2 = Position(instrument=AUDUSD_SIM, fill=fill2) position3 = Position(instrument=GBPUSD_SIM, fill=fill3) last_audusd = QuoteTick( instrument_id=AUDUSD_SIM.id, bid=Price.from_str("0.80501"), ask=Price.from_str("0.80505"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, ts_init=0, ) last_gbpusd = QuoteTick( instrument_id=GBPUSD_SIM.id, bid=Price.from_str("1.30315"), ask=Price.from_str("1.30317"), bid_size=Quantity.from_int(1), ask_size=Quantity.from_int(1), ts_event=0, ts_init=0, ) self.cache.add_quote_tick(last_audusd) self.cache.add_quote_tick(last_gbpusd) self.portfolio.update_tick(last_audusd) self.portfolio.update_tick(last_gbpusd) self.cache.add_position(position1, OMSType.HEDGING) self.cache.add_position(position2, OMSType.HEDGING) self.cache.add_position(position3, OMSType.HEDGING) # Act self.portfolio.update_position( TestStubs.event_position_opened(position1)) self.portfolio.update_position( TestStubs.event_position_opened(position2)) self.portfolio.update_position( TestStubs.event_position_opened(position3)) position3.apply(fill4) self.cache.update_position(position3) self.portfolio.update_position( TestStubs.event_position_closed(position3)) # Assert assert { USD: Money(-38998.00, USD) } == self.portfolio.unrealized_pnls(SIM) assert { USD: Money(161002.00, USD) } == self.portfolio.net_exposures(SIM) assert Money(161002.00, USD) == self.portfolio.net_exposure(AUDUSD_SIM.id) assert Money(-38998.00, USD) == self.portfolio.unrealized_pnl(AUDUSD_SIM.id) assert self.portfolio.unrealized_pnl(GBPUSD_SIM.id) == Money(0, USD) assert self.portfolio.net_position(AUDUSD_SIM.id) == Decimal(200000) assert self.portfolio.net_position(GBPUSD_SIM.id) == Decimal(0) assert self.portfolio.is_net_long(AUDUSD_SIM.id) assert self.portfolio.is_flat(GBPUSD_SIM.id) assert not self.portfolio.is_completely_flat()
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 TestLiveExecutionPerformance(PerformanceHarness): def setup(self): # Fixture Setup self.loop = asyncio.get_event_loop() self.loop.set_debug(True) self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.logger = Logger(self.clock, bypass=True) self.trader_id = TestIdStubs.trader_id() self.account_id = AccountId(BINANCE.value, "001") self.msgbus = MessageBus( trader_id=self.trader_id, clock=self.clock, logger=self.logger, ) self.cache = TestComponentStubs.cache() self.portfolio = Portfolio( msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.data_engine = LiveDataEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.risk_engine = LiveRiskEngine( loop=self.loop, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.exec_client = MockExecutionClient( client_id=ClientId("BINANCE"), venue=BINANCE, account_type=AccountType.CASH, base_currency=None, # Multi-currency account msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.portfolio.update_account(TestEventStubs.margin_account_state()) self.exec_engine.register_client(self.exec_client) self.strategy = TradingStrategy() self.strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) @pytest.fixture(autouse=True) @pytest.mark.benchmark(disable_gc=True, warmup=True) def setup_benchmark(self, benchmark): self.benchmark = benchmark def submit_order(self): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("1.00000000"), ) self.strategy.submit_order(order) @pytest.mark.skip(reason="For development only, event loop issue") def test_execute_command(self): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("1.00000000"), ) command = SubmitOrder( None, self.trader_id, self.strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) def execute_command(): self.exec_engine.execute(command) self.benchmark.pedantic(execute_command, iterations=100, rounds=100, warmup_rounds=5) # ~0.0ms / ~0.2μs / 218ns minimum of 10,000 runs @ 1 iteration each run. @pytest.mark.asyncio async def test_submit_order(self): self.exec_engine.start() await asyncio.sleep(1) def submit_order(): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("1.00000000"), ) self.strategy.submit_order(order) self.benchmark.pedantic(submit_order, iterations=100, rounds=100, warmup_rounds=5) # ~0.0ms / ~25.3μs / 25326ns minimum of 10,000 runs @ 1 iteration each run. @pytest.mark.asyncio async def test_submit_order_end_to_end(self): self.exec_engine.start() await asyncio.sleep(1) def run(): for _ in range(1000): order = self.strategy.order_factory.market( BTCUSDT_BINANCE.id, OrderSide.BUY, Quantity.from_str("1.00000000"), ) self.strategy.submit_order(order) self.benchmark.pedantic(run, rounds=10, warmup_rounds=5)
class TestLiveExecutionClient: 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 = LiveLogger(self.loop, 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.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.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=InstrumentProvider(), 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) # Prepare components self.cache.add_instrument(AUDUSD_SIM) def teardown(self): self.client.dispose() @pytest.mark.asyncio async def test_reconcile_state_given_no_order_and_not_in_cache_returns_false( self): # Arrange report = OrderStatusReport( client_order_id=ClientOrderId("O-123456"), venue_order_id=VenueOrderId("1"), order_status=OrderStatus.FILLED, filled_qty=Quantity.from_int(100000), ts_init=0, ) # Act result = await self.client.reconcile_state( report, order=None) # <- order won't be in the cache # Assert assert not result @pytest.mark.asyncio async def test_reconcile_state_when_order_rejected_generates_event(self): # Arrange self.exec_engine.start() 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.stop_market( 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.risk_engine.execute(submit_order) await asyncio.sleep(0) # Process queue self.exec_engine.process(TestStubs.event_order_submitted(order)) await asyncio.sleep(0) # Process queue self.exec_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_status=OrderStatus.REJECTED, filled_qty=Quantity.zero(), ts_init=0, ) # Act result = self.client.reconcile_state(report, order) # Assert assert result @pytest.mark.asyncio async def test_reconcile_state_when_order_expired_generates_event(self): # Arrange self.exec_engine.start() 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.stop_market( 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.risk_engine.execute(submit_order) await asyncio.sleep(0) # Process queue self.exec_engine.process(TestStubs.event_order_submitted(order)) await asyncio.sleep(0) # Process queue self.exec_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_status=OrderStatus.EXPIRED, filled_qty=Quantity.zero(), ts_init=0, ) # Act result = self.client.reconcile_state(report, order) # Assert assert result @pytest.mark.asyncio async def test_reconcile_state_when_order_canceled_generates_event(self): # Arrange self.exec_engine.start() 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.stop_market( 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.risk_engine.execute(submit_order) await asyncio.sleep(0) # Process queue self.exec_engine.process(TestStubs.event_order_submitted(order)) await asyncio.sleep(0) # Process queue self.exec_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_status=OrderStatus.CANCELED, filled_qty=Quantity.zero(), ts_init=0, ) # Act result = self.client.reconcile_state(report, order) # Assert assert result @pytest.mark.asyncio async def test_reconcile_state_when_order_completed_returns_true_with_warning1( self): # Arrange self.exec_engine.start() 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.stop_market( 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.risk_engine.execute(submit_order) await asyncio.sleep(0) # Process queue self.exec_engine.process(TestStubs.event_order_submitted(order)) await asyncio.sleep(0) # Process queue self.exec_engine.process(TestStubs.event_order_accepted(order)) await asyncio.sleep(0) # Process queue self.exec_engine.process(TestStubs.event_order_canceled(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_status=OrderStatus.CANCELED, filled_qty=Quantity.zero(), ts_init=0, ) # Act result = await self.client.reconcile_state(report, order) # Assert assert result @pytest.mark.asyncio async def test_reconcile_state_when_order_completed_returns_true_with_warning2( self): # Arrange self.exec_engine.start() 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.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.risk_engine.execute(submit_order) await asyncio.sleep(0) # Process queue self.exec_engine.process(TestStubs.event_order_submitted(order)) await asyncio.sleep(0) # Process queue self.exec_engine.process(TestStubs.event_order_accepted(order)) await asyncio.sleep(0) # Process queue self.exec_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_status=OrderStatus.FILLED, filled_qty=Quantity.from_int(100000), ts_init=0, ) # Act result = await self.client.reconcile_state(report, order) # Assert assert result @pytest.mark.asyncio async def test_reconcile_state_with_filled_order_when_trades_not_given_returns_false( self, ): # Arrange self.exec_engine.start() 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.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.risk_engine.execute(submit_order) await asyncio.sleep(0) # Process queue self.exec_engine.process(TestStubs.event_order_submitted(order)) await asyncio.sleep(0) # Process queue self.exec_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_status=OrderStatus.FILLED, filled_qty=Quantity.from_int(100000), ts_init=0, ) # Act result = await self.client.reconcile_state(report, order) # Assert assert not result
class TestLiveExecutionEngine: def setup(self): # Fixture Setup self.loop = asyncio.get_event_loop() self.loop.set_debug(True) self.clock = LiveClock() self.uuid_factory = UUIDFactory() self.logger = Logger(self.clock) self.trader_id = TestIdStubs.trader_id() self.order_factory = OrderFactory( trader_id=self.trader_id, strategy_id=StrategyId("S-001"), clock=self.clock, ) self.random_order_factory = OrderFactory( trader_id=TraderId("RANDOM-042"), strategy_id=StrategyId("S-042"), clock=self.clock, ) self.msgbus = MessageBus( trader_id=self.trader_id, clock=self.clock, logger=self.logger, ) self.cache = TestComponentStubs.cache() self.portfolio = Portfolio( msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.data_engine = LiveDataEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.risk_engine = LiveRiskEngine( loop=self.loop, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.instrument_provider = InstrumentProvider( venue=SIM, logger=self.logger, ) self.instrument_provider.add(AUDUSD_SIM) self.instrument_provider.add(GBPUSD_SIM) self.client = MockLiveExecutionClient( loop=self.loop, client_id=ClientId(SIM.value), venue=SIM, account_type=AccountType.CASH, base_currency=USD, instrument_provider=self.instrument_provider, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) self.portfolio.update_account(TestEventStubs.cash_account_state()) self.exec_engine.register_client(self.client) self.cache.add_instrument(AUDUSD_SIM) def teardown(self): self.exec_engine.dispose() @pytest.mark.asyncio async def test_start_when_loop_not_running_logs(self): # Arrange, Act self.exec_engine.start() # Assert assert True # No exceptions raised self.exec_engine.stop() @pytest.mark.asyncio async def test_message_qsize_at_max_blocks_on_put_command(self): # Arrange # Deregister test fixture ExecutionEngine from msgbus) self.msgbus.deregister( endpoint="ExecEngine.execute", handler=self.exec_engine.execute, ) self.msgbus.deregister( endpoint="ExecEngine.process", handler=self.exec_engine.process, ) self.msgbus.deregister( endpoint="ExecEngine.reconcile_report", handler=self.exec_engine.reconcile_report, ) self.msgbus.deregister( endpoint="ExecEngine.reconcile_mass_status", handler=self.exec_engine.reconcile_mass_status, ) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, config=LiveExecEngineConfig(qsize=1), ) strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.exec_engine.execute(submit_order) self.exec_engine.execute(submit_order) await asyncio.sleep(0.1) # Assert assert self.exec_engine.qsize() == 1 assert self.exec_engine.command_count == 0 @pytest.mark.asyncio async def test_message_qsize_at_max_blocks_on_put_event(self): # Arrange # Deregister test fixture ExecutionEngine from msgbus) self.msgbus.deregister( endpoint="ExecEngine.execute", handler=self.exec_engine.execute, ) self.msgbus.deregister( endpoint="ExecEngine.process", handler=self.exec_engine.process, ) self.msgbus.deregister( endpoint="ExecEngine.reconcile_report", handler=self.exec_engine.reconcile_report, ) self.msgbus.deregister( endpoint="ExecEngine.reconcile_mass_status", handler=self.exec_engine.reconcile_mass_status, ) self.exec_engine = LiveExecutionEngine( loop=self.loop, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, config=LiveExecEngineConfig(qsize=1), ) strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) event = TestEventStubs.order_submitted(order) # Act self.exec_engine.execute(submit_order) self.exec_engine.process(event) # Add over max size await asyncio.sleep(0.1) # Assert assert self.exec_engine.qsize() == 1 assert self.exec_engine.command_count == 0 @pytest.mark.asyncio async def test_start(self): # Arrange, Act self.exec_engine.start() await asyncio.sleep(0.1) # Assert assert self.exec_engine.is_running # Tear Down self.exec_engine.stop() @pytest.mark.asyncio async def test_kill_when_running_and_no_messages_on_queues(self): # Arrange, Act self.exec_engine.start() await asyncio.sleep(0) self.exec_engine.kill() # Assert assert self.exec_engine.is_stopped @pytest.mark.asyncio async def test_kill_when_not_running_with_messages_on_queue(self): # Arrange, Act self.exec_engine.kill() # Assert assert self.exec_engine.qsize() == 0 @pytest.mark.asyncio async def test_execute_command_places_command_on_queue(self): # Arrange self.exec_engine.start() strategy = TradingStrategy() strategy.register( trader_id=self.trader_id, portfolio=self.portfolio, msgbus=self.msgbus, cache=self.cache, clock=self.clock, logger=self.logger, ) order = strategy.order_factory.market( AUDUSD_SIM.id, OrderSide.BUY, Quantity.from_int(100000), ) submit_order = SubmitOrder( self.trader_id, strategy.id, None, order, self.uuid_factory.generate(), self.clock.timestamp_ns(), ) # Act self.exec_engine.execute(submit_order) await asyncio.sleep(0.1) # Assert assert self.exec_engine.qsize() == 0 assert self.exec_engine.command_count == 1 # Tear Down self.exec_engine.stop() def test_handle_order_status_report(self): # Arrange order_report = OrderStatusReport( account_id=AccountId("SIM", "001"), instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456"), order_list_id=OrderListId("1"), venue_order_id=VenueOrderId("2"), order_side=OrderSide.SELL, order_type=OrderType.STOP_LIMIT, contingency_type=ContingencyType.OCO, time_in_force=TimeInForce.DAY, expire_time=None, order_status=OrderStatus.REJECTED, price=Price.from_str("0.90090"), trigger_price=Price.from_str("0.90100"), trigger_type=TriggerType.DEFAULT, limit_offset=None, trailing_offset=Decimal("0.00010"), offset_type=TrailingOffsetType.PRICE, quantity=Quantity.from_int(1_000_000), filled_qty=Quantity.from_int(0), display_qty=None, avg_px=None, post_only=True, reduce_only=False, cancel_reason="SOME_REASON", report_id=UUID4(), ts_accepted=1_000_000, ts_triggered=1_500_000, ts_last=2_000_000, ts_init=3_000_000, ) # Act self.exec_engine.reconcile_report(order_report) # Assert assert self.exec_engine.report_count == 1 def test_handle_trade_report(self): # Arrange trade_report = TradeReport( account_id=AccountId("SIM", "001"), instrument_id=AUDUSD_SIM.id, client_order_id=ClientOrderId("O-123456789"), venue_order_id=VenueOrderId("1"), venue_position_id=PositionId("2"), trade_id=TradeId("3"), order_side=OrderSide.BUY, last_qty=Quantity.from_int(100), last_px=Price.from_str("100.50"), commission=Money("4.50", USD), liquidity_side=LiquiditySide.TAKER, report_id=UUID4(), ts_event=0, ts_init=0, ) # Act self.exec_engine.reconcile_report(trade_report) # Assert assert self.exec_engine.report_count == 1 def test_handle_position_status_report(self): # Arrange position_report = PositionStatusReport( account_id=AccountId("SIM", "001"), instrument_id=AUDUSD_SIM.id, venue_position_id=PositionId("1"), position_side=PositionSide.LONG, quantity=Quantity.from_int(1_000_000), report_id=UUID4(), ts_last=0, ts_init=0, ) # Act self.exec_engine.reconcile_report(position_report) # Assert assert self.exec_engine.report_count == 1 def test_execution_mass_status(self): # Arrange mass_status = ExecutionMassStatus( client_id=ClientId("SIM"), account_id=TestIdStubs.account_id(), venue=Venue("SIM"), report_id=UUID4(), ts_init=0, ) # Act self.exec_engine.reconcile_mass_status(mass_status) # Assert assert self.exec_engine.report_count == 1