Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

Grid Trading Bots, Examined Honestly

A grid trading bot buys each step down and sells each step up inside a fixed range. See the market shape that pays for the mechanism and the one that breaks it.

A grid trading bot places a ladder of orders at fixed price intervals: a buy at every rung below the current price, a sell at every rung above it. Each time the market steps down, the bot buys. Each time it steps back up, the bot sells what it bought lower. The mechanism harvests oscillation, and it accumulates an open position whenever the market walks away from the ladder in one direction and keeps going.

This page takes the machinery apart, then measures the two market shapes that decide its outcome. Every panel below is a stored query over real US equity prices, pinned to fixed dates so the figures stay reproducible.

How a grid trading bot works

Three settings define a grid: the upper and lower bounds of the range, the number of lines inside it, and the order size at each line. The spacing between lines follows from the first two.

Here is the arithmetic, using round numbers picked for clarity rather than from any real quote. Suppose a stock trades near $100 and an operator sets a grid from $95 to $105 with eleven lines one dollar apart and 100 shares per line. Price slips to $99 and the resting buy fills. Price returns to $100 and the matching sell fills, closing the pair for a gross $100. Both orders re-arm. Nothing about the company changed in that hour. The position simply completed a round trip between two adjacent rungs.

Two variants appear in the open-source projects that circulate publicly. An arithmetic grid spaces lines by a fixed dollar amount. A geometric grid spaces them by a fixed percentage, which holds the percentage profit per round trip steady across a wide range. Documentation also separates a neutral grid, which starts flat and works both directions, from a long grid, which starts holding inventory and sells into strength.

The payoff shape deserves one plain sentence. Profit on a completed round trip is capped at one grid step minus costs. The open position that builds up when price leaves the range carries no matching cap.

The market shape a grid needs

A grid is paid by travel, not by direction. Two stocks can finish a period at the same price after covering wildly different distances. Adding up the absolute size of every daily move gives the total distance traveled. Dividing the net move by that distance gives an efficiency ratio, a standard measure of how directional a path was. A low ratio describes a path that wandered. A high ratio describes a straight march.

QueryDistance traveled vs net move: twelve familiar stocks, Jan 2024 through Jun 2026
The exact SQL behind every number
WITH daily AS (
    SELECT ticker,
           toDate(toTimeZone(window_start, 'America/New_York')) AS dt,
           argMax(toFloat64(close), toTimeZone(window_start, 'America/New_York')) AS c
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker IN ('SPY','KO','PG','JNJ','VZ','MO','XOM','JPM','WMT','NVDA','TSLA','NFLX')
      AND toDate(toTimeZone(window_start, 'America/New_York')) BETWEEN toDate('2024-01-02') AND toDate('2026-06-30')
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
    GROUP BY ticker, dt
),
steps AS (
    SELECT ticker, dt, c,
           c / lagInFrame(c) OVER (PARTITION BY ticker ORDER BY dt) - 1 AS ret
    FROM daily
)
SELECT ticker,
       round(sum(abs(ret)) * 100, 0) AS travel_pct,
       round(abs(argMax(c, dt) / argMin(c, dt) - 1) * 100, 1) AS net_move_pct,
       round(100 * abs(argMax(c, dt) / argMin(c, dt) - 1) / sum(abs(ret)), 1) AS efficiency_pct
FROM steps
WHERE ret > -0.5 AND ret < 0.5
GROUP BY ticker
ORDER BY efficiency_pct
Run this yourself

Read the first and last rows together. PG covered 527% of cumulative daily movement and finished 0.9% from where it started, an efficiency of 0.2%. SPY sits at the opposite end at 14.3%, turning 415% of travel into a 59.2% net move. A grid ladder placed on the first path meets its own rungs over and over. The same ladder placed on the second path gets left behind.

Calm and range-bound are related but distinct properties. A stock can drift upward in very small daily increments and still leave a grid stranded, which is one reason the names studied in the low-volatility anomaly literature are not automatically grid material.

Counting the round trips

Efficiency is one lens. Grid income depends on something more concrete: how many times price actually revisits a level. Anchoring on the first close of each calendar year and counting the sessions whose close lands on the opposite side of that anchor gives the number of crossings a single grid line would have caught. Coca-Cola (KO) is the subject here, a large, liquid consumer name of the sort grid documentation tends to feature.

QueryKO: crossings of the year's opening level, by calendar year, 2019 to 2025
The exact SQL behind every number
WITH daily AS (
    SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS dt,
           argMax(toFloat64(close), toTimeZone(window_start, 'America/New_York')) AS c
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'KO'
      AND toDate(toTimeZone(window_start, 'America/New_York')) BETWEEN toDate('2019-01-01') AND toDate('2025-12-31')
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
    GROUP BY dt
),
anchored AS (
    SELECT toYear(dt) AS year, dt, c,
           first_value(c) OVER (PARTITION BY toYear(dt) ORDER BY dt) AS anchor
    FROM daily
),
sided AS (
    SELECT year, dt, c, anchor,
           if(c >= anchor, 1, 0) AS side,
           row_number() OVER (PARTITION BY year ORDER BY dt) AS rn,
           lagInFrame(if(c >= anchor, 1, 0)) OVER (PARTITION BY year ORDER BY dt) AS prev_side
    FROM anchored
)
SELECT year,
       countIf(rn > 1 AND side != prev_side) AS crossing_count,
       count() AS sessions,
       round((argMax(c, dt) / any(anchor) - 1) * 100, 1) AS net_change_pct
FROM sided
GROUP BY year
ORDER BY year
Run this yourself

In 2019 the close crossed its own opening level 8 times across 252 sessions, ending the year 17.9% from that level. In 2025 the count was 6 over 250 sessions, with a net change of 13%. One line, one stock, and the raw material for a grid varies several-fold from year to year with no way to know the coming year's count in advance.

That variability is the quiet problem. A grid's advertised return is usually quoted per completed round trip, and the number of round trips is set by the market rather than by the settings.

Where a grid breaks

The failure mode has a specific shape. Fix a grid at a range, then let the market leave it. Below, the ceiling and floor are the highest and lowest closes of the first twenty sessions of 2024, held flat across the following thirty months, with the S&P 500 tracker's monthly close drawn against them.

QueryA grid fixed on early 2024, and where the S&P 500 tracker went afterward
The exact SQL behind every number
WITH daily AS (
    SELECT toDate(toTimeZone(window_start, 'America/New_York')) AS dt,
           argMax(toFloat64(close), toTimeZone(window_start, 'America/New_York')) AS c
    FROM global_markets.delayed_stocks_minute_aggs
    WHERE ticker = 'SPY'
      AND toDate(toTimeZone(window_start, 'America/New_York')) BETWEEN toDate('2024-01-02') AND toDate('2026-06-30')
      AND (toHour(toTimeZone(window_start, 'America/New_York')) * 60
           + toMinute(toTimeZone(window_start, 'America/New_York'))) BETWEEN 570 AND 959
    GROUP BY dt
),
opening_20 AS (
    SELECT c FROM daily ORDER BY dt LIMIT 20
),
band AS (
    SELECT min(c) AS grid_low, max(c) AS grid_high FROM opening_20
),
monthly AS (
    SELECT toStartOfMonth(dt) AS month_start, argMax(c, dt) AS close_px
    FROM daily
    GROUP BY month_start
)
SELECT formatDateTime(monthly.month_start, '%Y-%m') AS month,
       round(monthly.close_px, 2) AS spy_close,
       round(band.grid_high, 2) AS grid_top,
       round(band.grid_low, 2) AS grid_bottom
FROM monthly, band
ORDER BY monthly.month_start
Run this yourself

The band runs from $467.27 to $491.26. The first month in the series closed at $482.92, inside it. The last of the 30 months closed at $746.32, far above the ceiling, and the chart shows the line leaving the band and never coming back.

Walk through what the ladder does along that path. Rising through the range, the bot sells inventory rung by rung, banking one step of profit each time. Above the ceiling it holds cash and watches. Every further point of the advance is a gain the operator no longer participates in, which is the mild version of the failure.

The severe version is the mirror image. Price falls through the floor and the bot has bought at every rung on the way down, so it now holds the full ladder of inventory at an average cost above the market, with each additional step widening the unrealized loss. A range that once produced steady, small, visibly successful trades turns into a single concentrated position. Grid marketing describes this as the bot being "stuck"; on a brokerage statement it is an open drawdown with no built-in exit.

Costs scale with the trade count

A grid completes many small trades, and each one pays the full cost of a round trip: the spread, plus commissions or exchange fees where they apply. The bid-ask spread is the piece most often left out of grid math, since it never appears on a statement as a line item.

The illustrative arithmetic is quick. On a $100 stock with a one-cent spread, a round trip that crosses the spread on both sides gives up roughly two cents, or 0.02% of notional. Against a one-dollar grid step, that is about 2% of the gross profit per trip. Widen the spread to five cents, a routine figure on a thinner name, and the same trip gives up ten cents against a dollar of gross, or 10%. Tightening the grid to ten-cent steps to catch more oscillation cuts the gross per trip to ten cents while leaving the cost unchanged, and the five-cent spread now consumes the entire trade.

That relationship sets a floor under useful grid spacing: steps must stay large relative to round-trip costs, and the tighter the grid, the more of its income belongs to the venue rather than the operator.

Why backtests flatter grid bots

Grid strategies produce unusually attractive backtests, and four structural reasons explain most of it.

  1. Range selection with hindsight. Bounds chosen after seeing the data will contain the data. A grid fitted to a period that ranged will show a high hit rate on that period.
  2. The unclosed ladder. A test that stops on a chosen end date often counts realized round trips while treating leftover inventory generously. The honest accounting marks the open position to market on the final day and includes that loss in the result.
  3. Fill assumptions. Simulated resting orders fill whenever price touches them. Real queues do not always fill at the touch, and the trips that fail to fill are disproportionately the profitable ones in fast markets.
  4. Parameter density. Bounds, line count, spacing, and order size give four dials over one price series, which is enough to fit almost any historical window.

The crossing counts above are the counterweight to all four. They come from price history alone, with no settings attached, and they vary year to year in a way no parameter choice controls. Anyone evaluating a grid claim can ask a single question of it: how many completed round trips does this depend on, and where did that count come from? A period of practice on a simulator, with the open ladder marked to market at the end, answers it more honestly than a chart of realized trips.

FAQ

Do grid trading bots actually make money?

A grid earns a capped profit on each completed round trip and carries an uncapped open position when price leaves its range. Whether the realized trips outweigh the open position over any period depends on how much the market oscillated inside that range, which is a property of the market rather than of the software.

What market conditions suit a grid bot?

The mechanism is built for a price that repeatedly revisits the same levels: high total distance traveled relative to net movement. In the twelve-name panel above, efficiency ratios ran from 0.2% at the wandering end to 14.3% at the directional end, and the ladder mechanism meets its own rungs only at the first end.

What happens when price breaks out of the grid range?

Above the range the bot has sold its inventory and sits in cash while the market continues without it. Below the range it holds every rung it bought on the way down, at an average cost above the current price, and each further decline widens that unrealized loss.

How is grid trading different from dollar-cost averaging?

Both buy more as price falls, and there the similarity ends. Dollar-cost averaging adds a fixed amount on a fixed schedule with no exit rule and no upper bound. A grid buys and sells on price triggers inside a range chosen in advance, and its behavior changes completely once price leaves that range.

Does tighter grid spacing produce more income?

Tighter spacing raises the number of round trips and lowers the gross profit on each one, while the round-trip cost stays the same. Past a point set by the spread, added trips subtract from the total rather than adding to it.


Every figure above comes from a stored, versioned query over real equity prices; open the SQL beneath any panel to check it, or measure travel against net movement on your own list of names on the Strasmore terminal.

#grid trading#trading bots#automation#mean reversion#market data