Trading strategies

Grid trading as inventory rebalancing

How our grid strategy turns a price channel into target inventory levels, and how it sizes orders through partial fills, price gaps and exchange limits.

If you've run a grid bot, you know the textbook version: a ladder of fixed-size buys below the price and sells above it. That version assumes each rung fills completely. When a buy fills halfway, or the price gaps through three rungs between ticks, a grid that tracks expected fills without reconciling its balances no longer matches what you hold.

At 3Jane, the grid runs in the same execution engine as our market-making and arbitrage strategies. We built it as an inventory-allocation rule: each price level has a target split between base and quote, and the strategy places whichever order restores that target. Here's how the levels, sizing and edge cases work, using the arithmetic from our test fixtures.

Why a ladder that assumes complete fills drifts out of sync

Some standing-order grids treat each rung as an independent event. A rung fills completely, the opposite order goes up, and the bookkeeping assumes the wallet followed along. If the implementation never reconciles that assumption against balances, real exchange behavior breaks it in a few predictable ways:

  • A partial fill leaves a rung half done.
  • A fast move skips rungs, so orders you meant to fill never trade.
  • Quantities get rounded to exchange precision, and tiny remainders fall below the minimum notional.
  • Balances change for reasons outside the grid.

Our grid sizes every order from the wallet. It reads the current balances, works out which level those balances correspond to, and sizes the next order as the gap between what you hold and what that level should hold.

That design follows from the failure modes above. When holdings locate the level, a partial fill or a change in capital value changes the next order size directly. The strategy never has to assume that any grid step executed in full.

The channel, the levels and the value of each level

The strategy is configured with three numbers: a top price, a channel size as a percentage of that top, and the number of levels. Everything else is derived from them:

  • Bottom price is top × (1 − channelPct).
  • Level width is top × channelPct / numberOfLevels.
  • Boundary index counts down from the top. Index 0 is the top price, and the index rises as price falls. The index of a price is its distance from the top divided by the level width, rounded down.

Our test configuration uses BTC/USDT with a top price of 20,000, a 20% channel and 40 levels. The bottom is 20,000 × 0.8 = 16,000, and each level is 4,000 / 40 = 100 USDT wide. Boundary 1 sits at 19,900, boundary 3 at 19,700, and boundary 20, the middle of the channel, at 18,000.

To divide capital across levels, the strategy values the base inventory at a reference price near the middle of the channel, adds the quote inventory and divides the total by the number of levels. That quotient is the value each level is responsible for.

Finding the current level from holdings

The current level comes from the quote balance. The filled-level index is numberOfLevels − round(quoteTotal / stepQuoteValue). If you hold only quote, the index is 0 and the grid sits at the top of the channel. As price falls and the grid buys, quote shrinks and the index moves toward the bottom.

Take the first fixture. The account holds 0.0025 BTC and 855 USDT, and the market is 19,749 bid and 19,751 ask. The calculation runs in three steps:

  1. Reference-valued capital is 0.0025 × 18,000 + 855 = 900 USDT.
  2. The allocation per level is 900 / 40 = 22.5 USDT.
  3. The filled index is 40 − round(855 / 22.5) = 40 − 38 = 2.

The next buy and sell boundaries come from this allocation state, not only from the latest trade price. If a fill, a transfer or a rounding remainder changes the balances, the strategy recomputes the level from the wallet.

Creating the next buy or sell

From filled level 2, the strategy quotes a buy at the next boundary down (index 3, or 19,700) and a sell at index 1, or 19,900. Each order's size is the difference between current holdings and the desired quote allocation at that boundary. The strategy converts that difference into base units at the boundary price and rounds it to the instrument's precision.

In the first fixture, the desired quote balance at index 3 is 37 × 22.5 = 832.5 USDT. The account holds 855, so the buy covers 22.5 USDT: 22.5 / 19,700 = 0.00114213198 BTC. Rounded to the instrument's five-decimal quantity precision, the expected output is a buy of 0.00114 BTC at 19,700.

The sell works the same way in reverse. At index 1, the desired quote balance is 39 × 22.5 = 877.5 USDT, which is 22.5 more than the account holds. Selling 22.5 USDT of BTC at 19,900 gives 0.00113 BTC, which is the expected sell order.

Partial fills and moves across several levels

This is where the inventory view earns its keep. Two fixtures change one thing at a time from the starting state: the first changes the holdings, and the second changes the price.

Partial fills resize the next order

The second fixture starts from the same holdings and then updates them to 0.003 BTC and 845.15 USDT. That matches 0.0005 BTC bought at 19,700, a partial fill of the 0.00114 BTC buy. Reference-valued capital drops slightly to 899.15 USDT, because the BTC cost 19,700 but is valued at 18,000. Each level is now worth 22.47875 USDT.

The quote balance still rounds to 38 levels, so the filled index stays at 2. The buy at 19,700 shrinks to the remaining gap of 845.15 − 37 × 22.47875 ≈ 13.44 USDT, or 0.00068 BTC. The sell at 19,900 grows to 876.67 − 845.15 ≈ 31.52 USDT, or 0.00158 BTC, because there's more BTC to give back.

Price jumps that skip levels

The third fixture holds inventory unchanged and moves the market from 19,849/19,851 to 19,449/19,451. The price now sits in level 5, three levels below the filled state. A ladder would have had resting orders at the skipped boundaries. Our grid buys the missed levels in one order.

The expected buy is 0.00346 BTC at the current bid of 19,449. That's roughly three levels' allocation, since 3 × 22.5 = 67.5 USDT is about 0.0035 BTC at that price. The sell stays at 0.00113 BTC at 19,900. Comments in the strategy describe the mirror case too: after an upward jump, it sells the missed levels.

Catching up across crossed levels is what makes this a target-inventory rule. After a gap, the strategy works out what it should hold at the current price and sizes one order to reach that holding.

Precision, minimum notional, balances and channel edges

Exchange rules and wallet balances shape every order the grid emits. Some of that behavior appears in the fixtures above, and the rest is covered by tests.

Precision and minimum notional

The quantity is rounded to the instrument's precision, which is five decimals in our BTC/USDT fixtures. That's why 0.00114213198 becomes 0.00114.

Minimum notional matters most for small remainders. After a partial fill, the gap to a boundary can be worth less than the exchange accepts. In that case the strategy moves the next quote to another boundary instead of submitting an order the exchange would reject.

Balances and channel edges

Every order is sized from the balances the strategy reads, so the wallet sets what each order can draw on. A buy spends the quote held above the boundary's target, and a sell gives up base worth the quote shortfall. The same discipline runs through our market making and arbitrage systems, where spot quotes are bounded by the base and quote you hold.

The channel endpoints come from configuration: index 0 at the 20,000 top and the bottom at 16,000. Holding only quote places the grid at index 0. Our test suite also covers the following cases, though this article doesn't show their resulting orders:

  • missing balances
  • missing prices
  • rebalancing
  • tiny remainders
  • prices outside the channel, including jumps beyond the grid

A worked path through a five-level channel

Here's a simplified illustration with made-up numbers to show the three events together. The channel runs from 100 down to 50 across five levels, each 10 wide. The starting wallet holds 5 units of base and 600 USDT. With a reference price of 80, assumed for this example, capital is 5 × 80 + 600 = 1,000 USDT, or 200 USDT per level.

The table holds that 200 USDT per-level value fixed to isolate the allocation changes. The target quote balance at index k is (5 − k) × 200.

Step Event Quote held Filled index Next orders
Start Price near 78 600 2 Buy 200 USDT at 70, sell 200 USDT at 90
1 Buy at 70 fills fully 400 3 Buy 200 USDT at 60, sell 200 USDT at 80
2 Buy at 60 fills 25% 350 3 (1.75 rounds to 2) Buy 150 USDT at 60, sell 250 USDT at 80
3 Price gaps up to 89 350 3, price in level 1 Sell 450 USDT of base to reach 800

In the implementation, capital is recomputed from base holdings at the reference price plus quote holdings. Trades at prices away from the reference price change that value, and the size of the change depends on the executed amounts and prices, as the partial-fill fixture shows.

How we test the grid

The 40-level BTC/USDT scenarios above are saved event fixtures; the five-level table is a simplified illustration. Our harness uses Chronicle's YAML message-testing tools to feed market and order events into the strategy runner. It then compares the emitted order-management messages against expected YAML output. Separate assertions check level size, channel endpoints and configuration parsing.

The BTC/USDT numbers in this article come from those fixtures. They show exactly what the strategy does with a given wallet and market, but they don't say what it earns. We keep that distinction because a grid that sizes correctly can still lose money in a channel the market leaves.

Try the three events on your own grid

If you run a grid, replay three cases against it before trusting it with capital:

  1. An ordinary fill at one boundary.
  2. A partial fill that leaves a remainder below the minimum notional.
  3. A gap that skips two or more levels in one tick.

After each event, check that the next order sizes follow from your actual balances. If they follow from what your ladder expected instead, the grid will drift. Our backtesting and position sizing article covers how we evaluate sizing rules like this one, and the strategies overview shows where the grid fits alongside the rest of our trading.