---
title: "How to Build a Hyperliquid Trading Bot in Python"
description: "Build a Hyperliquid trading bot in Python: tested market data, a cost-aware signal test, a dry-run loop with risk limits, reversals and a kill switch."
canonical_url: "https://0xarchive.io/resources/market-data-guides/hyperliquid-trading-bot"
markdown_url: "https://0xarchive.io/resources/market-data-guides/hyperliquid-trading-bot.md"
route: "/resources/market-data-guides/hyperliquid-trading-bot"
robots: "index, follow, max-image-preview:large"
generated_from: "prerendered_html"
---

# How to Build a Hyperliquid Trading Bot in Python

Build a Hyperliquid trading bot in Python: data, a cost-aware test, a dry-run live loop, risk limits and a kill switch. Tested 8 and 9 Nov 2026.

By

[0xArchive Team](https://0xarchive.io/authors/0xarchive-team)

November 9, 2026

20 min read

- [Backtesting](https://0xarchive.io/resources?topic=backtesting)
- [API and alerts](https://0xarchive.io/resources?topic=api-and-alerts)
- [Hyperliquid](https://0xarchive.io/resources?topic=hyperliquid)

## Key findings

1. [Step 1: get enough history to test on](https://0xarchive.io/resources/market-data-guides/hyperliquid-trading-bot#article-section-step-1-get-enough-history-to-test-on) 14,293 Hourly BTC candles from 22 Mar 2025 in 2 requests. Hyperliquid's API stops at 5,000.
2. [Step 2: price your fills](https://0xarchive.io/resources/market-data-guides/hyperliquid-trading-bot#article-section-step-2-price-your-fills) 0.2 bps Median slippage for a $250,000 BTC market order. Fees cost far more.
3. [Step 3: test the signal before it trades](https://0xarchive.io/resources/market-data-guides/hyperliquid-trading-bot#article-section-step-3-test-the-signal-before-it-trades) -36.0% EMA 24/96 long-short on BTC over 19 months after fees, slippage and funding. Buy and hold: -13.8%.
4. [Step 4: run the live loop in dry-run mode](https://0xarchive.io/resources/market-data-guides/hyperliquid-trading-bot#article-section-step-4-run-the-live-loop-in-dry-run-mode) $499.99 Largest order the dry-run loop would send under a $500 cap.

A trading bot has three parts: a signal computed from data, a test that shows whether the signal ever worked, and a loop that turns signals into orders without hurting you when something breaks. This guide builds all three in Python for BTC on Hyperliquid, an onchain crypto exchange, and adds the risk limits and kill switch that go around them. The signal is deliberately simple, and its first result is a loss. Every request ran on 8 and 9 November 2026, and every output is what came back.

**No orders were placed for this guide.** The live loop runs in dry-run mode, and the order code is shown for reading only.

## Short answer

- **The build:** install `hyperliquid-python-sdk` , approve a separate API wallet for your account, compute a signal from closed candles, test it with fees and funding on history, then send IOC orders behind position, loss and data-age checks, and read back what filled.
- **Data:** Hyperliquid's own `candleSnapshot` returns the latest 5,000 candles, 207 days at one hour. That is enough to run a live signal. 0xArchive returned 19 months of hourly BTC candles in 2 requests, which is what testing the signal takes.
- **Costs:** fees dominate. Hyperliquid's base taker fee is 4.5 basis points. A $250,000 BTC market order cost a median 0.2 basis points of slippage in the 116 of 120 stored order books deep enough to fill it.
- **The test:** a 24/96-hour moving-average crossover lost 36.0% after costs over 19 months, against 13.8% for buy and hold.
- **The live loop:** each pass reads Hyperliquid's own API, checks limits before building an order, closes with reduce-only before it reverses, and stops if a kill file exists. Signing and flattening are shown for reading, not run. 0xArchive does not place orders.

## Why build a bot on Hyperliquid

Hyperliquid has a public API for both market data and trading. Anyone with a funded account can trade programmatically from an approved API wallet, with no broker in between. That makes a bot cheap to start, and it makes the discipline around it the whole job: a tested signal, size limits and a way to stop.

## What you need

- Python 3 with `requests` . For live trading, `pip install eth-account hyperliquid-python-sdk` , and set `HL_API_WALLET_KEY` and `HL_ACCOUNT_ADDRESS` in the environment. The first is the API wallet's private key. The second is your main account's address, where positions are read. If your bot already trades through ccxt, it supports Hyperliquid too; this guide uses Hyperliquid's official SDK. The snippets are short prototypes with no retry logic.
- A 0xArchive API key in `OXARCHIVE_API_KEY` for history. Hyperliquid's info endpoint needs no key. The Free plan covers the last 30 days. Testing on 19 months needs a paid plan, listed on [pricing](https://0xarchive.io/pricing) .

## Step 1: get enough history to test on

Ask Hyperliquid's `candleSnapshot` for every hourly BTC candle since 22 March 2025, then ask 0xArchive for the same span:

```
import os, requests

START, END = 1742644800000, 1794096000000      # 22 Mar 2025 12:00 UTC to 8 Nov 2026 00:00 UTC
# Hyperliquid's own API: ask for all the 1h candles in that span
native = requests.post("https://api.hyperliquid.xyz/info", json={
    "type": "candleSnapshot", "req": {"coin": "BTC", "interval": "1h", "startTime": START, "endTime": END}}).json()
print("native rows", len(native), "first candle open time (ms)", native[0]["t"])

# 0xArchive: same span, same interval
H = {"X-API-Key": os.environ["OXARCHIVE_API_KEY"]}
rows, cursor = [], None
while True:
    p = {"interval": "1h", "start": START, "end": END, "limit": 10000}
    if cursor: p["cursor"] = cursor
    r = requests.get("https://api.0xarchive.io/v1/hyperliquid/candles/BTC", headers=H, params=p).json()
    rows += r["data"]
    if not r["meta"].get("has_more"): break
    cursor = r["meta"]["next_cursor"]
print("0xArchive rows", len(rows), "first", rows[0]["timestamp"], "requests", 1 + len(rows) // 10000)
```

```
native rows 4976 first candle open time (ms) 1776186000000
0xArchive rows 14293 first 2025-03-22T12:00:00Z requests 2
```

Hyperliquid's docs say only the latest 5,000 candles are available, and the call agrees: 4,976 hourly candles, back to mid-April 2026. 0xArchive returned all 14,293 from 22 March 2025 in two requests. For a live signal the native window is plenty. For a test that spans rising and falling markets it is not. For a full treatment of history for testing, see [Hyperliquid historical data for backtesting](https://0xarchive.io/resources/market-data-guides/hyperliquid-historical-data-api-for-backtesting).

## Step 2: price your fills

A test that fills at the close price is fiction. This script measures what a market order costs: it walks orders of three sizes through 120 stored 20-level BTC books spread from March 2025 to November 2026, then reads the taker fee from ten minutes of stored fills:

```
import os, statistics as st, datetime as dt, requests

H = {"X-API-Key": os.environ["OXARCHIVE_API_KEY"]}
B = "https://api.0xarchive.io/v1/hyperliquid"
ms = lambda *a: int(dt.datetime(*a, tzinfo=dt.timezone.utc).timestamp() * 1000)

def walk(levels, usd):
    """Average price for a market order of `usd` through (price, size) levels; None if 20 levels run out."""
    left, cost, qty = usd, 0.0, 0.0
    for px, sz in levels:
        take = min(left, px * sz)
        cost, qty, left = cost + take, qty + take / px, left - take
        if left <= 1e-9:
            return cost / qty
    return None

t0, t1, N = ms(2025, 3, 23), ms(2026, 11, 7), 120   # 120 books, evenly spaced
slip = {50_000: [], 250_000: [], 1_000_000: []}
for k in range(N):
    d = requests.get(f"{B}/orderbook/BTC", headers=H, timeout=30,
                     params={"timestamp": t0 + (t1 - t0) * k // (N - 1), "depth": 20}).json()["data"]
    bids = [(float(l["px"]), float(l["sz"])) for l in d["bids"]]
    asks = [(float(l["px"]), float(l["sz"])) for l in d["asks"]]
    mid = (bids[0][0] + asks[0][0]) / 2
    for usd in slip:
        a, b = walk(asks, usd), walk(bids, usd)
        if a and b:
            slip[usd].append(((a - mid) + (mid - b)) / 2 / mid * 1e4)   # bps from the mid, buy and sell averaged
for usd, v in slip.items():
    print(f"${usd:>9,} market order: priced in {len(v)} of {N} books, median {st.median(v):.2f} bps, 90th pct {sorted(v)[int(0.9 * len(v))]:.2f}")

s = ms(2026, 11, 6, 12)
fills = requests.get(f"{B}/trades/BTC", headers=H, timeout=60,
                     params={"start": s, "end": s + 600_000, "limit": 50000}).json()["data"]
taker = [float(f["fee"]) / (float(f["price"]) * float(f["size"])) * 1e4 for f in fills if f["crossed"] and float(f["fee"]) > 0]
print(len(fills), "fill rows in 10 minutes; taker fee median", f"{st.median(taker):.2f} bps")
fees = requests.post("https://api.hyperliquid.xyz/info", timeout=30,
                     json={"type": "userFees", "user": fills[0]["user_address"]}).json()["feeSchedule"]
print("Hyperliquid base schedule: taker", fees["cross"], "maker", fees["add"])
```

```
$   50,000 market order: priced in 120 of 120 books, median 0.07 bps, 90th pct 0.47
$  250,000 market order: priced in 116 of 120 books, median 0.20 bps, 90th pct 0.67
$1,000,000 market order: priced in 104 of 120 books, median 0.54 bps, 90th pct 0.87
2798 fill rows in 10 minutes; taker fee median 4.32 bps
Hyperliquid base schedule: taker 0.00045 maker 0.00015
```

BTC is deep. A $250,000 market order cost a median 0.2 basis points against the mid. At $1,000,000 the 20 levels ran out in 16 of 120 books, so the figure is a floor. The fee is the bigger cost: taker fills paid a median 4.32 basis points, close to Hyperliquid's base rate of 4.5. Fees vary with your volume tier, referral and any builder fee, so read your own with `userFees`. The test below charges 4.7 basis points per unit of position changed: 4.5 of fee plus 0.2 of slippage. A flip from long to short changes two units and pays twice.

## Step 3: test the signal before it trades

The signal is a fast and a slow exponential moving average of the hourly close. Long when the fast one is above, short when it is below, at one times leverage. We fixed the setting, 24 hours against 96, before running anything. The loop decides on one bar's close and trades at the next bar's open, so it never uses a price it could not have had. It charges costs on every change and Hyperliquid's settled funding every hour, read from its own `fundingHistory`. 0xArchive's hourly funding rows sample the rate during the hour, so they are the wrong series for what a position paid.

```
import os, time, requests
from datetime import datetime

H = {"X-API-Key": os.environ["OXARCHIVE_API_KEY"]}
B = "https://api.0xarchive.io/v1/hyperliquid"
START, END = 1742644800000, 1794096000000          # 22 Mar 2025 12:00 UTC to 8 Nov 2026 00:00 UTC
COST = 4.7                                          # bps per unit of position change: 4.5 fee + 0.2 slippage (Step 2)

def pages(path, **params):
    while True:
        j = requests.get(f"{B}{path}", headers=H, params=params, timeout=60).json()
        yield from j["data"]
        if not j["meta"].get("has_more"):
            return
        params["cursor"] = j["meta"]["next_cursor"]

def settled_funding(coin, start, end):                # Hyperliquid's own settled hourly payments, 500 a request
    out, t = {}, start
    while t < end:
        rows = requests.post("https://api.hyperliquid.xyz/info", timeout=60,
                             json={"type": "fundingHistory", "coin": coin, "startTime": t, "endTime": end}).json()
        if not rows:
            break
        for r in rows:
            out[r["time"] // 3_600_000 * 3_600_000] = float(r["fundingRate"])   # keyed by settlement hour
        t = rows[-1]["time"] + 1
        time.sleep(0.3)
    return out

C = list(pages("/candles/BTC", interval="1h", start=START, end=END, limit=10000))
FUND = settled_funding("BTC", START, END + 3_600_000)
op, cl, ts = [c["open"] for c in C], [c["close"] for c in C], [c["timestamp"] for c in C]
settle = [int(datetime.fromisoformat(t.replace("Z", "+00:00")).timestamp() * 1000) + 3_600_000 for t in ts]  # end of each bar

def ema(x, span):
    a, out = 2 / (span + 1), [x[0]]
    for v in x[1:]:
        out.append(a * v + (1 - a) * out[-1])
    return out

def run(fast, slow, cost=COST, funding=True, lo=96, hi=len(C)):
    ef, es = ema(cl, fast), ema(cl, slow)
    eq, pos, changes, peak, dd, curve = 1.0, 0, 0, 1.0, 0.0, []
    for i in range(lo, hi):
        want = 1 if ef[i - 1] > es[i - 1] else -1      # decided on the previous close
        eq *= 1 + pos * (op[i] / cl[i - 1] - 1)        # old position across the gap to this open
        if want != pos:
            eq *= 1 - abs(want - pos) * cost / 1e4     # trade at this open
            changes, pos = changes + 1, want
        eq *= 1 + pos * (cl[i] / op[i] - 1)
        if funding:
            eq *= 1 - pos * FUND.get(settle[i], 0.0)   # settled at the end of the bar: longs pay positive funding
        curve.append(eq); peak = max(peak, eq); dd = max(dd, 1 - eq / peak)
    return eq - 1, changes, dd, curve

pct = lambda x: f"{x * 100:+.1f}%"
hold = lambda lo, hi: cl[hi - 1] / op[lo] - 1
r, n, dd, _ = run(24, 96)
missing = sum(1 for i in range(96, len(C)) if settle[i] not in FUND)
print(len(C), "candles,", len(FUND), "settled funding payments,", missing, "bars without one")
print("EMA 24/96 after costs:", pct(r), f"{n} changes, worst drawdown {dd:.1%}; buy and hold", pct(hold(96, len(C))))

grid = [(f, s) for f in (12, 24, 48) for s in (96, 192, 384)]
for f, s in grid:
    print(f"{f:>3}/{s:<3}  no costs {pct(run(f, s, 0, False)[0]):>7}  fee+funding {pct(run(f, s, 4.5)[0]):>7}  all costs {pct(run(f, s)[0]):>7}")

mid = 96 + (len(C) - 96) // 2                       # walk forward: choose on the first half, trade the second
best = max(grid, key=lambda g: run(*g, hi=mid)[0])
print("first half", ts[96][:10], "to", ts[mid - 1][:10], "| 24/96", pct(run(24, 96, hi=mid)[0]), "| buy and hold", pct(hold(96, mid)))
print("best on the first half:", f"{best[0]}/{best[1]}", pct(run(*best, hi=mid)[0]))
print("second half", ts[mid][:10], "to", ts[-1][:10], "| that setting", pct(run(*best, lo=mid)[0]),
      "| 24/96", pct(run(24, 96, lo=mid)[0]), "| buy and hold", pct(hold(mid, len(C))))
```

```
14293 candles, 14293 settled funding payments, 1 bars without one
EMA 24/96 after costs: -36.0% 192 changes, worst drawdown 46.3%; buy and hold -13.8%
 12/96   no costs  -43.3%  fee+funding  -56.1%  all costs  -56.6%
 12/192  no costs  -14.8%  fee+funding  -28.5%  all costs  -29.0%
 12/384  no costs  +37.3%  fee+funding  +22.7%  all costs  +22.2%
 24/96   no costs  -22.1%  fee+funding  -35.5%  all costs  -36.0%
 24/192  no costs  -17.8%  fee+funding  -28.2%  all costs  -28.6%
 24/384  no costs  +26.2%  fee+funding  +15.1%  all costs  +14.7%
 48/96   no costs  -39.2%  fee+funding  -48.3%  all costs  -48.6%
 48/192  no costs   -2.4%  fee+funding  -11.7%  all costs  -12.0%
 48/384  no costs  +51.2%  fee+funding  +40.2%  all costs  +39.8%
first half 2025-03-26 to 2026-01-16 | 24/96 -19.5% | buy and hold +8.8%
best on the first half: 48/384 -3.0%
second half 2026-01-16 to 2026-11-08 | that setting +44.0% | 24/96 -20.5% | buy and hold -20.8%
```

![Line chart of growth of one unit, March 2025 to November 2026. Buy and hold BTC, gray, peaks at 1.43 and ends at 0.86, down 13.8 percent. The EMA 24/96 strategy after costs, gold, peaks at 1.18 and ends at 0.64, down 36.0 percent, below buy and hold in 78 percent of hours.](https://api.0xarchive.io/cms-assets/2026/11/5ebc7e92-acf4-4142-8d05-305f6b1d31a4.webp)

EMA 24/96 long-short on BTC hourly candles from 0xArchive, after fees, slippage and funding. Past results, not a forecast.

The fixed setting lost 36.0% across 192 position changes, with a 46.3% worst drawdown, while BTC lost 13.8%. It lost in both halves: -19.5% while BTC rose 8.8%, then -20.5% while BTC fell 20.8%. Being able to go short did not protect it in a falling market.

All nine settings on the same bars, as fast/slow EMA lengths in hours:

| EMA | No costs | Fees, funding | All costs |
| --- | --- | --- | --- |
| 12/96 | -43.3% | -56.1% | -56.6% |
| 12/192 | -14.8% | -28.5% | -29.0% |
| 12/384 | +37.3% | +22.7% | +22.2% |
| 24/96 | -22.1% | -35.5% | -36.0% |
| 24/192 | -17.8% | -28.2% | -28.6% |
| 24/384 | +26.2% | +15.1% | +14.7% |
| 48/96 | -39.2% | -48.3% | -48.6% |
| 48/192 | -2.4% | -11.7% | -12.0% |
| 48/384 | +51.2% | +40.2% | +39.8% |

Fees and funding took 9 to 15 points off every setting. Slippage took under one. Only the three 384-hour settings finished positive, and that is the trap: picking 48/384 now, because it won on this data, proves nothing. The script's last lines show the honest version. Choosing on the first half alone also picks 48/384, which lost 3.0% there. Traded on the unseen second half, it made 44.0% while BTC fell 20.8%. That is one split of one asset, and the second half was a falling market that suits a slow trend rule. Repeat it on other periods and markets before a setting earns real money.

## Step 4: run the live loop in dry-run mode

Every pass does three things: read fresh data, decide, and apply limits before anything leaves the machine. A reversal takes two passes: a reduce-only order closes the old side, and the next pass opens the new side once the close has filled. Opening first from a long would only flatten it. For a live signal Hyperliquid's own API is enough: 400 hourly candles of warm-up fit well inside its 5,000. This script does one pass and prints the order it would send:

```
# One pass of the live loop in dry-run mode: read, decide, check limits, print the order it would sign.
# It never signs and never calls /exchange.
import os, sys, time, json, math, requests

INFO = "https://api.hyperliquid.xyz/info"
COIN, FAST, SLOW = "BTC", 24, 96
KILL_FILE = sys.argv[1] if len(sys.argv) > 1 else "KILL"           # create this file to stop the bot
LIMITS = {"max_notional_usd": 500.0, "max_daily_loss_usd": 25.0, "max_data_age_s": 120, "ioc_band": 0.002}
# Dry-run inputs. A live bot reads both on every pass, PnL from its fills and position from clearinghouseState,
# and halts if either read fails.
DAILY_PNL_USD = 0.0
POSITION_USD = float(os.environ.get("DRY_RUN_POSITION_USD", "0"))      # signed: +500 is a $500 long

def ema(x, span):
    a = 2 / (span + 1); out = [x[0]]
    for v in x[1:]:
        out.append(a * v + (1 - a) * out[-1])
    return out

def post(body):
    return requests.post(INFO, json=body, timeout=15).json()

def decide():
    if os.path.exists(KILL_FILE):
        return {"action": "halt", "why": "kill switch file present: " + KILL_FILE}
    now = int(time.time() * 1000)
    c = post({"type": "candleSnapshot", "req": {"coin": COIN, "interval": "1h", "startTime": now - 400 * 3600_000, "endTime": now}})
    closed = [x for x in c if x["T"] < now]                           # drop the candle still forming
    book = post({"type": "l2Book", "coin": COIN})
    age = now - book["time"]
    if age > LIMITS["max_data_age_s"] * 1000:
        return {"action": "halt", "why": "book is %d s old" % (age // 1000)}
    if DAILY_PNL_USD <= -LIMITS["max_daily_loss_usd"]:
        return {"action": "halt", "why": "daily loss limit hit"}
    closes = [float(x["c"]) for x in closed]
    ef, es = ema(closes, FAST)[-1], ema(closes, SLOW)[-1]
    want = 1 if ef > es else -1
    have = 0 if POSITION_USD == 0 else (1 if POSITION_USD > 0 else -1)
    out = {"candles_used": len(closed), "last_closed_bar": closed[-1]["t"], "ema_fast": round(ef, 1), "ema_slow": round(es, 1), "want": want, "have": have}
    if want == have:
        return out | {"action": "hold"}
    bid, ask = float(book["levels"][0][0]["px"]), float(book["levels"][1][0]["px"])
    is_buy = want > 0
    px = round((ask if is_buy else bid) * (1 + LIMITS["ioc_band"] * (1 if is_buy else -1)))   # BTC prices are whole dollars at this size
    meta = post({"type": "meta"})
    asset = next(i for i, u in enumerate(meta["universe"]) if u["name"] == COIN)
    dec = next(u["szDecimals"] for u in meta["universe"] if u["name"] == COIN)
    if have != 0:                    # reversing: close with reduce-only first, open the new side on a later pass
        usd, reduce, step = abs(POSITION_USD), True, "close"
    else:
        usd, reduce, step = LIMITS["max_notional_usd"], False, "open"
    sz = math.floor(usd / px * 10 ** dec) / 10 ** dec                  # round down, never over the cap
    if sz * px > LIMITS["max_notional_usd"] * 1.0001:
        return out | {"action": "halt", "why": "size over notional cap"}
    action = {"type": "order", "orders": [{"a": asset, "b": is_buy, "p": str(px), "s": str(sz), "r": reduce, "t": {"limit": {"tif": "Ioc"}}}], "grouping": "na"}
    return out | {"action": "would_send", "step": step, "exchange_action_unsigned": action, "notional_usd": round(sz * px, 2)}

print(json.dumps(decide(), indent=1))
```

Run at 02:20 UTC on 9 November 2026 three ways: flat, with a $500 position open on the wrong side, and with a file named `KILL` present:

```
$ python3 04_live_loop_dry_run.py
{
 "candles_used": 400,
 "last_closed_bar": 1794186000000,
 "ema_fast": 75507.2,
 "ema_slow": 76286.4,
 "want": -1,
 "have": 0,
 "action": "would_send",
 "step": "open",
 "exchange_action_unsigned": {
  "type": "order",
  "orders": [
   {
    "a": 0,
    "b": false,
    "p": "75186",
    "s": "0.00665",
    "r": false,
    "t": {
     "limit": {
      "tif": "Ioc"
     }
    }
   }
  ],
  "grouping": "na"
 },
 "notional_usd": 499.99
}
$ DRY_RUN_POSITION_USD=500 python3 04_live_loop_dry_run.py
{
 "candles_used": 400,
 "last_closed_bar": 1794186000000,
 "ema_fast": 75507.2,
 "ema_slow": 76286.4,
 "want": -1,
 "have": 1,
 "action": "would_send",
 "step": "close",
 "exchange_action_unsigned": {
  "type": "order",
  "orders": [
   {
    "a": 0,
    "b": false,
    "p": "75182",
    "s": "0.00665",
    "r": true,
    "t": {
     "limit": {
      "tif": "Ioc"
     }
    }
   }
  ],
  "grouping": "na"
 },
 "notional_usd": 499.96
}
$ touch KILL && python3 04_live_loop_dry_run.py
{
 "action": "halt",
 "why": "kill switch file present: KILL"
}
```

The fast average was below the slow one, so the rule wanted to be short. Flat, it built an immediate-or-cancel (IOC) limit sell for $499.99 of BTC, under the $500 cap, priced 0.2% through the bid so it can fill but cannot chase. Holding a $500 long, it built a reduce-only sell of the same size instead, which can close the long but can never open a short by mistake. Round the size down, as the script does. Rounding to the nearest step can land a cent over the cap, and the loop then refuses the order. With the kill file present, it halts before reading any data.

To run it continuously, call one pass after each hourly candle closes. Store the last bar you acted on so a restart cannot trade the same signal twice, and halt when the position or PnL read fails. Between candles, keep watching the position and open orders on their own schedule, and settle any order you are unsure about before the next signal.

## Step 5: sign and send orders to Hyperliquid

This is the only step that moves money, and we did not run it. No key was loaded. Hyperliquid's docs recommend an existing SDK for signing, because a wrong signature fails without saying why. The order is the shape the dry run built: `a` (asset index), `b` (buy), `p` (price), `s` (size), `r` (reduce only) and `t` (order type), posted to `https://api.hyperliquid.xyz/exchange` with a nonce and a signature.

```
"""PROTOTYPE. NOT RUN. This file was never executed and sends nothing: no key was loaded and no order was placed.
It shows where signing and sending happen. 0xArchive does not place orders; orders go to Hyperliquid's own /exchange endpoint.
Method names checked against the public hyperliquid-python-sdk source (exchange.py) on 8 Nov 2026."""
import os
from eth_account import Account
from hyperliquid.exchange import Exchange
from hyperliquid.utils import constants

API_WALLET_KEY = os.environ["HL_API_WALLET_KEY"]      # an API wallet approved for the account, never the main key
ACCOUNT = os.environ["HL_ACCOUNT_ADDRESS"]            # the trading account the API wallet acts for

wallet = Account.from_key(API_WALLET_KEY)
ex = Exchange(wallet, constants.MAINNET_API_URL, account_address=ACCOUNT)

def send(decision):
    """decision is the dict the Step 4 dry run prints when action is would_send."""
    o = decision["exchange_action_unsigned"]["orders"][0]
    r = ex.order("BTC", o["b"], float(o["s"]), float(o["p"]), {"limit": {"tif": "Ioc"}}, reduce_only=o["r"])
    status = r["response"]["data"]["statuses"][0] if r.get("status") == "ok" else {"error": r}
    if "error" in status:
        raise SystemExit(f"order rejected: {status['error']}")    # halt; do not retry blindly
    return status                                                 # {"filled": ...} or {"resting": ...}

def flatten():
    return ex.market_close("BTC")                                  # kill switch action: close the position
```

- **Trade from an API wallet,** a separate key the account owner approves to sign for the account, so the owner's key never sits on the bot server. Hyperliquid's docs call it an agent wallet. Generate a fresh one for each bot and never reuse an old address: once a wallet is replaced, its nonce history can be pruned and old signed actions replayed.
- **Use IOC with a price band for entries and reduce-only for exits,** so a bug cannot flip you into a bigger position.
- **Read the order result.** The response lists one status per order: filled, resting or an error. An HTTP 200 does not mean it filled, and an IOC can fill in part, so act on the filled size and read the position back before the next pass. Tag each order with a client order id ( `cloid` ): after a timeout, look it up instead of sending it again.
- **Arm a dead man's switch only if orders rest.** Hyperliquid's `scheduleCancel` cancels every open order at a deadline unless you push it back, and it can trigger at most 10 times a day. An IOC-only bot like this one leaves nothing resting. If yours posts limit orders, renew the deadline from its own heartbeat, not from the hourly signal.
- **Use one process and one nonce source.** Hyperliquid's docs recommend a millisecond timestamp.
- **Run it on testnet first.** Pass `constants.TESTNET_API_URL` ( `https://api.hyperliquid-testnet.xyz` ) instead of the mainnet URL, and the same code trades test funds.

## Step 6: add risk limits and a kill switch

Put the limits in code that runs before an order is built, so no signal can bypass them:

- **Caps.** A notional cap per order and a position cap. The dry run refuses any order over $500. It does not check the position an order would leave you with: a live bot reads its position and open orders first, and refuses anything that would take it past the position cap.
- **Daily loss limit.** When realized plus open loss passes the limit, halt until a person resets the bot. In the dry run the day's PnL is fixed at 0, so the check is wired but not fed. A live bot computes it from its fills and positions, keeps it across restarts, and halts if it cannot read them. It never assumes zero.
- **Fresh data.** Refuse to act on a book more than two minutes old.
- **Price band.** IOC limits through the touch, never an unbounded market order.
- **Software kill switch.** The loop checks for a file at the top of every pass. Creating the file stops it.
- **Exchange-side kill switch.** Hyperliquid's `scheduleCancel` cancels all open orders at a time you set, at least 5 seconds ahead. Push the deadline forward every pass. If the bot dies, the deadline passes and the orders are cancelled, but positions stay open. A deadline that fires counts as a trigger, and Hyperliquid allows 10 a day, reset at 00:00 UTC.
- **Flatten on halt.** A cancel does not close a position. The dry run's kill file only stops new orders. A live halt handler cancels anything resting, calls `flatten()` to close with a reduce-only order, then reads the position back and confirms it is zero.

## Side by side: Hyperliquid's API and 0xArchive

The same jobs, run on 8 and 9 November 2026:

| Job | Hyperliquid's API (free) | 0xArchive |
| --- | --- | --- |
| Hourly BTC candles | 4,976, back to mid-April 2026 | 14,293 from 22 March 2025, 2 requests |
| Settled hourly funding | `fundingHistory`, 500 payments a request, back to May 2023. Use this. | Hourly rate samples, not settlements |
| Fill costs from past books | Live book only | 120 stored books across 19 months |
| Live signal | Enough: 400 candles fit in 5,000 | Not needed |
| Orders | `/exchange`, signed by you | Not offered |

Run the live loop on Hyperliquid's own API. Use 0xArchive to test the signal and price its fills before it trades. For more on where each source stops, see [Hyperliquid's native API vs 0xArchive](https://0xarchive.io/resources/comparisons/native-hyperliquid-api-vs-0xarchive).

## What to know before you build

- **Candles start 22 March 2025 at 12:00 UTC.** A request that ends earlier returns HTTP 400.
- **Decide on closed candles.** The newest candle is still forming, so drop it, as the dry run does.
- **20 levels cannot price a very large order.** At $1,000,000, 16 of 120 books ran out.
- **Use your own fee rate,** plus any builder fee charged by a front end.
- **Stay inside the rate limits.** Hyperliquid allows 1,200 request weight a minute per IP. `l2Book` weighs 2, most other info requests 20, and `candleSnapshot` adds weight for every 60 candles returned, so one dry-run pass costs about 50. Orders also draw on a per-address allowance of one request per 1 USDC traded, after a starting buffer of 10,000.
- **Keep a WebSocket alive if you add one.** Hyperliquid closes a connection that has had no message for 60 seconds. Send `{"method": "ping"}` on quiet channels, resubscribe after every reconnect, and read the position back before trading again.
- **A test ignores your own market impact and latency.** Treat its result as an upper bound.
- **One asset, one rule, one period** is a measurement, not a forecast.

## FAQ

### Can you build a trading bot on Hyperliquid?

Yes. Hyperliquid has a public trading API. You sign orders with an approved API wallet and post them to its `/exchange` endpoint, and the official Python SDK handles signing.

### How much Hyperliquid history can I test a bot on?

Hyperliquid's own candle API returns the latest 5,000 candles, 207 days at one hour. 0xArchive serves Hyperliquid candles from 22 March 2025.

### Are Hyperliquid trading bots profitable?

Not by default. A bot only automates a rule, and the rule has to beat its costs. The moving-average bot in this guide lost 36.0% after costs over 19 months, against 13.8% for buy and hold, and fees and funding alone took 9 to 15 points. Test the rule on history before it trades.

### Is there a Hyperliquid testnet for bots?

Yes, at `https://api.hyperliquid-testnet.xyz`. The Python SDK exposes it as `constants.TESTNET_API_URL`, so a bot can switch between test and live funds by changing one URL.

### How do I stop a Hyperliquid bot quickly?

Use two layers: a flag the loop checks before each action, and `scheduleCancel`, so Hyperliquid cancels your open orders if the bot goes silent. Then close the position.

### What does a simple strategy cost to run?

In our test, fees and funding took 9 to 15 points over 19 months, and slippage at 0.2 basis points took under one. Fees differ by account.

### Does 0xArchive place orders?

No. It supplies market data. Orders go from your key to Hyperliquid.

## Next steps

- More history for testing: [Hyperliquid historical data for backtesting](https://0xarchive.io/resources/market-data-guides/hyperliquid-historical-data-api-for-backtesting) .
- Hyperliquid's own rules for signing and API wallets: the [exchange endpoint](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint) and [nonces and API wallets](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets) .
- Add flow to the signal: [How to Build a Hyperliquid Order Flow Dashboard](https://0xarchive.io/resources/market-data-guides/hyperliquid-order-flow-dashboard) .
- See the book behind your fills: [How to Build a Hyperliquid Order Book Heatmap](https://0xarchive.io/resources/market-data-guides/hyperliquid-order-book-heatmap) .
- Reference: [candles](https://docs.0xarchive.io/api-reference/hyperliquid--candles/get-hyperliquid-ohlcv-candles) , [funding](https://docs.0xarchive.io/api-reference/hyperliquid--funding/get-hyperliquid-funding-rate-history) , [candles data](https://0xarchive.io/data-types/candles) and [Hyperliquid data](https://0xarchive.io/hyperliquid) .

Tested on 8 and 9 November 2026 on BTC. Not financial advice.
