폴리마켓 봇 튜토리얼 · 32장 중 8장

봇을 위한 폴리마켓 CLOB API: order book 스냅샷용 REST endpoint, 실시간 업데이트용 WebSocket subscription, bids/asks 파싱, mid-price와 depth 계산, 코드 예제.

이 챕터에서 다루는 내용

CLOB API는 주문이 서명되고, 전송되고, 매칭되며, order book이 존재하는 곳입니다. 폴리마켓에는 두 세대의 SDK가 있습니다. 사용 중단된 v1과 현재의 v2입니다. 이 챕터는 v2만 다루며, 2026년에 배포할 봇에는 v1이 들어가서는 안 됩니다. 여기서는 REST 스냅샷 경로, WebSocket 업데이트 채널, 새 빌더들이 자주 실수하는 파싱 세부사항, 그리고 장시간 실행되는 봇이 몇 시간 만에 sync에서 벗어나지 않도록 하는 reconnect 로직까지 살펴봅니다.

  • CLOB v1 vs v2 (v2 사용]
  • Order book REST snapshot
  • WebSocket subscriptions: market and user channels
  • Parsing bids/asks/depth
  • Computing mid-price and best-bid/ask
  • Maker fees, taker fees, rebates
  • Code: connect WS and process price-change events
  • Reconnect and gap-handling

CLOB v1 vs v2 (v2 사용]

폴리마켓은 두 세대의 SDK를 유지합니다. v1(@polymarket/clob-client on npm, py-clob-client <0.30)은 사용 중단되었고 2024년에 추가된 여러 order type이 없습니다. v2(@polymarket/clob-client-v2 Node v1.0.2, Python용 py-clob-client 0.34.6+)가 현재 표준입니다.

구체적인 차이는 세 가지입니다. v2는 다중 outcome market을 위한 negRisk flag를 지원합니다. 이는 2024년 말 NegRisk exchange가 출시된 이후 필수입니다. v2는 WebSocket message shape에 대한 TypeScript type을 제공합니다. v1은 any를 반환합니다. v2는 2025년 8월의 Gnosis Safe signature flow를 기본적으로 처리합니다. v1은 커스텀 signing glue가 필요합니다.

이 챕터의 나머지는 전부 v2를 전제로 작성되었습니다. 오래된 튜토리얼에서 v1 코드를 보게 되면, 반증되기 전까지는 깨진 것으로 간주하세요. 특히 NegRisk market에 대한 order placement는 v1에서 잘못된 exchange contract로 조용히 라우팅될 수 있습니다.

Order book REST snapshot

REST snapshot endpoint는 특정 시점의 단일 token에 대한 전체 book을 반환합니다.

GET https://clob.polymarket.com/book?token_id=<ERC1155_TOKEN_ID>

Response shape:

{
  "market": "0x...",
  "asset_id": "5413...",
  "timestamp": "1715600000000",
  "hash": "0x...",
  "bids": [{"price":"0.45","size":"120"}, {"price":"0.44","size":"380"}, ...],
  "asks": [{"price":"0.47","size":"85"}, {"price":"0.48","size":"210"}, ...]
}

가격은 소수점 2~3자리 문자열이며, size는 share 수를 나타내는 문자열입니다(달러가 아님). bids는 높은 가격에서 낮은 가격 순으로, asks는 낮은 가격에서 높은 가격 순으로 정렬됩니다. hash는 중복 제거 marker입니다. 변하지 않은 book을 반복 조회하면 같은 hash가 반환되므로, 봇은 처리를 건너뛸 수 있습니다.

REST snapshot은 일회성 조회(진입 판단 시 price check)에 적합합니다. 지속적인 모니터링에는 아래 WebSocket channel을 사용하세요.

WebSocket subscriptions: market and user channels

중요한 WebSocket channel은 두 개입니다.

Market channel: wss://ws-subscriptions-clob.polymarket.com/ws/market. 하나 또는 여러 token에 subscribe하면, order-book updates가 발생하는 대로 받게 됩니다.

{"type":"Market","markets":["0xCondId1","0xCondId2"]}

message는 변경이 있을 때마다 도착합니다. type에는 book(전체 snapshot), price_change(delta), tick_size_change(드묾), last_trade_price(가장 최근 fill)이 포함됩니다.

User channel: wss://ws-subscriptions-clob.polymarket.com/ws/user. 인증이 필요하며, fill, partial fill, cancellation 등 자신의 order event를 받습니다.

{"type":"User","auth":{"apiKey":"...","secret":"...","passphrase":"..."}}

User channel은 fill을 감지하는 가장 깔끔한 방법입니다. orders REST endpoint를 polling하면 비용이 더 들고, poll 사이의 state change를 놓칠 수 있습니다. WebSocket은 matcher가 이를 승인하는 순간 event를 바로 push합니다.

Parsing bids/asks/depth

order book은 aggregated size를 가진 price level 목록입니다. 제대로 맞춰야 하는 parsing convention이 두 가지 있습니다.

Order direction: bids는 buy order입니다(누군가 이 가격에 BUY하려는 것). YOUR bot이 sell할 때는 bid를 hit합니다. 봇이 buy할 때는 ask를 lift합니다. 폴리마켓 UI는 같은 방향을 표시합니다. 일부 다른 exchange는 이를 반대로 표시합니다.

Sorting: bids는 내림차순으로 도착합니다(가장 좋은 bid가 먼저). asks는 오름차순으로 도착합니다(가장 좋은 ask가 먼저). best bid는 bids[0], best ask는 asks[0]입니다. 주의: public WebSocket은 때때로 pre-sorted되지 않은 partial book update를 보냅니다. merge 후에는 항상 방어적으로 다시 정렬하세요.

한 level의 depth는 실제로 거래 가능한 달러 가치입니다: price * size. top-5-level depth는 흔한 liquidity metric입니다: sum(b.price * b.size for b in bids[:5]). top-5 depth가 $100 미만이면 book은 illiquid하며 대부분의 전략 가정이 깨집니다.

Computing mid-price and best-bid/ask

봇이 필요로 하는 파생 price point는 세 가지입니다.

  • Best bid / best ask: bids[0].priceasks[0].price. 실제로 거래할 수 있는 가격이며, 1 share 기준입니다.
  • Mid-price: (best_bid + best_ask) / 2. spread의 수학적 중앙값입니다. valuation에는 유용하지만, 실제로는 mid에서 거래하지 않습니다.
  • VWAP price for size N: cumulative size가 N에 도달할 때까지 book을 따라가며, size-weighted average price를 반환합니다. 더 깊은 level까지 밀고 들어가는 것을 반영한, 지금 당장 N shares를 BUY하는 실제 비용입니다.

edge case: bid 또는 ask 한쪽이 비어 있는 경우(파는 사람이 없거나 사는 사람이 없음) book이 one-sided라는 뜻입니다. 폴리마켓의 market structure에서는 보통 해소되었거나 거의 해소된 market에서 이런 일이 발생하며, 한쪽은 0.999이고 loser side에는 아무도 liquidity를 제공하지 않을 때 나타납니다. best-bid = 0 또는 best-ask = 1은 "do not trade" 신호로 취급하세요.

Maker fees, taker fees, rebates

폴리마켓은 maker-taker fee model을 사용합니다. 2026년 5월 기준 수치는 다음과 같습니다.

  • Taker fee: 0(제로) - 기존 book liquidity를 가져가는 order에는 fee가 없습니다. 단, proxy operation에는 gas / network cost가 적용됩니다.
  • Maker rebate: 소액의 positive 금액이며, 적격 reward-program market에서 resting order가 체결될 때 programmatically 지급됩니다. 모든 market에 reward가 있는 것은 아닙니다.
  • NegRisk markets: 동일한 fee structure지만 별도의 exchange contract에서 동작하며, reward는 따로 누적됩니다.

zero taker fee 때문에 폴리마켓은 전통적인 CFD venue와 의미 있게 다릅니다. 대부분의 trading "cost"는 명시적인 fee가 아니라 bid-ask spread 자체입니다. 모든 trade에서 spread를 넘는 전략이라면, 진짜 비용은 spread tax입니다. 일반적인 book에서는 round-trip당 1~3센트, illiquid한 book에서는 더 크다고 가정하세요.

Maker rebate는 liquidity-rewards-eligible market이 전략 아이디어와 맞아떨어질 때만 노릴 가치가 있습니다. Chapter 19에서는 liquidity-rewards farming을 별도의 접근법으로 다룹니다.

Code: connect WS and process price-change events

최소한의 Node 예제입니다. 연결하고, subscribe하고, 한 token에 대한 모든 price-change event를 로그로 남깁니다.

import WebSocket from "ws";
const ws = new WebSocket("wss://ws-subscriptions-clob.polymarket.com/ws/market");
ws.on("open", () => {
  ws.send(JSON.stringify({ type: "Market", markets: ["<CONDITION_ID>"] }));
});
ws.on("message", (data) => {
  const msg = JSON.parse(data.toString());
  if (msg.event_type === "price_change") {
    console.log("price_change", msg.asset_id, msg.changes);
  } else if (msg.event_type === "book") {
    console.log("book snapshot", msg.bids?.[0], msg.asks?.[0]);
  }
});
ws.on("close", () => console.log("closed"));
ws.on("error", (e) => console.error("err", e.message));

WebSocket connection 하나당 약 30개 token까지는 무난하게 subscribe할 수 있습니다. 그 이상이면 여러 connection으로 나누세요. 서버가 가끔 큰 subscription을 오류 없이 떨어뜨리기 때문에, 조용히 stale book을 읽는 문제가 생길 수 있습니다.

Reconnect and gap-handling

장시간 실행되는 WebSocket connection은 끊길 수 있습니다. Cloudflare는 몇 시간마다 connection을 순환시키고, network도 순간적으로 흔들리며, 폴리마켓도 때때로 배포를 진행합니다. 이를 전제로 설계하세요.

Reconnect strategy: close 또는 error가 발생하면 jitter를 섞어 min(2^attempt, 30)초 대기한 뒤 다시 subscribe합니다. reconnect 후 첫 성공 message가 도착하면 attempt counter를 초기화합니다.

gap handling은 reconnect 속도보다 더 중요합니다. WebSocket이 끊겨 있는 동안 book은 움직였습니다. reconnect할 때마다 subscribe한 모든 token의 REST snapshot을 다시 가져와 reconcile하세요. book이 의미 있게 움직인 open position은 state 재확인이 필요하고, exit이 발동해야 할 수도 있으며, alarm은 stale할 수 있습니다. "30초 동안 book update를 놓쳤다"는 장시간 실행 봇의 조용한 치명타입니다. 봇은 stale state 위에서 계속 실행되며, 더 이상 존재하지 않는 가격에 order를 넣게 됩니다.

방어적 패턴: WebSocket 상태와 무관하게 subscribe한 book을 1분마다 snapshot하고, WS는 snapshot polling 위에 얹는 빠른 경로로 취급하세요.

자주 묻는 질문

폴리마켓 CLOB API endpoint는 무엇인가요?
기본 CLOB endpoint는 https://clob.polymarket.com(REST)와 wss://ws-subscriptions-clob.polymarket.com/ws/market(WebSocket)입니다. 이는 @polymarket/clob-client-v2와 py-clob-client가 사용하는 V2 endpoint입니다.
order book을 읽으려면 API key가 필요한가요?
아니요. order book 조회(snapshots와 WebSocket subscriptions)는 public이며 인증이 필요하지 않습니다. API key는 order를 넣거나 취소하고, account-specific data(position, fill)를 읽을 때만 필요합니다.
CLOB WebSocket은 price update를 얼마나 빠르게 push하나요?
order가 매칭되는 속도만큼 빠릅니다. active market은 몇 백 밀리초마다 update가 보이고, 얇은 market은 실제 order가 있을 때만 update됩니다. depth change와 trade event는 같은 WS channel을 통해 흐르므로, 각 event type을 파싱해 올바르게 처리해야 합니다.
폴리마켓 order book의 mid-price는 어떻게 계산하나요?
mid = (best_bid + best_ask) / 2, 단 둘 다 존재할 때만 그렇습니다. 그렇지 않으면 last_trade_price를 fallback으로 사용하세요. best_bid가 best_ask보다 훨씬 낮은 얇은 book에서는 mid가 의미가 없을 수 있습니다. mid를 fair price로 취급하기 전에 항상 spread도 함께 고려하세요.
2026년 폴리마켓의 maker fee는 얼마인가요?
대부분의 카테고리에서 0%입니다. Makers는 taker fee의 20~25%에 해당하는 rebate를 받습니다. Taker fee는 카테고리별로 다르며 sports 0.75%, politics 1.00%, economics 1.25%, crypto 1.80%입니다. rebate와 fee의 비대칭성 때문에 active bot은 거의 항상 market order보다 limit order로 quote합니다.
WebSocket disconnect는 어떻게 처리하나요?
exponential backoff(1초, 2초, 4초, 최대 30초)로 reconnect하고, 같은 market에 다시 subscribe한 뒤, gap을 메우기 위해 REST snapshot을 다시 가져오세요. stale order book은 절대 믿지 마세요. 5초 이상 disconnect되었다면 order를 넣기 전에 fresh snapshot을 요청하세요.