MMashDiv

Bicrypto 6.7.7

10 September 2026

This release has upgrade notes. Read them before updating — they describe behaviour changes that need your attention.

TRADINGORDERSORDERBOOKCHARTSMARKET DATAWEBSOCKETSECOSYSTEMFUTURESHUMMINGBOTI18N

Core v6.7.7

Release Date: September 10, 2026 Tags: TRADING, ORDERS, ORDERBOOK, CHARTS, MARKET DATA, WEBSOCKETS, ECOSYSTEM, FUTURES, HUMMINGBOT, I18N

Overview

A trading-screen release. Four things reported on one market — a chart that never moved, a cancelled order that would not leave the open-orders list, a "Cancel All" that had to be pressed twice, and depth bars that rendered as stubs — were four separate defects, and fixing them found the candle data underneath was being written wrong as well.

Reported volume falls, and your stored candles need checking. Volume counted an order's whole size again on every cycle it filled in, so an order filling over five cycles added five times its size to the day. And every interval above one minute was bucketed in the box's local time rather than UTC, so a "5m" series was really a 1-minute series wearing another name. Both are repaired going forward; neither repairs the rows already stored, and there is a new tool for those.

If you run the Hummingbot bridge, redeploy the connector. A cancel of an order the platform had already cancelled was answered in words the connector could not read as finished, so a strategy re-issued it every tick.

It is all in Upgrade Notes; read it before you update.

Update Instructions

pnpm updator

There is no schema change in this release — no new table, no seeder, and nothing to run by hand. Both halves matter: the matching engine, the candle writers and the WebSocket relay are server-side and need the backend restart; the order book, the orders panel and both charts live in the browser bundle and need the frontend rebuild.

  • Redeploy the Hummingbot connector if you run it. The cancel back-off and the "already gone" handling are in the connector's own Python, and the backend half alone does not deliver them.
  • After the restart, run the candle doctor once per active market — see Upgrade Notes. It is read-only until asked otherwise.

Upgrade Notes

Your stored candle series carry two defects, and they do not repair themselves

Both writers are corrected in this release, but a candle is a stored row and nothing in the update rewrites history.

Bucket boundaries. The engine bucketed a timestamp by looking at the interval's LAST CHARACTER only, using local-time helpers. So 3m, 5m, 15m and 30m all bucketed to the minute; 2h, 4h, 6h and 12h all bucketed to the hour; 1d began at local midnight and 1w on local Monday. On a UTC+3 install the day candle started at 21:00 UTC. Every other participant — the read path, the shard projector, the AI market maker — already used UTC buckets, so the engine was filing rows under keys nobody else looks them up by, leaving two rival rows per period on every interval except 1m.

Volume. The engine added the order's full original size once per cycle per symbol, rather than the amount that actually traded. An order of 10 that filled across five cycles added 50 to the day's volume.

What to do. Run the new doctor against each active market. It is read-only and reports off-boundary rows, duplicate buckets, price outliers cross-checked against ecosystem.trades, and how far each interval trails the tape:

pnpm eco:candle:doctor -- --symbol BTC/USDT

Add :fix to act on what it found. It repairs one thing and refuses the rest: a row that is both off its interval boundary and shadowed by an on-boundary row for the same bucket is merged into that sibling and then deleted. It will not touch a price outlier, a lone off-boundary row, or anything the tape confirms really traded — those are printed for a person to decide about. A trade that genuinely happened at an odd price is market data, and deleting it is falsifying history.

Reported volume falls, and the old figure was the wrong one

Because of the defect above, 24-hour volume, market lists and every figure derived from candle volume have been overstated on ecosystem markets — by more the longer orders took to fill. From this release they carry the amount that actually traded.

Nothing is lost and no trade has been removed; the number was double-counting. Expect the drop, and do not read it as a fall in activity. Rows already written keep their inflated volume until the interval rolls off your retention window.

A chart that showed a long flat run will now show a gap instead

The read path invented flat bars forward from the last real close to fill a gap. Past a threshold it now stops and returns nothing rather than drawing hundreds of them.

This is the fix for the reported chart whose y axis ran -30000 to 110000 around a single "Low 100.00" while the market traded at 78,000: the visible damage was never that one bar, it was the plateau extended forward from it. A gap where the platform has no data is the honest picture, and the chart's own live-edge test then reports the series as behind, which it is. If you see one, the doctor above will tell you whether anything was written for that period at all.

Hummingbot operators: the connector half is not optional

A cancel for an order the platform had ALREADY cancelled was answered "Open order not found" — wording deliberately chosen so a connector cannot read it as proof an order is finished, which is right for an id nothing can describe and wrong for a row we are looking at that says CANCELED. The connector's already-gone matcher did not match it, so the strategy re-issued the cancel every tick until its own status poll caught up.

Measured over a 37-minute XEMM run on a customer's staging box: 3,127 cancel requests for 571 orders — 2.75 cancels for every placement. Of 40 sampled orders answered "not found", 37 were CANCELED and one was filled. That traffic drew 1,388 refusals from the per-IP rate cap, and a refused cancel waits 1-20 seconds before the next try — so the cancels that were delayed were the ones that mattered. Median order lifetime ran from 4.1s in the calm opening minute to 47-297s through the refusals, and 1,212 placements were refused for insufficient funds held inside those long-lived orders.

What to do. Redeploy the connector alongside the backend. Both doors are changed and they work together: the backend now says "no longer open" in wording the connector ends on, and the connector no longer spends one cause's retry budget on another's failures.


Added

  • Added pnpm eco:candle:doctor, read-only forensics for one market's candle series: rows filed off their UTC interval boundary, duplicate buckets, price outliers each cross-checked against ecosystem.trades, and how far the newest candle of every interval trails the tape. pnpm eco:candle:doctor:fix performs the one repair it is willing to make by itself — merging an off-boundary row into the on-boundary sibling that shadows it, then deleting it. Everything else is printed for a person.
  • Added a periodic candle snapshot to the futures market feed. The only producer of a futures candle was the engine's per-fill push, so a chart on a quiet market sat still and a single frame lost to a reconnect was lost for good. The feed now re-reads the store on its own clock, the way the ecosystem chart always has.
  • Added the market symbol to every ohlcv frame both market feeds send, so a chart can tell one market's bars from another's on a shared connection.
  • Added a line under Taker connector in the Hummingbot strategy studio explaining that _paper_trade on that field rehearses only the hedge — the maker leg still places real orders — which is a different thing from an instance's Paper trading switch.

Changed

  • Changed the order book's third column to carry its unit. On the cumulative basis it is the running base size, on the other it is this row's price × amount: two quantities in two different units under one heading, which read as though the book had re-sorted itself when the toggle was pressed. Each now names its own currency, the way price and amount already did, and the heading reads Total or Value to match the terminal.
  • Changed the sweep readout and the ladder tint to address a level by its distance from the touch of the book rather than by price, so a level being consumed while the pointer is on it is no longer an event either has to survive.
  • Changed the WebSocket relay so it carries broadcasts between any two backend processes, not only from the scheduler to the web process. It publishes a frame only while another process has announced that it holds a socket for that route, so on a single-process install nothing is ever published and the cost is one map lookup.
  • Changed an unreachable matching shard's answer to a cancel from a 200 carrying deferred: true to a 503 with a Retry-After. deferred means an intent is recorded and will be served; nothing was recorded in this branch and nothing ever would be, so a client that correctly read the boolean waited for work that did not exist.
  • Changed the connector's cancel retry budget so each cause keeps its own tally and its own back-off ramp. A cancel refused three times by the rate limiter and then answered "not yet applied" arrived with the short budget already spent, and was abandoned without waiting even once for the deferral that was about to clear on its own — 82 cancels were given up on that way in one 37-minute run, each leaving an order resting on the book holding funds.
  • Changed the connector's log line for a deferred cancel from an error telling the operator to go and cancel by hand to a warning saying what actually happened. The cancel is written to a durable queue and applied by the process holding the matching lease; the previous wording sent operators hunting for orders the platform was in the middle of cancelling, 82 times in one run.

Fixed

The chart never moved on its own

The realtime handler took data[0] from each frame under a comment calling it "the latest". It is not: that stream carries a snapshot of the last ~100 bars in chronological order, sent every couple of seconds, so [0] was the OLDEST bar in the window — up to a hundred intervals stale. The chart ignores any bar older than the one it is drawing, so every realtime frame was discarded and the chart only ever moved when something forced a refetch. The reported symptom was a still chart beside a live Trades tab.

  • Fixed the realtime bar to be chosen by maximum timestamp, which is correct for both shapes the stream carries and cannot be broken again by a producer that reorders its window.

A cancelled order stayed in the open-orders list until the page was reloaded

The cancel paths ended at the engine's cancellation handler, which refreshes the BOOK and kicks the matching cycle and says nothing about the order itself. The panel removes a row on any non-live status — it was simply never told. Pressing X again made it worse: the second request answers 400 Order is not open, and the failure branch reported an error and skipped the refresh, so the stale row was pinned there however many times it was pressed.

That this was an omission and not a design is settled by the paths that already did it correctly: a shard emits CANCELED through the same funnel, and so does every stop-order transition. Only the classic limit-order cancel was silent.

  • Fixed all three ecosystem cancel doors and the futures cancel door to emit the owner's terminal frame after the money has moved and the row is marked — so it can only ever describe a cancellation that actually happened — and before the engine is told, so an engine that fails to refresh a ladder cannot also cost the trader the one frame that clears their row.
  • Fixed the panel to treat "already cancelled", "already filled" and "position already closed" as the satisfied intent they are: the list refreshes instead of reporting a failure.
  • Fixed the panel reading only the HTTP status. This stack answers { statusCode, message } inside a 200, and these handlers use bare fetch, so a genuine failure carried in the body read as success — no error banner, and the list refetched as though the row had gone.

"Cancel All" appeared to need pressing twice

A cancel that arrives at a process which does not hold the matching lease is recorded and served by the leaseholder a moment later, and the route says so in its body rather than its status. The panel tested only "did the request return something", so a deferred cancel counted as done and the list was refetched immediately — before the leaseholder had written the status. The row came back, and nothing looked again.

  • Fixed the panel to read the route's booleans and, when a cancel is deferred, to poll briefly until the row actually leaves the list. Repeated cancels extend the one loop rather than stacking a fetcher per press.
  • Fixed the settle loop holding the panel's loading flag. It ran for up to six seconds inside the cancel's own window, leaving Cancel All, every row's X and the futures close button dead for six seconds after the cancel had already been accepted.
  • Fixed a socket frame raising that same loading flag, which let a busy market disable the trader's cancel buttons at random and clear the flag out from under a cancel already in flight.

Every depth bar rendered as a stub

@/lib/orderbook measures depth over exactly what is rendered. The stacked layout drew 14 rows but scaled every bar against maxima taken over 50 levels, so on the cumulative basis the widest bar could only ever reach 14/50 of the track and the whole ladder read as a column of stubs.

  • Fixed the book to be BUILT at the depth the layout draws rather than sliced afterwards, so the denominators and the rows are the same set. The subscription still asks for the full 50; the cut moves to where the maxima are computed, and a resize across the layout breakpoint rebuilds from the last payload without waiting for the market to move.

The sweep readout vanished mid-hover, and the whole bid side lit up

The hover was keyed by price. A book is live, so the level under the pointer gets consumed and on the next frame that price is not in the array: the readout's lookup returned nothing and the card disappeared, while the ladder's tint — which compared a RANGE — still matched. It degraded far worse than that. Once the hovered price fell outside the book's span, every remaining bid satisfied the comparison and the entire buy side rendered as swept.

  • Fixed by addressing the hovered level by slot. Slot N is valid for as long as the ladder is N levels deep, so the tint covers exactly the levels the readout counted and nothing once the ladder is shorter. The Pro terminal was already correct; this is the same repair in the retail panel.

One market's candles were drawn on another market's chart

The client's stream key for ohlcv is ohlcv:<interval> with no symbol in it, so every ohlcv callback at one interval shared a single callback set — and the wire frame carried no symbol either, because the payload is an array of [time, o, h, l, c, v] tuples with nowhere to put one. There was nothing to filter on and so nothing was filtered: a frame for another market at the same interval was delivered to, and drawn by, whichever chart was open. The same defect had already been fixed for orderbook and ticker; it could not be fixed here without the field.

  • Fixed the market feeds to stamp the symbol on the frame, and the client to carry it through the envelope unwrap onto the array — Array.isArray, length and index access are unchanged, so nothing that iterates the payload can notice.
  • Fixed the client to apply the symbol test to ohlcv as it already did to every other type. A frame that carries no symbol is still accepted, which is every CCXT-backed venue where one socket serves one market, so this narrows nothing that used to work.

Candle volume counted an order's whole size on every cycle it filled in

Covered in Upgrade Notes.

  • Fixed the engine to book the amount that actually traded in the cycle. It added the order's full original size once per cycle per symbol, so an order of 10 filling over five cycles added 50 to the day.

One minute's bar showed one fill's price, not the minute's range

The engine kept a single order per symbol per cycle and threw away the prices every other fill traded at, then built the bar from that one order's last trade. A taker that swept four levels traded at four prices, and a high and low derived from one of them are not the period's high and low.

  • Fixed the aggregate to walk every fill of the cycle, so the bar's range and close describe what actually traded. Computed in the single pass that already visits the fills rather than by spreading the prices into a call, because a busy cycle is exactly when a length limit would bite.
  • Fixed the high and low to span the written close as well as the traded range: a pinned settlement close could sit outside the range the raw trades traced, which renders as a broken wick.

A restart could overwrite the bucket a candle was already in

Nothing in memory is not the same as nothing stored. A restarted process, or the first fill after a symbol's cache entry was evicted, arrived with an empty cache while the store may well already hold a row for the current bucket — written moments earlier by the process that just died, by the AI market maker, or by a shard.

  • Fixed the engine to adopt the stored row for the current bucket instead of reading its close for continuity and then opening a new one over the top of it.

Every interval above one minute was filed on the wrong boundary

Covered in Upgrade Notes.

  • Fixed the engine's bucket function to delegate to the one UTC normaliser the read path, the shard projector and the AI market maker all already use, so there is now exactly one answer to "which bucket is this" on any box in any timezone. The refusal on an unknown interval is kept, because the shared normaliser truncates to the second rather than refusing, and a caller arriving with a typo would otherwise write a candle per second under that name.

A quiet market cost a full history reload every fifteen seconds

The chart's live-edge test asks whether the newest candle is behind the wall clock, which is the right question — but it is a condition a quiet market or a paused engine satisfies permanently. The backend has nothing newer to give, the refetch returns the same series, the newest candle is still behind, and the next frame asks again.

  • Fixed by recording the newest candle time when a recovery fetch is issued and grading the next one against it. A series that moved keeps the fast cooldown, because that is a real gap closing; a series that did not move drops to one probe every five minutes — 12 requests an hour on a quiet market instead of 240 — and the first productive fetch clears the mark, so a market that comes back to life still repairs itself.
  • Fixed the per-frame work. Frames carry up to a thousand bars and the handler walked all of them to reach the two or three at the end that can change anything; it now finds where the history stops with a binary search, falling back to the full walk on any frame that cannot be shown to be sorted.

The futures trades list was blanked twice a second

The futures market feed published a hard-coded empty trades payload on every 500ms tick to every subscriber, having read nothing — futures has no recent-trades source at all. Any client that renders the payload it is handed had its list, populated by the REST read, wiped by a frame carrying no news.

  • Fixed by sending nothing when there is nothing to send. The subscription is still accepted, so the gap stays visible for whenever futures grows a trades feed.

A stale candle subscription was never pruned

An ohlcv subscription is per interval, but "is this still watched" compared only the route and type, so any registered ohlcv on the symbol satisfied every interval and the key was never deleted. Switching timeframe subscribes the new interval before the debounced unsubscribe of the old one, so the old key always found the new registration and survived.

  • Fixed the comparison to include the interval. This was harmless while the feed had no candle case and the stranded entries were inert; it stopped being harmless the moment one was added, because each dead key had become a live candle read every poll, delivered to nobody.

The trading process's broadcasts reached nobody

With ECO_TRADING_ENABLED, the matching engines run in a fourth process that holds every engine lease, while the browser's socket is held by the web process. Both run with cron delegated, and the relay only ever carried scheduler-to-web traffic — so every orders and ticker frame the matcher produced was handed to an empty client map.

The visible symptom was the same cancelled order described above, and it looked different on each route because of what each one falls back to: the market feed re-reads the shared store every two seconds, so the ladder recovered; the orders route is push-only, so the row never cleared. Fills had the same fate with nothing to mask them.

  • Fixed by deciding publication per frame rather than per process. A dedicated scheduler publishes its allow-listed routes exactly as before; any split process publishes the narrow trading set — the two per-user orders streams and the two ticker streams — but only while another process has announced that it holds a socket for that route.
  • Fixed the echo this makes possible. Delivering a relayed frame re-enters the broker, whose broadcast methods publish; with both halves publishing, a frame would be delivered on the far side, republished, delivered back, and double on every hop. Delivery is synchronous, so a re-entrancy guard is exact, and it is released in a finally — a throw from one socket write must not leave the process unable to relay anything for the rest of its life.
  • Everything else the market routes carry — order book, trades, candles — is read from the shared store by that route's own poller and is deliberately NOT duplicated onto the bus.

The order book's total column had two names for one thing

  • Fixed the lower-case total string and removed the unused sum, so the panel and the terminal name the same column the same way.
  • Fixed the spread strip's labels being rebuilt against the wrong translators: it listed one it does not use and omitted one it does, so a language switch could leave the previous language's labels in place.