Scrapear la API de Mercado V5 de Bybit con proxies rotativos es una técnica esencial para bots de trading y pipelines de datos que necesitan acceso fiable a api.bybit.com/v5/market/ desde regiones restringidas o a alta frecuencia. Esta guía cubre los endpoints públicos, los límites de tasa por IP, las restricciones geográficas y cómo configurar proxies residenciales a través de gate.proxyhat.com con rotación de IPs y sesiones sticky.
Aviso legal: Los endpoints de mercado públicos de Bybit están documentados oficialmente, pero debes revisar los Términos de Servicio de Bybit antes de cualquier uso comercial. Esta guía se centra en datos públicos de mercado y no promueve el scraping de endpoints autenticados ni el violar restricciones legales locales.
Por qué scrapear la API de Mercado V5 de Bybit con proxies rotativos
Bybit migró a su API unificada V5 en 2023, consolidando endpoints de spot, derivados lineares y opciones bajo un mismo esquema (documentación oficial de Bybit V5). Los endpoints de mercado públicos bajo /v5/market/ no requieren autenticación, pero sí aplican límites de tasa por IP y restricciones geográficas agresivas.
Si tu infraestructura corre desde EE. UU., el Reino Unido o regiones restringidas, Bybit devuelve un 403 Forbidden a nivel de CloudFront antes de que la solicitud llegue al backend. Y si haces demasiadas peticiones desde una sola IP, Bybit impone un ban temporal de aproximadamente 10 minutos con un retCode de error específico en el cuerpo JSON.
Los proxies residenciales rotativos resuelven ambos problemas: te dan IPs de países permitidos y distribuyen el tráfico para evitar tripar los límites por IP. ProxyHat ofrece acceso a través de gate.proxyhat.com:8080 (HTTP) y gate.proxyhat.com:1080 (SOCKS5), con geo-segmentación por país y ciudad en el nombre de usuario.
Endpoints públicos de /v5/market/
La V5 Market API expone cuatro endpoints principales para datos públicos:
| Endpoint | Método | Parámetros clave | Uso típico |
|---|---|---|---|
/v5/market/orderbook | GET | category (spot|linear|option), symbol, limit (depth) | Snapshot de libro de órdenes |
/v5/market/tickers | GET | category, symbol (opcional) | Precios y stats de 24h |
/v5/market/kline | GET | category, symbol, interval, start, end, limit | Velas OHLCV históricas |
/v5/market/recent-trade | GET | category, symbol, limit | Trades recientes |
El parámetro category
El parámetro category es obligatorio en todos los endpoints de mercado. Acepta spot, linear (futuros perpetuos lineares USDT), inverse y option. Omitirlo o usar un valor inválido devuelve retCode: 10001 con un retMsg descriptivo.
El envelope de respuesta retCode/retMsg
Toda la V5 API usa un envelope JSON consistente:
{
"retCode": 0,
"retMsg": "OK",
"result": {
"category": "spot",
"symbol": "BTCUSDT",
"list": [...]
}
}
Un retCode de 0 indica éxito. Cualquier otro valor señala un error —puede ser de validación de parámetros, rate limit, o de servicio. Es crítico comprobar retCode antes de procesar result, porque Bybit devuelve HTTP 200 incluso en errores lógicos.
Límites de tasa por IP y bans temporales
Bybit aplica límites de tasa por endpoint y por IP. Según la documentación oficial de rate limits de Bybit V5, los endpoints de mercado públicos tienen límites distintos:
- /v5/market/orderbook: límite estricto, típicamente 10–20 requests/segundo por IP. Las ráfagas que excedan esto provocan un ban temporal.
- /v5/market/tickers: más permisivo, ~50 requests/segundo por IP.
- /v5/market/kline: ~10 requests/segundo por IP, con un máximo de 1000 velas por request.
- /v5/market/recent-trade: ~10 requests/segundo por IP, máximo 1000 trades por request.
Cuando una IP excede el límite, Bybit devuelve un 403 HTTP o un retCode de rate limit (típicamente 10001 o un código específico de rate limit). El ban dura aproximadamente 10 minutos, durante los cuales todas las peticiones desde esa IP fallan. Esto hace que la rotación de IPs sea crítica para mantener throughput.
Restricciones geográficas y CloudFront
Bybit bloquea tráfico desde EE. UU., el Reino Unido, Singapur y otras regiones reguladas. El bloqueo ocurre a nivel de CloudFront: la respuesta es un 403 con un body HTML genérico de CloudFront, no un JSON de Bybit. Esto significa que ni siquiera llegas a la API.
Con proxies residenciales de ProxyHat, puedes enmascarar tu IP de origen con una IP de un país permitido (por ejemplo, Alemania, Japón, Brasil). La geo-segmentación se controla en el nombre de usuario:
# HTTP proxy con geo-targeting a Alemania
http://user-country-DE:PASSWORD@gate.proxyhat.com:8080
# SOCKS5 proxy con geo-targeting a Japón
socks5://user-country-JP:PASSWORD@gate.proxyhat.com:1080
Consulta las ubicaciones disponibles de ProxyHat para ver qué países soporta.
Implementación: rotación de IPs en Python
Empezamos con un ejemplo en Python usando requests con proxy raw y luego con el patrón de rotación de ProxyHat. El objetivo es obtener tickers de múltiples símbolos rotando IPs en cada request.
Ejemplo 1: Proxy HTTP raw con requests
import requests
import time
import logging
from requests.exceptions import RequestException
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
logger = logging.getLogger(__name__)
BYBIT_BASE = "https://api.bybit.com"
PROXY_URL = "http://user-country-DE:PASSWORD@gate.proxyhat.com:8080"
PROXIES = {"http": PROXY_URL, "https": PROXY_URL}
SYMBOLS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "DOGEUSDT"]
def fetch_tickers(category="spot", symbol=None):
params = {"category": category}
if symbol:
params["symbol"] = symbol
for attempt in range(5):
try:
resp = requests.get(
f"{BYBIT_BASE}/v5/market/tickers",
params=params,
proxies=PROXIES,
timeout=10
)
data = resp.json()
if data.get("retCode") != 0:
logger.warning(f"Bybit retCode={data.get('retCode')} retMsg={data.get('retMsg')}")
if data.get("retCode") == 10001:
time.sleep(2 ** attempt)
continue
return None
return data["result"]
except RequestException as e:
logger.error(f"Request error: {e}")
time.sleep(2 ** attempt)
return None
for sym in SYMBOLS:
result = fetch_tickers(symbol=sym)
if result and result.get("list"):
ticker = result["list"][0]
logger.info(f"{sym}: lastPrice={ticker['lastPrice']}, volume24h={ticker['volume24h']}")
time.sleep(0.1) # Rate budget: ~10 req/s per IP
Ejemplo 2: Rotación de IPs con proxy URL builder
import requests
import time
import random
import string
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
logger = logging.getLogger(__name__)
BYBIT_BASE = "https://api.bybit.com"
PROXYHAT_USER = "user"
PROXYHAT_PASS = "PASSWORD"
COUNTRIES = ["DE", "JP", "BR", "NL", "SG"]
def build_proxy_url(country=None, session_id=None):
username = PROXYHAT_USER
if country:
username += f"-country-{country}"
if session_id:
username += f"-session-{session_id}"
return f"http://{username}:{PROXYHAT_PASS}@gate.proxyhat.com:8080"
def fetch_with_rotation(url, params, max_retries=5):
for attempt in range(max_retries):
country = random.choice(COUNTRIES)
proxy_url = build_proxy_url(country=country)
proxies = {"http": proxy_url, "https": proxy_url}
try:
resp = requests.get(url, params=params, proxies=proxies, timeout=10)
if resp.status_code == 403:
logger.warning(f"403 from {country}, rotating...")
time.sleep(1 + attempt)
continue
data = resp.json()
if data.get("retCode") == 0:
return data["result"]
logger.warning(f"retCode={data.get('retCode')} msg={data.get('retMsg')}")
time.sleep(2 ** attempt)
except Exception as e:
logger.error(f"Error with {country}: {e}")
time.sleep(2 ** attempt)
return None
# Scrapear orderbook de BTCUSDT con rotación
result = fetch_with_rotation(
f"{BYBIT_BASE}/v5/market/orderbook",
{"category": "linear", "symbol": "BTCUSDT", "limit": 50}
)
if result:
logger.info(f"Bids: {len(result.get('b', []))}, Asks: {len(result.get('a', []))}")
Implementación: rotación de IPs en Node.js con axios
Ejemplo 3: Proxy raw con axios (HttpsProxyAgent)
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const logger = require('pino')();
const BYBIT_BASE = 'https://api.bybit.com';
const PROXY_URL = 'http://user-country-DE:PASSWORD@gate.proxyhat.com:8080';
const agent = new HttpsProxyAgent(PROXY_URL);
const SYMBOLS = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT', 'DOGEUSDT'];
async function fetchTickers(symbol, category = 'spot', retries = 5) {
for (let attempt = 0; attempt < retries; attempt++) {
try {
const resp = await axios.get(`${BYBIT_BASE}/v5/market/tickers`, {
params: { category, symbol },
httpsAgent: agent,
timeout: 10000
});
if (resp.data.retCode !== 0) {
logger.warn({ retCode: resp.data.retCode, retMsg: resp.data.retMsg });
await sleep(Math.pow(2, attempt) * 1000);
continue;
}
return resp.data.result;
} catch (err) {
logger.error({ err: err.message, symbol });
await sleep(Math.pow(2, attempt) * 1000);
}
}
return null;
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
async function main() {
for (const sym of SYMBOLS) {
const result = await fetchTickers(sym);
if (result?.list?.[0]) {
const t = result.list[0];
logger.info({ sym, lastPrice: t.lastPrice, volume24h: t.volume24h });
}
await sleep(100); // Rate budget: ~10 req/s per IP
}
}
main().catch(logger.error);
Ejemplo 4: Rotación de IPs con proxy builder en Node.js
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const logger = require('pino')();
const BYBIT_BASE = 'https://api.bybit.com';
const PROXYHAT_USER = 'user';
const PROXYHAT_PASS = 'PASSWORD';
const COUNTRIES = ['DE', 'JP', 'BR', 'NL', 'SG'];
function buildProxyUrl(opts = {}) {
let username = PROXYHAT_USER;
if (opts.country) username += `-country-${opts.country}`;
if (opts.session) username += `-session-${opts.session}`;
return `http://${username}:${PROXYHAT_PASS}@gate.proxyhat.com:8080`;
}
function randomCountry() {
return COUNTRIES[Math.floor(Math.random() * COUNTRIES.length)];
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
async function fetchWithRotation(endpoint, params, retries = 5) {
for (let attempt = 0; attempt < retries; attempt++) {
const country = randomCountry();
const proxyUrl = buildProxyUrl({ country });
const agent = new HttpsProxyAgent(proxyUrl);
try {
const resp = await axios.get(`${BYBIT_BASE}${endpoint}`, {
params,
httpsAgent: agent,
timeout: 10000
});
if (resp.status === 403) {
logger.warn({ country, status: 403 });
await sleep(1000 + attempt * 1000);
continue;
}
if (resp.data.retCode === 0) return resp.data.result;
logger.warn({ retCode: resp.data.retCode, retMsg: resp.data.retMsg, country });
await sleep(Math.pow(2, attempt) * 1000);
} catch (err) {
logger.error({ err: err.message, country });
await sleep(Math.pow(2, attempt) * 1000);
}
}
return null;
}
// Scrapear orderbook de múltiples símbolos con rotación
async function scrapeOrderbooks(symbols) {
for (const sym of symbols) {
const result = await fetchWithRotation('/v5/market/orderbook', {
category: 'linear',
symbol: sym,
limit: 50
});
if (result) {
logger.info({
sym,
bids: result.b?.length,
asks: result.a?.length,
bestBid: result.b?.[0]?.[0],
bestAsk: result.a?.[0]?.[0]
});
}
await sleep(120); // Rate budget per IP
}
}
scrapeOrderbooks(['BTCUSDT', 'ETHUSDT', 'SOLUSDT']).catch(logger.error);
Sesiones sticky para paginación de kline
El endpoint /v5/market/kline permite un máximo de 1000 velas por request. Para descargar historial largo necesitas paginar usando los parámetros start y end (timestamps en milisegundos). Es crítico mantener la misma IP durante toda la paginación para evitar inconsistencias y bans intermedios.
ProxyHat soporta sesiones sticky mediante el flag -session- en el nombre de usuario. Mientras la sesión esté activa, todas las peticiones saldrán desde la misma IP.
Ejemplo 5: Paginación de kline con sesión sticky en Python
import requests
import time
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
logger = logging.getLogger(__name__)
BYBIT_BASE = "https://api.bybit.com"
PROXYHAT_USER = "user"
PROXYHAT_PASS = "PASSWORD"
def build_sticky_proxy(session_id, country="DE"):
username = f"{PROXYHAT_USER}-country-{country}-session-{session_id}"
return f"http://{username}:{PROXYHAT_PASS}@gate.proxyhat.com:8080"
def fetch_kline_page(symbol, category, interval, start, end, session_id, country="DE"):
proxy_url = build_sticky_proxy(session_id, country)
proxies = {"http": proxy_url, "https": proxy_url}
for attempt in range(5):
try:
resp = requests.get(
f"{BYBIT_BASE}/v5/market/kline",
params={
"category": category,
"symbol": symbol,
"interval": interval,
"start": start,
"end": end,
"limit": 1000
},
proxies=proxies,
timeout=15
)
if resp.status_code == 403:
logger.warning(f"403, switching country...")
return None
data = resp.json()
if data.get("retCode") == 0:
return data["result"]["list"]
logger.warning(f"retCode={data.get('retCode')} msg={data.get('retMsg')}")
time.sleep(2 ** attempt)
except Exception as e:
logger.error(f"Error: {e}")
time.sleep(2 ** attempt)
return None
def download_kline_history(symbol, category, interval, start_ms, end_ms):
session_id = f"kline-{symbol}-{int(time.time())}"
all_candles = []
cursor = start_ms
while cursor < end_ms:
page_end = min(cursor + 1000 * 60_000, end_ms) # Ajustar según interval
candles = fetch_kline_page(
symbol, category, interval, cursor, page_end, session_id
)
if not candles:
logger.error(f"Failed at cursor={cursor}, aborting")
break
all_candles.extend(candles)
logger.info(f"Got {len(candles)} candles, total={len(all_candles)}")
cursor = page_end + 1
time.sleep(0.15) # Rate budget: ~6-7 req/s per IP
return all_candles
# Descargar 1 hora de velas de 1 minuto para BTCUSDT
now_ms = int(time.time() * 1000)
start_ms = now_ms - 3600 * 1000
candles = download_kline_history("BTCUSDT", "linear", "1", start_ms, now_ms)
logger.info(f"Total candles downloaded: {len(candles)}")
Ejemplo 6: Sesión sticky en Node.js para kline
const axios = require('axios');
const { HttpsProxyAgent } = require('https-proxy-agent');
const logger = require('pino')();
const BYBIT_BASE = 'https://api.bybit.com';
const PROXYHAT_USER = 'user';
const PROXYHAT_PASS = 'PASSWORD';
function buildStickyProxy(sessionId, country = 'DE') {
const username = `${PROXYHAT_USER}-country-${country}-session-${sessionId}`;
return `http://${username}:${PROXYHAT_PASS}@gate.proxyhat.com:8080`;
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
async function fetchKlinePage(symbol, category, interval, start, end, sessionId, country = 'DE') {
const proxyUrl = buildStickyProxy(sessionId, country);
const agent = new HttpsProxyAgent(proxyUrl);
for (let attempt = 0; attempt < 5; attempt++) {
try {
const resp = await axios.get(`${BYBIT_BASE}/v5/market/kline`, {
params: { category, symbol, interval, start, end, limit: 1000 },
httpsAgent: agent,
timeout: 15000
});
if (resp.data.retCode === 0) return resp.data.result.list;
logger.warn({ retCode: resp.data.retCode, retMsg: resp.data.retMsg });
await sleep(Math.pow(2, attempt) * 1000);
} catch (err) {
logger.error({ err: err.message });
await sleep(Math.pow(2, attempt) * 1000);
}
}
return null;
}
async function downloadKlineHistory(symbol, category, interval, startMs, endMs) {
const sessionId = `kline-${symbol}-${Date.now()}`;
const allCandles = [];
let cursor = startMs;
while (cursor < endMs) {
const pageEnd = Math.min(cursor + 1000 * 60000, endMs);
const candles = await fetchKlinePage(symbol, category, interval, cursor, pageEnd, sessionId);
if (!candles) {
logger.error({ msg: 'Failed, aborting', cursor });
break;
}
allCandles.push(...candles);
logger.info({ got: candles.length, total: allCandles.length });
cursor = pageEnd + 1;
await sleep(150);
}
return allCandles;
}
const now = Date.now();
const start = now - 3600 * 1000;
downloadKlineHistory('BTCUSDT', 'linear', '1', start, now)
.then(candles => logger.info({ total: candles.length }))
.catch(logger.error);
curl: prueba rápida de conectividad
# Probar orderbook con proxy HTTP de Alemania
curl -x "http://user-country-DE:PASSWORD@gate.proxyhat.com:8080" \
"https://api.bybit.com/v5/market/orderbook?category=linear&symbol=BTCUSDT&limit=50"
# Probar tickers con proxy SOCKS5 de Japón
curl -x "socks5://user-country-JP:PASSWORD@gate.proxyhat.com:1080" \
"https://api.bybit.com/v5/market/tickers?category=spot"
REST snapshots vs WebSocket orderbook.50
El endpoint REST /v5/market/orderbook devuelve un snapshot puntual del libro de órdenes. Para datos en tiempo real, Bybit ofrece un WebSocket público con el stream orderbook.50.{symbol} que envía actualizaciones incrementales cada ~50ms.
| Aspecto | REST /v5/market/orderbook | WebSocket orderbook.50 |
|---|---|---|
| Latencia | ~200–500ms por request | ~50–100ms por update |
| Rate limit | 10–20 req/s por IP | 1 conexión por símbolo |
| Carga de red | Alta (snapshot completo) | Baja (delta incremental) |
| Complejidad | Baja (HTTP simple) | Media (gestión de conexión, reconexión) |
| Ideal para | Snapshots puntuales, backtesting | Trading en vivo, arbitraje |
Para la mayoría de bots de trading en vivo, el WebSocket es superior. Pero para pipelines de datos que necesitan snapshots periódicos (cada minuto, cada hora), el REST con rotación de proxies es más sencillo y suficiente.
Errores comunes y edge cases
- No comprobar retCode: Bybit devuelve HTTP 200 incluso en errores lógicos. Siempre valida
retCode == 0antes de procesarresult. - Rate limit en orderbook: El endpoint de orderbook tiene el límite más estricto. Las ráfagas de más de 20 req/s por IP provocan bans de 10 minutos.
- Sin backoff exponencial: Reintentar inmediatamente después de un 403 o rate limit empeora el problema. Usa backoff exponencial con jitter.
- Cambio de IP en mitad de paginación: Si rotas IPs durante la paginación de kline, puedes perder velas o duplicarlas. Usa sesiones sticky.
- Ignorar el parámetro category: Es obligatorio. Usar
spotcuando el símbolo es un perpetuo linear devuelve un error. - No respetar robots.txt ni ToS: Revisa siempre los términos de servicio de Bybit. Los endpoints públicos están documentados, pero el uso comercial puede tener restricciones.
Configuración de ProxyHat y enlaces útiles
Para empezar con ProxyHat, consulta la documentación oficial de ProxyHat. Puedes ver los planes de precios y elegir entre proxies residenciales, móviles o datacenter.
Para casos de uso de scraping más amplios, revisa nuestra guía de web scraping con proxies y de SERP tracking.
Puntos clave
- El envelope retCode/retMsg es obligatorio: Bybit devuelve HTTP 200 incluso en errores. Siempre valida
retCode == 0. - Rate limits por IP: El endpoint de orderbook es el más restrictivo (~20 req/s). Los bans duran ~10 minutos.
- Geo-bloqueo a nivel de CloudFront: Usa proxies residenciales con geo-segmentación (
-country-DE) para evitar 403 de regiones restringidas. - Sesiones sticky para paginación: Usa
-session-abc123en el nombre de usuario para mantener la misma IP durante paginación de kline. - Backoff exponencial: Esencial para manejar rate limits y bans temporales sin empeorar la situación.
- REST vs WebSocket: Para datos en vivo usa WebSocket
orderbook.50; para snapshots periódicos, REST con rotación es suficiente.
¿Listo para scrapear la API de Bybit sin bans ni bloqueos? Configura tus proxies residenciales en
gate.proxyhat.com:8080y empieza con los ejemplos de esta guía.






