Type/to search
2
Follow
538
Followers
抢不过机器人,就让机器人替你抢:一次 BSC 链上套利的复盘与转向
Discussions
Created 2026-09-30 17:29:07  Updated 2026-10-15 11:25:16
 0
 12

img

最近牛市风头暂且平息了一波,meme 狂潮暂时落幕,市场好像又恢复了平静。不过这一轮狂潮确实让大家注意到了链上这个市场。链上作为一级市场,还有不少潜力可挖,所以这次我们做一些探索的工作。

市场平静的时候能干什么?还能怎么样,老实套利呗。不过今天研究的不是交易所之间的套利,而是链上和交易所之间的套利:BSC 上的 PancakeSwap 和币安 USDT 永续。

思路很老套:同一个币,链上池子的价格偶尔会和币安对不上。链上便宜了,就在链上买、同时在币安开空;等价格回归,两边一起平掉,中间的差价就是利润。下面按时间顺序,把这一周走过的路、踩过的坑和最后的策略都记下来。


一、第一版策略:跑了 13 个小时,一笔没开

第一版就是最直接的“发现价差 → 冲过去吃掉”。部署到发明者 FMZ 上之后,它跑了 13 个多小时,扫描了 4.3 万多轮,每轮 700 多毫秒,一笔都没开。

翻日志后发现一个很整齐的规律:这段时间里一共出现过 30 来次开仓信号,信号出现时,链上买入价比币安买一便宜 50~147bp,远超开仓门槛。但每一次到了发单前的“重新询价”,价差都只剩 0~40bp,又回到了这个币平时的水平,于是被复核拦了下来。30 次,没有一次例外。

信号是真的,复核也没错,问题出在两者之间不到一秒的时间里。

img

二、价差去哪了:369U 的卖家和下一个区块的第一笔交易

为了搞清楚,我把 ARIA 在凌晨 03:51 前后的链上成交一笔一笔拉了出来。那一分钟里,日志记下了三次几乎一样的信号:信号价差 92.6、91.1、92.8bp,复核时只剩 40.9、41.5、42.3bp,每次都差了大约 51bp。

链上记录是这样的:有一个卖家按固定金额出货,每次卖出 369U 的 ARIA,每一笔都把池子价格从 0.03703 砸到 0.03684 左右,正好 51bp 上下。而紧接着的下一个区块里,排在第 1 或第 2 笔的,永远是一笔几百 U 的买入,把价格拉回原位。三次砸盘,三次被拉回,时间和策略日志一一对应。

img

这就是链上的 backrun 机器人。它们盯着每一个区块,发现有人砸盘,就在下一个区块的最前面把便宜的币买走,再去币安卖出对冲。BSC 现在大约 0.45 秒出一个块,这些机器人不靠多付 Gas 抢位置,它们的 Gas 价格往往就是最低的 0.05 gwei,而是直接给出块的 builder 转一笔约 0.0006 BNB 的小费,买下区块开头的位置。

我们的策略从扫描、拉币安深度、查两次余额、重新询价到发出交易,至少要跨两三个区块。所以每次“看到”的价差,在能动手之前就已经没了。复核拦下的,全是过期的机会。

三、全市场扫一遍:钱是真的,但已经有五十多个人在分

一个币可能是巧合,所以我写了一个探测器,把 217 个“币安有永续、PancakeSwap 有池子”的币、1428 个池子过去 24 小时的 86.7 万笔成交全部扫了一遍,找出所有单笔把价格推动 30bp 以上的冲击,再看它们多久被拉回、被谁拉回。

被拉回的速度一天多少次拉回方的理论毛利我们能不能做
0~1 个区块内约 2700 次约 1300~2900U(未扣小费)要竞拍区块开头的位置
3 个区块以上约 1000 次全市场上限约 140U冲击多为 35~45bp,扣费后只剩几个 bp
一直没被拉回约 1400 次—是真实行情,不是机会

img

两点结论:

  1. 这门生意确实存在,全市场一天一两千 U 毛利,不少来自 ARIA 这种“定额、定时”出货的卖家。HANA 上有人每隔约 42 个块卖出 2700U 左右,AAVE 上有人每 90 个块卖 190U。
  2. 它早就被分完了。一天里出现过的回补机器人超过 50 个,头部几个固定排在块内第一位,单笔毛利中位数只有 0.6U,抽查的几笔里有三到五成还要作为小费交给 builder。慢一点的那档没什么人抢,可扣完池子手续费和币安手续费,几乎不剩什么。

四、四次碰壁

接下来一天里,我换了几种思路,每次都用数据去验证。

1. 影子模式。 程序订阅池子的实时成交,一发现砸盘,就在砸盘后的区块状态上模拟一笔回补,对照币安实时报价算扣费后净利,再看最后是谁赢了。“发现”这件事我们做得到:从出块到收到事件,中位数 204 毫秒。但 30 分钟、89 个事件里,14 个在同一区块内就被别人回补了,18 个被下一区块开头的人拿走,剩下“别人慢、我们有机会”的 33 个里,只有 11 个赚钱。

2. 提前看到大单。 订阅公共节点的待上链交易,27 笔大单里能看到 21 笔,但看到的时间中位数比出块还晚 148 毫秒。这些大单走的是私有通道,公共节点出块后才广播。要真提前,得买专业的交易流服务,再和现有机器人拼出价。

3. 链上当信号,去币安做。 回补机器人在链上买回时要在币安卖出对冲,理论上会压一下币安价格。实测 258 次链上冲击后币安 1~60 秒的走势:1 秒、3 秒、10 秒后中间价变化的中位数都是 0,跟随链上方向的比例只有 21%~38%,吃单进出平均是亏的。链上是跟着币安走的,反过来不成立。

4. 备好货等着。 砸盘时链上买入加币安开空,拿着不动,等有人拉盘再卖。乐观账(假设下一块就能成交)一小时两笔都赚;保守账(晚一个区块)在约 280 次砸盘里只买到 1 次。最后还是卡在速度上。

img

五、转折:不去抢,去当被抢的那一方

换角度的契机,是 FMZ 专栏里小草几年前写的一篇文章。他在币安 STORJ 的 1 秒 K 线上发现,有个机器人每隔几秒就用市价单砸出一两万个币,不计成本。他没去比快,而是提前在盘口挂好买单,等对方砸下来时被动接货,反弹后再挂单卖出。那篇文章给我的启发就一句话:提前挂好的单子,不需要速度。

链上 AMM 没有盘口,这原本是我觉得走不通的原因。在 PancakeSwap V3 里把流动性集中放在现价下方一档,看着像限价买单,但价格被拉回时会原路穿过这一档,把币按同样的价格买回去,价差最终还是被回补机器人拿走。

转机在 1inch 的限价单协议上。它的单子是“链下签名、链上成交”:我在链下签好一张“用 20U 按某个价格买某个币”的单子,交给 1inch 挂单平台,不上链、不花 Gas;任何人都可以拿着它去链上成交,只要愿意按我的价格把币给我。

这样局面就反过来了。有人砸盘把池子价格压到我的买单价以下时,回补机器人会发现一条无风险路径:同一笔交易里,从被砸的池子低价买币,再卖给我的挂单,赚中间差价。速度和小费是它们的事,我只等着被成交,接到货后在币安开空。

先确认有没有人在做:最近约 5 小时里,BSC 上 1inch 限价单成交约 260 笔,由 6 个固定的机器人合约完成,其中约三分之一是“同一笔交易里先去 DEX 兑换、再成交挂单”的形态,成交金额中位数 50U 左右。替我们干活的人已经在那里了。

img

六、策略实现

策略在 FMZ 上用 JavaScript 写,需要两个交易所对象:exchanges[0] 币安 USDT 永续,exchanges[1] Web3(BSC)。整体流程:

img

1. 选币:盯住“稳定出货”的人

利润来自反复砸盘的卖家,那就只在他们身上挂单。策略先从 PancakeSwap 代币列表里找出币安也有永续的币,再用 Multicall 一次性查出它们在 V2/V3 工厂里对 USDT、WBNB 的全部池子,大约 200 个币、1400 个池子。

之后每 20 秒用 eth_getLogs 拉一次新的 Swap 事件,计算每笔成交前后的池子价格。单笔把价格砸下去 40bp 以上、金额 100U 以上的记为一次砸盘。同一个币的砸盘再按金额分档,相差 10% 以内算同一档,档内次数越多,越像是有人在按固定金额出货:

javascript
// 同一个币的砸盘按金额分档(相差 10% 以内算同一档),档内次数越多,越像有人在定额出货 s.dumps.forEach(function (u) { var b = Math.round(Math.log(u) / Math.log(1.1)); (bucket[b] = bucket[b] || []).push(u) }) var best = [] Object.keys(bucket).forEach(function (b) { if (bucket[b].length > best.length) best = bucket[b] }) return { sym: k, dumps: s.dumps.length, pumps: s.pumps, repeat: best.length, size: median(best), mv: median(s.mv) }

最后按“定额砸盘次数”、再按“砸盘总次数”排序,取前 5 个。默认统计最近 3 小时;下面这张是用最近 1 小时真实数据试跑的结果:

币定额砸盘次数每次约多少 U砸盘总次数拉盘次数每次砸下去多少
DUSK91181240.71%
O417516110.63%
HANA42517690.98%
BR2213743.03%
UNI2538640.56%

DUSK 一小时被砸了 12 次,其中 9 次都在 118U 左右;HANA 还是那个每次卖 2500U 左右的老熟人。

2. 开仓:签一张 1inch 限价买单

挂单价格 = 币安买一 × (1 − 40bp),有效期 15 秒。1inch v4 的单子是 EIP-712 结构化签名,过期时间和“允许部分成交”都编码在 makerTraits 里:

javascript
function makerTraits(expireTs) { return (BigInt(1) << BigInt(254)) | (BigInt(expireTs) << BigInt(80)) } // 允许多次部分成交 + 过期时间 var o = { salt: String(now()) + String(Math.floor(Math.random() * 1000)), maker: WALLET, receiver: ZERO, makerAsset: USDT, takerAsset: t.addr, // 拿 USDT 换币 makingAmount: usdRaw.toString(), takingAmount: tokRaw.toString(), makerTraits: makerTraits(expire).toString(), } var hash = orderDigest(o), sg = DEX.IO("sign", "0x" + hash) // FMZ 自带签名 HttpQuery(API_BASE, { method: "POST", body: body, headers: { "Authorization": "Bearer " + P.KEY, "Content-Type": "application/json" } })

签名格式错一位,单子就会被平台拒掉或者链上无法成交。所以策略启动时会先签一张测试单,用 1inch 合约的 hashOrder 核对指纹,再用 ecrecover 预编译合约核对签名人,两项都对上才往下跑。

挂单节奏没有固定间隔:上一张过期了,或者币安买一相对挂单时变动超过 10bp,就按新价再挂一张。旧单不撤,链上撤单要花 Gas,让它自己过期就行。所以同一时间可能有好几张单有效,总量用“单币持仓 + 未过期买单 ≤ 60U”“全部币 ≤ 200U”卡住:

javascript
var fresh = o && now() < o.expire && Math.abs(t.bid / o.ref - 1) * 1e4 < P.DRIFT if (fresh) return var buyPx = t.bid * (1 - P.BUY_BPS / 1e4) var room = Math.min(P.MAX_INV - inv.qty * t.bid - pendingUsd(t), P.MAX_TOTAL - totalUsd())

挂单前还有一步试算:假设这张单成交了,把手里的币在链上立刻卖掉,会比币安卖一低多少?超过 30bp 说明池子太浅,买进来也卖不出去,这个币就先不挂。

3. 成交与对冲

实盘里,每轮查一次钱包里该币的余额,余额变多就说明买单被成交了,马上在币安按成交数量市价开空。不够币安最小下单量的零头先攒着,凑够了再一起对冲。

模拟模式(DRY_RUN)流程和实盘一样,只是不提交、不下单。成交判定是:挂单有效期内,链上某笔成交的价格穿过了挂单价,而且差额够付池子手续费(机器人有钱赚),就视为整张成交,按那一刻的币安买一记对冲:

javascript
if (o.side !== "buy" || pxUsd > o.px * (1 - p.fee)) return o.filled = true hedge(t, o.qty, o.px, snapAt(t, ts))

4. 平仓:等价差回归,两边同时做

最初设想是手里有币后再挂一张“比币安卖一高 40bp”的卖单,等有人拉盘时被机器人吃掉。但从选币数据看,不少币只有单方向的大单,卖单可能很久都等不来。所以平仓改成了主动的:每轮用 PancakeSwap 的 Quoter 合约在该币所有 USDT 池里询价,算出“现在卖掉能拿多少”,和币安卖一比:

javascript
var entryBp = (inv.shortCost / inv.short / (inv.cost / inv.qty) - 1) * 1e4 // 开仓时锁定的价差 var exitBp = (best.px / t.ask - 1) * 1e4 // 现在链上卖价相对币安卖一 var netBp = entryBp + exitBp - 2 * TAKER_FEE * 1e4 if (netBp < P.TP_BPS && !force) return

开仓价差 + 平仓价差 − 币安两次手续费 ≥ 5bp,就在 PancakeSwap 上卖出、同时在币安平空。两边同时成交,币价涨跌互相抵消,只赚价差。持有超过 72 小时价差还没回来,按当时价差强制平仓。

5. 风控

两条腿跨链上和交易所,出问题的地方比单交易所多,所以风控写得比较细:

风控项做法
总仓位所有币持仓 + 未过期买单 ≤ 200U,最多同时持有 3 个币
行情剧烈波动币安 60 秒内波动超过 50bp,暂停该币挂买单
池子太浅试卖滑点超过 30bp 不挂单
单腿链上和币安数量对不上时每轮补对冲,30 秒仍补不上就平掉多出来的那条腿,并冷却该币
账本核对实盘每 60 秒用链上真实余额和币安真实空单校正账本
被扫旧单买单成交时锁定价差比 −30bp 还差,说明有人在用旧价格吃单,冷却该币 60 分钟
单币亏损单币(已实现 + 浮动)亏 2U,强平并冷却
总亏损总亏 10U,停止所有新开仓,只平不开

其中单腿处理的核心:

javascript
if (now() - t.legSince < P.LEG_TIMEOUT * 1000) { hedge(t, 0, inv.qty > 0 ? inv.cost / inv.qty : t.bid, { bid: t.bid, ask: t.ask }) // 先每轮补对冲 return } // 超时:链上多出来就在链上卖掉,币安空单多出来就平掉

链上那条腿的浮盈也按“链上现在真实能卖出的价格”来算,不用币安价格代替。这样两边价差拉大时,状态栏会直接体现为亏损,不会被掩盖。

七、现在走到哪一步

选币扫描已经用真实链上数据跑通,策略目前在模拟模式下运行。接下来的计划:

  1. 模拟空跑,看选币和挂单价格是否合理、模拟成交和平仓的价差分布如何;
  2. 挂一张远离市价的单,确认 1inch 平台接受;
  3. 20U 一张的小额实盘。

能预见的风险:

  • 被聪明人吃掉。 买单有效期内币安突然大跌,我的买单比新价格还贵,第一个来成交的就不是回补机器人,而是知道价格要跌的人,也就是逆向选择。有效期从最初设想的 45 秒缩到了 15 秒,再加上“成交价差太差就冷却”,但能挡住多少还得实盘量。
  • 对冲裸露。 链上成交到币安对冲之间有几秒钟的空档。
  • 平仓等不到。 价差可能长时间不回归,最后被 72 小时强平按不利价差平掉。
  • 钱包不能动。 钱包里币的任何变动都会被当成挂单成交去对冲,运行期间不能手动操作这个钱包。

八、结语

这次我们只是把一个框架探索了一遍。从整个过程看,链上套利的价差确实被成熟的机器人吃得死死的:拼速度、拼私有交易流、拼给 builder 的小费,普通策略在这些方面没有优势。

不过收获还是不小的。我们摸清了链上回补的节奏,也知道了那些“定额出货”的卖家长什么样,最后找到了 1inch 限价单这条不需要速度的路。它最后能不能赚钱,要等小额实盘去验证。市场中的机会总是有的,这次的代码、数据和踩过的坑可以作为储备,下一次机会出现时,能更快地发现、更快地动手。

策略源码: 1inch链上挂单套利_BSC币安

本文为个人策略研究记录,不构成投资建议。链上交易存在合约、私钥、逆向选择与对冲失败等风险,模拟与历史数据不代表未来收益。

Comment
All comments (0)
No data
No data
  • 1
Forums
PINE Language
Get the app
iPhone Download
© 2015 - ∞ INVENTOR PTE LTD (SG)