Bybit V5 Market APIをローテーションプロキシでスクレイピングする完全ガイド

Bybit V5 Market APIのorderbook・tickers・klineエンドポイントをローテーションプロキシ経由でスクレイピングする方法を、Node.js・Pythonのコード例とともに解説。レート制限・地域制限の回避策もカバーします。

How to Scrape the Bybit V5 Market API with Rotating Proxies
この記事の内容

免責事項: 本記事はBybitの公開市場データエンドポイントへのアクセスに関する技術的解説です。Bybitの利用規約および該当管轄区の法律を遵守してください。スクレイピングを行う前に必ず公式ドキュメントとToSを確認し、許可された範囲でデータを収集してください。

Bybit V5 Market APIのローテーションプロキシによるスクレイピングは、仮想通貨取引ボットや市場データパイプラインを構築する開発者にとって中核的な課題です。BybitはV5統合APIでエンドポイント構造を刷新しましたが、IPベースのレート制限と地理的制限により、高頻度データ収集は容易ではありません。本ガイドでは、api.bybit.com/v5/market/orderbook/v5/market/tickers/v5/market/kline/v5/market/recent-tradeの各エンドポイントを、ProxyHatのローテーションプロキシ(gate.proxyhat.com:8080)経由でスクレイピングする実践的な方法を、Node.js・Pythonのコード例とともに解説します。

Bybit V5 Market APIのエンドポイント構造とレスポンスエンベロープ

Bybit V5 Market APIは、取引所の公開市場データへのアクセスを提供するRESTエンドポイント群です。認証不要でアクセスでき、公式V5ドキュメントに仕様が公開されています。主要なエンドポイントは以下の通りです。

エンドポイントメソッド必須パラメータ用途
/v5/market/orderbookGETcategory, symbol板情報(オーダーブック)のスナップショット取得
/v5/market/tickersGETcategory全銘柄または指定銘柄のティッカー情報(価格、出来高等)
/v5/market/klineGETcategory, symbol, intervalローソク足(OHLCV)データの取得
/v5/market/recent-tradeGETcategory, symbol直近の約定履歴取得

categoryパラメータの理解

V5 APIの重要な設計は category パラメータです。これは spotlinearinverseoption のいずれかを指定し、取得する市場のタイプを切り替えます。例えば、/v5/market/orderbook?category=spot&symbol=BTCUSDT は現物のBTCUSDT板情報を返し、category=linear はUSDT無期限先物の板情報を返します。各エンドポイントでサポートされるcategoryは異なるため、ドキュメントで確認が必要です。

retCode / retMsg レスポンスエンベロープ

Bybit V5のすべてのレスポンスは統一されたJSONエンベロープで返されます。

{
  "retCode": 0,
  "retMsg": "OK",
  "result": {
    "category": "spot",
    "symbol": "BTCUSDT",
    "bids": [["42000.50", "1.234"], ...],
    "asks": [["42000.60", "0.567"], ...]
  },
  "retExtInfo": {},
  "time": 1700000000000
}

retCode0 の場合は成功、それ以外はエラーです。retMsg には人間可読なメッセージが含まれます。スクレイピング時は必ず retCode をチェックし、非ゼロの場合はリトライまたはエラーハンドリングを行う必要があります。HTTPステータスが200でも retCode が非ゼロのケースがある点に注意してください。

BybitのIPベースレート制限と403バンの仕組み

BybitはエンドポイントごとにIPベースのレート制限を設けています。/v5/market/orderbook は高頻度アクセスが想定されるため比較的緩い制限ですが、バースト的なアクセスは制限に到達しやすくなります。制限を超過すると以下が発生します。

  • HTTP 403レスポンス: レート制限超過時に即座に返されます。
  • IPバンウィンドウ: 制限超過後、約10分間そのIPからのアクセスがブロックされます。
  • エンドポイント別カウント: orderbook へのバーストは tickers の制限には影響しませんが、同一IPでの全体的なリクエスト頻度は監視されます。

この仕組みがローテーションプロキシを必要とする理由です。単一IPで高頻度に api.bybit.com/v5/market/orderbook を叩くと、数分以内に403とIPバンが発生し、データ収集パイプラインが停止します。リクエストごとにIPをローテーションすることで、各IPのレート制限クォータを独立して消費できます。

Bybitの地理的制限と住宅プロキシによる回避

Bybitは規制上の理由から、米国、英国、および一部制限地域からのアクセスをブロックしています。このブロックはAWS CloudFrontのジオフィルタリングレイヤーで実装されており、IPの地理的位置に基づいてHTTP 403を返します。データセンタープロキシを使用すると、そのIPが制限地域のデータセンターに属している場合、CloudFrontレベルでブロックされる可能性があります。

ProxyHatの住宅プロキシを使用することで、許可された国からの実際のISP IPアドレスでアクセスできます。ユーザー名にジオターゲティングフラグを指定するだけで、アクセス元の国を制御できます。

# 日本からのアクセス
http://user-country-JP:pass@gate.proxyhat.com:8080

# ドイツからのアクセス
http://user-country-DE:pass@gate.proxyhat.com:8080

# シンガポールからのアクセス(Bybit本社所在地)
http://user-country-SG:pass@gate.proxyhat.com:8080

ProxyHatの利用可能なロケーションはロケーション一覧で確認できます。価格についてはプランページを参照してください。

実装例1: curlでの基本アクセス(プロキシなし vs プロキシあり)

まずはcurlでの基本比較から始めます。

# プロキシなし(制限地域からの場合、403の可能性)
curl -s "https://api.bybit.com/v5/market/orderbook?category=spot&symbol=BTCUSDT&limit=50"

# ProxyHatローテーションプロキシ経由(日本IP)
curl -s -x "http://user-country-JP:pass@gate.proxyhat.com:8080" \
  "https://api.bybit.com/v5/market/orderbook?category=spot&symbol=BTCUSDT&limit=50"

# SOCKS5経由
curl -s -x "socks5://user-country-JP:pass@gate.proxyhat.com:1080" \
  "https://api.bybit.com/v5/market/tickers?category=linear"

# kline取得(1分足、200件)
curl -s -x "http://user-country-JP:pass@gate.proxyhat.com:8080" \
  "https://api.bybit.com/v5/market/kline?category=spot&symbol=ETHUSDT&interval=1&limit=200"

# 直近の約定履歴
curl -s -x "http://user-country-JP:pass@gate.proxyhat.com:8080" \
  "https://api.bybit.com/v5/market/recent-trade?category=spot&symbol=BTCUSDT&limit=50"

実装例2: Node.js (axios) で複数銘柄のIPローテーション

次に、Node.jsで複数銘柄のオーダーブックをローテーションプロキシ経由で取得する例を示します。ProxyHat SDKパターンと生プロキシパターンを並べて示します。

// ===== 生プロキシパターン (axios + http-proxy-agent) =====
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');

const PROXYHAT_HOST = 'gate.proxyhat.com';
const PROXYHAT_PORT = 8080;
const PROXY_USER = 'user-country-JP';
const PROXY_PASS = 'pass';

const proxyUrl = `http://${PROXY_USER}:${PROXY_PASS}@${PROXYHAT_HOST}:${PROXYHAT_PORT}`;
const agent = new HttpsProxyAgent(proxyUrl);

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

async function fetchOrderbook(symbol, retries = 3) {
  const url = `https://api.bybit.com/v5/market/orderbook?category=spot&symbol=${symbol}&limit=50`;
  for (let attempt = 0; attempt < retries; attempt++) {
    try {
      const res = await axios.get(url, {
        httpsAgent: agent,
        timeout: 10000,
        headers: {
          'Accept': 'application/json',
          'User-Agent': 'Mozilla/5.0 (compatible; MarketDataBot/1.0)'
        }
      });
      if (res.data.retCode === 0) {
        return res.data.result;
      } else {
        throw new Error(`retCode=${res.data.retCode}, retMsg=${res.data.retMsg}`);
      }
    } catch (err) {
      const wait = Math.pow(2, attempt) * 500;
      console.error(`[${symbol}] attempt ${attempt + 1} failed: ${err.message}, retry in ${wait}ms`);
      if (attempt === retries - 1) throw err;
      await new Promise(r => setTimeout(r, wait));
    }
  }
}

// 全銘柄を並列取得(ただしレート制限を考慮して5並列まで)
async function main() {
  const results = await Promise.allSettled(
    symbols.map(sym => fetchOrderbook(sym))
  );
  results.forEach((r, i) => {
    if (r.status === 'fulfilled') {
      const bids = r.value.bids.slice(0, 3);
      console.log(`${symbols[i]}: best bid=${bids[0]?.[0]}, asks count=${r.value.asks.length}`);
    } else {
      console.error(`${symbols[i]}: FAILED - ${r.reason.message}`);
    }
  });
}

main();
// ===== ProxyHat SDKパターン =====
// ProxyHat SDK はローテーション・リトライ・ジオターゲティングを抽象化
const { ProxyHatClient } = require('@proxyhat/sdk'); // 仮想SDK例

const client = new ProxyHatClient({
  gateway: 'gate.proxyhat.com',
  port: 8080,
  username: 'user',
  password: 'pass',
  country: 'JP',
  rotation: 'per-request',  // リクエストごとにIPローテーション
  maxRetries: 3,
  backoff: 'exponential',
  timeout: 10000
});

async function fetchOrderbookSDK(symbol) {
  const url = `https://api.bybit.com/v5/market/orderbook?category=spot&symbol=${symbol}&limit=50`;
  const data = await client.get(url, {
    headers: { 'Accept': 'application/json' }
  });
  if (data.retCode !== 0) {
    throw new Error(`retCode=${data.retCode}, retMsg=${data.retMsg}`);
  }
  return data.result;
}

// 複数銘柄を順次取得(レート制限を考慮してシーケンシャル)
async function scrapeAll(symbols) {
  const results = [];
  for (const sym of symbols) {
    try {
      const ob = await fetchOrderbookSDK(sym);
      results.push({ symbol: sym, bestBid: ob.bids[0]?.[0], bestAsk: ob.asks[0]?.[0] });
      console.log(`${sym}: bid=${ob.bids[0]?.[0]}, ask=${ob.asks[0]?.[0]}`);
    } catch (err) {
      console.error(`${sym}: ${err.message}`);
    }
    // エンドポイント別レート予算: orderbook は 5 req/sec に制限
    await new Promise(r => setTimeout(r, 200));
  }
  return results;
}

scrapeAll(['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT', 'DOGEUSDT']);

実装例3: Python (httpx) でklineデータのページネーション

klineデータの過去分を取得する場合、startend パラメータで時間範囲を指定し、limit(最大1000件)でページネーションを行います。この場合、sticky sessionを使用することで、同一IPから継続的にアクセスし、IPローテーションによる一貫性の問題を回避できます。

# ===== 生プロキシパターン (httpx) =====
import httpx
import asyncio
import time
from datetime import datetime, timedelta, timezone

PROXY_URL = "http://user-country-JP-session-kline01:pass@gate.proxyhat.com:8080"

BASE_URL = "https://api.bybit.com/v5/market/kline"

async def fetch_kline_page(symbol, category, interval, start_ts, end_ts, retries=3):
    params = {
        "category": category,
        "symbol": symbol,
        "interval": str(interval),
        "start": str(start_ts),
        "end": str(end_ts),
        "limit": "1000"
    }
    for attempt in range(retries):
        try:
            async with httpx.AsyncClient(
                proxy=PROXY_URL,
                timeout=15.0,
                headers={
                    "Accept": "application/json",
                    "User-Agent": "Mozilla/5.0 (compatible; KlineCollector/1.0)"
                }
            ) as client:
                resp = await client.get(BASE_URL, params=params)
                resp.raise_for_status()
                data = resp.json()
                if data.get("retCode") == 0:
                    return data["result"]
                else:
                    raise Exception(f"retCode={data['retCode']}, retMsg={data['retMsg']}")
        except Exception as e:
            wait = 2 ** attempt * 500
            print(f"  [retry {attempt+1}/{retries}] {e}, waiting {wait}ms")
            if attempt == retries - 1:
                raise
            await asyncio.sleep(wait / 1000)

async def scrape_kline_history(symbol, category, interval, days_back):
    """過去N日分のklineを1000件ずつページネーションで取得"""
    now = int(datetime.now(timezone.utc).timestamp() * 1000)
    start = int((datetime.now(timezone.utc) - timedelta(days=days_back)).timestamp() * 1000)
    all_candles = []
    cursor = start
    
    while cursor < now:
        end = min(cursor + (interval * 60_000 * 1000), now)  # 1000本分の時間窓
        print(f"Fetching {symbol} kline: {datetime.fromtimestamp(cursor/1000, tz=timezone.utc)} -> {datetime.fromtimestamp(end/1000, tz=timezone.utc)}")
        result = await fetch_kline_page(symbol, category, interval, cursor, end)
        candles = result.get("list", [])
        if not candles:
            break
        all_candles.extend(candles)
        print(f"  Got {len(candles)} candles (total: {len(all_candles)})")
        # 最後のキャンドルのタイムスタンプ + 1ms から次ページ
        cursor = int(candles[-1][0]) + 1
        # レート予算: kline は 2 req/sec に制限
        await asyncio.sleep(0.5)
    
    return all_candles

# 実行例: BTCUSDT現物の1時間足を過去7日分取得
async def main():
    candles = await scrape_kline_history("BTCUSDT", "spot", 60, 7)
    print(f"\nTotal candles collected: {len(candles)}")
    if candles:
        print(f"First: ts={candles[0][0]}, open={candles[0][1]}")
        print(f"Last:  ts={candles[-1][0]}, close={candles[-1][4]}")

asyncio.run(main())
# ===== ProxyHat SDKパターン (Python) =====
from proxyhat import ProxyHatClient  # 仮想SDK例
import asyncio

async def fetch_kline_sdk(symbol, category, interval, start_ts, end_ts):
    client = ProxyHatClient(
        gateway="gate.proxyhat.com",
        port=8080,
        username="user",
        password="pass",
        country="JP",
        session="kline-pagination-01",  # sticky sessionでIP固定
        max_retries=3,
        backoff="exponential",
        timeout=15.0
    )
    url = f"https://api.bybit.com/v5/market/kline?category={category}&symbol={symbol}&interval={interval}&start={start_ts}&end={end_ts}&limit=1000"
    data = await client.get_async(url)
    if data.get("retCode") != 0:
        raise Exception(f"retCode={data['retCode']}, retMsg={data['retMsg']}")
    return data["result"]

async def scrape_with_sdk(symbol, category, interval, days_back):
    from datetime import datetime, timedelta, timezone
    now = int(datetime.now(timezone.utc).timestamp() * 1000)
    start = int((datetime.now(timezone.utc) - timedelta(days=days_back)).timestamp() * 1000)
    all_candles = []
    cursor = start
    while cursor < now:
        end = min(cursor + (interval * 60_000 * 1000), now)
        result = await fetch_kline_sdk(symbol, category, interval, cursor, end)
        candles = result.get("list", [])
        if not candles:
            break
        all_candles.extend(candles)
        cursor = int(candles[-1][0]) + 1
        await asyncio.sleep(0.5)  # レート予算
    return all_candles

asyncio.run(scrape_with_sdk("ETHUSDT", "spot", 60, 3))

実装例4: Python (aiohttp) でティッカー情報を高並列スクレイピング

複数銘柄のティッカー情報を高並列で取得する例です。エンドポイント別のレート予算を管理しつつ、ローテーションプロキシでIPを分散させます。

# ===== 生プロキシパターン (aiohttp) =====
import aiohttp
import asyncio
import logging
from aiohttp import ClientTimeout, ClientProxyConnectionError

logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s')
logger = logging.getLogger("bybit_scraper")

PROXY = "http://user-country-JP:pass@gate.proxyhat.com:8080"
BASE = "https://api.bybit.com/v5/market/tickers"

# エンドポイント別レート予 budget (req/sec)
RATE_BUDGETS = {
    "tickers": 3,   # 3 req/sec
    "orderbook": 5, # 5 req/sec
    "kline": 2,     # 2 req/sec
}

class RateLimiter:
    """トークンバケット方式のレート制限"""
    def __init__(self, rate_per_sec):
        self.rate = rate_per_sec
        self.tokens = rate_per_sec
        self.last_refill = asyncio.get_event_loop().time()
        self.lock = asyncio.Lock()
    
    async def acquire(self):
        async with self.lock:
            now = asyncio.get_event_loop().time()
            elapsed = now - self.last_refill
            self.tokens = min(self.rate, self.tokens + elapsed * self.rate)
            self.last_refill = now
            if self.tokens < 1:
                wait = (1 - self.tokens) / self.rate
                await asyncio.sleep(wait)
                self.tokens = 0
            else:
                self.tokens -= 1

class CircuitBreaker:
    """連続エラー時のサーキットブレーカー"""
    def __init__(self, threshold=5, reset_timeout=60):
        self.failures = 0
        self.threshold = threshold
        self.reset_timeout = reset_timeout
        self.opened_at = None
    
    def record_failure(self):
        self.failures += 1
        if self.failures >= self.threshold:
            self.opened_at = asyncio.get_event_loop().time()
            logger.warning(f"Circuit breaker OPEN after {self.failures} failures")
    
    def record_success(self):
        self.failures = 0
        self.opened_at = None
    
    def is_open(self):
        if self.opened_at is None:
            return False
        elapsed = asyncio.get_event_loop().time() - self.opened_at
        if elapsed > self.reset_timeout:
            logger.info("Circuit breaker resetting")
            self.failures = 0
            self.opened_at = None
            return False
        return True

limiter = RateLimiter(RATE_BUDGETS["tickers"])
breaker = CircuitBreaker(threshold=5, reset_timeout=60)

async def fetch_tickers(session, category, retries=3):
    if breaker.is_open():
        raise Exception("Circuit breaker is open")
    
    url = f"{BASE}?category={category}"
    for attempt in range(retries):
        await limiter.acquire()
        try:
            async with session.get(url, timeout=ClientTimeout(total=10)) as resp:
                if resp.status == 403:
                    logger.warning(f"403 on attempt {attempt+1} - rate limited or geo-blocked")
                    breaker.record_failure()
                    await asyncio.sleep(2 ** attempt)
                    continue
                resp.raise_for_status()
                data = await resp.json()
                if data.get("retCode") == 0:
                    breaker.record_success()
                    return data["result"]
                else:
                    raise Exception(f"retCode={data['retCode']}, retMsg={data['retMsg']}")
        except (aiohttp.ClientError, asyncio.TimeoutError) as e:
            logger.error(f"Attempt {attempt+1} failed: {type(e).__name__}: {e}")
            breaker.record_failure()
            if attempt < retries - 1:
                await asyncio.sleep(2 ** attempt)
            else:
                raise

async def scrape_all_tickers(categories):
    connector = aiohttp.TCPConnector(limit=20, limit_per_host=5)
    timeout = ClientTimeout(total=30)
    async with aiohttp.ClientSession(
        connector=connector,
        timeout=timeout,
        headers={"Accept": "application/json", "User-Agent": "Mozilla/5.0 (compatible; TickerBot/1.0)"}
    ) as session:
        tasks = []
        for cat in categories:
            tasks.append(fetch_tickers(session, cat))
        results = await asyncio.gather(*tasks, return_exceptions=True)
        
        for cat, result in zip(categories, results):
            if isinstance(result, Exception):
                logger.error(f"{cat}: FAILED - {result}")
            else:
                tickers = result.get("list", [])
                logger.info(f"{cat}: {len(tickers)} tickers retrieved")
                if tickers:
                    first = tickers[0]
                    logger.info(f"  Sample: {first.get('symbol')} lastPrice={first.get('lastPrice')}")

asyncio.run(scrape_all_tickers(["spot", "linear"]))

実装例5: 直近の約定履歴を継続的にポーリング

/v5/market/recent-trade エンドポイントを使用して、直近の約定履歴を定期的にポーリングする例です。リクエストごとにIPをローテーションしつつ、重複データを除外します。

# ===== Python (httpx) + ローテーションプロキシ =====
import httpx
import asyncio
import time
import json

# リクエストごとに異なるセッションID = IPローテーション
def get_proxy_url(request_id):
    return f"http://user-country-JP-session-rt{request_id}:pass@gate.proxyhat.com:8080"

BASE = "https://api.bybit.com/v5/market/recent-trade"
seen_trade_ids = set()
request_counter = 0

async def poll_recent_trades(symbol, category, interval_sec=2, max_polls=100):
    global request_counter, seen_trade_ids
    new_trades = []
    
    for poll_idx in range(max_polls):
        request_counter += 1
        proxy = get_proxy_url(request_counter)
        params = {"category": category, "symbol": symbol, "limit": "50"}
        
        try:
            async with httpx.AsyncClient(
                proxy=proxy,
                timeout=10.0,
                headers={"Accept": "application/json"}
            ) as client:
                resp = await client.get(BASE, params=params)
                resp.raise_for_status()
                data = resp.json()
                
                if data.get("retCode") != 0:
                    print(f"[poll {poll_idx}] retCode={data['retCode']}, retMsg={data['retMsg']}")
                    await asyncio.sleep(interval_sec * 2)
                    continue
                
                trades = data["result"].get("list", [])
                fresh = []
                for t in trades:
                    trade_id = t.get("execId", "")
                    if trade_id and trade_id not in seen_trade_ids:
                        seen_trade_ids.add(trade_id)
                        fresh.append(t)
                
                if fresh:
                    new_trades.extend(fresh)
                    print(f"[poll {poll_idx}] {len(fresh)} new trades (total new: {len(new_trades)})")
                    for ft in fresh[:3]:
                        print(f"  {ft.get('time')} price={ft.get('price')} qty={ft.get('size')} side={ft.get('side')}")
                else:
                    print(f"[poll {poll_idx}] no new trades")
        
        except Exception as e:
            print(f"[poll {poll_idx}] ERROR: {type(e).__name__}: {e}")
            await asyncio.sleep(interval_sec * 3)  # バックオフ
            continue
        
        await asyncio.sleep(interval_sec)
    
    return new_trades

asyncio.run(poll_recent_trades("BTCUSDT", "spot", interval_sec=2, max_polls=30))

RESTスナップショット vs WebSocket orderbook.50ストリーム

Bybitは公開WebSocketチャンネルも提供しており、orderbook.50.{symbol}orderbook.200.{symbol} でリアルタイムの板情報ストリームを購読できます。RESTのポーリング方式とWebSocket方式の比較は以下の通りです。

項目REST (/v5/market/orderbook)WebSocket (orderbook.50)
データ形式スナップショット(取得時点の完全な板)スナップショット+差分更新
レイテンシポーリング間隔に依存(通常200ms〜2s)リアルタイム(通常50ms未満)
レート制限リクエストごとに消費接続ごと(購読後は追加リクエスト不要)
プロキシ要件リクエストごとのIPローテーションが可能WSS接続を維持(sticky session推奨)
適用ケース定期的なスナップショット保存、バッチ分析リアルタイムトレーディング、ライブダッシュボード

WebSocketを使用する場合、ProxyHatのSOCKS5プロキシ(gate.proxyhat.com:1080)とsticky sessionを組み合わせることで、安定したWSS接続を維持できます。

# ===== WebSocket + SOCKS5 プロキシ (Node.js) =====
const WebSocket = require('ws');
const { SocksProxyAgent } = require('socks-proxy-agent');

// sticky session でIPを固定(WSS接続の安定性確保)
const socksAgent = new SocksProxyAgent(
  'socks5://user-country-JP-session-ws01:pass@gate.proxyhat.com:1080'
);

const ws = new WebSocket('wss://stream.bybit.com/v5/public/spot', {
  agent: socksAgent
});

ws.on('open', () => {
  console.log('WebSocket connected via ProxyHat SOCKS5');
  // orderbook.50 ストリームを購読
  ws.send(JSON.stringify({
    op: 'subscribe',
    args: ['orderbook.50.BTCUSDT', 'orderbook.50.ETHUSDT']
  }));
});

ws.on('message', (data) => {
  const msg = JSON.parse(data.toString());
  if (msg.topic && msg.topic.startsWith('orderbook.50')) {
    const symbol = msg.topic.split('.').pop();
    if (msg.type === 'snapshot') {
      console.log(`[${symbol}] snapshot: ${msg.data.bids.length} bids, ${msg.data.asks.length} asks`);
    } else if (msg.type === 'delta') {
      console.log(`[${symbol}] delta: delete=${msg.data.delete?.length}, update=${msg.data.update?.length}, insert=${msg.data.insert?.length}`);
    }
  }
});

ws.on('error', (err) => {
  console.error('WebSocket error:', err.message);
});

ws.on('close', () => {
  console.log('WebSocket closed');
});

// グレースフルシャットダウン
process.on('SIGINT', () => {
  ws.close();
  process.exit(0);
});

よくある失敗とエッジケース

1. retCodeのチェック漏れ

HTTPステータス200でも retCode が非ゼロのケースがあります。必ずJSONボディの retCode を検証してください。retMsg には「rate limit exceeded」や「invalid symbol」などのメッセージが含まれます。

2. バーストアクセスによるIPバン

ループ内で await なしに連続リクエストを送ると、数秒で403と10分間のIPバンが発生します。レートリミッター(トークンバケット)を必ず実装してください。エンドポイント別のレート予算を分けることも重要です。

3. 地域制限の見落とし

データセンタープロキシを使用して「アクセスできるのにデータが返ってこない」場合は、CloudFrontレベルでジオブロックされている可能性があります。user-country-JP などのジオターゲティングフラグで許可された国を指定してください。

4. kline ページネーションでの時刻の扱い

Bybitのklineエンドポイントは startend をミリ秒タイムスタンプで受け取ります。ページネーション時は最後のキャンドルのタイムスタンプ +1ms を次の start に設定してください。intervalパラメータは分単位の整数(1, 5, 15, 30, 60, 240, 720, D, W, M)です。

5. プロキシ認証エラー

ユーザー名にジオターゲティングフラグやセッションフラグを含める場合、コロン(:)で区切る必要があります。形式は user-country-JP-session-abc123 です。パスワードに特殊文字が含まれる場合はURLエンコードしてください。

ProxyHat固有のセットアップとベストプラクティス

ProxyHatを使用したBybit APIスクレイピングのベストプラクティスをまとめます。

  • エンドポイント別レート予算: orderbook は5 req/sec、tickers は3 req/sec、kline は2 req/secを目安に設定。
  • per-request ローテーション: 高頻度ポーリングにはリクエストごとにIPを変える user-session-{unique_id} パターンを使用。
  • sticky session: klineページネーションやWebSocket接続では user-session-abc123 でIPを固定。
  • 指数バックオフ: 403または retCode 非ゼロ時は2^attempt * 500msでリトライ。
  • サーキットブレーカー: 連続5回失敗で60秒間リクエストを停止し、IPバンの延長を防ぐ。
  • ジオターゲティング: user-country-JP または user-country-SG で許可された国からアクセス。
  • タイムアウト: プロキシ経由のレイテンシを考慮し、10〜15秒のタイムアウトを設定。

詳細なプロキシ設定についてはProxyHatドキュメントを参照してください。ウェブスクレイピングのユースケースについてはこちらを、SERPトラッキングについてはこちらを参照してください。

Key Takeaways(重要ポイント)

  • Bybit V5 Market APIcategory パラメータで spot/linear/inverse/option を切り替え、統一された retCode/retMsg エンベロープでレスポンスを返す。
  • IPベースのレート制限はエンドポイントごとに設定され、超過すると403と約10分間のIPバンが発生する。
  • 地理的制限はCloudFrontレベルで実装され、米国・英国・制限地域からのアクセスがブロックされる。住宅プロキシのジオターゲティングで回避可能。
  • ローテーションプロキシgate.proxyhat.com:8080)でリクエストごとにIPを変えることで、各IPのレート制限クォータを独立消費できる。
  • Sticky session-session-abc123)はklineページネーションやWebSocket接続の安定性に不可欠。
  • REST vs WebSocketの選択は用途次第。定期的なスナップショット保存はREST、リアルタイムトレーディングはWebSocket(SOCKS5 :1080)。
  • 本ガイドのプロキシ設定: HTTPは gate.proxyhat.com:8080、SOCKS5は gate.proxyhat.com:1080。ユーザー名に country-JPsession-xxx フラグを指定。

Bybit V5 Market APIのスクレイピングは、適切なプロキシインフラとレート管理があれば、安定した市場データパイプラインを構築できます。ProxyHatのローテーションプロキシを活用し、エンドポイント別のレート予算と指数バックオフを組み合わせることで、403バンや地理的制限を回避しながら高品質なデータ収集が可能になります。ProxyHatのプランを確認し、今すぐBybit市場データの収集を始めましょう。

よくある質問

Bybit V5 Market APIのローテーションプロキシスクレイピングとは何ですか?

Bybitの公開市場データエンドポイント(/v5/market/orderbook、/v5/market/tickersなど)に対して、リクエストごとにIPを切り替えるローテーションプロキシを経由してアクセスし、レート制限やIPベースのブロックを回避しながらデータを収集する手法です。

なぜBybit APIスクレイピングでプロキシが重要なのですか?

BybitはエンドポイントごとにIPベースのレート制限を設けており、制限超過で403レスポンスと約10分間のIPバンが発生します。また、米国・英国などの地域制限によりCloudFront経由でアクセスがブロックされる場合があります。ローテーションプロキシを使うことで、これらの制限を回避し、安定したデータ収集が可能になります。

Bybit APIスクレイピングに最適なプロキシタイプは何ですか?

Bybitの地理的制限を回避するには、住宅プロキシ(Residential Proxy)が最適です。データセンタープロキシは高速ですが、CloudFrontの geo-block に検出されやすい傾向があります。住宅プロキシは実際のISP IPアドレスを使用するため、ブロックされるリスクが低く、国・都市レベルのジオターゲティングも可能です。

Bybit APIスクレイピングでブロックを回避するにはどうすればよいですか?

リクエストごとのIPローテーション、エンドポイント別のレート予算管理、指数バックオフ付きリトライ、適切なUser-AgentとAcceptヘッダーの設定、そして許可された国からの住宅プロキシの使用を組み合わせることで、ブロックを大幅に減らすことができます。また、robots.txtとBybitの利用規約を遵守することが重要です。

始める準備はできましたか?

148か国以上の住宅用・ISP・モバイルプロキシ。無料アカウントを作成。

無料アカウントを作成
← ブログに戻る