Orders and positions

How a futures order is validated, priced and matched, when it pays maker or taker, what a fill does to a position, and every way a position can end — close, stop-loss, take-profit or liquidation.

7 min readUpdated 3 August 2026orders, positions, matching, stop-loss, take-profit

A futures trade on this platform is two objects. The order is the instruction and lives in Scylla's orders table; it is what rests in the book and what gets matched. The position is the exposure it created, in the position table, and it is what carries margin, PnL and a liquidation price.

Understanding which of the two a screen is showing you saves a lot of confused support tickets — an order that is CLOSED is not a position that is CLOSED.

Placing an order

A trader opens /trade?symbol=BTC-USDT&type=futures, picks a leverage rung, an amount and (for a limit order) a price, and optionally sets a stop loss and a take profit. The ticket sends one request to POST /api/futures/order.

The route validates in this order, and stops at the first failure:

  1. Authentication and KYC — the caller must be signed in, and must satisfy the futures_trading KYC feature if you have gated it.

  2. The market exists and is enabled — a market whose status is off answers "Trading is disabled for BTC/USDT." Note that only entry is blocked; cancelling an order and closing a position stay open, so switching a market off never traps money.

  3. Side and typeBUY or SELL, LIMIT or MARKET. Nothing else. This is strict for a reason: the matcher only ever looks at those four literals, so anything else would be debited and then rest in the book forever, unmatchable.

  4. Amount — a finite number greater than zero, and inside the market's limits.amount.min / max. The limits describe the contract, so they apply to both sides.

  5. Price — required and positive for LIMIT. For MARKET the submitted price is ignored entirely; the order is priced off the book (below).

  6. Leverage — must be one of the rungs the market publishes. 1000x on a market offering "1,5,20" is rejected with the available list in the message.

  7. Cost limits — measured against the notional (amount × price), not against the margin.

  8. Balance — the FUTURES wallet in the quote currency must hold margin + fee.

Every one of those rejections comes back with its real status code and a message naming the thing to change. A 500 from this route means something genuinely broke.

How a market order is priced

There is no external price feed. A MARKET order is priced by walking your own resting book at the moment of placement, and that walk produces three different numbers that do three different jobs.

Number Taken from the sweep Used for
Per-unit cap The deepest level the sweep reaches Stored as the order's price; the matcher refuses to fill beyond it
Reservation price The dearest level the sweep touches Sizing the margin hold
Average price Volume-weighted average of the sweep The fee, and the cost limit check

If the book has no liquidity on the other side, or not enough of it, the order is rejected with 422 and a message telling the trader to reduce the size or place a limit order instead. It is never partially priced and never filled at an arbitrary price.

The per-unit cap matters more than it looks. The matcher only crosses when buy.price >= sell.price, so the cap is what stops a market buy sweeping a resting ask five times deeper than the depth it was priced against.

Maker or taker

The rate is decided by liquidity, not by side. At placement the route looks at the resting book:

  • A MARKET order always takes.
  • A LIMIT order that crosses the best opposing level takes.
  • A LIMIT order that rests makes.

The decision is made once and stored on the order as isTaker, along with the rate applied. Every refund path reads the stored value, so the debit and the refund can never disagree about what was charged. A resting buy far below the market is charged the maker rate, as it should be.

Both rates come from the market's metadata (maker and taker, as percentages). A 0% rate is valid configuration and is honoured; a missing rate is rejected, because a market with no fee configuration is a mistake, not a free market.

Matching

The engine matches on price crossing, serialised so two placements landing in the same tick cannot walk the same book snapshot.

The print is the resting side's price. For an ordinary limit pair that is the seller's ask: a buy that crosses gets the seller's price, and the difference between its own limit and the print is released back to it (see Leverage and margin). When the seller is the one taking liquidity — a market sell, or a reduce-only liquidation — the print is the resting bid instead, so a liquidation cannot hand the whole spread to whoever happened to be resting.

Orders that never rest in the book — market orders and liquidations — are retired rather than cancelled when they do not fully fill. They are flipped to CLOSED with whatever filled recorded, and the book is left alone, because subtracting them from depth they never contributed to would silently delete another trader's resting size.

Cancelling

DELETE /api/futures/order/{id}?timestamp=... cancels a single open order; DELETE /api/futures/order/all cancels every open order for the caller.

Two things a cancel gets right, and it is worth knowing why:

  • The book is decremented by what is still resting, not by the order's original size. Every fill has already taken its own share out of the level.
  • The refund is proportional: (cost + fee) × remaining ÷ amount. Cancelling the remainder of a half-filled order returns half the margin and half the fee, not all of it. The refunded share of the fee is written back out of platform revenue as an offsetting entry, so cancelled orders do not inflate your reported income.

Refunds always land in the quote-currency FUTURES wallet, whichever side the order was.

From fill to position

When two orders match, each side's position is updated.

A new position records the price the trade actually filled at as its entry — never the limit the order carried. A buy limit at 110 that crosses a resting ask at 100 opens at 100.

An existing position on the same symbol and side is added to, and its entry price is re-weighted by the fill:

newEntry = (oldEntry × oldAmount + fillPrice × fillAmount) ÷ (oldAmount + fillAmount)

A position on the opposite side is left completely alone. This desk is hedge mode. Placing a sell while holding a long opens a second, independently margined short; it does not reduce the long. Traders arriving from a netting exchange will expect otherwise, and the position list marks every row mode: HEDGE so the UI can say so.

The life of a position

A position row carries: symbol, side, entry price, amount, leverage, unrealised PnL, an optional stop-loss and take-profit price, and a status. Status is one of exactly three values — OPEN, CLOSED, LIQUIDATED. There is no PARTIALLY_LIQUIDATED.

Every open position is re-marked against its market's live ticker on a two-second sweep, plus a 60-second cron backstop. On each pass, for each position, in this order:

  1. Stop loss / take profit are checked first. If the same print reaches both — a gap straight through the range — the stop wins, because that is the side the trader asked to be protected from.
  2. If it survived that, liquidation is evaluated.

A market whose ticker has never printed has a last price of zero, and those positions are skipped rather than marked at zero — otherwise every position on a brand-new market would read as a 100% adverse move and liquidate instantly.

Closing manually

DELETE /api/futures/position/{id} with the currency, pair and side closes an open position at the live ticker price, falling back to the entry price (so, zero PnL) when the engine has no price at all.

The payout is isolated margin:

margin = entryPrice × amount ÷ leverage
pnl    = (mark − entry) × amount     for a long
         (entry − mark) × amount     for a short
credit = max(0, margin + pnl)

There is no branch that debits on close. A loss is already bounded by the posted margin; taking more would reach into money the position never held.

Closing on a stop or a target

Identical arithmetic, run by the engine's sweep rather than by a request, and keyed for idempotency off the position id so a replay cannot pay twice. The position goes to CLOSED and the trader is credited max(0, margin + pnl).

Closing writes the status only. The row's amount still shows the size the position had, which is what makes the admin table readable as history. A position that was liquidated against the mark, by contrast, has its amount written to zero. That difference is the quickest way to tell the two endings apart in the positions table.

Being liquidated

The engine's own ending. It behaves differently enough to deserve its own page — see Liquidation, which is precise about what each variant writes to the row.

Order statuses

Status Meaning
OPEN Resting in the book, or partly filled with a remainder still resting
CLOSED Fully filled, cancelled by the user, or retired without ever resting

Filled quantity, remaining quantity and the average fill price are stored on the row, so a CLOSED order still tells you exactly how much of it traded.