The futures order desk

The read-only futures order table under Finance — why it is not in the Futures menu, what every column means, how to tell an engine liquidation from a trader order, and why every KPI on it is a row count.

9 min readUpdated 6 August 2026admin, orders, scylladb, fees, liquidation

Every futures order your desk has ever accepted is on one screen, and it is not the screen you would guess. This is the page you open when a trader asks why they were charged the taker rate, when you need to reconstruct what actually filled, or when you want to know whether a close was the trader's decision or the engine's.

Admin → Finance → Order Management → Futures Orders (/admin/finance/order/futures), permission access.futures.order.

Why it is not in the Futures menu

The addon ships a three-item menu — Dashboard, Markets, Positions — and orders are not on it. Nothing under /admin/futures links here, so an operator who navigates the addon never finds this table.

It lives under Finance instead, in the Order Management group, next to Spot Orders, Ecosystem Orders and Binary Orders. That is deliberate: it is one of several order books an operator compares side by side, and it is the only futures screen that behaves like the rest of the finance back office.

Two consequences worth knowing:

  • The menu entry is gated on the access.futures.order permission and on the futures extension. A role granted access.futures.market and access.futures.position — enough for the whole Futures menu — still cannot see or open this page.
  • In the admin menu, an item belonging to a disabled extension is rendered in a disabled state rather than hidden. So if Futures is switched off, the Futures Orders entry is still listed under Finance, greyed out, with an inert link.

What the screen is

A paginated, filterable, sortable table over the ScyllaDB orders table in the futures keyspace, served by GET /api/admin/futures/order (permission view.futures.order). Twelve rows a page, newest first.

It is view-only. No create, no edit, no delete — the screen declares all three off, and no admin write route exists behind it either. The header copy says "View and manage futures trading orders"; the manage half is not a thing. There is no admin action anywhere in this addon that cancels a trader's order or closes their position. What you can do is disable the market it trades on (see Futures markets).

Columns

Seven columns are on the row itself; the rest open with the row.

Column Shown Notes
Status Row OPEN or CLOSED — see below on CANCELLED
Symbol Row BASE/QUOTE, e.g. BTC/USDT
Order Type Row LIMIT or MARKET
Side Row BUY or SELL
Price Row Shown for limit orders. A market order stores the deepest level its book sweep touched — a per-unit cap, not a quote
Amount Row Contract size in the base currency
Leverage Row A plain integer — see below
ID, User ID Expanded Both are UUIDs
Time in Force Expanded, limit orders Always GTC — the writer hard-codes it, so the IOC value the column offers never appears on a row
Fee, Fee Currency Expanded Charged on the notional, always in the quote currency
Average, Filled, Remaining Expanded Survive on a closed row
Cost Expanded The margin reserved, not the notional
Stop Loss, Take Profit Expanded Trigger prices carried onto the position the fill creates
Trades Expanded The raw fill list, rendered as a fills table in the view dialog

Opening a row gives a six-section dialog — Pricing, Risk exits, Position, Fills, Order & account and Timeline — with the leverage and a Reduce only marker promoted to badges in the header, because those are the two facts you either read at a glance or not at all. Position is hidden on rows that predate its columns, and Fills on an order that never traded.

Reading a row

"Why was I charged taker on a limit order?"

Because it crossed. isTaker is decided once, at placement, from the resting book — not from the side of the order and not from its type — and the decision is stored on the row. A MARKET order is always taker. A LIMIT BUY is taker if its price is at or above the best ask; a LIMIT SELL is taker if its price is at or below the best bid. Anything that rests is maker.

In the view dialog this appears as Liquidity, spelled out as Taker or Maker. That stored value is what was charged, and every refund path reads the stored fee, so the debit and the refund can never disagree about which rate applied.

isTaker is null on rows the engine wrote (liquidations set no value) and on rows written before the column existed. Those count as neither taker nor maker in the analytics.

A completed order still tells you what traded

filled, remaining and average survive on a CLOSED order. Closing an order writes the status and leaves those three alone, so a finished row still says exactly how much of it traded and at what average price. average is 0 on an order that never filled.

cost is the margin the order reserved, not the notional — for a limit order, amount × price ÷ leverage. At 20x, a 1,000 notional order shows a cost of 50. A market order reserves against the dearest level its sweep could reach, and the difference between that and the price it actually got is released back fill by fill, so its cost reads slightly high until it is done.

The list route converts the fixed-point columns back to real numbers — amount, cost, fee, filled, price, remaining, stopLossPrice, takeProfitPrice — and average is not on that list. What reaches the Average field is the stored value, which is the real price multiplied by 10^18. Divide by 10^18 to read it, or take the fill prices from the Fills section instead. The same gap exists on the Ecosystem orders table.

Telling an engine liquidation from a trader order

A liquidation is placed as a real reduce-only order against the book, so it appears in this table alongside genuine trader flow. Three fields identify it together:

  • reduceOnly is true,
  • positionId points at the position being closed,
  • cost and fee are both zero.

The margin that order is closing has already been posted, so there is no wallet debit and nothing to refund — which is also why the trader is never charged for being liquidated. The order is a LIMIT on the opposite side, priced at the bankruptcy price (where the posted margin is exactly gone), and any part the book could not absorb is retired to CLOSED rather than left resting. See Liquidation for what the same event writes to the position row.

A reduceOnly order with a non-zero cost does not exist on this desk; if you ever see one, treat it as a data problem, not a trade.

Leverage is a plain integer

leverage is stored as a VARINT like everything else, but unlike every other numeric column it holds a plain integer — a 10x order stores 10, not 10 × 10^18. The list route deliberately excludes it from the fixed-point de-scaling it applies to amount, cost, fee, filled, price, remaining, stopLossPrice and takeProfitPrice.

If a display anywhere renders leverage as 0.00x, that display is de-scaling a value that was never scaled. The real figure is 1e-17 and the underlying row is fine.

The CANCELLED status is a filter with nothing behind it

Status offers three values — OPEN, CLOSED, CANCELLED — and the status pie chart draws a CANCELLED slice. Nothing in the futures backend ever writes it.

Cancelling a resting order deletes the row from the orders table (and decrements the order book level by the size still resting) in a single batch. An order that never rested — a market order, or a reduce-only liquidation — is flipped to CLOSED instead, so whatever did fill stays auditable. Either way, the CANCELLED bucket reads zero.

The practical consequence: a cancelled order is not in this table at all. If a trader says they placed an order you cannot find, "they cancelled it" is a real answer. The wallet side of that cancellation is still auditable — the refund carries an idempotency key of futures_order_<orderId>_cancel in the transaction table, and Engine and storage lists the rest of the keys.

The table filters against Scylla, which constrains what the toolbar can express.

  • Default sort is createdAt descending. The orders table is partitioned by userId, and Scylla will only order server-side when every partition key is pinned to an exact value — which the table's filters never do. In practice the sort is therefore done in the backend, over the whole matching set, and the requested page is sliced from the sorted result. Sorting is correct across every row, not just the visible page.
  • User ID is matched as a UUID prefix, not as a substring. A partial id is turned into a range query, so pasting the first block of a UUID works; a fragment from the middle does not, and a value that cannot be read as a UUID prefix answers 400.
  • An operator the builder cannot express is a 400, not a wider result set. That is on purpose: an unrecognised operator used to be dropped, which turned a user-scoped query into an unscoped one and showed every user's orders.

Because the sort happens after the read, the query behind this screen is an unbounded SELECT * … ALLOW FILTERING over the rows that match your filter, plus a count(*) beside it. On a busy desk with no filter applied, that is the entire order history — on every page of pagination. Filter to a user, a symbol or a status before you go browsing, and treat an unfiltered load on a large book as an expensive query against your cluster.

Why every KPI on this page is a row count

The analytics strip above the table shows counts, and only counts:

Card Counts
Total Futures Orders Every row in the window
Open Orders status = OPEN
Filled Orders status = CLOSED
Risk-Increasing Orders reduceOnly = false
Reduce-Only Orders reduceOnly = true
Taker Orders isTaker = true
Maker Orders isTaker = false

Plus three charts: the status distribution pie, Risk-Increasing vs Reduce-Only Flow and Taker vs Maker Flow over time.

That is not a design choice. This page runs against ScyllaDB, and the Scylla analytics path understands exactly one operation: count the rows whose column equals a literal value. Sums, averages and distinct counts are not available on it, so cost, fee and leverage cannot be aggregated here — there is no "total volume" or "fees collected" figure this page could honestly produce.

Money figures live on the futures dashboard (/admin/futures), which does its own scan of the position store and reports notional exposure, open interest per market and period volume — along with a truncated flag when the scan hit its cap. See Risk console.

Two of these counts are worth watching rather than glancing at. A sharp rise in Reduce-Only flow is the crowd de-risking, and it usually arrives just before the volatility your risk desk needs to be awake for. The taker/maker mix is both your fee revenue and the answer to whether the venue can hold a book without house quoting — see Getting liquidity onto a futures book.

A short reconstruction routine

When a trader disputes a trade:

  1. Find the order. Filter by their user id (paste the first block of the UUID) and the symbol. If nothing comes back and they insist they placed one, check whether they cancelled it — the row would be gone.

  2. Open the row. The header badges give you status, side, leverage and whether it was reduce-only. That last one answers "did I do this, or did you?" before you read anything else.

  3. Read Liquidity, in the Position section. Taker or Maker is the rate that was charged, decided from the book at placement.

  4. Read Filled / Remaining / Average. These are true on a closed order, so they settle "how much actually traded and at what price" without needing the position row.

  5. Cross-check the money in core. The placement debit, the fee and any refund are transactions on the trader's FUTURES wallet, keyed by the order id. The order table is the trade; the wallet is the money.