Разработка API криптобиржи (REST, WebSocket)

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка API криптобиржи (REST, WebSocket)
Сложная
~1-2 недели
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

Разработка API криптобиржи (REST, WebSocket)

API биржи — это публичный интерфейс для трейдеров, ботов и третьесторонних интеграций. Его качество определяет привлекательность платформы для профессиональных участников. Binance, Kraken, OKX стандартизировали ожидания: REST для управления ордерами и аккаунтом, WebSocket для реалтайм данных.

Дизайн REST API

Структура эндпоинтов

Стандартная структура публичного и приватного API:

Public API (без аутентификации):
GET  /api/v1/markets                     - список торговых пар
GET  /api/v1/markets/{pair}/ticker       - текущий тикер
GET  /api/v1/markets/{pair}/orderbook    - снэпшот order book
GET  /api/v1/markets/{pair}/trades       - последние сделки
GET  /api/v1/markets/{pair}/candles      - OHLCV данные

Private API (требует аутентификации):
GET    /api/v1/account/balances          - балансы
GET    /api/v1/account/orders            - список ордеров
POST   /api/v1/account/orders            - разместить ордер
DELETE /api/v1/account/orders/{id}       - отменить ордер
POST   /api/v1/account/orders/cancel-all - отменить все ордера
GET    /api/v1/account/trades            - история сделок
GET    /api/v1/account/deposits          - история депозитов
POST   /api/v1/account/withdrawals       - создать вывод

Аутентификация

Стандарт для криптовалютных API — HMAC-SHA256 подпись запроса:

// Серверная верификация подписи
func verifySignature(r *http.Request, secret string) bool {
    apiKey := r.Header.Get("X-API-Key")
    timestamp := r.Header.Get("X-Timestamp")
    signature := r.Header.Get("X-Signature")
    
    // Проверяем timestamp (не старше 5 секунд — защита от replay)
    ts, _ := strconv.ParseInt(timestamp, 10, 64)
    if time.Now().UnixMilli()-ts > 5000 {
        return false
    }
    
    // Строка для подписи: method + path + timestamp + body
    method := r.Method
    path := r.URL.RequestURI()
    body, _ := io.ReadAll(r.Body)
    r.Body = io.NopCloser(bytes.NewBuffer(body)) // restore body
    
    message := method + path + timestamp + string(body)
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write([]byte(message))
    expected := hex.EncodeToString(mac.Sum(nil))
    
    return hmac.Equal([]byte(signature), []byte(expected))
}

Клиентская сторона подписи:

import hmac, hashlib, time, requests

def signed_request(method: str, path: str, body: dict = None):
    timestamp = str(int(time.time() * 1000))
    body_str = json.dumps(body) if body else ''
    
    message = method + path + timestamp + body_str
    signature = hmac.new(SECRET.encode(), message.encode(), hashlib.sha256).hexdigest()
    
    headers = {
        'X-API-Key': API_KEY,
        'X-Timestamp': timestamp,
        'X-Signature': signature,
        'Content-Type': 'application/json',
    }
    return requests.request(method, BASE_URL + path, headers=headers, data=body_str)

Формат ответов

Консистентный формат для всех эндпоинтов:

// Успех
{
    "success": true,
    "data": { ... },
    "timestamp": 1700000000000
}

// Ошибка
{
    "success": false,
    "error": {
        "code": "INSUFFICIENT_BALANCE",
        "message": "Insufficient balance to place order",
        "details": { "available": "0.5", "required": "1.0" }
    },
    "timestamp": 1700000000000
}

Стандартизированные коды ошибок важны для интеграторов. Лучше INSUFFICIENT_BALANCE чем 400 Bad Request с текстом.

Rate limiting

// Rate limiter по API ключу с bucket алгоритмом
type RateLimiter struct {
    redis *redis.Client
}

func (rl *RateLimiter) Check(apiKey string, weight int) error {
    key := "rate_limit:" + apiKey
    
    // Sliding window counter
    now := time.Now().UnixMilli()
    windowStart := now - 60000 // 60 секунд
    
    pipe := rl.redis.Pipeline()
    // Удаляем устаревшие записи
    pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(windowStart, 10))
    // Добавляем текущий запрос
    pipe.ZAdd(ctx, key, redis.Z{Score: float64(now), Member: fmt.Sprintf("%d-%d", now, rand.Int())})
    // Считаем сумму весов
    pipe.ZCard(ctx, key)
    pipe.Expire(ctx, key, 2*time.Minute)
    
    results, _ := pipe.Exec(ctx)
    count := results[2].(*redis.IntCmd).Val()
    
    // Разные лимиты для разных уровней пользователей
    limit := rl.getUserLimit(apiKey) // 1200 req/min для стандарта
    if int(count) > limit {
        return ErrRateLimitExceeded
    }
    return nil
}

Binance-style weight system: разные эндпоинты имеют разный "вес" (GET orderbook = 5, POST order = 1, GET all orders = 10). Это позволяет гибко управлять нагрузкой.

Pagination

Для исторических данных — cursor-based pagination (лучше чем offset для больших таблиц):

type TradesQuery struct {
    Pair      string    `json:"pair"`
    Limit     int       `json:"limit"`  // max 1000
    StartTime *int64    `json:"start_time,omitempty"`
    EndTime   *int64    `json:"end_time,omitempty"`
    FromID    *string   `json:"from_id,omitempty"` // cursor
}

func (h *Handler) GetTrades(w http.ResponseWriter, r *http.Request) {
    q := parseTradesQuery(r)
    
    trades, err := h.db.GetTrades(q)
    
    var nextCursor *string
    if len(trades) == q.Limit {
        lastID := trades[len(trades)-1].ID
        nextCursor = &lastID
    }
    
    writeJSON(w, map[string]interface{}{
        "trades":      trades,
        "next_cursor": nextCursor,
        "has_more":    nextCursor != nil,
    })
}

WebSocket API

Архитектура WebSocket сервера

type WSHub struct {
    clients    map[*WSClient]bool
    subscriptions map[string]map[*WSClient]bool  // channel -> clients
    broadcast  chan WSMessage
    register   chan *WSClient
    unregister chan *WSClient
    mu         sync.RWMutex
}

func (h *WSHub) Run() {
    for {
        select {
        case client := <-h.register:
            h.mu.Lock()
            h.clients[client] = true
            h.mu.Unlock()
            
        case client := <-h.unregister:
            h.mu.Lock()
            delete(h.clients, client)
            for _, subs := range h.subscriptions {
                delete(subs, client)
            }
            h.mu.Unlock()
            
        case message := <-h.broadcast:
            h.mu.RLock()
            for client := range h.subscriptions[message.Channel] {
                select {
                case client.send <- message.Data:
                default:
                    // Клиент медленный, буфер переполнен — дропаем
                    close(client.send)
                    delete(h.clients, client)
                }
            }
            h.mu.RUnlock()
        }
    }
}

Протокол подписок

// Клиент → Сервер: подписка
{"op": "subscribe", "channels": ["ticker.BTC-USDT", "orderbook.ETH-USDT.50", "trades.BTC-USDT"]}

// Сервер → Клиент: подтверждение
{"op": "subscribed", "channels": ["ticker.BTC-USDT", "orderbook.ETH-USDT.50", "trades.BTC-USDT"]}

// Сервер → Клиент: данные тикера
{
    "channel": "ticker.BTC-USDT",
    "data": {
        "pair": "BTC-USDT",
        "last": "42150.50",
        "bid": "42148.00",
        "ask": "42152.00",
        "volume_24h": "1234.56",
        "change_24h": "+2.35",
        "ts": 1700000000000
    }
}

// Order book diff update
{
    "channel": "orderbook.ETH-USDT.50",
    "type": "diff",
    "seq": 12345,
    "data": {
        "bids": [["2200.00", "5.5"], ["2195.00", "0"]],
        "asks": [["2205.00", "3.2"]]
    }
}

Аутентификация WebSocket

func (ws *WSClient) HandleAuth(msg AuthMessage) {
    // Верифицируем подпись как в REST
    if !verifyWSAuth(msg.APIKey, msg.Timestamp, msg.Signature) {
        ws.sendError("AUTH_FAILED", "Invalid signature")
        return
    }
    
    ws.userID = getUserIDByAPIKey(msg.APIKey)
    ws.authenticated = true
    ws.sendSuccess("authenticated")
}

// После авторизации доступны приватные каналы
// {"op": "subscribe", "channels": ["orders", "trades", "balances"]}

Heartbeat

func (ws *WSClient) startPingPong() {
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()
    
    for {
        select {
        case <-ticker.C:
            ws.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
            if err := ws.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
                ws.close()
                return
            }
        case <-ws.done:
            return
        }
    }
}

Клиент должен отвечать на Ping в течение 10 секунд. Если нет — соединение закрывается. Клиенты также могут слать {"op": "ping"} для поддержания соединения.

API документация

OpenAPI 3.0 + Swagger UI — стандарт для REST документации. Документация генерируется из аннотаций кода или пишется вручную:

# openapi.yaml
paths:
  /api/v1/account/orders:
    post:
      summary: Place Order
      security:
        - ApiKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PlaceOrderRequest'
            example:
              pair: "BTC-USDT"
              side: "buy"
              type: "limit"
              quantity: "0.001"
              price: "42000"
              time_in_force: "GTC"

WebSocket документация — отдельная страница с описанием протокола подписок, форматов сообщений и примерами кода (Python, JavaScript, Go).

SDK для основных языков

Публичное API без SDK — дополнительный барьер для интеграции. Минимальный набор:

# Python SDK
class ExchangeClient:
    def __init__(self, api_key: str, api_secret: str, testnet: bool = False):
        self.base_url = 'https://testnet-api.exchange.com' if testnet else 'https://api.exchange.com'
        self.api_key = api_key
        self.api_secret = api_secret
        self.session = aiohttp.ClientSession()
    
    async def place_order(self, pair: str, side: str, type: str, 
                           quantity: str, price: str = None) -> dict:
        return await self._post('/api/v1/account/orders', {
            'pair': pair, 'side': side, 'type': type,
            'quantity': quantity, 'price': price,
        })
    
    # WebSocket
    async def subscribe_ticker(self, pairs: list[str], callback: Callable):
        async with websockets.connect(self.ws_url) as ws:
            await ws.send(json.dumps({
                'op': 'subscribe',
                'channels': [f'ticker.{p}' for p in pairs]
            }))
            async for msg in ws:
                await callback(json.loads(msg))

Тестирование

Testnet — обязателен. Отдельная инфраструктура с теми же эндпоинтами, но тестовой валютой. Binance, OKX, Kraken — все предоставляют testnet.

Нагрузочное тестирование: k6 или wrk для REST (цель — 10,000 req/sec при latency p99 < 50 мс), artillery для WebSocket (1000+ concurrent connections, проверка broadcast latency).

Сроки

Компонент Срок
Public REST API (5–7 эндпоинтов) 3–4 недели
Private REST API + HMAC auth 3–4 недели
WebSocket server + subscriptions 3–4 недели
Rate limiting + API keys management 1–2 недели
OpenAPI документация 1 неделя
Python SDK 1–2 недели
Нагрузочное тестирование 1–2 недели

Полный API с документацией, SDK и тестированием: 3–4 месяца.