Skip to content
Draft
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
8 changes: 8 additions & 0 deletions src/polymarket/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
OrderSide,
OrderType,
PerpsBalance,
PerpsBatchLeverageResult,
PerpsBook,
PerpsCandle,
PerpsCredentials,
Expand All @@ -120,6 +121,9 @@
PerpsFundingPayment,
PerpsFundingRate,
PerpsInstrument,
PerpsLeverageUpdate,
PerpsLeverageUpdateRejection,
PerpsLeverageUpdateSuccess,
PerpsOrder,
PerpsOrderPlacement,
PerpsOrderRequest,
Expand Down Expand Up @@ -354,6 +358,7 @@
"Page",
"Paginator",
"PerpsBalance",
"PerpsBatchLeverageResult",
"PerpsBook",
"PerpsCandle",
"PerpsCredentials",
Expand All @@ -363,6 +368,9 @@
"PerpsFundingPayment",
"PerpsFundingRate",
"PerpsInstrument",
"PerpsLeverageUpdate",
"PerpsLeverageUpdateRejection",
"PerpsLeverageUpdateSuccess",
"PerpsOrder",
"PerpsOrderPlacement",
"PerpsOrderRequest",
Expand Down
29 changes: 29 additions & 0 deletions src/polymarket/_internal/actions/perps/trading.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from polymarket.models.perps.orders import PerpsOrder, PerpsPostOrderAck
from polymarket.models.perps.requests import (
DecimalInput,
PerpsLeverageUpdate,
PerpsOrderRequest,
PerpsPositionTpSlTrigger,
PerpsTpSlTrigger,
Expand Down Expand Up @@ -324,6 +325,26 @@ def update_leverage_op(*, instrument_id: int, leverage: int, cross_margin: bool)
return ["updateLeverage", [instrument_id, leverage, cross_margin]]


def update_leverages_op(updates: Sequence[PerpsLeverageUpdate]) -> list[Any]:
items = list(updates)
if not 1 <= len(items) <= 100:
raise UserInputError("updates must contain between 1 and 100 items")

rows: list[list[int | bool]] = []
instrument_ids: set[int] = set()
for update in items:
if not isinstance(update, PerpsLeverageUpdate): # pyright: ignore[reportUnnecessaryIsInstance]
raise UserInputError("updates must contain PerpsLeverageUpdate values")
if update.instrument_id in instrument_ids:
raise UserInputError(
f"updates must contain unique instrument IDs; duplicate {update.instrument_id}"
)
instrument_ids.add(update.instrument_id)
rows.append([update.instrument_id, update.leverage, update.cross_margin])

return ["updateLeverages", rows]


def update_margin_op(*, instrument_id: int, amount: DecimalInput) -> list[Any]:
if isinstance(instrument_id, bool) or not isinstance(instrument_id, int): # pyright: ignore[reportUnnecessaryIsInstance]
raise UserInputError("instrument_id must be an int")
Expand Down Expand Up @@ -361,6 +382,14 @@ def to_command_body_op(op: Sequence[Any]) -> dict[str, Any]:
"type": op_type,
"args": {"cross": cross_margin, "iid": instrument_id, "lev": leverage},
}
if op_type == "updateLeverages":
return {
"type": op_type,
"args": [
{"cross": cross_margin, "iid": instrument_id, "lev": leverage}
for instrument_id, leverage, cross_margin in op[1]
],
}
if op_type == "updateMargin":
instrument_id, amount = op[1]
return {"type": op_type, "args": {"amt": amount, "iid": instrument_id}}
Expand Down
60 changes: 60 additions & 0 deletions src/polymarket/_internal/perps_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
create_orders_op,
to_command_body_op,
update_leverage_op,
update_leverages_op,
update_margin_op,
)
from polymarket._internal.actions.perps.trading import (
Expand Down Expand Up @@ -74,15 +75,19 @@
)
from polymarket.models.perps.orders import (
PerpsAutoCancelResponse,
PerpsBatchLeverageResult,
PerpsCancelAllOrdersResponse,
PerpsCancelOrderResult,
PerpsFill,
PerpsLeverageUpdateRejection,
PerpsLeverageUpdateSuccess,
PerpsOrder,
PerpsPostOrderAck,
PerpsUpdateLeverageResult,
)
from polymarket.models.perps.requests import (
DecimalInput,
PerpsLeverageUpdate,
PerpsOrderRequest,
PerpsPositionTpSlTrigger,
PerpsTpSlTrigger,
Expand Down Expand Up @@ -554,6 +559,28 @@ async def update_leverage(
timeout_message="Perps update leverage response timed out.",
)

async def update_leverages(
self, updates: Sequence[PerpsLeverageUpdate]
) -> tuple[PerpsBatchLeverageResult, ...]:
"""Update leverage for 1–100 unique instruments in request order.

The operation is sequential and non-atomic. Per-instrument rejections
are returned as data. In particular, ``internal_error`` means whether
that instrument's update was applied is unknown. Whole-request and
transport failures raise the normal SDK errors.
"""
items = tuple(updates)
op = update_leverages_op(items)
expected_instrument_ids = tuple(update.instrument_id for update in items)
results = await self._send_signed_command(
op,
parse=lambda data: _parse_batch_leverage_results(
data, expected_instrument_ids=expected_instrument_ids
),
timeout_message="Perps batch leverage response timed out.",
)
return tuple(results)

async def update_margin(self, *, instrument_id: int, amount: DecimalInput) -> None:
"""Adjust isolated margin for an instrument position.

Expand Down Expand Up @@ -1081,6 +1108,39 @@ def _parse_cancel_results(data: object) -> list[PerpsCancelOrderResult]:
return [PerpsCancelOrderResult.parse_response(item) for item in cast("list[object]", data)]


def _parse_batch_leverage_results(
data: object, *, expected_instrument_ids: tuple[int, ...]
) -> list[PerpsBatchLeverageResult]:
if not isinstance(data, list) or not data:
raise ValueError("expected a non-empty list of Perps batch leverage results")

entries = cast("list[object]", data)
first = cast("dict[str, Any]", entries[0]) if isinstance(entries[0], dict) else None
if first is not None and first.get("status") == "err" and "instrument_id" not in first:
raise RequestRejectedError(
_error_ack(first) or "Perps batch leverage request was rejected.", status=200
)
if len(entries) != len(expected_instrument_ids):
raise ValueError("Perps batch leverage response length did not match the request")

results: list[PerpsBatchLeverageResult] = []
for entry in entries:
wire = cast("dict[str, Any]", entry) if isinstance(entry, dict) else None
if wire is None:
raise ValueError("invalid Perps batch leverage result")
if wire.get("status") == "ok":
results.append(PerpsLeverageUpdateSuccess.parse_response(wire))
elif wire.get("status") == "err":
results.append(PerpsLeverageUpdateRejection.parse_response(wire))
else:
raise ValueError("invalid Perps batch leverage result status")

returned_instrument_ids = tuple(result.instrument_id for result in results)
if returned_instrument_ids != expected_instrument_ids:
raise ValueError("Perps batch leverage response order did not match the request")
return results


def _error_ack(value: object) -> str | None:
if not isinstance(value, dict):
return None
Expand Down
8 changes: 8 additions & 0 deletions src/polymarket/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
PerpsAccountStats,
PerpsAutoCancelStatus,
PerpsBalance,
PerpsBatchLeverageResult,
PerpsBbo,
PerpsBook,
PerpsBookDepth,
Expand Down Expand Up @@ -171,6 +172,9 @@
PerpsInstrumentId,
PerpsKlineInterval,
PerpsKnownWithdrawalStatus,
PerpsLeverageUpdate,
PerpsLeverageUpdateRejection,
PerpsLeverageUpdateSuccess,
PerpsMarginSummary,
PerpsOrder,
PerpsOrderId,
Expand Down Expand Up @@ -366,6 +370,7 @@
"PerpsAccountStats",
"PerpsAutoCancelStatus",
"PerpsBalance",
"PerpsBatchLeverageResult",
"PerpsBbo",
"PerpsBook",
"PerpsBookDepth",
Expand All @@ -392,6 +397,9 @@
"PerpsInstrumentCategory",
"PerpsInstrumentId",
"PerpsKlineInterval",
"PerpsLeverageUpdate",
"PerpsLeverageUpdateRejection",
"PerpsLeverageUpdateSuccess",
"PerpsKnownWithdrawalStatus",
"PerpsMarginSummary",
"PerpsOrder",
Expand Down
8 changes: 8 additions & 0 deletions src/polymarket/models/perps/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,18 @@
PerpsPositionLiquidatedNotification,
)
from polymarket.models.perps.orders import (
PerpsBatchLeverageResult,
PerpsCancelOrderResult,
PerpsFill,
PerpsLeverageUpdateRejection,
PerpsLeverageUpdateSuccess,
PerpsOrder,
PerpsPostOrderAck,
PerpsTpSlOrderFields,
PerpsUpdateLeverageResult,
)
from polymarket.models.perps.requests import (
PerpsLeverageUpdate,
PerpsOrderRequest,
PerpsPositionTpSlTrigger,
PerpsTpSlTrigger,
Expand Down Expand Up @@ -127,6 +131,7 @@
"PerpsAutoCancelStatus",
"PerpsBalance",
"PerpsBalanceEvent",
"PerpsBatchLeverageResult",
"PerpsBbo",
"PerpsBboEvent",
"PerpsBook",
Expand Down Expand Up @@ -161,6 +166,9 @@
"PerpsInstrumentId",
"PerpsIsolatedLiquidationWarningNotification",
"PerpsKlineInterval",
"PerpsLeverageUpdate",
"PerpsLeverageUpdateRejection",
"PerpsLeverageUpdateSuccess",
"PerpsKnownWithdrawalStatus",
"PerpsLimitOrderCanceledNotification",
"PerpsLiquidationWarningNotification",
Expand Down
28 changes: 27 additions & 1 deletion src/polymarket/models/perps/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from datetime import datetime
from decimal import Decimal
from typing import Any, Literal, cast
from typing import Any, Literal, TypeAlias, cast

from pydantic import AliasChoices, Field, field_validator, model_validator

Expand Down Expand Up @@ -220,13 +220,39 @@ class PerpsUpdateLeverageResult(BaseModel):
cross_margin: bool = Field(validation_alias="cross")


class PerpsLeverageUpdateSuccess(BaseModel):
"""Accepted configuration from a batch leverage update."""

status: Literal["ok"]
instrument_id: PerpsInstrumentId
leverage: int
cross_margin: bool = Field(validation_alias="cross")


class PerpsLeverageUpdateRejection(BaseModel):
"""Per-instrument rejection from a batch leverage update.

An ``internal_error`` means whether the update was applied is unknown.
"""

status: Literal["err"]
instrument_id: PerpsInstrumentId
error: str = Field(min_length=1)


PerpsBatchLeverageResult: TypeAlias = PerpsLeverageUpdateSuccess | PerpsLeverageUpdateRejection


__all__ = [
"PerpsAutoCancelResponse",
"PerpsCancelAllOrdersResponse",
"PerpsBatchLeverageResult",
"PerpsCancelOrderResult",
"PerpsFill",
"PerpsOrder",
"PerpsPostOrderAck",
"PerpsLeverageUpdateRejection",
"PerpsLeverageUpdateSuccess",
"PerpsTpSlOrderFields",
"PerpsUpdateLeverageResult",
]
26 changes: 26 additions & 0 deletions src/polymarket/models/perps/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from polymarket.models.types import OrderSide

_CLIENT_ORDER_ID = re.compile(r"^[0-9a-f]{32}$")
_MAX_U32 = 2**32 - 1

DecimalInput = Decimal | int | float | str
"""Decimal-valued input accepted for Perps prices and quantities."""
Expand Down Expand Up @@ -166,8 +167,33 @@ def __post_init__(self) -> None:
to_decimal_string("trigger_price", self.trigger_price)


@dataclass(frozen=True, slots=True, kw_only=True)
class PerpsLeverageUpdate:
"""One instrument configuration in a batch leverage update."""

instrument_id: int
"""Perps instrument identifier."""
leverage: int
"""Positive leverage multiplier."""
cross_margin: bool
"""Whether the instrument should use cross margin."""

def __post_init__(self) -> None:
if isinstance(self.instrument_id, bool) or not isinstance(self.instrument_id, int): # pyright: ignore[reportUnnecessaryIsInstance]
raise UserInputError("instrument_id must be an int")
if not 0 <= self.instrument_id <= _MAX_U32:
raise UserInputError(f"instrument_id must be between 0 and {_MAX_U32}")
if isinstance(self.leverage, bool) or not isinstance(self.leverage, int): # pyright: ignore[reportUnnecessaryIsInstance]
raise UserInputError("leverage must be an int")
if not 1 <= self.leverage <= _MAX_U32:
raise UserInputError(f"leverage must be between 1 and {_MAX_U32}")
if not isinstance(self.cross_margin, bool): # pyright: ignore[reportUnnecessaryIsInstance]
raise UserInputError("cross_margin must be a bool")


__all__ = [
"DecimalInput",
"PerpsLeverageUpdate",
"PerpsOrderRequest",
"PerpsPositionTpSlTrigger",
"PerpsTpSlTrigger",
Expand Down
10 changes: 10 additions & 0 deletions src/polymarket/perps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
PerpsNotificationsPage,
PerpsNotificationsPaginator,
)
from polymarket.models.perps.orders import (
PerpsBatchLeverageResult,
PerpsLeverageUpdateRejection,
PerpsLeverageUpdateSuccess,
)
from polymarket.models.perps.requests import PerpsLeverageUpdate
from polymarket.models.perps.results import (
PerpsOrderPlacement,
PerpsPlacedTpSlOrder,
Expand All @@ -20,6 +26,10 @@

__all__ = [
"PerpsNotificationEntry",
"PerpsBatchLeverageResult",
"PerpsLeverageUpdate",
"PerpsLeverageUpdateRejection",
"PerpsLeverageUpdateSuccess",
"PerpsNotificationsPage",
"PerpsNotificationsPaginator",
"PerpsOrderPlacement",
Expand Down
Loading
Loading