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 fills it until you point a maker
at it.
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.
What can quote this book, and what cannot
AI Market Maker is no longer an Ecosystem-only
product. A maker is created against a venue — ECO for an Ecosystem spot
market, FUTURES for a perpetual — and the FUTURES tab appears in the create
wizard only once this server answers for the Futures extension. Without it the
endpoint refuses with The Futures extension is not installed on this server, so
a Futures market maker cannot be created.
A futures maker is not a spot maker pointed at another table. Its pool holds the quote currency only, as margin; a fill opens or closes a position rather than moving two balances; rebalancing is refused, because there is no ratio to rebalance; and its orders go in under one shared pool account funded with real money — so the maker's own position is liquidatable on exactly the same terms as a customer's. Leverage ships at 1x. Creating a market has the full comparison and the leverage field.
The Algo Trading Bots addon and the copy-trading addon still place nothing here. The trading-bot engine is Ecosystem-only end to end — it prices through the ecosystem market price, executes through the ecosystem order path, and holds a bot's funds in the ECO wallet — and it refuses a bot on a market it cannot trade. The copy-trading addon does not mention futures anywhere.
Three things write a futures order directly: the user-facing
POST /api/futures/order route, the liquidation engine, and the AI Market
Maker's futures venue, which mirrors that route's money arithmetic rather than
calling it. The Hummingbot Connector is not a fourth — its perpetual
endpoints call the same route the trading screen does.
The routes that do work
Three things can put depth on a futures book: your own hand-placed limit orders, a Hummingbot instance through the connector addon, and the AI Market Maker's futures venue described above. The first two are covered here; the maker is covered in Creating a market.
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 — your own two orders will not fill each other, so a crossed pair simply rests in the book doing nothing. -
Watch the margin. Each resting order holds
amount × price ÷ leverageplus its fee, debited at placement — see Leverage and margin.
The matcher refuses a crossing pair belonging to the same account and steps past the older of the two, so an operator quoting both sides cannot trade with themselves. That is deliberate rather than a courtesy: a fill writes the last price and the current candle, two of the three readings the mark is built from, and an account that can print at will can move what every position on the market is worth.
Neither order is cancelled. They stay in the book, crossed, until a genuine counterparty takes one — so a crossed self-quote does not fill and does not error, it just sits there looking stuck. Note also that positions are keyed by side: getting hit on both of your quotes leaves the same account holding a long and a short, each posting its own margin 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 something different here from what a bot will assume.
Funding is real, and the rate is one you chose.
GET /api/hb/perpetual/funding-rate/… reports what that market is actually
configured to charge and when it will next charge it: lastFundingRate is the
clamped rate from metadata.fundingRate and nextFundingTime is the next
boundary on that market's own interval. The settleFuturesFunding job settles
it against open positions on those boundaries and writes a payment row per
position. What it is not is a premium-driven rate — this venue has no
index to take a premium against, so the number is one an operator typed rather
than one derived from anything, and a maker treating it as a spot-premium signal
is modelling something this desk does not produce. It is capped at 0.75% per
window, and a market with no rate configured answers 0 and settles nothing —
which is where every market starts, so tell an integrator which of your markets
are funded.
markPrice and indexPrice are 0 unless somebody hand-edits the metadata.
Nothing on the platform writes either field, and there is no index price for it
to write. 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 median of the last
trade, the current minute's weighted close and the book mid, taken whenever the
book has two sides to disagree with a single print.
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 the engine's own mark rather than an outside index. A maker running at a high rung on a thin contract is exposed to exactly the thin-book pricing 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 60% of margin lost and the full close at 80% 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.8 ÷ L adverse and is priced at 1 ÷ L adverse, which leaves a
window of 0.2 ÷ 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 | 16.0% adverse | 20.0% | 4.00% |
| 10x | 8.0% | 10.0% | 2.00% |
| 20x | 4.0% | 5.0% | 1.00% |
| 50x | 1.6% | 2.0% | 0.40% |
| 100x | 0.8% | 1.0% | 0.20% |
The partial trim fires earlier, at 0.6 ÷ L, so it has a wider window
(0.4 ÷ 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 USDTThat number is booked against the insurance fund as well as logged. The fund is capitalised by withholding a share of the futures fees you already collect, so it is your own revenue absorbing it; when the fund cannot, the default policy closes profitable positions on the opposite side of that market early instead. Either way a shortfall is a real cost with your name on it — see Liquidation.
Publishing "1,5,20,50,100" is a commitment that a position opened at 100x can
be traded out inside a 0.2% window on your own book. If nothing is resting there,
the liquidation books a shortfall against the insurance fund — and past that,
deleverages a profitable trader who did nothing wrong. 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.2 ÷ 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.