Si automatizas un exchange con ccxt, tarde o temprano lo verás en los logs: rachas cortas de 403 Forbidden que pegan a fetch_balance, a los OHLCV o al saldo de earn, y que desaparecen solas a los pocos minutos. No es que tu API key esté mal. Es el WAF (Cloudflare) que muchos exchanges ponen delante de su REST, challengueando a algo que "parece un bot".
Y tu bot es un bot — pero uno legítimo, operando tu propia cuenta contra la API oficial. El problema no es de permisos, es de reputación de cliente HTTP. Esto va de reducir los falsos positivos del WAF, no de evadir ningún control de acceso.
Dos capas que lo mitigan
Saqué este patrón de un bot propio sobre OKX, tras varias rachas de 403, y lo publiqué como librería: ccxt-resilience (Apache-2.0).
1. Que el WAF challengue menos: harden
Un cliente ccxt por defecto se anuncia como lo que es. Ajustar un User-Agent de navegador, la cabecera Accept-Language y un timeout holgado hace que Cloudflare lo desafíe con menos frecuencia:
import ccxt
from ccxt_resilience import harden
exchange = harden(ccxt.okx({
"apiKey": ...,
"secret": ...,
"password": ...,
}))
harden toca un cliente ya construido, devuelve el mismo objeto (encadenable) y nunca rompe su construcción: si algo falla al fijar los atributos, los deja como estaban.
2. Reintentar solo lo que se debe: with_retry
La tentación es envolver todo en un try/except que reintente. Es una trampa: reintentar un error de credenciales o de fondos solo gasta tiempo, termina igual de mal, y esconde bugs de lógica detrás de esperas.
La clave es reintentar únicamente lo transitorio —403/Cloudflare, 429, timeouts— con backoff exponencial y jitter, y re-lanzar los errores reales en el acto:
from ccxt_resilience import with_retry
balance = with_retry(exchange.fetch_balance)
ohlcv = with_retry(exchange.fetch_ohlcv, "BTC/USDT", timeframe="1m",
attempts=4, base=1.0, max_s=8.0)
Un error de autenticación se re-lanza inmediatamente, sin reintentar. Y si se agotan los intentos, se re-lanza la última excepción, para que tu manejador de fail-safe siga gobernando (por ejemplo, devolver el último valor cacheado).
La espera del intento i es min(max_s, base * 2**i) + rand()*base: crece de forma acotada y el jitter evita que varios clientes reintenten sincronizados.
La clasificación es explícita
Lo que hace o no reintetable a una excepción no está escondido: es una función que puedes inspeccionar y sustituir.
from ccxt_resilience import is_transient_error
# ¿Un 403 de Cloudflare? Sí. ¿Una clave inválida? No.
is_transient_error(Exception("403 Forbidden Cloudflare")) # True
is_transient_error(Exception("invalid api key")) # False
Con ccxt instalado clasifica también por tipo de excepción (DDoSProtection, RequestTimeout…); sin él, por el mensaje. Por eso ccxt es dependencia blanda: la librería funciona con o sin él. Y si tu caso es otra API, with_retry acepta tu propio predicado de reintento.
Instalación
pip install ccxt-resilience
El código, los tests y el detalle están en el repo: github.com/isazajuancarlos/ccxt-resilience. Es infraestructura pequeña y sin estado; úsala por partes.
Xiliux