Getting liquidity onto a futures book

A new futures market's book is empty and every market order fails — what actually seeds depth, the Hummingbot perpetual API, and the depth to verify before launch.

11 min readUpdated 6 August 2026liquidity, order-book, market-making, hummingbot, depth

Creating a futures market creates a row, not a market. The contract goes live the moment you submit the wizard — status: true — and its order book is empty. Nothing in this addon fills it, and nothing else in the platform fills it either.

Until something rests in that book, the contract is not merely quiet. It is unusable in a specific, mechanical way, and the failures point at the trader rather than at you.

What an empty book actually does

The book lives in ScyllaDB, keyed by symbol, and it is written only by orders that rest. A market order is priced by walking it, so with nothing to walk it cannot be priced at all.

What the trader did Response Message
Market order, that side of the book is empty 422 There is no resting sell liquidity on BTC/USDT to fill a market order against. Place a limit order instead.
Market order, some depth but not enough 422 Order book has insufficient liquidity to fill 5 BTC at market. Reduce the amount or place a limit order.
Market order through the Hummingbot perp bridge 422 Cannot execute MARKET perp order: no price available in the order book

Both futures messages come from the sweep in futures/utils/queries/orderbook.ts; the third is the connector refusing to forward an order it cannot price. None of them is a bug to suppress — they are the guard that stops a market order filling at an arbitrary price.

Two consequences are easy to miss:

  • A limit order is the only thing that can be placed first. It does not need a counterparty; it becomes the book. Every market order needs one that is already there.
  • A market that has never printed has no mark, so the two-second mark sweep skips it entirely. Nothing is stopped out and nothing is liquidated on a symbol with no last price, and a manual close settles at the entry price for zero PnL. See Funding and the mark price.

So a market with no depth is not a market that trades badly. It is a market where market orders fail, positions cannot be opened by taking, and the risk engine has nothing to mark against.

The AI Market Maker does not do this

AI Market Maker attaches one-to-one to an ecosystemMarket row: its create route looks the market up with models.ecosystemMarket.findByPk(marketId) and answers 404 — Ecosystem market not found for anything else. Its engine, its bots and its pool all read the same model. There is no reference to futures anywhere in its code, no futures market appears in its market picker, and no setting changes that.

If you have the addon installed and assumed it was quoting your perpetuals, it is not — check Admin → AI Market Maker and you will find only spot markets listed.

The same goes for the Algo Trading Bots addon and the copy-trading addon: neither places futures orders. Only two things in the whole platform write a futures order — the user-facing POST /api/futures/order route, and the liquidation engine.

The two routes that do work

Hand-placed limit orders

Every futures order, whoever places it, goes through POST /api/futures/order. That includes yours. An account you control can quote both sides of a new contract from the ordinary trading screen.

  1. Fund the account in the quote currency — margin is always the quote currency in a FUTURES wallet, and a FUTURES wallet can only be funded ECO → FUTURES. See Install.

  2. Clear the KYC gate. Order placement calls the futures_trading KYC feature check for every account, including yours. If you have added it to a verification level under CRM → KYC, the quoting account has to satisfy it.

  3. Place LIMIT orders on both sides at /trade?symbol=BTC-USDT&type=futures. Keep the bid below the ask: a crossing pair is matched by price alone.

  4. Watch the margin. Each resting order holds amount × price ÷ leverage plus its fee, debited at placement — see Leverage and margin.

The matcher pairs a buy and a sell on price crossing only; it never compares userId. If your own bid crosses your own ask, it fills. Because positions are keyed by side, that leaves the same account holding a long and a short, each posting its own margin, each paying a fee, and each independently liquidatable. Nothing nets them.

This is the right way to bootstrap and the wrong way to run a book. Hand-placed quotes do not follow the price, do not re-quote after a fill, and do not pull themselves when the market moves.

The Hummingbot Connector's perpetual API

The supported way to run a continuous maker on a futures book is the Hummingbot Connector addon and its bicrypto_perpetual connector. It is a bridge, not an engine: everything it places is an ordinary order on your own book, through the same route the trading screen uses, settling through the same FUTURES wallet.

Endpoint Auth Scope
GET /api/hb/perpetual/exchange-info public
GET /api/hb/perpetual/orderbook/{currency}/{pair} public
GET /api/hb/perpetual/funding-rate/{currency}/{pair} public
POST /api/hb/perpetual/order signed hb:trade:perp
DELETE /api/hb/perpetual/order (batch, optional ?symbol=) signed hb:cancel:perp
DELETE /api/hb/perpetual/order/{id} signed hb:cancel:perp
GET /api/hb/perpetual/order/{id} signed hb:read:account
PUT /api/hb/perpetual/order/{id} (amend) signed hb:trade:perp and hb:cancel:perp
GET /api/hb/perpetual/positions signed hb:perp:positions
POST /api/hb/perpetual/leverage signed hb:perp:positions

Scope checks are AND, never OR: the amend route requires both of its scopes, so a key minted with hb:trade:perp alone can place orders but answers 403 — Insufficient API key scope on every amend. Signed calls carry X-API-Key, X-Timestamp, X-Nonce and X-Signature; the Perp trading key preset at /hb/keys grants every scope in the table above plus hb:read:market, so a key minted from the preset satisfies all of them.

Signed calls are metered in two buckets — 600 a minute for trading calls (place, cancel, amend) and 240 for account calls (order lookup, positions, leverage) — and both apply the addon defaults in Admin → Hummingbot → Settings and then a key's own rateLimitOverride. The public market-data routes are throttled per IP at 1200 requests a minute, and that figure is fixed in code: the public path consumes the literal budget without consulting either the admin setting or an override. There is no key on an unsigned request, so raising a key's read budget does nothing for a market-data client — an integrator who needs more than 1200 a minute of orderbook has to sign the calls.

The perp side of the connector needs the Futures addon installed on the same server. Without it every perp call answers 503 — The Futures extension is not installed on this server.

Two ways to run the bot:

  • Someone else's machine — a user, or you, running Hummingbot with the connector kit. The flow is on Connecting a bot.
  • Your own serverAdmin → Hummingbot → Bot Instances, which supervises a Hummingbot process on the platform box. Its market picker lists your active futures markets against the bicrypto_perpetual connector, so a perpetual instance is registered the same way a spot one is. See Bot Instances.

A perpetual PMM preset is a pmm strategy in the Strategy Studio with its market set to perp; the generator then emits connector_name: bicrypto_perpetual and a leverage: line into the controller YAML. Strategies and presets covers the parameters.

If a supervised bot is the only maker on a contract, hbKillSwitch and hbReadOnly in Admin → Hummingbot → Settings remove your entire book when engaged. Read-only mode blocks hb:cancel:perp as well, so the bot cannot even pull its resting quotes — they stay in the book and can only be cancelled through the platform's own order screens.

What the perpetual API tells a bot that is not true of this desk

The endpoints are Binance-Futures-shaped so an off-the-shelf connector parses them. Three fields in that shape mean less here than a bot will assume, and all three read from the market's metadata JSON — which the market wizard never writes.

Funding is a number you typed, or zero. GET /api/hb/perpetual/funding-rate/… returns lastFundingRate straight from metadata.fundingRate, defaults it to 0, and computes nextFundingTime as the next 8-hour UTC boundary. The engine accrues no funding at all — no cron, no ledger entry, no schedule. The countdown is arithmetic on the clock, not a settlement. An external maker pricing inventory on expected funding cashflows will be pricing a cashflow that never happens; tell them, or leave the field absent.

markPrice and indexPrice are 0 unless somebody hand-edits the metadata. Nothing on the platform writes either field. The positions endpoint falls back to reporting markPrice equal to the position's own entry price when it is absent. The mark the engine actually liquidates against is a different number entirely — the in-memory ticker's last traded price on your own book.

maxLeverage and the rungs you publish are two different checks. Exchange-info reports defaultLeverage: 10 and maxLeverage: 100 when the metadata does not carry them, and POST /api/hb/perpetual/leverage validates only against maxLeverage (defaulting to 125). So on a market whose limits.leverage is "1,5,10", a bot's set_leverage 25 succeeds, and then every order it places is refused:

Leverage 25x is not offered on BTC/USDT. Available: 1, 5, 10.

That looks like an exchange fault from the bot's side. It is the discrete rung list doing its job — see Futures markets. Note that the refusal names the market in the platform's internal BTC/USDT form, not the BTC-USDT the bot sent, so grep the logs for the slash.

feeMaker and feeTaker in exchange-info are echoed verbatim from metadata.maker and metadata.taker, which this desk denominates in percent (taker: 0.075 means 0.075%). A Binance-shaped client reads those fields as fractions. A maker that takes 0.075 at face value prices its spread as if the fee were 7.5% — a hundred times the real rate. State the units to anyone integrating.

What quoting this book actually costs

Making a market here is not a neutral activity, and the arithmetic differs from a spot book in one way that dominates everything else.

Inventory is gross, not net. A fill looks its position up by user, symbol and side. Buys accumulate a long; sells accumulate a short; the two coexist and never offset. A maker that quotes both sides and gets hit on both ends up with two open positions, two lots of isolated margin posted, and two independent liquidation prices. Margin usage grows with turnover until somebody closes a leg.

Every fill is a real position with real margin. At leverage L, a ladder carrying total notional N on one side locks N ÷ L of the FUTURES wallet plus the fee, before anything is filled. Cancel releases it; a fill converts it into position margin.

Fees are charged on notional and taken immediately. The maker rate applies to an order that rests, the taker rate to one that crosses — decided from the book at placement, not from the side. At 20x a 0.05% fee is 1% of the margin posted.

Positions the maker holds can be liquidated. They are ordinary positions on your own book, marked against your own last traded price. A maker running at a high rung on a thin contract is exposed to exactly the wash-print problem described in Funding and the mark price.

Reconcile what the bot thinks it holds against GET /api/hb/perpetual/positions and Admin → Futures → Positions (/admin/futures/position, permission access.futures.position) rather than against the bot's own view.

Liquidations trade against the same book

This is why depth is a solvency question and not a UX one.

A liquidation is not a mark-price settlement with a flag flip. The engine submits a reduce-only order on the opposite side, priced at the bankruptcy price, and whatever the book does not absorb is retired immediately — it is never left resting. The trim at 70% of margin lost and the full close at 90% both work this way. Liquidation has the full mechanics.

So the order has a floor it will not trade past. For a long at leverage L it fires at 0.9 ÷ L adverse and is priced at 1 ÷ L adverse, which leaves a window of 0.1 ÷ L of the entry price for the book to absorb it in:

Top rung Full liquidation fires at Order priced at Window it can trade in
5x 18.0% adverse 20.0% 2.00%
10x 9.0% 10.0% 1.00%
20x 4.5% 5.0% 0.50%
50x 1.8% 2.0% 0.20%
100x 0.9% 1.0% 0.10%

The partial trim fires earlier, at 0.7 ÷ L, so it has a wider window (0.3 ÷ L) — but it only takes 80% of the size, and a trim the book cannot absorb is abandoned rather than settled, because settling it would pay both sides for the same exposure.

What the book cannot absorb on a full liquidation is settled against the mark and the difference is written to the log:

FUTURES_SHORTFALL  The book could not absorb a liquidation; 0.4 of BTC/USDT BUY was
settled against the mark instead. … shortfall=812.34 USDT

There is no insurance fund. That number is yours.

Publishing "1,5,20,50,100" is a commitment that a position opened at 100x can be traded out inside a 0.1% window on your own book. If nothing is resting there, the liquidation books a shortfall the platform absorbs. Match the top rung to the depth the contract actually has — not to what another venue advertises.

Before you open a market to customers

Depth is not on the futures dashboard: /admin/futures reports open interest, side skew, the at-risk queue and leverage bands, but no book. Read the book from one of these instead:

  • The order book panel on /trade?symbol=BTC-USDT&type=futures, fed by the orderbook stream on the futures market WebSocket — top 50 levels a side.
  • GET /api/hb/perpetual/orderbook/{currency}/{pair}?limit=50, if the Hummingbot Connector addon is enabled. It is public, so curl works, and it answers 404 for a market that is disabled or does not exist.

Then check the following, on the market's own numbers:

    • There is resting depth on both sides. A book with only bids cannot absorb a short's liquidation, which has to buy
    • The total resting size inside 0.1 ÷ L of the mark, for the highest rung you offer, is at least the largest position the market permits — that is limits.amount.max, or limits.cost.max ÷ price where you set a cost cap
    • limits.amount.max and limits.cost.max are set to real numbers, so one position cannot exceed what the book can take
    • limits.leverage lists only rungs whose window the depth above covers
    • The market has printed at least once, so the mark sweep is marking it
    • If a bot is your only maker, it is running, its API key is not expired or IP-blocked, and hbKillSwitch / hbReadOnly are off
    • FUTURES_SHORTFALL is in whatever you monitor logs with

If the depth is not there, the honest options are the same three every time: lower the top rung, cap position size, or leave the market switched off until it has a maker. Deleting the market is not one of them — see Futures markets.

  • Futures markets — the wizard, the metadata fields, and retiring a contract safely.
  • Liquidation — thresholds, the bankruptcy price and what a shortfall is.
  • Risk console — reading open interest and the at-risk queue.
  • Troubleshooting — the order rejections, one by one.