Учебник по ботам Полимаркет · Глава 24 из 32
У Полимаркет Perps есть полноценный публичный API с официальными SDK для TypeScript и Python. В этой главе мы строим бота на реальном интерфейсе: аутентификация через прокси-ключ для торговли, рыночные данные по REST и WebSocket, ордера с тейк-профитом и стоп-лоссом, а также контроль рисков, который не даёт боту с кредитным плечом опустошить счёт.
О чём эта глава
- Ключевые цифры для разработчиков ботов
- Как устроен API Perps
- Шаг 1: установка SDK и открытие сессии
- Шаг 2: чтение рыночных данных по REST и WebSocket
- Шаг 3: ордера с тейк-профитом и стоп-лоссом
- Шаг 4: каркас бота с контролем рисков в коде
- Ошибки, выключатель мертвеца, правила для ботов и хостинг
Каждый метод SDK и каждый эндпоинт в этой главе взяты из документации Полимаркет по Perps (docs.polymarket.com/perps, прочитано 6 ноября 2026 г.). Наша собственная логика отмечена в комментариях, а функция стратегии является помеченной заглушкой.
Ключевые цифры для разработчиков ботов (на ноябрь 2026 г.)
| Параметр | Значение |
|---|---|
| Базовый URL REST | https://api.perpetuals.polymarket.com |
| WebSocket | wss://ws.perpetuals.polymarket.com/v1/ws |
| SDK для TypeScript | @polymarket/client (с viem для подписи) |
| SDK для Python | polymarket-client (асинхронный) |
| Публичные данные | /v1/info/*, без аутентификации |
| Приватное чтение | /v1/account/* с заголовками polymarket-proxy и polymarket-secret |
| Подписанная запись | /v1/trade/*, подпись EIP-712 прокси-ключом, действует 5 минут |
| Учётные данные сессии | По умолчанию истекают через неделю |
| Минимальный ордер | $10 номинала; минимальный депозит 10 pUSD |
| Максимальный рыночный ордер | $1 млн на рынках с плечом 50x и 20x, $500 тыс. на большинстве остальных, $50 тыс. на монетах с плечом 3x и 5x |
| Формат цены и размера | Десятичные строки, не больше знаков после запятой, чем допускает рынок, и не более 5 значащих цифр |
| Максимальное плечо | 50x (BTC, ETH, USA500, US100), на остальных рынках 20x, 10x, 5x или 3x, для крупных позиций ниже |
| Комиссии (базовый уровень) | Тейкер 0.040%, мейкер 0.0125%; для ANT в два раза дешевле |
| Финансирование | Ежечасно, не более +/-4% в час |
| Триггеры TP/SL | Цена маркировки, а не последняя сделка |
| Поддержание соединения WebSocket | Ping примерно каждые 25 секунд; считайте соединение устаревшим примерно после 65 секунд тишины |
| Актуальность тикера | Задержка до 10 секунд |
Как устроен API Perps
Perps представляет собой гибридную биржу: сопоставление ордеров происходит offchain, а хранение средств и расчёты проходят в Polygon. Ончейн выполняются только депозиты и выводы, поэтому за ордер газ не платится. API состоит из трёх уровней:
- Публичные данные (
/v1/info/): инструменты, тикеры, стакан заявок, свечи, сделки, финансирование, комиссии. Ключ не нужен, географической блокировки нет. - Приватное чтение (
/v1/account/): портфель, балансы, ордера, исполнения, настройки. - Подписанная запись (
/v1/trade/): ордера, отмены, плечо, изолированная маржа, автоотмена. Каждый запрос на запись кодируется в MessagePack, хешируется keccak256 и подписывается прокси-ключом как типизированные данные EIP-712.
Подпись SDK выполняют за вас. Также есть спецификация OpenAPI (/api-spec/perps-openapi.json на сайте документации) и спецификация AsyncAPI для WebSocket.
BTC-PERP. Рабочий API возвращает BTC-USD, SP500-USD (USA500), NAS100-USD (US100), ANT-USD и так далее. Загрузите список инструментов, найдите нужный актуальный symbol и сохраните числовой id. Не копируйте ID из примеров: в рабочей среде у BTC ID 6, а у SP500 ID 1.Шаг 1: установка SDK и открытие сессии
Установите SDK командой pip install polymarket-client или npm install @polymarket/client@latest viem. Боту нужны адрес вашего кошелька Полимаркет и приватный ключ подписанта, которому принадлежит аккаунт Perps (аккаунт привязан к EOA подписанта, а не к Safe). При открытии сессии выполняется одна подпись, которая авторизует новый прокси-ключ для торговли. Дальше каждый ордер подписывает он, так что ключ кошелька с вашими средствами никогда не торгует. Сохраните session.credentials в зашифрованном виде, чтобы позже возобновить сессию.
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);
Для депозита из кода (deposit_to_perps / depositToPerps) дополнительно нужен ключ Relayer API, который выдаётся на polymarket.com в разделе Settings, API Keys. Проще и безопаснее пополнять счёт в веб-приложении и вообще не допускать бота к движению денег. С 5 ноября 2026 г. аккаунты, у которых есть адресная книга для вывода, могут выводить средства только на сохранённые адреса. Для дашбордов используйте ключ только для чтения (прокси с меткой read-only:, который отклоняет подписанные запросы на запись) и никогда не используйте для них торговый ключ. Хранение ключей разобрано в нашей главе о кошельках и аутентификации.
Шаг 2: чтение рыночных данных по REST и WebSocket
У тикера три цены: индексная (справедливая стоимость по данным фидов Pyth, Chainlink Data Streams, Hyperliquid и других), цена маркировки (по ней считаются PnL, маржа, ликвидация и TP/SL) и цена последней сделки (только для исполнения). Кроме того, тикер содержит ставку финансирования и время следующего начисления. Стройте сигналы на цене маркировки или на индексной цене.
# 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);
}
Без SDK подписаться можно напрямую через WebSocket сообщением {"id": 1, "req": "sub", "chs": ["bbo::6"]}. Среди каналов: bbo::<id>, book::<id>, trades::<id>, tickers::<id> и klines::<id>::1m. У каждого обновления есть порядковый номер sq: после пропуска или переподключения заново прочитайте состояние через REST. Для бэктестов list_perps_candles / listPerpsCandles загружает историю постранично (эндпоинт REST возвращает до 1 000 свечей за вызов).
Шаг 3: ордера с тейк-профитом и стоп-лоссом
У ордера есть side (BUY или SELL), quantity в единицах базового актива, необязательная price в pUSD и срок действия (gtc, ioc или fok). Для post-only нужен GTC; reduce-only может только уменьшать позицию. Отдельного флага лонга или шорта нет: если позиции нет, BUY открывает лонг, а SELL открывает шорт.
Брекет-ордер прикрепляет к входному ордеру один тейк-профит и один стоп-лосс. Они неактивны, пока вход не исполнится полностью, и отменяются, если вход отменён или исполнен лишь частично. Цены ниже гипотетические.
# 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);
Для открытой позиции place_position_tp_sl / placePositionTpSl добавляет триггеры, которые закрывают всю позицию. Правила:
- Триггеры отслеживают цену маркировки и отправляют ордер reduce-only. TP/SL позиции закрывает её по рынку, поэтому в тонком стакане заявок ждите проскальзывания.
- На одном рынке допускается один тейк-профит и один стоп-лосс позиции. Чтобы перенести стоп, его нужно отменить и создать заново, и эта операция не атомарна.
- Триггер, который цена маркировки уже пересекла, отклоняется; лимитные цены, слишком далёкие от цены маркировки, тоже не принимаются.
Чтобы опробовать работу с ордерами вручную до автоматизации, откройте рынок BTC на Полимаркет Perps и разместите минимально допустимый ордер. Когда вы увидите исполнения, комиссии и панель позиции своими глазами, ответы API станет проще читать.
Шаг 4: каркас бота с контролем рисков в коде
Каркас на Python ниже использует только документированные вызовы SDK; логика управления рисками наша. Три уровня защиты:
- Ограничение плеча. Плечо устанавливается на 3x или на максимум рынка, если он ниже, с изолированной маржой, а размеры, которые попадают в уровень риска с более низким лимитом плеча, отклоняются.
- Аварийные выключатели. Файл HALT, дневной лимит убытка 5% и выход при ликвидации. Выключатель закрывает позиции ордерами IOC reduce-only, отменяет оставшиеся ордера, а затем проверяет, что не осталось ни позиций, ни ордеров.
- Стоп на каждой позиции. Входы fill-or-kill исключают незащищённые частичные исполнения. После каждого исполнения выставляются стоп и тейк-профит позиции; если их отклоняют, позиция сразу закрывается.
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())
Расчёт размера позиции. Размер определяется расстоянием до стопа, а не плечом. При гипотетическом аске BTC 100 000 pUSD и стопе 2% стоп находится на уровне 98 000. Риск на единицу составляет 2 000, поэтому количество равно 20 / 2 000 = 0.01 BTC, то есть номинал 1 000 pUSD. При плече 3x для этого нужно около 333 pUSD маржи. Уровень поддерживающей маржи для BTC составляет 1% (0.5 / 50x), поэтому цена ликвидации лонга равна 100 000 x (1 - 1/3) / (1 - 0.01) = примерно 67 340, что соответствует падению на 32.7%. Стоп на -2% срабатывает задолго до этого. Если стоп исполнится по цене триггера, убыток составит 20 pUSD плюс комиссии: 0.40 при открытии и около 0.39 при закрытии по тарифу тейкера 0.040%, итого около 20.79 pUSD без учёта проскальзывания. Плечо определяет только объём задействованной маржи. На убыток при срабатывании стопа оно не влияет.
Перед запуском в рабочем режиме добавьте дневной лимит сделок, оповещения об исполнениях и срабатывании аварийных выключателей, а также сверку состояния при запуске (fetch_portfolio, fetch_open_orders, fetch_orders, list_fills). Смотрите наши главы о коде управления рисками и бумажной торговле, руководство по кредитному плечу и ликвидации в Perps и руководство по ставкам финансирования в Perps.
Ошибки, повторные попытки и выключатель мертвеца
503 service_unavailableне означает, что запрос точно не выполнен. Повторяйте запросы на чтение и изменения плеча с нарастающей задержкой и соблюдайтеRetry-After. Для подписанных изменений баланса (маржа, выводы) повторно отправляйте только тот же самый исходный подписанный запрос; новая подпись означает вторую корректировку.signature expired: подписи больше 5 минут. Держите часы синхронизированными.insufficient_margin/invalid_leverage: проверки учитывают вашу позицию плюс все активные ордера.message_rate_limited: лимит сообщений WebSocket исчерпан. Ключи только для чтения получают 1 000 сообщений в минуту на IP (всплеск до 50) и четыре соединения на аккаунт на шлюз; остальные лимиты смотрите на странице Rate Limits в документации.- Ошибки формата: слишком много знаков после запятой или больше 5 значащих цифр. Для этого и нужна
fit().
Автоотмена отменяет все открытые ордера, если бот перестаёт её перевзводить. Срок должен быть минимум на 5 секунд впереди, а число срабатываний за сутки по UTC ограничено, поэтому при штатном завершении работы отключайте её.
// 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();
Соревнования, коды билдеров и правила для ботов
В документации среди вариантов использования API названы «торговые боты, реагирующие на рыночные сигналы» и маркетмейкеры. Действуют два набора правил.
- Соревнования. Запрещены фиктивная торговля (wash trading), сделки с самим собой, несколько аккаунтов, торговля в основном ради объёма и «боты, скрипты или автоматизированные средства, предназначенные для манипулирования Программой». Бот, который торгует по-настоящему, допустим; бот, накручивающий объём, может быть дисквалифицирован.
- Коды билдеров. Приложение, которое торгует для других пользователей, может добавлять комиссию до 10 базисных пунктов с согласия каждого трейдера. Оно обязано проверять местоположение каждого пользователя и блокировать ордера из юрисдикций с ограничениями, а не просто предупреждать.
Вознаграждения мейкерам за ликвидность и соревнования описаны в руководстве по Полимаркет Perps.
Хостинг бота
Бот с кредитным плечом не должен работать на ноутбуке. Ему нужно поддерживать соединение WebSocket, реагировать на исполнения за секунды и переживать обрыв домашнего интернета. Запускайте его на VPS под systemd с автоматическим перезапуском, храните ключ в файле окружения, доступном для чтения только пользователю бота, и синхронизируйте часы. Размещать ордера из мест с ограничениями запрещено, поэтому не размещайте бота в США или Канаде и никогда не используйте VPS, чтобы обойти блокировку, которая распространяется на вас. Своих собственных ботов мы запускаем на TradingVPS (партнёрская ссылка: мы получаем небольшую комиссию, а цена для вас не меняется). В нашей главе о VPS-хостинге он сравнивается с другими провайдерами по задержке, цене и регионам.
После бумажной торговли пополните счёт небольшой суммой на странице Полимаркет Perps и неделю работайте на минимальном размере, прежде чем наращивать объёмы.
Главное
- Используйте официальные SDK:
@polymarket/clientилиpolymarket-client. Ваш кошелёк подписывает один раз, а ордера подписывает прокси-ключ. - Сопоставляйте рынки по актуальному символу (
BTC-USD, а неBTC-PERP) и сохраняйте числовой ID. - Рассчитывайте размер от расстояния до стопа, ограничивайте плечо в коде, используйте изолированную маржу и ставьте стоп на каждую позицию сразу после исполнения.
- TP/SL срабатывает по цене маркировки и закрывает позицию по рынку. Автоотмена может снять стопы.
- Боты разрешены, манипулирование соревнованиями запрещено. Никогда не торгуйте из страны с ограничениями.





