Winding the futures desk down safely
How to close a futures desk in the order that protects traders' money — disable markets, drain open interest, then the extension row and a restart — and exactly what stops, what does not, and what survives.
Turning this addon on is one switch and a restart — Install and enable covers it. Turning it off is not one switch, and the order matters far more on the way down than it did on the way up. The thing you are switching off is the only process that marks leveraged positions, and every open position on your book has real customer money posted against it.
The extension row is the last step, not the first. Stopping the addon stops the matching engine and its two-second mark sweep, and a position that is not marked is a position where no stop-loss, no take-profit and no liquidation will ever fire, while its margin stays debited from the trader's FUTURES wallet. The market can move a hundred per cent against a 50x long and nothing will close it. Drain the book first.
The order to do it in
-
Stop new trading, market by market. Switch each market's status off from Futures → Markets (
/admin/futures/market), or withPUT /api/admin/futures/market/{id}/statusfor one market andPUT /api/admin/futures/market/statusfor a list of ids. Both take{ "status": false }and both neededit.futures.market.This — not the extension row — is the switch that actually refuses orders.
POST /api/futures/orderreads the market row and answers "Trading is disabled for BTC/USDT." with a 400. The market-data WebSocket also refuses new subscriptions for a disabled market. -
Tell traders, and let open interest drain. Every exit stays open deliberately: cancelling a resting order, closing a position, stop-loss, take-profit and liquidation all keep working on a disabled market. There is no admin force-close on this desk — you cannot end a position on a trader's behalf, so the only ways a position ends are the trader closing it or the engine closing it.
-
Confirm the book is empty. On the risk console dashboard (
/admin/futures), open positions and notional exposure should read zero. Cross-check/admin/futures/positionfiltered toOPEN, and the orders desk filtered toOPENfor anything still resting. -
Only now switch the extension off. System → Extensions (
/admin/system/extension), find Futures, toggle it off. This writesstatus = falseon thefuturesextension row and clears the settings cache. -
Restart the backend. This is the step that actually stops the engine.
pnpm restart
What each switch actually stops
The two switches do very different jobs, and confusing them is how a desk ends up with unmarked positions.
| Market status off | Extension row off | |
|---|---|---|
| New orders on that market | Refused (400) | Not refused — see below |
| New market-data subscriptions | Refused | Unaffected |
| Cancel a resting order | Still works | Still works |
| Close an open position | Still works | Still works |
| Stop-loss / take-profit / liquidation | Still runs | Stops at the next restart |
| The three cron jobs | Unaffected | Stopped within about a minute |
| Admin menu entries | Unaffected | Rendered disabled |
What stops when the extension row is off
The three cron jobs stop without a restart. The scheduler re-reads the
enabled-extension rows about every 60 seconds and deregisters the jobs of an
addon that is no longer enabled, logging
"Extension futures disabled — stopped cron …". All three go:
sweepFuturesPositions (the 60-second backstop sweep),
reconcileFuturesPositions and reconcileFuturesOrders (the 5-minute Scylla
replayers). They disappear from System → Cron.
The matching engine stops at the next restart, and not before. The engine is
started once, during boot, and only when the futures extension row is enabled
and the process is the main thread. There is no teardown wired to the toggle:
an already-running engine keeps its lease and keeps its two-second mark
sweep — the thing that re-marks every open position, enforces stop-loss and
take-profit, and liquidates — until the process is restarted. That is why step 5
exists, and it is also why the window between step 4 and step 5 is still a
working desk.
After the restart, nothing is scheduled to mark positions. No engine boots
at start-up, so no process holds the futures-matching lease and no 2-second
sweep exists; the 60-second cron backstop has been deregistered as well. The
only thing that can still arm an engine is a futures request happening to build
one lazily — which is a coincidence, not a plan. This is the intended end state,
and it is only safe because you drained the book in steps 1 to 3.
What the extension switch does not stop
Three things surprise people, and all three are reasons the market switch has to come first.
The trader API stays registered. Backend routes are registered by walking
the source tree at boot, not from the enabled-extension list, so
/api/futures/… still exists after the row is off and after a restart. The
licence gate covers /api/admin/futures only, and it grades the licence file,
not the row's status. Nothing in the request path consults extension.status
for these routes.
A request can revive the engine. GET /api/futures/ticker, the ticker
socket, an order cancellation and a position close all reach
FuturesMatchingEngine.getInstance(), which builds and arms an engine if the
process does not already have one — mark sweep included. Disabling the addon is
therefore not a kill switch for the engine; it only removes the guaranteed boot.
The admin screens stay reachable by URL. The Futures entries in the admin
menu carry extension: "futures", and the admin menu renders an item for a
disabled extension in a disabled state rather than hiding it — the link is
inert, but typing /admin/futures/market still loads, because the page's own
gate checks the licence, not the enabled row.
If you switch the extension off and leave the markets enabled, a trader who reaches the trading ticket can still place an order, and the odds that anything liquidates it depend on whether some request happened to re-arm an engine. Close the markets. Every time.
If you skip the wind-down
The failure is quiet, which is what makes it expensive. Positions sit OPEN in
Scylla. Their margin — entryPrice × amount ÷ leverage, plus the fee — is still
debited from the trader's FUTURES wallet, exactly where the placement debit put
it. Nothing re-marks them, so:
- stop-loss and take-profit never fire, however far past their trigger the price goes;
- liquidation never fires, so a position that should have been trimmed at 70% of margin lost and closed at 90% simply keeps existing;
unrealizedPnlon the row freezes at whatever the last mark happened to be, so the positions table shows a number that stopped being true when you restarted.
A trader can still close such a position manually. What they get depends on whether anything can still price the symbol: the close route asks the matching engine for the live ticker, and when there is no price at all it settles at the entry price — zero PnL, exactly the margin handed back. That is the honest answer when nothing has a price. It is not the answer a trader who was 40% up expects.
What survives, and what re-enabling gets you back
Switching off destroys nothing.
ScyllaDB keeps everything. The keyspace named by SCYLLA_FUTURES_KEYSPACE
(default futures) keeps its orders, position, orderbook and candles
tables and their materialised views, untouched. Every order, every position,
every resting book level and the whole candle series is still there.
MySQL keeps the market definitions. The only futures data in MySQL is the
futures_market table. Disabling a market sets status = false on that row; it
does not delete it, and the metadata blob — precision, limits, leverage rungs,
fee rates — is preserved exactly as configured.
So re-enabling resumes rather than restarts. Re-enable the extension row,
restart the backend, confirm the engine claimed the futures-matching lease,
then switch markets back on one at a time. The book you left is the book you get
back, including any depth still resting in it — which is worth checking before
you re-open a market that has been closed for a month.
The platform's built-in database backup covers MySQL only, and a wound-down desk does not change that. If you are shutting futures down for a long period, take your own Scylla backup while the data still matters to you. See Engine and storage.
Traders' FUTURES wallets are core, not part of the addon
This is the part operators worry about most, and it is the part that needs no action at all.
The FUTURES wallet type is a value of the core wallet.type enum, alongside
FIAT, SPOT, ECO and COPY_TRADING. The transfer route
(POST /api/finance/transfer) is a core route, not an addon route, and its
matrix is unchanged by anything on this page:
| From | May transfer to |
|---|---|
ECO |
FIAT, SPOT (when spot is enabled), FUTURES |
FUTURES |
ECO only |
So after the desk is closed, balances left in FUTURES wallets remain, and
remain transferable. A trader moves them out with a normal
FUTURES → ECO transfer; anything else is refused with "FUTURES wallet can
only transfer to ECO wallet". Nothing traps money in a FUTURES wallet because
the addon is off.
What you should do is tell them to. A balance sitting in a wallet type whose product has been retired is a support ticket waiting to happen.
Retiring a market is not deleting one
Everything above uses the status toggle. Resist the delete button.
The delete route forces a hard delete — the row is gone, there is no restore —
and 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. The full warning, including why the
cleanup step does not clear the orders and candles it claims to, is in
Futures markets.
The rule stands whether you are retiring one contract or the whole desk: switch markets off; delete only markets that never traded.
Bringing it back
-
Enable the extension row — System → Extensions, toggle Futures back on. The three cron jobs re-register on the next re-sync pass, within about a minute, without a restart.
-
Restart the backend so the matching engine boots and claims the
futures-matchinglease. Check the log: if you see "Ecosystem extension not available, futures matching engine disabled", the problem is Ecosystem or Scylla, not Futures. -
Verify before you re-open a market.
GET /api/futures/marketshould answer (it returns only enabled markets, so an empty array is correct at this point), and the engine must hold the lease. The install verification steps are the same ones that apply here. -
Re-enable markets one at a time, checking the book has usable depth before you let leveraged orders in. See Getting liquidity onto a futures book.