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.
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
- Directional neutrality: You profit whether price goes up or down (as long as orders fill)
- Frequent small profits: High fill rate = consistent income
- Maker rebates: Paid to provide liquidity on major exchanges
- 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:
- Spread captured vs impermanent loss (for DEX MM)
- Fill rate โ narrow spreads fill more but earn less per fill
- Maker rebates โ can add 0.01-0.02% per trade on top exchanges
- 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.
Tagged in
Related Articles
Crypto Market Making: How Bots Profit from the Bid-Ask Spread
4 min read
Crypto BotsCrypto Bot Risk Management: The 10 Rules That Separate Winners From Losers
7 min read
Crypto BotsHow to Build a Self-Healing Trading Bot That Fixes Its Own Errors
5 min read
Crypto BotsPump.fun and Solana Meme Coin Bots: How to Automate the Hottest Trend
5 min read