Skip to content

Orders

Orders, cancels and the other trading actions are L1 actions: an API wallet can sign them, and they act for the account that approved it. The snippets on this page build on this setup and use Testnet:

signer, err := hyperliquid.NewPrivateKeySigner(os.Getenv("HL_AGENT_KEY"))
if err != nil {
	log.Fatal(err)
}
info := hyperliquid.NewInfoClient(hyperliquid.Testnet)
ex := hyperliquid.NewExchangeClient(hyperliquid.Testnet, signer)
user := hyperliquid.MustParseAddress(os.Getenv("HL_ACCOUNT")) // the account, for queries

markets, err := hyperliquid.LoadMarkets(ctx, info)
if err != nil {
	log.Fatal(err)
}
eth, ok := markets.Asset("ETH")
if !ok {
	log.Fatal("ETH not listed")
}

Orders take the asset ID, and prices and sizes rounded to the market’s tick and lot size.

Limit orders

An Order with a LimitOrder type rests on the book according to its time in force:

TifBehavior
TifGtcRests until filled or canceled.
TifAloAdd liquidity only: canceled instead of taking liquidity (post-only).
TifIocFills what it can immediately and cancels the rest.
px, err := eth.FormatPrice("2500")
if err != nil {
	log.Fatal(err)
}
sz, err := eth.FormatSize("0.01")
if err != nil {
	log.Fatal(err)
}
results, err := ex.Order(ctx, hyperliquid.OrderAction{
	Orders: []hyperliquid.Order{{
		Asset: eth.ID,
		IsBuy: true,
		Price: px,
		Size:  sz,
		Type:  hyperliquid.OrderType{Limit: &hyperliquid.LimitOrder{Tif: hyperliquid.TifAlo}},
	}},
})

One OrderAction can carry many orders; they are accepted or rejected one by one. Set ReduceOnly on an order that must only shrink a position.

Handling results

ExchangeClient.Order returns one OrderResult per order, in request order. Exactly one of its fields is set:

results, err := ex.Order(ctx, action)
if _, rejected := errors.AsType[*hyperliquid.StatusError](err); err != nil && !rejected {
	log.Fatal(err) // the whole action failed; see the errors page
}
for i, r := range results {
	switch {
	case r.Resting != nil:
		fmt.Printf("order %d resting, oid %d\n", i, r.Resting.Oid)
	case r.Filled != nil:
		fmt.Printf("order %d filled %s @ %s, oid %d\n", i, r.Filled.TotalSz, r.Filled.AvgPx, r.Filled.Oid)
	case r.Error != "":
		fmt.Printf("order %d rejected: %s\n", i, r.Error)
	default:
		fmt.Printf("order %d: %s\n", i, r.Status) // "waitingForFill", "waitingForTrigger"
	}
}

Rejected orders are also reported as a joined *StatusError per order, so a non-nil error does not mean that nothing was placed. A filled order gets an oid too, which fills and order queries refer to. See Errors for the other error kinds and for deciding whether an action executed when the response was lost.

Market orders

Hyperliquid has no market order type. Send an IOC limit order with a price far enough through the book to fill, which also caps your slippage:

mids, err := info.AllMids(ctx, hyperliquid.AllMidsRequest{})
if err != nil {
	log.Fatal(err)
}
mid, err := mids[eth.Name].Float64()
if err != nil {
	log.Fatal(err)
}
const slippage = 0.01 // accept up to 1% worse than mid
px, err := eth.FormatPrice(hyperliquid.DecimalFromFloat(mid * (1 + slippage)))
if err != nil {
	log.Fatal(err)
}
results, err := ex.Order(ctx, hyperliquid.OrderAction{
	Orders: []hyperliquid.Order{{
		Asset: eth.ID,
		IsBuy: true,
		Price: px,
		Size:  "0.01",
		Type:  hyperliquid.OrderType{Limit: &hyperliquid.LimitOrder{Tif: hyperliquid.TifIoc}},
	}},
})

For a sell, use mid * (1 - slippage). An IOC order that finds no liquidity within its price is rejected rather than left resting.

Client order IDs

A Cloid is a 16-byte ID you choose. Set one on every order to look it up, cancel it, and reconcile after a lost response without knowing the exchange’s oid:

var id hyperliquid.Cloid
rand.Read(id[:]) // crypto/rand

order := hyperliquid.Order{
	Asset: eth.ID, IsBuy: true, Price: "2500", Size: "0.01",
	Type:  hyperliquid.OrderType{Limit: &hyperliquid.LimitOrder{Tif: hyperliquid.TifGtc}},
	Cloid: &id,
}
if _, err := ex.Order(ctx, hyperliquid.OrderAction{Orders: []hyperliquid.Order{order}}); err != nil {
	log.Fatal(err)
}

status, err := info.OrderStatus(ctx, hyperliquid.OrderStatusRequest{
	User: user,
	Oid:  hyperliquid.OrderRef{Cloid: &id},
})
if err != nil {
	log.Fatal(err)
}
if status != nil {
	fmt.Println(id, "is", status.Status)
}

ParseCloid reads one from its 0x-prefixed hex form, and fills, open orders and order updates carry it back.

Take profit and stop loss

A trigger order rests off the book until the mark price crosses TriggerPx. Tpsl says whether it is a take profit or a stop loss, and IsMarket executes it as a market order when triggered; otherwise Price is its limit price. TP/SL orders are normally reduce-only and on the opposite side of the position.

The Grouping of the action links them to a position:

  • GroupingNormalTpsl attaches TP/SL orders of a fixed size to the first order of the action, typically an entry order.
  • GroupingPositionTpsl attaches TP/SL orders that track the size of the existing position.

An entry with a bracket:

size := hyperliquid.Decimal("0.01")
trigger := func(px hyperliquid.Decimal, kind hyperliquid.Tpsl) hyperliquid.Order {
	return hyperliquid.Order{
		Asset: eth.ID, IsBuy: false, Price: px, Size: size, ReduceOnly: true,
		Type: hyperliquid.OrderType{Trigger: &hyperliquid.TriggerOrder{
			IsMarket: true, TriggerPx: px, Tpsl: kind,
		}},
	}
}
results, err := ex.Order(ctx, hyperliquid.OrderAction{
	Orders: []hyperliquid.Order{
		{ // entry
			Asset: eth.ID, IsBuy: true, Price: "2500", Size: size,
			Type: hyperliquid.OrderType{Limit: &hyperliquid.LimitOrder{Tif: hyperliquid.TifGtc}},
		},
		trigger("2750", hyperliquid.TpslTakeProfit),
		trigger("2400", hyperliquid.TpslStopLoss),
	},
	Grouping: hyperliquid.GroupingNormalTpsl,
})

A stop loss on a position you already hold, sized from the account state:

state, err := info.ClearinghouseState(ctx, hyperliquid.ClearinghouseStateRequest{User: user})
if err != nil {
	log.Fatal(err)
}
for _, ap := range state.AssetPositions {
	p := ap.Position
	if p.Coin != eth.Name {
		continue
	}
	long := !strings.HasPrefix(string(p.Szi), "-")
	stopPx, err := eth.FormatPrice("2400") // below the entry of a long
	if err != nil {
		log.Fatal(err)
	}
	_, err = ex.Order(ctx, hyperliquid.OrderAction{
		Orders: []hyperliquid.Order{{
			Asset: eth.ID, IsBuy: !long, Price: stopPx,
			Size:       hyperliquid.Decimal(strings.TrimPrefix(string(p.Szi), "-")),
			ReduceOnly: true,
			Type: hyperliquid.OrderType{Trigger: &hyperliquid.TriggerOrder{
				IsMarket: true, TriggerPx: stopPx, Tpsl: hyperliquid.TpslStopLoss,
			}},
		}},
		Grouping: hyperliquid.GroupingPositionTpsl,
	})
	if err != nil {
		log.Fatal(err)
	}
}

Modifying orders

Modify replaces a resting order, identified by oid or by cloid, with a new one. Unless AlwaysPlace is set, the new order must be a non-trigger ALO order or a GTC order that would not take liquidity, and it is not placed if canceling the old one fails:

err := ex.Modify(ctx, hyperliquid.ModifyAction{
	Oid: hyperliquid.OrderRef{Oid: oid},
	Order: hyperliquid.Order{
		Asset: eth.ID, IsBuy: true, Price: "2490", Size: "0.01",
		Type: hyperliquid.OrderType{Limit: &hyperliquid.LimitOrder{Tif: hyperliquid.TifAlo}},
	},
})

BatchModify replaces several orders in one action and returns one OrderResult per new order, like Order:

results, err := ex.BatchModify(ctx, hyperliquid.BatchModifyAction{
	Modifies: []hyperliquid.Modify{
		{Oid: hyperliquid.OrderRef{Oid: oid}, Order: order},
		{Oid: hyperliquid.OrderRef{Cloid: &cloid}, Order: order},
	},
})

Canceling orders

Cancel cancels by asset and oid, CancelByCloid by asset and client order ID. A cancel that fails, for example because the order already filled, is reported as a joined *StatusError per item:

err := ex.Cancel(ctx, hyperliquid.CancelAction{
	Cancels: []hyperliquid.Cancel{{Asset: eth.ID, Oid: oid}},
})
if err != nil {
	log.Print(err)
}

err = ex.CancelByCloid(ctx, hyperliquid.CancelByCloidAction{
	Cancels: []hyperliquid.CancelByCloid{{Asset: eth.ID, Cloid: cloid}},
})
if err != nil {
	log.Print(err)
}

To cancel everything on a market, list the open orders with InfoClient.OpenOrders and cancel them in one action.

Dead man’s switch

ScheduleCancel cancels all open orders at a given time, at least 5 seconds ahead. Push the time forward while your process is healthy; if it dies or loses connectivity, its orders are canceled. A zero Time clears the schedule. The switch may fire at most 10 times per day.

ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
	at := time.Now().Add(30 * time.Second).UnixMilli()
	if err := ex.ScheduleCancel(ctx, hyperliquid.ScheduleCancelAction{Time: at}); err != nil {
		log.Print(err)
	}
	select {
	case <-ticker.C:
	case <-ctx.Done():
		return
	}
}

Each refresh is an action and counts against your rate limit.

TWAP orders

TwapOrder trades a total size over 5 to 1440 minutes in slices and returns the TWAP ID; TwapCancel stops it. Both return a bare *StatusError when the exchange rejects them:

twapID, err := ex.TwapOrder(ctx, hyperliquid.TwapOrderAction{
	Twap: hyperliquid.Twap{
		Asset:     eth.ID,
		IsBuy:     true,
		Size:      "1",
		Minutes:   60,
		Randomize: true,
	},
})
if err != nil {
	log.Fatal(err)
}

err = ex.TwapCancel(ctx, hyperliquid.TwapCancelAction{Asset: eth.ID, TwapID: twapID})
if err != nil {
	log.Fatal(err)
}

Details optionally delays the start until the mark price crosses a price and stops the TWAP at a stop price. Follow progress with InfoClient.TwapHistory and UserTwapSliceFills.

Trailing stops

TrailingStop places a stop that follows the price at a fixed distance, as a percentage ("1.5" is 1.5%) or an absolute price distance, and returns its oid. ActivationPx delays trailing until the price reaches it:

pct := hyperliquid.Decimal("1.5")
oid, err := ex.TrailingStop(ctx, hyperliquid.TrailingStopAction{
	Asset:       eth.ID,
	IsBuy:       false, // protects a long
	Size:        "0.01",
	ReduceOnly:  true,
	Retracement: hyperliquid.Retracement{Pct: &pct},
})
if err != nil {
	log.Fatal(err)
}
fmt.Println("trailing stop oid", oid)

Leverage and margin

UpdateLeverage sets the leverage and margin mode (cross or isolated) of a perp for the account. UpdateIsolatedMargin adds margin to or removes it from an isolated position; Ntli is the USDC amount times 106, negative to remove:

err := ex.UpdateLeverage(ctx, hyperliquid.UpdateLeverageAction{
	Asset:    eth.ID,
	IsCross:  false,
	Leverage: 5,
})
if err != nil {
	log.Fatal(err)
}

err = ex.UpdateIsolatedMargin(ctx, hyperliquid.UpdateIsolatedMarginAction{
	Asset: eth.ID,
	IsBuy: true,       // the official SDKs always send true
	Ntli:  25_000_000, // add 25 USDC
})
if err != nil {
	log.Fatal(err)
}

The maximum leverage of each asset is in Meta.

Builder fees

A builder, such as a front end or a trading bot service, can charge a fee on the orders it submits. The account first approves a maximum rate with its own key, because ApproveBuilderFee is a user-signed action. MaxFeeRate is a percentage of notional:

builder := hyperliquid.MustParseAddress("0x1234567890abcdef1234567890abcdef12345678")

owner, err := hyperliquid.NewPrivateKeySigner(os.Getenv("HL_ACCOUNT_KEY"))
if err != nil {
	log.Fatal(err)
}
err = hyperliquid.NewExchangeClient(hyperliquid.Testnet, owner).ApproveBuilderFee(ctx, hyperliquid.ApproveBuilderFeeAction{
	Builder:    builder,
	MaxFeeRate: "0.01", // 0.01%
})
if err != nil {
	log.Fatal(err)
}

Orders then name the builder and the fee in Builder, in tenths of a basis point (10 is 0.01%), at most the approved rate:

results, err := ex.Order(ctx, hyperliquid.OrderAction{
	Orders:  []hyperliquid.Order{order},
	Builder: &hyperliquid.Builder{Address: builder, Fee: 10},
})

InfoClient.MaxBuilderFee returns the rate an account has approved for a builder, in the same tenths of a basis point.

Last updated on