class BitmexExchangeTests(unittest.TestCase):
    def setUp(self):
        # Fixture Setup
        self.strategies = [
            MockStrategy(TestStubs.bartype_btcusdt_binance_1min_bid())
        ]

        self.clock = TestClock()
        self.uuid_factory = UUIDFactory()
        self.logger = TestLogger(self.clock)

        self.portfolio = Portfolio(
            clock=self.clock,
            logger=self.logger,
        )

        self.data_engine = DataEngine(
            portfolio=self.portfolio,
            clock=self.clock,
            logger=self.logger,
            config={'use_previous_close':
                    False},  # To correctly reproduce historical data bars
        )
        self.data_engine.cache.add_instrument(XBTUSD_BITMEX)
        self.portfolio.register_cache(self.data_engine.cache)

        self.analyzer = PerformanceAnalyzer()

        self.trader_id = TraderId("TESTER", "000")
        self.account_id = AccountId("BITMEX", "001")

        exec_db = BypassExecutionDatabase(
            trader_id=self.trader_id,
            logger=self.logger,
        )

        self.exec_engine = ExecutionEngine(
            database=exec_db,
            portfolio=self.portfolio,
            clock=self.clock,
            logger=self.logger,
        )

        self.exchange = SimulatedExchange(
            venue=Venue("BITMEX"),
            oms_type=OMSType.HEDGING,
            generate_position_ids=True,
            is_frozen_account=False,
            starting_balances=[Money(1_000_000, USD)],
            exec_cache=self.exec_engine.cache,
            instruments=[XBTUSD_BITMEX],
            modules=[],
            fill_model=FillModel(),
            clock=self.clock,
            logger=self.logger,
        )

        self.exec_client = BacktestExecClient(
            exchange=self.exchange,
            account_id=self.account_id,
            engine=self.exec_engine,
            clock=self.clock,
            logger=self.logger,
        )

        self.exec_engine.register_client(self.exec_client)
        self.exchange.register_client(self.exec_client)

        self.strategy = MockStrategy(
            bar_type=TestStubs.bartype_btcusdt_binance_1min_bid())
        self.strategy.register_trader(
            self.trader_id,
            self.clock,
            self.logger,
        )

        self.data_engine.register_strategy(self.strategy)
        self.exec_engine.register_strategy(self.strategy)
        self.data_engine.start()
        self.exec_engine.start()
        self.strategy.start()

    def test_commission_maker_taker_order(self):
        # Arrange
        # Prepare market
        quote1 = QuoteTick(
            XBTUSD_BITMEX.symbol,
            Price("11493.70"),
            Price("11493.75"),
            Quantity(1500000),
            Quantity(1500000),
            UNIX_EPOCH,
        )

        self.data_engine.process(quote1)
        self.exchange.process_tick(quote1)

        order_market = self.strategy.order_factory.market(
            XBTUSD_BITMEX.symbol,
            OrderSide.BUY,
            Quantity(100000),
        )

        order_limit = self.strategy.order_factory.limit(
            XBTUSD_BITMEX.symbol,
            OrderSide.BUY,
            Quantity(100000),
            Price("11493.65"),
        )

        # Act
        self.strategy.submit_order(order_market)
        self.strategy.submit_order(order_limit)

        quote2 = QuoteTick(
            XBTUSD_BITMEX.symbol,
            Price("11493.60"),
            Price("11493.64"),
            Quantity(1500000),
            Quantity(1500000),
            UNIX_EPOCH,
        )

        self.exchange.process_tick(quote2)  # Fill the limit order
        self.portfolio.update_tick(quote2)

        # Assert
        self.assertEqual(
            LiquiditySide.TAKER,
            self.strategy.object_storer.get_store()[2].liquidity_side)
        self.assertEqual(
            LiquiditySide.MAKER,
            self.strategy.object_storer.get_store()[6].liquidity_side)
        self.assertEqual(Money("0.00652529", BTC),
                         self.strategy.object_storer.get_store()[2].commission)
        self.assertEqual(Money("-0.00217511", BTC),
                         self.strategy.object_storer.get_store()[6].commission)
Example #2
0
class ExecutionEngineTests(unittest.TestCase):
    def setUp(self):
        # Fixture Setup
        self.clock = LiveClock()
        self.uuid_factory = UUIDFactory()
        self.logger = TestLogger(self.clock)

        self.trader_id = TraderId("TESTER", "000")
        self.account_id = TestStubs.account_id()

        self.order_factory = OrderFactory(
            trader_id=self.trader_id,
            strategy_id=StrategyId("S", "001"),
            clock=self.clock,
        )

        self.portfolio = Portfolio(
            clock=self.clock,
            logger=self.logger,
        )
        self.portfolio.register_cache(DataCache(self.logger))

        self.analyzer = PerformanceAnalyzer()

        # Fresh isolated loop testing pattern
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)

        self.database = BypassExecutionDatabase(trader_id=self.trader_id,
                                                logger=self.logger)
        self.exec_engine = LiveExecutionEngine(
            loop=self.loop,
            database=self.database,
            portfolio=self.portfolio,
            clock=self.clock,
            logger=self.logger,
        )

    def tearDown(self):
        self.exec_engine.dispose()
        self.loop.stop()
        self.loop.close()

    def test_message_qsize_at_max_blocks_on_put_command(self):
        self.exec_engine = LiveExecutionEngine(loop=self.loop,
                                               database=self.database,
                                               portfolio=self.portfolio,
                                               clock=self.clock,
                                               logger=self.logger,
                                               config={"qsize": 1})

        strategy = TradingStrategy(order_id_tag="001")
        strategy.register_trader(
            TraderId("TESTER", "000"),
            self.clock,
            self.logger,
        )

        self.exec_engine.register_strategy(strategy)

        order = strategy.order_factory.market(
            AUDUSD_SIM.symbol,
            OrderSide.BUY,
            Quantity(100000),
        )

        submit_order = SubmitOrder(
            Venue("SIM"),
            self.trader_id,
            self.account_id,
            strategy.id,
            PositionId.null(),
            order,
            self.uuid_factory.generate(),
            self.clock.utc_now(),
        )

        # Act
        self.exec_engine.execute(submit_order)
        self.exec_engine.execute(submit_order)

        # Assert
        self.assertEqual(1, self.exec_engine.qsize())
        self.assertEqual(0, self.exec_engine.command_count)

    def test_message_qsize_at_max_blocks_on_put_event(self):
        self.exec_engine = LiveExecutionEngine(loop=self.loop,
                                               database=self.database,
                                               portfolio=self.portfolio,
                                               clock=self.clock,
                                               logger=self.logger,
                                               config={"qsize": 1})

        strategy = TradingStrategy(order_id_tag="001")
        strategy.register_trader(
            TraderId("TESTER", "000"),
            self.clock,
            self.logger,
        )

        self.exec_engine.register_strategy(strategy)

        order = strategy.order_factory.market(
            AUDUSD_SIM.symbol,
            OrderSide.BUY,
            Quantity(100000),
        )

        submit_order = SubmitOrder(
            Venue("SIM"),
            self.trader_id,
            self.account_id,
            strategy.id,
            PositionId.null(),
            order,
            self.uuid_factory.generate(),
            self.clock.utc_now(),
        )

        event = TestStubs.event_order_submitted(order)

        # Act
        self.exec_engine.execute(submit_order)
        self.exec_engine.process(event)

        # Assert
        self.assertEqual(1, self.exec_engine.qsize())
        self.assertEqual(0, self.exec_engine.command_count)

    def test_get_event_loop_returns_expected_loop(self):
        # Arrange
        # Act
        loop = self.exec_engine.get_event_loop()

        # Assert
        self.assertEqual(self.loop, loop)

    def test_start(self):
        async def run_test():
            # Arrange
            # Act
            self.exec_engine.start()
            await asyncio.sleep(0.1)

            # Assert
            self.assertEqual(ComponentState.RUNNING, self.exec_engine.state)

            # Tear Down
            self.exec_engine.stop()

        self.loop.run_until_complete(run_test())

    def test_kill(self):
        async def run_test():
            # Arrange
            # Act
            self.exec_engine.start()
            await asyncio.sleep(0)
            self.exec_engine.kill()

            # Assert
            self.assertEqual(ComponentState.STOPPED, self.exec_engine.state)

        self.loop.run_until_complete(run_test())

    def test_execute_command_places_command_on_queue(self):
        async def run_test():
            # Arrange
            self.exec_engine.start()

            strategy = TradingStrategy(order_id_tag="001")
            strategy.register_trader(
                TraderId("TESTER", "000"),
                self.clock,
                self.logger,
            )

            self.exec_engine.register_strategy(strategy)

            order = strategy.order_factory.market(
                AUDUSD_SIM.symbol,
                OrderSide.BUY,
                Quantity(100000),
            )

            submit_order = SubmitOrder(
                Venue("SIM"),
                self.trader_id,
                self.account_id,
                strategy.id,
                PositionId.null(),
                order,
                self.uuid_factory.generate(),
                self.clock.utc_now(),
            )

            # Act
            self.exec_engine.execute(submit_order)
            await asyncio.sleep(0.1)

            # Assert
            self.assertEqual(0, self.exec_engine.qsize())
            self.assertEqual(1, self.exec_engine.command_count)

            # Tear Down
            self.exec_engine.stop()

        self.loop.run_until_complete(run_test())

    def test_handle_position_opening_with_position_id_none(self):
        async def run_test():
            # Arrange
            self.exec_engine.start()

            strategy = TradingStrategy(order_id_tag="001")
            strategy.register_trader(
                TraderId("TESTER", "000"),
                self.clock,
                self.logger,
            )

            self.exec_engine.register_strategy(strategy)

            order = strategy.order_factory.market(
                AUDUSD_SIM.symbol,
                OrderSide.BUY,
                Quantity(100000),
            )

            event = TestStubs.event_order_submitted(order)

            # Act
            self.exec_engine.process(event)
            await asyncio.sleep(0.1)

            # Assert
            self.assertEqual(0, self.exec_engine.qsize())
            self.assertEqual(1, self.exec_engine.event_count)

            # Tear Down
            self.exec_engine.stop()

        self.loop.run_until_complete(run_test())
class DataEngineTests(unittest.TestCase):
    def setUp(self):
        # Fixture Setup
        self.clock = TestClock()
        self.uuid_factory = UUIDFactory()
        self.logger = Logger(self.clock)

        self.portfolio = Portfolio(
            clock=self.clock,
            logger=self.logger,
        )

        self.data_engine = DataEngine(
            portfolio=self.portfolio,
            clock=self.clock,
            logger=self.logger,
        )

        self.portfolio.register_cache(self.data_engine.cache)

        self.binance_client = BacktestMarketDataClient(
            instruments=[BTCUSDT_BINANCE, ETHUSDT_BINANCE],
            name=BINANCE.value,
            engine=self.data_engine,
            clock=self.clock,
            logger=self.logger,
        )

        self.bitmex_client = BacktestMarketDataClient(
            instruments=[XBTUSD_BITMEX],
            name=BITMEX.value,
            engine=self.data_engine,
            clock=self.clock,
            logger=self.logger,
        )

        self.quandl = MockMarketDataClient(
            name="QUANDL",
            engine=self.data_engine,
            clock=self.clock,
            logger=self.logger,
        )

    def test_registered_venues(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual([], self.data_engine.registered_clients)

    def test_subscribed_instruments_when_nothing_subscribed_returns_empty_list(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual([], self.data_engine.subscribed_instruments)

    def test_subscribed_quote_ticks_when_nothing_subscribed_returns_empty_list(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual([], self.data_engine.subscribed_quote_ticks)

    def test_subscribed_trade_ticks_when_nothing_subscribed_returns_empty_list(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual([], self.data_engine.subscribed_trade_ticks)

    def test_subscribed_bars_when_nothing_subscribed_returns_empty_list(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual([], self.data_engine.subscribed_bars)

    def test_register_client_successfully_adds_client(self):
        # Arrange
        # Act
        self.data_engine.register_client(self.binance_client)

        # Assert
        self.assertIn(BINANCE.value, self.data_engine.registered_clients)

    def test_deregister_client_successfully_removes_client(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)

        # Act
        self.data_engine.deregister_client(self.binance_client)

        # Assert
        self.assertNotIn(BINANCE.value, self.data_engine.registered_clients)

    def test_register_strategy_successfully_registered_with_strategy(self):
        # Arrange
        strategy = TradingStrategy("000")

        # Act
        strategy.register_data_engine(self.data_engine)

        # Assert
        self.assertEqual(self.data_engine.cache, strategy.data)

    def test_reset(self):
        # Arrange
        # Act
        self.data_engine.reset()

        # Assert
        self.assertEqual(0, self.data_engine.command_count)
        self.assertEqual(0, self.data_engine.data_count)
        self.assertEqual(0, self.data_engine.request_count)
        self.assertEqual(0, self.data_engine.response_count)

    def test_stop_and_resume(self):
        # Arrange
        self.data_engine.start()

        # Act
        self.data_engine.stop()
        self.data_engine.resume()
        self.data_engine.stop()
        self.data_engine.reset()

        # Assert
        self.assertEqual(0, self.data_engine.command_count)
        self.assertEqual(0, self.data_engine.data_count)
        self.assertEqual(0, self.data_engine.request_count)
        self.assertEqual(0, self.data_engine.response_count)

    def test_dispose(self):
        # Arrange
        self.data_engine.reset()

        # Act
        self.data_engine.dispose()

        # Assert
        self.assertEqual(0, self.data_engine.command_count)
        self.assertEqual(0, self.data_engine.data_count)
        self.assertEqual(0, self.data_engine.request_count)
        self.assertEqual(0, self.data_engine.response_count)

    def test_check_connected_when_client_disconnected_returns_false(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.data_engine.register_client(self.bitmex_client)

        self.binance_client.disconnect()
        self.bitmex_client.disconnect()

        # Act
        result = self.data_engine.check_connected()

        # Assert
        self.assertFalse(result)

    def test_check_connected_when_client_connected_returns_true(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.data_engine.register_client(self.bitmex_client)

        self.binance_client.connect()
        self.bitmex_client.connect()

        # Act
        result = self.data_engine.check_connected()

        # Assert
        self.assertTrue(result)

    def test_check_disconnected_when_client_disconnected_returns_true(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.data_engine.register_client(self.bitmex_client)

        # Act
        result = self.data_engine.check_disconnected()

        # Assert
        self.assertTrue(result)

    def test_check_disconnected_when_client_connected_returns_false(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.data_engine.register_client(self.bitmex_client)

        self.binance_client.connect()
        self.bitmex_client.connect()

        # Act
        result = self.data_engine.check_disconnected()

        # Assert
        self.assertFalse(result)

    def test_reset_when_already_disposed_raises_invalid_state_trigger(self):
        # Arrange
        self.data_engine.dispose()

        # Act
        # Assert
        self.assertRaises(InvalidStateTrigger, self.data_engine.reset)

    def test_dispose_when_already_disposed_raises_invalid_state_trigger(self):
        # Arrange
        self.data_engine.dispose()

        # Act
        # Assert
        self.assertRaises(InvalidStateTrigger, self.data_engine.dispose)

    def test_execute_unrecognized_message_logs_and_does_nothing(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)

        # Bogus message
        command = DataCommand(
            client_name=BINANCE.value,
            data_type=DataType(str),
            handler=[].append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(command)

        # Assert
        self.assertEqual(1, self.data_engine.command_count)

    def test_send_request_when_no_data_clients_registered_does_nothing(self):
        # Arrange
        handler = []
        request = DataRequest(
            client_name="RANDOM",
            data_type=DataType(
                QuoteTick,
                metadata={
                    "InstrumentId": InstrumentId(Symbol("SOMETHING"), Venue("RANDOM")),
                    "FromDateTime": None,
                    "ToDateTime": None,
                    "Limit": 1000,
                },
            ),
            callback=handler.append,
            request_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.send(request)

        # Assert
        self.assertEqual(1, self.data_engine.request_count)

    def test_send_data_request_when_data_type_unrecognized_logs_and_does_nothing(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)

        handler = []
        request = DataRequest(
            client_name=BINANCE.value,
            data_type=DataType(
                str,
                metadata={  # str data type is invalid
                    "InstrumentId": InstrumentId(Symbol("SOMETHING"), Venue("RANDOM")),
                    "FromDateTime": None,
                    "ToDateTime": None,
                    "Limit": 1000,
                },
            ),
            callback=handler.append,
            request_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.send(request)

        # Assert
        self.assertEqual(1, self.data_engine.request_count)

    def test_send_data_request_with_duplicate_ids_logs_and_does_not_handle_second(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.data_engine.start()

        handler = []
        uuid = self.uuid_factory.generate()  # We'll use this as a duplicate

        request1 = DataRequest(
            client_name=BINANCE.value,
            data_type=DataType(
                QuoteTick,
                metadata={  # str data type is invalid
                    "InstrumentId": InstrumentId(Symbol("SOMETHING"), Venue("RANDOM")),
                    "FromDateTime": None,
                    "ToDateTime": None,
                    "Limit": 1000,
                },
            ),
            callback=handler.append,
            request_id=uuid,  # Duplicate
            timestamp_ns=self.clock.timestamp_ns(),
        )

        request2 = DataRequest(
            client_name=BINANCE.value,
            data_type=DataType(
                QuoteTick,
                metadata={  # str data type is invalid
                    "InstrumentId": InstrumentId(Symbol("SOMETHING"), Venue("RANDOM")),
                    "FromDateTime": None,
                    "ToDateTime": None,
                    "Limit": 1000,
                },
            ),
            callback=handler.append,
            request_id=uuid,  # Duplicate
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.send(request1)
        self.data_engine.send(request2)

        # Assert
        self.assertEqual(2, self.data_engine.request_count)

    def test_execute_subscribe_when_data_type_unrecognized_logs_and_does_nothing(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)

        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(str),  # str data type is invalid
            handler=[].append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual(1, self.data_engine.command_count)

    def test_execute_subscribe_when_already_subscribed_does_not_add_and_logs(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=[].append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(subscribe)
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual(2, self.data_engine.command_count)

    def test_execute_subscribe_custom_data(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.data_engine.register_client(self.quandl)
        self.binance_client.connect()

        subscribe = Subscribe(
            client_name="QUANDL",
            data_type=DataType(str, metadata={"Type": "news"}),
            handler=[].append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual(1, self.data_engine.command_count)
        self.assertEqual(["subscribe"], self.quandl.calls)

    def test_execute_unsubscribe_custom_data(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.data_engine.register_client(self.quandl)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            client_name="QUANDL",
            data_type=DataType(str, metadata={"Type": "news"}),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        unsubscribe = Unsubscribe(
            client_name="QUANDL",
            data_type=DataType(str, metadata={"Type": "news"}),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual(2, self.data_engine.command_count)
        self.assertEqual(["subscribe", "unsubscribe"], self.quandl.calls)

    def test_execute_unsubscribe_when_data_type_unrecognized_logs_and_does_nothing(
        self,
    ):
        # Arrange
        self.data_engine.register_client(self.binance_client)

        handler = []
        unsubscribe = Unsubscribe(
            client_name=BINANCE.value,
            data_type=DataType(type(str)),  # str data type is invalid
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual(1, self.data_engine.command_count)

    def test_execute_unsubscribe_when_not_subscribed_logs_and_does_nothing(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        unsubscribe = Unsubscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                type(QuoteTick), metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual(1, self.data_engine.command_count)

    def test_receive_response_when_no_data_clients_registered_does_nothing(self):
        # Arrange
        response = DataResponse(
            client_name=BINANCE.value,
            data_type=DataType(QuoteTick),
            data=[],
            correlation_id=self.uuid_factory.generate(),
            response_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.receive(response)

        # Assert
        self.assertEqual(1, self.data_engine.response_count)

    def test_process_unrecognized_data_type_logs_and_does_nothing(self):
        # Arrange
        data = Data(0)

        # Act
        self.data_engine.process(data)  # Invalid

        # Assert
        self.assertEqual(1, self.data_engine.data_count)

    def test_process_data_places_data_on_queue(self):
        # Arrange
        tick = TestStubs.trade_tick_5decimal()

        # Act
        self.data_engine.process(tick)

        # Assert
        self.assertEqual(1, self.data_engine.data_count)

    def test_execute_subscribe_instrument_then_adds_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                Instrument, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=[].append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_instruments)

    def test_execute_unsubscribe_instrument_then_removes_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                Instrument, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        unsubscribe = Unsubscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                Instrument, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual([], self.data_engine.subscribed_instruments)

    def test_process_instrument_when_subscriber_then_sends_to_registered_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                Instrument, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        # Act
        self.data_engine.process(ETHUSDT_BINANCE)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE], handler)

    def test_process_instrument_when_subscribers_then_sends_to_registered_handlers(
        self,
    ):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler1 = []
        subscribe1 = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                Instrument, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler1.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        handler2 = []
        subscribe2 = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                Instrument, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler2.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe1)
        self.data_engine.execute(subscribe2)

        # Act
        self.data_engine.process(ETHUSDT_BINANCE)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_instruments)
        self.assertEqual([ETHUSDT_BINANCE], handler1)
        self.assertEqual([ETHUSDT_BINANCE], handler2)

    def test_execute_subscribe_order_book_stream_then_adds_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                OrderBook,
                metadata={
                    "InstrumentId": ETHUSDT_BINANCE.id,
                    "Level": 2,
                    "Depth": 10,
                    "Interval": 0,
                },
            ),
            handler=[].append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_order_books)

    def test_execute_subscribe_order_book_intervals_then_adds_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                OrderBook,
                metadata={
                    "InstrumentId": ETHUSDT_BINANCE.id,
                    "Level": 2,
                    "Depth": 25,
                    "Interval": 10,
                },
            ),
            handler=[].append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_order_books)

    def test_execute_unsubscribe_order_book_stream_then_removes_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                OrderBook,
                metadata={
                    "InstrumentId": ETHUSDT_BINANCE.id,
                    "Level": 2,
                    "Depth": 25,
                    "Interval": 0,
                },
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        unsubscribe = Unsubscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                OrderBook,
                metadata={
                    "InstrumentId": ETHUSDT_BINANCE.id,
                    "Interval": 0,
                },
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual([], self.data_engine.subscribed_order_books)

    def test_execute_unsubscribe_order_book_interval_then_removes_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                OrderBook,
                metadata={
                    "InstrumentId": ETHUSDT_BINANCE.id,
                    "Level": 2,
                    "Depth": 25,
                    "Interval": 10,
                },
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        unsubscribe = Unsubscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                OrderBook,
                metadata={
                    "InstrumentId": ETHUSDT_BINANCE.id,
                    "Interval": 10,
                },
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual([], self.data_engine.subscribed_order_books)

    def test_process_order_book_snapshot_when_one_subscriber_then_sends_to_registered_handler(
        self,
    ):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                OrderBook,
                {
                    "InstrumentId": ETHUSDT_BINANCE.id,
                    "Level": OrderBookLevel.L2,
                    "Depth": 25,
                    "Interval": 0,  # Streaming
                },
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        snapshot = OrderBookSnapshot(
            instrument_id=ETHUSDT_BINANCE.id,
            level=OrderBookLevel.L2,
            bids=[[1000, 1]],
            asks=[[1001, 1]],
            timestamp_ns=0,
        )

        # Act
        self.data_engine.process(snapshot)

        # Assert
        assert self.data_engine.subscribed_order_books == [ETHUSDT_BINANCE.id]
        assert handler[0].instrument_id == ETHUSDT_BINANCE.id
        assert type(handler[0]) == L2OrderBook

    def test_process_order_book_ops_then_sends_to_registered_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                OrderBook,
                {
                    "InstrumentId": ETHUSDT_BINANCE.id,
                    "Level": OrderBookLevel.L2,
                    "Depth": 25,
                    "Interval": 0,  # Streaming
                },
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        ops = OrderBookOperations(
            instrument_id=ETHUSDT_BINANCE.id,
            level=OrderBookLevel.L2,
            ops=[],
            timestamp_ns=0,
        )

        # Act
        self.data_engine.process(ops)

        # Assert
        assert self.data_engine.subscribed_order_books == [ETHUSDT_BINANCE.id]
        assert handler[0].instrument_id == ETHUSDT_BINANCE.id
        assert type(handler[0]) == L2OrderBook

    def test_process_order_book_when_multiple_subscribers_then_sends_to_registered_handlers(
        self,
    ):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler1 = []
        subscribe1 = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                OrderBook,
                {
                    "InstrumentId": ETHUSDT_BINANCE.id,
                    "Level": OrderBookLevel.L2,
                    "Depth": 25,
                    "Interval": 0,  # Streaming
                },
            ),
            handler=handler1.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        handler2 = []
        subscribe2 = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                OrderBook,
                {
                    "InstrumentId": ETHUSDT_BINANCE.id,
                    "Level": OrderBookLevel.L2,
                    "Depth": 25,
                    "Interval": 0,  # Streaming
                },
            ),
            handler=handler2.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe1)
        self.data_engine.execute(subscribe2)

        snapshot = OrderBookSnapshot(
            instrument_id=ETHUSDT_BINANCE.id,
            level=OrderBookLevel.L2,
            bids=[[1000, 1]],
            asks=[[1001, 1]],
            timestamp_ns=0,
        )

        # Act
        self.data_engine.process(snapshot)

        # Assert
        cached_book = self.data_engine.cache.order_book(ETHUSDT_BINANCE.id)
        assert self.data_engine.subscribed_order_books == [ETHUSDT_BINANCE.id]
        assert type(cached_book) == L2OrderBook
        assert cached_book.instrument_id == ETHUSDT_BINANCE.id
        assert handler1[0] == cached_book
        assert handler2[0] == cached_book

    def test_execute_subscribe_for_quote_ticks(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_quote_ticks)

    def test_execute_unsubscribe_for_quote_ticks(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        unsubscribe = Unsubscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual([], self.data_engine.subscribed_quote_ticks)

    def test_process_quote_tick_when_subscriber_then_sends_to_registered_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        tick = QuoteTick(
            ETHUSDT_BINANCE.id,
            Price("100.003"),
            Price("100.003"),
            Quantity(1),
            Quantity(1),
            0,
        )

        # Act
        self.data_engine.process(tick)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_quote_ticks)
        self.assertEqual([tick], handler)

    def test_process_quote_tick_when_subscribers_then_sends_to_registered_handlers(
        self,
    ):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler1 = []
        subscribe1 = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler1.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        handler2 = []
        subscribe2 = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                QuoteTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler2.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe1)
        self.data_engine.execute(subscribe2)

        tick = QuoteTick(
            ETHUSDT_BINANCE.id,
            Price("100.003"),
            Price("100.003"),
            Quantity(1),
            Quantity(1),
            0,
        )

        # Act
        self.data_engine.process(tick)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_quote_ticks)
        self.assertEqual([tick], handler1)
        self.assertEqual([tick], handler2)

    def test_subscribe_trade_tick_then_subscribes(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                TradeTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.id], self.data_engine.subscribed_trade_ticks)

    def test_unsubscribe_trade_tick_then_unsubscribes(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                TradeTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        unsubscribe = Unsubscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                TradeTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual([], self.data_engine.subscribed_trade_ticks)

    def test_process_trade_tick_when_subscriber_then_sends_to_registered_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                TradeTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        tick = TradeTick(
            ETHUSDT_BINANCE.id,
            Price("1050.00000"),
            Quantity(100),
            OrderSide.BUY,
            TradeMatchId("123456789"),
            0,
        )

        # Act
        self.data_engine.process(tick)

        # Assert
        self.assertEqual([tick], handler)

    def test_process_trade_tick_when_subscribers_then_sends_to_registered_handlers(
        self,
    ):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler1 = []
        subscribe1 = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                TradeTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler1.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        handler2 = []
        subscribe2 = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(
                TradeTick, metadata={"InstrumentId": ETHUSDT_BINANCE.id}
            ),
            handler=handler2.append,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe1)
        self.data_engine.execute(subscribe2)

        tick = TradeTick(
            ETHUSDT_BINANCE.id,
            Price("1050.00000"),
            Quantity(100),
            OrderSide.BUY,
            TradeMatchId("123456789"),
            0,
        )

        # Act
        self.data_engine.process(tick)

        # Assert
        self.assertEqual([tick], handler1)
        self.assertEqual([tick], handler2)

    def test_subscribe_bar_type_then_subscribes(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID)
        bar_type = BarType(ETHUSDT_BINANCE.id, bar_spec, internal_aggregation=True)

        handler = ObjectStorer()
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(Bar, metadata={"BarType": bar_type}),
            handler=handler.store_2,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual([bar_type], self.data_engine.subscribed_bars)

    def test_unsubscribe_bar_type_then_unsubscribes(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID)
        bar_type = BarType(ETHUSDT_BINANCE.id, bar_spec, internal_aggregation=True)

        handler = ObjectStorer()
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(Bar, metadata={"BarType": bar_type}),
            handler=handler.store_2,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        unsubscribe = Unsubscribe(
            client_name=BINANCE.value,
            data_type=DataType(Bar, metadata={"BarType": bar_type}),
            handler=handler.store_2,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual([], self.data_engine.subscribed_bars)

    def test_process_bar_when_subscriber_then_sends_to_registered_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID)
        bar_type = BarType(ETHUSDT_BINANCE.id, bar_spec, internal_aggregation=True)

        handler = ObjectStorer()
        subscribe = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(Bar, metadata={"BarType": bar_type}),
            handler=handler.store,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe)

        bar = Bar(
            bar_type,
            Price("1051.00000"),
            Price("1055.00000"),
            Price("1050.00000"),
            Price("1052.00000"),
            Quantity(100),
            0,
        )

        # Act
        self.data_engine.process(bar)

        # Assert
        self.assertEqual([bar], handler.get_store())

    def test_process_bar_when_subscribers_then_sends_to_registered_handlers(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID)
        bar_type = BarType(ETHUSDT_BINANCE.id, bar_spec, internal_aggregation=True)

        handler1 = ObjectStorer()
        subscribe1 = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(Bar, metadata={"BarType": bar_type}),
            handler=handler1.store,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        handler2 = ObjectStorer()
        subscribe2 = Subscribe(
            client_name=BINANCE.value,
            data_type=DataType(Bar, metadata={"BarType": bar_type}),
            handler=handler2.store,
            command_id=self.uuid_factory.generate(),
            timestamp_ns=self.clock.timestamp_ns(),
        )

        self.data_engine.execute(subscribe1)
        self.data_engine.execute(subscribe2)

        bar = Bar(
            bar_type,
            Price("1051.00000"),
            Price("1055.00000"),
            Price("1050.00000"),
            Price("1052.00000"),
            Quantity(100),
            0,
        )

        # Act
        self.data_engine.process(bar)

        # Assert
        self.assertEqual([bar], handler1.get_store())
        self.assertEqual([bar], handler2.get_store())
Example #4
0
class TraderTests(unittest.TestCase):
    def setUp(self):
        # Fixture Setup
        clock = TestClock()
        logger = Logger(clock)

        trader_id = TraderId("TESTER", "000")
        account_id = TestStubs.account_id()

        self.portfolio = Portfolio(
            clock=clock,
            logger=logger,
        )

        self.data_engine = DataEngine(
            portfolio=self.portfolio,
            clock=clock,
            logger=logger,
            config={"use_previous_close": False},
        )

        self.portfolio.register_cache(self.data_engine.cache)
        self.analyzer = PerformanceAnalyzer()

        self.exec_db = BypassExecutionDatabase(
            trader_id=trader_id,
            logger=logger,
        )

        self.exec_engine = ExecutionEngine(
            database=self.exec_db,
            portfolio=self.portfolio,
            clock=clock,
            logger=logger,
        )

        self.exchange = SimulatedExchange(
            venue=Venue("SIM"),
            oms_type=OMSType.HEDGING,
            is_frozen_account=False,
            starting_balances=[Money(1_000_000, USD)],
            exec_cache=self.exec_engine.cache,
            instruments=[USDJPY_SIM],
            modules=[],
            fill_model=FillModel(),
            clock=clock,
            logger=logger,
        )

        self.data_client = BacktestMarketDataClient(
            instruments=[USDJPY_SIM],
            name="SIM",
            engine=self.data_engine,
            clock=clock,
            logger=logger,
        )

        self.data_engine.register_client(self.data_client)

        self.exec_client = BacktestExecClient(
            exchange=self.exchange,
            account_id=account_id,
            engine=self.exec_engine,
            clock=clock,
            logger=logger,
        )

        self.risk_engine = RiskEngine(
            exec_engine=self.exec_engine,
            portfolio=self.portfolio,
            clock=clock,
            logger=logger,
        )

        self.exec_engine.register_risk_engine(self.risk_engine)
        self.exec_engine.register_client(self.exec_client)

        strategies = [
            TradingStrategy("001"),
            TradingStrategy("002"),
        ]

        self.trader = Trader(
            trader_id=trader_id,
            strategies=strategies,
            portfolio=self.portfolio,
            data_engine=self.data_engine,
            exec_engine=self.exec_engine,
            risk_engine=self.risk_engine,
            clock=clock,
            logger=logger,
        )

    def test_initialize_trader(self):
        # Arrange
        # Act
        trader_id = self.trader.id

        # Assert
        self.assertEqual(TraderId("TESTER", "000"), trader_id)
        self.assertEqual(IdTag("000"), trader_id.tag)
        self.assertEqual(ComponentState.INITIALIZED, self.trader.state)
        self.assertEqual(2, len(self.trader.strategy_states()))

    def test_get_strategy_states(self):
        # Arrange
        # Act
        status = self.trader.strategy_states()

        # Assert
        self.assertTrue(StrategyId("TradingStrategy", "001") in status)
        self.assertTrue(StrategyId("TradingStrategy", "002") in status)
        self.assertEqual("INITIALIZED", status[StrategyId("TradingStrategy", "001")])
        self.assertEqual("INITIALIZED", status[StrategyId("TradingStrategy", "002")])
        self.assertEqual(2, len(status))

    def test_change_strategies(self):
        # Arrange
        strategies = [
            TradingStrategy("003"),
            TradingStrategy("004"),
        ]

        # Act
        self.trader.initialize_strategies(strategies, warn_no_strategies=True)

        # Assert
        self.assertTrue(strategies[0].id in self.trader.strategy_states())
        self.assertTrue(strategies[1].id in self.trader.strategy_states())
        self.assertEqual(2, len(self.trader.strategy_states()))

    def test_trader_detects_duplicate_identifiers(self):
        # Arrange
        strategies = [
            TradingStrategy("000"),
            TradingStrategy("000"),
        ]

        # Act
        self.assertRaises(
            ValueError,
            self.trader.initialize_strategies,
            strategies,
            True,
        )

    def test_start_a_trader(self):
        # Arrange
        # Act
        self.trader.start()

        strategy_states = self.trader.strategy_states()

        # Assert
        self.assertEqual(ComponentState.RUNNING, self.trader.state)
        self.assertEqual(
            "RUNNING", strategy_states[StrategyId("TradingStrategy", "001")]
        )
        self.assertEqual(
            "RUNNING", strategy_states[StrategyId("TradingStrategy", "002")]
        )

    def test_stop_a_running_trader(self):
        # Arrange
        self.trader.start()

        # Act
        self.trader.stop()

        strategy_states = self.trader.strategy_states()

        # Assert
        self.assertEqual(ComponentState.STOPPED, self.trader.state)
        self.assertEqual(
            "STOPPED", strategy_states[StrategyId("TradingStrategy", "001")]
        )
        self.assertEqual(
            "STOPPED", strategy_states[StrategyId("TradingStrategy", "002")]
        )
Example #5
0
class DataEngineTests(unittest.TestCase):
    def setUp(self):
        # Fixture Setup
        self.clock = TestClock()
        self.uuid_factory = UUIDFactory()
        self.logger = TestLogger(self.clock)

        self.portfolio = Portfolio(
            clock=self.clock,
            logger=self.logger,
        )

        self.data_engine = DataEngine(
            portfolio=self.portfolio,
            clock=self.clock,
            logger=self.logger,
        )

        self.portfolio.register_cache(self.data_engine.cache)

        self.binance_client = BacktestDataClient(
            instruments=[BTCUSDT_BINANCE, ETHUSDT_BINANCE],
            venue=BINANCE,
            engine=self.data_engine,
            clock=self.clock,
            logger=self.logger,
        )

        self.bitmex_client = BacktestDataClient(
            instruments=[XBTUSD_BITMEX],
            venue=BITMEX,
            engine=self.data_engine,
            clock=self.clock,
            logger=self.logger,
        )

    def test_registered_venues(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual([], self.data_engine.registered_venues)

    def test_subscribed_instruments_when_nothing_subscribed_returns_empty_list(
            self):
        # Arrange
        # Act
        # Assert
        self.assertEqual([], self.data_engine.subscribed_instruments)

    def test_subscribed_quote_ticks_when_nothing_subscribed_returns_empty_list(
            self):
        # Arrange
        # Act
        # Assert
        self.assertEqual([], self.data_engine.subscribed_quote_ticks)

    def test_subscribed_trade_ticks_when_nothing_subscribed_returns_empty_list(
            self):
        # Arrange
        # Act
        # Assert
        self.assertEqual([], self.data_engine.subscribed_trade_ticks)

    def test_subscribed_bars_when_nothing_subscribed_returns_empty_list(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual([], self.data_engine.subscribed_bars)

    def test_register_client_successfully_adds_client(self):
        # Arrange
        # Act
        self.data_engine.register_client(self.binance_client)

        # Assert
        self.assertIn(BINANCE, self.data_engine.registered_venues)

    def test_deregister_client_successfully_removes_client(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)

        # Act
        self.data_engine.deregister_client(self.binance_client)

        # Assert
        self.assertNotIn(BINANCE, self.data_engine.registered_venues)

    def test_register_strategy_successfully_registered_with_strategy(self):
        # Arrange
        strategy = TradingStrategy("000")

        # Act
        strategy.register_data_engine(self.data_engine)

        # Assert
        self.assertEqual(self.data_engine.cache, strategy.data)

    def test_reset(self):
        # Arrange
        # Act
        self.data_engine.reset()

        # Assert
        self.assertEqual(0, self.data_engine.command_count)
        self.assertEqual(0, self.data_engine.data_count)
        self.assertEqual(0, self.data_engine.request_count)
        self.assertEqual(0, self.data_engine.response_count)

    def test_stop_and_resume(self):
        # Arrange
        self.data_engine.start()

        # Act
        self.data_engine.stop()
        self.data_engine.resume()
        self.data_engine.stop()
        self.data_engine.reset()

        # Assert
        self.assertEqual(0, self.data_engine.command_count)
        self.assertEqual(0, self.data_engine.data_count)
        self.assertEqual(0, self.data_engine.request_count)
        self.assertEqual(0, self.data_engine.response_count)

    def test_dispose(self):
        # Arrange
        self.data_engine.reset()

        # Act
        self.data_engine.dispose()

        # Assert
        self.assertEqual(0, self.data_engine.command_count)
        self.assertEqual(0, self.data_engine.data_count)
        self.assertEqual(0, self.data_engine.request_count)
        self.assertEqual(0, self.data_engine.response_count)

    def test_check_connected_when_client_disconnected_returns_false(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.data_engine.register_client(self.bitmex_client)

        self.binance_client.disconnect()
        self.bitmex_client.disconnect()

        # Act
        result = self.data_engine.check_connected()

        # Assert
        self.assertFalse(result)

    def test_check_connected_when_client_connected_returns_true(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.data_engine.register_client(self.bitmex_client)

        self.binance_client.connect()
        self.bitmex_client.connect()

        # Act
        result = self.data_engine.check_connected()

        # Assert
        self.assertTrue(result)

    def test_check_disconnected_when_client_disconnected_returns_true(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.data_engine.register_client(self.bitmex_client)

        # Act
        result = self.data_engine.check_disconnected()

        # Assert
        self.assertTrue(result)

    def test_check_disconnected_when_client_connected_returns_false(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.data_engine.register_client(self.bitmex_client)

        self.binance_client.connect()
        self.bitmex_client.connect()

        # Act
        result = self.data_engine.check_disconnected()

        # Assert
        self.assertFalse(result)

    def test_reset_when_already_disposed_raises_invalid_state_trigger(self):
        # Arrange
        self.data_engine.dispose()

        # Act
        # Assert
        self.assertRaises(InvalidStateTrigger, self.data_engine.reset)

    def test_dispose_when_already_disposed_raises_invalid_state_trigger(self):
        # Arrange
        self.data_engine.dispose()

        # Act
        # Assert
        self.assertRaises(InvalidStateTrigger, self.data_engine.dispose)

    def test_execute_unrecognized_message_logs_and_does_nothing(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)

        # Bogus message
        command = DataCommand(
            venue=BINANCE,
            data_type=QuoteTick,
            metadata={},
            handler=[].append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(command)

        # Assert
        self.assertEqual(1, self.data_engine.command_count)

    def test_send_request_when_no_data_clients_registered_does_nothing(self):
        # Arrange
        handler = []
        request = DataRequest(
            venue=Venue("RANDOM"),
            data_type=QuoteTick,
            metadata={
                "Symbol": Symbol("SOMETHING", Venue("RANDOM")),
                "FromDateTime": None,
                "ToDateTime": None,
                "Limit": 1000,
            },
            callback=handler.append,
            request_id=self.uuid_factory.generate(),
            request_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.send(request)

        # Assert
        self.assertEqual(1, self.data_engine.request_count)

    def test_send_data_request_when_data_type_unrecognized_logs_and_does_nothing(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)

        handler = []
        request = DataRequest(
            venue=BINANCE,
            data_type=str,  # str data type is invalid
            metadata={
                "Symbol": Symbol("SOMETHING", Venue("RANDOM")),
                "FromDateTime": None,
                "ToDateTime": None,
                "Limit": 1000,
            },
            callback=handler.append,
            request_id=self.uuid_factory.generate(),
            request_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.send(request)

        # Assert
        self.assertEqual(1, self.data_engine.request_count)

    def test_send_data_request_with_duplicate_ids_logs_and_does_not_handle_second(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)

        handler = []
        uuid = self.uuid_factory.generate()  # We'll use this as a duplicate

        request1 = DataRequest(
            venue=BINANCE,
            data_type=QuoteTick,  # str data type is invalid
            metadata={
                "Symbol": Symbol("SOMETHING", Venue("RANDOM")),
                "FromDateTime": None,
                "ToDateTime": None,
                "Limit": 1000,
            },
            callback=handler.append,
            request_id=uuid,  # Duplicate
            request_timestamp=self.clock.utc_now(),
        )

        request2 = DataRequest(
            venue=BINANCE,
            data_type=QuoteTick,  # str data type is invalid
            metadata={
                "Symbol": Symbol("SOMETHING", Venue("RANDOM")),
                "FromDateTime": None,
                "ToDateTime": None,
                "Limit": 1000,
            },
            callback=handler.append,
            request_id=uuid,  # Duplicate
            request_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.send(request1)
        self.data_engine.send(request2)

        # Assert
        self.assertEqual(2, self.data_engine.request_count)

    def test_execute_subscribe_when_data_type_unrecognized_logs_and_does_nothing(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)

        subscribe = Subscribe(
            venue=BINANCE,
            data_type=str,  # str data type is invalid
            metadata={},  # Invalid anyway
            handler=[].append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual(1, self.data_engine.command_count)

    def test_execute_subscribe_when_already_subscribed_does_not_add_and_logs(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        subscribe = Subscribe(
            venue=BINANCE,
            data_type=QuoteTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=[].append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(subscribe)
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual(2, self.data_engine.command_count)

    def test_execute_unsubscribe_when_data_type_unrecognized_logs_and_does_nothing(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)

        handler = []
        unsubscribe = Unsubscribe(
            venue=BINANCE,
            data_type=str,  # str data type is invalid
            metadata={},  # Invalid anyway
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual(1, self.data_engine.command_count)

    def test_execute_unsubscribe_when_not_subscribed_logs_and_does_nothing(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        unsubscribe = Unsubscribe(
            venue=BINANCE,
            data_type=QuoteTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual(1, self.data_engine.command_count)

    def test_receive_response_when_no_data_clients_registered_does_nothing(
            self):
        # Arrange
        response = DataResponse(
            venue=BINANCE,
            data_type=QuoteTick,
            metadata={},
            data=[],
            correlation_id=self.uuid_factory.generate(),
            response_id=self.uuid_factory.generate(),
            response_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.receive(response)

        # Assert
        self.assertEqual(1, self.data_engine.response_count)

    def test_process_unrecognized_data_type_logs_and_does_nothing(self):
        # Arrange
        # Act
        self.data_engine.process("DATA!")  # Invalid

        # Assert
        self.assertEqual(1, self.data_engine.data_count)

    def test_process_data_places_data_on_queue(self):
        # Arrange
        tick = TestStubs.trade_tick_5decimal()

        # Act
        self.data_engine.process(tick)

        # Assert
        self.assertEqual(1, self.data_engine.data_count)

    def test_execute_subscribe_instrument_then_adds_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        subscribe = Subscribe(
            venue=BINANCE,
            data_type=Instrument,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=[].append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.symbol],
                         self.data_engine.subscribed_instruments)

    def test_execute_unsubscribe_instrument_then_removes_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            venue=BINANCE,
            data_type=Instrument,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe)

        unsubscribe = Unsubscribe(
            venue=BINANCE,
            data_type=Instrument,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual([], self.data_engine.subscribed_instruments)

    def test_process_instrument_when_subscriber_then_sends_to_registered_handler(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            venue=BINANCE,
            data_type=Instrument,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe)

        # Act
        self.data_engine.process(ETHUSDT_BINANCE)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE], handler)

    def test_process_instrument_when_subscribers_then_sends_to_registered_handlers(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler1 = []
        subscribe1 = Subscribe(
            venue=BINANCE,
            data_type=Instrument,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler1.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        handler2 = []
        subscribe2 = Subscribe(
            venue=BINANCE,
            data_type=Instrument,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler2.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe1)
        self.data_engine.execute(subscribe2)

        # Act
        self.data_engine.process(ETHUSDT_BINANCE)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.symbol],
                         self.data_engine.subscribed_instruments)
        self.assertEqual([ETHUSDT_BINANCE], handler1)
        self.assertEqual([ETHUSDT_BINANCE], handler2)

    def test_execute_subscribe_order_book_stream_then_adds_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        subscribe = Subscribe(
            venue=BINANCE,
            data_type=OrderBook,
            metadata={
                "Symbol": ETHUSDT_BINANCE.symbol,
                "Level": 2,
                "Depth": 10,
                "Interval": 0,
            },
            handler=[].append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.symbol],
                         self.data_engine.subscribed_order_books)

    def test_execute_subscribe_order_book_intervals_then_adds_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        subscribe = Subscribe(
            venue=BINANCE,
            data_type=OrderBook,
            metadata={
                "Symbol": ETHUSDT_BINANCE.symbol,
                "Level": 2,
                "Depth": 25,
                "Interval": 10,
            },
            handler=[].append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.symbol],
                         self.data_engine.subscribed_order_books)

    def test_execute_unsubscribe_order_book_stream_then_removes_handler(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            venue=BINANCE,
            data_type=OrderBook,
            metadata={
                "Symbol": ETHUSDT_BINANCE.symbol,
                "Level": 2,
                "Depth": 25,
                "Interval": 0,
            },
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe)

        unsubscribe = Unsubscribe(
            venue=BINANCE,
            data_type=OrderBook,
            metadata={
                "Symbol": ETHUSDT_BINANCE.symbol,
                "Interval": 0,
            },
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual([], self.data_engine.subscribed_order_books)

    def test_execute_unsubscribe_order_book_interval_then_removes_handler(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            venue=BINANCE,
            data_type=OrderBook,
            metadata={
                "Symbol": ETHUSDT_BINANCE.symbol,
                "Level": 2,
                "Depth": 25,
                "Interval": 10,
            },
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe)

        unsubscribe = Unsubscribe(
            venue=BINANCE,
            data_type=OrderBook,
            metadata={
                "Symbol": ETHUSDT_BINANCE.symbol,
                "Interval": 10,
            },
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual([], self.data_engine.subscribed_order_books)

    def test_process_order_book_when_subscriber_then_sends_to_registered_handler(
            self):
        pass
        # # Arrange
        # self.data_engine.register_client(self.binance_client)
        # self.binance_client.connect()
        #
        # handler = []
        # subscribe = Subscribe(
        #     venue=BINANCE,
        #     data_type=Instrument,
        #     metadata={"Symbol": ETHUSDT_BINANCE.symbol},
        #     handler=handler.append,
        #     command_id=self.uuid_factory.generate(),
        #     command_timestamp=self.clock.utc_now(),
        # )
        #
        # self.data_engine.execute(subscribe)
        #
        # # Act
        # self.data_engine.process(ETHUSDT_BINANCE)
        #
        # # Assert
        # self.assertEqual([ETHUSDT_BINANCE], handler)

    def test_process_order_book_when_subscribers_then_sends_to_registered_handlers(
            self):
        pass
        # # Arrange
        # self.data_engine.register_client(self.binance_client)
        # self.binance_client.connect()
        #
        # handler1 = []
        # subscribe1 = Subscribe(
        #     venue=BINANCE,
        #     data_type=Instrument,
        #     metadata={"Symbol": ETHUSDT_BINANCE.symbol},
        #     handler=handler1.append,
        #     command_id=self.uuid_factory.generate(),
        #     command_timestamp=self.clock.utc_now(),
        # )
        #
        # handler2 = []
        # subscribe2 = Subscribe(
        #     venue=BINANCE,
        #     data_type=Instrument,
        #     metadata={"Symbol": ETHUSDT_BINANCE.symbol},
        #     handler=handler2.append,
        #     command_id=self.uuid_factory.generate(),
        #     command_timestamp=self.clock.utc_now(),
        # )
        #
        # self.data_engine.execute(subscribe1)
        # self.data_engine.execute(subscribe2)
        #
        # # Act
        # self.data_engine.process(ETHUSDT_BINANCE)
        #
        # # Assert
        # self.assertEqual([ETHUSDT_BINANCE.symbol], self.data_engine.subscribed_instruments)
        # self.assertEqual([ETHUSDT_BINANCE], handler1)
        # self.assertEqual([ETHUSDT_BINANCE], handler2)

    def test_execute_subscribe_for_quote_ticks(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            venue=BINANCE,
            data_type=QuoteTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.symbol],
                         self.data_engine.subscribed_quote_ticks)

    def test_execute_unsubscribe_for_quote_ticks(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            venue=BINANCE,
            data_type=QuoteTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe)

        unsubscribe = Unsubscribe(
            venue=BINANCE,
            data_type=QuoteTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual([], self.data_engine.subscribed_quote_ticks)

    def test_process_quote_tick_when_subscriber_then_sends_to_registered_handler(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            venue=BINANCE,
            data_type=QuoteTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe)

        tick = QuoteTick(
            ETHUSDT_BINANCE.symbol,
            Price("100.003"),
            Price("100.003"),
            Quantity(1),
            Quantity(1),
            UNIX_EPOCH,
        )

        # Act
        self.data_engine.process(tick)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.symbol],
                         self.data_engine.subscribed_quote_ticks)
        self.assertEqual([tick], handler)

    def test_process_quote_tick_when_subscribers_then_sends_to_registered_handlers(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler1 = []
        subscribe1 = Subscribe(
            venue=BINANCE,
            data_type=QuoteTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler1.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        handler2 = []
        subscribe2 = Subscribe(
            venue=BINANCE,
            data_type=QuoteTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler2.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe1)
        self.data_engine.execute(subscribe2)

        tick = QuoteTick(
            ETHUSDT_BINANCE.symbol,
            Price("100.003"),
            Price("100.003"),
            Quantity(1),
            Quantity(1),
            UNIX_EPOCH,
        )

        # Act
        self.data_engine.process(tick)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.symbol],
                         self.data_engine.subscribed_quote_ticks)
        self.assertEqual([tick], handler1)
        self.assertEqual([tick], handler2)

    def test_subscribe_trade_tick_then_subscribes(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            venue=BINANCE,
            data_type=TradeTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual([ETHUSDT_BINANCE.symbol],
                         self.data_engine.subscribed_trade_ticks)

    def test_unsubscribe_trade_tick_then_unsubscribes(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            venue=BINANCE,
            data_type=TradeTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe)

        unsubscribe = Unsubscribe(
            venue=BINANCE,
            data_type=TradeTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual([], self.data_engine.subscribed_trade_ticks)

    def test_process_trade_tick_when_subscriber_then_sends_to_registered_handler(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler = []
        subscribe = Subscribe(
            venue=BINANCE,
            data_type=TradeTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe)

        tick = TradeTick(
            ETHUSDT_BINANCE.symbol,
            Price("1050.00000"),
            Quantity(100),
            OrderSide.BUY,
            TradeMatchId("123456789"),
            UNIX_EPOCH,
        )

        # Act
        self.data_engine.process(tick)

        # Assert
        self.assertEqual([tick], handler)

    def test_process_trade_tick_when_subscribers_then_sends_to_registered_handlers(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        handler1 = []
        subscribe1 = Subscribe(
            venue=BINANCE,
            data_type=TradeTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler1.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        handler2 = []
        subscribe2 = Subscribe(
            venue=BINANCE,
            data_type=TradeTick,
            metadata={"Symbol": ETHUSDT_BINANCE.symbol},
            handler=handler2.append,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe1)
        self.data_engine.execute(subscribe2)

        tick = TradeTick(
            ETHUSDT_BINANCE.symbol,
            Price("1050.00000"),
            Quantity(100),
            OrderSide.BUY,
            TradeMatchId("123456789"),
            UNIX_EPOCH,
        )

        # Act
        self.data_engine.process(tick)

        # Assert
        self.assertEqual([tick], handler1)
        self.assertEqual([tick], handler2)

    def test_subscribe_bar_type_then_subscribes(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID)
        bar_type = BarType(ETHUSDT_BINANCE.symbol,
                           bar_spec,
                           internal_aggregation=True)

        handler = ObjectStorer()
        subscribe = Subscribe(
            venue=BINANCE,
            data_type=Bar,
            metadata={"BarType": bar_type},
            handler=handler.store_2,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(subscribe)

        # Assert
        self.assertEqual([bar_type], self.data_engine.subscribed_bars)

    def test_unsubscribe_bar_type_then_unsubscribes(self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID)
        bar_type = BarType(ETHUSDT_BINANCE.symbol,
                           bar_spec,
                           internal_aggregation=True)

        handler = ObjectStorer()
        subscribe = Subscribe(
            venue=BINANCE,
            data_type=Bar,
            metadata={"BarType": bar_type},
            handler=handler.store_2,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe)

        unsubscribe = Unsubscribe(
            venue=BINANCE,
            data_type=Bar,
            metadata={"BarType": bar_type},
            handler=handler.store_2,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        # Act
        self.data_engine.execute(unsubscribe)

        # Assert
        self.assertEqual([], self.data_engine.subscribed_bars)

    def test_process_bar_when_subscriber_then_sends_to_registered_handler(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID)
        bar_type = BarType(ETHUSDT_BINANCE.symbol,
                           bar_spec,
                           internal_aggregation=True)

        handler = ObjectStorer()
        subscribe = Subscribe(
            venue=BINANCE,
            data_type=Bar,
            metadata={"BarType": bar_type},
            handler=handler.store_2,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe)

        bar = Bar(
            Price("1051.00000"),
            Price("1055.00000"),
            Price("1050.00000"),
            Price("1052.00000"),
            Quantity(100),
            UNIX_EPOCH,
        )

        data = BarData(bar_type, bar)

        # Act
        self.data_engine.process(data)

        # Assert
        self.assertEqual([(bar_type, bar)], handler.get_store())

    def test_process_bar_when_subscribers_then_sends_to_registered_handlers(
            self):
        # Arrange
        self.data_engine.register_client(self.binance_client)
        self.binance_client.connect()

        bar_spec = BarSpecification(1000, BarAggregation.TICK, PriceType.MID)
        bar_type = BarType(ETHUSDT_BINANCE.symbol,
                           bar_spec,
                           internal_aggregation=True)

        handler1 = ObjectStorer()
        subscribe1 = Subscribe(
            venue=BINANCE,
            data_type=Bar,
            metadata={"BarType": bar_type},
            handler=handler1.store_2,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        handler2 = ObjectStorer()
        subscribe2 = Subscribe(
            venue=BINANCE,
            data_type=Bar,
            metadata={"BarType": bar_type},
            handler=handler2.store_2,
            command_id=self.uuid_factory.generate(),
            command_timestamp=self.clock.utc_now(),
        )

        self.data_engine.execute(subscribe1)
        self.data_engine.execute(subscribe2)

        bar = Bar(
            Price("1051.00000"),
            Price("1055.00000"),
            Price("1050.00000"),
            Price("1052.00000"),
            Quantity(100),
            UNIX_EPOCH,
        )

        data = BarData(bar_type, bar)

        # Act
        self.data_engine.process(data)

        # Assert
        self.assertEqual([(bar_type, bar)], handler1.get_store())
        self.assertEqual([(bar_type, bar)], handler2.get_store())
Example #6
0
class LiveExecutionPerformanceTests(unittest.TestCase):

    def setUp(self):
        # Fixture Setup
        self.clock = LiveClock()
        self.uuid_factory = UUIDFactory()
        self.logger = TestLogger(self.clock, bypass_logging=True)

        self.trader_id = TraderId("TESTER", "000")
        self.account_id = AccountId("BINANCE", "001")

        self.portfolio = Portfolio(
            clock=self.clock,
            logger=self.logger,
        )
        self.portfolio.register_cache(DataCache(self.logger))

        self.analyzer = PerformanceAnalyzer()

        # Fresh isolated loop testing pattern
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)

        database = BypassExecutionDatabase(trader_id=self.trader_id, logger=self.logger)
        self.exec_engine = LiveExecutionEngine(
            loop=self.loop,
            database=database,
            portfolio=self.portfolio,
            clock=self.clock,
            logger=self.logger,
        )

        exec_client = MockExecutionClient(
            venue=Venue("BINANCE"),
            account_id=self.account_id,
            engine=self.exec_engine,
            clock=self.clock,
            logger=self.logger,
        )

        self.exec_engine.register_client(exec_client)
        self.exec_engine.process(TestStubs.event_account_state(self.account_id))

        self.strategy = TradingStrategy(order_id_tag="001")
        self.strategy.register_trader(
            TraderId("TESTER", "000"),
            self.clock,
            self.logger,
        )

        self.exec_engine.register_strategy(self.strategy)

    def submit_order(self):
        order = self.strategy.order_factory.market(
            BTCUSDT_BINANCE.id,
            OrderSide.BUY,
            Quantity("1.00000000"),
        )

        self.strategy.submit_order(order)

    def test_execute_command(self):
        order = self.strategy.order_factory.market(
            BTCUSDT_BINANCE.id,
            OrderSide.BUY,
            Quantity("1.00000000"),
        )

        command = SubmitOrder(
            order.venue,
            self.trader_id,
            self.account_id,
            self.strategy.id,
            PositionId.null(),
            order,
            self.uuid_factory.generate(),
            self.clock.utc_now(),
        )

        def execute_command():
            self.exec_engine.execute(command)

        PerformanceHarness.profile_function(execute_command, 10000, 1)
        # ~0.0ms / ~0.3μs / 253ns minimum of 10,000 runs @ 1 iteration each run.

    def test_submit_order(self):
        self.exec_engine.start()
        time.sleep(0.1)

        async def run_test():
            def submit_order():
                order = self.strategy.order_factory.market(
                    BTCUSDT_BINANCE.id,
                    OrderSide.BUY,
                    Quantity("1.00000000"),
                )

                self.strategy.submit_order(order)

            PerformanceHarness.profile_function(submit_order, 10000, 1)
        self.loop.run_until_complete(run_test())
        # ~0.0ms / ~24.5μs / 24455ns minimum of 10,000 runs @ 1 iteration each run.

    def test_submit_order_end_to_end(self):
        self.exec_engine.start()
        time.sleep(0.1)

        async def run_test():
            for _ in range(10000):
                order = self.strategy.order_factory.market(
                    BTCUSDT_BINANCE.id,
                    OrderSide.BUY,
                    Quantity("1.00000000"),
                )

                self.strategy.submit_order(order)

        stats_file = "perf_live_execution.prof"
        cProfile.runctx("self.loop.run_until_complete(run_test())", globals(), locals(), stats_file)
        s = pstats.Stats(stats_file)
        s.strip_dirs().sort_stats("time").print_stats()
class AccountTests(unittest.TestCase):
    def setUp(self):
        # Fixture Setup
        self.clock = TestClock()
        logger = Logger(self.clock)
        self.order_factory = OrderFactory(
            trader_id=TraderId("TESTER", "000"),
            strategy_id=StrategyId("S", "001"),
            clock=TestClock(),
        )

        self.portfolio = Portfolio(self.clock, logger)
        self.portfolio.register_cache(DataCache(logger))

    def test_instantiated_accounts_basic_properties(self):
        # Arrange
        event = AccountState(
            AccountId("SIM", "001"),
            [Money(1_000_000, USD)],
            [Money(1_000_000, USD)],
            [Money(0, USD)],
            info={"default_currency": "USD"},  # Set the default currency
            event_id=uuid4(),
            timestamp_ns=0,
        )

        # Act
        account = Account(event)

        # Wire up account to portfolio
        account.register_portfolio(self.portfolio)
        self.portfolio.register_account(account)

        # Assert
        self.assertEqual(AccountId("SIM", "001"), account.id)
        self.assertEqual("Account(id=SIM-001)", str(account))
        self.assertEqual("Account(id=SIM-001)", repr(account))
        self.assertEqual(int, type(hash(account)))
        self.assertTrue(account == account)
        self.assertFalse(account != account)

    def test_instantiate_single_asset_account(self):
        # Arrange
        event = AccountState(
            AccountId("SIM", "001"),
            [Money(1_000_000, USD)],
            [Money(1_000_000, USD)],
            [Money(0, USD)],
            info={"default_currency": "USD"},  # Set the default currency
            event_id=uuid4(),
            timestamp_ns=0,
        )

        # Act
        account = Account(event)

        # Wire up account to portfolio
        account.register_portfolio(self.portfolio)
        self.portfolio.register_account(account)

        # Assert
        self.assertEqual(USD, account.default_currency)
        self.assertEqual(event, account.last_event)
        self.assertEqual([event], account.events)
        self.assertEqual(1, account.event_count)
        self.assertEqual(Money(1_000_000, USD), account.balance())
        self.assertEqual(Money(1_000_000, USD), account.balance_free())
        self.assertEqual(Money(0, USD), account.balance_locked())
        self.assertEqual({USD: Money(1_000_000, USD)}, account.balances())
        self.assertEqual({USD: Money(1_000_000, USD)}, account.balances_free())
        self.assertEqual({USD: Money(0, USD)}, account.balances_locked())
        self.assertEqual(Money(0, USD), account.unrealized_pnl())
        self.assertEqual(Money(1_000_000, USD), account.equity())
        self.assertEqual({}, account.initial_margins())
        self.assertEqual({}, account.maint_margins())
        self.assertEqual(None, account.initial_margin())
        self.assertEqual(None, account.maint_margin())

    def test_instantiate_multi_asset_account(self):
        # Arrange
        event = AccountState(
            AccountId("SIM", "001"),
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("0.00000000", BTC),
             Money("0.00000000", ETH)],
            info={},  # No default currency set
            event_id=uuid4(),
            timestamp_ns=0,
        )

        # Act
        account = Account(event)

        # Wire up account to portfolio
        account.register_portfolio(self.portfolio)
        self.portfolio.register_account(account)

        # Assert
        self.assertEqual(AccountId("SIM", "001"), account.id)
        self.assertEqual(None, account.default_currency)
        self.assertEqual(event, account.last_event)
        self.assertEqual([event], account.events)
        self.assertEqual(1, account.event_count)
        self.assertEqual(Money("10.00000000", BTC), account.balance(BTC))
        self.assertEqual(Money("20.00000000", ETH), account.balance(ETH))
        self.assertEqual(Money("10.00000000", BTC), account.balance_free(BTC))
        self.assertEqual(Money("20.00000000", ETH), account.balance_free(ETH))
        self.assertEqual(Money("0.00000000", BTC), account.balance_locked(BTC))
        self.assertEqual(Money("0.00000000", ETH), account.balance_locked(ETH))
        self.assertEqual(
            {
                BTC: Money("10.00000000", BTC),
                ETH: Money("20.00000000", ETH)
            },
            account.balances(),
        )
        self.assertEqual(
            {
                BTC: Money("10.00000000", BTC),
                ETH: Money("20.00000000", ETH)
            },
            account.balances_free(),
        )
        self.assertEqual(
            {
                BTC: Money("0.00000000", BTC),
                ETH: Money("0.00000000", ETH)
            },
            account.balances_locked(),
        )
        self.assertEqual(Money("0.00000000", BTC), account.unrealized_pnl(BTC))
        self.assertEqual(Money("0.00000000", ETH), account.unrealized_pnl(ETH))
        self.assertEqual(Money("10.00000000", BTC), account.equity(BTC))
        self.assertEqual(Money("20.00000000", ETH), account.equity(ETH))
        self.assertEqual({}, account.initial_margins())
        self.assertEqual({}, account.maint_margins())
        self.assertEqual(None, account.initial_margin(BTC))
        self.assertEqual(None, account.initial_margin(ETH))
        self.assertEqual(None, account.maint_margin(BTC))
        self.assertEqual(None, account.maint_margin(ETH))

    def test_apply_given_new_state_event_updates_correctly(self):
        # Arrange
        event1 = AccountState(
            AccountId("SIM", "001"),
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("0.00000000", BTC),
             Money("0.00000000", ETH)],
            info={},  # No default currency set
            event_id=uuid4(),
            timestamp_ns=0,
        )

        # Act
        account = Account(event1)

        # Wire up account to portfolio
        account.register_portfolio(self.portfolio)
        self.portfolio.register_account(account)

        event2 = AccountState(
            AccountId("SIM", "001"),
            [Money("9.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("8.50000000", BTC),
             Money("20.00000000", ETH)],
            [Money("0.50000000", BTC),
             Money("0.00000000", ETH)],
            info={},  # No default currency set
            event_id=uuid4(),
            timestamp_ns=0,
        )

        # Act
        account.apply(event=event2)

        # Assert
        self.assertEqual(event2, account.last_event)
        self.assertEqual([event1, event2], account.events)
        self.assertEqual(2, account.event_count)
        self.assertEqual(Money("9.00000000", BTC), account.balance(BTC))
        self.assertEqual(Money("8.50000000", BTC), account.balance_free(BTC))
        self.assertEqual(Money("0.50000000", BTC), account.balance_locked(BTC))
        self.assertEqual(Money("20.00000000", ETH), account.balance(ETH))
        self.assertEqual(Money("20.00000000", ETH), account.balance_free(ETH))
        self.assertEqual(Money("0.00000000", ETH), account.balance_locked(ETH))

    def test_update_initial_margin(self):
        # Arrange
        event = AccountState(
            AccountId("SIM", "001"),
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("0.00000000", BTC),
             Money("0.00000000", ETH)],
            info={},  # No default currency set
            event_id=uuid4(),
            timestamp_ns=0,
        )

        # Act
        account = Account(event)

        # Wire up account to portfolio
        account.register_portfolio(self.portfolio)
        self.portfolio.register_account(account)

        margin = Money("0.00100000", BTC)

        # Act
        account.update_initial_margin(margin)

        # Assert
        self.assertEqual(margin, account.initial_margin(BTC))
        self.assertEqual({BTC: margin}, account.initial_margins())

    def test_update_maint_margin(self):
        # Arrange
        event = AccountState(
            AccountId("SIM", "001"),
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("0.00000000", BTC),
             Money("0.00000000", ETH)],
            info={},  # No default currency set
            event_id=uuid4(),
            timestamp_ns=0,
        )

        # Act
        account = Account(event)

        # Wire up account to portfolio
        account.register_portfolio(self.portfolio)
        self.portfolio.register_account(account)

        margin = Money("0.00050000", BTC)

        # Act
        account.update_maint_margin(margin)

        # Assert
        self.assertEqual(margin, account.maint_margin(BTC))
        self.assertEqual({BTC: margin}, account.maint_margins())

    def test_unrealized_pnl_with_single_asset_account_when_no_open_positions_returns_zero(
        self, ):
        # Arrange
        event = AccountState(
            AccountId("SIM", "001"),
            balances=[Money(1_000_000, USD)],
            balances_free=[Money(1_000_000, USD)],
            balances_locked=[Money(0, USD)],
            info={"default_currency": "USD"},  # No default currency set
            event_id=uuid4(),
            timestamp_ns=0,
        )

        account = Account(event)

        # Wire up account to portfolio
        account.register_portfolio(self.portfolio)
        self.portfolio.register_account(account)

        # Act
        result = account.unrealized_pnl()

        # Assert
        self.assertEqual(Money(0, USD), result)

    def test_unrealized_pnl_with_multi_asset_account_when_no_open_positions_returns_zero(
        self, ):
        # Arrange
        event = AccountState(
            AccountId("SIM", "001"),
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("0.00000000", BTC),
             Money("0.00000000", ETH)],
            info={},  # No default currency set
            event_id=uuid4(),
            timestamp_ns=0,
        )

        account = Account(event)

        # Wire up account to portfolio
        account.register_portfolio(self.portfolio)
        self.portfolio.register_account(account)

        # Act
        result = account.unrealized_pnl(BTC)

        # Assert
        self.assertEqual(Money("0.00000000", BTC), result)

    def test_equity_with_single_asset_account_no_default_returns_none(self):
        # Arrange
        event = AccountState(
            AccountId("SIM", "001"),
            [Money("100000.00", USD)],
            [Money("0.00", USD)],
            [Money("0.00", USD)],
            info={},  # No default currency set
            event_id=uuid4(),
            timestamp_ns=0,
        )

        account = Account(event)

        # Wire up account to portfolio
        account.register_portfolio(self.portfolio)
        self.portfolio.register_account(account)

        # Act
        result = account.equity(BTC)

        # Assert
        self.assertIsNone(result)

    def test_equity_with_single_asset_account_returns_expected_money(self):
        # Arrange
        event = AccountState(
            AccountId("SIM", "001"),
            [Money("100000.00", USD)],
            [Money("0.00", USD)],
            [Money("0.00", USD)],
            info={"default_currency": "USD"},  # No default currency set
            event_id=uuid4(),
            timestamp_ns=0,
        )

        account = Account(event)

        # Wire up account to portfolio
        account.register_portfolio(self.portfolio)
        self.portfolio.register_account(account)

        # Act
        result = account.equity()

        # Assert
        self.assertEqual(Money("100000.00", USD), result)

    def test_equity_with_multi_asset_account_returns_expected_money(self):
        # Arrange
        event = AccountState(
            AccountId("SIM", "001"),
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("0.00000000", BTC),
             Money("0.00000000", ETH)],
            info={},  # No default currency set
            event_id=uuid4(),
            timestamp_ns=0,
        )

        account = Account(event)

        # Wire up account to portfolio
        account.register_portfolio(self.portfolio)
        self.portfolio.register_account(account)

        # Act
        result = account.equity(BTC)

        # Assert
        self.assertEqual(Money("10.00000000", BTC), result)

    def test_margin_available_for_single_asset_account(self):
        # Arrange
        event = AccountState(
            AccountId("SIM", "001"),
            [Money("100000.00", USD)],
            [Money("0.00", USD)],
            [Money("0.00", USD)],
            info={"default_currency": "USD"},  # No default currency set
            event_id=uuid4(),
            timestamp_ns=0,
        )

        account = Account(event)

        # Wire up account to portfolio
        account.register_portfolio(self.portfolio)
        self.portfolio.register_account(account)

        # Act
        result1 = account.margin_available()
        account.update_initial_margin(Money("500.00", USD))
        result2 = account.margin_available()
        account.update_maint_margin(Money("1000.00", USD))
        result3 = account.margin_available()

        # Assert
        self.assertEqual(Money("100000.00", USD), result1)
        self.assertEqual(Money("99500.00", USD), result2)
        self.assertEqual(Money("98500.00", USD), result3)

    def test_margin_available_for_multi_asset_account(self):
        # Arrange
        event = AccountState(
            AccountId("SIM", "001"),
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("10.00000000", BTC),
             Money("20.00000000", ETH)],
            [Money("0.00000000", BTC),
             Money("0.00000000", ETH)],
            info={},  # No default currency set
            event_id=uuid4(),
            timestamp_ns=0,
        )

        account = Account(event)

        # Wire up account to portfolio
        account.register_portfolio(self.portfolio)
        self.portfolio.register_account(account)

        # Act
        result1 = account.margin_available(BTC)
        account.update_initial_margin(Money("0.00010000", BTC))
        result2 = account.margin_available(BTC)
        account.update_maint_margin(Money("0.00020000", BTC))
        result3 = account.margin_available(BTC)
        result4 = account.margin_available(ETH)

        # Assert
        self.assertEqual(Money("10.00000000", BTC), result1)
        self.assertEqual(Money("9.99990000", BTC), result2)
        self.assertEqual(Money("9.99970000", BTC), result3)
        self.assertEqual(Money("20.00000000", ETH), result4)
class PortfolioTests(unittest.TestCase):

    def setUp(self):
        # Fixture Setup
        clock = TestClock()
        logger = TestLogger(clock)
        self.order_factory = OrderFactory(
            trader_id=TraderId("TESTER", "000"),
            strategy_id=StrategyId("S", "001"),
            clock=TestClock(),
        )

        state = AccountState(
            account_id=AccountId("BINANCE", "1513111"),
            balances=[Money("10.00000000", BTC)],
            balances_free=[Money("0.00000000", BTC)],
            balances_locked=[Money("0.00000000", BTC)],
            info={},
            event_id=uuid4(),
            event_timestamp=UNIX_EPOCH,
        )

        self.data_cache = DataCache(logger)
        self.account = Account(state)

        self.portfolio = Portfolio(clock, logger)
        self.portfolio.register_account(self.account)
        self.portfolio.register_cache(self.data_cache)

        self.data_cache.add_instrument(AUDUSD_SIM)
        self.data_cache.add_instrument(GBPUSD_SIM)
        self.data_cache.add_instrument(BTCUSDT_BINANCE)
        self.data_cache.add_instrument(BTCUSD_BITMEX)
        self.data_cache.add_instrument(ETHUSD_BITMEX)

    def test_account_when_no_account_returns_none(self):
        # Arrange
        # Act
        # Assert
        self.assertIsNone(self.portfolio.account(SIM))

    def test_account_when_account_returns_the_account_facade(self):
        # Arrange
        # Act
        result = self.portfolio.account(BINANCE)

        # Assert
        self.assertEqual(self.account, result)

    def test_net_position_when_no_positions_returns_zero(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual(Decimal(0), self.portfolio.net_position(AUDUSD_SIM.symbol))

    def test_is_net_long_when_no_positions_returns_false(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual(False, self.portfolio.is_net_long(AUDUSD_SIM.symbol))

    def test_is_net_short_when_no_positions_returns_false(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual(False, self.portfolio.is_net_short(AUDUSD_SIM.symbol))

    def test_is_flat_when_no_positions_returns_true(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual(True, self.portfolio.is_flat(AUDUSD_SIM.symbol))

    def test_is_completely_flat_when_no_positions_returns_true(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual(True, self.portfolio.is_flat(AUDUSD_SIM.symbol))

    def test_unrealized_pnl_for_symbol_when_no_instrument_returns_none(self):
        # Arrange
        # Act
        # Assert
        self.assertIsNone(self.portfolio.unrealized_pnl(USDJPY_SIM.symbol))

    def test_unrealized_pnl_for_venue_when_no_account_returns_empty_dict(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual({}, self.portfolio.unrealized_pnls(SIM))

    def test_initial_margins_when_no_account_returns_none(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual(None, self.portfolio.initial_margins(SIM))

    def test_maint_margins_when_no_account_returns_none(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual(None, self.portfolio.maint_margins(SIM))

    def test_open_value_when_no_account_returns_none(self):
        # Arrange
        # Act
        # Assert
        self.assertEqual(None, self.portfolio.market_values(SIM))

    def test_update_tick(self):
        # Arrange
        tick = TestStubs.quote_tick_5decimal(GBPUSD_SIM.symbol)

        # Act
        self.portfolio.update_tick(tick)

        # Assert
        self.assertIsNone(self.portfolio.unrealized_pnl(GBPUSD_SIM.symbol))

    def test_update_orders_working(self):
        # Arrange
        self.portfolio.register_account(self.account)

        # Create two working orders
        order1 = self.order_factory.stop_market(
            BTCUSDT_BINANCE.symbol,
            OrderSide.BUY,
            Quantity("10.5"),
            Price("25000.00"),
        )

        order2 = self.order_factory.stop_market(
            BTCUSDT_BINANCE.symbol,
            OrderSide.BUY,
            Quantity("10.5"),
            Price("25000.00"),
        )

        # Push state to FILLED
        order1.apply(TestStubs.event_order_submitted(order1))
        order1.apply(TestStubs.event_order_accepted(order1))
        filled1 = TestStubs.event_order_filled(
            order1,
            instrument=BTCUSDT_BINANCE,
            position_id=PositionId("P-1"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("25000.00"),
        )
        order1.apply(filled1)

        # Push state to ACCEPTED
        order2.apply(TestStubs.event_order_submitted(order2))
        order2.apply(TestStubs.event_order_accepted(order2))

        # Update the last quote
        last = QuoteTick(
            BTCUSDT_BINANCE.symbol,
            Price("25001.00"),
            Price("25002.00"),
            Quantity(1),
            Quantity(1),
            UNIX_EPOCH,
        )

        # Act
        self.portfolio.update_tick(last)
        self.portfolio.initialize_orders({order1, order2})

        # Assert
        self.assertEqual({}, self.portfolio.initial_margins(BINANCE))

    def test_update_positions(self):
        # Arrange
        self.portfolio.register_account(self.account)

        # Create a closed position
        order1 = self.order_factory.market(
            BTCUSDT_BINANCE.symbol,
            OrderSide.BUY,
            Quantity("10.50000000"),
        )

        order2 = self.order_factory.market(
            BTCUSDT_BINANCE.symbol,
            OrderSide.SELL,
            Quantity("10.50000000"),
        )

        filled1 = TestStubs.event_order_filled(
            order1,
            instrument=BTCUSDT_BINANCE,
            position_id=PositionId("P-1"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("25000.00"),
        )

        filled2 = TestStubs.event_order_filled(
            order2,
            instrument=BTCUSDT_BINANCE,
            position_id=PositionId("P-1"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("25000.00"),
        )

        position1 = Position(filled1)
        position1.apply(filled2)

        order3 = self.order_factory.market(
            BTCUSDT_BINANCE.symbol,
            OrderSide.BUY,
            Quantity("10.00000000"),
        )

        filled3 = TestStubs.event_order_filled(
            order3,
            instrument=BTCUSDT_BINANCE,
            position_id=PositionId("P-2"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("25000.00"),
        )

        position2 = Position(filled3)

        # Update the last quote
        last = QuoteTick(
            BTCUSDT_BINANCE.symbol,
            Price("25001.00"),
            Price("25002.00"),
            Quantity(1),
            Quantity(1),
            UNIX_EPOCH,
        )

        # Act
        self.portfolio.initialize_positions({position1, position2})
        self.portfolio.update_tick(last)

        # Assert
        self.assertTrue(self.portfolio.is_net_long(BTCUSDT_BINANCE.symbol))

    def test_opening_one_long_position_updates_portfolio(self):
        # Arrange
        order = self.order_factory.market(
            BTCUSDT_BINANCE.symbol,
            OrderSide.BUY,
            Quantity("10.000000"),
        )

        fill = TestStubs.event_order_filled(
            order=order,
            instrument=BTCUSDT_BINANCE,
            position_id=PositionId("P-123456"),
            strategy_id=StrategyId("S", "001"),
            fill_price=Price("10500.00"),
        )

        last = QuoteTick(
            BTCUSDT_BINANCE.symbol,
            Price("10510.00"),
            Price("10511.00"),
            Quantity("1.000000"),
            Quantity("1.000000"),
            UNIX_EPOCH,
        )

        self.data_cache.add_quote_tick(last)
        self.portfolio.update_tick(last)

        position = Position(fill)

        # Act
        self.portfolio.update_position(TestStubs.event_position_opened(position))

        # Assert
        self.assertEqual({USDT: Money("105100.00000000", USDT)}, self.portfolio.market_values(BINANCE))
        self.assertEqual({USDT: Money("100.00000000", USDT)}, self.portfolio.unrealized_pnls(BINANCE))
        self.assertEqual({}, self.portfolio.maint_margins(BINANCE))
        self.assertEqual(Money("105100.00000000", USDT), self.portfolio.market_value(BTCUSDT_BINANCE.symbol))
        self.assertEqual(Money("100.00000000", USDT), self.portfolio.unrealized_pnl(BTCUSDT_BINANCE.symbol))
        self.assertEqual(Decimal("10.00000000"), self.portfolio.net_position(order.symbol))
        self.assertTrue(self.portfolio.is_net_long(order.symbol))
        self.assertFalse(self.portfolio.is_net_short(order.symbol))
        self.assertFalse(self.portfolio.is_flat(order.symbol))
        self.assertFalse(self.portfolio.is_completely_flat())

    def test_opening_one_short_position_updates_portfolio(self):
        # Arrange
        order = self.order_factory.market(
            BTCUSDT_BINANCE.symbol,
            OrderSide.SELL,
            Quantity("0.515"),
        )

        fill = TestStubs.event_order_filled(
            order=order,
            instrument=BTCUSDT_BINANCE,
            position_id=PositionId("P-123456"),
            strategy_id=StrategyId("S", "001"),
            fill_price=Price("15000.00")
        )

        last = QuoteTick(
            BTCUSDT_BINANCE.symbol,
            Price("15510.15"),
            Price("15510.25"),
            Quantity("12.62"),
            Quantity("3.1"),
            UNIX_EPOCH,
        )

        self.data_cache.add_quote_tick(last)
        self.portfolio.update_tick(last)

        position = Position(fill)

        # Act
        self.portfolio.update_position(TestStubs.event_position_opened(position))

        # Assert
        self.assertEqual({USDT: Money("7987.77875000", USDT)}, self.portfolio.market_values(BINANCE))
        self.assertEqual({USDT: Money("-262.77875000", USDT)}, self.portfolio.unrealized_pnls(BINANCE))
        self.assertEqual({}, self.portfolio.maint_margins(BINANCE))
        self.assertEqual(Money("7987.77875000", USDT), self.portfolio.market_value(BTCUSDT_BINANCE.symbol))
        self.assertEqual(Money("-262.77875000", USDT), self.portfolio.unrealized_pnl(BTCUSDT_BINANCE.symbol))
        self.assertEqual(Decimal("-0.515"), self.portfolio.net_position(order.symbol))
        self.assertFalse(self.portfolio.is_net_long(order.symbol))
        self.assertTrue(self.portfolio.is_net_short(order.symbol))
        self.assertFalse(self.portfolio.is_flat(order.symbol))
        self.assertFalse(self.portfolio.is_completely_flat())

    def test_opening_positions_with_multi_asset_account(self):
        # Arrange
        state = AccountState(
            account_id=AccountId("BITMEX", "01234"),
            balances=[Money("10.00000000", BTC), Money("10.00000000", ETH)],
            balances_free=[Money("0.00000000", BTC), Money("10.00000000", ETH)],
            balances_locked=[Money("0.00000000", BTC), Money("0.00000000", ETH)],
            info={},
            event_id=uuid4(),
            event_timestamp=UNIX_EPOCH,
        )

        account = Account(state)

        self.portfolio.register_account(account)

        last_ethusd = QuoteTick(
            ETHUSD_BITMEX.symbol,
            Price("376.05"),
            Price("377.10"),
            Quantity("16"),
            Quantity("25"),
            UNIX_EPOCH,
        )

        last_btcusd = QuoteTick(
            BTCUSD_BITMEX.symbol,
            Price("10500.05"),
            Price("10501.51"),
            Quantity("2.54"),
            Quantity("0.91"),
            UNIX_EPOCH,
        )

        self.data_cache.add_quote_tick(last_ethusd)
        self.data_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.symbol,
            OrderSide.BUY,
            Quantity(10000),
        )

        fill = TestStubs.event_order_filled(
            order=order,
            instrument=ETHUSD_BITMEX,
            position_id=PositionId("P-123456"),
            strategy_id=StrategyId("S", "001"),
            fill_price=Price("376.05"),
        )

        position = Position(fill)

        # Act
        self.portfolio.update_position(TestStubs.event_position_opened(position))

        # Assert
        self.assertEqual({ETH: Money("2.65922085", ETH)}, self.portfolio.market_values(BITMEX))
        self.assertEqual({ETH: Money("0.03855870", ETH)}, self.portfolio.maint_margins(BITMEX))
        self.assertEqual(Money("2.65922085", ETH), self.portfolio.market_value(ETHUSD_BITMEX.symbol))
        self.assertEqual(Money("0.00000000", ETH), self.portfolio.unrealized_pnl(ETHUSD_BITMEX.symbol))

    def test_unrealized_pnl_when_insufficient_data_for_xrate_returns_none(self):
        # Arrange
        state = AccountState(
            account_id=AccountId("BITMEX", "01234"),
            balances=[Money("10.00000000", BTC), Money("10.00000000", ETH)],
            balances_free=[Money("10.00000000", BTC), Money("10.00000000", ETH)],
            balances_locked=[Money("0.00000000", BTC), Money("0.00000000", ETH)],
            info={},
            event_id=uuid4(),
            event_timestamp=UNIX_EPOCH,
        )

        account = Account(state)

        self.portfolio.register_account(account)
        order = self.order_factory.market(
            ETHUSD_BITMEX.symbol,
            OrderSide.BUY,
            Quantity(100),
        )

        fill = TestStubs.event_order_filled(
            order=order,
            instrument=ETHUSD_BITMEX,
            position_id=PositionId("P-123456"),
            strategy_id=StrategyId("S", "001"),
            fill_price=Price("376.05"),
        )

        position = Position(fill)

        self.portfolio.update_position(TestStubs.event_position_opened(position))

        # Act
        result = self.portfolio.unrealized_pnls(BITMEX)

        # # Assert
        self.assertIsNone(result)

    def test_market_value_when_insufficient_data_for_xrate_returns_none(self):
        # Arrange
        state = AccountState(
            account_id=AccountId("BITMEX", "01234"),
            balances=[Money("10.00000000", BTC), Money("10.00000000", ETH)],
            balances_free=[Money("10.00000000", BTC), Money("10.00000000", ETH)],
            balances_locked=[Money("0.00000000", BTC), Money("0.00000000", ETH)],
            info={},
            event_id=uuid4(),
            event_timestamp=UNIX_EPOCH,
        )

        account = Account(state)

        self.portfolio.register_account(account)

        order = self.order_factory.market(
            ETHUSD_BITMEX.symbol,
            OrderSide.BUY,
            Quantity(100),
        )

        fill = TestStubs.event_order_filled(
            order=order,
            instrument=ETHUSD_BITMEX,
            position_id=PositionId("P-123456"),
            strategy_id=StrategyId("S", "001"),
            fill_price=Price("376.05"),
        )

        last_ethusd = QuoteTick(
            ETHUSD_BITMEX.symbol,
            Price("376.05"),
            Price("377.10"),
            Quantity("16"),
            Quantity("25"),
            UNIX_EPOCH,
        )

        position = Position(fill)

        self.portfolio.update_position(TestStubs.event_position_opened(position))
        self.data_cache.add_quote_tick(last_ethusd)
        self.portfolio.update_tick(last_ethusd)

        # Act
        result = self.portfolio.market_values(BITMEX)

        # Assert
        # TODO: Currently no Quanto thus no xrate required
        self.assertEqual({ETH: Money('0.02659221', ETH)}, result)

    def test_opening_several_positions_updates_portfolio(self):
        # Arrange
        state = AccountState(
            AccountId("SIM", "01234"),
            balances=[Money(1_000_000.00, USD)],
            balances_free=[Money(1_000_000.00, USD)],
            balances_locked=[Money(0.00, USD)],
            info={"default_currency": "USD"},
            event_id=uuid4(),
            event_timestamp=UNIX_EPOCH,
        )

        account = Account(state)

        self.portfolio.register_account(account)

        last_audusd = QuoteTick(
            AUDUSD_SIM.symbol,
            Price("0.80501"),
            Price("0.80505"),
            Quantity(1),
            Quantity(1),
            UNIX_EPOCH,
        )

        last_gbpusd = QuoteTick(
            GBPUSD_SIM.symbol,
            Price("1.30315"),
            Price("1.30317"),
            Quantity(1),
            Quantity(1),
            UNIX_EPOCH,
        )

        self.data_cache.add_quote_tick(last_audusd)
        self.data_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.symbol,
            OrderSide.BUY,
            Quantity(100000),
        )

        order2 = self.order_factory.market(
            GBPUSD_SIM.symbol,
            OrderSide.BUY,
            Quantity(100000),
        )

        order1_filled = TestStubs.event_order_filled(
            order1,
            instrument=AUDUSD_SIM,
            position_id=PositionId("P-1"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("1.00000"),
        )

        order2_filled = TestStubs.event_order_filled(
            order2,
            instrument=AUDUSD_SIM,
            position_id=PositionId("P-2"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("1.00000"),
        )

        position1 = Position(order1_filled)
        position2 = Position(order2_filled)
        position_opened1 = TestStubs.event_position_opened(position1)
        position_opened2 = TestStubs.event_position_opened(position2)

        # Act
        self.portfolio.update_position(position_opened1)
        self.portfolio.update_position(position_opened2)

        # Assert
        self.assertEqual({USD: Money("4216.32", USD)}, self.portfolio.market_values(SIM))
        self.assertEqual({USD: Money("10816.00", USD)}, self.portfolio.unrealized_pnls(SIM))
        self.assertEqual({USD: Money("130.71", USD)}, self.portfolio.maint_margins(SIM))
        self.assertEqual(Money("1610.02", USD), self.portfolio.market_value(AUDUSD_SIM.symbol))
        self.assertEqual(Money("2606.30", USD), self.portfolio.market_value(GBPUSD_SIM.symbol))
        self.assertEqual(Money("-19499.00", USD), self.portfolio.unrealized_pnl(AUDUSD_SIM.symbol))
        self.assertEqual(Money("30315.00", USD), self.portfolio.unrealized_pnl(GBPUSD_SIM.symbol))
        self.assertEqual(Decimal(100000), self.portfolio.net_position(AUDUSD_SIM.symbol))
        self.assertEqual(Decimal(100000), self.portfolio.net_position(GBPUSD_SIM.symbol))
        self.assertTrue(self.portfolio.is_net_long(AUDUSD_SIM.symbol))
        self.assertFalse(self.portfolio.is_net_short(AUDUSD_SIM.symbol))
        self.assertFalse(self.portfolio.is_flat(AUDUSD_SIM.symbol))
        self.assertFalse(self.portfolio.is_completely_flat())

    def test_modifying_position_updates_portfolio(self):
        # Arrange
        state = AccountState(
            AccountId("SIM", "01234"),
            balances=[Money(1_000_000.00, USD)],
            balances_free=[Money(1_000_000.00, USD)],
            balances_locked=[Money(0.00, USD)],
            info={"default_currency": "USD"},
            event_id=uuid4(),
            event_timestamp=UNIX_EPOCH,
        )

        account = Account(state)

        self.portfolio.register_account(account)

        last_audusd = QuoteTick(
            AUDUSD_SIM.symbol,
            Price("0.80501"),
            Price("0.80505"),
            Quantity(1),
            Quantity(1),
            UNIX_EPOCH,
        )

        self.data_cache.add_quote_tick(last_audusd)
        self.portfolio.update_tick(last_audusd)

        order1 = self.order_factory.market(
            AUDUSD_SIM.symbol,
            OrderSide.BUY,
            Quantity(100000),
        )

        order1_filled = TestStubs.event_order_filled(
            order1,
            instrument=AUDUSD_SIM,
            position_id=PositionId("P-123456"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("1.00000"),
        )

        position = Position(order1_filled)

        self.portfolio.update_position(TestStubs.event_position_opened(position))

        order2 = self.order_factory.market(
            AUDUSD_SIM.symbol,
            OrderSide.SELL,
            Quantity(50000),
        )

        order2_filled = TestStubs.event_order_filled(
            order2,
            instrument=AUDUSD_SIM,
            position_id=PositionId("P-123456"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("1.00000"),
        )

        position.apply(order2_filled)

        # Act
        self.portfolio.update_position(TestStubs.event_position_changed(position))

        # Assert
        self.assertEqual({USD: Money("805.01", USD)}, self.portfolio.market_values(SIM))
        self.assertEqual({USD: Money("-9749.50", USD)}, self.portfolio.unrealized_pnls(SIM))
        self.assertEqual({USD: Money("24.96", USD)}, self.portfolio.maint_margins(SIM))
        self.assertEqual(Money("805.01", USD), self.portfolio.market_value(AUDUSD_SIM.symbol))
        self.assertEqual(Money("-9749.50", USD), self.portfolio.unrealized_pnl(AUDUSD_SIM.symbol))
        self.assertEqual(Decimal(50000), self.portfolio.net_position(AUDUSD_SIM.symbol))
        self.assertTrue(self.portfolio.is_net_long(AUDUSD_SIM.symbol))
        self.assertFalse(self.portfolio.is_net_short(AUDUSD_SIM.symbol))
        self.assertFalse(self.portfolio.is_flat(AUDUSD_SIM.symbol))
        self.assertFalse(self.portfolio.is_completely_flat())
        self.assertEqual({}, self.portfolio.unrealized_pnls(BINANCE))
        self.assertEqual({}, self.portfolio.market_values(BINANCE))

    def test_closing_position_updates_portfolio(self):
        # Arrange
        state = AccountState(
            AccountId("SIM", "01234"),
            balances=[Money(1_000_000.00, USD)],
            balances_free=[Money(1_000_000.00, USD)],
            balances_locked=[Money(0.00, USD)],
            info={"default_currency": "USD"},
            event_id=uuid4(),
            event_timestamp=UNIX_EPOCH,
        )

        account = Account(state)

        self.portfolio.register_account(account)

        order1 = self.order_factory.market(
            AUDUSD_SIM.symbol,
            OrderSide.BUY,
            Quantity(100000),
        )

        order1_filled = TestStubs.event_order_filled(
            order1,
            instrument=AUDUSD_SIM,
            position_id=PositionId("P-123456"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("1.00000"),
        )

        position = Position(order1_filled)

        self.portfolio.update_position(TestStubs.event_position_opened(position))

        order2 = self.order_factory.market(
            AUDUSD_SIM.symbol,
            OrderSide.SELL,
            Quantity(100000),
        )

        order2_filled = TestStubs.event_order_filled(
            order2,
            instrument=AUDUSD_SIM,
            position_id=PositionId("P-123456"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("1.00010"),
        )

        position.apply(order2_filled)

        # Act
        self.portfolio.update_position(TestStubs.event_position_closed(position))

        # Assert
        self.assertEqual({}, self.portfolio.market_values(SIM))
        self.assertEqual({}, self.portfolio.unrealized_pnls(SIM))
        self.assertEqual({}, self.portfolio.maint_margins(SIM))
        self.assertEqual(Money("0", USD), self.portfolio.market_value(AUDUSD_SIM.symbol))
        self.assertEqual(Money("0", USD), self.portfolio.unrealized_pnl(AUDUSD_SIM.symbol))
        self.assertEqual(Decimal(0), self.portfolio.net_position(AUDUSD_SIM.symbol))
        self.assertFalse(self.portfolio.is_net_long(AUDUSD_SIM.symbol))
        self.assertFalse(self.portfolio.is_net_short(AUDUSD_SIM.symbol))
        self.assertTrue(self.portfolio.is_flat(AUDUSD_SIM.symbol))
        self.assertTrue(self.portfolio.is_completely_flat())

    def test_several_positions_with_different_symbols_updates_portfolio(self):
        # Arrange
        state = AccountState(
            AccountId("SIM", "01234"),
            balances=[Money(1_000_000.00, USD)],
            balances_free=[Money(1_000_000.00, USD)],
            balances_locked=[Money(0.00, USD)],
            info={"default_currency": "USD"},
            event_id=uuid4(),
            event_timestamp=UNIX_EPOCH,
        )

        account = Account(state)

        self.portfolio.register_account(account)

        order1 = self.order_factory.market(
            AUDUSD_SIM.symbol,
            OrderSide.BUY,
            Quantity(100000),
        )

        order2 = self.order_factory.market(
            AUDUSD_SIM.symbol,
            OrderSide.BUY,
            Quantity(100000),
        )

        order3 = self.order_factory.market(
            GBPUSD_SIM.symbol,
            OrderSide.BUY,
            Quantity(100000),
        )

        order4 = self.order_factory.market(
            GBPUSD_SIM.symbol,
            OrderSide.SELL,
            Quantity(100000),
        )

        order1_filled = TestStubs.event_order_filled(
            order1,
            instrument=GBPUSD_SIM,
            position_id=PositionId("P-1"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("1.00000"),
        )

        order2_filled = TestStubs.event_order_filled(
            order2,
            instrument=GBPUSD_SIM,
            position_id=PositionId("P-2"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("1.00000"),
        )

        order3_filled = TestStubs.event_order_filled(
            order3,
            instrument=GBPUSD_SIM,
            position_id=PositionId("P-3"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("1.00000"),
        )

        order4_filled = TestStubs.event_order_filled(
            order4,
            instrument=GBPUSD_SIM,
            position_id=PositionId("P-3"),
            strategy_id=StrategyId("S", "1"),
            fill_price=Price("1.00100"),
        )

        position1 = Position(order1_filled)
        position2 = Position(order2_filled)
        position3 = Position(order3_filled)

        last_audusd = QuoteTick(
            AUDUSD_SIM.symbol,
            Price("0.80501"),
            Price("0.80505"),
            Quantity(1),
            Quantity(1),
            UNIX_EPOCH,
        )

        last_gbpusd = QuoteTick(
            GBPUSD_SIM.symbol,
            Price("1.30315"),
            Price("1.30317"),
            Quantity(1),
            Quantity(1),
            UNIX_EPOCH,
        )

        self.data_cache.add_quote_tick(last_audusd)
        self.data_cache.add_quote_tick(last_gbpusd)
        self.portfolio.update_tick(last_audusd)
        self.portfolio.update_tick(last_gbpusd)

        # 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(order4_filled)
        self.portfolio.update_position(TestStubs.event_position_closed(position3))

        # Assert
        self.assertEqual({USD: Money("-38998.00", USD)}, self.portfolio.unrealized_pnls(SIM))
        self.assertEqual({USD: Money("3220.04", USD)}, self.portfolio.market_values(SIM))
        self.assertEqual({USD: Money("99.82", USD)}, self.portfolio.maint_margins(SIM))
        self.assertEqual(Money("3220.04", USD), self.portfolio.market_value(AUDUSD_SIM.symbol))
        self.assertEqual(Money("-38998.00", USD), self.portfolio.unrealized_pnl(AUDUSD_SIM.symbol))
        self.assertEqual(Money("0", USD), self.portfolio.unrealized_pnl(GBPUSD_SIM.symbol))
        self.assertEqual(Decimal(200000), self.portfolio.net_position(AUDUSD_SIM.symbol))
        self.assertEqual(Decimal(0), self.portfolio.net_position(GBPUSD_SIM.symbol))
        self.assertTrue(self.portfolio.is_net_long(AUDUSD_SIM.symbol))
        self.assertTrue(self.portfolio.is_flat(GBPUSD_SIM.symbol))
        self.assertFalse(self.portfolio.is_completely_flat())