Most trading strategies assume price goes somewhere: up for trend-followers, down for short-sellers. Grid trading assumes something more realistic: price oscillates. In a ranging market, an asset bounces between a floor and ceiling. Grid bots exploit every bounce — buying on dips, selling on pops, collecting the bid-ask spread on each completed cycle.
For AI agents, grid trading is a natural fit. It requires no directional prediction, no fundamental analysis, no news interpretation. The agent deploys a mechanical system, monitors fills, places replacement orders, and accumulates small profits at scale. This article covers the mechanics, the math, the optimal spacing calculation, and a complete Python GridBot class using the Purple Flea Trading API.
How Grid Trading Works
The concept is straightforward. You define a price range — say $2,800 to $3,400 for ETH/USDC — and divide it into N equally-spaced levels. At each level, you place a buy order below current price and a sell order above. When any buy fills, you immediately place a sell at the next level up. When any sell fills, you immediately place a buy at the next level down.
Every completed buy-sell cycle generates profit equal to the grid spacing minus trading fees. If the asset oscillates continuously within your range, each grid level fires multiple times per day. The more oscillations, the more profit.
When price drops to $3,000, the BUY fills. The bot immediately places a SELL at $3,060. When price rises back to $3,060, the SELL fills. Net profit per cycle: $60 minus fees. Repeat across 9 active grid levels, multiple times per day.
Spacing Calculation: The Core Math
Grid spacing determines profitability. Too narrow: fees eat the profit. Too wide: fewer cycles per day, lower total return. The optimal spacing depends on asset volatility, trading fees, and desired capital allocation.
from decimal import Decimal import math def calculate_grid_params( lower_price: Decimal, upper_price: Decimal, capital_usdc: Decimal, trading_fee_pct: Decimal = Decimal("0.001"), # 0.1% per side min_profit_per_cycle_pct: Decimal = Decimal("0.003"), ) -> dict: """ Calculate optimal grid parameters given a price range and capital. Returns: num_levels, spacing, capital_per_level, expected_profit_per_cycle """ price_range = upper_price - lower_price midpoint = (upper_price + lower_price) / 2 # Minimum spacing to be profitable after fees (both sides) min_spacing_pct = (trading_fee_pct * 2) + min_profit_per_cycle_pct min_spacing_abs = midpoint * min_spacing_pct # Number of levels that fit with minimum spacing num_levels = int(price_range / min_spacing_abs) num_levels = max(num_levels, 4) # at least 4 levels num_levels = min(num_levels, 50) # cap at 50 orders (API limits) actual_spacing = price_range / num_levels capital_per_level = capital_usdc / num_levels # Profit per completed cycle (buy + sell = 1 cycle) profit_per_cycle_usdc = (actual_spacing / midpoint) * capital_per_level fee_per_cycle_usdc = capital_per_level * trading_fee_pct * 2 net_per_cycle_usdc = profit_per_cycle_usdc - fee_per_cycle_usdc return { "num_levels": num_levels, "spacing": round(actual_spacing, 2), "spacing_pct": round(actual_spacing / midpoint * 100, 3), "capital_per_level": round(capital_per_level, 2), "net_per_cycle_usdc": round(net_per_cycle_usdc, 4), "breakeven_cycles_day": 0, # depends on volatility } # Example: ETH/USDC, $2,700 to $3,600, $10,000 capital params = calculate_grid_params( lower_price=Decimal("2700"), upper_price=Decimal("3600"), capital_usdc=Decimal("10000"), ) print(params) # {num_levels: 18, spacing: 50.0, spacing_pct: 1.587, # capital_per_level: 555.56, net_per_cycle_usdc: 6.89}
With 18 levels and $6.89 net profit per completed cycle, 5 cycles per day across the grid yields approximately $34.45/day on $10,000 capital — roughly 0.34% daily return, or 125% annualized (assuming consistent ranging conditions).
Grid trading loses money when price trends strongly outside your defined range. If you set a $2,700–$3,600 range and ETH breaks to $4,500, your sell orders exhaust and you hold only USDC — missing the upside. Set ranges wide enough to absorb expected volatility, or implement dynamic range extension.
Purple Flea Trading API Integration
The Purple Flea Trading API provides limit order placement, order status polling, and cancellation — the three operations a grid bot needs. With 275+ markets and a referral rate of 20% on trading fees your bot generates, it is a natural execution venue for AI agents.
Key API endpoints for grid trading:
| Operation | Endpoint | Notes |
|---|---|---|
| Place limit order | POST /api/v1/orders | side, price, qty, market |
| Get order status | GET /api/v1/orders/:id | open | filled | cancelled |
| Cancel order | DELETE /api/v1/orders/:id | for range rebalancing |
| List open orders | GET /api/v1/orders?status=open | paginated, max 100 |
| Get ticker | GET /api/v1/markets/:market/ticker | last, bid, ask |
Full Python GridBot Class
import time, os, logging from decimal import Decimal from typing import Optional import requests logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger("gridbot") TRADING_BASE = "https://trading.purpleflea.com" class GridBot: """ Grid trading bot for Purple Flea Trading API. Maintains a set of buy and sell limit orders across evenly-spaced price levels. When a fill is detected, replaces it with a counter-order one level in the opposite direction. """ def __init__( self, api_key: str, market: str, lower_price: Decimal, upper_price: Decimal, num_levels: int, capital_usdc: Decimal, poll_interval: float = 5.0, ): self.api_key = api_key self.market = market self.lower = lower_price self.upper = upper_price self.num_levels = num_levels self.capital = capital_usdc self.poll_interval = poll_interval self.session = requests.Session() self.session.headers["Authorization"] = f"Bearer {api_key}" self.session.headers["Content-Type"] = "application/json" # spacing between grid levels self.spacing = (upper_price - lower_price) / (num_levels - 1) self.levels = [ lower_price + self.spacing * i for i in range(num_levels) ] self.qty_per_level = (capital_usdc / num_levels) / ( (upper_price + lower_price) / 2 ) # order_id -> {side, price, level_idx} self.open_orders: dict = {} self.filled_count = 0 self.total_profit = Decimal("0") def _post(self, path: str, data: dict) -> dict: r = self.session.post(f"{TRADING_BASE}{path}", json=data, timeout=10) r.raise_for_status() return r.json() def _get(self, path: str, params: dict = None) -> dict: r = self.session.get(f"{TRADING_BASE}{path}", params=params, timeout=10) r.raise_for_status() return r.json() def _delete(self, path: str) -> None: r = self.session.delete(f"{TRADING_BASE}{path}", timeout=10) r.raise_for_status() def get_current_price(self) -> Decimal: data = self._get(f"/api/v1/markets/{self.market}/ticker") return Decimal(str(data["last"])) def place_order(self, side: str, price: Decimal, level_idx: int) -> Optional[str]: """Place a limit order and track it. Returns order_id or None on error.""" try: resp = self._post("/api/v1/orders", { "market": self.market, "side": side, "type": "limit", "price": str(round(price, 2)), "qty": str(round(self.qty_per_level, 6)), }) order_id = resp["id"] self.open_orders[order_id] = { "side": side, "price": price, "level_idx": level_idx, } log.info(f"Placed {side} @ {price:.2f} (level {level_idx}) id={order_id}") return order_id except Exception as e: log.warning(f"Failed to place {side} @ {price:.2f}: {e}") return None def initialize_grid(self, current_price: Decimal) -> None: """Place initial orders: buys below current price, sells above.""" log.info(f"Initializing grid. Current price: {current_price:.2f}") log.info(f"Range: {self.lower}–{self.upper} Levels: {self.num_levels} Spacing: {self.spacing:.2f}") for i, level_price in enumerate(self.levels): if level_price < current_price: self.place_order("buy", level_price, i) time.sleep(0.1) # avoid rate limits elif level_price > current_price: self.place_order("sell", level_price, i) time.sleep(0.1) log.info(f"Grid initialized. {len(self.open_orders)} orders placed.") def check_fills(self) -> None: """Poll all tracked orders. On fill, place counter-order and update P&L.""" filled_ids = [] for order_id, meta in list(self.open_orders.items()): try: resp = self._get(f"/api/v1/orders/{order_id}") status = resp["status"] if status == "filled": filled_ids.append(order_id) side = meta["side"] price = meta["price"] level_idx = meta["level_idx"] self.filled_count += 1 # Place counter-order one level in opposite direction if side == "buy": counter_price = price + self.spacing counter_side = "sell" # Profit realized when sell fills = spacing - 2*fee else: # sell filled counter_price = price - self.spacing counter_side = "buy" if self.lower <= counter_price <= self.upper: self.place_order(counter_side, counter_price, level_idx) log.info( f"FILL: {side} @ {price:.2f} fill #{self.filled_count}" ) elif status == "cancelled": filled_ids.append(order_id) # remove from tracking except Exception as e: log.warning(f"Error checking order {order_id}: {e}") for oid in filled_ids: self.open_orders.pop(oid, None) def cancel_all(self) -> None: """Cancel all open orders (called on shutdown).""" for order_id in list(self.open_orders.keys()): try: self._delete(f"/api/v1/orders/{order_id}") log.info(f"Cancelled {order_id}") except Exception as e: log.warning(f"Failed to cancel {order_id}: {e}") self.open_orders.clear() def run(self) -> None: """Main run loop. Initializes grid then polls for fills.""" current_price = self.get_current_price() self.initialize_grid(current_price) try: while True: self.check_fills() log.info( f"Status: {len(self.open_orders)} open orders, " f"{self.filled_count} total fills" ) time.sleep(self.poll_interval) except KeyboardInterrupt: log.info("Shutting down. Cancelling all orders...") self.cancel_all() # --- Entry point --- if __name__ == "__main__": bot = GridBot( api_key = os.environ["PURPLE_FLEA_API_KEY"], market = "ETH-USDC", lower_price = Decimal("2700"), upper_price = Decimal("3600"), num_levels = 18, capital_usdc = Decimal("10000"), poll_interval= 5.0, ) bot.run()
Expected Returns by Market Condition
Grid trading return varies dramatically with market conditions. The key driver is the number of grid oscillations per day, which depends on the asset's historical volatility relative to your grid spacing.
| Market Condition | Oscillations/day | Daily Return (est.) | Monthly Return (est.) |
|---|---|---|---|
| High volatility ranging | 12–20 | 0.5–0.9% | 15–28% |
| Moderate ranging | 4–8 | 0.2–0.4% | 6–12% |
| Low volatility ranging | 1–3 | 0.05–0.15% | 1.5–5% |
| Trending (outside range) | 0 | 0% (exposure risk) | Negative |
A human grid trader cannot practically manage more than 2–3 grids across different assets. An AI agent can run 20+ simultaneous grids with automated fill detection, counter-order placement, and P&L tracking. Portfolio diversification across uncorrelated assets dramatically improves return consistency.
Dynamic Range Extension
The biggest failure mode for grid bots is price escaping the defined range. Rather than letting this happen passively, your agent should monitor price relative to range boundaries and adjust:
def check_range_extension(bot: GridBot) -> None: """ If price approaches the range boundary (within 2 levels), extend the range by cancelling outermost orders on the other side and placing new orders beyond the boundary. """ current = bot.get_current_price() buffer = bot.spacing * 2 if current > bot.upper - buffer: log.info(f"Price {current:.2f} near upper bound {bot.upper:.2f}. Extending range up.") # Cancel lowest buy order, add new sell above upper new_upper = bot.upper + bot.spacing bot.place_order("sell", new_upper, bot.num_levels) bot.upper = new_upper elif current < bot.lower + buffer: log.info(f"Price {current:.2f} near lower bound {bot.lower:.2f}. Extending range down.") new_lower = bot.lower - bot.spacing bot.place_order("buy", new_lower, -1) bot.lower = new_lower
Capital Allocation Across Multiple Grids
Running a single grid concentrates all capital in one asset and one range. A more robust approach allocates across multiple uncorrelated grids:
- ETH/USDC: 40% of capital — highest liquidity, consistent ranging behavior
- BTC/USDC: 30% of capital — lower volatility, wider spacing required
- SOL/USDC: 20% of capital — higher volatility, tighter spacing viable
- Reserve: 10% USDC — for range extension funding
With uncorrelated assets, the probability that all grids simultaneously trend outside range is low. Diversification smooths returns across market regimes.
Getting Started
To run the GridBot using Purple Flea infrastructure:
- Register at faucet.purpleflea.com to get $1 USDC free for testing.
- Authenticate with the Purple Flea Trading API.
- Run the grid calculator above to choose appropriate level counts and spacing.
- Start with 5–8 levels and $500–$1,000 capital to validate fill detection before scaling.
- Monitor daily P&L and adjust grid range if price trends persistently in one direction.
Grid bots generate significant trading volume. The Purple Flea Trading API offers a 20% referral rate on all fees generated by agents you refer. If you share your referral link and other agents use it to execute grid strategies, you earn a passive income stream on top of your own trading returns.
- Trading API: trading.purpleflea.com
- Faucet (free $1 USDC): faucet.purpleflea.com
- Docs: purpleflea.com/docs