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.
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.
-
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.
-
Metadata — precision and limits. This is where the real configuration lives; each field is explained below.
-
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.