942 lines
48 KiB
Python
942 lines
48 KiB
Python
from decimal import Decimal
|
||
import logging
|
||
import math
|
||
import random
|
||
from unittest.mock import ANY
|
||
import pytest
|
||
|
||
from connection.tcp_connection_manager import TcpConnectionManager
|
||
|
||
from proto.common_pb2 import Instrument, Side
|
||
from proto.info_pb2 import PriceLevel, SubscriptionType
|
||
from tests.test_client.admin_test_client import AdminClientConnectionHandlerFactory, AdminTestClient
|
||
from tests.test_client.info_test_client import InfoClientConnectionHandlerFactory, InfoTestClient
|
||
from tests.test_client.order_book_test_client import OrderBookClientConnectionHandlerFactory, OrderBookTestClient
|
||
from tests.common.auth_tests import AuthenticationTests
|
||
from tests.common.mock_expectations import CallExpectationsManager
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
|
||
class TestInfoSystem(AuthenticationTests):
|
||
PROTOCOL = "info"
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def setup(self, request: pytest.FixtureRequest) -> None:
|
||
self.test_name = request.node.name
|
||
logger.info(f"Setting up test: {self.test_name}")
|
||
self.tcp_connection_manager = TcpConnectionManager()
|
||
self.call_expectations_manager = CallExpectationsManager()
|
||
self.call_expectations_manager.setup_network(self.tcp_connection_manager)
|
||
self.info_client_factory = InfoClientConnectionHandlerFactory(self.call_expectations_manager)
|
||
self._admin_factory = AdminClientConnectionHandlerFactory(self.call_expectations_manager)
|
||
self._order_book_factory = OrderBookClientConnectionHandlerFactory(self.call_expectations_manager)
|
||
self._admin_client: AdminTestClient | None = None
|
||
self._order_book_client: OrderBookTestClient | None = None
|
||
self._next_instrument_id = 1
|
||
|
||
def _connect_unauthenticated(self) -> InfoTestClient:
|
||
return self.tcp_connection_manager.connect(self.server_address, self.info_client_factory)
|
||
|
||
def _connect_and_login(self, username: str | None = None) -> InfoTestClient:
|
||
"""Connects to info, logs in (if auth required), and expects OnInstrument for existing instruments."""
|
||
client = self._connect_unauthenticated()
|
||
if self.auth_required:
|
||
client.test_login(username=username or self.test_name)
|
||
if self.instrument is not None:
|
||
client.expect_on_instrument_event(self.instrument, tick_size=self.tick_size, order_book_id=self.order_book_id)
|
||
return client
|
||
|
||
def _get_admin_client(self) -> AdminTestClient:
|
||
if self._admin_client is None:
|
||
admin_address = self.orchestrator.get_server_address("admin")
|
||
self._admin_client = self.tcp_connection_manager.connect(admin_address, self._admin_factory)
|
||
return self._admin_client
|
||
|
||
def _get_order_book_client(self) -> OrderBookTestClient:
|
||
if self._order_book_client is None:
|
||
ob_address = self.orchestrator.get_server_address("order_book")
|
||
self._order_book_client = self.tcp_connection_manager.connect(ob_address, self._order_book_factory)
|
||
if self.order_book_id is not None:
|
||
self._order_book_client.expect_on_order_book_created_event(
|
||
tick_size=self.tick_size, order_book_id=self.order_book_id)
|
||
self.call_expectations_manager.verify_expectations(assert_no_pending_calls=False)
|
||
self._order_book_client._last_order_book_id = self.order_book_id
|
||
return self._order_book_client
|
||
|
||
def _create_instrument_via_admin(self, instrument: Instrument | None = None, tick_size: Decimal = Decimal("0.01")) -> tuple[Instrument, Decimal, int]:
|
||
"""Creates a test instrument via admin. Sets self.instrument, self.tick_size, self.order_book_id.
|
||
Returns (instrument, tick_size, order_book_id) for convenience in multi-instrument tests."""
|
||
self.tick_size = tick_size
|
||
if instrument is None:
|
||
self.instrument = Instrument(
|
||
symbol=f"TEST.{self._next_instrument_id}",
|
||
description="Test instrument",
|
||
currency="USD",
|
||
multiplier=1.0)
|
||
self._next_instrument_id += 1
|
||
else:
|
||
self.instrument = instrument
|
||
|
||
admin_client = self._get_admin_client()
|
||
response_expectation = admin_client.test_create_instrument(self.instrument, self.tick_size)
|
||
response = response_expectation.get_response()
|
||
self.order_book_id = response.order_book_id
|
||
return self.instrument, self.tick_size, self.order_book_id
|
||
|
||
def _expect_book_update(self, client: InfoTestClient, subscription_type,
|
||
instrument_symbol: str,
|
||
expected_bids: dict[Decimal, int],
|
||
expected_asks: dict[Decimal, int]) -> None:
|
||
"""Set up the correct TOB or PDB expectation based on subscription type."""
|
||
if subscription_type == SubscriptionType.TOP_OF_BOOK:
|
||
best_bid = None
|
||
best_ask = None
|
||
if expected_bids:
|
||
best_price = max(expected_bids)
|
||
best_bid = PriceLevel(price=float(best_price), quantity=expected_bids[best_price])
|
||
if expected_asks:
|
||
best_price = min(expected_asks)
|
||
best_ask = PriceLevel(price=float(best_price), quantity=expected_asks[best_price])
|
||
client.expect_on_top_of_book_event(instrument_symbol, best_bid=best_bid, best_ask=best_ask)
|
||
elif subscription_type == SubscriptionType.PRICE_DEPTH_BOOK:
|
||
client.expect_on_price_depth_book_event(
|
||
instrument_symbol,
|
||
bids=[PriceLevel(price=float(p), quantity=q) for p, q in expected_bids.items()],
|
||
asks=[PriceLevel(price=float(p), quantity=q) for p, q in expected_asks.items()])
|
||
|
||
def _insert_orders(self, side: Side.ValueType | None, orders_per_level: int, number_of_levels: int,
|
||
best_bid_price: Decimal, best_ask_price: Decimal, quantity_per_order: int,
|
||
add_cancellation: bool = False) -> tuple[dict[Decimal, int], dict[Decimal, int]]:
|
||
"""Insert orders into the real order book and return the expected price level state."""
|
||
order_book_client = self._get_order_book_client()
|
||
|
||
def next_side(i: int) -> Side.ValueType:
|
||
if side is not None:
|
||
return side
|
||
return Side.BUY if i % 2 == 0 else Side.SELL
|
||
|
||
def next_price_adjustment(i: int) -> Decimal:
|
||
# if side is None we always alternate between buy and sell, so the adjustment needs to be divided by 2
|
||
tick_adjustment = math.floor(i / 2) if side is None else i
|
||
return self.tick_size * tick_adjustment
|
||
|
||
price_levels_by_side: dict[Side.ValueType, dict[Decimal, int]] = {Side.BUY: {}, Side.SELL: {}}
|
||
|
||
for level_index in range(number_of_levels):
|
||
level_side = next_side(level_index)
|
||
price_adjustment = next_price_adjustment(level_index)
|
||
if level_side == Side.BUY:
|
||
level_price = best_bid_price - price_adjustment
|
||
else:
|
||
level_price = best_ask_price + price_adjustment
|
||
|
||
price_levels_by_side[level_side][level_price] = quantity_per_order * orders_per_level
|
||
|
||
for i in range(orders_per_level):
|
||
order_book_client.test_insert_order(
|
||
side=level_side, price=level_price, quantity=quantity_per_order,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
|
||
is_top_of_book_level = price_adjustment == 0
|
||
if add_cancellation and not is_top_of_book_level and (level_index + i) % 2 == 0:
|
||
order_book_client.test_cancel_order(order_book_id=self.order_book_id)
|
||
price_levels_by_side[level_side][level_price] -= quantity_per_order
|
||
if price_levels_by_side[level_side][level_price] == 0:
|
||
del price_levels_by_side[level_side][level_price]
|
||
|
||
bids = price_levels_by_side[Side.BUY]
|
||
asks = price_levels_by_side[Side.SELL]
|
||
return bids, asks
|
||
|
||
def test_create_instrument(self) -> None:
|
||
self._create_instrument_via_admin(
|
||
instrument=Instrument(symbol="AAPL", description="Apple", currency="USD", multiplier=1))
|
||
|
||
client = self._connect_and_login(username="test_create_instrument")
|
||
self.call_expectations_manager.verify_expectations()
|
||
self.call_expectations_manager.verify_no_unexpected_calls()
|
||
|
||
@pytest.mark.parametrize(
|
||
"subscription_type, side, orders_per_level, number_of_levels",
|
||
[
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, Side.BUY, 1, 1, id="TOB 1 buy order"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, Side.SELL, 1, 1, id="TOB 1 sell order"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, Side.BUY, 2, 1, id="TOB 2 buy orders, 1 level"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, Side.SELL, 2, 1, id="TOB 2 sell orders, 1 level"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, Side.BUY, 1, 2, id="TOB 1 buy order by 2 levels"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, Side.SELL, 1, 2, id="TOB 1 sell order by 2 levels"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, None, 1, 2, id="TOB 1 order by level, both sides"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, None, 2, 2, id="TOB 2 orders by 2 levels, both sides"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, None, 5, 2, id="TOB 5 orders by 2 levels, both sides (unbalanced)"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, Side.BUY, 1, 1, id="PDB 1 buy order"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, Side.SELL, 1, 1, id="PDB 1 sell order"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, Side.BUY, 2, 1, id="PDB 2 buy orders, 1 level"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, Side.SELL, 2, 1, id="PDB 2 sell orders, 1 level"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, Side.BUY, 1, 2, id="PDB 1 buy order by 2 levels"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, Side.SELL, 1, 2, id="PDB 1 sell order by 2 levels"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, None, 1, 2, id="PDB 1 order by level, both sides"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, None, 2, 2, id="PDB 2 orders by 2 levels, both sides"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, None, 5, 2, id="PDB 5 orders by 2 levels, both sides (unbalanced)"),
|
||
]
|
||
)
|
||
def test_order_book_on_subscribe_with_insert(self, subscription_type: SubscriptionType.ValueType, side: Side.ValueType | None, orders_per_level: int, number_of_levels: int) -> None:
|
||
self._create_instrument_via_admin()
|
||
|
||
client = self._connect_and_login()
|
||
|
||
best_bid_price = random.randint(100, 10000) * self.tick_size
|
||
best_ask_price = best_bid_price + self.tick_size
|
||
quantity_per_order = random.randint(1, 1000)
|
||
|
||
expected_bids, expected_asks = self._insert_orders(
|
||
side, orders_per_level, number_of_levels,
|
||
best_bid_price, best_ask_price, quantity_per_order)
|
||
|
||
logger.info("Checking that no updates were sent before subscribing")
|
||
self.call_expectations_manager.verify_expectations()
|
||
self.call_expectations_manager.verify_no_unexpected_calls()
|
||
|
||
if subscription_type == SubscriptionType.TOP_OF_BOOK:
|
||
best_bid = PriceLevel(price=float(best_bid_price), quantity=expected_bids[best_bid_price]) if side is None or side == Side.BUY else None
|
||
best_ask = PriceLevel(price=float(best_ask_price), quantity=expected_asks[best_ask_price]) if side is None or side == Side.SELL else None
|
||
client.test_subscribe_to_order_book(instrument_symbol=self.instrument.symbol, type=SubscriptionType.TOP_OF_BOOK)
|
||
client.expect_on_top_of_book_event(self.instrument.symbol, best_bid=best_bid, best_ask=best_ask)
|
||
elif subscription_type == SubscriptionType.PRICE_DEPTH_BOOK:
|
||
client.test_subscribe_to_order_book(instrument_symbol=self.instrument.symbol, type=SubscriptionType.PRICE_DEPTH_BOOK)
|
||
client.expect_on_price_depth_book_event(
|
||
self.instrument.symbol,
|
||
bids=[PriceLevel(price=float(price), quantity=quantity) for price, quantity in expected_bids.items()],
|
||
asks=[PriceLevel(price=float(price), quantity=quantity) for price, quantity in expected_asks.items()])
|
||
else:
|
||
raise ValueError(f"Unexpected subscription type: {subscription_type}")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
@pytest.mark.parametrize(
|
||
"subscription_type, side, orders_per_level, number_of_levels",
|
||
[
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, Side.BUY, 1, 1, id="TOB 1 buy order"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, Side.SELL, 1, 1, id="TOB 1 sell order"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, Side.BUY, 2, 1, id="TOB 2 buy orders, 1 level"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, Side.SELL, 2, 1, id="TOB 2 sell orders, 1 level"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, Side.BUY, 1, 2, id="TOB 1 buy order by 2 levels"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, Side.SELL, 1, 2, id="TOB 1 sell order by 2 levels"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, None, 1, 2, id="TOB 1 order by level, both sides"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, None, 2, 2, id="TOB 2 orders by 2 levels, both sides"),
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, None, 5, 2, id="TOB 5 orders by 2 levels, both sides (unbalanced)"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, Side.BUY, 1, 1, id="PDB 1 buy order"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, Side.SELL, 1, 1, id="PDB 1 sell order"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, Side.BUY, 2, 1, id="PDB 2 buy orders, 1 level"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, Side.SELL, 2, 1, id="PDB 2 sell orders, 1 level"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, Side.BUY, 1, 2, id="PDB 1 buy order by 2 levels"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, Side.SELL, 1, 2, id="PDB 1 sell order by 2 levels"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, None, 1, 2, id="PDB 1 order by level, both sides"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, None, 2, 2, id="PDB 2 orders by 2 levels, both sides"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, None, 5, 2, id="PDB 5 orders by 2 levels, both sides (unbalanced)"),
|
||
]
|
||
)
|
||
def test_order_book_on_subscribe_with_insert_and_cancel(self, subscription_type: SubscriptionType.ValueType, side: Side.ValueType | None, orders_per_level: int, number_of_levels: int) -> None:
|
||
self._create_instrument_via_admin()
|
||
|
||
client = self._connect_and_login()
|
||
|
||
best_bid_price = random.randint(100, 10000) * self.tick_size
|
||
best_ask_price = best_bid_price + self.tick_size
|
||
quantity_per_order = random.randint(1, 1000)
|
||
|
||
expected_bids, expected_asks = self._insert_orders(
|
||
side, orders_per_level, number_of_levels,
|
||
best_bid_price, best_ask_price, quantity_per_order,
|
||
add_cancellation=True)
|
||
|
||
logger.info("Checking that no updates were sent before subscribing")
|
||
self.call_expectations_manager.verify_expectations()
|
||
self.call_expectations_manager.verify_no_unexpected_calls()
|
||
|
||
if subscription_type == SubscriptionType.TOP_OF_BOOK:
|
||
best_bid = PriceLevel(price=float(best_bid_price), quantity=expected_bids[best_bid_price]) if side is None or side == Side.BUY else None
|
||
best_ask = PriceLevel(price=float(best_ask_price), quantity=expected_asks[best_ask_price]) if side is None or side == Side.SELL else None
|
||
client.test_subscribe_to_order_book(instrument_symbol=self.instrument.symbol, type=SubscriptionType.TOP_OF_BOOK)
|
||
client.expect_on_top_of_book_event(self.instrument.symbol, best_bid=best_bid, best_ask=best_ask)
|
||
elif subscription_type == SubscriptionType.PRICE_DEPTH_BOOK:
|
||
client.test_subscribe_to_order_book(instrument_symbol=self.instrument.symbol, type=SubscriptionType.PRICE_DEPTH_BOOK)
|
||
client.expect_on_price_depth_book_event(
|
||
self.instrument.symbol,
|
||
bids=[PriceLevel(price=float(price), quantity=quantity) for price, quantity in expected_bids.items()],
|
||
asks=[PriceLevel(price=float(price), quantity=quantity) for price, quantity in expected_asks.items()])
|
||
else:
|
||
raise ValueError(f"Unexpected subscription type: {subscription_type}")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
@pytest.mark.parametrize(
|
||
"subscription_type",
|
||
[
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, id="TOB"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, id="PDB"),
|
||
]
|
||
)
|
||
def test_order_book_on_subscribe_in_cross_then_trade(self, subscription_type: SubscriptionType.ValueType) -> None:
|
||
self._create_instrument_via_admin()
|
||
|
||
client = self._connect_and_login()
|
||
|
||
order_book_client = self._get_order_book_client()
|
||
|
||
logger.info("Inserting passive buy order")
|
||
crossing_price = Decimal("100.0")
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=crossing_price, quantity=10,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_buyer")
|
||
|
||
logger.info("Checking that no updates were sent before subscribing")
|
||
self.call_expectations_manager.verify_expectations()
|
||
self.call_expectations_manager.verify_no_unexpected_calls()
|
||
|
||
client.test_subscribe_to_order_book(instrument_symbol=self.instrument.symbol, type=subscription_type)
|
||
if subscription_type == SubscriptionType.TOP_OF_BOOK:
|
||
client.expect_on_top_of_book_event(self.instrument.symbol, best_bid=PriceLevel(price=float(crossing_price), quantity=10), best_ask=None)
|
||
elif subscription_type == SubscriptionType.PRICE_DEPTH_BOOK:
|
||
client.expect_on_price_depth_book_event(self.instrument.symbol, bids=[PriceLevel(price=float(crossing_price), quantity=10)], asks=[])
|
||
else:
|
||
raise ValueError(f"Unexpected subscription type: {subscription_type}")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
logger.info("Inserting crossing sell order (triggers trade)")
|
||
order_book_client.test_insert_order(
|
||
side=Side.SELL, price=crossing_price, quantity=10,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_seller")
|
||
order_book_client.expect_on_trade_event(
|
||
trade_id=ANY, order_book_id=self.order_book_id,
|
||
buy_order_id=ANY, sell_order_id=ANY,
|
||
aggressive_side=Side.SELL, price=float(crossing_price), quantity=10)
|
||
|
||
if subscription_type == SubscriptionType.TOP_OF_BOOK:
|
||
client.expect_on_top_of_book_event(self.instrument.symbol, best_bid=None, best_ask=None)
|
||
elif subscription_type == SubscriptionType.PRICE_DEPTH_BOOK:
|
||
client.expect_on_price_depth_book_event(self.instrument.symbol, bids=[], asks=[])
|
||
else:
|
||
raise ValueError(f"Unexpected subscription type: {subscription_type}")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Instrument lifecycle – push to already-connected clients
|
||
# ----------------------------------------------------------------
|
||
|
||
def test_instrument_pushed_to_already_connected_client(self) -> None:
|
||
"""Client connects (and logs in if auth required) before any instrument
|
||
exists, then instrument is created via admin. The info component should
|
||
push OnInstrument."""
|
||
client = self._connect_unauthenticated()
|
||
if self.auth_required:
|
||
client.test_login(username=self.test_name)
|
||
self.call_expectations_manager.verify_no_unexpected_calls()
|
||
|
||
self._create_instrument_via_admin()
|
||
|
||
client.expect_on_instrument_event(
|
||
self.instrument, tick_size=self.tick_size, order_book_id=self.order_book_id)
|
||
self.call_expectations_manager.verify_expectations()
|
||
self.call_expectations_manager.verify_no_unexpected_calls()
|
||
|
||
def test_multiple_instruments_received_on_login(self) -> None:
|
||
"""Create several instruments, then connect (and log in if auth
|
||
required) a new client. All instruments should be delivered as
|
||
OnInstrument events."""
|
||
instruments: list[tuple[Instrument, Decimal, int]] = []
|
||
for _ in range(3):
|
||
instruments.append(self._create_instrument_via_admin())
|
||
|
||
client = self._connect_unauthenticated()
|
||
if self.auth_required:
|
||
client.test_login(username=self.test_name)
|
||
for inst, tick, ob_id in instruments:
|
||
client.expect_on_instrument_event(inst, tick_size=tick, order_book_id=ob_id)
|
||
self.call_expectations_manager.verify_expectations()
|
||
self.call_expectations_manager.verify_no_unexpected_calls()
|
||
|
||
def test_instrument_pushed_to_multiple_connected_clients(self) -> None:
|
||
"""Two clients are connected before the instrument is created.
|
||
Both should receive the OnInstrument event."""
|
||
client1 = self._connect_unauthenticated()
|
||
if self.auth_required:
|
||
client1.test_login(username=self.test_name)
|
||
|
||
client2 = self._connect_unauthenticated()
|
||
if self.auth_required:
|
||
client2.test_login(username=f"{self.test_name}_2")
|
||
|
||
self.call_expectations_manager.verify_no_unexpected_calls()
|
||
|
||
self._create_instrument_via_admin()
|
||
|
||
client1.expect_on_instrument_event(
|
||
self.instrument, tick_size=self.tick_size, order_book_id=self.order_book_id)
|
||
client2.expect_on_instrument_event(
|
||
self.instrument, tick_size=self.tick_size, order_book_id=self.order_book_id)
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Subscription edge cases
|
||
# ----------------------------------------------------------------
|
||
|
||
def test_subscribe_to_nonexistent_instrument(self) -> None:
|
||
"""Subscribing to an unknown symbol should return an error."""
|
||
self._create_instrument_via_admin()
|
||
client = self._connect_and_login()
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
client.test_subscribe_to_order_book(
|
||
instrument_symbol="NONEXISTENT", type=SubscriptionType.TOP_OF_BOOK,
|
||
expect_success=False)
|
||
|
||
# ----------------------------------------------------------------
|
||
# Live updates – order insert
|
||
# ----------------------------------------------------------------
|
||
|
||
@pytest.mark.parametrize(
|
||
"subscription_type",
|
||
[
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, id="TOB"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, id="PDB"),
|
||
]
|
||
)
|
||
def test_live_update_on_order_insert(self, subscription_type: SubscriptionType.ValueType) -> None:
|
||
"""Subscribe to an empty order book, then insert an order.
|
||
The subscriber should receive a book update with the new order."""
|
||
self._create_instrument_via_admin()
|
||
client = self._connect_and_login()
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
client.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=subscription_type)
|
||
|
||
price = Decimal("100.0")
|
||
quantity = 10
|
||
self._expect_book_update(client, subscription_type, self.instrument.symbol,
|
||
expected_bids={price: quantity}, expected_asks={})
|
||
|
||
order_book_client = self._get_order_book_client()
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=price, quantity=quantity,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Live updates – order cancel
|
||
# ----------------------------------------------------------------
|
||
|
||
@pytest.mark.parametrize(
|
||
"subscription_type",
|
||
[
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, id="TOB"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, id="PDB"),
|
||
]
|
||
)
|
||
def test_live_update_on_order_cancel(self, subscription_type: SubscriptionType.ValueType) -> None:
|
||
"""Subscribe, insert an order (verify update), then cancel it.
|
||
The book should go back to empty."""
|
||
self._create_instrument_via_admin()
|
||
client = self._connect_and_login()
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
client.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=subscription_type)
|
||
|
||
price = Decimal("100.0")
|
||
quantity = 10
|
||
|
||
self._expect_book_update(client, subscription_type, self.instrument.symbol,
|
||
expected_bids={price: quantity}, expected_asks={})
|
||
order_book_client = self._get_order_book_client()
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=price, quantity=quantity,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
self._expect_book_update(client, subscription_type, self.instrument.symbol,
|
||
expected_bids={}, expected_asks={})
|
||
order_book_client.test_cancel_order(order_book_id=self.order_book_id)
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Live updates – full trade clears book
|
||
# ----------------------------------------------------------------
|
||
|
||
@pytest.mark.parametrize(
|
||
"subscription_type",
|
||
[
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, id="TOB"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, id="PDB"),
|
||
]
|
||
)
|
||
def test_live_update_after_full_trade(self, subscription_type: SubscriptionType.ValueType) -> None:
|
||
"""Subscribe, insert a passive buy, then a crossing sell that fully
|
||
trades. The book should end up empty."""
|
||
self._create_instrument_via_admin()
|
||
client = self._connect_and_login()
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
client.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=subscription_type)
|
||
|
||
price = Decimal("100.0")
|
||
quantity = 10
|
||
|
||
self._expect_book_update(client, subscription_type, self.instrument.symbol,
|
||
expected_bids={price: quantity}, expected_asks={})
|
||
order_book_client = self._get_order_book_client()
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=price, quantity=quantity,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_buyer")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
self._expect_book_update(client, subscription_type, self.instrument.symbol,
|
||
expected_bids={}, expected_asks={})
|
||
order_book_client.test_insert_order(
|
||
side=Side.SELL, price=price, quantity=quantity,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_seller")
|
||
order_book_client.expect_on_trade_event(
|
||
trade_id=ANY, order_book_id=self.order_book_id,
|
||
buy_order_id=ANY, sell_order_id=ANY,
|
||
aggressive_side=Side.SELL, price=float(price), quantity=quantity)
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Live updates – partial trade
|
||
# ----------------------------------------------------------------
|
||
|
||
@pytest.mark.parametrize(
|
||
"subscription_type",
|
||
[
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, id="TOB"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, id="PDB"),
|
||
]
|
||
)
|
||
def test_live_update_after_partial_trade(self, subscription_type: SubscriptionType.ValueType) -> None:
|
||
"""Subscribe, insert a large passive buy (qty 10), then a smaller
|
||
crossing sell (qty 4). After the partial fill the remaining buy
|
||
quantity (6) should be reflected in the book update."""
|
||
self._create_instrument_via_admin()
|
||
client = self._connect_and_login()
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
client.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=subscription_type)
|
||
|
||
price = Decimal("100.0")
|
||
buy_qty = 10
|
||
sell_qty = 4
|
||
remaining = buy_qty - sell_qty
|
||
|
||
self._expect_book_update(client, subscription_type, self.instrument.symbol,
|
||
expected_bids={price: buy_qty}, expected_asks={})
|
||
order_book_client = self._get_order_book_client()
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=price, quantity=buy_qty,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_buyer")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
self._expect_book_update(client, subscription_type, self.instrument.symbol,
|
||
expected_bids={price: remaining}, expected_asks={})
|
||
order_book_client.test_insert_order(
|
||
side=Side.SELL, price=price, quantity=sell_qty,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_seller")
|
||
order_book_client.expect_on_trade_event(
|
||
trade_id=ANY, order_book_id=self.order_book_id,
|
||
buy_order_id=ANY, sell_order_id=ANY,
|
||
aggressive_side=Side.SELL, price=float(price), quantity=sell_qty)
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Live updates – multiple sequential inserts
|
||
# ----------------------------------------------------------------
|
||
|
||
@pytest.mark.parametrize(
|
||
"subscription_type",
|
||
[
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, id="TOB"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, id="PDB"),
|
||
]
|
||
)
|
||
def test_live_update_multiple_sequential_inserts(self, subscription_type: SubscriptionType.ValueType) -> None:
|
||
"""Subscribe then insert several orders one by one, verifying
|
||
the book update after each insert."""
|
||
self._create_instrument_via_admin()
|
||
client = self._connect_and_login()
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
client.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=subscription_type)
|
||
|
||
order_book_client = self._get_order_book_client()
|
||
|
||
cumulative_bids: dict[Decimal, int] = {}
|
||
prices = [Decimal("100.0"), Decimal("99.99"), Decimal("99.98")]
|
||
quantity = 5
|
||
|
||
for price in prices:
|
||
cumulative_bids[price] = quantity
|
||
self._expect_book_update(client, subscription_type, self.instrument.symbol,
|
||
expected_bids=dict(cumulative_bids), expected_asks={})
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=price, quantity=quantity,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
self.call_expectations_manager.verify_no_unexpected_calls()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Multi-client – both subscribers receive updates
|
||
# ----------------------------------------------------------------
|
||
|
||
@pytest.mark.parametrize(
|
||
"subscription_type",
|
||
[
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, id="TOB"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, id="PDB"),
|
||
]
|
||
)
|
||
def test_multiple_clients_receive_updates(self, subscription_type: SubscriptionType.ValueType) -> None:
|
||
"""Two clients subscribe to the same instrument. Both should
|
||
receive book updates when an order is inserted."""
|
||
self._create_instrument_via_admin()
|
||
client1 = self._connect_and_login(username=self.test_name)
|
||
client2 = self._connect_and_login(username=f"{self.test_name}_2")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
client1.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=subscription_type)
|
||
client2.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=subscription_type)
|
||
|
||
price = Decimal("100.0")
|
||
quantity = 10
|
||
|
||
self._expect_book_update(client1, subscription_type, self.instrument.symbol,
|
||
expected_bids={price: quantity}, expected_asks={})
|
||
self._expect_book_update(client2, subscription_type, self.instrument.symbol,
|
||
expected_bids={price: quantity}, expected_asks={})
|
||
|
||
order_book_client = self._get_order_book_client()
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=price, quantity=quantity,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Multi-client – late subscriber gets current snapshot
|
||
# ----------------------------------------------------------------
|
||
|
||
@pytest.mark.parametrize(
|
||
"subscription_type",
|
||
[
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, id="TOB"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, id="PDB"),
|
||
]
|
||
)
|
||
def test_late_subscriber_gets_current_snapshot(self, subscription_type: SubscriptionType.ValueType) -> None:
|
||
"""Client A subscribes and receives updates as orders are placed.
|
||
Client B subscribes later and should receive a snapshot reflecting
|
||
the current book state, including all previous inserts."""
|
||
self._create_instrument_via_admin()
|
||
client_a = self._connect_and_login(username=self.test_name)
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
client_a.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=subscription_type)
|
||
|
||
bid_price = Decimal("100.0")
|
||
ask_price = Decimal("100.01")
|
||
quantity = 10
|
||
|
||
self._expect_book_update(client_a, subscription_type, self.instrument.symbol,
|
||
expected_bids={bid_price: quantity}, expected_asks={})
|
||
order_book_client = self._get_order_book_client()
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=bid_price, quantity=quantity,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
self._expect_book_update(client_a, subscription_type, self.instrument.symbol,
|
||
expected_bids={bid_price: quantity},
|
||
expected_asks={ask_price: quantity})
|
||
order_book_client.test_insert_order(
|
||
side=Side.SELL, price=ask_price, quantity=quantity,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
logger.info("Late subscriber (client B) connects and subscribes")
|
||
client_b = self._connect_and_login(username=f"{self.test_name}_2")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
self._expect_book_update(client_b, subscription_type, self.instrument.symbol,
|
||
expected_bids={bid_price: quantity},
|
||
expected_asks={ask_price: quantity})
|
||
client_b.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=subscription_type)
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Multi-client – mixed subscription types on the same instrument
|
||
# ----------------------------------------------------------------
|
||
|
||
@pytest.mark.parametrize(
|
||
"side",
|
||
[
|
||
pytest.param(Side.BUY, id="BUY"),
|
||
pytest.param(Side.SELL, id="SELL"),
|
||
]
|
||
)
|
||
def test_mixed_subscription_types_same_instrument(self, side: Side.ValueType) -> None:
|
||
"""One client subscribes with TOP_OF_BOOK and another with
|
||
PRICE_DEPTH_BOOK to the same instrument. Each client should
|
||
receive the appropriate update format when an order is inserted."""
|
||
self._create_instrument_via_admin()
|
||
self._tob_client = self._connect_and_login(username=self.test_name)
|
||
self._pdb_client = self._connect_and_login(username=f"{self.test_name}_2")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
self._tob_client.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=SubscriptionType.TOP_OF_BOOK)
|
||
self._pdb_client.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=SubscriptionType.PRICE_DEPTH_BOOK)
|
||
|
||
self._mixed_best_price = Decimal("100.0")
|
||
self._mixed_quantity = 10
|
||
|
||
best_bid = PriceLevel(price=float(self._mixed_best_price), quantity=self._mixed_quantity) if side == Side.BUY else None
|
||
best_ask = PriceLevel(price=float(self._mixed_best_price), quantity=self._mixed_quantity) if side == Side.SELL else None
|
||
expected_bids = [PriceLevel(price=float(self._mixed_best_price), quantity=self._mixed_quantity)] if side == Side.BUY else []
|
||
expected_asks = [PriceLevel(price=float(self._mixed_best_price), quantity=self._mixed_quantity)] if side == Side.SELL else []
|
||
|
||
self._tob_client.expect_on_top_of_book_event(
|
||
self.instrument.symbol, best_bid=best_bid, best_ask=best_ask)
|
||
self._pdb_client.expect_on_price_depth_book_event(
|
||
self.instrument.symbol, bids=expected_bids, asks=expected_asks)
|
||
|
||
order_book_client = self._get_order_book_client()
|
||
order_book_client.test_insert_order(
|
||
side=side, price=self._mixed_best_price, quantity=self._mixed_quantity,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
@pytest.mark.parametrize(
|
||
"side",
|
||
[
|
||
pytest.param(Side.BUY, id="BUY"),
|
||
pytest.param(Side.SELL, id="SELL"),
|
||
]
|
||
)
|
||
def test_mixed_subscriptions_tob_unchanged_after_non_best_insert(self, side: Side.ValueType) -> None:
|
||
"""After the first order establishes the best price, insert a second
|
||
order at a worse price. The PDB subscriber should see a new level
|
||
added while the TOB subscriber sees the same best price/quantity."""
|
||
self.test_mixed_subscription_types_same_instrument(side)
|
||
|
||
worse_price = (self._mixed_best_price - self.tick_size) if side == Side.BUY else (self._mixed_best_price + self.tick_size)
|
||
worse_qty = 5
|
||
|
||
best_bid = PriceLevel(price=float(self._mixed_best_price), quantity=self._mixed_quantity) if side == Side.BUY else None
|
||
best_ask = PriceLevel(price=float(self._mixed_best_price), quantity=self._mixed_quantity) if side == Side.SELL else None
|
||
self._tob_client.expect_on_top_of_book_event(
|
||
self.instrument.symbol, best_bid=best_bid, best_ask=best_ask)
|
||
|
||
if side == Side.BUY:
|
||
self._pdb_client.expect_on_price_depth_book_event(
|
||
self.instrument.symbol,
|
||
bids=[
|
||
PriceLevel(price=float(self._mixed_best_price), quantity=self._mixed_quantity),
|
||
PriceLevel(price=float(worse_price), quantity=worse_qty),
|
||
],
|
||
asks=[])
|
||
else:
|
||
self._pdb_client.expect_on_price_depth_book_event(
|
||
self.instrument.symbol,
|
||
bids=[],
|
||
asks=[
|
||
PriceLevel(price=float(self._mixed_best_price), quantity=self._mixed_quantity),
|
||
PriceLevel(price=float(worse_price), quantity=worse_qty),
|
||
])
|
||
|
||
order_book_client = self._get_order_book_client()
|
||
order_book_client.test_insert_order(
|
||
side=side, price=worse_price, quantity=worse_qty,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Subscription isolation – different instruments
|
||
# ----------------------------------------------------------------
|
||
|
||
def test_subscriptions_to_different_instruments_are_isolated(self) -> None:
|
||
"""Subscribe to instrument A only. Insert orders for both A and B.
|
||
The subscriber should only receive updates for instrument A."""
|
||
inst_a, tick_a, ob_id_a = self._create_instrument_via_admin()
|
||
|
||
client = self._connect_and_login()
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
client.test_subscribe_to_order_book(
|
||
instrument_symbol=inst_a.symbol, type=SubscriptionType.TOP_OF_BOOK)
|
||
|
||
order_book_client = self._get_order_book_client()
|
||
|
||
price_a = Decimal("100.0")
|
||
client.expect_on_top_of_book_event(
|
||
inst_a.symbol,
|
||
best_bid=PriceLevel(price=float(price_a), quantity=10),
|
||
best_ask=None)
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=price_a, quantity=10,
|
||
order_book_id=ob_id_a, username=f"{self.test_name}_orders")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
order_book_client.expect_on_order_book_created_event(
|
||
tick_size=Decimal("0.02"), order_book_id=None)
|
||
_, _, ob_id_b = self._create_instrument_via_admin(tick_size=Decimal("0.02"))
|
||
inst_b = self.instrument
|
||
|
||
client.expect_on_instrument_event(
|
||
inst_b, tick_size=self.tick_size, order_book_id=ob_id_b)
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=Decimal("200.0"), quantity=20,
|
||
order_book_id=ob_id_b, username=f"{self.test_name}_orders")
|
||
self.call_expectations_manager.verify_expectations()
|
||
self.call_expectations_manager.verify_no_unexpected_calls()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Unsubscribed client – should not get order book updates
|
||
# ----------------------------------------------------------------
|
||
|
||
@pytest.mark.parametrize(
|
||
"subscription_type",
|
||
[
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, id="TOB"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, id="PDB"),
|
||
]
|
||
)
|
||
def test_unsubscribed_client_receives_no_updates(self, subscription_type: SubscriptionType.ValueType) -> None:
|
||
"""A client that is logged in but has not subscribed should not
|
||
receive any TOB or PDB updates when the order book changes."""
|
||
self._create_instrument_via_admin()
|
||
client = self._connect_and_login()
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
order_book_client = self._get_order_book_client()
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=Decimal("100.0"), quantity=10,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
self.call_expectations_manager.verify_expectations()
|
||
self.call_expectations_manager.verify_no_unexpected_calls()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Live update – new instrument created while subscribed to another
|
||
# ----------------------------------------------------------------
|
||
|
||
def test_new_instrument_while_subscribed_to_another(self) -> None:
|
||
"""While subscribed to instrument A, a new instrument B is created.
|
||
The client should receive OnInstrument for B but no spurious
|
||
book updates for A."""
|
||
self._create_instrument_via_admin()
|
||
client = self._connect_and_login()
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
client.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=SubscriptionType.TOP_OF_BOOK)
|
||
|
||
order_book_client = self._get_order_book_client()
|
||
order_book_client.expect_on_order_book_created_event(
|
||
tick_size=Decimal("0.05"), order_book_id=None)
|
||
self._create_instrument_via_admin(tick_size=Decimal("0.05"))
|
||
|
||
client.expect_on_instrument_event(
|
||
self.instrument, tick_size=self.tick_size, order_book_id=self.order_book_id)
|
||
self.call_expectations_manager.verify_expectations()
|
||
self.call_expectations_manager.verify_no_unexpected_calls()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Live updates – both sides of the book
|
||
# ----------------------------------------------------------------
|
||
|
||
@pytest.mark.parametrize(
|
||
"subscription_type",
|
||
[
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, id="TOB"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, id="PDB"),
|
||
]
|
||
)
|
||
def test_live_update_with_both_sides(self, subscription_type: SubscriptionType.ValueType) -> None:
|
||
"""Subscribe, insert a buy then a sell at different prices.
|
||
The book should reflect orders on both sides."""
|
||
self._create_instrument_via_admin()
|
||
client = self._connect_and_login()
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
client.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=subscription_type)
|
||
|
||
bid_price = Decimal("100.0")
|
||
ask_price = Decimal("100.01")
|
||
quantity = 10
|
||
|
||
self._expect_book_update(client, subscription_type, self.instrument.symbol,
|
||
expected_bids={bid_price: quantity}, expected_asks={})
|
||
order_book_client = self._get_order_book_client()
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=bid_price, quantity=quantity,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
self._expect_book_update(client, subscription_type, self.instrument.symbol,
|
||
expected_bids={bid_price: quantity},
|
||
expected_asks={ask_price: quantity})
|
||
order_book_client.test_insert_order(
|
||
side=Side.SELL, price=ask_price, quantity=quantity,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
# ----------------------------------------------------------------
|
||
# Live updates – aggregation at same price level
|
||
# ----------------------------------------------------------------
|
||
|
||
@pytest.mark.parametrize(
|
||
"subscription_type",
|
||
[
|
||
pytest.param(SubscriptionType.TOP_OF_BOOK, id="TOB"),
|
||
pytest.param(SubscriptionType.PRICE_DEPTH_BOOK, id="PDB"),
|
||
]
|
||
)
|
||
def test_live_update_quantity_aggregation_at_same_price(self, subscription_type: SubscriptionType.ValueType) -> None:
|
||
"""Insert two orders at the same price after subscribing. The
|
||
reported quantity should be the sum of both orders."""
|
||
self._create_instrument_via_admin()
|
||
client = self._connect_and_login()
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
client.test_subscribe_to_order_book(
|
||
instrument_symbol=self.instrument.symbol, type=subscription_type)
|
||
|
||
price = Decimal("100.0")
|
||
qty1 = 10
|
||
qty2 = 15
|
||
|
||
self._expect_book_update(client, subscription_type, self.instrument.symbol,
|
||
expected_bids={price: qty1}, expected_asks={})
|
||
order_book_client = self._get_order_book_client()
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=price, quantity=qty1,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
self.call_expectations_manager.verify_expectations()
|
||
|
||
self._expect_book_update(client, subscription_type, self.instrument.symbol,
|
||
expected_bids={price: qty1 + qty2}, expected_asks={})
|
||
order_book_client.test_insert_order(
|
||
side=Side.BUY, price=price, quantity=qty2,
|
||
order_book_id=self.order_book_id, username=f"{self.test_name}_orders")
|
||
self.call_expectations_manager.verify_expectations()
|