đźš§ SportsPerp is currently live on devnet. Mainnet target: before Jun 12, 2026 (World Cup kickoff).
TradingStop Loss & Take Profit

Stop Loss & Take Profit

Stop-loss (SL) and take-profit (TP) orders close an existing position when the oracle price crosses a threshold. Both are reduce-only by protocol construction.

When they fire

OrderPosition directionFires when oracle is
Stop LossLongBelow the trigger price
Stop LossShortAbove the trigger price
Take ProfitLongAbove the trigger price
Take ProfitShortBelow the trigger price

The condition is evaluated against market.oracle_price — the raw oracle, not the mark EMA. (The EMA is only used for liquidation eligibility; triggers must fire on a price every keeper can read deterministically from a single account snapshot.) See Mark Price → What each instruction prices against.

Placing an SL or TP

The SDK builder signature:

await client.placeTriggerOrder({
  marketId,
  orderType: OrderType.StopLoss,     // or OrderType.TakeProfit
  direction: Direction.Long,         // the direction of the position being protected
  triggerPrice: new BN(450_000_000), // price in 10^6 fixed-point
  size: position.size,               // typically the full position; partial close also allowed
  reduceOnly: true,                  // required to be true for SL/TP
  expiry: 0,                         // 0 = never expires
});

At placement time:

  • The position must exist and be owned by the caller.
  • reduce_only must be true — the program rejects false for SL/TP.
  • The size parameter must not exceed the current position size (but may be smaller for partial-close triggers).
  • No collateral is reserved — SL/TP are closing orders, not opening orders. The original position’s collateral is what will be unlocked.

Execution

When the keeper detects that the oracle price has crossed the trigger, it calls execute_trigger_close:

  1. Pre-flight — the position still exists; the trigger condition is still satisfied at the current oracle price; the market is not paused; the oracle is not stale.
  2. Funding settlement — any pending funding is applied against the position’s remaining collateral before the close.
  3. Close at oracle — PnL is realized against the current oracle price (not the trigger price). This prevents “sticky” execution during fast markets — the keeper fills at whatever the oracle reads when the tx lands, which is the economically correct price.
  4. Fee — taker fee (10 bps) deducted from realized proceeds.
  5. Keeper reward — a small SPL transfer to the keeper’s signer account, paid from the trader’s collateral. This is what incentivizes third-party keepers to run.
  6. Order closes — the TriggerOrder PDA is closed, rent refunded to the trader.

Why execution price ≠ trigger price

The trigger price is a condition, not a promise. A SL at 450 says “close when oracle reaches 450”; the actual execution happens on whatever block that condition first becomes true. In a gapping market, the oracle could be 448 when the keeper transaction lands — so the trader’s realized PnL reflects 448, not 450.

This is identical to how stop orders behave on every major venue. Traders should not set SL triggers at critical price levels (e.g., exactly at the liquidation price) and expect protection — slippage is a fact of fast markets, not a bug.

Laddered TP / trailing SL patterns

The 10-orders-per-market cap enables common patterns:

  • 3-rung TP ladder: place three TP orders at 1.05Ă—, 1.10Ă—, 1.20Ă— entry, each sized at 1/3 of the position. Lock in progressively better prices as the market moves in your favor.
  • Trailing SL (manual): cancel and re-place the SL order as the position moves — the cancel returns the rent, the re-place consumes a new slot. (A native trailing-SL order type is a potential v2 addition.)
  • SL + TP bracket: place both simultaneously. Whichever triggers first closes the position; the other can be left on-chain and will fail a no-op execution attempt safely (the keeper checks position-existence pre-flight).

Cancellation

A user calls cancel_trigger_order with the order index. This:

  • Closes the TriggerOrder account.
  • Refunds the rent exemption back to the signer.
  • Does not touch the underlying position.

Cancellation is instant (single transaction) and can be done at any time as long as the order hasn’t already executed.

Expiry

SL and TP orders can be given a Unix-timestamp expiry. After expiry:

  • The order is no longer eligible for execution by any caller.
  • The permissionless cancel_trigger_order becomes callable by anyone — the user’s rent is refunded, and the slot freed.

In practice, most traders set expiry = 0 (never) for SL/TP since the orders are cheap to hold and only fire if meaningful.

Edge cases

  • Position fully closed before trigger fires. If the trader manually closes the position or it gets liquidated, any outstanding SL/TP becomes inert. On the next keeper attempt, the position-existence check fails and the order is a no-op (the keeper doesn’t pay gas for a revert).
  • Trigger already true at placement. The program accepts placement regardless of current price — the next keeper cycle will execute it promptly. This is useful for “close at next bar” setups.
  • Partial close via trigger. Setting size < position size turns the trigger into a partial close. The position persists with reduced size; any other triggers on the same position are unaffected.

Further reading

  • Order Types — all four types side-by-side.
  • Limit Open — the opposite flow: open when price reaches target.
  • Mark Price — how the trigger condition is evaluated.