diff --git a/README.md b/README.md index 32cd1eb..45b07a7 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,7 @@ WebSocket methods (`connect()`, `subscribe()`, `close()`) are async and must be - `position_update` - Position changes - `account_balance_snapshot` - Initial balance - `account_balance_update` - Balance changes +- `rfq_event` - RFQ and quote lifecycle events - `heartbeat` - Connection keepalive - `error` - Error events - `close` - Connection closed @@ -358,6 +359,30 @@ aliases are retained, and both the named callback and `message` receive the original envelope without renaming fields. An empty `error` string no longer suppresses a successful data callback. +RFQ subscriptions deliver lifecycle events through `rfq_event` without an initial +snapshot. Market filters are not supported. + +```python +from polymarket_us.websocket import RFQEvent + + +def on_rfq_event(data: RFQEvent) -> None: + event = data["rfqEvent"] + created = event.get("rfqCreated") + rfq = created["rfq"] if created is not None else None + if rfq is not None: + print(rfq["id"], rfq.get("qtyDecimal")) + + +private_ws.on("rfq_event", on_rfq_event) +await private_ws.subscribe_rfq("rfqs-1") +``` + +Other event keys are `rfqClosed`, `quoteCreated`, `quoteDeleted`, `quoteAccepted`, +`quoteConfirmed`, and `quoteExecuted`. Timestamps and nested RFQ/quote objects may +be null. Portfolio activity trades also expose `qtyDecimal` as an exact decimal +string; use it instead of the rounded `qty` when fractional quantities matter. + #### Private callback type migration (2.0.0) `PositionUpdate`, `AccountBalanceSnapshot` and `AccountBalanceUpdate` now describe diff --git a/examples/websocket_example.py b/examples/websocket_example.py index 017368e..96dda44 100644 --- a/examples/websocket_example.py +++ b/examples/websocket_example.py @@ -5,7 +5,12 @@ from typing import Any from polymarket_us import PolymarketUS -from polymarket_us.websocket import AccountBalanceSnapshot, AccountBalanceUpdate, PositionUpdate +from polymarket_us.websocket import ( + AccountBalanceSnapshot, + AccountBalanceUpdate, + PositionUpdate, + RFQEvent, +) async def main() -> None: @@ -19,7 +24,7 @@ async def main() -> None: client = PolymarketUS(key_id=key_id, secret_key=secret_key) - # Private WebSocket for orders, positions, and balances + # Private WebSocket for orders, positions, balances, and RFQs private_ws = client.ws.private() def on_order_snapshot(data: dict[str, Any]) -> None: @@ -46,6 +51,9 @@ def on_balance_update(data: AccountBalanceUpdate) -> None: if after is not None: print(f"[Private] Balance: {after.get('currentBalance')} {after.get('currency')}") + def on_rfq_event(data: RFQEvent) -> None: + print(f"[Private] RFQ event: {data['rfqEvent']}") + def on_error(error: Exception) -> None: print(f"[Error] {error}") @@ -57,6 +65,7 @@ def on_heartbeat() -> None: private_ws.on("position_update", on_position_update) private_ws.on("account_balance_snapshot", on_balance_snapshot) private_ws.on("account_balance_update", on_balance_update) + private_ws.on("rfq_event", on_rfq_event) private_ws.on("error", on_error) private_ws.on("heartbeat", on_heartbeat) @@ -66,6 +75,7 @@ def on_heartbeat() -> None: await private_ws.subscribe_orders("orders-1") await private_ws.subscribe_positions("positions-1") await private_ws.subscribe_account_balance("balance-1") + await private_ws.subscribe_rfq("rfqs-1") # Markets WebSocket for order book and trades markets_ws = client.ws.markets() diff --git a/polymarket_us/types/__init__.py b/polymarket_us/types/__init__.py index 8176b9d..35cda35 100644 --- a/polymarket_us/types/__init__.py +++ b/polymarket_us/types/__init__.py @@ -77,6 +77,7 @@ Trade, UserPosition, ) +from polymarket_us.types.rfq import RFQ, Quote, QuoteStatus, RFQComboLeg, RFQSide, RFQStatus from polymarket_us.types.search import SearchParams, SearchResponse from polymarket_us.types.series import ( GetSeriesListResponse, @@ -106,6 +107,13 @@ "ComboLegDetail", "ComboLegState", "ComboSettlement", + # RFQs + "RFQ", + "Quote", + "RFQComboLeg", + "RFQSide", + "RFQStatus", + "QuoteStatus", # Events "Event", "Market", diff --git a/polymarket_us/types/portfolio.py b/polymarket_us/types/portfolio.py index fc785e1..7eabed6 100644 --- a/polymarket_us/types/portfolio.py +++ b/polymarket_us/types/portfolio.py @@ -72,6 +72,7 @@ class Trade(TypedDict, total=False): updateTime: str price: Amount qty: str + qtyDecimal: str isAggressor: bool costBasis: Amount realizedPnl: Amount diff --git a/polymarket_us/types/rfq.py b/polymarket_us/types/rfq.py new file mode 100644 index 0000000..cf3903e --- /dev/null +++ b/polymarket_us/types/rfq.py @@ -0,0 +1,77 @@ +"""Private RFQ stream types.""" + +from typing import Literal, TypedDict + +RFQStatus = Literal["RFQ_STATUS_UNSPECIFIED", "RFQ_STATUS_OPEN", "RFQ_STATUS_CLOSED"] + +QuoteStatus = Literal[ + "QUOTE_STATUS_UNDEFINED", + "QUOTE_STATUS_ACTIVE", + "QUOTE_STATUS_CONFIRMED", + "QUOTE_STATUS_DELETED", + "QUOTE_STATUS_ACCEPTED", + "QUOTE_STATUS_EXECUTED", +] + +RFQSide = Literal["SIDE_UNDEFINED", "SIDE_BUY", "SIDE_SELL"] + + +class _OptionalRFQComboLeg(TypedDict, total=False): + settlementPrice: str + + +class RFQComboLeg(_OptionalRFQComboLeg): + """A leg in an RFQ's combo market.""" + + symbol: str + side: RFQSide + + +class _OptionalRFQ(TypedDict, total=False): + qtyDecimal: str + cashOrderQty: str + tickSize: float + + +class RFQ(_OptionalRFQ): + """RFQ details emitted by the private stream.""" + + id: str + symbol: str + rfqCreatorUserId: str + createdTime: str | None + restRemainder: bool + status: RFQStatus + updatedTime: str | None + comboLegs: list[RFQComboLeg] + + +class _OptionalQuote(TypedDict, total=False): + rfqCashOrderQty: str + rfqCreatorOrderId: str + creatorOrderId: str + + +class Quote(_OptionalQuote): + """Quote details, including lifecycle timestamps and exact decimal strings.""" + + id: str + rfqId: str + creatorRfqUserId: str + symbol: str + status: QuoteStatus + createdTime: str | None + buyPrice: str + sellPrice: str + restRemainder: bool + postOnly: bool + rfqCreatorUserId: str + buyQtyDecimal: str + sellQtyDecimal: str + updatedTime: str | None + acceptedSide: RFQSide + acceptedTime: str | None + confirmedTime: str | None + confirmationDeadline: str | None + executionDeadline: str | None + executedTime: str | None diff --git a/polymarket_us/websocket/__init__.py b/polymarket_us/websocket/__init__.py index f96b3a7..9f5be1c 100644 --- a/polymarket_us/websocket/__init__.py +++ b/polymarket_us/websocket/__init__.py @@ -16,6 +16,7 @@ PositionUpdate, PrivateMessage, PrivateSubscriptionType, + RFQEvent, SubscribeRequest, Trade, UnsubscribeRequest, @@ -39,6 +40,7 @@ "PositionUpdate", "AccountBalanceSnapshot", "AccountBalanceUpdate", + "RFQEvent", "MarketData", "MarketDataLite", "Trade", diff --git a/polymarket_us/websocket/private.py b/polymarket_us/websocket/private.py index 07bd0bb..5efd9e8 100644 --- a/polymarket_us/websocket/private.py +++ b/polymarket_us/websocket/private.py @@ -9,7 +9,7 @@ class PrivateWebSocket(BaseWebSocket): - """WebSocket for private data (orders, positions, balances).""" + """WebSocket for private data (orders, positions, balances, RFQs).""" def __init__(self, **kwargs: str) -> None: """Initialize private WebSocket.""" @@ -45,6 +45,10 @@ async def subscribe_account_balance(self, request_id: str) -> None: """ await self.subscribe(request_id, "SUBSCRIPTION_TYPE_ACCOUNT_BALANCE") + async def subscribe_rfq(self, request_id: str) -> None: + """Subscribe to RFQ events without a market filter.""" + await self.subscribe(request_id, "SUBSCRIPTION_TYPE_RFQ") + async def subscribe_all( self, request_id: str, @@ -99,3 +103,5 @@ def _handle_message(self, data: str) -> None: or "accountBalanceUpdate" in message ): self._emit("account_balance_update", message) + elif "rfqEvent" in message: + self._emit("rfq_event", message) diff --git a/polymarket_us/websocket/types.py b/polymarket_us/websocket/types.py index 33af980..3d6f6be 100644 --- a/polymarket_us/websocket/types.py +++ b/polymarket_us/websocket/types.py @@ -2,13 +2,14 @@ from typing import Literal, TypedDict -from polymarket_us.types import Amount, Execution, Order, UserBalance, UserPosition +from polymarket_us.types import RFQ, Amount, Execution, Order, Quote, UserBalance, UserPosition PrivateSubscriptionType = Literal[ "SUBSCRIPTION_TYPE_ORDER", "SUBSCRIPTION_TYPE_ORDER_SNAPSHOT", "SUBSCRIPTION_TYPE_POSITION", "SUBSCRIPTION_TYPE_ACCOUNT_BALANCE", + "SUBSCRIPTION_TYPE_RFQ", ] MarketSubscriptionType = Literal[ @@ -138,6 +139,47 @@ class AccountBalanceUpdate(TypedDict): accountBalancesUpdate: _AccountBalancesUpdate +class _RFQChange(TypedDict): + rfq: RFQ | None + + +class _QuoteChange(TypedDict): + quote: Quote | None + + +class _QuoteAccepted(_QuoteChange): + confirmationDeadline: str | None + + +class _QuoteConfirmed(_QuoteChange): + executionDeadline: str | None + + +class _QuoteExecuted(_QuoteChange): + orderId: str + clientOrderId: str + executedTime: str | None + + +class _RFQEventPayload(TypedDict, total=False): + # The gateway emits only the selected event; unknown future events may be empty. + rfqCreated: _RFQChange + rfqClosed: _RFQChange + quoteCreated: _QuoteChange + quoteDeleted: _QuoteChange + quoteAccepted: _QuoteAccepted + quoteConfirmed: _QuoteConfirmed + quoteExecuted: _QuoteExecuted + + +class RFQEvent(TypedDict): + """Private RFQ event in the gateway wire format.""" + + requestId: str + subscriptionType: Literal["SUBSCRIPTION_TYPE_RFQ"] + rfqEvent: _RFQEventPayload + + class _OrderBookLevel(TypedDict): px: Amount qty: str @@ -225,6 +267,7 @@ class WebSocketErrorMessage(TypedDict, total=False): | PositionUpdate | AccountBalanceSnapshot | AccountBalanceUpdate + | RFQEvent | Heartbeat | WebSocketErrorMessage ) diff --git a/pyproject.toml b/pyproject.toml index aa3b341..9760c23 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "polymarket-us" -version = "2.0.0" +version = "2.1.0" description = "Polymarket US Python SDK" readme = "README.md" license = "MIT" diff --git a/tests/test_portfolio.py b/tests/test_portfolio.py index 069d2a7..10c9f6e 100644 --- a/tests/test_portfolio.py +++ b/tests/test_portfolio.py @@ -86,13 +86,20 @@ def test_get_activities_with_auth( mock_response = MagicMock() mock_response.is_success = True mock_response.text = '{"activities": []}' - mock_response.json.return_value = {"activities": [{"type": "ACTIVITY_TYPE_TRADE"}]} + mock_response.json.return_value = { + "activities": [ + {"type": "ACTIVITY_TYPE_TRADE", "trade": {"qty": "0", "qtyDecimal": "0.0100"}} + ] + } mock_request.return_value = mock_response response = auth_client.portfolio.activities() assert "activities" in response assert len(response["activities"]) == 1 + trade = response["activities"][0]["trade"] + assert trade["qty"] == "0" + assert trade["qtyDecimal"] == "0.0100" @patch.object(httpx.Client, "request") def test_uses_correct_path(self, mock_request: MagicMock, auth_client: PolymarketUS) -> None: diff --git a/tests/test_rfq_callbacks.py b/tests/test_rfq_callbacks.py new file mode 100644 index 0000000..d3b86a4 --- /dev/null +++ b/tests/test_rfq_callbacks.py @@ -0,0 +1,75 @@ +"""RFQ wire regressions from gateway eb891a3e and protojson EmitUnpopulated.""" + +import asyncio +import json +from unittest.mock import Mock + +import pytest +from websockets.asyncio.server import ServerConnection, serve + +from polymarket_us import PolymarketUS +from polymarket_us.errors import WebSocketError +from polymarket_us.websocket import PrivateWebSocket, RFQEvent +from tests.types.rfq_callbacks import frames + +TEST_SECRET_KEY = "nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=" + + +@pytest.mark.parametrize( + "frame", frames, ids=[next(iter(frame["rfqEvent"]), "emptyEvent") for frame in frames] +) +async def test_gateway_rfq_frames_reach_callback(frame: RFQEvent) -> None: + async def handler(peer: ServerConnection) -> None: + request = json.loads(await peer.recv()) + assert request == { + "subscribe": {"requestId": "rfqs", "subscriptionType": "SUBSCRIPTION_TYPE_RFQ"} + } + await peer.send(json.dumps(frame)) + await peer.send('{"heartbeat": {}}') + await peer.wait_closed() + + async with serve(handler, "127.0.0.1", 0, close_timeout=1) as server: + port = server.sockets[0].getsockname()[1] + with PolymarketUS( + key_id="offline", secret_key=TEST_SECRET_KEY, api_base_url=f"http://127.0.0.1:{port}" + ) as client: + ws = client.ws.private() + raw, callback, errors = Mock(), Mock(), Mock() + done = asyncio.Event() + ws.on("message", raw) + ws.on("rfq_event", callback) + ws.on("error", errors) + ws.on("heartbeat", done.set) + try: + await asyncio.wait_for(ws.connect(), 5) + await ws.subscribe_rfq("rfqs") + await asyncio.wait_for(done.wait(), 5) + callback.assert_called_once_with(frame) + assert raw.call_count == 2 + assert callback.call_args.args[0] is raw.call_args_list[0].args[0] + errors.assert_not_called() + finally: + await asyncio.wait_for(ws.close(), 5) + + +@pytest.mark.parametrize("error", ["", "subscription failed"]) +def test_rfq_error_precedence(error: str) -> None: + ws = PrivateWebSocket(key_id="offline", secret_key=TEST_SECRET_KEY) + raw, callback, errors = Mock(), Mock(), Mock() + ws.on("message", raw) + ws.on("rfq_event", callback) + ws.on("error", errors) + frame = {**frames[0], "error": error} + ws._handle_message(json.dumps(frame)) + raw.assert_called_once_with(frame) + if error: + callback.assert_not_called() + errors.assert_called_once() + failure = errors.call_args.args[0] + assert isinstance(failure, WebSocketError) + assert failure.request_id == "rfqs" + assert str(failure) == error + else: + errors.assert_not_called() + callback.assert_called_once_with(frame) + assert callback.call_args.args[0] is raw.call_args.args[0] diff --git a/tests/types/rfq_callbacks.py b/tests/types/rfq_callbacks.py new file mode 100644 index 0000000..cfc636e --- /dev/null +++ b/tests/types/rfq_callbacks.py @@ -0,0 +1,320 @@ +"""Typed consumers of gateway RFQ frames and exact portfolio quantities. + +Fixtures were serialized from gateway eb891a3e with protojson EmitUnpopulated. +""" + +from decimal import Decimal + +from polymarket_us.types import ( + RFQ, + Activity, + Quote, + QuoteStatus, + RFQComboLeg, + RFQSide, + RFQStatus, + Trade, +) +from polymarket_us.websocket import PrivateMessage, PrivateSubscriptionType, RFQEvent + +frames: list[RFQEvent] = [ + { + "requestId": "rfqs", + "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", + "rfqEvent": { + "rfqCreated": { + "rfq": { + "id": "rfq-1", + "qtyDecimal": "0.0100", + "symbol": "combo-example", + "rfqCreatorUserId": "requester-1", + "createdTime": "2026-09-28T12:00:00.123456789Z", + "restRemainder": False, + "status": "RFQ_STATUS_OPEN", + "updatedTime": "2026-09-28T12:00:01.123456789Z", + "comboLegs": [ + {"symbol": "market-a", "side": "SIDE_BUY", "settlementPrice": "0"}, + {"symbol": "market-b", "side": "SIDE_SELL"}, + ], + "tickSize": 0.0001, + } + } + }, + }, + { + "requestId": "rfqs", + "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", + "rfqEvent": { + "rfqClosed": { + "rfq": { + "id": "rfq-1", + "cashOrderQty": "12.3400", + "symbol": "combo-example", + "rfqCreatorUserId": "requester-1", + "createdTime": "2026-09-28T12:00:00.123456789Z", + "restRemainder": False, + "status": "RFQ_STATUS_CLOSED", + "updatedTime": "2026-09-28T12:00:01.123456789Z", + "comboLegs": [ + {"symbol": "market-a", "side": "SIDE_BUY", "settlementPrice": "0"}, + {"symbol": "market-b", "side": "SIDE_SELL"}, + ], + "tickSize": 0.0001, + } + } + }, + }, + { + "requestId": "rfqs", + "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", + "rfqEvent": { + "quoteCreated": { + "quote": { + "id": "quote-1", + "rfqId": "rfq-1", + "creatorRfqUserId": "quoter-1", + "symbol": "combo-example", + "status": "QUOTE_STATUS_ACTIVE", + "createdTime": "2026-09-28T12:00:00.123456789Z", + "buyPrice": "0.1234", + "sellPrice": "0.8766", + "restRemainder": True, + "postOnly": False, + "rfqCreatorUserId": "requester-1", + "rfqCashOrderQty": "12.3400", + "buyQtyDecimal": "19.6000", + "sellQtyDecimal": "0.0100", + "updatedTime": "2026-09-28T12:00:01.123456789Z", + "acceptedSide": "SIDE_UNDEFINED", + "acceptedTime": None, + "confirmedTime": None, + "confirmationDeadline": None, + "executionDeadline": None, + "executedTime": None, + } + } + }, + }, + { + "requestId": "rfqs", + "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", + "rfqEvent": { + "quoteDeleted": { + "quote": { + "id": "quote-1", + "rfqId": "rfq-1", + "creatorRfqUserId": "quoter-1", + "symbol": "combo-example", + "status": "QUOTE_STATUS_DELETED", + "createdTime": "2026-09-28T12:00:00.123456789Z", + "buyPrice": "0.1234", + "sellPrice": "0.8766", + "restRemainder": True, + "postOnly": False, + "rfqCreatorUserId": "requester-1", + "rfqCashOrderQty": "12.3400", + "buyQtyDecimal": "19.6000", + "sellQtyDecimal": "0.0100", + "updatedTime": "2026-09-28T12:00:01.123456789Z", + "acceptedSide": "SIDE_UNDEFINED", + "acceptedTime": None, + "confirmedTime": None, + "confirmationDeadline": None, + "executionDeadline": None, + "executedTime": None, + } + } + }, + }, + { + "requestId": "rfqs", + "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", + "rfqEvent": { + "quoteAccepted": { + "quote": { + "id": "quote-1", + "rfqId": "rfq-1", + "creatorRfqUserId": "quoter-1", + "symbol": "combo-example", + "status": "QUOTE_STATUS_ACCEPTED", + "createdTime": "2026-09-28T12:00:00.123456789Z", + "buyPrice": "0.1234", + "sellPrice": "0.8766", + "restRemainder": True, + "postOnly": False, + "rfqCreatorUserId": "requester-1", + "rfqCashOrderQty": "12.3400", + "buyQtyDecimal": "19.6000", + "sellQtyDecimal": "0.0100", + "updatedTime": "2026-09-28T12:00:01.123456789Z", + "acceptedSide": "SIDE_BUY", + "acceptedTime": "2026-09-28T12:00:02.123456789Z", + "confirmedTime": None, + "confirmationDeadline": "2026-09-28T12:00:10.123456789Z", + "executionDeadline": None, + "executedTime": None, + }, + "confirmationDeadline": "2026-09-28T12:00:10.123456789Z", + } + }, + }, + { + "requestId": "rfqs", + "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", + "rfqEvent": { + "quoteConfirmed": { + "quote": { + "id": "quote-1", + "rfqId": "rfq-1", + "creatorRfqUserId": "quoter-1", + "symbol": "combo-example", + "status": "QUOTE_STATUS_CONFIRMED", + "createdTime": "2026-09-28T12:00:00.123456789Z", + "buyPrice": "0.1234", + "sellPrice": "0.8766", + "restRemainder": True, + "postOnly": False, + "rfqCreatorUserId": "requester-1", + "rfqCashOrderQty": "12.3400", + "buyQtyDecimal": "19.6000", + "sellQtyDecimal": "0.0100", + "updatedTime": "2026-09-28T12:00:01.123456789Z", + "acceptedSide": "SIDE_BUY", + "acceptedTime": "2026-09-28T12:00:02.123456789Z", + "confirmedTime": "2026-09-28T12:00:03.123456789Z", + "confirmationDeadline": "2026-09-28T12:00:10.123456789Z", + "executionDeadline": "2026-09-28T12:00:11.123456789Z", + "executedTime": None, + }, + "executionDeadline": "2026-09-28T12:00:11.123456789Z", + } + }, + }, + { + "requestId": "rfqs", + "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", + "rfqEvent": { + "quoteExecuted": { + "quote": { + "id": "quote-1", + "rfqId": "rfq-1", + "creatorRfqUserId": "quoter-1", + "symbol": "combo-example", + "status": "QUOTE_STATUS_EXECUTED", + "createdTime": "2026-09-28T12:00:00.123456789Z", + "buyPrice": "0.1234", + "sellPrice": "0.8766", + "restRemainder": True, + "postOnly": False, + "rfqCreatorUserId": "requester-1", + "rfqCashOrderQty": "12.3400", + "buyQtyDecimal": "19.6000", + "sellQtyDecimal": "0.0100", + "updatedTime": "2026-09-28T12:00:01.123456789Z", + "acceptedSide": "SIDE_BUY", + "acceptedTime": "2026-09-28T12:00:02.123456789Z", + "confirmedTime": "2026-09-28T12:00:03.123456789Z", + "confirmationDeadline": "2026-09-28T12:00:10.123456789Z", + "executionDeadline": "2026-09-28T12:00:11.123456789Z", + "executedTime": "2026-09-28T12:00:11.123456789Z", + "rfqCreatorOrderId": "requester-order-1", + "creatorOrderId": "quoter-order-1", + }, + "orderId": "own-order-1", + "clientOrderId": "own-client-order-1", + "executedTime": "2026-09-28T12:00:11.123456789Z", + } + }, + }, + { + "requestId": "rfqs", + "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", + "rfqEvent": { + "rfqCreated": { + "rfq": { + "id": "", + "symbol": "", + "rfqCreatorUserId": "", + "createdTime": None, + "restRemainder": False, + "status": "RFQ_STATUS_UNSPECIFIED", + "updatedTime": None, + "comboLegs": [{"symbol": "", "side": "SIDE_UNDEFINED"}], + } + } + }, + }, + { + "requestId": "rfqs", + "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", + "rfqEvent": { + "quoteCreated": { + "quote": { + "id": "", + "rfqId": "", + "creatorRfqUserId": "", + "symbol": "", + "status": "QUOTE_STATUS_UNDEFINED", + "createdTime": None, + "buyPrice": "", + "sellPrice": "", + "restRemainder": False, + "postOnly": False, + "rfqCreatorUserId": "", + "buyQtyDecimal": "", + "sellQtyDecimal": "", + "updatedTime": None, + "acceptedSide": "SIDE_UNDEFINED", + "acceptedTime": None, + "confirmedTime": None, + "confirmationDeadline": None, + "executionDeadline": None, + "executedTime": None, + } + } + }, + }, + { + "requestId": "rfqs", + "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", + "rfqEvent": {"rfqCreated": {"rfq": None}}, + }, + { + "requestId": "rfqs", + "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", + "rfqEvent": {"quoteAccepted": {"quote": None, "confirmationDeadline": None}}, + }, + {"requestId": "rfqs", "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", "rfqEvent": {}}, + { + "requestId": "rfqs", + "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", + "rfqEvent": {"rfqCreated": {"rfq": None}}, + }, + {"requestId": "rfqs", "subscriptionType": "SUBSCRIPTION_TYPE_RFQ", "rfqEvent": {}}, +] + +subscription: PrivateSubscriptionType = "SUBSCRIPTION_TYPE_RFQ" +messages: list[PrivateMessage] = [*frames] +side: RFQSide = "SIDE_BUY" +status: RFQStatus = "RFQ_STATUS_CLOSED" +quote_status: QuoteStatus = "QUOTE_STATUS_EXECUTED" +leg: RFQComboLeg = {"symbol": "market-a", "side": side, "settlementPrice": "0"} +trade: Trade = {"qty": "0", "qtyDecimal": "0.0100"} +activity: Activity = {"trade": trade} + + +def exact_trade_quantity(data: Activity) -> Decimal | None: + value = data.get("trade", {}).get("qtyDecimal") + return Decimal(value) if value is not None else None + + +def created_rfq(data: RFQEvent) -> RFQ | None: + created = data["rfqEvent"].get("rfqCreated") + return created["rfq"] if created is not None else None + + +def executed_quote(data: RFQEvent) -> tuple[Quote | None, str, str | None] | None: + executed = data["rfqEvent"].get("quoteExecuted") + if executed is None: + return None + return executed["quote"], executed["orderId"], executed["executedTime"]