Розробка API криптобіржи (REST, WebSocket)
API біржи — це публічний інтерфейс для трейдерів, ботів та третьесторонніх інтеграцій. Його якість визначає привабливість платформи для професійних учасників. Binance, Kraken, OKX стандартизували очікування: REST для управління ордерами та рахунком, WebSocket для реалтайм даних.
Дизайн REST API
Структура Endpoints
Стандартна структура публічного та приватного 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")
// Перевірка часової мітки (не старше 5 секунд — захист від replay)
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if time.Now().UnixMilli()-ts > 5000 {
return false
}
// Строка для підпису: метод + шлях + часова мітка + тіло
method := r.Method
path := r.URL.RequestURI()
body, _ := io.ReadAll(r.Body)
r.Body = io.NopCloser(bytes.NewBuffer(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)
Формат відповідей
Консистентний формат для всіх endpoints:
// Успіх
{
"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 система ваг: різні endpoints мають різні "ваги" (GET orderbook = 5, POST order = 1, GET all orders = 10). Дозволяє гнучко управляти навантаженням.
Пагінація
Для історичних даних — cursor-based пагінація (краще чим offset для великих таблиць):
type TradesQuery struct {
Pair string `json:"pair"`
Limit int `json:"limit"` // макс 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 обов'язковий. Окрема інфраструктура з тими ж endpoints, але тестовою валютою. Binance, OKX, Kraken — всі надають testnet.
Нагрузкове тестування: k6 або wrk для REST (мета — 10,000 req/sec при p99 latency < 50 мс), artillery для WebSocket (1000+ одночасних з'єднань, перевірка broadcast latency).
Часова шкала
| Компонент | Часова шкала |
|---|---|
| Public REST API (5–7 endpoints) | 3–4 тижні |
| Private REST API + HMAC auth | 3–4 тижні |
| WebSocket сервер + підписки | 3–4 тижні |
| Rate limiting + управління API ключами | 1–2 тижні |
| OpenAPI документація | 1 тиждень |
| Python SDK | 1–2 тижні |
| Нагрузкове тестування | 1–2 тижні |
Повний API з документацією, SDK та тестуванням: 3–4 місяці.







