Take Profit / Stop Loss
Attach take-profit and stop-loss triggers to an open basket.
Arm a close trigger on an open basket with POST /triggers/close (SDK: sdk.core.triggers.close). It rests off-book and closes the whole position when the condition is met.
Request
Take profit at 5% unrealized PnL, measured in basis points:
{
"type": "MARKET",
"intent": "CLOSE",
"positionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"bracketType": "TAKE_PROFIT",
"condition": {
"type": "upnl_bps",
"data": { "priceSource": "mark", "threshold": 500 }
},
"clientId": "your-client-id"
}Stop loss at 300 USD of unrealized loss:
{
"type": "MARKET",
"intent": "CLOSE",
"positionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"bracketType": "STOP_LOSS",
"condition": {
"type": "upnl",
"data": { "priceSource": "mark", "threshold": -300 }
}
}Take profit and stop loss are separate triggers. Arm both to bracket a position.
The call answers 200 with the resting trigger. Nothing trades until it fires.
How many a position can hold
| Limit | Value |
|---|---|
| Active triggers per position | 5 |
| Trailing triggers per position | 1 |
A sixth is rejected with You cannot have more than 5 active triggers for a position. A second trailing is rejected with Trailing stop loss trigger already exists for this position. Ladder rungs do not count against the 5.
bracketType
bracketType is required on a static close trigger. It sets which way the measured value must cross threshold, which is why a close condition carries no track field. That is the difference from an open trigger, which does.
bracketType | Fires when the value |
|---|---|
TAKE_PROFIT | reaches or passes threshold from below |
STOP_LOSS | drops below threshold |
NONE | reaches or passes threshold from below, with no profit or loss label. Same crossing as TAKE_PROFIT. |
A trailing close omits bracketType.
On a price condition the labels invert for a short leg
For every condition type except price, bracketType is taken as your intent and stored as you sent it.
price is different. It watches one instrument's price, and a rising price is profit only if you are long that instrument. bracketType stays a crossing direction, so on the short leg of a pair the profit and loss labels swap:
| Watched leg | "TAKE_PROFIT" | "STOP_LOSS" |
|---|---|---|
| Long | Fires above threshold — your profit. | Fires below threshold — your loss. |
| Short | Fires above threshold — your loss. | Fires below threshold — your profit. |
To arm a price stop loss on the short leg, send bracketType: "TAKE_PROFIT". Send "STOP_LOSS" and the trigger sits on the wrong side of the market.
semanticBracketType
semanticBracketType is an optional field that records what you meant: TAKE_PROFIT or STOP_LOSS. It does not change which way the trigger fires. Pear derives your intent from bracketType and the watched leg's side, then checks your value against that derivation, so it is a guard against encoding the crossing backwards:
| Case | Result |
|---|---|
price condition, watched leg is long | Must equal bracketType. |
price condition, watched leg is short | Must be the opposite of bracketType. |
| Any other condition type | Must equal bracketType. |
bracketType: "NONE" | Must be omitted. |
The condition's symbol is not in the position, or that leg is flat | Must be omitted. The side cannot be proved, so nothing can be validated. |
A value that disagrees is rejected with semanticBracketType contradicts the bracketType encoding. Omitting the field is always valid.
Conditions
type | data | Closes on |
|---|---|---|
upnl | priceSource, threshold | Unrealized PnL in USD. |
upnl_bps | priceSource, threshold | Unrealized PnL in basis points of the position. |
notional | priceSource, threshold | The position's notional value. |
price | priceSource, symbol, threshold | One instrument's price. |
ratio | priceSource, symbol_a, symbol_b, threshold | The price ratio of two instruments. |
weighted_ratio | priceSource, basket, threshold | The basket's weighted ratio. |
prediction_market | source, marketKey, threshold | A prediction-market outcome price. |
priceSource is mid or mark. source is kalshi or polymarket. Every symbol is an instrument ID, and ratio needs two distinct ones.
weighted_ratio takes a basket of at least two entries, each { "symbol", "side", "weight" }. Symbols are distinct, side is BUY or SELL, and the weights sum to exactly 1.
threshold
| Condition | threshold |
|---|---|
upnl, upnl_bps | Signed. Negative marks a loss. Must not be 0. |
notional, price, ratio, weighted_ratio, prediction_market | Must be greater than 0. |
Trailing stops
Every condition above except prediction_market has a _trailing twin, which follows the position's best value instead of resting at a fixed level. A trailing close omits bracketType and adds a trailing object:
{
"type": "MARKET",
"intent": "CLOSE",
"positionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"condition": {
"type": "upnl_bps_trailing",
"data": { "priceSource": "mark" }
},
"trailing": {
"mode": "RELATIVE_BPS",
"deltaValue": 200,
"activationValue": 500
}
}That arms once the position is 500 bps up, then closes it if it gives back 200 bps from its peak.
| Field | Meaning |
|---|---|
mode | RELATIVE_BPS reads deltaValue as basis points. ABSOLUTE_POINTS reads it in the condition's own units. |
deltaValue | How far the value may retrace before the trigger closes the position. |
activationValue | Optional. The trigger stays dormant until the value reaches this level. |
The trailing condition has no threshold, because the level moves.
deltaValue rules:
| Rule | Applies to |
|---|---|
Greater than 0 | Every mode |
| A whole number | RELATIVE_BPS only |
Between 1 and 5000 | RELATIVE_BPS only |
Less than activationValue | notional_trailing in ABSOLUTE_POINTS mode, when activationValue is present |
activationValue rules:
| Rule | Applies to |
|---|---|
Must be greater than 0 | notional_trailing |
Must not be 0 | upnl_trailing, upnl_bps_trailing |
A trailing close is always recorded as a stop loss, whatever it tracks.
Arm at open time
To make sure a basket is never unprotected, attach the close trigger to the opening call instead. POST /trade/open takes a trigger, and POST /triggers/open and POST /ladders take attachTriggers. Attached triggers accept the notional, upnl, and upnl_bps conditions and their trailing twins. See Basket Trade.
What a fired trigger leaves behind
A trigger is spent the moment it fires, whatever the close it launched does next. Its status becomes TRIGGERED and it can never fire again — including when that close failed outright, or filled only part of the basket.
When the close does not empty the position:
- The position stays
OPEN, holding whatever the close left. - No
position.closedarrives. - The spent trigger cannot be cancelled.
PATCH /triggers/{triggerId}/cancelon it returns 409. - Your other triggers and schedules on that position stay armed. Pear cancels them only when the close actually closed the position.
Watch for a trigger.triggered that is not followed by position.closed. Read the leftover exposure with GET /positions and decide what to do with it.
What else cancels a close trigger
| Event | cancelledReason |
|---|---|
| The position closes | POSITION_CLOSED |
| The position is reversed, on any real fill, including a partial that did not flip it | POSITION_REVERSED |
| The position is netted away by another trade | POSITION_NETTED |
| You cancel it | USER |
Manage close triggers
Cancel a close trigger like any other, with PATCH /triggers/{triggerId}/cancel. Cancelling a trigger that is not ACTIVE returns 409. List them with GET /triggers. Fires stream over the WebSocket as trigger.triggered, then position.closed.
A condition that names an instrument the price oracles cannot resolve is rejected with 422. Use the ids from GET /instruments.