ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Polymarket链上预测市场自动化交易代理开发实战

Polymarket链上预测市场自动化交易代理开发实战 Polymarket这几年来把“链上预测市场”这个概念做得最完整也让它成了很多程序化交易者盯上的新战场。用户在Polygon网络上用USDC买卖二元结果代币市场里每一个价格本质上都是事件概率的实时投票。我最初接触它时还挺惊讶因为它没有走传统DEX那种AMM资金池路线而是保留了一张真正的中央限价订单簿还开放了REST API和Python SDK。对于一个写惯了量化代理的人来说这种结构近乎理想有买一卖一、有深度、有清晰的订单生命周期不需要跟合约里的恒定乘积公式较劲。这篇文章我会从零开始分享自己开发Polymarket链上预测市场自动化交易代理的完整思路和实操代码覆盖账户凭证、数据层、订单执行、风控和部署监控。如果你有Python基础接触过一点点DeFi或量化交易想尝试agent开发但又不想从抽象框架入手这条路应该能给你一个很具体的落地样本。1. 为什么我会盯上PolymarketCLOB订单簿与事件驱动的套利空间1.1 从AMM到CLOB链上预测市场的底层交易模型绝大多数人在提到链上交易时第一反应是Uniswap式的AMM你往池子里塞两种资产交易价格由池内资产比例决定成交必然带来滑点。Polymarket没有走这条路它把订单簿放在了链下由官方CLOB服务负责撮合再把最终的结果代币放到Polygon链上结算。这意味着你看到的不是一个被公式约束的价格曲线而是真实的买卖双边队列{ bids: [[0.59, 400], [0.58, 100]], asks: [[0.61, 120], [0.62, 300]] }买一0.59、卖一0.61价差2分深度几百股。这种盘口形态对做市和限价策略极其友好。代理可以在订单簿上挂买一挂卖一等着别人来成交而不是被迫吃一个AMM算出的滑点价格。这是Polymarket和市面上大多数DeFi协议最大的区别也是它能成为自动化交易试验田的第一原因。对于具备传统金融市场经验的交易者来说这套模型的门槛很低。你不需要理解复杂的池化逻辑只需要处理订单簿、限价单、委托状态这些熟悉的概念。跨市场迁移成本低意味着可以很快把在其他市场积累的定价和风控经验搬过来。1.2 CTF条件代币与USDC结算一笔委托背后的链上链路Polymarket采用Conditional Token FrameworkCTF来发行结果代币。每一个预测事件都会被拆成两个结果YES和NO。买入“是”代币时你实际上是锁定USDC作为条件代币的抵押事件结算后结果为真的那一方代币可以按1 USDC兑换另一方则归零。这套链路里真正发生在Polygon链上的操作其实不多存入USDC到Polymarket协议授权合约使用你的USDC事件结算后提交代币赎回做市或其他需要链上交互的复杂操作。而日常的下单、撤单、改单都发生在链下CLOB。这样设计的好处是明显的下单不需要等待区块确认几十毫秒就能进入订单簿同时也不会因为Polygon网络的瞬时拥堵而错过关键价格。正因如此代理才能像抢占事件概率一样抢在别人前面更新委托。1.3 适合代理交易的四个特征与整体架构设计综合来看Polymarket吸引自动化代理的原因可以归纳成四点特征对代理的意义订单簿可读能看到完整盘口深度策略可基于买一卖一和挂单分布决策REST API友好不需要连接WebSocket也能拿到行情上手门槛低事件驱动定价每个市场的价格围绕“概率”波动信号边界清晰链下撮合链上结算下单无区块延迟资金可追溯审计路径清晰基于这些特征我建议把代理拆成四个模块而不是写成一个巨大的while循环模块职责外部接口数据层扫描事件、拉订单簿、同步持仓>import os from dotenv import load_dotenv from py_clob_client.client import ClobClient load_dotenv() host os.getenv(CLOB_HOST, https://clob.polymarket.com) private_key os.getenv(PRIVATE_KEY) chain_id int(os.getenv(CHAIN_ID, 137)) # 普通EOA钱包一般对应 signature_type2 client ClobClient( hosthost, keyprivate_key, chain_idchain_id, signature_type2, ) # 创建或恢复API凭证 client.set_api_creds(client.create_or_derive_api_creds())这里最容易踩坑的就是signature_type。它取决于钱包的托管方式普通EOA地址、Gnosis Safe多签钱包、还是Polymarket平台内部托管钱包对应的签名参数不同。如果你用的是浏览器插件钱包生成的普通地址通常填2如果用了多签或平台托管需要去翻SDK源码里的常量定义再确定。签名类型填错后面下单会稳定报签名错误而且这类报错不会提示你“类型不对”排查起来很费时间干脆开局就选对。2.3 项目目录与依赖轻量而不失扩展性我推荐的项目结构是这样的功能边界清晰后续加策略也不用重构polymarket-agent/ ├── .env # 私钥和API URL永远不要提交到Git ├── requirements.txt ├── agent.py # 主循环 ├── data_feed.py # 数据层封装 ├── strategy.py # 信号与订单决策 ├── execution.py # CLOB下单封装 └── risk.py # 风控逻辑依赖越少越好四个人就够跑通第一版python-dotenv requests py-clob-client pydantic很多人在项目初期就急着上pandas、numpy甚至打算训练一个模型。但代理的核心瓶颈在订单状态管理和基础数据流的一致性上不在计算速度。先把骨架跑稳再按需加分析库不要提前优化。3. 数据层的工程实现从扫盘到盘口快照3.1 筛选目标事件Data API使用细节Polymarket开放了两类接口。CLOB API负责账户和委托Data API负责查询市场、订单簿和交易数据。我的惯例是用Data API做扫盘因为它的响应结构更适合批量过滤。下面这个函数可以拉取指定标签下未关闭的事件import requests DATA_API https://data-api.polymarket.com def fetch_active_events(tag_slug: str politics, limit: int 20): r requests.get( f{DATA_API}/events, params{ tag_slug: tag_slug, closed: False, limit: limit, }, timeout10, ) r.raise_for_status() events r.json() result [] for ev in events: for market in ev.get(markets, []): result.append({ event_id: ev.get(id), question: market.get(question), condition_id: market.get(condition_id), token_ids: [t[token_id] for t in market.get(tokens, [])], prices: [t.get(price) for t in market.get(tokens, [])], }) return resulttoken_id是你后续执行所有操作的关键索引。每个二元市场有两个结果代币务必把两个都存下来。我见过不少人在这一步只保存了事件ID结果后面想拉订单簿还得重新解析整个事件结构白白浪费一次请求。3.2 解析订单簿并处理快照老化拿到token_id以后拉订单簿很简单def fetch_orderbook(token_id: str): r requests.get( f{DATA_API}/orderbook, params{token_id: token_id}, timeout10, ) r.raise_for_status() return r.json()返回结构类似前面那种格式价格和数量可能是字符串这一点会被很多人忽略。用Python直接做字符串比较或计算会埋下隐雷建议统一转换from decimal import Decimal bids [(Decimal(p), Decimal(s)) for p, s in book.get(bids, [])] asks [(Decimal(p), Decimal(s)) for p, s in book.get(asks, [])] mid (bids[0][0] asks[0][0]) / 2做盘口快照时我还会特别关注数据的“年龄”。尤其是长尾市场流动性薄Data API返回的可能是几秒甚至几十秒前的盘口。直接用过期快照下单很容易买在已经消失的价位上。我的处理规则非常简单如果快照的更新时间和本地时间差超过2秒这一轮就跳过等下一个轮询周期再决策。3.3 持仓同步与资金占用统计代理运行过程中外部操作可能改变真实持仓比如你在官网手动买了某个结果或者另一个脚本也在操作同一个钱包地址。所以每次主循环开始都应该重新拉取账户余额。balances client.get_balances() for pos in balances.data: if Decimal(pos.balance) 0: print(pos.token_id, pos.balance, pos.asset_type)别把持仓缓存太久。链下CLOB的成交状态可能刚被对手盘改变下一轮循环必须看到最新数据。拉余额的成本不高我默认每个循环都执行一次这样可以避免重复下单也能准确计算当前资金占用和浮动盈亏。4. 交易策略与订单执行信号到委托的完整链路4.1 公平概率合成谁来告诉你市场错了预测市场代理的核心任务不是“预测未来”而是“发现定价偏差”。所以策略层的输入不是某个天降神奇信号而是一组外部概率来源的合成结果。我常用的信号来源有几类其他预测市场或博彩平台的同步赔率统计模型或民调数据新闻NLP情绪打分适合做辅助信号时间衰减规则越临近结算价格应向公开信息收敛。一个极简但可用的合成器是这样def fair_value(sources: dict[str, float]) - float: sources: {kalshi: 0.68, poll_model: 0.70, news_sentiment: 0.61} return sum(sources.values()) / len(sources)真实的信号可以复杂得多但第一步永远是合成一个“我认为合理的概率”然后把它与市场盘口中间价比较差出来的就是边际优势。4.2 买卖方向、限价与期望价值计算有了公平概率就有了买卖依据。设fair 0.49盘口中间价mid 0.435那么买入YES的边际优势是正数。但这里立刻会出现一个新手很容易犯的错误看到正期望就直接按卖一价吃单。吃单确实能保证成交但你是在被动接受对手盘的不利报价。我的原则是尽量当maker按自己认定的公平价格挂限价单等别人来成交。虽然成交率下降但每一笔成交的滑点成本都更低长期跑下来期望价值更容易为正。方向选择可以简化为from decimal import Decimal EDGE_THRESHOLD Decimal(0.03) def decide_order(side: str, fair: Decimal, mid: Decimal): if side BUY and fair - mid EDGE_THRESHOLD: return mid - Decimal(0.01) if side SELL and mid - fair EDGE_THRESHOLD: return mid Decimal(0.01) return None这个0.01的让步相当于菜市场砍价你觉得值0.70现在报0.69让利一分等流动性来打。单笔看起来微不足道但在高频迭代中这个微小的执行差异就是稳定盈利和反复亏手续费的分水岭。4.3 构造Maker订单、提交与生命周期管理在py-clob-client中构造和提交一个限价单非常简洁from py_clob_client.clob_types import OrderArgs from py_clob_client.order_builder.constants import BUY, SELL def place_limit_order(client, token_id: str, side: str, price: float, size: float): args OrderArgs( priceprice, sizesize, sideBUY if side BUY else SELL, token_idtoken_id, ) signed client.create_order(args) resp client.post_order(signed, timeout10) return resp有几个参数直接决定成交行为下单前必须想清楚price你愿意为每股支付的最高价或接受的最低价size按结果代币股数计不是USDC金额。比如买10股价格0.6实际占用约6 USDC订单类型GTC是默认的长期有效单如果只想挂一段时间要设置带有效期的订单类型。订单提交成功后会返回orderID。我建议立刻把这个ID连同价格、数量、时间戳一起记录到日志或本地数据库。不要只打印到控制台然后人肉盯否则一段日志滚动过去订单状态就找不到了。4.4 撤单、改单与重启恢复运维视角挂单之后代理要维护一张活跃委托表定期轮询状态。最简单的方式是拉取所有活跃委托逐一核对本地记录open_orders client.get_orders() for order in open_orders: if order.status open and should_cancel(order): client.cancel_order(order.order_id)当代理重启时还要处理一个棘手场景上次进程崩了本地状态表丢失但CLOB服务端还挂着那些委托。不处理的话订单会在无监控状态下成交账面风险会脱离控制。稳妥的做法是在启动流程里先执行一次全量撤单再重新评估市场按最新信号重新挂单。client.cancel_all()cancel_all()虽然粗暴但作为安全兜底非常有效。注意这只适合代理专用账户如果同一个钱包还有其他模块在下单就要逐一识别和过滤不要误伤正常委托。5. 风控和安全长期存活比单笔盈利更重要5.1 价格精度、最小下单量与neg risk市场Polymarket对订单参数有严格约束价格步进是0.01范围在0.01到0.99之间最小下单量通常是1股。代码里算出0.698这样的价格直接提交必被拒。写一个固定的清洗函数import math from decimal import Decimal, ROUND_FLOOR def sanitize_price(price_d: Decimal) - Decimal: price_d max(Decimal(0.01), min(Decimal(0.99), price_d)) return price_d.quantize(Decimal(0.01), roundingROUND_FLOOR) def sanitize_size(size_d: Decimal) - Decimal: return max(Decimal(1), size_d.quantize(Decimal(1), roundingROUND_FLOOR))另外要注意negRisk标记。部分多结果事件下各二元市场之间存在无风险关系比如“谁会当选”这种多选一事件多个候选人的YES代币共用一套对手方机制。如果策略把每个二元市场都独立按“是/否”模型处理可能会在同一轮事件上重复建仓把自己暴露在不必要的相关性风险里。下单前先查看市场元数据里是否有negRisk标记有的话单独处理。5.2 频率控制、超时与指数退避调用Polymarket API最常遇到的错误之一就是429限流。高频轮询不仅容易触发限流还会让你的代理变成自己给自己制造噪音的机器。我定了一套简单的规则订单簿拉取循环至少间隔500毫秒所有requests请求都设置超时一般5到10秒遇到429或5xx时采用指数退避第一次等1秒第二次等2秒最多等8秒再重试。实现一个最简单的退避import time import random def retry_with_backoff(fn, max_retries4): for attempt in range(max_retries): try: return fn() except Exception as e: wait min(2 ** attempt random.uniform(0, 0.5), 8) time.sleep(wait) raise RuntimeError(请求重试超过最大次数)限流并不可怕可怕的是代理在限流期间发生逻辑错乱订单提交失败却没有重试或者盘口已经变了还拿着旧数据继续决策。退避机制配合状态机能大幅减少这种问题。5.3 私钥保护与资金隔离这是老生常谈但必须反复强调。运行代理的机器必须保密建议做到这几点用专供代理使用的钱包地址不要和主流转账户共用私钥.env文件权限设成600只有服务账号能读取CI/CD流程里绝不输出PRIVATE_KEY如果平台支持更细粒度的API权限尽量只授权代理所需的最小下单范围。我见过太多人把私钥硬编码进Python文件再把整个项目推到GitHub几小时内钱包就被清空。类DeFi项目的安全边界薄如纸一个不经意的提交动作就能摧毁所有策略收益。资金隔离不是可选项而是代理存活的前提条件。5.4 常见报错对照表跑代理过程中下面这张表是我实际踩过的坑可以直接当排查手册用现象常见原因处理方式下单返回400 invalid_signaturesignature_type与钱包类型不匹配根据钱包托管方式换对应签名类型429 Too Many Requests轮询/下单频率过高退避重试降低频率加缓存insufficient balance钱包USDC余额不足或已有委托占用资金查余额统计所有open order冻结额invalid price precision价格小数位超过0.01步进下单前做quantizeorder size too small数量小于最小下单量clamp到最少1股签名者与funder地址不一致代理账户和资金来源地址不匹配检查初始化参数中是否传了funder6. 部署为服务systemd、日志告警与实盘复盘6.1 主循环状态机别写一个流浪的while代理的主循环不建议做成“一个while里面连续做所有事”的抱团脚本而应该是一个状态机每个周期执行固定步骤import time def run_once(client, state, config): sync_positions(client, state) scan_markets(client, state, config) run_strategy(client, state, config) check_orders(client, state) run_risk_checks(state) def main_loop(client, state, config): while True: try: run_once(client, state, config) except Exception as exc: logger.exception(本轮循环异常: %s, exc) time.sleep(config.loop_interval)每个步骤拆成独立函数即使某一步抛出异常进程也不会崩而是记录日志后继续下一轮。这是无值守运行最基本的容错手段。如果想让代理更健壮还可以引入一个简单的熔断机制连续N次循环都失败就停止下单动作只保留行情监听和告警。6.2 用systemd托管代理进程本地跑demo和服务器跑实盘是两种体验。部署到Linux VPS时我习惯用systemd把代理变成系统服务崩溃自动重启开机自启也一并解决。[Unit] DescriptionPolymarket Trading Agent Afternetwork-online.target [Service] Useragent Groupagent WorkingDirectory/opt/polymarket-agent ExecStart/opt/polymarket-agent/.venv/bin/python agent.py Restartalways RestartSec10 EnvironmentPYTHONUNBUFFERED1 [Install] WantedBymulti-user.target启动命令systemctl enable --now polymarket-agent journalctl -u polymarket-agent -f服务器不用选太贵的2核2GB的轻量云完全够跑。更重要的是网络到Polygon RPC和Polymarket API的稳定性如果服务器经常断线代理就会在关键事件时变成睁眼瞎。6.3 日志分级与机器人告警日志是一切排障的基础。我会把所有关键行为分成三级INFO轮询心跳、持仓变化、成交事件WARN订单长时间未成交、单轮亏损超过阈值、盘口快照过期ERRORAPI报错、连接超时、风控拒绝下单。告警我推荐直接用机器人Webhook推到群里或私聊简单直接def send_alert(msg: str): try: requests.post(WEBHOOK_URL, json{msg_type: text, content: msg}, timeout5) except Exception: pass很多代理账户的损失不是死于策略而是死于无人值守时的静默故障。告警不是锦上添花它是第二道风控是唯一能让你在凌晨三点被叫醒的机会。6.4 一次简化事件复盘的完整流程下面用一个虚构事件“某央行7月加息50个基点”来拆解代理在实盘循环中的工作流数字仅用于逻辑演示数据层扫描到该市场盘口为Bid 0.42 / Ask 0.45中间价0.435外部信号源给出的公平概率为0.49边际优势0.49 - 0.435约0.055大于阈值0.03判定YES值得买入执行层构造价格0.44、数量20股的maker买单成功挂入订单簿两小时后订单部分成交8股主循环检查状态时发现剩余12股仍挂在0.44没动此时盘口已经从0.45/0.46抬升到0.52/0.54公平概率0.55边际优势仅0.02小于阈值策略判定没有继续加码的理由主动撤掉剩余12股委托等待下一次价格偏离。整个流程看上去不复杂但每一步背后都是对订单生命周期、风险上限和价格偏离度的量化管理。真实市场里还要考虑流动性深度、手续费、对手方撮合概率代码会更复杂但骨架和这个完全一致。以自己的一个习惯收尾别让代理替你承担你亏不起的钱。如果最多能承受1000 USDC的亏损就给代理账户只放1000 USDC。再好的信号模型都只是让这个亏损上限换取了正期望而不是让上限凭空消失。开发预测市场代理顺序很重要先把数据层和订单状态机折腾扎实再谈复杂的机器学习信号。顺序搞反了你学会的将是一大堆教训而不是利润。
返回列表