1-我在预测市场踩过的坑:你看到的价格,不是你能成交的价格


1-我在预测市场踩过的坑:你看到的价格,不是你能成交的价格

第一次写这类文章,写得不好,请多多指教,请多多包涵。

我是一名算法工程师、小小的程序员,主要做雷达火灾探测,同时研究量化交易。

接触 Poly 是因为它的定价机制让我觉得有利可图——预测市场本质上是一个概率定价系统,而市场参与者的情绪经常会让赔率偏离真实概率,这种偏差对做过套利的人来说,就是机会。

这篇文章不是教程,也不是炫耀,只是把我这段时间踩过的坑和摸索出来的东西记录下来。如果你也在研究 Poly,希望能省你一点时间。

Poly 套利是怎么回事

Poly 是一个基于区块链(Polygon)的预测市场平台,用户对现实世界事件的结果下注,比如”某国大选谁会赢””某政策会不会通过”。每个事件有若干结果选项,每个选项价格在 0 到 1 优之间,代表市场当前认为该结果发生的概率。例如”A 候选人赢得大选”价格为 0.6 优,意味着市场隐含概率 60%。

套利机会来自哪里?

Poly 用的是条件代币(conditional token)机制:1 优可以拆成一整套互斥结果的代币(split),反过来,只要你手上凑齐一整套结果,就能随时合并(merge)换回 1 优——不用等事件结算。

这是套利的核心。当一整套结果的买入成本之和 < 1 时,买齐一套立刻合并,就能锁定差价:

# 买齐一整套结果的成本(注意:要用能成交的卖一价,不是中间价,见下文)cost_per_set = sum(best_ask(o) for o in outcomes)# 成本 < 1 → 买齐后立即合并(merge)换回 1 优,锁定无风险差价# 成本 > 1 → 不做

这里要特别强调”立刻合并”。早期我以为套利就是买齐一套、然后持有到事件结算兑付——那样资金会被锁到事件解决(可能几个月),年化收益被严重稀释,还白白承担了预言机延迟和解决期的风险。无风险套利的标准退出是 买齐即合并,把资金周转起来。

一个很多人栽跟头的细节:你看到的价格,不是你能成交的价格

这是我后来想明白的最重要的一点,必须放在最前面。

Poly 用的是中央限价订单簿(CLOB)。它在界面和 Gamma API 里给你看的那个 price,是买卖盘的中间价(midpoint);当价差超过 $0.10 时,显示的则是最后成交价。但你真要交易:

买入付的是卖一价(best ask)

卖出收的是买一价(best bid)

举个例子:Yes 的买一是 0.34、卖一是 0.40,界面显示中间价 0.37。你盯着 0.37 算套利,实际买入要付 0.40。

所以真正的无风险套利条件,不是”中间价之和 < 1″,而是 “所有结果按你下单量加权后的卖一价之和 < 1″。因为 ask 永远 ≥ 中间价,用中间价算出来的价差,换成实际成交价后会大幅缩水甚至变负。这一点想不通,后面所有的”账面赚、实际亏”都源于此。

为什么会出现偏差

流动性不均匀,不同选项池子深度差异大;

信息反应速度不同,新消息对各选项影响不同步;

多市场割裂,同一事件在不同合约间可能存在价差。

补一句:单个二元 Yes/No 市场其实最不容易有持续套利——Poly 的撮合是统一订单簿,买 Yes 等价于卖 No,两价之和会被做市商压得很紧。真正有肉的是多结果(categorical)市场、neg-risk 市场,以及跨市场价差。

费率:2025 年免费,现在变了,而且是冲着套利来的

这条我必须单独写,因为它直接改变了套利的数学。

整个 2025 年,Poly 全球站对充值、提现、交易都不收费。但后来它上线了按概率动态的 taker 费,到 2026 年已覆盖十个品类。规则大致是:

挂限价单的 maker 不收 taker 费,还能拿返佣;

吃单的 taker 要交费,费率随价格变化,在 50¢(最大不确定性)处最高,有效费率峰值约 1.80%,越靠近 1¢ 或 99¢ 越趋近于零。

官方说得很直白:这个设计就是专门针对高不确定性市场里的延迟套利和快速刷单。对做套利的影响很直接——套利单往往落在接近 50/50 的市场,正好是费率峰值;而你要保证成交用的 FOK 单是 taker 单,每条腿都按最高档收费。一个三结果市场,三条腿 ×约 1.8%,光手续费就吃掉 5% 多。

这就引出后面坑三里那个真正的两难,先记住这条。

踩过的四个坑

坑一:账面价差是假的——我把中间价当成了成交价

刚开始看到 3% 的账面价差就冲,结果执行完经常不赚反亏。

我一开始以为是 Gas 和滑点。后来才发现,Polygon 上单笔交易的 Gas 大多数时候不到一美分,根本不是主因。真正的原因就是上面那条:我拿中间价在算 edge,实际成交要按卖一价,那 3% 一过 ask 就没了。

真正吃利润的是三样:跨价差(按 ask 买而不是按 mid)、滑点、taker 手续费。

解决办法是把信号源从”中间价”换成”真实订单簿”,并加一层净利润过滤——预估按订单簿深度吃单的加权成交价,再扣掉手续费和滑点缓冲后仍 > 1.5% 才执行:

gross = 1 - cost_per_set            cost_per_set 必须用真实加权成交价net   = gross - taker_fee_total - slippage_bufferif net > MIN_PROFIT_THRESHOLD:      # 例如 0.015        execute_arb(...)

坑二:流动性陷阱——进得去出不来

冷门市场价差大,但买入后资金被锁,卖出时滑点极大甚至根本卖不掉。

解决办法:

只做热门市场(交易量/流动性前 30%);

下单前用 CLOB 的 /book 检查订单簿深度,必须是下单量的 5 倍以上;

小额试单验证真实滑点。

坑三:以为在套利,其实在赌方向

在比特币 15 分钟涨跌市场上亏了 800+ 优。表面同时买 Yes 和 No,实际因为成交时间差导致对冲不完整,最后变成裸赌方向。

这里我后来想明白两件事:

第一,FOK 不等于整组原子成交。 我一度以为给每条腿都用 FOK(Fill or Kill)就万事大吉。但 FOK 只保证”这一条腿要么全成、要么取消”,不保证”整组同时成交”。完全可能 Yes 腿成了、No 腿被 kill,你立刻就裸了一条腿。Poly 的 CLOB 没有多腿原子下单这种东西。

第二,FOK 是 taker,正好踩在最高费率上。 结合上面的费率,短周期、接近 50/50 的市场用 FOK 套利,手续费会把利润直接吃光。

所以解决办法不是”用 FOK 就行”,而是:

成交后立刻对没配平的腿做平仓/对冲(unwind),把腿差当成必须管理的风险;

想清楚 maker / taker 的取舍:maker 零费但不保成交(腿差风险又回来了),taker 保成交但最高费——这个二选一才是这套策略当下真正的难点,没有免费的午餐;

放弃周期过短(< 1 小时)的市场,优先做结算周期较长的主流事件。

坑四:网络延迟——到手的利润凭空消失

信号检测到 → 下单 → 上链确认,这中间的延迟经常让机会消失甚至反向亏损。

解决办法:

用更快的 RPC 节点;

动态提高 Gas Price(基础 + 20%);

加入延迟补偿,只有价差足以覆盖预期波动时才下单。

技术栈与实现

1. 数据获取层

Gamma 用来发现市场,但价格一定要从 CLOB 订单簿取,而不是用 Gamma 的中间价:

import requestsfrom py_clob_client.client import ClobClientGAMMA_API = "https://gamma-api.****.com"CLOB_API  = "https://clob.****.com"#只读行情,无需鉴权clob = ClobClient(host=CLOB_API, chain_id=137)def fetch_markets():        resp = requests.get(f"{GAMMA_API}/markets", params={       "active"True"closed"False"limit"200,    })        return resp.json()def get_book(token_id):        # 返回真实买卖盘 {bids:[...], asks:[...]},而不是中间价        return clob.get_order_book(token_id)# 辅助:某现货交易所 + Chainlink 交叉验证(尤其是加密相关市场的真实标的价)

2. 信号检测层

核心是:用真实 ask 的加权成交价之和算成本,再扣手续费和滑点:

def weighted_fill_price(asks, size):    """按订单簿深度模拟吃单,返回买满 size 份的加权成交价;深度不足返回 None"""    filled, cost = 0.00.0    for lvl in sorted(asks, key=lambda x: float(x["price"])):        px, sz = float(lvl["price"]), float(lvl["size"])        take = min(sz, size - filled)        cost += take * px        filled += take        if filled >= size:            return cost / size    return None  # 深度不够,吃不满def taker_fee_total(fills, size):    # Poly taker 费 ≈ Σ shares × 品类费率 × p × (1 - p),50¢ 处最高    # maker 挂单为 0;这里按 taker 估,函数体略    ...def scan_arb_opportunities(markets, size=500):    opps = []    for m in markets:        fills = [weighted_fill_price(get_book(o["token_id"])["asks"], size)                 for o in m["outcomes"]]        if any(f is None for f in fills):            continue                                    # 深度不够,跳过        cost_per_set = sum(fills)                       # 真实买齐成本        net = (1 - cost_per_set) - taker_fee_total(fills, size) - SLIPPAGE_BUFFER        if net > 0.015:            opps.append({"market": m, "net": net})    return sorted(opps, key=lambda x: x["net"], reverse=True)

3. 交易执行层(官方 py-clob-client)

from py_clob_client.client import ClobClientfrom py_clob_client.clob_types import OrderArgs, OrderTypeclient = ClobClient(host=CLOB_API, key=PRIVATE_KEY, chain_id=137)# 下单需要从私钥派生 L2 凭证,仅有私钥不够client.set_api_creds(client.create_or_derive_api_creds())def execute_arb(market, size):    legs = []    for o in market["outcomes"]:        best_ask = float(get_book(o["token_id"])["asks"][0]["price"])        order = OrderArgs(            token_id=o["token_id"],            price=best_ask,       # FOK 买单价要 ≥ 卖一价才吃得动,挂中间价会被直接 kill            size=size,            # size 是"份额"不是"金额",各腿必须等量才能合并成完整套            side="BUY",            order_type=OrderType.FOK,        )        legs.append((o, client.post_order(order)))    # FOK 只保证单腿全成或取消,不保证整组原子成交    # 若有腿没成,立即对已成交的腿平仓/对冲,避免裸单    unwind_unbalanced(legs)

4. 风控监控层

单市场仓位 ≤ 总资金 10%;

每日最大回撤 5% 触发熔断;

周期 < 60 分钟的市场直接拉黑;

每次套利后检查各腿是否配平,没配平立刻 unwind;

异常推送 Telegram Bot + SQLite 日志。

最后

以上是我在 Poly 做套利这段时间的真实记录,亏钱的坑一个都没藏。

如果说这段时间最大的认知更新,就两条:你看到的价格不是你能成交的价格,以及新费率下 maker/taker 的取舍才是真正的难点。想清楚这两条,比抄任何代码都重要。

这个市场在国内还比较早期,很多东西只能自己摸索。我还在持续迭代,也欢迎同样在研究的朋友交流。

后续计划输出:

Poly 做市的实际体验(maker 零费 + 返佣这条线,我觉得比吃单套利更有搞头);

预言机延迟的深入利用分析;

雷达火灾探测算法与量化交易策略的共性思考。

不定期更新,每篇都是自己真正跑过、踩过的东西。有问题欢迎交流。

(本文为个人技术研究记录,不构成任何投资建议。)