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:
| Field | Values | Meaning |
|---|---|---|
symbol | instrument ID | The instrument to trade. Not the ticker, see Instrument ID. |
side | BUY, SELL | BUY makes the leg long, SELL makes it short. |
mode | USD, QUANTITY | How to read amount. |
amount | decimal string | Notional 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.CreateTradeOpenRules the gateway enforces:
- At least one leg.
- Each leg names a distinct instrument.
amountis an unsigned decimal string. Direction comes fromside, never from a minus sign.- A leg on
/trade/opentakes noreduceOnly. The gateway sets it tofalseand drops the field if you send it. OnlyPOST /trade/{positionId}/adjustreadsreduceOnlyfrom 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 by | Endpoint |
|---|---|
| TWAP schedules, which spread one target notional over the legs | POST /schedules |
| Manual rebalancing, which resets the target shares of an open position | POST /rebalance/manual/{positionId} |
| Weighted-ratio trigger conditions | POST /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.
| Form | type | data | Measures |
|---|---|---|---|
| Static | notional | priceSource, threshold | Position notional in USD. threshold must be greater than 0. |
| 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 | notional_trailing | priceSource | Position notional, trailing its peak. |
| Trailing | upnl_trailing | priceSource | Unrealized PnL in USD, trailing its peak. |
| Trailing | upnl_bps_trailing | priceSource | Unrealized 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 field | Rule |
|---|---|
mode | RELATIVE_BPS or ABSOLUTE_POINTS. |
deltaValue | Greater 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. |
activationValue | Optional. 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.