Automatización de Precios
Cómo construir un sistema de repricing automático para anuncios P2P en Binance C2C. Esta guía cubre el loop completo: obtener el order book, analizar la competencia, calcular el precio objetivo, actualizar el anuncio y gestionar cada caso de error.
El Loop de Repricing
┌────────────────────────────────────────────────────────────────┐ │ Loop de Repricing (por anuncio) │ └────────────────────────────────────────────────────────────────┘ Cada N segundos: ───────────────────────────────────────────── 1. Obtener order book EP-0 BAPI search 2. Filtrar competidores en memoria 3. Ordenar por precio en memoria 4. Calcular precio obj. lógica de estrategia 5. Actualizar anuncio EP-7 /ads/update ───────────────────────────────────────────── Manejo de errores en EP-7: 187049 ──▶ Eliminar surplusAmount, reintentar 187055 ──▶ Búsqueda binaria de precio válido, reintentar -1021 ──▶ Sincronizar tiempo (UTIL-1), reconstruir, reintentar 83229 ──▶ Marcar anuncio pausado, saltar
Paso 1: Obtener el Order Book (EP-0)
El order book es la fuente de datos competitivos en bruto. Para un anuncio SELL (vendes USDT), consulta tradeType=BUY — esos son los vendedores que ve el comprador, que es la lista en la que compite tu anuncio. Siempre pasa publisherType: "merchant".
import httpx
async def fetch_competitors(asset: str, fiat: str) -> list[dict]:
"""Obtener páginas 1-2 de vendedores (= competidores de tu anuncio SELL)."""
url = "https://p2p.binance.com/bapi/c2c/v2/friendly/c2c/adv/search"
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Content-Type": "application/json",
}
ads: list[dict] = []
async with httpx.AsyncClient() as client:
for page in range(1, 3): # 40 anuncios en total
resp = await client.post(url, headers=headers, json={
"asset": asset,
"fiat": fiat,
"tradeType": "BUY", # devuelve anuncios SELL = competidores
"page": page,
"rows": 20,
"publisherType": "merchant",
})
data = resp.json().get("data", [])
if not data:
break
ads.extend(data)
return adsPaso 2: Analizar la Competencia
Filtra y ordena los datos brutos del order book para extraer señales competitivas útiles. Filtros clave: excluir tus propios anuncios, excluir anuncios con métodos de pago incompatibles, excluir anuncios con cantidades muy pequeñas o muy grandes.
def analyze_order_book(
ads: list[dict],
my_adv_nos: set[str],
payment_filter: list[str] | None = None,
min_surplus: float = 100.0,
) -> list[dict]:
"""Filtrar y ordenar competidores del order book en bruto."""
result = []
for ad in ads:
adv_no = ad.get("advNo") or ad.get("adv", {}).get("advNo")
# Excluir propios anuncios
if adv_no in my_adv_nos:
continue
price = float(ad.get("adv", {}).get("price") or ad.get("price", 0))
surplus = float(
ad.get("adv", {}).get("tradableQuantity")
or ad.get("tradableQuantity", 0)
)
# Excluir anuncios delgados
if surplus < min_surplus:
continue
# Filtro opcional: método de pago
if payment_filter:
methods = [
m.get("identifier")
for m in (ad.get("adv", {}).get("tradeMethods") or [])
]
if not any(m in payment_filter for m in methods):
continue
result.append({
"advNo": adv_no,
"price": price,
"surplus": surplus,
})
# Ordenar ascendente por precio (vendedor más barato primero)
return sorted(result, key=lambda x: x["price"])
def best_ask(competitors: list[dict]) -> float | None:
"""Devuelve el precio del competidor más bajo."""
return competitors[0]["price"] if competitors else NonePaso 3: Calcular el Precio Objetivo
El precio objetivo depende de tu estrategia. Enfoques comunes: superar al mejor ask por un spread fijo, posicionarte en el rank N del order book, o flotar sobre un precio piso de referencia. Siempre cuantiza con la precisión decimal correcta para la moneda fiat.
from decimal import Decimal, ROUND_DOWN
# Cuantización por moneda (ejemplos)
CURRENCY_PRECISION: dict[str, str] = {
"USD": "0.01",
"ARS": "0.01",
"COP": "1",
"CLP": "1",
"UYU": "0.01",
"CRC": "1",
}
def quantize(price: float, fiat: str) -> str:
"""Redondear a los decimales correctos para la moneda fiat."""
precision = CURRENCY_PRECISION.get(fiat, "0.01")
return str(Decimal(str(price)).quantize(Decimal(precision), rounding=ROUND_DOWN))
def calculate_target_price(
competitors: list[dict],
my_current_price: float,
strategy: str = "undercut",
spread: float = 0.01,
target_rank: int = 1,
fiat: str = "USD",
) -> str | None:
"""Calcular nuevo precio objetivo.
strategy="undercut": igualar mejor ask menos spread
strategy="position": precio para alcanzar target_rank en el book
"""
if not competitors:
return None # Sin datos — no actualizar
if strategy == "undercut":
best = competitors[0]["price"]
target = best - spread
return quantize(target, fiat)
if strategy == "position":
# Precio justo por debajo del competidor en la posición target_rank
if len(competitors) >= target_rank:
anchor = competitors[target_rank - 1]["price"]
target = anchor - spread
return quantize(target, fiat)
# No hay suficientes competidores — superar al mejor
return quantize(competitors[0]["price"] - spread, fiat)
return NonePaso 4: Actualizar el Anuncio (Patrón Solo-Precio EP-7)
advNo + price. Añadir cualquier otro campo (especialmente surplusAmount) causa el error 187049 porque Binance valida el surplus contra un valor en caché que puede diferir de tu saldo actual.import hmac
import hashlib
import time
import httpx
async def update_price(
client: httpx.AsyncClient,
api_key: str,
api_secret: str,
adv_no: str,
price: str, # string ya cuantizado
) -> dict:
"""EP-7: Actualización solo de precio."""
timestamp = int(time.time() * 1000)
query = f"recvWindow=60000×tamp={timestamp}"
signature = hmac.new(
api_secret.encode(), query.encode(), hashlib.sha256
).hexdigest()
resp = await client.post(
f"https://api.binance.com/sapi/v1/c2c/ads/update?{query}&signature={signature}",
# CRÍTICO: SOLO advNo + price. Nada más.
json={"advNo": adv_no, "price": price},
headers={"X-MBX-APIKEY": api_key, "clientType": "web"},
)
return resp.json()Paso 5: Manejar Errores
Tres errores son específicos del path de repricing. Cada uno tiene una estrategia de recuperación determinística:
import asyncio
import httpx
async def reprice_with_recovery(
client: httpx.AsyncClient,
api_key: str,
api_secret: str,
adv_no: str,
price: str,
fiat: str,
) -> bool:
"""Repricing completo con recuperación de errores."""
for attempt in range(5):
result = await update_price(client, api_key, api_secret, adv_no, price)
code = str(result.get("code", ""))
if result.get("success"):
return True
if code in ("-1021", "-1022"):
# Drift de reloj — sincronizar tiempo y reconstruir timestamp
server_time = await get_server_time(client)
# Usar server_time para generar timestamp fresco en el siguiente loop
await asyncio.sleep(0.1)
continue
if code == "187049":
# surplusAmount en el body — nunca debería pasar
# con el patrón solo-precio, pero manejar defensivamente
raise ValueError(
"187049: Eliminar surplusAmount del body de EP-7"
)
if code == "187055":
# Precio fuera del rango permitido
price = await find_valid_price(
client, api_key, api_secret, adv_no, float(price), fiat
)
continue
if code in ("83229", "83230"):
# Anuncio desactivado por Binance
print(f"Anuncio {adv_no} está offline — saltando reprice")
return False
if code in ("-1000", "429", "-9000"):
# Backoff y reintentar
backoff = min(2 ** attempt * 0.5, 30.0)
await asyncio.sleep(backoff)
continue
# Error desconocido
print(f"Error de reprice {code}: {result.get('message')}")
return False
return False
async def find_valid_price(
client: httpx.AsyncClient,
api_key: str,
api_secret: str,
adv_no: str,
current_price: float,
fiat: str,
) -> str:
"""Búsqueda binaria de precio válido cuando se devuelve 187055.
Incrementar precio en pasos hasta encontrar uno sin superposición.
En producción, obtener primero el rango dinámico de Binance.
"""
step = {"USD": 0.01, "ARS": 1.0, "COP": 100.0, "CLP": 10.0}.get(fiat, 0.01)
for i in range(50): # máximo 50 pasos
candidate = current_price + (step * i)
result = await update_price(
client, api_key, api_secret, adv_no, quantize(candidate, fiat)
)
if result.get("success"):
return quantize(candidate, fiat)
if str(result.get("code")) != "187055":
break # Error diferente — dejar de buscar
raise RuntimeError(f"No se encontró precio válido cerca de {current_price} {fiat}")
async def get_server_time(client: httpx.AsyncClient) -> int:
resp = await client.get("https://api.binance.com/api/v3/time")
return resp.json()["serverTime"]AutoP2P hace todo esto en menos de 1 segundo
El loop de repricing descrito en esta guía — obtención del order book, análisis de competencia, cuantización de precio, actualización solo-precio EP-7 y recuperación completa de errores — se ejecuta en AutoP2P en menos de 1 segundo por ciclo de anuncio. Gestiona múltiples anuncios en paralelo en múltiples exchanges y monedas, con rate limiter y circuit breaker incorporados.
<1s
Latencia de reprice
Automática
Recuperación de errores
3
Capas de rate limit