Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
14 changes: 12 additions & 2 deletions examples/websocket_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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}")

Expand All @@ -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)

Expand All @@ -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()
Expand Down
8 changes: 8 additions & 0 deletions polymarket_us/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -106,6 +107,13 @@
"ComboLegDetail",
"ComboLegState",
"ComboSettlement",
# RFQs
"RFQ",
"Quote",
"RFQComboLeg",
"RFQSide",
"RFQStatus",
"QuoteStatus",
# Events
"Event",
"Market",
Expand Down
1 change: 1 addition & 0 deletions polymarket_us/types/portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class Trade(TypedDict, total=False):
updateTime: str
price: Amount
qty: str
qtyDecimal: str
isAggressor: bool
costBasis: Amount
realizedPnl: Amount
Expand Down
77 changes: 77 additions & 0 deletions polymarket_us/types/rfq.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions polymarket_us/websocket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
PositionUpdate,
PrivateMessage,
PrivateSubscriptionType,
RFQEvent,
SubscribeRequest,
Trade,
UnsubscribeRequest,
Expand All @@ -39,6 +40,7 @@
"PositionUpdate",
"AccountBalanceSnapshot",
"AccountBalanceUpdate",
"RFQEvent",
"MarketData",
"MarketDataLite",
"Trade",
Expand Down
8 changes: 7 additions & 1 deletion polymarket_us/websocket/private.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
45 changes: 44 additions & 1 deletion polymarket_us/websocket/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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[
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -225,6 +267,7 @@ class WebSocketErrorMessage(TypedDict, total=False):
| PositionUpdate
| AccountBalanceSnapshot
| AccountBalanceUpdate
| RFQEvent
| Heartbeat
| WebSocketErrorMessage
)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
9 changes: 8 additions & 1 deletion tests/test_portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading
Loading