gtat-tech-career-kickstarte.../solution/tests/test_risk_limits_system.py

625 lines
31 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from decimal import Decimal
import logging
import pytest
from connection.tcp_connection_manager import TcpConnectionManager
from proto.common_pb2 import Instrument, Side
from proto.risk_limits_pb2 import (
InstrumentRiskLimits, RollingWindowLimit, UserRiskLimits,
)
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 TestRiskGatewaySystem(AuthenticationTests):
PROTOCOL = "risk_limits"
@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 risk gateway, log in (if auth required), and return the client."""
client: RiskGatewayTestClient = self.tcp_connection_manager.connect(self.server_address, self._client_factory)
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
# =========================================================================
# Risk limits CRUD user level
# =========================================================================
def test_set_and_get_user_risk_limits(self) -> None:
client = self._connect_and_login()
limits = UserRiskLimits(max_outstanding_quantity=500)
client.test_set_user_risk_limits(limits)
resp = client.test_get_user_risk_limits()
response = resp.get_response()
assert response.user_risk_limits.max_outstanding_quantity == 500
def test_get_user_risk_limits_when_not_set(self) -> None:
client = self._connect_and_login()
client.test_get_user_risk_limits(expect_success=False)
# =========================================================================
# Risk limits CRUD instrument level
# =========================================================================
def test_set_and_get_instrument_risk_limits(self) -> None:
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
limits = InstrumentRiskLimits(max_outstanding_quantity=100)
client.test_set_instrument_risk_limits(symbol, limits)
resp = client.test_get_instrument_risk_limits()
response = resp.get_response()
assert symbol in response.risk_limits_by_instrument
assert response.risk_limits_by_instrument[symbol].max_outstanding_quantity == 100
def test_get_instrument_risk_limits_when_not_set(self) -> None:
client = self._connect_and_login()
resp = client.test_get_instrument_risk_limits()
response = resp.get_response()
assert len(response.risk_limits_by_instrument) == 0
def test_set_instrument_risk_limits_for_multiple_instruments(self) -> None:
sym_a = self._create_instrument_via_admin()
sym_b = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_instrument_risk_limits(
sym_a, InstrumentRiskLimits(max_outstanding_quantity=100))
client.test_set_instrument_risk_limits(
sym_b, InstrumentRiskLimits(max_outstanding_quantity=200))
resp = client.test_get_instrument_risk_limits()
by_instrument = resp.get_response().risk_limits_by_instrument
assert by_instrument[sym_a].max_outstanding_quantity == 100
assert by_instrument[sym_b].max_outstanding_quantity == 200
# =========================================================================
# User max outstanding quantity
# =========================================================================
def test_user_max_outstanding_quantity_accepts_within_limit(self) -> None:
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_user_risk_limits(
UserRiskLimits(max_outstanding_quantity=20))
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 20)
def test_user_max_outstanding_quantity_rejects_over_limit(self) -> None:
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_user_risk_limits(
UserRiskLimits(max_outstanding_quantity=20))
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 20)
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
def test_user_max_outstanding_quantity_freed_by_cancellation(self) -> None:
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_user_risk_limits(
UserRiskLimits(max_outstanding_quantity=20))
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 20)
client.test_cancel_order(symbol)
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 20)
def test_user_max_outstanding_quantity_freed_by_full_trade(self) -> None:
symbol = self._create_instrument_via_admin()
user_a = self._connect_and_login(f"{self.test_name}_2")
user_b = self._connect_and_login(f"{self.test_name}_3")
user_a.test_set_user_risk_limits(
UserRiskLimits(max_outstanding_quantity=20))
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 20)
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
user_b.test_insert_order(symbol, Side.SELL, Decimal("100.0"), 20)
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 20)
def test_user_max_outstanding_quantity_freed_by_partial_trade(self) -> None:
symbol = self._create_instrument_via_admin()
user_a = self._connect_and_login(f"{self.test_name}_2")
user_b = self._connect_and_login(f"{self.test_name}_3")
user_a.test_set_user_risk_limits(
UserRiskLimits(max_outstanding_quantity=20))
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 20)
user_b.test_insert_order(symbol, Side.SELL, Decimal("100.0"), 10)
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
def test_user_max_outstanding_quantity_spans_all_instruments(self) -> None:
"""The user-level limit applies across all instruments combined."""
sym_a = self._create_instrument_via_admin()
sym_b = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_user_risk_limits(
UserRiskLimits(max_outstanding_quantity=30))
client.test_insert_order(sym_a, Side.BUY, Decimal("100.0"), 20)
client.test_insert_order(sym_b, Side.BUY, Decimal("100.0"), 10)
client.test_insert_order(sym_b, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
# =========================================================================
# Instrument max outstanding quantity
# =========================================================================
def test_instrument_max_outstanding_quantity_rejects_over_limit(self) -> None:
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_quantity=15))
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 15)
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
def test_instrument_max_outstanding_quantity_independent_per_instrument(self) -> None:
"""Limits on instrument A must not affect orders on instrument B."""
sym_a = self._create_instrument_via_admin()
sym_b = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_instrument_risk_limits(
sym_a, InstrumentRiskLimits(max_outstanding_quantity=10))
client.test_insert_order(sym_a, Side.BUY, Decimal("100.0"), 10)
client.test_insert_order(sym_a, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
client.test_insert_order(sym_b, Side.BUY, Decimal("100.0"), 100)
def test_instrument_max_outstanding_quantity_freed_by_cancellation(self) -> None:
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_quantity=10))
resp = client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
order_id = resp.get_response().order_id
client.test_cancel_order(symbol, order_id=order_id)
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
def test_instrument_max_outstanding_quantity_freed_by_trade(self) -> None:
symbol = self._create_instrument_via_admin()
user_a = self._connect_and_login(f"{self.test_name}_2")
user_b = self._connect_and_login(f"{self.test_name}_3")
user_a.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_quantity=10))
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
user_b.test_insert_order(symbol, Side.SELL, Decimal("100.0"), 10)
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
# =========================================================================
# Instrument max outstanding amount
# =========================================================================
def test_instrument_max_outstanding_amount_rejects_over_limit(self) -> None:
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_amount=1000.0))
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
def test_instrument_max_outstanding_amount_considers_price_and_quantity(self) -> None:
"""Amount = price x quantity. A higher price hits the limit sooner."""
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_amount=599.0))
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 5)
client.test_insert_order(symbol, Side.BUY, Decimal("10.0"), 10,
expect_success=False)
client.test_insert_order(symbol, Side.SELL, Decimal("100.0"), 1,
expect_success=False)
def test_instrument_max_outstanding_amount_freed_by_trade(self) -> None:
symbol = self._create_instrument_via_admin()
user_a = self._connect_and_login(f"{self.test_name}_2")
user_b = self._connect_and_login(f"{self.test_name}_3")
user_a.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_amount=1000.0))
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
user_b.test_insert_order(symbol, Side.SELL, Decimal("100.0"), 10)
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
# =========================================================================
# Message rate rolling limit
# =========================================================================
def test_message_rate_limit_rejects_excess(self) -> None:
"""After hitting the message rate limit within the window, further orders are rejected."""
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_user_risk_limits(UserRiskLimits(
message_rate_rolling_limit=RollingWindowLimit(limit=3, window_in_seconds=60)))
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1)
client.test_insert_order(symbol, Side.BUY, Decimal("101.0"), 1)
client.test_insert_order(symbol, Side.BUY, Decimal("102.0"), 1)
client.test_insert_order(symbol, Side.BUY, Decimal("103.0"), 1,
expect_success=False)
# =========================================================================
# Order quantity rolling limit
# =========================================================================
def test_order_quantity_rolling_limit_rejects_excess(self) -> None:
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_instrument_risk_limits(symbol, InstrumentRiskLimits(
order_quantity_rolling_limit=RollingWindowLimit(limit=50, window_in_seconds=60)))
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 30)
client.test_insert_order(symbol, Side.BUY, Decimal("101.0"), 20)
client.test_insert_order(symbol, Side.BUY, Decimal("102.0"), 1,
expect_success=False)
# =========================================================================
# Order amount rolling limit
# =========================================================================
def test_order_amount_rolling_limit_rejects_excess(self) -> None:
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_instrument_risk_limits(symbol, InstrumentRiskLimits(
order_amount_rolling_limit=RollingWindowLimit(limit=5000, window_in_seconds=60)))
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 30)
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 20)
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
# =========================================================================
# Multi-user isolation
# =========================================================================
def test_limits_are_isolated_between_users(self) -> None:
"""User A's limits must not affect user B's ability to trade."""
symbol = self._create_instrument_via_admin()
user_a = self._connect_and_login(f"{self.test_name}_2")
user_b = self._connect_and_login(f"{self.test_name}_3")
user_a.test_set_user_risk_limits(
UserRiskLimits(max_outstanding_quantity=10))
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
user_b.test_insert_order(symbol, Side.SELL, Decimal("101.0"), 100)
def test_order_from_user_a_does_not_count_against_user_b(self) -> None:
"""Outstanding orders from user A must not consume user B's limits."""
symbol = self._create_instrument_via_admin()
user_a = self._connect_and_login(f"{self.test_name}_2")
user_b = self._connect_and_login(f"{self.test_name}_3")
user_b.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_quantity=10))
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 100)
user_b.test_insert_order(symbol, Side.SELL, Decimal("101.0"), 10)
# =========================================================================
# State tracking across trades and partial fills
# =========================================================================
def test_partial_trade_correctly_updates_outstanding_quantity(self) -> None:
"""After a partial fill, the remaining quantity still counts towards limits."""
symbol = self._create_instrument_via_admin()
user_a = self._connect_and_login(f"{self.test_name}_2")
user_b = self._connect_and_login(f"{self.test_name}_3")
user_a.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_quantity=20))
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 20)
user_b.test_insert_order(symbol, Side.SELL, Decimal("100.0"), 5)
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 5)
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
def test_trade_between_two_limited_users_updates_both(self) -> None:
"""When two users trade, both users' outstanding quantities must decrease."""
symbol = self._create_instrument_via_admin()
user_a = self._connect_and_login(f"{self.test_name}_2")
user_b = self._connect_and_login(f"{self.test_name}_3")
user_a.test_set_user_risk_limits(
UserRiskLimits(max_outstanding_quantity=10))
user_b.test_set_user_risk_limits(
UserRiskLimits(max_outstanding_quantity=10))
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
user_b.test_insert_order(symbol, Side.SELL, Decimal("100.0"), 10)
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
user_b.test_insert_order(symbol, Side.SELL, Decimal("101.0"), 10)
def test_multiple_partial_fills_track_remaining_correctly(self) -> None:
"""Several small trades against a large order must reduce the outstanding by the cumulative traded amount."""
symbol = self._create_instrument_via_admin()
user_a = self._connect_and_login(f"{self.test_name}_2")
user_b = self._connect_and_login(f"{self.test_name}_3")
user_a.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_quantity=30))
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 30)
for _ in range(3):
user_b.test_insert_order(symbol, Side.SELL, Decimal("100.0"), 5)
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 15)
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
# =========================================================================
# Combined limits
# =========================================================================
def test_both_user_and_instrument_limits_checked(self) -> None:
"""An order must satisfy both user-level and instrument-level limits."""
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_user_risk_limits(
UserRiskLimits(max_outstanding_quantity=100))
client.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_quantity=10))
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
def test_instrument_limit_hit_while_user_limit_ok(self) -> None:
"""Even if the user-level limit has room, the instrument-level limit must block."""
sym_a = self._create_instrument_via_admin()
sym_b = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_user_risk_limits(
UserRiskLimits(max_outstanding_quantity=100))
client.test_set_instrument_risk_limits(
sym_a, InstrumentRiskLimits(max_outstanding_quantity=5))
client.test_insert_order(sym_a, Side.BUY, Decimal("100.0"), 5)
client.test_insert_order(sym_a, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
client.test_insert_order(sym_b, Side.BUY, Decimal("100.0"), 50)
def test_user_limit_hit_while_instrument_limit_ok(self) -> None:
"""Even if the instrument-level limit has room, the user-level limit must block."""
sym_a = self._create_instrument_via_admin()
sym_b = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_user_risk_limits(
UserRiskLimits(max_outstanding_quantity=15))
client.test_set_instrument_risk_limits(
sym_a, InstrumentRiskLimits(max_outstanding_quantity=100))
client.test_set_instrument_risk_limits(
sym_b, InstrumentRiskLimits(max_outstanding_quantity=100))
client.test_insert_order(sym_a, Side.BUY, Decimal("100.0"), 10)
client.test_insert_order(sym_b, Side.BUY, Decimal("100.0"), 5)
client.test_insert_order(sym_b, Side.BUY, Decimal("100.0"), 1,
expect_success=False)
# =========================================================================
# Floating-point precision (4-decimal-digit prices/amounts)
#
# The protocol uses double for prices and amounts. Naive use of IEEE 754
# floats leads to well-known rounding errors (e.g. 0.1 * 3 != 0.3).
# These tests verify that the solution uses proper precision handling so
# that orders exactly at the limit are not falsely rejected, and that
# add/subtract cycles leave no residual error that blocks future orders.
# =========================================================================
def test_amount_boundary_not_falsely_rejected_by_float_multiplication(self) -> None:
"""price * qty can overshoot in float (e.g. 0.1 * 3 = 0.30000000000000004).
An order whose exact amount (4-decimal precision) equals the limit must
be accepted despite IEEE 754 rounding in the multiplication.
"""
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_amount=300.03))
# Exact: 100.01 x 3 = 300.03 — equals the limit, must be accepted.
# In float: 100.01 * 3 may produce 300.03000000000003 due to the
# inexact representation of 100.01 in IEEE 754.
client.test_insert_order(symbol, Side.BUY, Decimal("100.01"), 3)
def test_amount_boundary_not_falsely_rejected_by_float_accumulation(self) -> None:
"""Repeated float additions drift away from the true sum.
Three additions of 33.3333 should total 99.9999, leaving room for
exactly 0.0001 more. A naive float accumulation may overshoot 99.9999,
falsely blocking the last order.
"""
symbol = self._create_instrument_via_admin(tick_size=Decimal("0.0001"))
client = self._connect_and_login()
client.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_amount=100.0))
client.test_insert_order(symbol, Side.BUY, Decimal("33.3333"), 1)
client.test_insert_order(symbol, Side.BUY, Decimal("33.3333"), 1)
client.test_insert_order(symbol, Side.BUY, Decimal("33.3333"), 1)
# Exact total so far: 99.9999. One more at 0.0001 → 100.0 = limit.
client.test_insert_order(symbol, Side.BUY, Decimal("0.0001"), 1)
def test_amount_boundary_with_classic_point_one_plus_point_two(self) -> None:
"""The textbook float error: 0.1 + 0.2 = 0.30000000000000004 in IEEE 754.
With limit = 3000, two orders (0.1 x 10000 = 1000, 0.2 x 10000 = 2000)
total exactly 3000. A third fills the boundary via small price.
"""
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_amount=3000.0))
client.test_insert_order(symbol, Side.BUY, Decimal("0.1"), 10000)
client.test_insert_order(symbol, Side.BUY, Decimal("0.2"), 10000)
# Exact total: 1000 + 2000 = 3000 = limit.
# Any additional order must be rejected.
client.test_insert_order(symbol, Side.BUY, Decimal("0.01"), 1,
expect_success=False)
def test_amount_freed_correctly_after_cancelling_fractional_price_orders(self) -> None:
"""After inserting and cancelling orders at fractional prices, the full
capacity must be available again — no residual float error may block.
"""
symbol = self._create_instrument_via_admin(tick_size=Decimal("0.0001"))
client = self._connect_and_login()
client.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_amount=1000.0))
for price in ["33.3333", "33.3334", "33.3333"]:
resp = client.test_insert_order(symbol, Side.BUY, Decimal(price), 1)
order_id = resp.get_response().order_id
client.test_cancel_order(symbol, order_id=order_id)
# Full capacity must be restored despite float add/subtract drift.
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
def test_amount_freed_correctly_after_trades_at_fractional_prices(self) -> None:
"""After trades fully fill orders at fractional prices, the outstanding
amount must return to zero so the full limit is available again.
"""
symbol = self._create_instrument_via_admin(tick_size=Decimal("0.0001"))
user_a = self._connect_and_login(f"{self.test_name}_2")
user_b = self._connect_and_login(f"{self.test_name}_3")
user_a.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_amount=1000.0))
# Insert and fully fill 3 orders at fractional prices.
for price in ["33.3333", "33.3334", "33.3333"]:
user_a.test_insert_order(symbol, Side.BUY, Decimal(price), 1)
user_b.test_insert_order(symbol, Side.SELL, Decimal(price), 1)
# Full capacity must be available again.
user_a.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 10)
def test_amount_freed_correctly_after_partial_trade_at_fractional_price(self) -> None:
"""A partial fill at a fractional price must reduce the outstanding
amount by exactly traded_qty x price, not by a drifted float value.
"""
symbol = self._create_instrument_via_admin(tick_size=Decimal("0.0001"))
user_a = self._connect_and_login(f"{self.test_name}_2")
user_b = self._connect_and_login(f"{self.test_name}_3")
user_a.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_amount=100.0))
# Insert qty 10 at 10.01 → amount = 100.1 > limit? No: 10.01 x 10 = 100.10
# Use a value that fits: 10.0 x 10 = 100.0 = limit
user_a.test_insert_order(symbol, Side.BUY, Decimal("10.0001"), 5)
# Outstanding: 50.0005. Partial fill 3 units → frees 30.0003.
user_b.test_insert_order(symbol, Side.SELL, Decimal("10.0001"), 3)
# Remaining outstanding: 20.0002. Room left: 79.9998.
user_a.test_insert_order(symbol, Side.BUY, Decimal("79.9998"), 1)
def test_order_amount_rolling_limit_boundary_with_fractional_multiplication(self) -> None:
"""The rolling-window amount sum must handle precision the same way as
the outstanding amount — no false rejection at the exact boundary.
"""
symbol = self._create_instrument_via_admin()
client = self._connect_and_login()
client.test_set_instrument_risk_limits(symbol, InstrumentRiskLimits(
order_amount_rolling_limit=RollingWindowLimit(
limit=300, window_in_seconds=60)))
# Exact: 100.01 x 3 = 300.03, but limit is 300 → rejected.
# First fill up to just under the limit.
client.test_insert_order(symbol, Side.BUY, Decimal("99.99"), 1)
client.test_insert_order(symbol, Side.BUY, Decimal("99.99"), 1)
client.test_insert_order(symbol, Side.BUY, Decimal("99.99"), 1)
# Exact rolling total: 299.97. Room for 0.03 more.
client.test_insert_order(symbol, Side.BUY, Decimal("0.01"), 3)
# Now exact total: 299.97 + 0.03 = 300.0 = limit. Next must fail.
client.test_insert_order(symbol, Side.BUY, Decimal("0.01"), 1,
expect_success=False)
def test_many_fractional_insert_cancel_cycles_leave_no_residual(self) -> None:
"""Repeated insert-cancel cycles at the same fractional price must not
accumulate residual float error that eventually blocks orders.
"""
symbol = self._create_instrument_via_admin(tick_size=Decimal("0.0001"))
client = self._connect_and_login()
client.test_set_instrument_risk_limits(
symbol, InstrumentRiskLimits(max_outstanding_amount=100.0))
for _ in range(20):
resp = client.test_insert_order(
symbol, Side.BUY, Decimal("33.3333"), 1)
order_id = resp.get_response().order_id
client.test_cancel_order(symbol, order_id=order_id)
# After 20 round-trips, full capacity must still be available.
client.test_insert_order(symbol, Side.BUY, Decimal("100.0"), 1)