Polymarketボットチュートリアル · 第24章 / 全32章
ポリマーケット Perpsには完全な公開APIがあり、公式のTypeScript SDKとPython SDKも用意されています。この章では実際のインターフェースを使ってボットを構築します。プロキシ取引キーによる認証、RESTとWebSocketによるマーケットデータの取得、利確(テイクプロフィット)と損切り(ストップロス)付きの注文、そしてレバレッジをかけたボットが口座を空にしないためのリスク管理を扱います。
この章の内容
- ボット開発者向けの主要な数値
- Perps APIの構成
- ステップ1:SDKをインストールしてセッションを開く
- ステップ2:RESTとWebSocketでマーケットデータを取得する
- ステップ3:利確と損切り付きで注文を出す
- ステップ4:リスク管理をコードに組み込んだボットの骨組み
- エラー、デッドマンスイッチ、ボットに関するルールとホスティング
ここで紹介するSDKメソッドとエンドポイントはすべて、ポリマーケットの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分 |
| セッション認証情報 | デフォルトでは1週間で失効 |
| 最小注文 | 想定元本$10、最低入金額は10 pUSD |
| 成行注文の上限 | 50倍と20倍のマーケットでは$1M、その他の大半では$500K、3倍と5倍の銘柄では$50K |
| 価格とサイズの形式 | 10進数の文字列。マーケットの小数桁数以内、かつ有効数字5桁以内 |
| 最大レバレッジ | 50倍(BTC、ETH、USA500、US100)、それ以外はマーケットごとに20倍、10倍、5倍、3倍のいずれか。大きなポジションではさらに低くなります |
| 手数料(基本ティア) | テイカー0.040%、メイカー0.0125%。ANTは半額 |
| ファンディング | 1時間ごと、上限は1時間あたり+/-4% |
| TP/SLのトリガー | 直近約定価格ではなくマーク価格 |
| WebSocketのキープアライブ | 約25秒ごとにping。約65秒間応答がなければ接続が古くなったとみなす |
| ティッカーの鮮度 | 最大10秒前のデータ |
Perps APIの構成
Perpsはハイブリッド型の取引所です。マッチングはオフチェーンで行われ、資産の保管と精算はPolygon上で行われます。オンチェーンで処理されるのは入金と出金だけなので、注文ごとのガス代はかかりません。APIは3つの層で構成されています。
- 公開データ(
/v1/info/):銘柄、ティッカー、板情報、ローソク足、約定履歴、ファンディング、手数料。キーは不要で、地域によるブロックもありません。 - 非公開の読み取り(
/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でインストールします。ボットには、ポリマーケットのウォレットアドレスと、Perps口座を所有する署名者の秘密鍵が必要です(口座はSafeではなく署名者のEOAに紐づいています)。セッションを開くと一度だけ署名が行われ、新しいプロキシ取引キーが承認されます。以降はこのキーがすべての注文に署名するため、資金を管理するキーが取引に使われることはありません。後で再開できるよう、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)は、polymarket.comの「設定」→「APIキー」で取得するRelayer APIキーも必要です。よりシンプルで安全なのは、ウェブアプリで口座に入金し、ボットには資金移動をさせない方法です。2026年11月5日以降、出金先アドレス帳を使っている口座は、登録済みのアドレスにしか出金できません。ダッシュボードには読み取り専用キー(read-only:というラベルのプロキシで、署名付きの書き込みを拒否します)を使い、取引用キーは絶対に使わないでください。キーの保管方法はウォレットと認証の章で解説しています。
ステップ2:RESTとWebSocketでマーケットデータを取得する
ティッカーには3つの価格があります。インデックス価格(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回の呼び出しで最大1,000本のローソク足を返します)。
ステップ3:利確と損切り付きで注文を出す
注文には、side(BUYまたはSELL)、基軸資産単位のquantity、任意のpUSD建てprice、そして有効期間条件(gtc、ioc、fok)を指定します。ポストオンリーにはGTCが必要で、リデュースオンリーはポジションを縮小することしかできません。ロングかショートかを示すフラグはありません。ポジションがない状態では、BUYでロング、SELLでショートが建ちます。
ブラケット注文は、エントリー注文に利確1つと損切り1つを付けるものです。これらはエントリーが全量約定するまで待機状態のままで、エントリーが取り消された場合や一部約定にとどまった場合はキャンセルされます。以下の価格は仮のものです。
# 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は成行で決済されるため、板が薄い場合はスリッページを想定してください。
- 利確と損切りは、1つのマーケットにつきポジションごとに1つずつです。損切りの位置を動かすには、取り消してから新規作成する必要があり、この操作はアトミックではありません。
- マーク価格がすでに越えているトリガーは拒否され、マーク価格から離れすぎた指値も受け付けられません。
自動化する前に手動で注文の流れを試したい場合は、ポリマーケット PerpsのBTCマーケットを開き、許容される最小サイズで注文してみましょう。約定、手数料、ポジションパネルを実際に見ておくと、APIのレスポンスが読みやすくなります。
ステップ4:リスク管理をコードに組み込んだボットの骨組み
以下のPythonの骨組みは、ドキュメントに記載されたSDK呼び出しだけを使っています。リスク管理のロジックは当サイト独自のものです。保護は3層構造です。
- レバレッジの上限。 レバレッジは3倍とマーケットの最大値のうち低い方に設定し、分離証拠金を使います。上限がより低いリスクティアに入るサイズの注文は出しません。
- キルスイッチ。 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です。1単位あたりのリスクは2,000なので、数量は20 / 2,000 = 0.01 BTC、想定元本は1,000 pUSDになります。3倍なら必要な証拠金は約333 pUSDです。BTCの維持証拠金率は1%(0.5 / 50倍)なので、ロングの清算価格は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となります。レバレッジが決めるのは拘束される証拠金の額だけで、損切り時の損失額は変わりません。
本番運用の前に、1日あたりの取引回数の上限、約定とキルスイッチの通知、そして起動時の照合(fetch_portfolio、fetch_open_orders、fetch_orders、list_fills)を追加してください。リスク管理コードとペーパートレードの章、Perpsのレバレッジと清算ガイド、Perpsのファンディングレートガイドもあわせてご覧ください。
エラー、リトライ、デッドマンスイッチ
503 service_unavailableは、リクエストが失敗したことを意味するとは限りません。読み取りとレバレッジ変更はバックオフを入れてリトライし、Retry-Afterに従ってください。残高が変わる署名付きリクエスト(証拠金、出金)は、元の署名付きリクエストをそのまま再送するだけにしてください。新しく署名すると、2回目の調整として扱われます。signature expired:署名から5分以上経過しています。時計を同期しておきましょう。insufficient_margin/invalid_leverage:チェックには、保有ポジションに加えて、未約定の注文すべてが含まれます。message_rate_limited:WebSocketの上限に達しました。読み取り専用キーは、IPごとに1分あたり1,000メッセージ(バースト50)、ゲートウェイごとに1口座あたり4接続までです。その他についてはドキュメントのRate Limitsページを参照してください。- 形式エラー:小数桁数が多すぎるか、有効数字が5桁を超えています。
fit()を用意しているのはこのためです。
自動取消は、ボットが再設定(リアーム)をやめると、すべての未約定注文を取り消します。期限は5秒以上先に設定する必要があり、UTCの1日あたりの発動回数にも上限があるため、正常にシャットダウンするときは解除してください。
// 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の用途として「マーケットのシグナルに反応するトレーディングボット」とマーケットメイカーが挙げられています。適用されるルールは2種類あります。
- コンペティション。 仮装売買(ウォッシュトレード)、自己取引、複数アカウントの使用、主に取引高を稼ぐための取引、そして「プログラムを操作する目的のボット、スクリプト、その他の自動化手段」は禁止されています。実際に取引を行うボットは問題ありませんが、取引高を水増しするボットは失格になる可能性があります。
- ビルダーコード。 他のユーザーの代わりに取引するアプリは、各トレーダーの同意を得たうえで最大10ベーシスポイントの手数料を上乗せできます。各ユーザーの所在地を確認し、制限対象の法域からの注文は警告するだけでなくブロックしなければなりません。
メイカー向けの流動性報酬とコンペティションについては、ポリマーケット Perpsガイドで解説しています。
ボットのホスティング
レバレッジをかけたボットをノートPCで動かすべきではありません。WebSocket接続を維持し、約定に数秒以内で反応し、自宅のインターネット回線が切れても動き続ける必要があります。VPS上でsystemdを使って自動再起動を設定し、キーはボット用ユーザーだけが読める環境ファイルに保管し、時計を同期させてください。制限対象の地域からの注文は許可されていないため、米国やカナダでホスティングしてはいけません。また、自分に適用されるブロックを回避するためにVPSを使うことは絶対にやめてください。当サイトでは自社のボットをTradingVPSで運用しています(アフィリエイトリンク:当サイトに少額の紹介料が入りますが、お支払いいただく価格は変わりません)。VPSホスティングの章では、レイテンシー、価格、リージョンの観点から他のプロバイダーと比較しています。
ペーパートレードを終えたら、ポリマーケット Perpsのページから少額を入金し、規模を拡大する前に最小サイズで1週間運用してみてください。
まとめ
- 公式SDKの
@polymarket/clientまたはpolymarket-clientを使いましょう。ウォレットが署名するのは一度だけで、注文にはプロキシキーが署名します。 - マーケットは実際のシンボル(
BTC-PERPではなくBTC-USD)で照合し、数値IDを保存しましょう。 - サイズは損切りまでの距離から決め、レバレッジの上限をコードで設定し、分離証拠金を使い、約定直後にすべてのポジションへ損切りを設定しましょう。
- TP/SLはマーク価格でトリガーされ、成行で決済されます。自動取消によって損切りが消えることがあります。
- ボットの利用は認められていますが、コンペティションの操作は禁止です。制限対象の国から取引することは絶対にやめてください。





