Crypto Bots

Crypto Market Making Bots: How to Profit From the Spread

Market making bots place simultaneous buy and sell orders around the current price, profiting from the bid-ask spread. Learn how to build a basic market maker on a DEX and CEX with maker rebate optimization.

A
AI Agents Hubยท2026-03-25ยท5 min readยท875 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 Market Making?

A market maker simultaneously quotes bid (buy) and ask (sell) prices for an asset. When a trader buys at your ask or sells at your bid, you capture the spread as profit.

Example:

  • BTC mid price: $65,000
  • Your bid: $64,935 (-0.10%)
  • Your ask: $65,065 (+0.10%)
  • Spread captured per round-trip: $130 (0.20%)

Market makers on CEXs typically earn maker rebates โ€” they get paid for providing liquidity. On Hyperliquid: -0.02% rebate. On Bybit: -0.01%. This means you earn money just for having orders resting on the book.

Why Market Making Is Attractive for Bots

  1. Directional neutrality: You profit whether price goes up or down (as long as orders fill)
  2. Frequent small profits: High fill rate = consistent income
  3. Maker rebates: Paid to provide liquidity on major exchanges
  4. Scales with capital: More capital = wider spreads = more profit

The risk: inventory risk โ€” if price moves sharply in one direction, you accumulate a losing position.

Building a Simple CEX Market Maker

import ccxt
import time
import math

class SimpleMarketMaker:
    def __init__(
        self,
        exchange: ccxt.Exchange,
        symbol: str,
        spread_pct: float = 0.002,    # 0.2% total spread
        order_size_usd: float = 100,  # $100 per order
        n_levels: int = 3,            # 3 bid + 3 ask levels
        rebalance_threshold: float = 0.6,  # Rebalance if 60%+ inventory
    ):
        self.exchange = exchange
        self.symbol = symbol
        self.spread = spread_pct
        self.order_size = order_size_usd
        self.n_levels = n_levels
        self.rebalance_threshold = rebalance_threshold
        self.open_orders = []
    
    def get_mid_price(self) -> float:
        ticker = self.exchange.fetch_ticker(self.symbol)
        return (ticker['bid'] + ticker['ask']) / 2
    
    def cancel_all_orders(self):
        """Cancel all open market making orders"""
        try:
            self.exchange.cancel_all_orders(self.symbol)
            self.open_orders = []
            print(f"Cancelled all orders for {self.symbol}")
        except Exception as e:
            print(f"Cancel error: {e}")
    
    def place_maker_orders(self, mid_price: float):
        """Place layered bid and ask orders around mid price"""
        new_orders = []
        
        base_currency = self.symbol.split('/')[0]
        qty_per_order = self.order_size / mid_price
        
        for level in range(1, self.n_levels + 1):
            # Each level slightly wider spread
            level_spread = self.spread * level * 0.5
            
            bid_price = mid_price * (1 - level_spread)
            ask_price = mid_price * (1 + level_spread)
            
            # Round to exchange precision
            bid_price = self.exchange.price_to_precision(self.symbol, bid_price)
            ask_price = self.exchange.price_to_precision(self.symbol, ask_price)
            qty = self.exchange.amount_to_precision(self.symbol, qty_per_order)
            
            try:
                bid_order = self.exchange.create_limit_buy_order(self.symbol, qty, bid_price)
                ask_order = self.exchange.create_limit_sell_order(self.symbol, qty, ask_price)
                
                new_orders.extend([bid_order['id'], ask_order['id']])
                print(f"Level {level}: BID ${float(bid_price):,.2f} | ASK ${float(ask_price):,.2f}")
            
            except Exception as e:
                print(f"Order placement error at level {level}: {e}")
        
        self.open_orders = new_orders
    
    def check_inventory_skew(self) -> float:
        """Return inventory ratio: 0.5 = balanced, >0.6 = too long, <0.4 = too short"""
        balance = self.exchange.fetch_balance()
        base = self.symbol.split('/')[0]
        quote = self.symbol.split('/')[1]
        
        base_value = balance[base]['total'] * self.get_mid_price()
        quote_value = balance[quote]['total']
        total = base_value + quote_value
        
        if total == 0:
            return 0.5
        
        return base_value / total
    
    def adjust_for_inventory(self, mid_price: float, inventory_ratio: float) -> tuple[float, float]:
        """Skew quotes based on inventory to rebalance naturally"""
        # If too long (holding too much base), move both prices down to encourage selling
        # If too short (holding too much quote), move prices up to encourage buying
        skew = (inventory_ratio - 0.5) * 0.001  # Small skew adjustment
        
        adjusted_mid = mid_price * (1 - skew)
        return adjusted_mid
    
    def run_cycle(self):
        """One cycle of market making: cancel stale orders โ†’ place fresh orders"""
        mid_price = self.get_mid_price()
        inventory = self.check_inventory_skew()
        
        print(f"\n๐Ÿ“Š Mid: ${mid_price:,.2f} | Inventory ratio: {inventory:.2f}")
        
        # Cancel all existing orders (stale quotes)
        self.cancel_all_orders()
        
        # Adjust for inventory
        adjusted_mid = self.adjust_for_inventory(mid_price, inventory)
        
        # Place fresh quotes
        self.place_maker_orders(adjusted_mid)
    
    def run(self, cycle_seconds: int = 30):
        """Run market making continuously"""
        print(f"๐Ÿค– Market maker started: {self.symbol}")
        print(f"   Spread: {self.spread*100:.2f}% | Orders: {self.n_levels} levels each side")
        
        while True:
            try:
                self.run_cycle()
                time.sleep(cycle_seconds)
            except KeyboardInterrupt:
                print("\nStopping... cancelling all orders")
                self.cancel_all_orders()
                break
            except Exception as e:
                print(f"Error: {e}")
                self.cancel_all_orders()
                time.sleep(10)

# Usage
exchange = ccxt.bybit({'apiKey': KEY, 'secret': SECRET})
mm = SimpleMarketMaker(exchange, 'ETH/USDT', spread_pct=0.002, n_levels=3)
mm.run()

Uniswap V3 as an Automated Market Maker

On DEXs, you provide liquidity in a price range โ€” essentially being a passive market maker:

from web3 import Web3

UNISWAP_V3_POSITION_MANAGER = '0xC36442b4a4522E871399CD717aBDD847Ab11FE88'

def calculate_optimal_range(
    current_price: float, 
    volatility_daily_pct: float,
    hold_days: int = 7
) -> tuple[float, float]:
    """
    Calculate optimal LP range based on expected price movement.
    Tighter range = higher fees but more impermanent loss risk.
    """
    # Expected price movement over holding period
    expected_move = volatility_daily_pct * math.sqrt(hold_days) / 100
    
    # Set range at 1.5x the expected move (balance fees vs IL)
    range_multiplier = 1 + (expected_move * 1.5)
    
    lower_price = current_price / range_multiplier
    upper_price = current_price * range_multiplier
    
    return lower_price, upper_price

# Example: BTC at $65,000, 3% daily vol, 7-day range
lower, upper = calculate_optimal_range(65000, 3.0, 7)
print(f"Optimal LP range: ${lower:,.0f} - ${upper:,.0f}")
# โ†’ $59,200 - $71,300

Market Making Profitability

Real profitability depends on:

  1. Spread captured vs impermanent loss (for DEX MM)
  2. Fill rate โ€” narrow spreads fill more but earn less per fill
  3. Maker rebates โ€” can add 0.01-0.02% per trade on top exchanges
  4. Volatility โ€” high vol = more fills but more inventory risk

On a CEX like Bybit with a 0.2% spread and 10 fills per day on $1,000 capital:

  • Daily revenue: 10 ร— $1,000 ร— 0.001 = $10
  • Monthly: ~$300
  • Annual yield: ~30%

Add maker rebates (0.01% ร— 20 fills ร— $1,000/day ร— 365 = $730/yr) and the total is compelling for a low-risk automated strategy.

Related Articles