Polymarket 机器人教程 · 第 24 章,共 32 章
Polymarket Perps 提供完整的公开 API,并配有官方 TypeScript 和 Python SDK。本章基于真实接口构建一个机器人,内容包括:使用代理交易密钥进行身份验证,通过 REST 和 WebSocket 获取市场数据,下达带止盈和止损的订单,以及防止杠杆机器人把账户亏空的风控措施。
本章内容
- 机器人开发者需要了解的关键数据
- Perps API 的架构
- 第 1 步:安装 SDK 并开启会话
- 第 2 步:通过 REST 和 WebSocket 读取市场数据
- 第 3 步:下达带止盈和止损的订单
- 第 4 步:在代码中内置风控的机器人框架
- 错误处理、失联保护开关、机器人规则与托管
本文中的每个 SDK 方法和端点都来自 Polymarket 的 Perps 文档(docs.polymarket.com/perps,查阅于 2026年11月6日)。我们自己编写的逻辑已在注释中标明,策略函数是一个标注清楚的占位符。
机器人开发者需要了解的关键数据(截至 2026年11月)
| 项目 | 数值 |
|---|---|
| REST 基础 URL | https://api.perpetuals.polymarket.com |
| WebSocket | wss://ws.perpetuals.polymarket.com/v1/ws |
| TypeScript SDK | @polymarket/client(签名使用 viem) |
| Python SDK | polymarket-client(异步) |
| 公开数据 | /v1/info/*,无需身份验证 |
| 私有读取 | /v1/account/*,需带 polymarket-proxy 和 polymarket-secret 请求头 |
| 签名写入 | /v1/trade/*,由代理密钥进行 EIP-712 签名,有效期 5 分钟 |
| 会话凭证 | 默认一周后过期 |
| 最小订单 | 名义价值 $10;最低入金 10 pUSD |
| 最大市价单 | 50x 和 20x 市场为 100万美元,大多数其他市场为 50万美元,3x 和 5x 币种为 5万美元 |
| 价格和数量格式 | 十进制字符串,小数位不超过该市场的精度,且有效数字不超过 5 位 |
| 最高杠杆 | 50x(BTC、ETH、USA500、US100),其他市场分别为 20x、10x、5x 或 3x,大仓位的杠杆上限更低 |
| 手续费(基础等级) | 吃单方 0.040%,挂单方 0.0125%;ANT 半价 |
| 资金费率 | 每小时结算,上限为每小时 +/-4% |
| TP/SL 触发 | 基于标记价格,而非最新成交价 |
| WebSocket 保活 | 约每 25 秒 ping 一次;静默约 65 秒后视为连接失效 |
| Ticker 数据时效 | 最多延迟 10 秒 |
Perps API 的架构
Perps 是一个混合型交易所:撮合在链下完成,资产托管和结算在 Polygon 上完成。只有入金和出金在链上进行,因此每笔订单都不需要支付 gas。API 分为三层:
- 公开数据(
/v1/info/):合约品种、ticker、订单簿、K 线、成交记录、资金费率、手续费。无需密钥,也没有地区限制。 - 私有读取(
/v1/account/):投资组合、余额、订单、成交、配置。 - 签名写入(
/v1/trade/):下单、撤单、杠杆、逐仓保证金、自动撤单。每次写入都经过 MessagePack 编码、keccak256 哈希,再由代理密钥以 EIP-712 类型化数据签名。
SDK 会替你完成签名。此外还有 OpenAPI 规范(文档站点上的 /api-spec/perps-openapi.json)以及 WebSocket 的 AsyncAPI 规范。
BTC-PERP,而线上 API 返回的是 BTC-USD、SP500-USD(USA500)、NAS100-USD(US100)、ANT-USD 等。请先获取合约品种列表,匹配线上的 symbol,再保存数字 id。不要从示例中复制 ID:线上 BTC 为 6,SP500 为 1。第 1 步:安装 SDK 并开启会话
使用 pip install polymarket-client 或 npm install @polymarket/client@latest viem 安装。机器人需要你的 Polymarket 钱包地址,以及拥有该 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 的“设置 > API 密钥”中获取。更简单也更安全的做法是:在网页端为账户入金,让机器人完全不碰资金划转。自 2026年11月5日起,启用了出金地址簿的账户只能向已保存的地址出金。仪表盘请使用只读密钥(标签为 read-only: 的代理密钥,会拒绝签名写入),切勿使用交易密钥。密钥存储方法详见我们的钱包与身份验证章节。
第 2 步:通过 REST 和 WebSocket 读取市场数据
Ticker 包含三种价格:指数价格(来自 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 根 K 线)。
第 3 步:下达带止盈和止损的订单
一个订单包含 side(BUY 或 SELL)、以基础资产为单位的 quantity、可选的以 pUSD 计价的 price,以及有效期类型(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 可添加平掉整个仓位的触发单。规则如下:
- 触发单监控标记价格,触发后发送只减仓订单。仓位 TP/SL 以市价平仓,因此在订单簿较薄时要预期滑点。
- 每个市场只能设置一个仓位止盈和一个仓位止损。移动止损需要先撤销再新建,这一过程不是原子操作。
- 如果标记价格已越过触发价,该触发单会被拒绝;限价与标记价格偏离过大时也会被拒绝。
在实现自动化之前,如果想先手动体验下单流程,可以打开 Polymarket Perps 上的 BTC 市场,下一笔允许的最小订单。亲眼看到成交、手续费和持仓面板之后,再读 API 返回的数据会容易得多。
第 4 步:在代码中内置风控的机器人框架
下面的 Python 框架只使用了文档中记载的 SDK 调用,风控逻辑由我们编写。它有三层保护:
- 杠杆上限。杠杆设为 3x 与市场上限中较低的一个,采用逐仓保证金;若仓位规模会落入杠杆上限更低的风险档位,则拒绝下单。
- 紧急停止开关。包括 HALT 文件、5% 的单日亏损上限,以及在强平时退出。开关触发后,会用只减仓的 IOC 订单平掉仓位,撤销剩余订单,然后确认两者都已清空。
- 每个仓位都有止损。入场使用全部成交否则取消(FOK)订单,因此不会出现缺乏保护的部分成交。每次成交后立即设置仓位止损和止盈;如果被拒绝,则立即平仓。
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.040% 的吃单费率,开仓 0.40,平仓约 0.39,合计滑点前约 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 配额已用完。只读密钥每个 IP 每分钟可发送 1,000 条消息(突发上限 50),每个账户在每个网关最多 4 个连接;其余限制请参阅文档中的 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();
交易竞赛、Builder 代码与机器人规则
文档将“对市场信号做出反应的交易机器人”和做市商列为 API 的用途。有两套规则需要遵守。
- 交易竞赛。禁止对敲交易、自成交、多账户、主要为刷成交量而交易,以及“意图操纵该活动的机器人、脚本或自动化手段”。真实交易的机器人没有问题;刷成交量的机器人可能被取消资格。
- Builder 代码。为其他用户执行交易的应用,在每位交易者同意的前提下,可额外收取最多 10 个基点的手续费。应用必须核验每个用户的所在地,并拦截来自受限司法管辖区的订单,而不能仅仅发出警告。
挂单方流动性奖励和交易竞赛的内容,请参阅 Polymarket Perps 指南。
托管机器人
杠杆机器人不应该跑在笔记本电脑上。它必须保持 WebSocket 连接、在数秒内响应成交,并且能扛住家庭网络断线。请在 VPS 上用 systemd 运行并开启自动重启,将密钥放在只有机器人用户才能读取的环境变量文件中,并同步系统时钟。受限地区境内不允许下单,因此不要把机器人托管在美国或加拿大,也绝不要用 VPS 绕过适用于你的限制。我们自己的机器人运行在 TradingVPS 上(推广链接:我们会获得少量佣金,你支付的价格不变)。我们的 VPS 托管章节从延迟、价格和可用地区等方面将它与其他服务商做了对比。
完成模拟交易后,请在 Polymarket Perps 页面向账户存入少量资金,以最小仓位规模运行一周,再考虑扩大规模。
要点总结
- 使用官方 SDK:
@polymarket/client或polymarket-client。你的钱包只签名一次,之后由代理密钥签署订单。 - 按线上交易代码匹配市场(
BTC-USD,而不是BTC-PERP),并保存数字 ID。 - 根据止损距离确定仓位规模,在代码中限制杠杆,使用逐仓保证金,并在成交后立即为每个仓位设置止损。
- TP/SL 按标记价格触发,以市价平仓。自动撤单可能会移除止损。
- 允许使用机器人,但不允许操纵竞赛。绝不要在受限国家进行交易。





