Minimal async execution library for Hyperliquid perps and Uniswap V3 / Sushiswap / Pancakeswap on Arbitrum One
Project description
dex-exec
Minimal async Python library for executing orders on Hyperliquid perps and Uniswap V3 / Sushiswap / Pancakeswap on Arbitrum One.
Why
The official hyperliquid-python-sdk is synchronous, untyped, and awkward to integrate into async applications. No clean Python library exists for V3 AMM execution either. This library fills both gaps with a small, well-typed async API.
Scope: execution only. No signal logic, no strategy framework, no risk management. If you need real-time market data to power your signals, see mackinac — it provides a WebSocket feed with Hawkes process metrics, order book imbalance, and cross-venue arb signals that pairs naturally with this library (see examples/07_mackinac_feed.py).
Install
pip install dex-exec
Requires Python 3.10+.
Hyperliquid — getting started
Step 1 — Create an HL account and fund it
Go to app.hyperliquid.xyz and create an account. Your master wallet is the MetaMask address you connect with. Fund it with USDC on Arbitrum One (bridge via Arbitrum Bridge), then deposit into HL from the app.
Step 2 — Create an agent key
HL uses an agent wallet to sign orders on your behalf. Your master key never touches the API after setup.
import asyncio
from dexec import create_agent
async def setup():
creds = await create_agent(
master_private_key = "0x...", # your master key, used once
agent_name = "my-bot",
testnet = False,
)
print(f"Agent address: {creds.address}")
print(f"Agent private key: {creds.private_key}")
# Store creds.private_key securely — use it for all HLClient calls
asyncio.run(setup())
See examples/04_hl_agent_setup.py for the full flow. After this step, the master key can go back offline.
Step 3 — Place orders
import asyncio
from dexec import HLClient
AGENT_KEY = "0x..." # from create_agent above
MASTER_ADDRESS = "0x..." # your master wallet address
async def main():
async with HLClient(AGENT_KEY, MASTER_ADDRESS) as hl:
bal = await hl.get_balance()
print(f"Account value: ${bal.account_value:,.2f}")
result = await hl.place_order("ETH", "buy", size=0.01, price=2000.0)
print(f"Order: {result.status} cloid={result.cloid}")
asyncio.run(main())
Testnet: pass
testnet=TruetoHLClientandcreate_agent. Get a testnet account at app.hyperliquid-testnet.xyz — faucet USDC is available on the testnet site.
HL order constraints
- Minimum notional: $10 per order (size × price).
- Price tick size: varies by asset. Use the current mark price as a reference — HL rejects prices more than 80% away from it.
- IOC at aggressive price: for market-like fills, use
order_type='ioc'with a price slightly above (buy) or below (sell) the current mark.
AMM — getting started
Step 1 — Fund your Arbitrum wallet
You need:
- ETH on Arbitrum for gas (~$0.01–$0.10 per transaction).
- The token you want to swap on Arbitrum One. For WETH/USDC pairs, you need WETH — not ETH. Wrap it first (see below).
Bridge ETH or tokens from Ethereum mainnet via Arbitrum Bridge or buy directly on Arbitrum via a centralised exchange that supports Arbitrum withdrawals.
Step 2 — Wrap ETH → WETH if needed
Most pairs trade against WETH, not native ETH.
import asyncio
from dexec import AMMClient
async def main():
async with AMMClient(private_key="0x...") as amm:
tx = await amm.wrap_eth(0.05) # wrap 0.05 ETH → WETH
print(f"Wrapped: {tx}")
asyncio.run(main())
Step 3 — Pick a venue and fee tier
Each venue (Uniswap V3, Sushiswap, Pancakeswap) runs pools at different fee tiers. The most liquid pools for common pairs on Arbitrum One:
| Pair | Venue | Fee tier | Notes |
|---|---|---|---|
| WETH/USDC | uni_v3 |
500 (0.05%) | Deepest WETH/USDC pool |
| WETH/USDC | uni_v3 |
3000 (0.3%) | Higher fee, less used |
| WETH/USDC | sushi |
3000 (0.3%) | Sushi has no 0.05% WETH/USDC pool |
| WETH/USDT | uni_v3 |
500 (0.05%) | |
| USDC/USDT | pancake |
100 (0.01%) | Stablecoin pair, tightest fee |
| WBTC/WETH | uni_v3 |
3000 (0.3%) |
You can also check pool depth live on mackinac — the AMM widget shows per-fee-tier liquidity and spread in real time.
If you're unsure which pool has liquidity for a non-standard pair, try get_quote() with each fee tier — it will raise ValueError if the pool doesn't exist.
Step 4 — Quote then swap
Always quote before swapping to compute min_amount_out:
import asyncio
from dexec import AMMClient
async def main():
async with AMMClient(private_key="0x...") as amm:
# Step 1: get a quote (read-only, no gas)
quote = await amm.get_quote("WETH", "USDC", amount_in=0.1,
venue="uni_v3", fee_tier=500)
print(f"Quote: {quote.amount_out:.2f} USDC at ${quote.amount_out/0.1:,.2f}/WETH")
# Step 2: execute with 0.5% slippage tolerance
min_out = quote.amount_out * 0.995
result = await amm.swap("WETH", "USDC", amount_in=0.1,
venue="uni_v3", fee_tier=500,
min_amount_out=min_out)
if result.success:
print(f"Swap tx: {result.tx_hash}")
else:
print(f"Failed: {result.error}")
asyncio.run(main())
Token approval (ERC-20 approve) is handled automatically on the first swap for each token/venue pair.
HLClient reference
async with HLClient(private_key, address, testnet=False) as hl:
# Orders
await hl.place_order(symbol, side, size, price, order_type='gtc', reduce_only=False, cloid=None)
await hl.cancel_order(symbol, cloid)
await hl.cancel_all(symbol=None) # returns count cancelled
# Account
await hl.get_balance() # → AccountBalance
await hl.get_positions() # → list[Position]
await hl.get_open_orders() # → list[Order]
await hl.get_fills() # → list[Fill]
# Streams
async with hl.fills_stream() as fills:
async for fill in fills: ...
async with hl.order_updates_stream() as updates:
async for update in updates: ...
# On-chain
await hl.deposit_usdc(amount_usd, arb_private_key) # → tx_hash
await hl.withdraw_usdc(amount_usd) # → bool
order_type options: 'gtc' (resting limit), 'ioc' (fill or cancel), 'alo' (add liquidity only).
Agent key functions
from dexec import create_agent, approve_agent
# Generate fresh keypair + approve on HL (one-time setup)
creds = await create_agent(master_private_key, agent_name="my-bot", testnet=False)
# → AgentCredentials(address, private_key)
# Approve an existing address as an agent (e.g. key generated externally)
ok = await approve_agent(master_private_key, agent_address, testnet=False)
AMMClient reference
async with AMMClient(private_key=None, rpc_url=None) as amm:
# Wrap ETH → WETH
await amm.wrap_eth(amount_eth) # → tx_hash
# Quote (read-only, no gas)
quote = await amm.get_quote(token_in, token_out, amount_in, venue, fee_tier)
# Swap (auto-approves if needed)
result = await amm.swap(token_in, token_out, amount_in, venue, fee_tier,
min_amount_out, deadline=60)
# Manual approval
await amm.approve_token(token, spender) # → tx_hash
venue options: 'uni_v3', 'sushi', 'pancake'.
Token symbols supported out of the box: WETH, USDC, USDC.e, USDT, WBTC, ARB, LINK, DAI, GMX.
Pass a raw 0x address for any other token (decimals assumed 18 if not in the registry).
Typed return values
@dataclass class AccountBalance:
account_value: float; margin_used: float
free_collateral: float; withdrawable: float
@dataclass class Position:
symbol: str; side: Literal['long','short']; size: float
entry_price: float; unrealized_pnl: float
margin_used: float; leverage: float; liquidation_price: float | None
@dataclass class OrderResult:
success: bool; cloid: str
status: Literal['resting','filled','error']
oid: int | None; error: str | None
@dataclass class SwapQuote:
token_in: str; token_out: str
amount_in: float; amount_out: float
price_impact_pct: float; venue: str; fee_tier: int
@dataclass class SwapResult:
success: bool; tx_hash: str | None
amount_in: float; amount_out: float
venue: str; error: str | None
Examples
| File | What it shows |
|---|---|
examples/01_hl_place_cancel.py |
HL order round-trip on testnet |
examples/02_hl_positions.py |
Poll positions + PnL |
examples/03_hl_fill_stream.py |
Async fill event loop |
examples/04_hl_agent_setup.py |
create_agent() full flow |
examples/05_hl_deposit.py |
USDC deposit to HL |
examples/06_amm_swap.py |
Wrap ETH, quote, and swap on Uniswap V3 |
examples/07_mackinac_feed.py |
mackinac WS signal → HL + AMM basis trade |
Notes
- Arbitrum RPC: defaults to the public
arb1.arbitrum.io/rpcendpoint. For production use Alchemy or Infura — the public endpoint is rate-limited and can drop connections under load. - ERC-20 approvals:
swap()checks allowance and callsapprove(max)automatically on the first swap for each token/router pair. No permit2 in V1. - Single-hop swaps only:
exactInputSinglefor V1 simplicity. Multi-hop routing (e.g. USDT → WETH → ARB) is not yet supported. - Uniswap V4: stubbed, not implemented. V4 uses a materially different router interface.
License
MIT
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 dex_exec-0.1.0.tar.gz.
File metadata
- Download URL: dex_exec-0.1.0.tar.gz
- Upload date:
- Size: 36.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d9e398345c5282893c39bceba565417aea0756d5d4fb60745f93819acec749c0
|
|
| MD5 |
ce5ec3181dc672909f69a15d907993f6
|
|
| BLAKE2b-256 |
035698b19f03f44f39414dcc9289dca30711bde07f42a2b9d9a4bd3efcbe1a36
|
File details
Details for the file dex_exec-0.1.0-py3-none-any.whl.
File metadata
- Download URL: dex_exec-0.1.0-py3-none-any.whl
- Upload date:
- Size: 29.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.9
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1b36c9935497219fe785f4fb47e13f8eed291602715ad6a28c17e306a2e7b611
|
|
| MD5 |
82c5d9448399fea0ea6fdded93662fb6
|
|
| BLAKE2b-256 |
0974df5e6fcdd89fec42593ea9215154f230de41ed00626cda997fec46dfc05f
|