Inter-Exchange Arbitrage Bot Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Inter-Exchange Arbitrage Bot Development
Medium
~1-2 weeks
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Market-making Bot Development

Market-making is the simultaneous placement of buy and sell orders to profit from the spread. A market maker (MM) provides liquidity to the market and earns when both bid and ask are executed. The main enemy of MM is adverse selection: trading against an informed trader who knows the price will move.

Market-making Economics

MM earns from spread capture: if bid = $42,000 and ask = $42,100, and both are executed — profit is $100 (minus fees). But if the price drops to $41,800 during that time, MM bought at $42,000 and now holds a losing position.

A successful MM must:

  • Earn enough from spread capture to cover adverse selection losses
  • Manage inventory risk (can't accumulate too much position in one direction)
  • Adapt spread to current volatility

Basic Quoting Strategy

from decimal import Decimal
import asyncio

class MarketMaker:
    def __init__(self, config: MMConfig):
        self.pair = config.pair
        self.base_spread_bps = Decimal(str(config.spread_bps))  # basis points
        self.order_levels = config.levels  # number of levels in the book
        self.level_size_usd = Decimal(str(config.level_size_usd))
        self.max_position_usd = Decimal(str(config.max_position_usd))
        
    async def run(self):
        while True:
            try:
                reference_price = await self.get_reference_price()
                inventory = await self.get_inventory()
                volatility = await self.estimate_volatility()
                
                quotes = self.calculate_quotes(reference_price, inventory, volatility)
                await self.update_orders(quotes)
                
                await asyncio.sleep(1)  # update every second
            except Exception as e:
                logger.error(f"MM cycle error: {e}")
                await self.cancel_all()
                await asyncio.sleep(5)  # pause before retry
    
    def calculate_quotes(self, mid: Decimal, inventory: Decimal, volatility: Decimal) -> Quotes:
        # Base spread
        half_spread = mid * self.base_spread_bps / Decimal('10000') / 2
        
        # Volatility adjustment: higher volatility — wider spread
        vol_multiplier = Decimal('1') + volatility * Decimal('10')  # 1x at vol=0, 2x at vol=10%
        adjusted_spread = half_spread * vol_multiplier
        
        # Inventory skew: shift quotes to reduce imbalance
        skew = self.calculate_inventory_skew(inventory, mid)
        
        best_bid = mid - adjusted_spread + skew
        best_ask = mid + adjusted_spread + skew
        
        # Generate multiple levels
        bids = []
        asks = []
        for i in range(self.order_levels):
            level_adjustment = adjusted_spread * Decimal(str(i)) * Decimal('0.5')
            size = self.level_size_usd / (best_bid - level_adjustment)
            
            bids.append(Quote(price=best_bid - level_adjustment, size=size))
            asks.append(Quote(price=best_ask + level_adjustment, size=size))
        
        return Quotes(bids=bids, asks=asks)
    
    def calculate_inventory_skew(self, inventory_usd: Decimal, mid: Decimal) -> Decimal:
        """
        Shift quoting based on current inventory.
        When accumulating long position — lower bid, raise ask (sell more aggressively).
        """
        target = Decimal('0')  # target inventory = 0 (neutral)
        max_inventory = self.max_position_usd
        
        inventory_ratio = inventory_usd / max_inventory
        inventory_ratio = max(Decimal('-1'), min(Decimal('1'), inventory_ratio))
        
        # Skew proportional to inventory: at 100% position — shift by 1 full spread
        skew_pct = Decimal('-0.0001') * inventory_ratio  # -0.01% * ratio
        return mid * skew_pct

Volatility Estimation

def estimate_volatility(self, prices: list[Decimal], window: int = 20) -> Decimal:
    """
    Realized volatility over last N periods.
    Used for adaptive spread widening.
    """
    if len(prices) < window + 1:
        return Decimal('0.01')  # default if insufficient data
    
    returns = []
    for i in range(1, window + 1):
        ret = float(prices[-i] / prices[-(i+1)] - 1)
        returns.append(ret ** 2)
    
    variance = sum(returns) / len(returns)
    vol = Decimal(str(variance ** 0.5))
    
    # Annualized volatility: for 1-minute data
    # multiply by sqrt(525600 minutes per year)
    return vol * Decimal(str((525600 ** 0.5)))

Order Management

Avoiding Unnecessary Cancel/Replace

Each cancellation and new order incurs a fee and latency. Optimization: only update an order if the price has shifted sufficiently.

async def update_orders(self, new_quotes: Quotes):
    current_orders = await self.get_open_orders()
    
    # Compare current orders with new quotes
    orders_to_cancel = []
    orders_to_place = []
    
    for new_bid in new_quotes.bids:
        matching = self.find_matching_order(current_orders.bids, new_bid.price)
        
        if matching:
            # Check if update is needed
            price_diff = abs(matching.price - new_bid.price) / matching.price
            if price_diff > Decimal('0.0005'):  # change > 0.05% — update
                orders_to_cancel.append(matching.id)
                orders_to_place.append(new_bid)
            # Otherwise keep existing order
        else:
            orders_to_place.append(new_bid)
    
    # Batch cancel + place to minimize latency
    if orders_to_cancel:
        await self.exchange.cancel_orders(orders_to_cancel)
    
    if orders_to_place:
        await asyncio.gather(*[
            self.exchange.place_order(quote) for quote in orders_to_place
        ])

Emergency Cancel

In case of sharp market movement or technical error — immediate cancellation of all orders:

async def handle_price_shock(self, current_price: Decimal, reference_price: Decimal):
    price_change = abs(current_price - reference_price) / reference_price
    
    if price_change > Decimal('0.02'):  # > 2% during update period
        logger.warning(f"Price shock detected: {price_change:.2%}. Cancelling all orders.")
        await self.exchange.cancel_all_orders(self.pair)
        
        # Pause and monitor
        await asyncio.sleep(30)
        
        # Check if price has stabilized
        new_price = await self.get_reference_price()
        new_change = abs(new_price - reference_price) / reference_price
        
        if new_change < Decimal('0.005'):  # < 0.5% — resume
            logger.info("Price stabilized, resuming market making")
            self.reference_price = new_price

Market-making Risks

Adverse Selection

A large participant knows the price will rise and aggressively buys from MM at bid. MM ends up with long position before the drop.

Detection: if all last N fills are on one side — stop and reassess. Sign of informed trading.

def detect_adverse_selection(self, recent_fills: list[Fill]) -> bool:
    if len(recent_fills) < 5:
        return False
    
    # All last 5 fills on one side — suspicious
    sides = [f.side for f in recent_fills[-5:]]
    if len(set(sides)) == 1:
        return True
    
    # Fill volume spiked
    avg_fill_volume = sum(f.quantity for f in recent_fills[:-3]) / max(len(recent_fills) - 3, 1)
    recent_volume = sum(f.quantity for f in recent_fills[-3:]) / 3
    
    if recent_volume > avg_fill_volume * 3:
        return True
    
    return False

Naked Position Risk

Hard limit on maximum position — strict circuit breaker:

async def check_position_limits(self):
    position = await self.get_net_position()  # $ in base asset
    
    if abs(position) > self.max_position_usd:
        logger.critical(f"Position limit exceeded: {position} USD")
        await self.cancel_all_orders()
        
        # Forced liquidation of excess position
        excess = abs(position) - self.max_position_usd
        if position > 0:  # long excess
            await self.market_sell(excess / self.last_price)
        else:  # short excess
            await self.market_buy(abs(excess) / self.last_price)

P&L Calculation

class MMPnLTracker:
    def __init__(self):
        self.realized_pnl = Decimal('0')
        self.fill_history: list[Fill] = []
        self.position = Decimal('0')  # in base currency
        self.avg_cost = Decimal('0')
    
    def on_fill(self, fill: Fill):
        self.fill_history.append(fill)
        
        if fill.side == 'buy':
            # Update avg cost
            new_qty = self.position + fill.quantity
            self.avg_cost = (self.position * self.avg_cost + fill.quantity * fill.price) / new_qty
            self.position = new_qty
        else:
            # Realized P&L on sale
            pnl = (fill.price - self.avg_cost) * fill.quantity
            self.realized_pnl += pnl
            self.position -= fill.quantity
    
    def get_unrealized_pnl(self, current_price: Decimal) -> Decimal:
        return (current_price - self.avg_cost) * self.position
    
    def get_total_pnl(self, current_price: Decimal) -> Decimal:
        return self.realized_pnl + self.get_unrealized_pnl(current_price)
Strategy Parameter Typical Values Impact
Spread (bps) 5–50 bps Higher = fewer fills, more capture per fill
Order levels 3–10 More = more capital tie-up, deeper book
Max position 5–20% of deposit Adverse selection risk
Volatility multiplier 1–3x Protection from gaps

Development Timeline

  • Basic MM bot with single-level quoting: 3–4 weeks
  • Multi-level with inventory management: 5–7 weeks
  • Production-ready with volatility adaptation and adverse selection detection: 8–12 weeks
  • Backtesting framework for strategy: +2–3 weeks