Ladder Order
Stagger multiple basket entries across a ratio range.
A ladder places several resting entries across a range, so the basket builds in steps as the market moves into your range instead of all at once.
Create one with POST /ladders (SDK: sdk.core.ladders.create).
Request
This builds the long BTC / short ETH basket in four steps, as the BTC/ETH ratio falls from 26 to 24:
{
"type": "MARKET",
"intent": "OPEN",
"condition": {
"type": "ratio",
"data": {
"priceSource": "mid",
"symbol_a": "0",
"symbol_b": "1",
"track": "TROUGH"
}
},
"config": {
"thresholdStart": 26,
"thresholdEnd": 24,
"levels": 4,
"weighting": "EQUAL"
},
"payload": {
"type": "MARKET",
"legs": [
{ "symbol": "0", "side": "BUY", "mode": "USD", "amount": "2000" },
{ "symbol": "1", "side": "SELL", "mode": "USD", "amount": "2000" }
]
},
"clientId": "your-client-id"
}Pear spaces the rungs evenly from thresholdStart to thresholdEnd, and gives each rung an equal 1 / levels share of the payload. The example above therefore places rungs at 26, 25.33, 24.67, and 24, each opening 500 USD per leg.
| Field | Meaning |
|---|---|
config.thresholdStart | The first rung. Greater than 0. |
config.thresholdEnd | The last rung. Greater than 0, and different from thresholdStart. The range can ascend or descend. |
config.levels | Number of rungs, 2 to 50. |
config.weighting | Optional. EQUAL is the only mode and the default. Every rung takes the same share. |
condition.data.track | TROUGH fires a rung when the value falls to it, PEAK when it rises to it. |
A ladder condition is ratio, price, or weighted_ratio. Unlike a trigger, the condition carries no threshold: the range in config supplies the thresholds.
The body rejects unknown top-level keys.
Attach a take profit or stop loss
attachTriggers arms close triggers on the position the rungs build. A ladder accepts a narrower condition set than POST /triggers/open. Only these four are legal:
| Form | type | data | Measures |
|---|---|---|---|
| Static | upnl | priceSource, threshold | Unrealized PnL in USD. threshold must not be 0. |
| Static | upnl_bps | priceSource, threshold | Unrealized PnL in basis points. threshold must not be 0. |
| Trailing | upnl_trailing | priceSource | Unrealized PnL in USD, trailing its best value. |
| Trailing | upnl_bps_trailing | priceSource | Unrealized PnL in basis points, trailing its best value. |
notional and notional_trailing are rejected on a ladder: "Ladder brackets cannot use notional conditions; use upnl or upnl_bps." A ladder's first fill is only a fraction of the intended size, so a notional bracket sized against the whole ladder would be breached the moment it is created.
{
"attachTriggers": [
{
"intent": "CLOSE",
"type": "MARKET",
"bracketType": "TAKE_PROFIT",
"condition": { "type": "upnl_bps", "data": { "priceSource": "mark", "threshold": 500 } }
}
]
}The list rules are the same as on a trigger: at most 5 entries, at most one of them trailing. See Trigger Order for the static and trailing forms and for what happens when a position is already at its trigger limit.
attachTriggers is rejected outright when the ladder targets instruments you already hold. See below.
Building onto a position you already hold
If a position already exists for exactly these instruments on this trade account, the ladder builds onto it. Every rung becomes an ADJUST order bound to that position, rather than an OPEN order that would create a second one. The match is made on the position key.
Two consequences:
- The position must be
OPENand not mid-execution. Otherwise the call returns409. attachTriggersis rejected with a400: "attachTriggers is not supported when the ladder targets an existing position". Arm the brackets directly withPOST /triggers/close, which acts on the position you already have.
With no such position, every rung is an OPEN order and carries the attachTriggers set. The brackets are created once a rung fires and the position exists.
What gets rejected
A ladder is validated against the live market when you create it, not only against the schema.
| Rejection | Status |
|---|---|
The band is already reached. Every rung is a static threshold, so a rung the market already satisfies fires on the next evaluation pass. The ladder would then open its whole size within seconds instead of one step at a time. Place the band ahead of the market: below it for TROUGH, above it for PEAK. | 400 |
| The market data is too old. The condition is priced from an anchor at most 60 seconds old. A price that stopped updating cannot prove the band is still ahead. | 400 |
A USD rung falls below the venue minimum. levels divides the payload, so 2,000 USD across 50 rungs is 40 USD a rung. Only USD-mode legs are checked. Read each instrument's floor from GET /instruments as precision.notional.min. This is the real constraint behind a high levels. | 400 |
| Too many active rungs. At most 50 active rungs per position key, counted across every ladder on it. | 400 |
| An unknown instrument ID in the condition. | 422 |
Response
200 OK, not 202. The body carries the ladder with its rungs already expanded into child triggers:
{
"ladder": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"connector": "hyperliquid",
"positionKey": "9f2a6c1b4e07d38a5c91f0b2d47e63a8c05b9142ef7d3608ba1c4f52d9e07a63",
"config": { "thresholdStart": 26, "thresholdEnd": 24, "levels": 4, "weighting": "EQUAL" },
"status": "ACTIVE",
"triggers": [
{
"id": "1c47a0f2-8b3d-4e51-9f6a-2d0c8e4b7a19",
"tradeAccountId": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"connector": "hyperliquid",
"type": "MARKET",
"intent": "OPEN",
"status": "ACTIVE",
"bracketType": "NONE",
"ladderId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"ladderIndex": 0,
"condition": {
"type": "ratio",
"data": {
"priceSource": "mid",
"symbol_a": "0",
"symbol_b": "1",
"track": "TROUGH",
"threshold": 26
}
},
"context": {
"type": "MARKET",
"legs": [
{ "symbol": "0", "side": "BUY", "mode": "USD", "amount": "500" },
{ "symbol": "1", "side": "SELL", "mode": "USD", "amount": "500" }
],
"attachedTriggers": []
},
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
}
],
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
}
}Only the first rung is shown. The real triggers array holds one entry per rung, each carrying its own ladderIndex, its own condition.data.threshold, and legs scaled to its share.
| Field | Meaning |
|---|---|
id | Ladder UUID. Cancel the whole ladder with it. |
status | ACTIVE while any rung is still resting, then COMPLETED or CANCELLED. |
triggers | The expanded rungs. intent is OPEN on a fresh instrument set, ADJUST when a position already exists. |
attachTriggers | Present only when you sent one. |
Cancel one rung with PATCH /triggers/{triggerId}/cancel, using that rung's id.
Manage ladders
| Action | Endpoint |
|---|---|
| List | GET /ladders |
| Read one | GET /ladders/{ladderId} |
| Cancel | POST /ladders/{ladderId}/cancel |
Cancelling removes the rungs that have not filled. Rungs that already filled stay as part of the position. Progress streams over the WebSocket as ladder.created, then ladder.updated on each rung fill or rung cancellation, then the position.* events. Cancelling the whole ladder emits ladder.cancelled.