PearPear

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.

FieldMeaning
config.thresholdStartThe first rung. Greater than 0.
config.thresholdEndThe last rung. Greater than 0, and different from thresholdStart. The range can ascend or descend.
config.levelsNumber of rungs, 2 to 50.
config.weightingOptional. EQUAL is the only mode and the default. Every rung takes the same share.
condition.data.trackTROUGH 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:

FormtypedataMeasures
StaticupnlpriceSource, thresholdUnrealized PnL in USD. threshold must not be 0.
Staticupnl_bpspriceSource, thresholdUnrealized PnL in basis points. threshold must not be 0.
Trailingupnl_trailingpriceSourceUnrealized PnL in USD, trailing its best value.
Trailingupnl_bps_trailingpriceSourceUnrealized 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 OPEN and not mid-execution. Otherwise the call returns 409.
  • attachTriggers is rejected with a 400: "attachTriggers is not supported when the ladder targets an existing position". Arm the brackets directly with POST /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.

RejectionStatus
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.

FieldMeaning
idLadder UUID. Cancel the whole ladder with it.
statusACTIVE while any rung is still resting, then COMPLETED or CANCELLED.
triggersThe expanded rungs. intent is OPEN on a fresh instrument set, ADJUST when a position already exists.
attachTriggersPresent only when you sent one.

Cancel one rung with PATCH /triggers/{triggerId}/cancel, using that rung's id.

Manage ladders

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.

On this page