PearPear
API IntegrationExecuting Trade

Basket Trade

Open a multi-leg basket in a single call.

A basket has one or more long legs and one or more short legs. You open the whole basket with a single POST /trade/open, scoped to a connected trade account.

Legs and instrument IDs

Each leg names an instrument on the trade account's exchange:

FieldValuesMeaning
symbolinstrument IDThe instrument to trade. Not the ticker, see Instrument ID.
sideBUY, SELLBUY makes the leg long, SELL makes it short.
modeUSD, QUANTITYHow to read amount.
amountdecimal stringNotional in USD, or a quantity of the base asset.

The symbol field takes the instrument ID, the exchange's own name for a market. On Hyperliquid BTC is "0"; on Binance it is "BTCUSDT". IDs never carry across venues, so Instrument ID is worth reading once before you send a leg.

Resolve tickers to IDs once with GET /instruments/symbol-to-id-map, and cache the result:

curl "https://pro-gateway.pearprotocol.io/instruments/symbol-to-id-map?connector=hyperliquid"
{ "data": { "BTC": "0", "ETH": "1", "SOL": "5", "HYPE": "159" } }

Use GET /instruments when you also need precision, margin mode, and whether the instrument is tradable.

Open a basket

This opens a long BTC / short ETH pair on Hyperliquid, 500 USD on each side:

curl -X POST "https://pro-gateway.pearprotocol.io/trade/open" \
  -H "x-api-key: $PEAR_API_KEY" \
  -H "x-trade-account-id: $TRADE_ACCOUNT_ID" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "MARKET",
    "legs": [
      { "symbol": "0", "side": "BUY",  "mode": "USD", "amount": "500" },
      { "symbol": "1", "side": "SELL", "mode": "USD", "amount": "500" }
    ]
  }'

With the SDK:

await sdk.core.trade.open({
  type: 'MARKET',
  legs: [
    { symbol: '0', side: 'BUY', mode: 'USD', amount: '500' },
    { symbol: '1', side: 'SELL', mode: 'USD', amount: '500' },
  ],
}); // body: HttpTypes.CreateTradeOpen

Rules the gateway enforces:

  • At least one leg.
  • Each leg names a distinct instrument.
  • amount is an unsigned decimal string. Direction comes from side, never from a minus sign.
  • A leg on /trade/open takes no reduceOnly. The gateway sets it to false and drops the field if you send it. Only POST /trade/{positionId}/adjust reads reduceOnly from you.

The response is an acknowledgement

202 Accepted returns an execution, not a position:

{
  "execution": {
    "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
    "connector": "hyperliquid",
    "intent": "OPEN",
    "orderType": "MARKET",
    "status": "IDLE",
    "legs": [
      { "symbol": "0", "side": "BUY",  "mode": "USD", "amount": "500", "reduceOnly": false },
      { "symbol": "1", "side": "SELL", "mode": "USD", "amount": "500", "reduceOnly": false }
    ],
    "createdAt": "2026-01-01T00:00:00.000Z",
    "updatedAt": "2026-01-01T00:00:00.000Z"
  }
}

legs echoes the legs you sent, with reduceOnly: false added.

The response carries no orders. No venue order exists yet. Orders appear on a later GET /executions/{id}, once the execution has placed them.

Watch the WebSocket for execution.completed, then position.created. See Error Handling for how failures surface.

Re-opening the same basket returns an ADJUST

Opening the same instruments on the same trade account again does not create a second position. The gateway finds the open position and adjusts it. The response changes shape: intent is "ADJUST", and an extra required position object carries the current exposure.

The match is made on the position key, a hash of the trade account and the legs' instruments. Side is not part of it, so an opposing basket on the same instruments also adjusts.

{
  "execution": {
    "id": "9b1f7c02-4c8a-4a0e-9a1e-6f0b5d2c7e31",
    "connector": "hyperliquid",
    "intent": "ADJUST",
    "orderType": "MARKET",
    "status": "IDLE",
    "position": {
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "exposure": { "0": "+0.0052", "1": "-0.1620" }
    },
    "legs": [
      { "symbol": "0", "side": "BUY",  "mode": "USD", "amount": "500", "reduceOnly": false },
      { "symbol": "1", "side": "SELL", "mode": "USD", "amount": "500", "reduceOnly": false }
    ],
    "createdAt": "2026-01-01T00:05:00.000Z",
    "updatedAt": "2026-01-01T00:05:00.000Z"
  }
}

Branch on execution.intent, not on the endpoint you called. Code that assumes intent === "OPEN" breaks on the second call.

Where weights are used

A market open sizes each leg directly, so it takes no weights. Weights appear where Pear has to split one number across the basket:

Weights are used byEndpoint
TWAP schedules, which spread one target notional over the legsPOST /schedules
Manual rebalancing, which resets the target shares of an open positionPOST /rebalance/manual/{positionId}
Weighted-ratio trigger conditionsPOST /triggers/open

In each of those, leg weights must sum to exactly 1. See Weighted Price Ratio.

Attach a take profit or stop loss at open

POST /trade/open accepts an optional trigger, which arms a close trigger the moment the position exists:

{
  "type": "MARKET",
  "legs": [
    { "symbol": "0", "side": "BUY",  "mode": "USD", "amount": "500" },
    { "symbol": "1", "side": "SELL", "mode": "USD", "amount": "500" }
  ],
  "trigger": {
    "intent": "CLOSE",
    "type": "MARKET",
    "bracketType": "TAKE_PROFIT",
    "condition": {
      "type": "upnl_bps",
      "data": { "priceSource": "mark", "threshold": 500 }
    }
  }
}

trigger takes exactly one object, not a list. To arm more brackets, call POST /triggers/close once the position exists.

Conditions you can attach at open

An attached trigger accepts a narrower condition set than POST /triggers/close. Only these six types are legal here. ratio, price, weighted_ratio, and prediction_market are rejected.

FormtypedataMeasures
StaticnotionalpriceSource, thresholdPosition notional in USD. threshold must be greater than 0.
StaticupnlpriceSource, thresholdUnrealized PnL in USD. threshold must not be 0.
Staticupnl_bpspriceSource, thresholdUnrealized PnL in basis points. threshold must not be 0.
Trailingnotional_trailingpriceSourcePosition notional, trailing its peak.
Trailingupnl_trailingpriceSourceUnrealized PnL in USD, trailing its peak.
Trailingupnl_bps_trailingpriceSourceUnrealized PnL in basis points, trailing its peak.

A static attachment carries bracketType, which is TAKE_PROFIT or STOP_LOSS, and puts the level in condition.data.threshold.

A trailing attachment carries a trailing object instead, and carries no bracketType. Its condition.data holds only priceSource:

{
  "trigger": {
    "intent": "CLOSE",
    "type": "MARKET",
    "condition": {
      "type": "upnl_bps_trailing",
      "data": { "priceSource": "mark" }
    },
    "trailing": {
      "mode": "RELATIVE_BPS",
      "deltaValue": 300,
      "activationValue": 500
    }
  }
}
trailing fieldRule
modeRELATIVE_BPS or ABSOLUTE_POINTS.
deltaValueGreater than 0. In RELATIVE_BPS it must be an integer from 1 to 5000. How far the metric may fall back from its best before the trigger fires.
activationValueOptional. The metric level at which trailing starts. Omit it to start trailing immediately. Must not be 0 for upnl_trailing and upnl_bps_trailing, and must be greater than 0 for notional_trailing.

See Take Profit / Stop Loss for the conditions available once the position is open.

On this page