Skip to main content

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=True to HLClient and create_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/rpc endpoint. 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 calls approve(max) automatically on the first swap for each token/router pair. No permit2 in V1.
  • Single-hop swaps only: exactInputSingle for 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dex_exec-0.1.0.tar.gz (36.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dex_exec-0.1.0-py3-none-any.whl (29.2 kB view details)

Uploaded Python 3

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

Hashes for dex_exec-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d9e398345c5282893c39bceba565417aea0756d5d4fb60745f93819acec749c0
MD5 ce5ec3181dc672909f69a15d907993f6
BLAKE2b-256 035698b19f03f44f39414dcc9289dca30711bde07f42a2b9d9a4bd3efcbe1a36

See more details on using hashes here.

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

Hashes for dex_exec-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1b36c9935497219fe785f4fb47e13f8eed291602715ad6a28c17e306a2e7b611
MD5 82c5d9448399fea0ea6fdded93662fb6
BLAKE2b-256 0974df5e6fcdd89fec42593ea9215154f230de41ed00626cda997fec46dfc05f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page