[TOC]

Polymarket 是一个预测市场,每一个问题只有两个结果——是或否、涨或跌、赢或输。以 BTC 的 15 分钟涨跌合约为例,每轮有 Up 和 Down 两个合约,赌的是这 15 分钟内 BTC 收盘比开盘涨还是跌。押对了兑付 1 美元,押错了归零。

这个市场有一个极其特殊的性质:两个结果加起来概率是 100%,所以 Up 的价格加上 Down 的价格,理论上应该精确等于 1。
[ \text{Up_price} + \text{Down_price} = 1 \quad \text{(理论值)} ]
这一性质在普通合约市场里不存在,它给了我们一个天然的确定性锚点。
量化交易一直在做一件事:从充满不确定性的市场里,找到相对确定的机会。
Polymarket 的二元结构提供了这样一个锚点:不管 BTC 这 15 分钟涨还是跌,Up 和 Down 必然有一个会兑付 1 美元。 利用这个锚点,我们不需要判断方向,只需要找到一个时机,让买入 Up 和 Down 的总成本低于 1,利润就是确定的。
问题在于,市场大多数时候两者价格之和都紧贴 1,直接的套利空间很难找到。机会究竟在哪里?
机会来自市场过度反应中的价格失衡。
设想这样一个场景:某一轮 15 分钟里,BTC 突然快速下跌,市场恐慌,Up 合约被大量抛售,价格从 0.5 跌向 0.35。理论上 Up 跌多少,Down 应该同步涨多少,两者之和始终等于 1。但市场两边的反应速度不一样,Up 已经被快速砸下去,Down 还在从 0.5 向 0.65 的移动过程中,可能还没有完全跟上。
这个过渡的瞬间,Up 加 Down 可能只有 0.93,出现了短暂的价格失衡:
[ \text{Up}(0.35) + \text{Down}(0.58) = 0.93 < 1 ]
这就是套利窗口:花 0.93 买入必然兑付 1 美元的组合,利润在买入那一刻就已经确定。
原始思路来自推特博主 @the_smart_ape,非常简洁,只有四个参数:
| 参数 | 含义 |
|---|---|
SHARES |
每腿买入份额 |
SUM_TARGET |
双腿合计目标价(上限) |
MOVE_PCT |
暴跌触发幅度 |
WINDOW_MIN |
监控窗口时间 |
执行逻辑是:在每轮开始的监控窗口内,持续检测价格,一旦某方向暴跌超过触发幅度,买入该方向作为第一腿;然后等待对立方向价格回落,两腿合计低于目标价时买入第二腿,套利闭合;等 15 分钟结算收取兑付。
这个思路干净利落,但直接拿去实盘,会遇到几个明显问题。
原始思路是被动等待:先买第一腿,然后守着看对立方向价格什么时候回落,两腿合计低于目标价了再买第二腿。
我们的改进思路不同——暴跌发生的瞬间,Up 和 Down 的移动速度有较高概率出现不同步,我们就在这个不同步里同时下手:
SUM_TARGET - 第一腿价格 反推限价,趁对立方向还没涨到位就把它锁住两腿同时提交,挂单那一刻套利空间已经被钉住,不再被动守着等市场满足条件,而是让市场来找我们。
核心代码实现如下:
function executeBothLegs(symbols, dumpSide, dumpAsk) {
var leg1Symbol = (dumpSide === "Up") ? symbols.up : symbols.down
var leg2Symbol = (dumpSide === "Up") ? symbols.down : symbols.up
leg1Price = _N(dumpAsk + SLIPPAGE, 4)
// 第二腿限价:SUM_TARGET - leg1Price,确保两腿合计 <= SUM_TARGET 即有利润
leg2Price = _N(SUM_TARGET - leg1Price, 4)
// 并发提交两个限价单
var goLeg1 = exchange.Go("CreateOrder", leg1Symbol, "buy", leg1Price, SHARES)
var goLeg2 = exchange.Go("CreateOrder", leg2Symbol, "buy", leg2Price, SHARES)
var id1 = goLeg1.wait()
var id2 = goLeg2.wait()
leg1OrderId = id1
leg2OrderId = id2
state = STATE.BOTH_PENDING
}
原始思路默认第二腿很快就能成交,但现实里经常出现:Up 被砸下去之后,Down 的价格就是不回落,市场情绪持续悲观,两腿合计一直达不到目标。
这段时间你手里拿着单边的 Up 仓位,价格可能继续下跌,直到结算归零。必须加止损。
我们加了两个参数:
FLOOR_PRICE(保底价):设一个绝对低位(如 0.05)。持仓价格跌到这里,说明市场已判定这个方向大概率输了,直接止损离场。EARLY_TAKE_PROFIT(前期止盈比例):第一腿买完之后,如果价格上涨达到盈利目标,直接卖出拿利润,不等第二腿。function handleLeg1OnlyRisk(symbols, upBid, downBid, isLastMin) {
var holdBid = (leg1Side === "Up") ? upBid : downBid
var profitLine = leg1EntryAsk * (1 + EARLY_TAKE_PROFIT)
var stopLine = leg1EntryAsk * (1 - LAST_MIN_STOP_LOSS)
var needClose = false
var reason = ""
if (holdBid <= FLOOR_PRICE) {
needClose = true; reason = "止损(保底)"
} else if (!isLastMin && holdBid >= profitLine) {
needClose = true; reason = "前期止盈"
} else if (isLastMin && holdBid <= stopLine) {
needClose = true; reason = "末段止损"
}
if (needClose) {
// 先撤对侧挂单,再平仓
cancelAndConfirmUntilClear(leg2OrderId)
closePosition(holdSymbol, holdBid, reason)
}
}
一个保下限,一个锁上限,把原本无限敞口的单边持仓套上了边界。
原始思路对时间维度没有区分,但临近结算阶段完全不同:结算马上来了,Down 的价格就算还没回落也没有时间再等,而且越临近结算价格波动往往越剧烈。
我们加了 LAST_MIN_S(末段时间阈值,可自己调整)。进入末段之后切换逻辑:
LAST_MIN_STOP_LOSS(末段止损比例),持仓价格跌超止损线就市价出var isLastMin = (remaining <= LAST_MIN_S)
// BOTH_PENDING 状态下
if (isLastMin) {
Log("⏰ 最后1分钟,撤销未成交挂单")
cancelAllPending("最后1分钟")
}
早期等价格恢复机会,末段避免归零,两套逻辑互不干扰。
Polymarket 是链上市场,订单状态和持仓信息的返回有时比较慢,下单之后可能要等几秒才能拿到确认。因此策略加入了订单超时检测与重试机制:
function placeOrderAndConfirm(symbol, side, price, amount) {
var orderId = exchange.CreateOrder(symbol, side, orderPrice, amount)
var deadline = Date.now() + ORDER_TIMEOUT_S * 1000
while (Date.now() < deadline) {
var order = exchange.GetOrder(orderId)
if (order && order.Status === 1) {
return { orderId: orderId, avgPrice: order.AvgPrice } // 成交
}
if (order && (order.Status === 2 || order.Status === 4)) {
return { orderId: orderId, avgPrice: null } // 已撤销
}
Sleep(1000)
}
// 超时后执行撤单
exchange.CancelOrder(orderId)
// ... 继续轮询确认最终状态
}
同时加了 SLIPPAGE(滑点参数)提高成交概率,买入时实际挂单价 = 目标价 + 滑点。
Polymarket 有一个特殊机制:合约结算后不会自动把兑付金额打回余额,需要主动调用 Redeem 接口资金才能释放出来。如果漏掉这一步,账户里的钱就被锁在已结算的持仓里。
策略在每一轮切换时自动批量发起 Redeem,保证资金及时回流:
function doRedeem() {
var positions = exchange.GetPositions()
for (var i = 0; i < positions.length; i++) {
var pos = positions[i]
if (pos.Info && pos.Info.redeemable) {
var result = exchange.IO("redeem", pos.Symbol, true)
Log("Redeem 结果:", result)
}
}
}
// 每轮结算前(840s 后)自动触发
if (!redeemDone && elapsed >= 840) {
doRedeem()
redeemDone = true
}

策略添加了实时面板,把账户权益、当前状态、价格监控、持仓盈亏整理成表格展示,跑实盘时随时知道策略在哪一步。面板包含四张表:

实盘运行示例:
新一轮开始,策略进入监控。检测到 Down 合约暴跌 18.60%,从 0.43 跌到 0.35,触发信号。同时挂出两腿限价单:第一腿 Down 挂在 0.37,第二腿 Up 挂在 0.60,两腿合计 0.97 低于目标价。两单并发提交,随后全部成交。实际成交均价:第一腿 0.34,第二腿 0.60,真实总成本 0.94,低于预设阈值 0.97,利润比预期更高。
这个策略在思路上比较完善,行情配合时能取得不错的收益。但有三个缺陷需要坦诚面对:
缺陷一:行情平稳时找不到开仓机会。 套利窗口依赖市场过度反应,行情平稳时暴跌概率很低,策略只能空转。放宽触发幅度可以增加机会,但进场质量会下降。
缺陷二:单边持仓的风险难以完全规避。 第一腿成交后,如果价格继续下跌而非恢复,第二腿一直等不到成交,会一路跌到止损线被踢出。止损阈值的设置是两难:太紧容易被正常波动扫出,太松则真走坏损失过大。
缺陷三:双腿合计目标价的阈值是个权衡。 设高了机会多但每次赚得少,设低了每次赚得多但可能很久等不到一次。这是策略定位的选择:频繁的小额积累,还是守着低阈值等偶尔出现的大机会。
这个策略是一个框架,有三个方向值得深入探索:
方向一:引入外部 BTC 价格数据。 策略比较适合震荡行情,单边趋势行情里止损会被频繁触发。可以接入交易所的 BTC 实时价格,在趋势明显时暂停开仓,减少无效损耗。
方向二:数学建模。 Up 和 Down 本质上是两个二元期权,期权领域有很多成熟的定价模型,理论上可以对套利窗口的出现概率、最优入场时机做更精确的建模,而不是靠固定阈值拍脑袋决定。
方向三:参数动态调整。 现在所有参数都是固定的,但市场波动率在变,高波动和低波动的时候用同一套参数明显不合适。根据实时市场状态自动调整参数,策略的适应性会强很多。
这个策略的核心逻辑——寻找二元市场中的价格失衡瞬间——并不局限于 Polymarket 的 BTC 合约。任何具有二元结构、存在短期定价偏差的市场,都可以用类似的思路去挖掘机会。我们做的只是把一个思路变成了可以跑起来的框架,真正有意思的部分,还在后面。