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.

9 min readUpdated 6 August 2026shutdown, maintenance, markets, extension, liquidation

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

  1. Stop new trading, market by market. Switch each market's status off from Futures → Markets (/admin/futures/market), or with PUT /api/admin/futures/market/{id}/status for one market and PUT /api/admin/futures/market/status for a list of ids. Both take { "status": false } and both need edit.futures.market.

    This — not the extension row — is the switch that actually refuses orders. POST /api/futures/order reads 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.

  2. 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.

  3. Confirm the book is empty. On the risk console dashboard (/admin/futures), open positions and notional exposure should read zero. Cross-check /admin/futures/position filtered to OPEN, and the orders desk filtered to OPEN for anything still resting.

  4. Only now switch the extension off. System → Extensions (/admin/system/extension), find Futures, toggle it off. This writes status = false on the futures extension row and clears the settings cache.

  5. 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;
  • unrealizedPnl on 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

  1. Enable the extension rowSystem → Extensions, toggle Futures back on. The three cron jobs re-register on the next re-sync pass, within about a minute, without a restart.

  2. Restart the backend so the matching engine boots and claims the futures-matching lease. Check the log: if you see "Ecosystem extension not available, futures matching engine disabled", the problem is Ecosystem or Scylla, not Futures.

  3. Verify before you re-open a market. GET /api/futures/market should 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.

  4. 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.