140 lines
6.4 KiB
Python
140 lines
6.4 KiB
Python
from decimal import Decimal
|
|
import logging
|
|
|
|
import pytest
|
|
|
|
from connection.tcp_connection_manager import TcpConnectionManager
|
|
from proto.common_pb2 import Instrument, Side
|
|
|
|
from tests.conftest import DEFAULT_PASSWORD
|
|
from tests.common.auth_tests import AuthenticationTests
|
|
from tests.common.mock_expectations import CallExpectationsManager
|
|
from tests.test_client.admin_test_client import (
|
|
AdminClientConnectionHandlerFactory, AdminTestClient,
|
|
)
|
|
from tests.test_client.risk_gateway_test_client import (
|
|
RiskGatewayClientConnectionHandlerFactory, RiskGatewayTestClient,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TestExecutionSystem(AuthenticationTests):
|
|
PROTOCOL = "execution"
|
|
|
|
@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._client_factory = RiskGatewayClientConnectionHandlerFactory(self.call_expectations_manager)
|
|
self._admin_factory = AdminClientConnectionHandlerFactory(self.call_expectations_manager)
|
|
self._admin_client: AdminTestClient | None = None
|
|
self._next_instrument_id = 1
|
|
|
|
def _connect_unauthenticated(self) -> RiskGatewayTestClient:
|
|
return self.tcp_connection_manager.connect(self.server_address, self._client_factory)
|
|
|
|
def _connect_and_login(self, username: str | None = None) -> RiskGatewayTestClient:
|
|
"""Connect to the execution server, log in (if auth required), and return the client."""
|
|
client: RiskGatewayTestClient = self._connect_unauthenticated()
|
|
if self.auth_required:
|
|
client.test_login(username=username or self.test_name, password=DEFAULT_PASSWORD)
|
|
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 _create_instrument_via_admin(self, tick_size: Decimal = Decimal("0.01")) -> str:
|
|
"""Create an instrument via admin and return its symbol."""
|
|
symbol = f"TEST.{self._next_instrument_id}"
|
|
self._next_instrument_id += 1
|
|
instrument = Instrument(
|
|
symbol=symbol, description="Test instrument",
|
|
currency="USD", multiplier=1.0)
|
|
admin = self._get_admin_client()
|
|
admin.test_create_instrument(instrument, tick_size)
|
|
return symbol
|
|
|
|
# =========================================================================
|
|
# Authentication
|
|
# =========================================================================
|
|
|
|
def test_insert_order_before_login_is_rejected(self) -> None:
|
|
"""Operations that require authentication must fail before login."""
|
|
if not self.auth_required:
|
|
pytest.skip("Component does not require authentication")
|
|
symbol = self._create_instrument_via_admin()
|
|
client = self._connect_unauthenticated()
|
|
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10,
|
|
expect_success=False)
|
|
|
|
# =========================================================================
|
|
# Insert order
|
|
# =========================================================================
|
|
|
|
def test_insert_order(self) -> None:
|
|
symbol = self._create_instrument_via_admin()
|
|
client = self._connect_and_login()
|
|
resp = client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
|
|
response = resp.get_response()
|
|
assert response.order_id != 0, "InsertOrderResponse must include an order_id"
|
|
assert response.timestamp != 0, "InsertOrderResponse must include a timestamp"
|
|
|
|
def test_insert_order_on_unknown_instrument(self) -> None:
|
|
self._create_instrument_via_admin()
|
|
client = self._connect_and_login()
|
|
client.test_insert_order("UNKNOWN.SYMBOL", Side.BUY, Decimal("100.0"), 10,
|
|
expect_success=False)
|
|
|
|
def test_insert_order_matching_produces_trade(self) -> None:
|
|
symbol = self._create_instrument_via_admin()
|
|
buyer = self._connect_and_login(f"{self.test_name}_2")
|
|
seller = self._connect_and_login(f"{self.test_name}_3")
|
|
|
|
buyer.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
|
|
|
|
sell_resp = seller.test_insert_order(symbol, Side.SELL, Decimal("100.0"), 10)
|
|
sell_response = sell_resp.get_response()
|
|
assert len(sell_response.trade_ids) == 1, "Expected exactly one trade"
|
|
assert sell_response.traded_quantity == 10
|
|
|
|
def test_orders_pass_without_any_limits_configured(self) -> None:
|
|
"""When no risk limits are set, orders must flow through without restriction."""
|
|
symbol = self._create_instrument_via_admin()
|
|
client = self._connect_and_login()
|
|
for _ in range(10):
|
|
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1000)
|
|
|
|
# =========================================================================
|
|
# Cancel order
|
|
# =========================================================================
|
|
|
|
def test_cancel_resting_order(self) -> None:
|
|
symbol = self._create_instrument_via_admin()
|
|
client = self._connect_and_login()
|
|
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
|
|
client.test_cancel_order(symbol)
|
|
|
|
def test_cancel_nonexistent_order(self) -> None:
|
|
symbol = self._create_instrument_via_admin()
|
|
client = self._connect_and_login()
|
|
client.test_cancel_order(symbol, order_id=999999, expect_success=False)
|
|
|
|
def test_cancel_other_users_order_is_rejected(self) -> None:
|
|
"""A user must not be able to cancel another user's order."""
|
|
symbol = self._create_instrument_via_admin()
|
|
client_a = self._connect_and_login(f"{self.test_name}_2")
|
|
client_b = self._connect_and_login(f"{self.test_name}_3")
|
|
|
|
resp_a = client_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
|
|
order_id_a = resp_a.get_response().order_id
|
|
|
|
client_b.test_cancel_order(symbol, order_id=order_id_a, expect_success=False)
|