把汇率看作图:套利为何会变成负环

套利课程第 5 课。把货币看作点、把汇率看作箭头,了解为什么取对数后乘法会变成加法,以及为什么有利可图的兑换环会变成总和为负的环。

《打造自己的套利机器人》课程第 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)的三个性质:

  1. ln(a × b) = ln(a) + ln(b)。乘法变成加法。
  2. ln(1) = 0。
  3. 如果 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。现在先不要搜索兑换环。

如果智能体打印了其他结果,说明检查没有通过,你就让它修好。这样,你就能用一个可以手动验证的数字来检查结果。

练习

  1. 运行上面的代码。打印出的五个数字和我的一样吗?
  2. 手动创建一个有三个汇率的市场:A 到 B 是 2.0,B 到 C 是 0.6,C 到 A 是 0.9。先手算乘积,再计算权重和权重总和。这个环能盈利吗?(乘积 = 1.08,所以可以。)然后用代码验证。
  3. 修改一个汇率,让乘积正好变成 1.0,再检查权重和是否为 0。
  4. 沿着同一个环反方向兑换。证明如果一个方向的乘积是 p,那么反方向的乘积约为 1/p(每条反向箭头使用汇率 1/r)。所以两个方向不可能同时盈利。

接下来

现在的问题是:面对有许多节点的图,如何不用尝试每个环就找到负环?第 6 课会介绍经典答案 Bellman-Ford。