《自己动手做套利机器人》课程第 9 课。世界 3 从这里开始:机器人的第 2 阶段,也就是成交模拟。到目前为止,我们每一边只用一个价格。今天你会明白为什么一个价格不够。
市场摊位
想象一个卖苹果的摊位。摊主有几箱苹果,每箱的价格都不同。第一箱有 2 千克,每千克 100 枚硬币。第二箱有 3 千克,每千克 100.20 枚。如果你想买 2 千克,每千克付 100 枚。如果想买 5 千克,前 2 千克每千克 100 枚,后 3 千克每千克 100.20 枚。你的平均价格会高于第一档价格。
订单簿就是一个卖币的摊位,有两边:
- 卖单一侧列出卖家。每一行表示:“我按这个价格卖这么多。”各行按价格从低到高排列(最优卖价在最前)。买入时,你从顶部开始逐行成交。
- 买单一侧列出买家。每一行表示:“我按这个价格买这么多。”各行按价格从高到低排列(最优买价在最前)。卖出时,你从顶部开始逐行成交。
每一行叫一个档位。真实订单簿有成千上万个档位,但最上面的几档通常最重要。
一个很小的订单簿:
| 卖价 | 卖单数量 |
|---|---|
| 100.20 | 3 |
| 100.00 | 2 |
| 买价 | 买单数量 |
|---|---|
| 99.90 | 5 |
| 99.80 | 4 |
(这里卖单按较差价格在上、较好价格在下的顺序显示,和许多交易所的显示方式一样,因此最优卖价和最优买价在中间相邻。)
最优卖价是 100.00,最优买价是 99.90,价差为 0.10。
直接计算:一个价格乘数量
简单的算法是把最优价格乘以数量:买 3 个币,按 100.00 计算,成本是 300.00。这正是第 1 到第 4 课的扫描器默认采用的假设。实际成本是 300.20,因此买 3 个币时简单算法少算了 0.20;数量越大,误差越大。对于 0.10 的套利利润空间,这个误差足以把盈利变成亏损。简单算法只适合快速初筛,不能作为决策依据。
遍历订单簿
遍历订单簿的意思是:计算交易成本时,先从最优档开始,取该档能提供的数量(或你还需要的数量,两者取较小值),再移动到下一档,直到凑够所需数量。
例如,在上面的订单簿中买入 3 个币(卖单一侧):
- 第 1 档:价格 100.00,数量 2。买 2 个。成本 2 × 100.00 = 200.00。还需要 1 个。
- 第 2 档:价格 100.20,数量 3。买 1 个。成本 1 × 100.20 = 100.20。
- 总成本:300.20。平均价格 300.20 / 3 = 100.0667。
你原本想按 100.00 成交,实际平均价格是 100.0667。每个币多出的 0.0667 成本叫滑点。交易数量越大,滑点通常越高。
卖出时也是一样,只是遍历买单一侧。如果你向买单卖出 3 个币,第一档是 1 个、价格 100.30,第二档是 5 个、价格 99.90:1 × 100.30 + 2 × 99.90 = 300.10。平均价格是 100.0333。
消失的套利机会
第 3 课中,屏幕看到交易所 A 的卖价是 100.00,交易所 B 的买价是 100.30,价差为 0.30。按 3 个币遍历两边的订单簿:你在 A 花 300.20,在 B 收到 300.10,净亏 0.10。第 1 阶段报告的价差对 3 个币的交易并不存在。它只对 1 个币成立,因为 B 最优档只有 1 个币:在 A 买 1 个币花 100.00,在 B 卖出收到 100.30,盈利 0.30。因此,对 1 个币来说价差是真的,对 3 个币来说则是假的。
所以第 2 阶段要问的不是“有没有价差?”,而是“交易多少数量时利润为正?最佳数量是多少?”
代码
def walk(levels, amount):
"""levels: [(price, size), ...], best price first.
Returns (money, filled): money spent or received, and how much was actually filled."""
money, left = 0.0, amount
for price, size in levels:
take = min(size, left)
money += take * price
left -= take
if left <= 0:
break
return money, amount - left
asks_A = [(100.00, 2), (100.20, 3)] # 在这里买入
bids_B = [(100.30, 1), (99.90, 5)] # 在这里卖出
paid, got_a = walk(asks_A, 3)
received, got_b = walk(bids_B, 3)
print("paid", paid, "for", got_a, "coins")
print("received", received, "for", got_b, "coins")
print("net", round(received - paid, 2))
print("average buy price", round(paid / got_a, 4), "average sell price", round(received / got_b, 4))
print(walk(asks_A, 10))
输出:
paid 300.2 for 3 coins
received 300.1 for 3 coins
net -0.1
average buy price 100.0667 average sell price 100.0333
(500.6, 5)
最后一行展示了订单簿深度不够时会怎样:你想买 10 个币,但订单簿里只有 5 个,因此得到 (500.6, 5),也就是花 500.60 买到 5 个币。一定要检查 filled 是否等于你想成交的数量。
真实订单簿
你可以下载真实订单簿。下面的代码读取某个公开交易所上比特币的前五档,不需要账户或密钥:
import json, urllib.request
req = urllib.request.Request("https://api.binance.com/api/v3/depth?symbol=BTCUSDT&limit=5",
headers={"User-Agent": "course"})
d = json.load(urllib.request.urlopen(req, timeout=10))
asks = [(float(p), float(q)) for p, q in d["asks"]]
bids = [(float(p), float(q)) for p, q in d["bids"]]
print("asks", asks[:3])
print("bids", bids[:3])
一次测试中,卖单开头是 (75694.8, 4.63912),买单开头是 (75694.79, 3.97826)。注意两点:价差只有一分钱,非常小;最优档大约有 4 个币,后面的档位则远不到一个币。交易 10 个币会触及价格更差的档位。你读到的数字会有所不同。
两个实用提醒
- 订单簿只是某一时刻的快照。你的订单到达时,别人可能已经成交了最优档。第 11 课会讲这个问题。
- 交易所发送订单簿的格式各不相同:价格可能是文本,数量可能以币或合约为单位。像第 4 课那样,再次检查单位。
让 AI 智能体编写并测试代码
一个含糊的请求:
添加订单簿支持。
一个自带检查条件的请求:
编写 book.py,实现 walk(levels, amount),按最优价格优先遍历档位并返回 (money, filled)。测试 1:asks [(100.00, 2), (100.20, 3)],amount 为 3 时必须返回 (300.2, 3)。测试 2:bids [(100.30, 1), (99.90, 5)],amount 为 3 时必须返回 (300.1, 3)。测试 3:在 asks 上请求 amount 10 时必须返回 (500.6, 5)。然后添加一个函数,从公开的 Binance depth 接口下载前 20 档,并分别按 0.1、1 和 10 BTC 的数量遍历,打印每种数量的平均价格。不要执行任何交易。
用计算器核对这三个测试数字。只有重新计算得到相同结果,智能体写的代码才算正确。
练习
- 运行代码,确认第一组输出和上面完全一致。
- 自己手工构造一个有三档卖单的订单簿。计算一个会用到全部三档的买入数量的成本和平均价格,再用函数验证。
- 对真实订单簿分别运行 0.1、1 和 10 BTC 的计算。数量增加时,平均价格上升了多少?
- 把代码另存为一个新版本。
接下来学什么
现在你能计算在一个订单簿上成交的真实成本了。一个环有好几条交易腿,每一腿都有自己的订单簿和手续费。第 10 课会把这些内容合在一起,找出利润最高的交易数量。