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.
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.
-
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. -
Clear the KYC gate. Order placement calls the
futures_tradingKYC 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. -
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. -
Watch the margin. Each resting order holds
amount × price ÷ leverageplus 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 server —
Admin → Hummingbot → Bot Instances, which supervises a Hummingbot process on the platform box. Its market picker lists your active futures markets against thebicrypto_perpetualconnector, 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 USDTThere 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 theorderbookstream 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, socurlworks, 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 ÷ Lof the mark, for the highest rung you offer, is at least the largest position the market permits — that islimits.amount.max, orlimits.cost.max ÷ pricewhere you set a cost cap -
limits.amount.maxandlimits.cost.maxare set to real numbers, so one position cannot exceed what the book can take -
limits.leveragelists 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/hbReadOnlyare off -
FUTURES_SHORTFALLis 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.
Related
- 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.