Asynchronous Python SDK for the RISEx perpetuals DEX (trading, market data, EIP-712 signing, WebSocket feeds).
Project description
RISEx Python SDK 🚀
The RISEx Python SDK is an asynchronous library for the RISEx perpetuals DEX. It is aimed at trading bots, market makers, and analytics, and handles the awkward parts for you: EIP-712 signing, session-signer registration, and the exact request/payload formats the exchange expects.
Note: RISEx settles actions on-chain — each signed action (order, cancel, leverage, TPSL) is a blockchain transaction whose response includes a receipt and block number. It is fast and supports concurrent submission, but it is not a sub-millisecond CEX matching engine; size your latency expectations accordingly.
✨ Features
- 🎯 One-call trading:
open_long("BTC", 100, leverage=3, tp=..., sl=...)— USD sizing, symbol markets, TP/SL attached in the same call. - 🔁 Built-in retry: transient timeouts, disconnects, 5xx and 429s are retried with backoff; signed orders are never blindly resent.
- ⚡ Asynchronous: built on
aiohttp+asyncio; signed actions accept a pre-fetchednonceso you can submit concurrently (see Concurrency). - 🔐 EIP-712 signing built in: builds, hashes and signs every action locally with
eth-account/eth-abi— payloads are verified byte-for-byte against the live API. - 🔑 Automatic session signer:
create()registers a throwaway session key so your main private key never signs individual orders. - 🛡️ Typed models: market data and account reads are
pydanticmodels with IDE autocompletion. (State-changing calls return the raw responsedictfrom the exchange.) - 📊 Full coverage: market data, account reads, orders, leverage/margin, and the complete TPSL lifecycle — all verified against mainnet (
scripts/live_smoke_test.py).
Full technical reference — every method, wire format, retry rule and EIP-712
structure — lives in docs/.
📦 Installation
pip install risex
Or from source:
git clone https://github.com/Dmkls/risex.git
cd risex
pip install -e .
✅ Verify it works (live smoke test)
A self-contained script exercises every public method against the real
mainnet API and prints a PASS/FAIL line per step plus a summary (exit code 0
if everything works):
# Safe: read-only endpoints only, no key needed
python scripts/live_smoke_test.py --read-only
# Full check: needs a funded test wallet. Opens AND closes a real position,
# and runs the full TPSL lifecycle. Always cleans up after itself.
export RISEX_PRIVATE_KEY=0x...
python scripts/live_smoke_test.py
⚠️ The full run places real orders and opens/closes a real position (tiny size, ~1.5x the market minimum). Use a dedicated test wallet with a small balance — it costs a little in fees/spread.
🚀 Quickstart
RISEx requires wallets to register a "signer" to place orders without signing metamask transactions every time. The SDK abstracts this entirely.
import asyncio
import os
from risex import RiseXClient
async def main():
# Never hard-code a key. Read it from the environment (or a .env you load).
private_key = os.environ["RISEX_PRIVATE_KEY"]
# create() registers a temporary session signer for you automatically.
client = await RiseXClient.create(private_key)
print(f"Connected to RISEx as {client.account_address}")
# Fetch your cross-margin balance (returns a float, in USDC)
balance = await client.info.get_cross_margin_balance(client.account_address)
print(f"Current Margin Balance: {balance} USDC")
await client.close()
if __name__ == "__main__":
asyncio.run(main())
Tip: both clients are async context managers, so you can let
async withclose the HTTP session for you:async with await RiseXClient.create(private_key) as client: balance = await client.info.get_cross_margin_balance(client.account_address)
🎯 One-call trading (high-level API)
The simplest way to trade. Everything is sized in USD, markets are addressed by symbol, and entry + leverage + TP/SL happen in a single call:
async with await RiseXClient.create(private_key) as client:
# Open a $100 LONG on BTC at 3x, with take-profit and stop-loss attached:
await client.open_long("BTC", 100, leverage=3, tp=95_000, sl=55_000)
# Or a short (tp/sl/leverage are all optional):
await client.open_short("ETH", 50)
# Read your own account without passing an address around:
balance = await client.get_balance() # cross-margin USDC
positions = await client.get_positions() # all open positions
pos = await client.get_position("BTC") # one market (None if flat)
orders = await client.get_open_orders() # resting orders
trades = await client.get_trade_history() # recent fills
# Flatten one market — or everything:
await client.close_position("BTC")
await client.close_all_positions()
tp/sl are USD trigger prices, placed as TPSL trigger orders for the same
size right after the entry (TPSL needs the one-time
approve_permit_single_budget() — see Authentication below). If the entry
succeeds but the TP/SL placement fails, the raised error says so explicitly, so
you never silently lose protection.
🛡️ Reliability: built-in retries, timeouts & proxies
The HTTP layer retries transient failures automatically, so one flaky request doesn't kill your bot:
- 429 rate limits — retried for every request, honoring
Retry-After. - Timeouts, disconnects, 5xx — retried with exponential backoff + jitter for all reads (GET) and for idempotent setup calls (signer registration, terms).
- Connect failures (DNS, refused, proxy down) — retried for everything: the request never reached the server, so a resend can't double-execute.
- Order placement is never blindly resent: if a signed action times out, the
server may still have processed it, so the SDK raises a clear
RiseXConnectionErrortelling you to check open orders/positions first.
Defaults: 4 retries, 10s per-attempt timeout — both tunable on every factory:
client = await RiseXClient.create(pk, timeout_ms=15_000, max_retries=6)
info = await RiseXInfoClient.create(max_retries=2)
Proxies. Pass proxy="http://user:pass@host:port" to any factory, or just
set the standard HTTPS_PROXY environment variable (the SDK honors it). The
API sits behind Cloudflare, which occasionally tarpits datacenter/flagged IPs
for non-browser clients — if you see persistent timeouts while the website
works, route the SDK through a clean proxy.
🔑 Authentication: main key vs. session signer ("API key")
RISEx never signs individual orders with your main wallet. Instead your main
wallet registers a session signer — a throwaway keypair (think "API key") —
and every order/cancel/leverage/TPSL action is signed by that signer. The main
private key is only needed for one-time setup: registering the signer and
approve_permit_single_budget.
So there are two ways to authenticate:
1. Main private key — RiseXClient.create(private_key). Generates and
registers a session signer for you automatically. Use this for first-time setup,
or if you're fine keeping the main key on the trading machine.
2. Address + session-signer key (no main key) —
RiseXClient.from_signer(account_address, signer_key). The bot authenticates
with just your public address and the signer key, so the main private
key never touches the trading machine. This is the recommended setup for bots.
# --- one-time, on a trusted machine: register a signer and save it as your API key ---
client = await RiseXClient.create(main_private_key)
api_key = client.signer_key # <-- save this (it's the session signer's private key)
await client.approve_permit_single_budget() # one-time, enables TPSL triggers
await client.close()
# --- afterwards, on the bot: address + API key only, NO main key ---
async with await RiseXClient.from_signer(account_address, api_key) as client:
await client.market_buy_usd("BTC", 100) # signed by the session key
The signer key can trade but is registered with an expiry (≈30 days), after which you re-register with
create(main_key).approve_permit_single_budgetand signer registration are unavailable on afrom_signerclient (they need the main key) and raise a clearRiseXError. Treat the signer key as a secret.
Managing signers. List the signers registered to an account and revoke ones you no longer use:
for s in await info.get_signers(account): # signer, label, expiration, status
print(s.signer, s.status)
await client.revoke_signer(old_signer_address) # needs the main key; status -> "Revoked"
📐 Units: steps & ticks
Order endpoints do not take USD. Size is an integer count of steps and price is an integer count of ticks, where each market defines the increment:
asset_size = size_steps * market.config.step_size
price_usd = price_ticks * market.config.step_price
💡 Prefer the USD helpers. If you just want to trade in dollars, skip this whole section and use
market_buy_usd/limit_sell_usdetc. (see Placing Orders) — they do the conversion for you. The manual path below is for when you need exact step/tick control.
The lowest-level helpers convert explicitly. You no longer have to fetch and
filter the market list yourself — pass a market id or symbol to
info.get_market(...):
from risex import RiseXInfoClient, usd_to_steps, price_to_ticks, steps_to_size, ticks_to_price
info = await RiseXInfoClient.create()
market = await info.get_market("BTC") # by symbol; or get_market(1) by id
size_steps = usd_to_steps(100, market) # $100 of notional -> steps (floored)
price_ticks = price_to_ticks(85_000, market) # $85,000 -> ticks (rounded)
# ...and back the other way:
size_in_btc = steps_to_size(size_steps, market)
price_in_usd = ticks_to_price(price_ticks, market)
On the trading client there are one-call shortcuts that resolve the market for
you: await client.to_steps("BTC", 100) and await client.to_ticks("BTC", 85_000).
⚠️
size_steps/price_ticksreturned insideOpenOrderare the backend's internal encoded form — they do not equal the values you submitted and won't round-trip through these helpers. Use them only as opaque fields; read real sizes/prices from positions, fills, or the orderbook.
📖 Public Data: RiseXInfoClient
The RiseXInfoClient handles all public API endpoints. It does not require a private key.
Initialization
from risex import RiseXInfoClient
info = await RiseXInfoClient.create()
🔢 Numeric fields are
Decimal, not strings. Prices, sizes and amounts across the typed models — market (last_price,mark_price,step_size,step_price,max_leverage,min_order_size), position (size,quote_amount), orderbook level (price,quantity) and trade (price,size,fee,realized_pnl,avg_price,leverage) — are parsed asDecimal. You can compare and compute on them directly (pos.size > 0,market.mark_price * 2,f"{lvl.price:.2f}") with no casting. (Identifiers/timestamps liketrade.timestay strings.)
1. Market Data
get_markets()
Returns a list of all available trading pairs (markets) on RISEx. Results are
cached briefly (markets_cache_ttl, default 5s) so repeated lookups and USD
sizing don't hit the network every call; pass refresh=True to force a refetch.
markets = await info.get_markets()
for market in markets:
print(f"[{market.market_id}] {market.display_name} - Mark Price: {market.mark_price}")
get_market(market)
Resolve a single market by numeric id or by symbol (case-insensitive,
matched against the display name / base token). Raises RiseXValidationError
listing the available symbols if nothing matches.
btc = await info.get_market("BTC") # same as get_market(1) or get_market("BTC-PERP")
print(btc.market_id, btc.config.step_size)
get_orderbook(market_id: int, limit: int = 20)
Fetches the current orderbook snapshot (limit price levels per side).
ob = await info.get_orderbook(market_id=1, limit=10) # BTC/USDC
print(f"Best Bid: {ob.bids[0].price}, Best Ask: {ob.asks[0].price}")
# each level also carries .order_count; the snapshot has .total_bids / .total_asks
2. Account Information
get_cross_margin_balance(account_address: str)
Get the available USDC margin balance for an account.
balance = await info.get_cross_margin_balance("0xYourAddress")
get_position(account_address: str, market_id: int)
Get the open position for an account in a market. Returns None if flat.
position = await info.get_position("0xYourAddress", market_id=1)
if position and position.size != 0: # size is a Decimal — no float() cast needed
print(f"Size: {position.size}, Side: {position.side}") # side: 0 = long, 1 = short
else:
print("No open position")
get_all_positions(account_address: str)
Get all open positions across all markets for a specific account. Each Position
carries its market_id and is normalized to the same human form as
get_position (decimal size/quote_amount, int side). The backend reports
this endpoint in on-chain 18-decimal fixed-point with a string side; the SDK
converts it for you. Extra per-position fields (e.g. leverage,
avg_entry_price) are scaled and available via position.model_extra.
for p in await info.get_all_positions("0xYourAddress"):
side = "long" if p.side == 0 else "short"
print(f"market {p.market_id}: {side} {p.size} @ {p.model_extra.get('avg_entry_price')}")
get_open_orders(account_address: str)
Retrieve all currently resting limit orders for an account.
orders = await info.get_open_orders("0xYourAddress")
for order in orders:
# Use order_id (hex) to cancel; size_steps/price_ticks here are encoded — see the units note above.
print(f"Order {order.order_id} (resting_order_id={order.resting_order_id})")
get_trade_history(account_address: str, limit: int = 50, market_id: int | None = None)
Get the account's most recent fills (optionally filtered to one market). Each
TradeRecord carries side ("BUY"/"SELL"), price, size, fee, time
(unix nanoseconds, as a string), plus realized_pnl, avg_price,
liquidity_indicator ("MAKER"/"TAKER"), and leverage.
trades = await info.get_trade_history("0xYourAddress", limit=50)
for t in trades:
print(f"{t.side} {t.size} @ {t.price} pnl={t.realized_pnl} ({t.liquidity_indicator})")
⚔️ Trading: RiseXClient
The RiseXClient is required for any state-modifying actions (trading, changing leverage). It inherits all methods from the RiseXInfoClient under client.info.*.
Account Management
update_leverage(market_id: int, leverage: int)
Updates the account's leverage multiplier for a specific market.
# Set leverage to 10x on BTC/USDC
await client.update_leverage(market_id=1, leverage=10)
update_margin_mode(market_id: int, margin_mode: MarginMode | int)
Change the margin mode. MarginMode.CROSS == 0, MarginMode.ISOLATED == 1.
from risex import MarginMode
await client.update_margin_mode(market_id=1, margin_mode=MarginMode.ISOLATED)
Setting the mode that's already active reverts on-chain with
MarginModeUnchanged— catchRiseXAPIErrorif that's expected.
update_isolated_margin(market_id: int, amount: int)
Add (amount > 0) or remove (amount < 0) isolated margin for a market. Only
valid in isolated mode — in cross mode it reverts with …BalanceInCross.
await client.update_isolated_margin(market_id=1, amount=5_000_000) # add margin
deposit(amount: str)
Notify the backend of a deposit. The actual USDC transfer must be made on-chain to the router contract separately; this only registers it.
await client.deposit("100")
Placing Orders
🎯 Every method below accepts a market
idor asymbol("BTC", "ETH-PERP", …) wherever it takesmarket_id. Soclient.market_buy("BTC", …)andclient.update_leverage("ETH", 10)both work — no need to remember numeric ids.
USD helpers (the easy way) 💵
Trade in dollars and let the SDK convert to steps/ticks for you. Size is a USD
notional; price is a USD price.
# Market buy $100 of BTC
await client.market_buy_usd("BTC", 100)
# Rest a $250 limit sell of ETH at $4,000 (post-only maker)
await client.limit_sell_usd("ETH", 250, 4_000)
# Also: market_sell_usd(market, usd), limit_buy_usd(market, usd, price)
Lower-level: size_steps / price_ticks
If you need exact step/tick control, the methods below take integer
size_steps and price_ticks — see Units: steps & ticks
for converting from USD.
limit_buy / limit_sell
Place a limit order at a specific price.
# Place a limit buy order of size 0.01 at price 85000
res = await client.limit_buy(
market_id=1,
size_steps=1000, # Amount of tokens (scaled by market step size)
price_ticks=850000, # Price (scaled by market tick size)
post_only=True # Ensure maker fee
)
print(f"Order placed: {res.order_id}")
market_buy / market_sell
Execute an immediate market order.
# Market buy
res = await client.market_buy(market_id=1, size_steps=500)
Risk Management (TP/SL)
Side convention: the order side is expressed with
Side.LONG(maps to a BUY trigger) andSide.SHORT(maps to a SELL trigger). To reduce a LONG position you submit a SELL, i.e.Side.SHORT.
approve_permit_single_budget(budget=MAX_UINT96, expiry_seconds=1yr)
One-time setup so the exchange operator can submit your TPSL triggers on your behalf. Signed by your main wallet; returns the approval tx plus fresh access/refresh tokens. Call it once before placing trigger orders.
await client.approve_permit_single_budget()
place_stop_loss(market_id, side, size, stop_price, limit_price)
Place a Stop-Loss order to protect an open position.
from risex.enums import Side
# If you are LONG on BTC (bought at 90,000), place a SELL stop-loss at 88,000
await client.place_stop_loss(
market_id=1,
side=Side.SHORT, # SELL, to reduce a LONG
size="0.1",
stop_price="88000",
limit_price="87900"
)
place_take_profit(market_id, side, size, stop_price, limit_price)
Place a Take-Profit as a TPSL trigger order (taker fee when it fires).
# Take profit for a LONG BTC position at 95,000
await client.place_take_profit(
market_id=1,
side=Side.SHORT, # SELL, to reduce a LONG
size="0.1",
stop_price="95000",
limit_price="95100"
)
take_profit_limit(market_id, side, size_steps, price_ticks, post_only=True)
Take profit as a resting maker reduce_only limit order — lower fees than a
TPSL trigger. Prefer this when you can wait for the price to come to you.
# Maker take-profit on a LONG: rest a SELL reduce_only limit at the target
await client.take_profit_limit(
market_id=1,
side=Side.SHORT, # SELL, to reduce a LONG
size_steps=1000,
price_ticks=950000,
)
close_position(market_id)
Automatically calculates your current position size and places a market order in the opposite direction to flatten your exposure entirely.
# Close all open exposure on BTC/USDC
await client.close_position(market_id=1)
Order Cancellation
cancel_order(market_id, order_id, resting_order_id=None)
Cancel a specific resting order. Pass the hex order_id from place_order /
get_open_orders; the SDK looks up the numeric resting_order_id it needs to
sign (pass it explicitly to skip that extra lookup).
res = await client.limit_buy(market_id=1, size_steps=1000, price_ticks=850000)
await client.cancel_order(market_id=1, order_id=res.order_id)
cancel_all_orders(market_id)
Cancel all open orders on a specific market in a single transaction. A market must be specified (the backend has no all-markets cancel — call it per market).
# Cancel all orders on BTC/USDC
await client.cancel_all_orders(market_id=1)
📡 Real-time data: WebSocket
RiseXWebSocketClient streams live data over wss://api.rise.trade/ws/ (built on
aiohttp — no extra dependency). It auto-reconnects and replays your
subscriptions, and delivers messages either by async for iteration or
per-channel callbacks.
| Channel | Auth | Subscribe with | Payload |
|---|---|---|---|
system |
no | subscribe_system() |
maintenance status |
orderbook |
no | subscribe_orderbook([1,2]) |
bids/asks per market_id |
trades |
no | subscribe_trades([1]) |
fills on the market |
oracle |
no | subscribe_oracle() |
index/mark prices (all markets, 1e18-scaled) |
positions |
no¹ | subscribe_positions([addr],[1]) |
position updates for the address |
orders |
no¹ | subscribe_orders([addr],[1]) |
order lifecycle for the address |
account, fills |
yes² | subscribe_account(), subscribe_fills() |
balance/equity / your fills |
¹ positions/orders are filtered by maker address and need no login (same as the
REST by-address reads). ² account/fills require an auth_v2 sign-in — one call,
see below.
from risex import RiseXWebSocketClient
async with RiseXWebSocketClient() as ws:
await ws.subscribe_orderbook([1]) # BTC/USDC book
await ws.subscribe_trades([1])
await ws.subscribe_positions([my_address], [1])
async for msg in ws: # msg is a WSMessage
if msg.is_ack or msg.is_error:
continue
if msg.channel == "orderbook":
best_ask = msg.data["asks"][0]
print(f"[{msg.market_id}] best ask {best_ask['price']} x {best_ask['quantity']}")
elif msg.channel == "positions":
print("position update:", msg.data)
Each message is a WSMessage with channel, type ("snapshot"/"update"/…),
market_id, block_number, and data (the channel payload — a dict or list).
Prefer callbacks for a long-running bot:
ws = RiseXWebSocketClient()
ws.on("trades", lambda m: print("trade", m.data))
ws.on("orders", on_my_order) # sync or async handlers
await ws.connect()
await ws.subscribe_trades([1])
await ws.subscribe_orders([my_address], [1])
await ws.run_forever()
Authenticating account / fills
These two channels need an auth_v2 sign-in: the SDK fetches a single-use nonce,
signs it (EIP-712 RegisterV2) with your session signer, and re-authenticates
automatically on reconnect. With a RiseXClient it's one call:
ws = await client.connect_websocket() # already authenticated with the session signer
await ws.subscribe_fills()
await ws.subscribe_account([client.account_address])
async for msg in ws:
if msg.channel == "fills":
print("fill:", msg.data) # side, price, size, fee, liquidity_indicator, ...
Or authenticate a standalone RiseXWebSocketClient yourself with an info client
and a signer key (your "API key"):
info = await RiseXInfoClient.create()
ws = await RiseXWebSocketClient().connect()
await ws.authenticate(account_address, signer_key, info)
await ws.subscribe_fills()
info.get_signers(account) lists the registered signers (label, expiry, status)
if you need to pick or check one. Without authenticate(),
subscribe_account/subscribe_fills return an authentication required error (a
WSMessage with is_error); every other channel works without it.
🔀 Concurrency & nonces
By default each signed action (place_order, cancel_order, cancel_all_orders)
fetches the account nonce first, which adds a round-trip. To submit several
actions concurrently — or to skip that fetch — pass a pre-fetched NonceState
via the nonce argument:
nonce = await client.get_nonce_state()
await asyncio.gather(
client.place_order(market_id=1, side=Side.LONG, size_steps=100, nonce=nonce),
client.place_order(market_id=2, side=Side.SHORT, size_steps=50, nonce=nonce),
)
Each signed action consumes a nonce slot on-chain, so when firing many at once you are responsible for advancing/spacing nonces to avoid collisions.
Logging
The SDK logs requests (and, at DEBUG, payloads) under the risex.http and
risex.client loggers — they're silent unless you enable them:
import logging
logging.getLogger("risex").setLevel(logging.DEBUG)
🚨 Error Handling
All API errors and smart contract reverts throw a RiseXAPIError exception, which includes the HTTP status code, request path, and decoded error message from the EVM.
from risex.exceptions import RiseXAPIError
try:
await client.market_buy(market_id=1, size_steps=100000000)
except RiseXAPIError as e:
print(f"Failed! Status: {e.status}")
print(f"Message: {e.message}") # e.g. 'CancelOrder reverted: SignerNotAuthorized'
📄 License
This SDK is distributed under the MIT License. See LICENSE for more information.
Disclaimer: This SDK interacts with live decentralized financial infrastructure. High-frequency trading carries significant financial risk. The authors are not responsible for any financial losses or unexpected smart contract behavior resulting from the use of this software. Always test thoroughly using small amounts before deploying to production.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file risex-0.10.0.tar.gz.
File metadata
- Download URL: risex-0.10.0.tar.gz
- Upload date:
- Size: 86.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
61a6746d8481433a6648a4d9fc0f5cf7864f6de503105a97d5eb8b1b97772ac7
|
|
| MD5 |
46a0c294cd0ef8280e35647dbda4ca79
|
|
| BLAKE2b-256 |
41a6683b77223816ca3bb7c58c1003b2be55a91ac40727a31117ebae4b5a0263
|
File details
Details for the file risex-0.10.0-py3-none-any.whl.
File metadata
- Download URL: risex-0.10.0-py3-none-any.whl
- Upload date:
- Size: 45.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d307c509959f3d1586a83a58329ec00e0735f26d3af2b1dd2391a14594263ebc
|
|
| MD5 |
475ca8c2edabd14963d335f8bef83cdd
|
|
| BLAKE2b-256 |
d4e6686494a3178907280f36b7662e0f9e630c327a543703b841de97b10d7204
|