Ecosystem 6.4.4
Latest19 August 2026
This release has upgrade notes. Read them before updating — they describe behaviour changes that need your attention.
Ecosystem v6.4.4
Release Date: August 19, 2026 Tags: TRADING, ORDER-BOOK, MATCHING-ENGINE, PERFORMANCE, WEBSOCKETS, BUG-FIXES
Overview
A latency release with one order-book correctness fix in it, and the fix is the one to read first. Every backend restart, and every hand-over between engine processes, re-read the open orders back out of the ledger and added each one to its price level a second time — even though the aggregated book is durable and already counted them. Cancelling such an order removed one copy and left the other, so the ladder accumulated depth that nothing was behind and a spread that would not close. An audit of one live market found seventeen of these against two genuinely open orders.
The rest is speed, and it was measured on a live install rather than reasoned about. A trader's own Order History list read every order they had ever placed on that pair and converted seven values on every row — 787 ms for 3,666 rows, 2.3 s under load — on the same thread that runs the matching engine. Placement itself took 264 ms. The seven to ten seconds a trader waited for a market order to settle was the engine being starved by the trading panel's own reads, with no bots running at all.
Read Upgrade Notes before you update. Ghost levels already in your book are not retracted by updating, and order history is now a recency window.
Update Instructions
pnpm updatorRestart the backend afterwards. No database change and no migration. If your book already holds ghost levels there is one script to run first — see Upgrade Notes.
Requires Core v6.6.9.
Upgrade Notes
Ghost levels already in your book are not removed by updating
This release stops new ones forming. It retracts nothing that is already there, because nothing on the platform can tell a phantom level from real depth by looking at it — the repair has to be derived from the open orders.
The tell is depth at a price that never trades, or a spread that will not close. The existing rebuild tool reconstructs the aggregated book from the orders behind it; it is not new in this release and some operators will already have run it.
cd backend
node scripts/rebuild-eco-orderbook.mjs # dry run, every symbol
node scripts/rebuild-eco-orderbook.mjs BTC/USDT # dry run, one market
node scripts/rebuild-eco-orderbook.mjs BTC/USDT --execute- Run the dry form first and read what it reports. It touches no orders, no trades and no balances — only the aggregated depth.
- Restart the backend afterwards. This is not optional: the matching engine holds its own copy of the book in memory and keeps serving it until it restarts.
Order History is now the most recent 250 orders
This is a deliberate cap, not a repair, and it has a visible edge. The market-scoped history list previously returned every closed order on that pair; the all-markets list returned everything the driver's first page held. Both now stop at the newest 250, which is about twenty-five pages at the ten rows a page the panels show.
Open orders are never capped, in either list. A resting order holds the customer's money and both views are newest-first, so any recency window would hide the oldest resting orders from the one list a customer cancels from.
- The market-scoped list accepts
?limit=up to 1000 —GET /api/ecosystem/order?currency=BTC&pair=USDT&type=CLOSED&limit=1000— if support needs to look further back on one pair. - The all-markets list ignores
limitentirely. There is no way to page past 250 on that view. - On the market-scoped list the cap counts rows read before the open/closed filter is applied, so a customer with resting orders among their newest rows sees slightly fewer than 250 closed ones.
Book updates are now sent as one frame per burst
Depth frames were built and pushed by every writer inline — after each placement, each cancellation and each fill. They are now gathered for a fifth of a second and sent as one, and they are not built at all for a symbol nobody is watching.
The frames were always complete snapshots rather than differences, so collapsing a burst loses nothing a client can act on, and the frame that goes out is read after the burst and is therefore fresher than any it replaces. The market handler's own two-second refresh is unchanged and remains the backstop.
- Set
ECO_BOOK_FRAME_INTERVAL_MSin the backend environment to change the window, orECO_BOOK_FRAME_INTERVAL_MS=0to send every change immediately. - Restart the backend for a change to that value to take effect.
Added
A per-market order lookup for automated traders
Reading one order by its id alone could only be answered by filtering the account's entire order history, across every market it has ever traded, to return a single row. That is acceptable for a person and quietly ruinous for a bot, which asks the question once per working order on every tick, against the same database session everyone else's placements are queued behind.
- Added a lookup that names the market as well as the account, so the read covers one market's orders instead of every market's. The Trading Bot and AI Market Maker add-ons use it from their own releases; existing lookups are unchanged and still available.
Changed
Writing one price level no longer reads the entire book
Changing a level needs one number: what is resting at that one price. It was obtained by paging the whole aggregated book for the symbol — both sides, every level — converting every row, taking one value out and throwing the rest away. That happened inside the lock that serialises the level, on every order that rests: a manual placement, a trading-bot quote, a market-maker quote, and every re-adoption. Nothing it computed was ever wrong; the cost simply grew with the market's depth, and with how many makers were quoting into it.
- Changed a level write to read only that level. Its cost no longer depends on how deep the market is.
- Changed the display read that follows a level write to happen after the lock is released, and to be unable to fail the placement. The level is already written and durable at that point, so a failed read now logs and leaves the next update to carry the picture, rather than failing an order whose depth is already on the book. A failed level write still fails the placement, as it must.
Depth frames are gathered, and skipped when nobody is watching
Every placement, cancellation and fill read the book and pushed a frame, whether or not anyone was subscribed — and when nobody was, the delivery end discarded it. On a market two bots are quoting, that was the largest repeated read in the add-on, charged to whichever trader happened to be placing an order at the time. The read after a fill was worse still: it sat inside the engine's own lock, ahead of the next matching cycle and of every cancellation waiting behind it.
- Changed book frames to be collected over a short window and sent as one.
- Changed the engine to skip building a frame entirely when no client on that process is subscribed to the symbol's book — including a split deployment where the engine runs outside the process holding the sockets, where the two-second refresh has always been the delivery path.
- Changed the post-fill book read out of the engine lock. Candles, tickers and the stop monitor still fire for every symbol that traded.
Fixed
Restarting the backend inflated the order book
The aggregated book lives in the database; the matcher's queue of live orders lives in one process's memory. Those two have different lifetimes, and the code that repopulated the queue could not tell them apart. On a restart, on a lease hand-over between processes, or when a symbol fell out of the resident window, it read every open order back from the ledger and pushed each one through the same entry point a brand-new placement uses — so each order's size was written into its price level again, on top of the copy that was already there.
Cancelling one of those orders afterwards subtracted once. What remained was exactly one order's amount, resting at the price it had been quoted at, with no order behind it: liquidity a trader could see, aim at, and never fill.
- Fixed re-adopted orders being added to the aggregated book a second time. Only a newly placed order now contributes to a price level.
A trader's own order list starved the matching engine
The order list the trading panel refreshes on every order update read the caller's entire history on that pair, with no limit, and converted seven values of every row before returning any of them. That work runs on the single thread that also matches orders and settles wallets, so a trader placing an order queued their own fill behind their own history reads — the more they had traded, the longer they waited.
The measurements are from one live install, with no bots running: the same query over the same partition answered in 20 ms for the open list, which converts only the handful of rows that are still open, and 787 ms for the history list, which converts all 3,666. Under load — a fill settling at the same moment — those stretched to 1.3 s and 2.3 s, and the fill's own steps could only run in the gaps between them.
- Fixed the order history list reading an account's whole history on a pair. The read is now bounded by what is displayed — see Upgrade Notes for the cap and how to widen it.
- Fixed the all-markets history list converting every row of a page it then discarded most of.