Market LabDocs
Bots

Mid-Price Market Maker

Learn how Market Lab quotes around the live midpoint, manages inventory, reports PnL, and exits safely.

Mid-Price Market Maker

The Mid-Price bot continuously offers to buy below the current market midpoint and sell above it. It does not predict whether BTC will rise or fall. It provides liquidity on both sides and changes its order sizes as inventory builds.

This mode is designed to protect the quoted spread. A working quote follows the live midpoint in both directions, but it is not cancelled for every tiny book update.

Start with a preview

Use --dry-run to inspect the complete plan without creating a job or placing an order:

mlab bot run mid-price BTC/USDT \
  --venue bulk \
  --margin 200 \
  --leverage 8 \
  --duration 3600 \
  --spread-bps 6 \
  --stop-loss-pct 5 \
  --dry-run

Remove --dry-run to deploy it:

mlab bot run mid-price BTC/USDT \
  --venue bulk \
  --margin 200 \
  --leverage 8 \
  --duration 3600 \
  --spread-bps 6 \
  --stop-loss-pct 5

The command returns immediately after deployment. The bot continues inside mlabd.

--venue accepts bulk or hyperliquid. Hyperliquid currently means testnet native perpetuals and requires mlab auth set hyperliquid before live deployment.

Understand the plan

Here is an example plan:

max inventory size: 0.024008
requested margin:   200.00000000
max inventory:      199.99853296 margin
max exposure:       1599.98826370
reference midpoint: 66632.064
initial bid / ask:  66612.074 / 66652.054
initial bid size:   0.012004 (799.61133630 exposure)
initial ask size:   0.012004 (800.09125622 exposure)
initial working:    1599.70259251 exposure
spread:             6 bps
refresh time:       0.5s
refresh tolerance:  0.25 bps
stop loss:          5% of allocated margin

What it means:

  • Requested margin is the collateral allocated to this bot.
  • Max exposure is the requested margin after leverage. Here, $200 at 8x gives approximately $1,600 of maximum one-sided exposure.
  • Max inventory size is that exposure converted into BTC at the deployment price.
  • Initial bid size is how much the bot offers to buy while flat.
  • Initial ask size is how much the bot offers to sell while flat.
  • Initial working adds the two resting order values. It is not the bot's directional exposure: if equal bid and ask quantities both fill, they offset and inventory returns to flat.
  • Reference midpoint is halfway between the current best bid and best ask.

With neutral inventory, half of the capacity starts on each side. In this example, the bot initially works roughly $800 on the bid and $800 on the ask.

--size can replace --margin when you already know the exact maximum base-asset inventory you want.

How the spread is placed

--spread-bps is the total distance between the bid and ask. A 6 bps spread places the bid about 3 bps below midpoint and the ask about 3 bps above it.

At a BTC midpoint near $66,632, that is roughly:

bid  ≈ $19.99 below midpoint
ask  ≈ $19.99 above midpoint
total quoted spread ≈ $39.98

The final prices are rounded to valid venue ticks. Both orders use post-only ALO, so the venue rejects an order rather than allowing it to cross the book as a taker.

A quoted 6 bps does not guarantee that every completed cycle captures 6 bps. The midpoint can move between the buy and sell fills. Fees, adverse selection, and the eventual shutdown trade also affect the result.

What happens after a fill

Suppose the bid fills first. The bot is now long. It responds by:

  • making the next bid smaller, so it is less eager to add more long inventory;
  • making the ask larger, so it has more opportunity to sell back toward flat;
  • never allowing its job-owned inventory to exceed the maximum shown in the plan.

If the ask fills first, the reverse happens: the bot reduces the ask and increases the bid.

This is why live quote sizes may look uneven. It is intentional inventory control, not random order splitting.

When inventory reaches the long limit, the bot stops adding bids and works only the inventory-reducing ask. At the short limit, it stops adding asks and works only the bid.

How Mid-Price repricing works

The local orderbook processes every venue update and always retains the newest valid midpoint. The bot does not cancel a quote for every update, because doing so would continually lose queue position.

For each working Mid-Price quote:

  1. Let it rest for at least 0.5 seconds.
  2. Calculate where that quote belongs around the newest midpoint.
  3. Keep it if the change is insignificant.
  4. Cancel and replace it when the desired price changed by at least 0.25 bps or two venue price ticks, whichever is larger.

This check works in both directions. A quote is replaced when the midpoint moves toward it or away from it. That keeps the order attached to the configured spread instead of leaving an old price resting indefinitely.

The 0.5s and 0.25 bps values define Mid-Price mode and are shown in the plan. They are not command-line inputs. The separate volume-mid mode exposes --refresh-time and --refresh-tolerance-bps and keeps approachable quotes resting longer to prioritize fills.

Fills and inventory changes are handled immediately; they do not wait for the 0.5 second price-review age.

Directional bias

The default --directional-bias 0 is neutral. Inventory alone decides the size skew.

  • A positive value makes bids larger and asks smaller, favoring long inventory.
  • A negative value makes asks larger and bids smaller, favoring short inventory.
  • The accepted range is -100 through 100.

Directional bias changes order sizes, not the midpoint or configured spread. The maximum inventory limit still applies.

Stop loss

--stop-loss-pct is a percentage of the margin allocated to the bot, not its leveraged exposure.

With $200 margin and --stop-loss-pct 5, the maximum configured loss is approximately $10.

The bot watches its own:

  • realized PnL;
  • unrealized PnL marked at the latest midpoint;
  • fill fees or maker rebates.

When net bot PnL reaches the limit, it stops quoting, cancels its working orders, unwinds its remaining inventory, and terminates. Funding is not included because account-level funding cannot yet be attributed safely to an individual bot.

High leverage and fast markets can move beyond the configured loss before cancellation and inventory unwind finish. Stop loss limits the controller's response; it is not a guaranteed fill price.

Read the logs

Follow a running job:

mlab bot logs <JOB_ID> --follow

Quote messages describe the order lifecycle:

resting BUY ... @ 66628.096 size=0.0075
cancelled BUY ... @ 66628.096 size=0.0075
fill SELL 0.0075 @ 66668.086 ...
filled SELL ... @ 66668.086 size=0.0075
  • resting means the post-only order is live on the venue.
  • cancelled means that specific quote is no longer working.
  • fill updates inventory and PnL. An order can emit more than one fill.
  • filled means the order has reached its final fully-filled state.

A fill summary looks like this:

fill SELL 0.014973 @ 66662.854
buy=0.0622 sell=0.0622 inventory=0.0
realized=2.1056 unrealized=0.0 fees=-1.6585 pnl=0.4471
  • buy and sell are cumulative base-asset quantities filled by this job.
  • inventory is this bot's signed unmatched quantity. Positive is long, negative is short, and zero is flat.
  • realized is gross PnL from matched inventory.
  • unrealized marks the remaining inventory at the current midpoint.
  • fees is signed: a negative number is a cost and a positive number is a rebate.
  • pnl combines realized, unrealized, and fees. In the example, gross realized PnL is $2.1056; after $1.6585 in fees, the bot is up approximately $0.4471.

The result will move while inventory remains open because unrealized changes with the midpoint.

Existing positions and multiple bots

The bot does not require the account to be flat. It attributes inventory using only fills from order IDs owned by that job.

You can therefore run it while a manual position or another algorithm exists on the same symbol. Each bot:

  • calculates inventory and PnL from its own fills;
  • cancels only its own orders;
  • unwinds only its own unmatched inventory when it stops;
  • does not flatten the account's complete symbol position.

For example, if one job owns +1 BTC and another owns -2 BTC, the venue may show a net -1 BTC position. Stopping the first job removes its +1 BTC contribution and leaves the second job's -2 BTC contribution intact.

All activity still shares the same account margin and liquidation risk. Two jobs can also compete or self-trade if their quotes overlap, subject to the venue's self-trade rules.

Duration and shutdown

The bot keeps quoting until its duration expires, you stop it, its stop loss triggers, or an unrecoverable error occurs. Returning to flat does not complete the job; it begins another quoting cycle.

During shutdown it:

  1. finishes accounting for in-flight actions;
  2. cancels only quotes owned by this job;
  3. reconciles any final fills;
  4. submits the opposite of this job's remaining inventory;
  5. confirms that the job-owned inventory is flat.

The inventory unwind is not based on the account's net position. This is what allows multiple algorithms to operate on the same symbol without one bot closing another bot's exposure.

Manage the job

mlab bot jobs
mlab bot status <JOB_ID>
mlab bot logs <JOB_ID> --follow
mlab bot stop <JOB_ID>

Use --output jsonl when you need the complete structured event stream instead of compact terminal logs.

Mid-Price is an execution system, not a profitability claim or trading advice. Spread capture can be outweighed by fees, adverse selection, inventory risk, funding, latency, and the final inventory-unwind trade.

On this page