Skip to content

feat(client): add perps support#133

Open
cesarenaldi wants to merge 3 commits into
mainfrom
feat/perps-support
Open

feat(client): add perps support#133
cesarenaldi wants to merge 3 commits into
mainfrom
feat/perps-support

Conversation

@cesarenaldi

@cesarenaldi cesarenaldi commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Ports Perps support from the TypeScript SDK (ts-sdk #133, #145, #147, #149, #154, #158–#162, #167, #172, and TP/SL from #153) to the Python SDK. Perps lands on the async clients only since the trading UX is WebSocket-centric.

Surface

Public reads (AsyncPublicClient and AsyncSecureClient)

  • fetch_perps_instruments/ticker/tickers/book/fees (tickers merge 24h statistics)
  • list_perps_candles/funding_history/trades with SDK-owned opaque cursors (forward interval stepping for candles, backward windows with boundary trade-id dedupe for trades)

Market data streaming

  • New specs routed through subscribe(): PerpsTradesSpec, PerpsBboSpec, PerpsBookSpec, PerpsCandlesSpec, PerpsTickersSpec, PerpsStatisticsSpec
  • Channel dedupe (an all-instruments subscription subsumes per-instrument channels) and reconnect with resubscribe

Delegated credentials (AsyncSecureClient)

  • open_perps_session() creates credentials with a wallet-signed CreateProxy payload (default 7d expiry, expires_in=timedelta(...) override) or validates and resumes existing PerpsCredentials
  • revoke_perps_credentials() signs a deleteProxy op with the owner account

PerpsSession (trading over WebSocket)

  • Commands signed as EIP-712 Op payloads committing to msgpack-encoded positional tuples, using the delegated session key
  • place_order (gtc/ioc/fok overloads; gtc requires price and allows post_only; optional take_profit/stop_loss triggers place a grouped bracket), post_orders, place_position_tp_sl, cancel_order(s), update_leverage
  • Account reads: balances, portfolio, stats, config, open orders, orders, and paginated fills, funding payments, deposits, withdrawals, equity, and PnL history
  • Async-iterable session events across all private channels (including tpsl) with sequence-gap detection and reconnect resync events

Funds

  • deposit_to_perps (EOA broadcast or gasless relayer call for proxy/safe/deposit wallets)
  • withdraw_from_perps (owner-signed Withdraw typed data; note the wire timestamp is seconds)

Notable decisions

  • expires_in takes a timedelta; models expose Decimal and datetime
  • Invalid pagination cursors consistently raise UserInputError (the TS session account reads raise TransportError; flagged as an inconsistency there)
  • place_order returns one PerpsOrderPlacement(order, tp_sl=None) result type instead of overloaded result shapes
  • The session buffers recent order updates before waiting on the post acknowledgement, because in asyncio the orders-channel update can be processed before the ack resumes the caller (TS avoids this only through microtask ordering)
  • msgpack added as a core dependency for op signing

Verification

  • Signing is pinned byte-for-byte against golden vectors generated from the TypeScript implementation: msgpack encoding, keccak Op.data hashes, and full signatures for createOrders (including grouped TP/SL rows), cancels, updateLeverage, deleteProxy, CreateProxy, and Withdraw; the deposit calldata is pinned against the TS encoder as well
  • uv run ruff check ., uv run ruff format --check ., uv run pyright (0 errors), uv run pytest -m "not integration" (1782 passed)
  • Live integration suite mirrors the TS perps.test.ts and passed against production, including the metered flows: 10 USDC deposit/withdraw roundtrip with deposit confirmation tracking, GTC place/cancel, TP/SL bracket place/cancel, and credential create/resume/revoke/invalid-secret
  • Integration tests reuse the shared deposit_wallet_client fixture; the deposit test uses a relayer-enabled variant with the builder API key already provided by the integration workflow

Note

High Risk
Large new trading, signing, and funds-moving surface (orders, withdrawals, deposits, delegated keys); mistakes affect live collateral and positions despite test coverage.

Overview
Introduces Perps (perpetuals) as a first-class async surface, ported from the TypeScript SDK. msgpack is added for EIP-712 command signing; Environment gains perps_url, perps_ws_url, and perps_deposit_contract; trading approvals now include the Perps deposit contract spender.

Public and secure async clients get REST helpers (fetch_perps_*, paginated list_perps_candles/funding_history/trades with SDK-owned cursors) and subscribe() support via new Perps*Spec types, backed by a multiplexed PerpsMarketStreamManager.

AsyncSecureClient adds delegated credential lifecycle (open_perps_session, revoke_perps_credentials), deposit_to_perps / withdraw_from_perps, and PerpsSession: one WebSocket for signed trading (place/cancel orders, leverage, TP/SL brackets), private account REST reads, and iterable session events with reconnect and sequence-gap resync.

New polymarket.models.perps types and package exports wire through __init__, streams, and polymarket.perps.

Reviewed by Cursor Bugbot for commit 4073de5. Bugbot is set up for automated code reviews on this repo. Configure here.

Port Perps support from the TypeScript SDK to the async clients:

- Public reads on AsyncPublicClient and AsyncSecureClient: instruments,
  tickers (with statistics merge), book, fees, and paginated candles,
  funding history, and trades with SDK-owned opaque cursors.
- Perps market data streaming through subscribe() with per-topic specs
  (trades, bbo, book, candles, tickers, statistics), channel dedupe,
  and reconnect with resubscribe.
- Delegated credential lifecycle: open_perps_session creates or resumes
  credentials (CreateProxy typed-data signature, validation, default 7d
  expiry) and revoke_perps_credentials signs deleteProxy ops.
- PerpsSession trading over WebSocket with EIP-712 signed msgpack ops:
  place_order (gtc/ioc/fok overloads, optional TP/SL triggers),
  post_orders, place_position_tp_sl, cancel_order(s), update_leverage,
  session account reads, and an async-iterable event stream with
  sequence-gap and reconnect resync events.
- Funds: deposit_to_perps (EOA broadcast or gasless relayer call) and
  withdraw_from_perps (owner-signed Withdraw typed data).

Signing is pinned byte-for-byte against golden vectors generated from
the TypeScript implementation (msgpack encoding, Op data hash, and
signatures). Verified with ruff, pyright, the unit suite, and the live
integration suite including a 10 USDC deposit/withdraw roundtrip and
GTC and TP/SL order place/cancel against production.
if has_more and last_ts is not None:
next_cursor = encode_perps_cursor(
{**state, "start_timestamp": last_ts + interval_ms(state["interval"])}
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Candles pagination ignores end bound

Medium Severity

list_perps_candles sets has_more from the API more flag and a parsed last timestamp only, without comparing the next window start to end_timestamp. Account equity/PnL paginators in the same change stop when the last point reaches the end bound. Candle paging can advance start_timestamp past end, causing an invalid range, skipped buckets near the end, or extra empty pages.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit c646fcd. Configure here.

Comment thread src/polymarket/_internal/perps_session.py

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4073de5. Configure here.

self._scheduler.reset()
if emit_resync:
self._sequences.clear()
self._push(PerpsResyncEvent(reason="reconnect"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale order buffer after reconnect

Medium Severity

After a WebSocket reconnect, _recent_orders is left intact while sequence state is cleared. place_order can then resolve from a pre-reconnect buffered update for the same order_id, returning an outdated order snapshot instead of waiting for the new placement’s orders-channel event.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4073de5. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant