Futures markets

Creating a perpetual contract from two Ecosystem tokens — the three-step wizard, what every precision, limit and fee field does to a live order, how to retire a market safely, and why deleting one is not it.

5 min readUpdated 3 August 2026markets, admin, configuration, leverage

A futures market is one row in the MySQL futures_market table: a base currency, a quote currency, three display flags and a JSON metadata blob. Everything a trader can and cannot do on that contract is in the metadata, so this is the most consequential screen in the addon.

Admin panel → Futures → Markets (/admin/futures/market), permission access.futures.market.

Both sides must be active Ecosystem tokens

A market pairs two tokens from the Ecosystem token list. The create route looks each one up by id and requires status: true; a missing or disabled token is a 404, and a currency/pair combination that already exists is a 409. The pair is unique at the database level too.

If the token dropdowns are empty, the problem is upstream: you have no active Ecosystem tokens, and Futures cannot invent one.

The three-step wizard

Create Market at /admin/futures/market/create walks three steps. Nothing is saved until you submit the third.

  1. Basic info — pick the base token (the currency) and the quote token (the pair), and set the Trending and Hot flags. Both flags are purely presentational: they surface the market on the public list. Both default to on, which is rarely what you want for a market you are still testing.

  2. Metadata — precision and limits. This is where the real configuration lives; each field is explained below.

  3. Fees — the taker and maker percentages.

A market is created with status: true, so it is live to traders the moment you submit. If you are not ready, switch it off from the markets table immediately after creating it.

Precision

Field Default in the wizard What it controls
precision.amount 8 Decimal places on contract size, in the base currency
precision.price 6 Decimal places on price — and the rounding applied to every margin calculation

Price precision is not cosmetic. The margin an order posts is rounded to it, and an order whose margin rounds to zero is rejected outright. If you set a coarse price precision on a market with a very small minimum amount, small orders become unplaceable with a message about precision that will not obviously point back here.

Limits

{
  "amount":   { "min": 0.001, "max": 10 },
  "price":    { "min": 0.01,  "max": 0 },
  "cost":     { "min": 10,    "max": 0 },
  "leverage": "1,2,5,10,20,50"
}

amount bounds the contract size in the base currency. Both bounds apply to both sides — they describe the contract, not the direction. A max of 0 means no maximum.

price bounds what an order may be priced at, in the quote currency. The minimum is checked against the deepest level a market order's sweep reaches; the maximum against the dearest one. These are your protection against a fat-fingered limit order becoming the mark for the whole book — see Funding and the mark price.

cost bounds the notional (amount × price), not the margin. A cost.min of 10 on a 20x market means an order of 10 quote-currency notional, backed by 0.5 of margin — not a minimum stake of 10. This is the field operators most often misread.

leverage is a comma-separated list of the exact rungs offered, rendered as the slider in the trading ticket and enforced at placement. An order at a leverage not on the list is rejected with the available values in the message. An empty string leaves the ticket falling back to a default and removes the server-side check, so always set it explicitly.

Every rung you publish is a promise that a liquidation on that market can be traded out. At 100x a 0.9% adverse move liquidates, and if nothing is resting inside that band the shortfall is the platform's. Match the top rung to the depth the market actually has, not to what competitors advertise.

Fees

Two percentages, taker and maker.

  • Taker is charged when the order takes liquidity — every market order, and any limit order that crosses the book.
  • Maker is charged when the order rests. The engine decides this from the book at placement, not from the side of the order, and stores the decision on the order row.

Both are charged on the notional. At 20x a 0.05% fee is a full 1% of the margin the trader posted.

taker: 1 and maker: 1 are the pre-filled values, and 1% of notional is extremely high — at 50x it is half the posted margin on the round trip. Set both deliberately. A 0 is valid and is honoured; a missing rate makes the market unusable, because the order route refuses to price against absent fee configuration.

Editing a live market

/admin/futures/market/[id] edits the trending and hot flags and the whole metadata blob. The currency and pair cannot be changed — they are the market's identity.

Changes take effect on the next order. Nothing recalculates:

  • Existing positions keep the leverage they were opened at, including a rung you have just removed.
  • Existing positions keep their entry price and therefore their liquidation price. Changing precision or limits does not move them.
  • Resting orders are not re-validated against new limits. They stay in the book until filled or cancelled.

The markets table also has toggles for status, trending and hot, and supports bulk status updates.

Retiring a market: switch it off

Setting a market's status off is the safe, reversible way to wind a contract down.

What stops: new orders. POST /api/futures/order answers "Trading is disabled for BTC/USDT.", and the market-data WebSocket refuses new subscriptions for it.

What keeps working, deliberately: cancelling resting orders, closing open positions, the mark sweep, stop-loss and take-profit, and liquidation. Disabling a market must never trap a trader's money, so every exit stays open.

So the wind-down is: switch the market off, let open interest drain, then decide whether the row needs to exist at all.

Deleting a market

The delete route forces a hard delete even though the table supports soft deletes, so the market row is gone — there is no restore. Its cleanup step cancels and refunds resting orders and clears candles and order book levels, but it does not touch the position table. Every open position on that symbol survives with no market behind it: no ticker, so the mark sweep skips it, so it can never be liquidated or stopped out. Traders can still close such a position manually, at the entry price, but nothing else will ever end it.

Compounding that, the cleanup is handed the market's base currency where the Scylla queries expect the full BASE/QUOTE symbol, so in practice it matches nothing and the orders, candles and book levels are left in place too.

The rule that follows is simple: switch markets off; delete only markets that never traded. Confirm open interest is zero on the risk console before you delete anything.

A sensible starting configuration

For a first market on a book you are still bootstrapping:

{
  "precision": { "amount": 6, "price": 2 },
  "limits": {
    "amount":   { "min": 0.001, "max": 5 },
    "price":    { "min": 0.01,  "max": 0 },
    "cost":     { "min": 20,    "max": 100000 },
    "leverage": "1,2,5,10"
  },
  "taker": 0.075,
  "maker": 0.025
}

Low rungs, a real cost.max so one order cannot become the whole book, and fees that are recognisable to anyone who has traded elsewhere. Raise the leverage only once the order book has depth inside the liquidation band — the Leverage and margin table shows how narrow that band gets.