Розробка бота для міжбіржового арбітражу

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка бота для міжбіржового арбітражу
Середній
~1-2 тижні
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1285
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1198
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    902
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1120
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    588
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    855

Розробка маркет-мейкер бота

Маркет-мейкинг — це одночасне виставлення ордерів на покупку та продаж для отримання прибутку на спреді. Маркет-мейкер (МM) забезпечує ліквідність ринку та заробляє, коли виконуються як bid, так і ask. Головний враг МM — adverse selection: торговля проти інформованого трейдера, який знає, що ціна незабаром зміниться.

Економіка маркет-мейкингу

МM заробляє на захопленні спреду: якщо bid = $42,000 і ask = $42,100, а обидва виконані — прибуток = $100 (мінус комісії). Але якщо ціна впала до $41,800 протягом цього часу, МM купив по $42,000 і тепер має збиткову позицію.

Успішний МM повинен:

  • Заробити достатньо на захопленні спреду, щоб покрити збитки від adverse selection
  • Керувати ризиком інвентарю (не накопичувати занадто багато позиції в одному напрямку)
  • Адаптувати спред до поточної волатильності

Базова стратегія котирування

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))  # базові пункти
        self.order_levels = config.levels  # кількість рівнів у стакані
        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)  # оновлюємо щосекунди
            except Exception as e:
                logger.error(f"MM цикл помилка: {e}")
                await self.cancel_all()
                await asyncio.sleep(5)  # пауза перед повторною спробою
    
    def calculate_quotes(self, mid: Decimal, inventory: Decimal, volatility: Decimal) -> Quotes:
        # Базовий спред
        half_spread = mid * self.base_spread_bps / Decimal('10000') / 2
        
        # Коригування волатильності: при високій волатильності — розширюємо спред
        vol_multiplier = Decimal('1') + volatility * Decimal('10')  # 1x при vol=0, 2x при vol=10%
        adjusted_spread = half_spread * vol_multiplier
        
        # Дисбаланс інвентарю: зміщуємо котирування для зменшення дисбалансу
        skew = self.calculate_inventory_skew(inventory, mid)
        
        best_bid = mid - adjusted_spread + skew
        best_ask = mid + adjusted_spread + skew
        
        # Генеруємо кілька рівнів
        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:
        """
        Зміщуємо котирування на основі поточного інвентарю.
        При накопленні long позиції — знижуємо bid, піднімаємо ask (продаємо активніше).
        """
        target = Decimal('0')  # цільовий інвентар = 0 (нейтральний)
        max_inventory = self.max_position_usd
        
        inventory_ratio = inventory_usd / max_inventory
        inventory_ratio = max(Decimal('-1'), min(Decimal('1'), inventory_ratio))
        
        # Skew пропорційний інвентарю: при 100% позиції — зміщуємо на 1 повний спред
        skew_pct = Decimal('-0.0001') * inventory_ratio  # -0.01% * ratio
        return mid * skew_pct

Оцінка волатильності

def estimate_volatility(self, prices: list[Decimal], window: int = 20) -> Decimal:
    """
    Реалізована волатильність за останні N періодів.
    Використовується для адаптивного розширення спреду.
    """
    if len(prices) < window + 1:
        return Decimal('0.01')  # default якщо недостатньо даних
    
    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))
    
    # Аннуалізована волатильність: для 1-хвилинних даних
    # multiply by sqrt(525600 minutes per year)
    return vol * Decimal(str((525600 ** 0.5)))

Управління ордерами

Уникнення ненав'язних скасувань/заміни

Кожне скасування та новий ордер — комісія та latency. Оптимізація: оновлюємо ордер тільки якщо ціна змінилася достатньо.

async def update_orders(self, new_quotes: Quotes):
    current_orders = await self.get_open_orders()
    
    # Порівнюємо поточні ордери з новими котируваннями
    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:
            # Перевіряємо чи потрібно оновити
            price_diff = abs(matching.price - new_bid.price) / matching.price
            if price_diff > Decimal('0.0005'):  # зміна > 0.05% — оновлюємо
                orders_to_cancel.append(matching.id)
                orders_to_place.append(new_bid)
            # Інакше залишаємо існуючий ордер
        else:
            orders_to_place.append(new_bid)
    
    # Пакетне скасування + розміщення для мінімізації 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
        ])

Аварійне скасування

При різкому русі ринку або технічній помилці — негайне скасування всіх ордерів:

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% за період оновлення
        logger.warning(f"Розпізнано шок ціни: {price_change:.2%}. Скасовуємо всі ордери.")
        await self.exchange.cancel_all_orders(self.pair)
        
        # Пауза та моніторинг
        await asyncio.sleep(30)
        
        # Перевіряємо чи стабілізувалася ціна
        new_price = await self.get_reference_price()
        new_change = abs(new_price - reference_price) / reference_price
        
        if new_change < Decimal('0.005'):  # < 0.5% — відновлюємо
            logger.info("Ціна стабілізована, відновлюємо маркет-мейкинг")
            self.reference_price = new_price

Ризики маркет-мейкингу

Adverse Selection

Крупний учасник знає, що ціна виросте, і агресивно купує у МM за bid. МM опиняється з long позицією перед падінням.

Виявлення: якщо останні N fillsів усі на одній стороні — стоп і переоцінка. Ознака інформованої торгівлі.

def detect_adverse_selection(self, recent_fills: list[Fill]) -> bool:
    if len(recent_fills) < 5:
        return False
    
    # Усі останні 5 fills на одній стороні — підозріло
    sides = [f.side for f in recent_fills[-5:]]
    if len(set(sides)) == 1:
        return True
    
    # Обсяг fills різко зріс
    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

Ризик голої позиції

Жорсткий ліміт на максимальну позицію — circuit breaker:

async def check_position_limits(self):
    position = await self.get_net_position()  # $ в базовому активі
    
    if abs(position) > self.max_position_usd:
        logger.critical(f"Ліміт позиції перевищений: {position} USD")
        await self.cancel_all_orders()
        
        # Примусова ліквідація частини позиції
        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

class MMPnLTracker:
    def __init__(self):
        self.realized_pnl = Decimal('0')
        self.fill_history: list[Fill] = []
        self.position = Decimal('0')  # у базовій валюті
        self.avg_cost = Decimal('0')
    
    def on_fill(self, fill: Fill):
        self.fill_history.append(fill)
        
        if fill.side == 'buy':
            # Оновлюємо середню вартість
            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:
            # Реалізований P&L при продажу
            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)
Параметр стратегії Типові значення Вплив
Спред (bps) 5–50 bps Вище = менше fillsів, більше захоплення на fill
Рівні ордерів 3–10 Більше = більше блокування капіталу, глибший стакан
Макс позиція 5–20% депозиту Ризик adverse selection
Множник волатильності 1–3x Захист від гепів

Сроки розробки

  • Базовий МM бот з однорівневим котируванням: 3–4 тижні
  • Багаторівневий з керуванням інвентарем: 5–7 тижнів
  • Production-ready з адаптацією волатильності та виявленням adverse selection: 8–12 тижнів
  • Фреймворк backtesting для стратегії: +2–3 тижні