Crypto Bots

Crypto Grid Trading Bot: The Complete 2026 Guide

Grid trading bots buy low and sell high automatically within a price range, generating income in sideways markets. Learn how grid trading works, optimal grid spacing math, and how to build one with CCXT.

A
AI Agents Hubยท2026-03-15ยท6 min readยท1,039 words

Builder of AI agents, crypto trading bots, and open-source automation tools. Sharing practical guides on how to build, deploy, and profit from AI and DeFi technology.

What Is Grid Trading?

Grid trading places buy and sell orders at regular price intervals within a range. When price moves up or down, orders fill automatically โ€” buying low and selling high on every oscillation.

Example grid on ETH ($3,000-$3,500 range, 10 grids):

Price Level | Action at Level | Profit per Grid
$3,500      | SELL all         | $50 (top)
$3,450      | SELL             | โ†• $50 spacing
$3,400      | SELL             |
$3,350      | SELL / BUY       | โ† Current price
$3,300      | BUY              |
$3,250      | BUY              |
$3,200      | BUY              |
$3,150      | BUY              |
$3,100      | BUY              |
$3,000      | BUY all          | (bottom)

Every time price oscillates $50 within the range, a buy and then sell fills, capturing $50 in profit. The more oscillations, the more profit.

Grid Trading Math

def calculate_grid_parameters(
    lower_price: float,
    upper_price: float,
    n_grids: int,
    capital_usd: float,
    arithmetic: bool = True  # True=arithmetic spacing, False=geometric (% spacing)
) -> dict:
    """Calculate optimal grid parameters"""
    
    if arithmetic:
        # Equal dollar spacing between grids
        step = (upper_price - lower_price) / n_grids
        levels = [lower_price + step * i for i in range(n_grids + 1)]
    else:
        # Equal percentage spacing (better for wider ranges)
        ratio = (upper_price / lower_price) ** (1 / n_grids)
        levels = [lower_price * (ratio ** i) for i in range(n_grids + 1)]
    
    # Capital allocation per grid
    base_capital = capital_usd / n_grids
    
    # For each buy level, calculate quantity
    orders = []
    for i, price in enumerate(levels[:-1]):  # Exclude top level (sell only)
        qty = base_capital / price
        profit_per_grid = (levels[i + 1] - price) * qty
        profit_pct = (levels[i + 1] - price) / price * 100
        
        orders.append({
            'buy_price': round(price, 2),
            'sell_price': round(levels[i + 1], 2),
            'qty': round(qty, 6),
            'profit_per_fill': round(profit_per_grid, 2),
            'profit_pct': round(profit_pct, 3),
        })
    
    # Estimate daily profit (assumes X oscillations per day)
    avg_daily_oscillations = 2  # Conservative estimate
    daily_profit = sum(o['profit_per_fill'] for o in orders) * avg_daily_oscillations / n_grids
    
    return {
        'levels': levels,
        'orders': orders,
        'grid_spacing_pct': ((levels[1] - levels[0]) / levels[0]) * 100,
        'capital_per_grid': base_capital,
        'estimated_daily_profit': daily_profit,
        'estimated_annual_yield': (daily_profit / capital_usd) * 365 * 100,
    }

# Example calculation
params = calculate_grid_parameters(
    lower_price=3000,
    upper_price=3500,
    n_grids=10,
    capital_usd=5000,
    arithmetic=True
)

print(f"Grid spacing: ${(params['levels'][1] - params['levels'][0]):.2f}")
print(f"Capital per grid: ${params['capital_per_grid']:.2f}")
print(f"Estimated daily profit: ${params['estimated_daily_profit']:.2f}")
print(f"Estimated annual yield: {params['estimated_annual_yield']:.1f}%")

Building the Grid Bot

import ccxt
import time
from typing import Optional

class GridBot:
    def __init__(
        self,
        exchange: ccxt.Exchange,
        symbol: str,
        lower: float,
        upper: float,
        n_grids: int,
        capital_usd: float,
    ):
        self.exchange = exchange
        self.symbol = symbol
        self.lower = lower
        self.upper = upper
        self.n_grids = n_grids
        self.capital = capital_usd
        
        # Calculate grid levels
        params = calculate_grid_parameters(lower, upper, n_grids, capital_usd)
        self.levels = params['levels']
        self.qty_per_grid = capital_usd / n_grids / lower  # approx qty
        
        self.open_orders = {}  # {order_id: level_info}
        self.filled_orders = []
        self.realized_pnl = 0
    
    def setup_initial_orders(self):
        """Place all initial grid orders based on current price"""
        current_price = self.get_current_price()
        
        print(f"\n๐Ÿ”ฒ Setting up grid: {self.lower} - {self.upper} ({self.n_grids} grids)")
        print(f"   Current price: ${current_price:.2f}")
        
        orders_placed = 0
        
        for i, level in enumerate(self.levels):
            if level < current_price:
                # Below current price โ†’ place buy orders
                qty = self.capital / self.n_grids / level
                qty = self.exchange.amount_to_precision(self.symbol, qty)
                price = self.exchange.price_to_precision(self.symbol, level)
                
                try:
                    order = self.exchange.create_limit_buy_order(self.symbol, qty, price)
                    self.open_orders[order['id']] = {
                        'level_index': i,
                        'type': 'buy',
                        'price': level,
                        'qty': float(qty),
                    }
                    orders_placed += 1
                except Exception as e:
                    print(f"  โŒ Failed to place buy at ${level}: {e}")
            
            elif level > current_price:
                # Above current price โ†’ place sell orders
                qty = self.capital / self.n_grids / current_price
                qty = self.exchange.amount_to_precision(self.symbol, qty)
                price = self.exchange.price_to_precision(self.symbol, level)
                
                try:
                    order = self.exchange.create_limit_sell_order(self.symbol, qty, price)
                    self.open_orders[order['id']] = {
                        'level_index': i,
                        'type': 'sell',
                        'price': level,
                        'qty': float(qty),
                    }
                    orders_placed += 1
                except Exception as e:
                    print(f"  โŒ Failed to place sell at ${level}: {e}")
        
        print(f"โœ… Placed {orders_placed} initial grid orders")
    
    def check_and_replace_filled_orders(self):
        """Check for filled orders and replace them with opposite orders"""
        
        for order_id, order_info in list(self.open_orders.items()):
            try:
                order = self.exchange.fetch_order(order_id, self.symbol)
                
                if order['status'] == 'closed':
                    # Order was filled!
                    del self.open_orders[order_id]
                    
                    level_idx = order_info['level_index']
                    fill_price = order['average']
                    qty = order['filled']
                    
                    if order_info['type'] == 'buy':
                        # Buy filled โ†’ place sell at next level up
                        next_level = self.levels[level_idx + 1]
                        new_order = self.exchange.create_limit_sell_order(
                            self.symbol, qty,
                            self.exchange.price_to_precision(self.symbol, next_level)
                        )
                        profit = (next_level - fill_price) * qty
                        print(f"๐Ÿ”„ BUY filled at ${fill_price:.2f} โ†’ placed SELL at ${next_level:.2f} (target ${profit:.2f} profit)")
                    
                    else:
                        # Sell filled โ†’ place buy at next level down
                        prev_level = self.levels[level_idx - 1]
                        new_order = self.exchange.create_limit_buy_order(
                            self.symbol, qty,
                            self.exchange.price_to_precision(self.symbol, prev_level)
                        )
                        profit = (fill_price - prev_level) * qty
                        self.realized_pnl += profit
                        print(f"๐Ÿ”„ SELL filled at ${fill_price:.2f} โ†’ placed BUY at ${prev_level:.2f} | Realized P&L: ${self.realized_pnl:.2f}")
                    
                    self.open_orders[new_order['id']] = {
                        'level_index': level_idx - 1 if order_info['type'] == 'sell' else level_idx + 1,
                        'type': 'buy' if order_info['type'] == 'sell' else 'sell',
                        'price': prev_level if order_info['type'] == 'sell' else next_level,
                        'qty': qty,
                    }
                    
            except Exception as e:
                print(f"Error checking order {order_id}: {e}")
    
    def check_price_in_range(self) -> bool:
        """Stop bot if price leaves the grid range"""
        price = self.get_current_price()
        if price < self.lower or price > self.upper:
            print(f"โš ๏ธ  Price ${price:.2f} outside grid range [{self.lower}, {self.upper}]")
            return False
        return True
    
    def get_current_price(self) -> float:
        return self.exchange.fetch_ticker(self.symbol)['last']
    
    def run(self, check_interval: int = 30):
        """Main grid bot loop"""
        self.setup_initial_orders()
        
        while True:
            try:
                if not self.check_price_in_range():
                    print("๐Ÿ›‘ Stopping grid bot โ€” price out of range")
                    self.exchange.cancel_all_orders(self.symbol)
                    break
                
                self.check_and_replace_filled_orders()
                time.sleep(check_interval)
                
            except KeyboardInterrupt:
                print(f"\nโน๏ธ  Bot stopped. Realized P&L: ${self.realized_pnl:.2f}")
                self.exchange.cancel_all_orders(self.symbol)
                break

# Run grid bot
exchange = ccxt.binance({'apiKey': KEY, 'secret': SECRET})
bot = GridBot(exchange, 'ETH/USDT', lower=3000, upper=3500, n_grids=10, capital_usd=5000)
bot.run()

When Grid Trading Works (and Fails)

Works well when:

  • Price oscillates within a predictable range (sideways/consolidation)
  • High volatility within the range (more fills = more profit)
  • Low trend bias โ€” ranging, not trending

Fails when:

  • Price breaks out of the range (all buys filled, price drops = unrealized loss)
  • Strong trending market (keeps filling one side only)

Pro tip: Set your grid range based on the 30-day ATR (Average True Range). A range of ยฑ2 ATR from current price gives a statistically reasonable expectation that price stays within bounds.

Grid trading is one of the simplest profitable bot strategies to build and one of the most widely used by retail algorithmic traders. Master it before moving to more complex approaches.

Related Articles