Bybit V5 Market API를 회전 프록시로 스크랩하는 방법

Bybit V5 Market API의 orderbook, tickers, kline, recent-trade 엔드포인트를 회전 프록시로 안정적으로 수집하는 개발자 가이드입니다. IP 레이트 리밋, 지역 제한, ProxyHat SDK 예제를 포함합니다.

How to Scrape the Bybit V5 Market API with Rotating Proxies
이 글의 목차

면책 조항: 이 글은 Bybit의 공개 마켓 데이터 엔드포인트를 기술적으로 설명할 뿐, Bybit 이용약관(TOS)을 위반하거나 거래소의 지적재산권을 침해하도록 조장하지 않습니다. 공개 데이터라도 robots.txt와 TOS를 확인하고 합리적인 요청 속도를 유지하세요.

Bybit V5 Market API를 회전 프록시로 스크랩하는 방법(bybit v5 market api proxy)을 찾는 개발자들이 가장 먼저 부딪히는 문제는 두 가지입니다. 첫째, api.bybit.com/v5/market/orderbookapi.bybit.com/v5/market/tickers에 버스트 요청을 보내면 403과 함께 약 10분간 IP가 차단됩니다. 둘째, 미국/영국 등 제한 지역에서는 Bybit 본사의 거리 기반 차단과 CloudFront 엣지 차단이 겹쳐 403 또는 451이 반환됩니다. 본 가이드는 Node.js와 Python에서 gate.proxyhat.com:8080(HTTP) 및 :1080(SOCKS5)을 통해 이 두 문제를 해결하는 실전 코드를 제공합니다.

Bybit V5 Market API 엔드포인트 개요

Bybit V5는 2023년에 출시된 통합 API로, 스팟(spot), 선물(linear), 인버스(inverse), 옵션(option)을 단일 엔드포인트 체계로 묶었습니다. 공개 마켓 데이터는 /v5/market/ 경로 아래에 있으며, 모두 category 파라미터로 자산군을 구분합니다. 자세한 스펙은 Bybit V5 공식 문서를 참고하세요.

엔드포인트필수 파라미터주요 옵션용도
GET /v5/market/orderbookcategory, symbollimit(spot/linear: 1–200, 옵션: 25)실시간 호가창 스냅샷
GET /v5/market/tickerscategorysymbol(선택)전체/단일 티커 24h 통계
GET /v5/market/klinecategory, symbol, intervalstart, end, limit(최대 1000)과거 캔들 데이터 페이지네이션
GET /v5/market/recent-tradecategory, symbollimit(최대 1000)최근 체결 내역

categoryspot, linear, inverse, option 중 하나입니다. 응답은 다음과 같은 봉투(envelope)로 옵니다:

{
  "retCode": 0,
  "retMsg": "OK",
  "result": { /* 실제 데이터 */ },
  "time": 1700000000000
}

retCode0이면 성공, 그 외 값은 에러입니다. retMsg는 사람이 읽을 수 있는 메시지이며, 레이트 리밋 위반 시 retMsgrate limit 문자열이 포함되거나 HTTP 403이 반환됩니다. HTTP 상태 코드만 보지 말고 반드시 retCode를 먼저 점검하세요.

IP 기반 레이트 리밋과 차단 창

Bybit는 엔드포인트별로 IP 기반 레이트 리밋을 적용합니다. 공식 문서에 따르면 /v5/market/orderbook은 약 600회/5초 수준의 캡을 가지며, /v5/market/tickers는 더 넉넉합니다(정확 수치는 Bybit가 업데이트하므로 문서를 확인하세요). 버스트 요청이 캡을 초과하면 403이 반환되고, 동일 IP는 약 10분 동안 추가 요청이 거부됩니다. 이는 Binance의 분 단위 가중치 모델과 달리 차단 창(ban window) 방식에 가깝습니다.

회전 프록시가 없다면 단일 서버 IP로 여러 심볼의 오더북을 폴링하는 것만으로도 몇 초 안에 차단됩니다. 회전 프록시는 요청마다 출발 IP를 바꿔 각 IP의 카운터를 분산시키지만, 프록시 풀의 IP 다양성이 충분하지 않으면 동일 IP가 반복 사용되어 결국 차단됩니다. 따라서 레즈iden셜 프록시(수백만 IP 풀)가 데이터센터 프록시(수만 IP)보다 훨씬 안전합니다.

지역 제한과 CloudFront 차단

Bybit는 미국, 영국, 싱가포르 등 일부 지역에서 서비스를 제한하고 있으며, 제한 지역 IP에서 API 호출 시 403 또는 451이 반환됩니다. 또한 Bybit는 CloudFront를 사용하므로 엣지 노드 단에서 차단될 수도 있습니다. 자세한 제한 지역 목록은 Bybit 서비스 제한 국가 문서를 참고하세요.

해결책은 Bybit가 허용하는 국가의 IP로 요청을 보내는 것입니다. ProxyHat은 사용자명에 -country-XX 형식으로 국가를 지정할 수 있습니다:

# HTTP (기본 포트 8080)
curl -x http://user-country-DE:pass@gate.proxyhat.com:8080 \
  "https://api.bybit.com/v5/market/orderbook?category=spot&symbol=BTCUSDT&limit=50"

# SOCKS5 (포트 1080)
curl -x socks5://user-country-DE:pass@gate.proxyhat.com:1080 \
  "https://api.bybit.com/v5/market/tickers?category=linear"

독일(DE), 일본(JP), 한국(KR) 등은 일반적으로 허용되지만, 정책은 변경될 수 있으므로 ProxyHat 로케이션 목록에서 최신 지원 국가를 확인하고, 실제 배포 전에 해당 국가 IP로 테스트하세요.

Node.js (axios) 회전 IP 예제

먼저 원시 프록시(raw proxy) 방식과 ProxyHat SDK 방식을 나란히 보여줍니다. SDK는 인증 문자열 빌더, 세션 관리, 재시도를 캡슐화합니다.

원시 프록시 — 매 요청마다 세션 ID로 IP 회전

// raw-proxy.js
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');

const GATE = 'gate.proxyhat.com';
const PORT = 8080;
const USER = 'user';
const PASS = 'pass';
const COUNTRY = 'DE';

const symbols = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT', 'DOGEUSDT'];

function buildAgent(sessionId) {
  const proxyUrl = `http://${USER}-country-${COUNTRY}-session-${sessionId}:${PASS}@${GATE}:${PORT}`;
  return new HttpsProxyAgent(proxyUrl);
}

async function fetchOrderbook(symbol, attempt = 0) {
  if (attempt >= 5) throw new Error(`${symbol} max retries exceeded`);
  const sessionId = `${symbol}-${Date.now()}`;
  try {
    const res = await axios.get('https://api.bybit.com/v5/market/orderbook', {
      params: { category: 'spot', symbol, limit: 50 },
      httpsAgent: buildAgent(sessionId),
      timeout: 8000,
    });
    if (res.data.retCode !== 0) throw new Error(`retCode ${res.data.retCode}: ${res.data.retMsg}`);
    return res.data.result;
  } catch (err) {
    const status = err.response?.status;
    if (status === 403 || status === 429) {
      const backoff = Math.min(1000 * Math.pow(2, attempt), 16000);
      console.warn(`${symbol} ${status}, retry in ${backoff}ms`);
      await new Promise(r => setTimeout(r, backoff));
      return fetchOrderbook(symbol, attempt + 1);
    }
    throw err;
  }
}

(async () => {
  for (const sym of symbols) {
    try {
      const ob = await fetchOrderbook(sym);
      console.log(sym, 'bid0', ob.b[0]?.[0], 'ask0', ob.a[0]?.[0]);
    } catch (e) { console.error(sym, e.message); }
    await new Promise(r => setTimeout(r, 200)); // 엔드포인트당 자체 속도 제한
  }
})();

ProxyHat SDK — 인증/세션 추상화

// proxyhat-sdk.js
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const { ProxyHat } = require('@proxyhat/sdk'); // 예시 SDK, 실제 패키지명은 docs 확인

const hat = new ProxyHat({
  user: 'user',
  password: 'pass',
  country: 'DE',
  gateway: 'gate.proxyhat.com',
  httpPort: 8080,
});

async function fetchTickers(category) {
  const agent = hat.httpAgent({ session: `tickers-${Date.now()}` });
  try {
    const res = await axios.get('https://api.bybit.com/v5/market/tickers', {
      params: { category },
      httpsAgent: agent,
      timeout: 8000,
    });
    if (res.data.retCode !== 0) throw new Error(res.data.retMsg);
    return res.data.result.list;
  } catch (err) {
    if (err.response?.status === 403) {
      await hat.rotateSession();
      return fetchTickers(category);
    }
    throw err;
  }
}

(async () => {
  const tickers = await fetchTickers('linear');
  console.log(`linear tickers: ${tickers.length} symbols`);
})();

SDK는 hat.httpAgent({ session }) 호출마다 새 세션 ID를 생성해 출발 IP를 바꿉니다. 403hat.rotateSession()으로 즉시 회전할 수 있습니다. SDK 설치와 최신 옵션은 ProxyHat 문서를 참고하세요.

Python 예제 — kline 페이지네이션과 스티키 세션

kline은 start/end 타임스탬프로 페이지네이션합니다. 한 페이지당 최대 1000개 캔들입니다. 페이지네이션 도중 IP가 바뀌면 Bybit 측에서 IP 일관성 검사를 할 수 있으므로, 단일 페이지네이션 시퀀스는 스티키 세션으로 동일 IP를 유지하는 것이 안전합니다. 시퀀스가 끝나면 세션을 바꿔 다음 심볼로 넘어갑니다.

# kline_paginate.py
import requests
import time
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

class ProxyHatSession:
    def __init__(self, user, password, country='DE', sticky_id=None):
        self.user = user
        self.password = password
        self.country = country
        self.sticky_id = sticky_id

    def proxy_url(self, session_id=None):
        sid = session_id or self.sticky_id or f'py-{int(time.time())}'
        auth = f'{self.user}-country-{self.country}-session-{sid}:{self.password}'
        return f'http://{auth}@gate.proxyhat.com:8080'

    def requests_session(self, session_id=None):
        s = requests.Session()
        s.proxies = {'http': self.proxy_url(session_id), 'https': self.proxy_url(session_id)}
        retry = Retry(total=4, backoff_factor=1.5,
                      status_forcelist=[429, 500, 502, 503, 504],
                      allowed_methods=['GET'])
        s.mount('https://', HTTPAdapter(max_retries=retry))
        return s

def fetch_kline(symbol, category='spot', interval='60', start_ms=None, end_ms=None, limit=1000):
    ph = ProxyHatSession(user='user', password='pass', country='DE',
                         sticky_id=f'kline-{symbol}')
    s = ph.requests_session()
    url = 'https://api.bybit.com/v5/market/kline'
    params = {'category': category, 'symbol': symbol, 'interval': interval,
              'limit': limit}
    if start_ms: params['start'] = start_ms
    if end_ms: params['end'] = end_ms
    try:
        r = s.get(url, params=params, timeout=8)
        data = r.json()
        if data.get('retCode') != 0:
            raise RuntimeError(f"retCode {data.get('retCode')}: {data.get('retMsg')}")
        return data['result']['list']
    except requests.HTTPError as e:
        if r.status_code == 403:
            print(f'403 on {symbol}, IP may be banned ~10min; rotate')
        raise

if __name__ == '__main__':
    # 1시간봉, 가장 최근 1000개
    now_ms = int(time.time() * 1000)
    candles = fetch_kline('BTCUSDT', interval='60', end_ms=now_ms)
    print(f'BTCUSDT 1h candles: {len(candles)}')
    oldest = int(candles[-1][0])
    # 다음 페이지는 oldest 이전으로
    prev = fetch_kline('BTCUSDT', interval='60', end_ms=oldest - 1)
    print(f'prev page candles: {len(prev)}')

주의: requests.Session은 프록시 URL을 고정하므로, 스티키 세션을 쓸 때는 세션 객체를 재사용해야 합니다. 매 요청마다 새 Session()을 만들면 프록시 URL이 바뀌지 않아 의도한 대로 동작하지만, sticky_id를 명시적으로 넣는 것이 IP 일관성을 보장합니다.

엔드포인트별 속도 예산과 회로 차단기

회전 프록시가 IP 분산을 담당하더라도, 애플리케이션 단에서 엔드포인트별로 독립된 속도 예산을 두어야 합니다. 예를 들어 orderbook은 5초당 600회 이하, tickers는 5초당 200회 이하로 제한하고, 403 비율이 임계값(예: 10%)을 넘으면 회로 차단기를 열어 60초간 해당 엔드포인트 호출을 중단합니다.

// rate-budget.js
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');

class RateBudget {
  constructor(windowMs, max) {
    this.windowMs = windowMs;
    this.max = max;
    this.timestamps = [];
  }
  allow() {
    const now = Date.now();
    this.timestamps = this.timestamps.filter(t => now - t < this.windowMs);
    if (this.timestamps.length >= this.max) return false;
    this.timestamps.push(now);
    return true;
  }
}

class CircuitBreaker {
  constructor(threshold = 0.1, cooldownMs = 60000) {
    this.threshold = threshold;
    this.cooldownMs = cooldownMs;
    this.failures = 0;
    this.total = 0;
    this.openUntil = 0;
  }
  isOpen() { return Date.now() < this.openUntil; }
  record(ok) {
    this.total++;
    if (!ok) this.failures++;
    if (this.total > 20 && this.failures / this.total > this.threshold) {
      this.openUntil = Date.now() + this.cooldownMs;
      this.failures = 0; this.total = 0;
    }
  }
}

const orderbookBudget = new RateBudget(5000, 500); // 여유분
const breaker = new CircuitBreaker(0.1, 60000);

async function guardedOrderbook(symbol) {
  if (breaker.isOpen()) throw new Error('circuit open, backing off');
  if (!orderbookBudget.allow()) {
    await new Promise(r => setTimeout(r, 200));
    return guardedOrderbook(symbol);
  }
  const sid = `${symbol}-${Date.now()}`;
  const agent = new HttpsProxyAgent(
    `http://user-country-DE-session-${sid}:pass@gate.proxyhat.com:8080`
  );
  try {
    const res = await axios.get('https://api.bybit.com/v5/market/orderbook', {
      params: { category: 'spot', symbol, limit: 50 },
      httpsAgent: agent, timeout: 8000,
    });
    breaker.record(res.data.retCode === 0);
    return res.data.result;
  } catch (e) {
    breaker.record(false);
    throw e;
  }
}

REST 스냅샷 vs WebSocket orderbook.50

REST /v5/market/orderbook은 특정 시점의 스냅샷이며, 폴링 간격이 짧으면 레이트 리밋과 차단 창 위험이 커집니다. 실시간 깊이가 필요하다면 Bybit의 공개 WebSocket 스트림 orderbook.50.{symbol} 또는 orderbook.200.{symbol}을 구독하는 것이 훨씬 효율적입니다. WebSocket은 단일 연결로 푸시를 받으므로 HTTP 요청당 레이트 리밋을 소비하지 않습니다. 다만 WebSocket 연결 자체도 지역 제한을 받으므로, 제한 지역에서는 프록시가 필요합니다.

# ws_orderbook.py
import asyncio, json, websockets

async def listen_orderbook(symbol='BTCUSDT'):
    # SOCKS5 프록시로 WebSocket 연결 (websockets 라이브러리는 socks 지원)
    proxy = 'socks5://user-country-DE-session-ws1:pass@gate.proxyhat.com:1080'
    url = 'wss://stream.bybit.com/v5/public/spot'
    async with websockets.connect(url, proxy=proxy, ping_interval=20) as ws:
        await ws.send(json.dumps({
            'op': 'subscribe',
            'args': [f'orderbook.50.{symbol}']
        }))
        async for msg in ws:
            data = json.loads(msg)
            if data.get('topic', '').startswith('orderbook.50'):
                ob = data['data']
                print(f'{symbol} bid0 {ob["b"][0][0]} ask0 {ob["a"][0][0]}')

asyncio.run(listen_orderbook())

WebSocket을 쓸 때는 스티키 세션이 권장됩니다. 회전 세션은 연결마다 새 IP를 할당하므로, Bybit가 연결을 끊거나 재인증을 요구할 수 있습니다. -session-ws1처럼 고정 세션을 쓰면 연결 안정성이 올라갑니다.

ProxyHat 설정 요약

  • HTTP 기본: http://user-country-DE:pass@gate.proxyhat.com:8080
  • SOCKS5: socks5://user-country-DE:pass@gate.proxyhat.com:1080
  • 도시 타겟: user-country-DE-city-berlin:pass@...
  • 스티키 세션: user-country-DE-session-abc123:pass@...
  • 회전(요청마다): 세션 ID를 매 요청마다 새로 생성

요금제와 풀 크기는 ProxyHat 요금 페이지에서 확인하세요. 웹 스크래핑 일반 패턴은 웹 스크래핑 사용 사례를, SERP/검색 결과 수집은 SERP 추적 사용 사례를 참고하세요.

핵심 요약

  • Bybit V5 마켓 데이터는 /v5/market/ 아래 4개 엔드포인트, category로 자산군 구분, retCode/retMsg 봉투 사용.
  • IP 기반 레이트 리밋 초과 시 403 + 약 10분 차단 창. 회전 프록시로 IP 분산 필수.
  • 미국/영국 등 제한 지역은 403/451. -country-DE 등 허용 국가 IP로 우회.
  • kline 페이지네이션은 스티키 세션(-session-abc123)으로 IP 일관성 유지.
  • 실시간 깊이는 REST 폴링 대신 WebSocket orderbook.50 구독 권장.

FAQ

Bybit V5 Market API를 회전 프록시로 스크랩하는 방법이란 무엇인가요?

Bybit의 공개 마켓 데이터 엔드포인트(orderbook, tickers, kline, recent-trade)를 호출할 때, 단일 서버 IP가 아닌 회전 프록시를 통해 요청마다 출발 IP를 변경해 레이트 리밋과 지역 차단을 우회하는 기법입니다. gate.proxyhat.com:8080 같은 게이트웨이와 -country-XX, -session-XX 형식의 사용자명 플래그를 조합해 국가와 세션 고정을 제어합니다.

왜 Bybit API 스크랩에 프록시가 필요한가요?

Bybit는 엔드포인트별 IP 기반 레이트 리밋을 두며, 초과 시 약 10분간 해당 IP를 차단합니다. 또한 미국/영국 등 제한 지역 IP는 403/451을 받습니다. 회전 레지덴셜 프록시는 IP 카운터를 분산시켜 차단 창을 피하고, 허용 국가 IP로 지역 제한을 우회할 수 있게 합니다.

어떤 프록시 유형이 Bybit 스크랩에 가장 적합한가요?

레지덴셜 프록시가 가장 안전합니다. 데이터센터 IP는 풀이 작아 동일 IP가 반복 사용되어 차단되기 쉽고, 일부 거래소가 데이터센터 ASN을 차단하기도 합니다. 레지덴셜은 수백만 IP 풀로 자연스러운 분산이 가능하며, -country-DE로 지역 제한 우회도 가능합니다. 모바일 프록시는 필요 이상 비용이 들 수 있습니다.

Bybit 스크랩 시 차단을 피하려면 어떻게 해야 하나요?

요청마다 새 세션 ID로 IP를 회전하고, 엔드포인트별 독립 속도 예산을 설정하세요. 403 수신 시 지수 백오프 재시도, 회로 차단기로 임계 초과 시 일시 중단, kline 페이지네이션에는 스티키 세션으로 IP 일관성 유지가 핵심입니다. 또한 robots.txt와 Bybit TOS를 준수해야 합니다.

자주 묻는 질문

Bybit V5 Market API를 회전 프록시로 스크랩하는 방법이란 무엇인가요?

Bybit의 공개 마켓 데이터 엔드포인트(orderbook, tickers, kline, recent-trade)를 호출할 때, 단일 서버 IP가 아닌 회전 프록시를 통해 요청마다 출발 IP를 변경해 레이트 리밋과 지역 차단을 우회하는 기법입니다. gate.proxyhat.com:8080 같은 게이트웨이와 -country-XX, -session-XX 형식의 사용자명 플래그를 조합해 국가와 세션 고정을 제어합니다.

왜 Bybit API 스크랩에 프록시가 필요한가요?

Bybit는 엔드포인트별 IP 기반 레이트 리밋을 두며, 초과 시 약 10분간 해당 IP를 차단합니다. 또한 미국/영국 등 제한 지역 IP는 403/451을 받습니다. 회전 레지덴셜 프록시는 IP 카운터를 분산시켜 차단 창을 피하고, 허용 국가 IP로 지역 제한을 우회할 수 있게 합니다.

어떤 프록시 유형이 Bybit 스크랩에 가장 적합한가요?

레지덴셜 프록시가 가장 안전합니다. 데이터센터 IP는 풀이 작아 동일 IP가 반복 사용되어 차단되기 쉽고, 일부 거래소가 데이터센터 ASN을 차단하기도 합니다. 레지덴셜은 수백만 IP 풀로 자연스러운 분산이 가능하며, -country-DE로 지역 제한 우회도 가능합니다.

Bybit 스크랩 시 차단을 피하려면 어떻게 해야 하나요?

요청마다 새 세션 ID로 IP를 회전하고, 엔드포인트별 독립 속도 예산을 설정하세요. 403 수신 시 지수 백오프 재시도, 회로 차단기로 임계 초과 시 일시 중단, kline 페이지네이션에는 스티키 세션으로 IP 일관성 유지가 핵심입니다. 또한 robots.txt와 Bybit TOS를 준수해야 합니다.

시작할 준비가 되셨나요?

148개국 이상의 주거용, ISP, 모바일 프록시. 무료 계정을 만드세요.

무료 계정 만들기
← 블로그로 돌아가기