PhoenixPerpetualsDocs API v1
Get an API key

Trading

Orders⁠.

Every order type, how the engine fills it, time in force, sizes and prices, and how to never place an order twice.

View as Markdown

On this page
  1. Order types
  2. Sizes and prices
  3. Time in force
  4. Slippage
  5. Protecting a position
  6. Never place an order twice
  7. Statuses
  8. When the engine restarts

An order is one POST to /v1/accounts/{account_id}/orders. This page explains what each field does and, just as important, exactly how the engine fills what you send. A bot that knows the fill rules never gets surprised by them.

Order types#

typeWhat it doesHow it fills
marketTrades nowThe whole size at the ask (buy) or the bid (sell)
limitWaits for a better priceAt your limit, once the last price trades strictly through it
stop_marketWaits for the price to break a level, then tradesAt the ask plus one tick (buy) or the bid minus one tick (sell) once the last price reaches the stop
stop_limitLike a stop, but never worse than your limitAt the last price, once it is at or through the stop and strictly better than the limit
take_marketTakes profit at a level, then tradesLike stop_market, on the profit side
take_limitTakes profit at a level, never worse than your limitLike stop_limit, on the profit side

Two facts shape everything else:

  • Fills are whole. An order fills completely or not at all. There are no partial fills, so filled_size is 0 or the full size.
  • A limit already through the market fills at once, at the ask or bid, which can be worse than your limit if the limit sits inside the spread. The API refuses that case with limit_inside_spread instead, so you never pay more than you meant to. Send post_only: true to be refused whenever an order would fill at once.

Sizes and prices#

Sizes are in the base asset (BTC for BTC-USD) and must be a multiple of the market's size_step. Prices must be a multiple of its tick_size. Both are in GET /v1/markets. A size off the step is refused with invalid_size_step, and the error's details.nearest gives the two valid sizes around yours.

Send notional_usd instead of size to size in dollars: the API rounds down to the step.

Prices, sizes and money are decimal strings ("62500.00"), never floats, so nothing is lost to rounding.

Time in force#

TypeChoicesDefault
marketioc, fokioc
limitgtc, gtd, ioc, fokgtc
stops and takesgtc, gtdgtc

gtd needs expires_at, in Unix milliseconds. Because fills are whole, fok and ioc behave the same.

Slippage#

A market order never fills worse than its worst price. Set it in percent with max_slippage_pct, or as a price with worst_price. Without either, the market's default applies; GET /v1/markets shows it with its bounds. When the market moved past your worst price, the order is refused with slippage_exceeded and nothing fills.

Protecting a position#

Attach exits to the order itself, and they are placed the moment it fills:

{
  "market": "BTC-USD",
  "side": "buy",
  "type": "limit",
  "size": "0.010",
  "limit_price": "62500.00",
  "take_profit": { "price": "64000.00" },
  "stop_loss": { "price": "61800.00", "trailing": { "distance": "350.00" } },
  "break_even": { "trigger_price": "63100.00", "offset_ticks": 2 }
}

The exits live on our servers as reduce-only orders: when one fills, the other is canceled, and they shrink if you reduce the position. They keep protecting you if your bot goes offline. Change them later with PUT .../positions/{market}/exits.

Never place an order twice#

Send an Idempotency-Key header with every order: any unique string, like a UUID. If the connection drops and you retry with the same key and the same body, you get the first order back instead of a second one. The key is remembered for 24 hours. The SDKs do this for you.

You can also give the order your own client_order_id and find it later with GET .../orders?client_order_id=, even from another process.

Statuses#

statusMeaning
pendingAccepted by the API, on its way to the engine
workingResting on the engine, waiting for its price
filledDone. average_fill_price and fee are set
canceledCanceled; cancel_reason says why
rejectedRefused; reject.code says why
expiredAn ioc, fok or gtd order that ended unfilled
unconfirmedThe engine's answer was lost on the way. Read the order again in a second: it settles to one of the above

A market order's answer waits up to two seconds for the fill, so you usually get filled straight away.

When the engine restarts#

If the trading engine restarts, it cancels every working order with cancel_reason: system_restart, and positions it held are closed. The API tells you on the account stream and your webhooks, and places nothing back by itself: your bot decides what to re-place.

Guides, endpoints, fields and error codes.