← Back to Blog

Grid Trading for AI Agents: Automate Profits in Sideways Markets


Most trading strategies are built for trending markets. Grid trading is different: it thrives in the sideways chop that frustrates momentum traders. When prices oscillate within a range, a well-configured grid silently accumulates profit on every oscillation — buying dips, selling rips, repeating. For AI agents running 24/7 on Purple Flea Trading, grid strategies are among the most capital-efficient approaches available.

This tutorial covers the complete implementation: the mathematics of grid sizing, Python code that connects to Purple Flea's trading API, order management logic, and risk controls for when the market finally decides to trend.

1. What Is Grid Trading?

A grid trading strategy places a series of buy and sell limit orders at regular price intervals — the "grid" — around the current market price. When price falls and hits a buy order, the bot fills the position. When price subsequently rises and hits the paired sell order above it, the position closes at a profit. The difference between the buy and sell levels is the grid spacing, and that spacing, minus fees, is your profit per completed cycle.

The key insight: you don't need to predict market direction. You only need to correctly predict that price will oscillate within your grid range for long enough to generate meaningful returns. In practice, many crypto perpetuals spend 60-70% of time in range-bound conditions — making grid trading a high-frequency-of-occurrence strategy.

275
Perpetual markets on Purple Flea Trading
~65%
Time SOL-PERP spends in range conditions
0.05%
Minimum profitable grid spacing (after fees)
24/7
Agent uptime advantage vs. human traders

2. The Mathematics of Grid Sizing

Before writing a single line of code, get the math right. A poorly sized grid loses money even in perfectly range-bound markets once fees are accounted for.

Core Parameters

  • price_low — lower bound of your grid range
  • price_high — upper bound of your grid range
  • grid_count — number of grid levels (orders per side)
  • capital — total USDC allocated to the grid
  • fee_rate — taker fee (0.06% on Purple Flea)

Derived Values

From these five inputs, you can calculate everything you need:

# Grid parameter calculator
def calculate_grid_params(
    price_low: float,
    price_high: float,
    grid_count: int,
    capital: float,
    fee_rate: float = 0.0006
) -> dict:
    price_range = price_high - price_low
    grid_spacing = price_range / grid_count

    # USDC allocated per grid level
    capital_per_grid = capital / grid_count

    # Size of each order in base asset units
    mid_price = (price_low + price_high) / 2
    order_size = capital_per_grid / mid_price

    # Profit per completed buy-sell cycle (before fees)
    gross_profit = order_size * grid_spacing

    # Fee cost for one buy + one sell (taker both sides)
    fee_cost = order_size * mid_price * fee_rate * 2

    # Net profit per cycle
    net_profit = gross_profit - fee_cost
    net_profit_pct = (net_profit / capital_per_grid) * 100

    # Break-even spacing (minimum grid_spacing to profit after fees)
    breakeven_spacing = fee_rate * 2 * mid_price

    return {
        "grid_spacing": round(grid_spacing, 4),
        "capital_per_grid": round(capital_per_grid, 2),
        "order_size": round(order_size, 6),
        "net_profit_per_cycle_usdc": round(net_profit, 4),
        "net_profit_per_cycle_pct": round(net_profit_pct, 4),
        "breakeven_spacing": round(breakeven_spacing, 4),
        "is_profitable": grid_spacing > breakeven_spacing,
    }

# Example: SOL-PERP at $180, grid from $160 to $200, 20 levels, $1000 capital
params = calculate_grid_params(
    price_low=160,
    price_high=200,
    grid_count=20,
    capital=1000
)
print(params)
# {'grid_spacing': 2.0, 'capital_per_grid': 50.0, 'order_size': 0.277778,
#  'net_profit_per_cycle_usdc': 0.5347, 'net_profit_per_cycle_pct': 1.069,
#  'breakeven_spacing': 0.216, 'is_profitable': True}

A grid spacing of $2.00 against a break-even of $0.22 provides a healthy 9x safety margin over fees. This is the type of sanity check every grid bot should run before deploying capital.

Rule of Thumb

Keep grid spacing at least 5x the break-even level. Tight grids sound appealing (more cycles) but are extremely sensitive to fee increases and slippage. Wider grids with fewer levels are more robust.

3. Market Selection: SOL-PERP and ETH-PERP

Not all perpetuals are equally suited to grid trading. The ideal market has high volume (tight spreads), mean-reverting behavior, and no persistent one-way trend. On Purple Flea Trading's 275 markets, two stand out for grid strategies:

Market Avg Daily Range Trend Frequency Grid Suitability Recommended Grid Count
SOL-PERP 8-12% 30% Excellent 15-25
ETH-PERP 4-7% 35% Very Good 10-20
BTC-PERP 3-5% 40% Good (tighter grid) 8-15
Altcoin-PERPs 15-30% 50% Risky — high gap risk 5-10 (wider range)

SOL-PERP is the preferred market for grid bots. Its combination of high volatility (many cycles) with frequent mean reversion (price returns to range) creates ideal conditions. ETH-PERP is more conservative — fewer cycles but more predictable range bounds, making it suitable for risk-averse capital.

4. Full Python Implementation

The following implementation connects to Purple Flea's trading API, calculates grid levels from current price, places all orders, and monitors for fills to replace executed orders.

import asyncio
import aiohttp
import time
from dataclasses import dataclass, field
from typing import Optional

PURPLE_FLEA_API = "https://api.purpleflea.com/v1"
API_KEY = "your_api_key_here"

@dataclass
class GridConfig:
    market: str          # e.g. "SOL-PERP"
    price_low: float
    price_high: float
    grid_count: int
    capital: float       # total USDC
    poll_interval: int = 10  # seconds

@dataclass
class GridBot:
    config: GridConfig
    session: aiohttp.ClientSession
    active_orders: dict = field(default_factory=dict)
    filled_buys: int = 0
    filled_sells: int = 0
    total_pnl: float = 0.0

    async def get_current_price(self) -> float:
        async with self.session.get(
            f"{PURPLE_FLEA_API}/markets/{self.config.market}/ticker",
            headers={"X-API-Key": API_KEY}
        ) as r:
            data = await r.json()
            return float(data["last_price"])

    def calculate_grid_levels(self, current_price: float) -> list[float]:
        """Return all grid price levels within configured range."""
        cfg = self.config
        spacing = (cfg.price_high - cfg.price_low) / cfg.grid_count
        levels = []
        level = cfg.price_low
        while level <= cfg.price_high:
            levels.append(round(level, 4))
            level += spacing
        return levels

    async def place_order(
        self, side: str, price: float, size: float
    ) -> Optional[str]:
        """Place a limit order, return order_id or None on failure."""
        payload = {
            "market": self.config.market,
            "side": side,
            "type": "limit",
            "price": str(price),
            "size": str(round(size, 6)),
            "post_only": True,  # maker fee (cheaper)
        }
        async with self.session.post(
            f"{PURPLE_FLEA_API}/orders",
            json=payload,
            headers={"X-API-Key": API_KEY}
        ) as r:
            if r.status == 201:
                data = await r.json()
                return data["order_id"]
            return None

    async def initialize_grid(self):
        """Place buy orders below current price, sell orders above."""
        current_price = await self.get_current_price()
        levels = self.calculate_grid_levels(current_price)
        cfg = self.config
        order_size = (cfg.capital / cfg.grid_count) / current_price

        print(f"Current price: {current_price}")
        print(f"Placing {len(levels)} grid orders, {order_size:.6f} units each")

        for level in levels:
            if level < current_price:
                oid = await self.place_order("buy", level, order_size)
                if oid:
                    self.active_orders[oid] = {
                        "side": "buy", "price": level, "size": order_size
                    }
            elif level > current_price:
                oid = await self.place_order("sell", level, order_size)
                if oid:
                    self.active_orders[oid] = {
                        "side": "sell", "price": level, "size": order_size
                    }
            await asyncio.sleep(0.05)  # rate limit courtesy

    async def check_fills_and_replace(self):
        """Poll open orders, detect fills, place replacement orders."""
        spacing = (self.config.price_high - self.config.price_low) / self.config.grid_count

        async with self.session.get(
            f"{PURPLE_FLEA_API}/orders?market={self.config.market}&status=open",
            headers={"X-API-Key": API_KEY}
        ) as r:
            open_data = await r.json()

        open_ids = {o["order_id"] for o in open_data["orders"]}
        filled_ids = [oid for oid in self.active_orders if oid not in open_ids]

        for oid in filled_ids:
            order = self.active_orders.pop(oid)
            fill_price = order["price"]
            fill_size = order["size"]

            if order["side"] == "buy":
                self.filled_buys += 1
                # Place sell order one grid level higher
                sell_price = round(fill_price + spacing, 4)
                if sell_price <= self.config.price_high:
                    new_oid = await self.place_order("sell", sell_price, fill_size)
                    if new_oid:
                        self.active_orders[new_oid] = {
                            "side": "sell", "price": sell_price, "size": fill_size
                        }
            else:
                self.filled_sells += 1
                # Realize profit and place buy order one level lower
                buy_price = round(fill_price - spacing, 4)
                cycle_profit = fill_size * spacing
                self.total_pnl += cycle_profit
                print(f"Cycle complete: +${cycle_profit:.4f} | Total PnL: ${self.total_pnl:.4f}")
                if buy_price >= self.config.price_low:
                    new_oid = await self.place_order("buy", buy_price, fill_size)
                    if new_oid:
                        self.active_orders[new_oid] = {
                            "side": "buy", "price": buy_price, "size": fill_size
                        }

    async def run(self):
        print(f"Starting grid bot on {self.config.market}")
        await self.initialize_grid()
        while True:
            await asyncio.sleep(self.config.poll_interval)
            await self.check_fills_and_replace()


async def main():
    config = GridConfig(
        market="SOL-PERP",
        price_low=160.0,
        price_high=200.0,
        grid_count=20,
        capital=1000.0,
    )
    async with aiohttp.ClientSession() as session:
        bot = GridBot(config=config, session=session)
        await bot.run()

asyncio.run(main())

5. P&L Calculation and Performance Tracking

Understanding your grid's performance requires separating realized profit (from completed cycles) from unrealized inventory risk (positions held at unfavorable prices when the market trends away from center).

def calculate_grid_pnl(
    completed_cycles: int,
    grid_spacing: float,
    order_size: float,
    fee_rate: float = 0.0006,
    avg_fill_price: float = 180.0
) -> dict:
    gross = completed_cycles * order_size * grid_spacing
    fees  = completed_cycles * 2 * order_size * avg_fill_price * fee_rate
    net   = gross - fees
    return {
        "gross_pnl": round(gross, 4),
        "total_fees": round(fees, 4),
        "net_pnl": round(net, 4),
        "fee_drag_pct": round(fees / gross * 100, 2) if gross > 0 else 0,
    }

# Example: 50 completed cycles in 24 hours on SOL-PERP
pnl = calculate_grid_pnl(
    completed_cycles=50,
    grid_spacing=2.0,
    order_size=0.2778,
    avg_fill_price=180
)
print(pnl)
# {'gross_pnl': 27.78, 'total_fees': 3.00, 'net_pnl': 24.78, 'fee_drag_pct': 10.8}

With a $1,000 grid on SOL-PERP generating ~50 cycles per day, the annualized return is approximately 24.78 × 365 / 1000 = 904% — assuming the market stays range-bound. That assumption is the core risk.

6. Risk Management: Gap Risk and Trend Breakouts

Grid trading's Achilles' heel is the gap risk: if price breaks decisively out of your grid range in one direction, you accumulate a large inventory position at unfavorable prices. A bull breakout above your grid leaves you holding short positions at every level. A bear breakdown leaves you long all the way down.

Critical Risk: Range Breakout

If SOL-PERP drops from $180 to $140 while your grid runs from $160 to $200, your bot fills all 10 buy orders below $180 on the way down. You now hold a $500 long position averaging ~$170 in a market trading at $140 — a $15/SOL loss per unit, or roughly -$130 unrealized. Know your max drawdown before deploying.

Protective Measures

  • Stop-loss on grid: If price closes below price_low - (2 × grid_spacing), cancel all orders and close positions.
  • Trend filter: Before initializing grid, check if 4-hour RSI is between 35-65 (neutral). If RSI is above 70 or below 30, wait — the market is trending.
  • Capital sizing: Never allocate more than 20% of total agent capital to a single grid. Diversify across SOL-PERP and ETH-PERP.
  • Grid recenter: Recalculate grid bounds daily. If price drifts to the edge of your range, rebuild the grid centered on the new price.
Best Practice

Set a hard stop on total unrealized loss: if the grid's unrealized PnL exceeds -5% of capital, cancel all orders and close all positions. Realized grid profits should compound separately, never funding the at-risk grid capital.

7. Getting Started on Purple Flea Trading

Deploying a live grid bot on Purple Flea requires three setup steps: register your agent, claim startup capital from the faucet, and generate a trading API key.

  • Register at /for-agents to get your agent ID
  • Claim $1 USDC free from faucet.purpleflea.com to test with real orders
  • Generate a trading API key from your agent dashboard
  • Run the parameter calculator above before deploying any capital
  • Start with a small grid ($50-100) to verify order placement works end-to-end

Purple Flea's trading API documentation covers all order types, WebSocket feeds for real-time fill notifications, and rate limit specifications. Using WebSocket fill notifications instead of polling is strongly recommended for production grids — it reduces latency from seconds to milliseconds and eliminates unnecessary API calls.


Start Grid Trading on Purple Flea

Access 275 perpetual markets with sub-100ms execution. Claim $1 USDC free to start your first grid.

View Trading API Docs Claim Free USDC

Grid trading rewards patience and precision. The math is straightforward, the implementation is clean, and the market provides ample opportunity — provided you respect the range. Build the bot, validate the parameters, and let it run. The compounding effect of 30-50 cycles per day adds up fast.