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.
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.orderpermission and on thefuturesextension. A role grantedaccess.futures.marketandaccess.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:
reduceOnlyistrue,positionIdpoints at the position being closed,costandfeeare 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.
Filtering and search
The table filters against Scylla, which constrains what the toolbar can express.
- Default sort is
createdAtdescending. Theorderstable is partitioned byuserId, 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:
-
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.
-
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.
-
Read Liquidity, in the Position section. Taker or Maker is the rate that was charged, decided from the book at placement.
-
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.
-
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.