《打造自己的套利机器人》课程第 5 课。机器人的第一阶段变得更聪明了:我们不再比较两个价格,而是查看一圈货币的兑换。
问题
在一家交易所上,你可以把 USD 换成 EUR,把 EUR 换成 GBP,再把 GBP 换成 USD,还可以兑换许多其他货币。第 2 课讲过,沿着 USD 到 EUR、EUR 到 GBP、再到 USD 的路线兑换一圈,结束时可能比开始时有更多的钱。只有三种货币时,你可以在脑中算出来;有五十种货币时,就会有几千种兑换环。你需要一种不用逐个尝试每个环的方法。
本课先打好基础,目前还不会搜索。第 6、7、8 课会讲如何搜索。
直接的方法,以及它的极限
最直接的方法是:列出每一个环,然后把其中的汇率相乘。只有 3 种货币时,环的数量不多,你可以像第 2 课那样在脑中算出来。但如果有 50 种货币,长度为 3 步的环有 50 × 49 × 48 = 117 600 个,长度为 4 步的环有 5 527 200 个,而且每多一步,数量大约就会乘以 50。程序还能处理少量步骤,但对于真实交易所来说,增长得太快。第 6 课会用代码运行这种直接搜索,并测量它会在哪个规模失效。本课先介绍一种更好的表示方式。
第 1 步:画出来
图是由箭头连接起来的一组点。这里的点代表货币,叫作节点。从 A 到 B 的箭头表示“你可以把 A 换成 B”,箭头旁边的数字是汇率:每 1 单位 A 可以换到多少单位 B。箭头也叫边。
这个小型市场:
USD --0.90--> EUR
EUR --0.88--> GBP
GBP --1.30--> USD
EUR --1.10--> USD
一个环是一条回到起点的路线。USD、EUR、GBP、USD 就是一个有三条边的环。
第 2 步:跟着钱走
从 100 USD 开始,沿着这个环兑换:
- USD 换成 EUR 后:100 × 0.90 = 90 EUR
- EUR 换成 GBP 后:90 × 0.88 = 79.2 GBP
- GBP 换成 USD 后:79.2 × 1.30 = 102.96 USD
沿着这个环兑换,相当于把你的钱乘以 0.90 × 0.88 × 1.30 = 1.0296。乘积大于 1 表示盈利,小于 1 表示亏损,正好等于 1 则没有变化。
所以问题是:有没有一个环,汇率乘积大于 1?
第 3 步:乘法的问题
搜索图的程序擅长把一条路径上的数字相加(“这段路是 3 km,接着是 5 km,总共 8”),却不擅长相乘。我们能把乘法变成加法吗?
可以,用对数。对于本课程,你只需要记住自然对数(写作 ln)的三个性质:
- ln(a × b) = ln(a) + ln(b)。乘法变成加法。
- ln(1) = 0。
- 如果 x 大于 1,ln(x) 是正数;如果 x 在 0 和 1 之间,ln(x) 是负数。
你可以用任何计算器验证这些性质。例如,ln(2) = 0.693,ln(3) = 1.099,而 ln(6) = 1.792,正好等于 0.693 + 1.099。
把性质 1 用在我们的环上:
ln(0.90 × 0.88 × 1.30) = ln(0.90) + ln(0.88) + ln(1.30)
计算结果:−0.1054 − 0.1278 + 0.2624 = +0.0292。
这个环的总和是 +0.0292。大于 1 的乘积变成了大于 0 的和。这样,我们就把“相乘”变成了“相加”。
第 4 步:反转符号
图算法通常要找较短或代价较低的路径,所以数字越小越好,负数最好。我们希望盈利对应较小的负数,因此要反转符号。把一条边的权重定义为:
权重 = −ln(汇率)
那么在我们的环上:
- USD 到 EUR:−ln(0.90) = +0.1054
- EUR 到 GBP:−ln(0.88) = +0.1278
- GBP 到 USD:−ln(1.30) = −0.2624
总权重 = −0.0292,小于零。
有利可图的环,是权重之和小于零的环。它叫作负环。
规则可以概括成两行:
- 汇率乘积大于 1,等价于权重之和小于 0;
- 环的资金倍数等于 e 的负总权重次方(这里 e^0.0292 = 1.0296)。
第 5 步:用代码看看
import math
rates = {
("USD", "EUR"): 0.90,
("EUR", "GBP"): 0.88,
("GBP", "USD"): 1.30,
("EUR", "USD"): 1.10,
}
money = 100.0
for a, b in [("USD", "EUR"), ("EUR", "GBP"), ("GBP", "USD")]:
money *= rates[(a, b)]
print(a, "->", b, "money =", round(money, 4))
print("product of rates:", round(0.90 * 0.88 * 1.30, 4))
weights = {edge: -math.log(rate) for edge, rate in rates.items()}
for edge, w in weights.items():
print(edge, round(w, 4))
cycle = [("USD", "EUR"), ("EUR", "GBP"), ("GBP", "USD")]
total = sum(weights[e] for e in cycle)
print("sum of weights:", round(total, 4))
print("money multiplier:", round(math.exp(-total), 4))
输出:
USD -> EUR money = 90.0
EUR -> GBP money = 79.2
GBP -> USD money = 102.96
product of rates: 1.0296
('USD', 'EUR') 0.1054
('EUR', 'GBP') 0.1278
('GBP', 'USD') -0.2624
('EUR', 'USD') -0.0953
sum of weights: -0.0292
money multiplier: 1.0296
两种检查结果一致:资金从 100 变成了 102.96,权重之和是 −0.0292,而 e 的 0.0292 次方等于 1.0296。
手续费如何计算
每笔交易收取 0.1% 的手续费,意味着你实际得到的金额是汇率所示金额的 0.999 倍。在取对数之前,把每个汇率 r 替换为 r × 0.999。对于有三条边的环,资金倍数会降低约 0.3%,我们的环还能扛住:1.0296 × 0.999 × 0.999 × 0.999 ≈ 1.0265,也就是 2.96% 变成大约 2.65%。但只有 0.2% 利润的环就会消失:手续费吃掉的比它赚的还多。这就是为什么真正的机器人的第一道筛选会把手续费放进权重里;我们会在第 10 课这样做。
让 AI 智能体编写程序
含糊的请求:
在货币图中寻找套利机会。
准确对应这一步的请求:
编写 graph.py。接收一个汇率字典 {("USD","EUR"): 0.90, ...},把权重设为自然对数的相反数,并添加一个 cycle_gain(cycle) 函数,让它返回给定兑换环的资金倍数,计算方式是 e 的负权重和次方。使用汇率 0.90、0.88、1.30,对 USD、EUR、GBP、USD 这条路线进行测试并打印结果。结果必须是 1.0296。现在先不要搜索兑换环。
如果智能体打印了其他结果,说明检查没有通过,你就让它修好。这样,你就能用一个可以手动验证的数字来检查结果。
练习
- 运行上面的代码。打印出的五个数字和我的一样吗?
- 手动创建一个有三个汇率的市场:A 到 B 是 2.0,B 到 C 是 0.6,C 到 A 是 0.9。先手算乘积,再计算权重和权重总和。这个环能盈利吗?(乘积 = 1.08,所以可以。)然后用代码验证。
- 修改一个汇率,让乘积正好变成 1.0,再检查权重和是否为 0。
- 沿着同一个环反方向兑换。证明如果一个方向的乘积是 p,那么反方向的乘积约为 1/p(每条反向箭头使用汇率 1/r)。所以两个方向不可能同时盈利。
接下来
现在的问题是:面对有许多节点的图,如何不用尝试每个环就找到负环?第 6 课会介绍经典答案 Bellman-Ford。