Saltar al contenido principal

Construir un Bot de Trading P2P con la API Binance C2C

Tutorial completo para construir un bot P2P automatizado en Binance C2C desde cero. Cubre el loop completo: monitorear el order book, gestionar anuncios, procesar órdenes entrantes e integrar el sistema de chat.

EP-0EP-4EP-7EP-8EP-13EP-15EP-33EP-34

1. Vista General de Arquitectura

Un bot P2P en producción necesita cinco componentes centrales trabajando juntos en un event loop asíncrono:

┌─────────────────────────────────────────────────────────────────┐
│                     Arquitectura del Bot P2P                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌──────────────┐    ┌───────────────┐    ┌────────────────┐  │
│   │  Market Data │    │  Gestor de    │    │  Procesador    │  │
│   │  (EP-0)      │───▶│  Anuncios     │    │  de Órdenes   │  │
│   │  Poll loop   │    │  (EP-4,7,8)  │    │  (EP-13,15)   │  │
│   └──────────────┘    └───────────────┘    └────────────────┘  │
│                                                    │            │
│   ┌──────────────┐    ┌───────────────┐           │            │
│   │  Sistema de  │◀───│  WebSocket    │◀──────────┘            │
│   │  Chat        │    │  (cred EP-33) │                        │
│   │  (EP-33,34)  │    │  Tiempo real  │                        │
│   └──────────────┘    └───────────────┘                        │
│                                                                 │
│   ┌──────────────────────────────────────────────────────────┐ │
│   │  Rate Limiter + Circuit Breaker  (todas las llamadas)    │ │
│   └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘

El loop de market data se ejecuta cada pocos segundos y alimenta al gestor de anuncios. El procesador de órdenes detecta nuevas órdenes y dispara el sistema de chat. Todas las llamadas a Binance pasan por un rate limiter compartido para evitar soft limits y circuit breakers.

2. Configuración de Autenticación

La mayoría de los endpoints requieren firmas HMAC-SHA256. Consulta la guía de Primeros Pasos para la configuración completa de firma. Aquí está el helper reutilizable que usarás en todo el tutorial:

auth.py
import hmac
import hashlib
import time
import httpx

BASE_URL = "https://api.binance.com"

def sign(api_secret: str, query: str) -> str:
    return hmac.new(
        api_secret.encode(),
        query.encode(),
        hashlib.sha256,
    ).hexdigest()

async def signed_post(
    client: httpx.AsyncClient,
    api_key: str,
    api_secret: str,
    path: str,
    body: dict,
) -> dict:
    timestamp = int(time.time() * 1000)
    query = f"recvWindow=60000&timestamp={timestamp}"
    signature = sign(api_secret, query)
    resp = await client.post(
        f"{BASE_URL}{path}?{query}&signature={signature}",
        json=body,
        headers={"X-MBX-APIKEY": api_key, "clientType": "web"},
    )
    return resp.json()

3. Monitorear el Order Book (EP-0)

EP-0 es el endpoint público BAPI de búsqueda — no requiere autenticación. Es la fuente principal para entender el paisaje competitivo. Consulta cada 5–30 segundos según la volatilidad del mercado.

Nota semántica: tradeType=BUY significa "quiero comprar" — devuelve anuncios SELL (vendedores). Usa SELL para obtener anuncios de compradores. Siempre pasa publisherType: "merchant" para evitar clasificar erróneamente a merchants como usuarios regulares.
market_data.py
import httpx
import asyncio

BAPI_URL = "https://p2p.binance.com/bapi/c2c/v2/friendly/c2c/adv/search"

async def fetch_order_book(
    asset: str,
    fiat: str,
    trade_type: str,  # "BUY" = quiero comprar → devuelve vendedores
    pages: int = 3,
) -> list[dict]:
    """Obtener hasta pages * 20 anuncios del order book público."""
    all_ads: list[dict] = []
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
        "Content-Type": "application/json",
    }
    async with httpx.AsyncClient() as client:
        for page in range(1, pages + 1):
            resp = await client.post(
                BAPI_URL,
                json={
                    "asset": asset,
                    "fiat": fiat,
                    "tradeType": trade_type,
                    "page": page,
                    "rows": 20,
                    "publisherType": "merchant",
                },
                headers=headers,
            )
            data = resp.json().get("data", [])
            if not data:
                break  # Vacío = sin más páginas o soft rate limit
            all_ads.extend(data)
            if page < pages:
                await asyncio.sleep(0.1)  # 100ms entre páginas

    return all_ads


async def poll_order_book(asset: str, fiat: str, interval_sec: float = 10.0):
    """Loop de polling continuo."""
    while True:
        try:
            sellers = await fetch_order_book(asset, fiat, "BUY")
            buyers = await fetch_order_book(asset, fiat, "SELL")
            yield {"sellers": sellers, "buyers": buyers}
        except Exception as e:
            print(f"Error order book: {e}")
        await asyncio.sleep(interval_sec)

4. Gestionar Anuncios (EP-4, EP-7, EP-8)

EP-4 lista tus anuncios activos. EP-7 actualiza el precio (y opcionalmente el estado). EP-8 actualiza el estado en lote (online/offline). La regla crítica en EP-7: envía únicamente advNo + price — incluir surplusAmount causa el error 187049.

ad_manager.py
import httpx
from auth import signed_post

async def list_my_ads(
    client: httpx.AsyncClient,
    api_key: str,
    api_secret: str,
) -> list[dict]:
    """EP-4: Listar todos los anuncios activos con paginación."""
    result = await signed_post(client, api_key, api_secret,
        "/sapi/v1/c2c/ads/listWithPagination",
        {"page": 1, "rows": 100},
    )
    return result.get("data", [])


async def update_ad_price(
    client: httpx.AsyncClient,
    api_key: str,
    api_secret: str,
    adv_no: str,
    price: str,
) -> bool:
    """EP-7: Actualización solo de precio. NUNCA incluir surplusAmount."""
    result = await signed_post(client, api_key, api_secret,
        "/sapi/v1/c2c/ads/update",
        # CRÍTICO: solo advNo + price
        {"advNo": adv_no, "price": price},
    )
    return result.get("success", False)


async def set_ads_online(
    client: httpx.AsyncClient,
    api_key: str,
    api_secret: str,
    adv_nos: list[str],
) -> bool:
    """EP-8: Poner anuncios online en lote (advStatus=1)."""
    result = await signed_post(client, api_key, api_secret,
        "/sapi/v1/c2c/ads/updateStatus",
        {"advNos": adv_nos, "advStatus": 1},
    )
    if result.get("code") == "-1102":
        # Fallback: algunos entornos necesitan CSV string
        result = await signed_post(client, api_key, api_secret,
            "/sapi/v1/c2c/ads/updateStatus",
            {"advNos": ",".join(adv_nos), "advStatus": 1},
        )
    return result.get("success", False)

5. Procesar Órdenes (EP-13, EP-15)

EP-15 lista tus órdenes abiertas. EP-13 obtiene el detalle de una orden específica. EP-13 usa auth adaptativa: primero intenta solo con API key (sin HMAC), luego cae al HMAC completo si es rechazada.

order_handler.py
import httpx
from auth import signed_post, BASE_URL

async def list_open_orders(
    client: httpx.AsyncClient,
    api_key: str,
    api_secret: str,
) -> list[dict]:
    """EP-15: Listar todas las órdenes con paginación."""
    result = await signed_post(client, api_key, api_secret,
        "/sapi/v1/c2c/orderMatch/listOrders",
        {"page": 1, "rows": 20},
    )
    return result.get("data", [])


async def get_order_detail(
    client: httpx.AsyncClient,
    api_key: str,
    api_secret: str,
    order_no: str,
) -> dict:
    """EP-13: Auth adaptativa — sin firma primero, HMAC como fallback."""
    url = f"{BASE_URL}/sapi/v1/c2c/orderMatch/getUserOrderDetail"

    # Intento 1: solo API key (sin HMAC)
    resp = await client.post(
        url,
        json={"adOrderNo": order_no},
        headers={"X-MBX-APIKEY": api_key, "clientType": "web"},
    )
    data = resp.json()

    if resp.status_code in (401, 403) or data.get("code") in ("-1021", "-1022"):
        # Intento 2: HMAC completo
        data = await signed_post(client, api_key, api_secret,
            "/sapi/v1/c2c/orderMatch/getUserOrderDetail",
            {"adOrderNo": order_no},
        )

    return data.get("data", {})

6. Integración de Chat (EP-33, EP-34)

EP-33 devuelve una credencial WebSocket de vida corta. Conéctate a la URL WebSocket y autentícate con el listenKey. EP-34 recupera mensajes existentes con paginación.

chat.py
import json
import websockets
import httpx
from auth import signed_post

async def get_chat_credential(
    client: httpx.AsyncClient,
    api_key: str,
    api_secret: str,
) -> dict:
    """EP-33: Obtener credencial WebSocket para chat en tiempo real."""
    import hmac, hashlib, time
    from auth import sign, BASE_URL
    timestamp = int(time.time() * 1000)
    query = f"recvWindow=60000&timestamp={timestamp}"
    signature = sign(api_secret, query)
    resp = await client.get(
        f"{BASE_URL}/sapi/v1/c2c/chat/retrieveChatCredential?{query}&signature={signature}",
        headers={"X-MBX-APIKEY": api_key, "clientType": "web"},
    )
    return resp.json().get("data", {})


async def stream_chat(api_key: str, api_secret: str):
    """Conectar al WebSocket de chat y procesar mensajes."""
    async with httpx.AsyncClient() as client:
        cred = await get_chat_credential(client, api_key, api_secret)

    wss_url = cred["wssUrl"]
    listen_key = cred["listenKey"]

    async with websockets.connect(wss_url) as ws:
        # Autenticar con listenKey
        await ws.send(json.dumps({"listenKey": listen_key}))

        async for message in ws:
            event = json.loads(message)
            order_no = event.get("orderNo")
            content = event.get("content")
            print(f"Nuevo mensaje para orden {order_no}: {content}")
            # Procesar mensaje, enviar auto-respuesta, marcar como leído...


async def get_chat_messages(
    client: httpx.AsyncClient,
    api_key: str,
    api_secret: str,
    order_no: str,
) -> list[dict]:
    """EP-34: Recuperar mensajes de chat paginados para una orden."""
    import time
    from auth import sign, BASE_URL
    timestamp = int(time.time() * 1000)
    query = f"page=1&rows=20&orderNo={order_no}&recvWindow=60000&timestamp={timestamp}"
    signature = sign(api_secret, query)
    resp = await client.get(
        f"{BASE_URL}/sapi/v1/c2c/chat/retrieveChatMessagesWithPagination?{query}&signature={signature}",
        headers={"X-MBX-APIKEY": api_key, "clientType": "web"},
    )
    return resp.json().get("data", [])

7. Patrones de Manejo de Errores

Los errores de Binance C2C caen en categorías predecibles con patrones de recuperación definidos. Consulta la referencia completa de Códigos de Error. Estos son los más frecuentes en un bot:

-1021 / -1022Timestamp

Llamar /api/v3/time, calcular offset, reconstruir query con timestamp fresco, reintentar

187049surplusAmount

Eliminar surplusAmount del body de EP-7. Enviar solo advNo + price

187055Rango de precio

Búsqueda binaria dentro del rango permitido — encontrar el primer precio válido que no se superponga

429 / -9000Rate limit

Backoff exponencial. Mínimo 100ms; duplicar en cada golpe consecutivo

83229 / 83230Anuncio offline

Marcar anuncio como pausado. Inspeccionar estado antes del próximo ciclo

error_handler.py
import asyncio

RETRY_WITH_SYNC = {"-1021", "-1022"}
RETRIABLE = {"-1000", "429", "-9000"}
VOLUME_SYNC = {"187049", "187040", "187031"}
AD_OFFLINE = {"83229", "83230"}

async def handle_binance_error(code: str, context: dict) -> str:
    """Devuelve acción a tomar."""
    if code in RETRY_WITH_SYNC:
        return "sync_and_retry"
    if code in RETRIABLE:
        await asyncio.sleep(context.get("backoff", 1.0))
        return "retry"
    if code in VOLUME_SYNC:
        await asyncio.sleep(0.3)
        return "refetch_and_retry"
    if code in AD_OFFLINE:
        return "pause_ad"
    if code == "187055":
        return "find_valid_price"
    return "abort"

8. Buenas Prácticas de Rate Limiting

Binance C2C aplica tres capas de rate limiting: peso por endpoint, RPS global, y un soft limit en el BAPI público. Consulta la guía completa de Rate Limiting. Prácticas clave:

Techo de 5 RPS global

Nunca superar 5 requests por segundo entre todos los endpoints de Binance C2C combinados. Usar un token bucket compartido.

100ms entre páginas de EP-0

Al paginar el order book público, añadir 100ms de delay entre páginas para evitar disparar el soft limit de respuestas vacías.

Rotar User-Agent en EP-0

El endpoint BAPI detecta headers de librerías por defecto antes. Rotar strings de User-Agent tipo browser para extender el umbral del soft limit.

Circuit breaker ante fallos consecutivos

Tras 15 errores consecutivos de Binance, abrir un circuit breaker por 45 segundos. No realizar nuevas llamadas en estado abierto.

O salta meses de desarrollo y usa AutoP2P

Todo lo descrito en esta guía — polling de market data, actualizaciones solo de precio, auth adaptativa, rate limiting, circuit breakers, recuperación de errores, integración de chat — ya está construido en AutoP2P y corriendo en producción en múltiples nodos VPS. El repricing ocurre en menos de 1 segundo.

Ver Precios