Skip to content

mackinac/dex-exec

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

About

Async Python execution library for Hyperliquid perps + Uniswap V3/Sushi/Pancake on Arbitrum

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors