Tutorial de bots para Polymarket · Capítulo 24 de 32
Polymarket Perps tiene una API pública completa con SDK oficiales para TypeScript y Python. En este capítulo construimos un bot sobre la interfaz real: autenticación con una clave de trading proxy, datos de mercado por REST y WebSocket, órdenes con take-profit y stop-loss, y los controles de riesgo que evitan que un bot con apalancamiento vacíe la cuenta.
Qué cubre este capítulo
- Cifras clave para quienes construyen bots
- Cómo está construida la API de Perps
- Paso 1: instala el SDK y abre una sesión
- Paso 2: lee datos de mercado por REST y WebSocket
- Paso 3: coloca órdenes con take-profit y stop-loss
- Paso 4: un esqueleto de bot con controles de riesgo en el código
- Errores, el interruptor de hombre muerto (dead man's switch), reglas para bots y alojamiento
Cada método del SDK y cada endpoint de este capítulo provienen de la documentación de Perps de Polymarket (docs.polymarket.com/perps, consultada el 6 de noviembre de 2026). Nuestra propia lógica está marcada en los comentarios, y la función de estrategia es un marcador de posición señalado como tal.
Cifras clave para quienes construyen bots (a noviembre de 2026)
| Elemento | Valor |
|---|---|
| URL base de REST | https://api.perpetuals.polymarket.com |
| WebSocket | wss://ws.perpetuals.polymarket.com/v1/ws |
| SDK de TypeScript | @polymarket/client (con viem para firmar) |
| SDK de Python | polymarket-client (asíncrono) |
| Datos públicos | /v1/info/*, sin autenticación |
| Lecturas privadas | /v1/account/* con los encabezados polymarket-proxy y polymarket-secret |
| Escrituras firmadas | /v1/trade/*, firma EIP-712 de la clave proxy, válida durante 5 minutos |
| Credenciales de sesión | Caducan tras una semana por defecto |
| Orden mínima | $10 de valor nocional; depósito mínimo de 10 pUSD |
| Orden a mercado más grande | $1M en los mercados de 50x y 20x, $500K en la mayoría de los demás, $50K en las monedas de 3x y 5x |
| Formato de precio y tamaño | Cadenas decimales, sin superar los decimales del mercado ni 5 cifras significativas |
| Apalancamiento máximo | 50x (BTC, ETH, USA500, US100); en los demás, 20x, 10x, 5x o 3x según el mercado, menos para posiciones grandes |
| Comisiones (nivel base) | Taker 0,040%, maker 0,0125%; ANT a mitad de precio |
| Financiación (funding) | Cada hora, con un tope de +/-4% por hora |
| Activadores de TP/SL | Precio de marca, no la última operación |
| Keepalive del WebSocket | Ping cada 25 segundos aprox.; considera la conexión obsoleta tras unos 65 segundos de silencio |
| Actualidad del ticker | Hasta 10 segundos de antigüedad |
Cómo está construida la API de Perps
Perps es un exchange híbrido: el emparejamiento de órdenes ocurre offchain, y la custodia y la compensación están en Polygon. Solo los depósitos y retiros son onchain, así que no hay gas por orden. La API tiene tres capas:
- Datos públicos (
/v1/info/): instrumentos, tickers, libro de órdenes, velas, operaciones, financiación, comisiones. No hace falta clave y no hay bloqueo geográfico. - Lecturas privadas (
/v1/account/): cartera, saldos, órdenes, ejecuciones, configuración. - Escrituras firmadas (
/v1/trade/): órdenes, cancelaciones, apalancamiento, margen aislado, cancelación automática. Cada escritura se codifica en MessagePack, se le aplica el hash keccak256 y la clave proxy la firma como datos tipados EIP-712.
Los SDK se encargan de la firma por ti. También hay una especificación OpenAPI (/api-spec/perps-openapi.json en el sitio de la documentación) y una especificación AsyncAPI para el WebSocket.
BTC-PERP. La API en vivo devuelve BTC-USD, SP500-USD (USA500), NAS100-USD (US100), ANT-USD, etc. Obtén la lista de instrumentos, busca el symbol en vivo y guarda el id numérico. No copies los ID de los ejemplos: en vivo, BTC es 6 y SP500 es 1.Paso 1: instala el SDK y abre una sesión
Instala con pip install polymarket-client o npm install @polymarket/client@latest viem. El bot necesita la dirección de tu billetera de Polymarket y la clave privada del firmante dueño de la cuenta de Perps (la cuenta está vinculada a la EOA firmante, no a la Safe). Al abrir una sesión se firma una sola vez para autorizar una nueva clave de trading proxy, que a partir de entonces firma cada orden, así tu clave de fondos nunca opera. Guarda session.credentials cifrado para reanudar más adelante.
import asyncio
import os
from datetime import timedelta
from polymarket import AsyncSecureClient
async def main():
client = await AsyncSecureClient.create(
private_key=os.environ["PRIVATE_KEY"],
wallet=os.environ["POLYMARKET_WALLET_ADDRESS"],
)
# One signature creates a proxy trading key. Orders are signed by the proxy,
# not by your wallet key. Credentials expire after one week by default.
session = await client.open_perps_session(
expires_in=timedelta(days=1),
label="perps-bot",
)
stored = session.credentials.model_dump(mode="json") # keep it encrypted
instruments = await client.fetch_perps_instruments()
btc = next(i for i in instruments if i.symbol == "BTC-USD")
print(btc.id, btc.max_leverage, btc.min_notional, btc.price_decimals)
asyncio.run(main())
import { createSecureClient } from "@polymarket/client";
import { privateKey } from "@polymarket/client/viem";
const client = await createSecureClient({
wallet: process.env.POLYMARKET_WALLET_ADDRESS!,
signer: privateKey(process.env.PRIVATE_KEY!),
});
// One signature creates a proxy trading key. Orders are signed by the proxy,
// not by your wallet key. Credentials expire after one week by default.
const session = await client.openPerpsSession({
expiresIn: 24 * 60 * 60 * 1000,
label: "perps-bot",
});
const stored = session.credentials; // keep it encrypted
const instruments = await client.fetchPerpsInstruments();
const btc = instruments.find((i) => i.symbol === "BTC-USD");
if (!btc) throw new Error("BTC-USD not listed");
console.log(btc.id, btc.maxLeverage, btc.minNotional, btc.priceDecimals);
Depositar desde el código (deposit_to_perps / depositToPerps) también requiere una clave de API de Relayer, que se obtiene en polymarket.com, Settings, API Keys. Lo más simple y seguro: fondea la cuenta en la aplicación web y mantén el bot alejado del movimiento de dinero. Desde el 5 de noviembre de 2026, las cuentas con libreta de direcciones de retiro solo pueden retirar a direcciones guardadas. Para paneles de control, usa una clave de solo lectura (un proxy con la etiqueta read-only:, que rechaza escrituras firmadas), nunca la clave de trading. El almacenamiento de claves se explica en nuestro capítulo de billeteras y autenticación.
Paso 2: lee datos de mercado por REST y WebSocket
El ticker tiene tres precios: índice (valor justo a partir de fuentes como Pyth, Chainlink Data Streams e Hyperliquid), marca (usado para PnL, margen, liquidación y TP/SL) y último (solo ejecución), además de la tasa de financiación y la hora de la próxima financiación. Basa tus señales en el precio de marca o en el índice.
# Inside your async main(), after Step 1 (btc is the BTC-USD instrument)
from polymarket import AsyncPublicClient
from polymarket.streams import PerpsBboSpec
client = AsyncPublicClient()
ticker = await client.fetch_perps_ticker(instrument_id=btc.id)
print(ticker.mark_price, ticker.index_price, ticker.funding_rate)
stream = await client.subscribe(PerpsBboSpec(instrument_id=btc.id))
async for event in stream:
print(event.payload.bid_price, event.payload.ask_price)
// After Step 1 (btc is the BTC-USD instrument)
import { createPublicClient } from "@polymarket/client";
const client = createPublicClient();
const ticker = await client.fetchPerpsTicker({ instrumentId: btc.id });
console.log(ticker.markPrice, ticker.indexPrice, ticker.fundingRate);
const stream = await client.subscribe([{ topic: "perps.bbo", instrumentId: btc.id }]);
for await (const event of stream) {
console.log(event.payload.bidPrice, event.payload.askPrice);
}
Sin el SDK, suscríbete en el WebSocket directo con {"id": 1, "req": "sub", "chs": ["bbo::6"]}. Los canales incluyen bbo::<id>, book::<id>, trades::<id>, tickers::<id> y klines::<id>::1m. Cada actualización tiene un número de secuencia sq: tras un salto o una reconexión, vuelve a leer el estado por REST. Para backtests, list_perps_candles / listPerpsCandles pagina el historial (el endpoint REST devuelve hasta 1.000 velas por llamada).
Paso 3: coloca órdenes con take-profit y stop-loss
Una orden tiene un side (BUY o SELL), una quantity en unidades del activo base, un price opcional en pUSD y una vigencia (gtc, ioc o fok). Post-only requiere GTC; reduce-only solo puede reducir una posición. No hay un indicador de largo o corto: sin posición, BUY abre un largo y SELL abre un corto.
Una orden bracket adjunta un take-profit y un stop-loss a la entrada. Permanecen inactivos hasta que la entrada se ejecuta por completo, y se cancelan si la entrada se cancela o solo se ejecuta en parte. Los precios de abajo son hipotéticos.
# Inside your async main(), after Step 1 (btc is the BTC-USD instrument)
from polymarket import PerpsTpSlTrigger
result = await session.place_order(
instrument_id=btc.id,
side="BUY",
price="100000",
quantity="0.01",
time_in_force="gtc",
take_profit=PerpsTpSlTrigger(trigger_price="104000"),
stop_loss=PerpsTpSlTrigger(trigger_price="98000"),
)
print(result.order.id, result.order.status)
// After Step 1 (btc is the BTC-USD instrument)
import { OrderSide, PerpsTimeInForce } from "@polymarket/client";
const result = await session.placeOrder({
instrumentId: btc.id,
side: OrderSide.BUY,
price: "100000",
quantity: "0.01",
timeInForce: PerpsTimeInForce.GTC,
takeProfit: { triggerPrice: "104000" },
stopLoss: { triggerPrice: "98000" },
});
console.log(result.order.id, result.tpSl.stopLoss?.orderId);
Para una posición abierta, place_position_tp_sl / placePositionTpSl añade activadores que cierran toda la posición. Reglas:
- Los activadores vigilan el precio de marca y envían una orden reduce-only. El TP/SL de posición sale a mercado, así que espera deslizamiento (slippage) en libros poco profundos.
- Un take-profit y un stop-loss de posición por mercado. Mover un stop implica cancelar y luego crear, lo cual no es atómico.
- Se rechaza un activador que el precio de marca ya haya cruzado, y también los precios límite demasiado alejados del precio de marca.
Para probar el flujo de órdenes a mano antes de automatizarlo, abre el mercado de BTC en Polymarket Perps y coloca la orden más pequeña permitida. Ver las ejecuciones, las comisiones y el panel de posición hace que las respuestas de la API sean más fáciles de leer.
Paso 4: un esqueleto de bot con controles de riesgo en el código
El esqueleto en Python de abajo usa solo llamadas documentadas del SDK; la lógica de riesgo es nuestra. Tres capas de protección:
- Tope de apalancamiento. El apalancamiento se fija en 3x o en el máximo del mercado, el que sea menor, con margen aislado, y se rechazan los tamaños que caen en un nivel de riesgo con un tope más bajo.
- Interruptores de emergencia (kill switches). Un archivo HALT, un límite de pérdida diaria del 5% y una salida en caso de liquidación. El interruptor cierra las posiciones con órdenes IOC reduce-only, cancela las órdenes restantes y luego comprueba que ambas hayan desaparecido.
- Un stop en cada posición. Las entradas fill-or-kill evitan ejecuciones parciales sin protección. Tras cada ejecución se colocan un stop y un take-profit de posición; si se rechazan, la posición se cierra de inmediato.
import asyncio
import logging
import os
from datetime import timedelta
from decimal import Decimal, ROUND_DOWN
from polymarket import AsyncSecureClient, PerpsPositionTpSlTrigger
SYMBOL = "BTC-USD" # live API symbol; docs examples say "BTC-PERP"
LEVERAGE_CAP = 3 # our cap, far below the 50x BTC allows
RISK_PER_TRADE = Decimal("20") # pUSD we accept losing if the stop fills
STOP_PCT = Decimal("0.02") # stop 2% from entry
TAKE_PCT = Decimal("0.04") # take profit 4% from entry
MAX_NOTIONAL = Decimal("2000") # hard cap per position, in pUSD
MAX_SLIPPAGE = Decimal("0.002") # worst entry price: ask + 0.2%
MAX_FUNDING = Decimal("0.0005") # skip longs if funding > 0.05% per hour
DAILY_LOSS_LIMIT = Decimal("0.05") # kill switch at -5% of start equity
HALT_FILE = "/opt/perps-bot/HALT" # touch this file to stop the bot
def fit(value: Decimal, decimals: int, sig: int = 5) -> str:
"""Round down to the market's decimals and to 5 significant figures."""
v = value.quantize(Decimal(1).scaleb(-decimals), rounding=ROUND_DOWN)
if v != 0:
step = Decimal(1).scaleb(v.adjusted() - sig + 1)
if step > Decimal(1).scaleb(-decimals):
v = v.quantize(step, rounding=ROUND_DOWN)
return format(v, "f")
def tier_cap(inst, notional: Decimal) -> int:
"""Max leverage allowed for this notional by the market's risk tiers."""
cap = inst.max_leverage
for tier in inst.risk_tiers:
if notional >= Decimal(tier.lower_bound):
cap = tier.max_leverage
return cap
async def close_all(session):
"""Close every position with reduce-only IOC orders, then cancel the rest."""
portfolio = await session.fetch_portfolio()
for pos in portfolio.positions:
if pos.size != 0:
await session.place_order(
instrument_id=pos.instrument_id,
side="SELL" if pos.size > 0 else "BUY",
quantity=abs(pos.size),
time_in_force="ioc",
reduce_only=True,
)
await session.cancel_all_orders()
async def kill_switch(session, reason: str):
logging.error("KILL SWITCH: %s", reason)
for attempt in range(3):
await close_all(session)
portfolio = await session.fetch_portfolio()
open_orders = await session.fetch_open_orders()
if not open_orders and all(p.size == 0 for p in portfolio.positions):
return
await asyncio.sleep(2)
logging.critical("Positions still open after kill switch: act manually")
async def open_long(client, session, inst):
ticker = await client.fetch_perps_ticker(instrument_id=inst.id)
if Decimal(ticker.funding_rate) > MAX_FUNDING:
return # longs would pay too much
book = await client.fetch_perps_book(instrument_id=inst.id, depth=10)
if not book.asks:
return
ask = Decimal(book.asks[0].price)
stop = ask * (1 - STOP_PCT)
qty = RISK_PER_TRADE / (ask - stop) # size from the stop, not leverage
qty = min(qty, MAX_NOTIONAL / ask)
notional = qty * ask
if notional < Decimal(inst.min_notional):
return
if tier_cap(inst, notional) < LEVERAGE_CAP:
return # size would break our cap
entry = await session.place_order(
instrument_id=inst.id,
side="BUY",
price=fit(ask * (1 + MAX_SLIPPAGE), inst.price_decimals),
quantity=fit(qty, inst.quantity_decimals),
time_in_force="fok", # all or nothing
)
if entry.order.status != "filled":
return
try: # stop on the whole position
await session.place_position_tp_sl(
instrument_id=inst.id,
take_profit=PerpsPositionTpSlTrigger(
trigger_price=fit(ask * (1 + TAKE_PCT), inst.price_decimals)),
stop_loss=PerpsPositionTpSlTrigger(
trigger_price=fit(stop, inst.price_decimals)),
)
except Exception:
logging.exception("TP/SL rejected: closing the unprotected position")
await close_all(session)
async def my_signal(client, inst) -> bool:
"""PLACEHOLDER: your strategy goes here. Not part of the SDK."""
return False
async def main():
logging.basicConfig(level=logging.INFO)
client = await AsyncSecureClient.create(
private_key=os.environ["PRIVATE_KEY"],
wallet=os.environ["POLYMARKET_WALLET_ADDRESS"],
)
session = await client.open_perps_session(
expires_in=timedelta(days=1), label="perps-bot")
inst = next(i for i in await client.fetch_perps_instruments()
if i.symbol == SYMBOL)
# Control 1: leverage cap and isolated margin, set before any order
await session.update_leverage(
instrument_id=inst.id,
leverage=min(LEVERAGE_CAP, inst.max_leverage),
cross_margin=False,
)
# Field names below follow the documented portfolio JSON
# (margin.total_account_value, in_liquidation). Check them in your SDK.
start_equity = Decimal((await session.fetch_portfolio()).margin.total_account_value)
while True:
portfolio = await session.fetch_portfolio()
equity = Decimal(portfolio.margin.total_account_value)
# Control 2: kill switches
if os.path.exists(HALT_FILE):
await kill_switch(session, "HALT file found")
break
if equity < start_equity * (1 - DAILY_LOSS_LIMIT):
await kill_switch(session, f"daily loss limit hit: {equity}")
break
if portfolio.in_liquidation:
await kill_switch(session, "account in liquidation")
break
in_position = any(p.instrument_id == inst.id and p.size != 0
for p in portfolio.positions)
if not in_position and await my_signal(client, inst):
await open_long(client, session, inst) # Control 3 lives here
await asyncio.sleep(5)
asyncio.run(main())
El cálculo del tamaño. El tamaño se calcula a partir de la distancia al stop, no del apalancamiento. Con una oferta de venta hipotética de BTC de 100.000 pUSD y un stop del 2%, el stop queda en 98.000. El riesgo por unidad es de 2.000, así que la cantidad es 20 / 2.000 = 0,01 BTC, un valor nocional de 1.000 pUSD. A 3x eso requiere unos 333 pUSD de margen. La tasa de margen de mantenimiento de BTC es del 1% (0,5 / 50x), así que el precio de liquidación del largo es 100.000 x (1 - 1/3) / (1 - 0,01) = unos 67.340, una caída del 32,7%. El stop en -2% se activa mucho antes. Si el stop se ejecuta en su precio de activación, la pérdida es de 20 pUSD más comisiones: 0,40 al abrir y unos 0,39 al cerrar con la tasa taker del 0,040%, es decir, unos 20,79 pUSD antes del deslizamiento. El apalancamiento solo decide cuánto margen queda inmovilizado. No cambia la pérdida en el stop.
Antes de pasar a producción, añade un límite diario de operaciones, alertas sobre ejecuciones e interruptores de emergencia, y una conciliación al arrancar (fetch_portfolio, fetch_open_orders, fetch_orders, list_fills). Consulta nuestros capítulos de código de riesgo y de trading simulado, la guía de apalancamiento y liquidación de Perps y la guía de tasas de financiación de Perps.
Errores, reintentos y el interruptor de hombre muerto
503 service_unavailableno demuestra que la solicitud haya fallado. Reintenta las lecturas y los cambios de apalancamiento con backoff y respetaRetry-After. Para cambios de saldo firmados (margen, retiros), reenvía solo la solicitud firmada original exacta; una firma nueva es un segundo ajuste.signature expired: tiene más de 5 minutos. Mantén el reloj sincronizado.insufficient_margin/invalid_leverage: las comprobaciones usan tu posición más todas las órdenes en espera.message_rate_limited: se agotó el presupuesto del WebSocket. Las claves de solo lectura tienen 1.000 mensajes por minuto por IP (ráfaga de 50) y cuatro conexiones por cuenta por gateway; consulta la página Rate Limits de la documentación para el resto.- Errores de formato: demasiados decimales o más de 5 cifras significativas. De ahí
fit().
La cancelación automática cancela todas las órdenes abiertas si tu bot deja de rearmarla. El plazo debe estar al menos 5 segundos en el futuro, y las activaciones por día UTC son limitadas, así que desármala al apagar el bot de forma limpia.
// For quoting bots: cancel all resting orders if this process dies
await session.armAutoCancel({ cancelAt: Date.now() + 60_000 });
const rearm = setInterval(() => {
// A missed re-arm is fail-safe: the armed switch still fires.
session.armAutoCancel({ cancelAt: Date.now() + 60_000 }).catch(console.error);
}, 20_000);
// On a clean shutdown
clearInterval(rearm);
await session.disarmAutoCancel();
Competiciones, builder codes y las reglas para bots
La documentación incluye «bots de trading que reaccionan a señales del mercado» y market makers entre los usos de la API. Se aplican dos conjuntos de reglas.
- Competiciones. Están prohibidos las operaciones ficticias (wash trading), el self-trading, las cuentas múltiples, operar principalmente para generar volumen y los «bots, scripts o medios automatizados destinados a manipular el Programa». Un bot que opera de verdad está bien; uno que genera volumen artificial puede ser descalificado.
- Builder codes. Una aplicación que opera para otros usuarios puede añadir una comisión de hasta 10 puntos básicos con la aprobación de cada trader. Debe verificar la ubicación de cada usuario y bloquear las órdenes de jurisdicciones restringidas, no solo advertir.
Las recompensas de liquidez para makers y las competiciones se explican en la guía de Polymarket Perps.
Alojamiento del bot
Un bot con apalancamiento no debería correr en una laptop. Tiene que mantener vivo su WebSocket, reaccionar a las ejecuciones en segundos y sobrevivir a un corte de internet doméstico. Ejecútalo en un VPS bajo systemd con reinicio automático, guarda la clave en un archivo de entorno que solo pueda leer el usuario del bot y sincroniza el reloj. No se permite colocar órdenes desde ubicaciones restringidas, así que no lo alojes en Estados Unidos ni en Canadá, y nunca uses un VPS para eludir un bloqueo que se aplique a ti. Nosotros ejecutamos nuestros propios bots en TradingVPS (enlace de afiliado: ganamos una pequeña comisión y el precio que pagas es el mismo). Nuestro capítulo de alojamiento en VPS lo compara con otros proveedores en latencia, precio y regiones.
Después del trading simulado, fondea la cuenta con una cantidad pequeña desde la página de Polymarket Perps y opera con el tamaño mínimo durante una semana antes de escalar.
Puntos clave
- Usa los SDK oficiales,
@polymarket/clientopolymarket-client. Tu billetera firma una vez; una clave proxy firma las órdenes. - Identifica los mercados por el símbolo en vivo (
BTC-USD, noBTC-PERP) y guarda el ID numérico. - Calcula el tamaño a partir de la distancia al stop, limita el apalancamiento en el código, usa margen aislado y pon un stop a cada posición justo después de la ejecución.
- El TP/SL se activa con el precio de marca y sale a mercado. La cancelación automática puede eliminar los stops.
- Los bots están permitidos; manipular las competiciones, no. Nunca operes desde un país restringido.





