如果你的 MEV 设置不是这样的,你就没救了。
这篇文章是构建套利机器人系列的一部分。该系列的目标是提供一个逐步指南,教你如何构建一个能够在热门去中心化交易所上找到并执行套利机会的自动化 MEV 交易机器人。
在这篇文章中,我们对感兴趣的币种进行了预先筛选。然后,我们推导了寻找同一币种两个池之间最佳套利的数学公式。最后,我们将这个公式实现在代码中,并返回一个潜在套利机会的列表。
在开始寻找套利机会之前,我们必须清晰地定义套利机器人的范围。具体来说,我们想要采取哪种类型的套利行动。最安全的套利是涉及 ETH 的池之间的套利。由于 ETH 是我们交易的燃气支付的资产,总是想要在套利之后持有 ETH 是很自然的。但是每个人都会有这种想法。请记住,在交易中,随着越来越多的人参与,一次性机会变得越来越不赚钱。
为了简单起见,我们将专注于涉及 ETH 的池之间的套利机会。我们只会寻找两个相同币种的池之间的机会。我们不会交易涉及多于 2 个池的机会(所谓的多跳机会)。请注意,将这种策略升级为更高风险的策略是改善您的机器人盈利能力的第一步。
要改进这种策略,您可以例如保留一些稳定币库存,并在产生稳定币的套利机会时采取行动。对于更高风险的资产,例如垃圾币(需采取必要的预防措施),也可以这样做,并定期将您的投资组合重新平衡为 ETH 以支付燃气费用。
另一个方向是放弃我们之前做出的原子性假设,并在我们的策略中引入统计推理。例如,当价格朝着有利方向移动超过某个标准差量时在池中购买一个代币,并稍后出售它(均值回归策略)。这对于那些没有被列在更有效的中心化交易所上的垃圾币,或者虽然被列在中心化交易所上但价格在链上没有被正确跟踪的垃圾币来说是理想的。这涉及许多移动部分,超出了本系列的范围。
现在我们已经定义了套利机器人的范围,我们需要选择我们想要交易的币种。以下是我们将使用的两个选择标准:
重新使用第二篇文章中的代码:池价格的高效读取,我们有以下代码,列出了由提供的工厂合约部署的所有币种:
# [...]
# 加载工厂合约的地址
with open("FactoriesV2.json", "r") as f:
factories = json.load(f)
# [...]
WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
pair_pool_dict = {}
for pair_object in pairDataList:
# 检查币种是否包含 ETH(WETH)。
pair = (pair_object['token0'], pair_object['token1'])
if WETH not in pair:
continue
# 确保该币种在字典中被引用。
if pair not in pair_pool_dict:
pair_pool_dict[pair] = []
# 将该池添加到交易此对的池列表中。
pair_pool_dict[pair].append(pair_object)
# 创建最终的将进行交易的池的字典。
pool_dict = {}
for pair, pool_list in pair_pool_dict.items():
if len(pool_list) >= 2:
pool_dict[pair] = pool_list
应打印一些统计数据,以便更好地掌握我们正在处理的数据:
#不同币种的数量
print(f'We have {len(pool_dict)} different pairs.')
# 总池数量
print(f'We have {sum([len(pool_list) for pool_list in pool_dict.values()])} pools in total.')
# 拥有最多池的币种
print(f'The pair with the most pools is {max(pool_dict, key=lambda k: len(pool_dict[k]))} with {len(max(pool_dict.values(), key=len))} pools.')
# 每对币种的池数量分布,十分位
pool_count_list = [len(pool_list) for pool_list in pool_dict.values()]
pool_count_list.sort(reverse=True)
print(f'Number of pools per pair, in deciles: {pool_count_list[::int(len(pool_count_list)/10)]}')
#每个币种的池数量分布,百分位数(第一个百分位数的十分位)
pool_count_list.sort(reverse=True)
print(f'Number of pools per pair, in percentiles: {pool_count_list[::int(len(pool_count_list)/100)][:10]}')
在撰写本文时,输出如下:
我们有第1431章 不同的对。
我们有3081 水池在 全部的。
这对和 最多的池是 (’0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2’,’0xdAC17F958D2ee523a2206206994597C13D831ec7’)和 16 水池。
每对池的数量,在 十分位数:[16,2,2,2,2,2,2,2,2,2,2]
每对池的数量,在 百分位数:[16,5,4,3,3,3,3,3,3,3]
使用公共 RPC 节点可以在不到 1 秒的时间内获取 3000 个池的储备。这是一个合理的时间。
现在,我们已经拥有了所需的所有数据,我们需要开始寻找套利机会。
每当两个交易相同币种的池之间存在价格差异时,就会存在套利机会。然而,并非所有的价格差异都是可利用的:交易的燃气成本设定了交易必须收回的最低价值,而每个池中的流动性限制了可以从给定价格差异中提取的价值。
为了找到对我们可访问的最有利可图的套利机会,我们需要计算每个价格差异可提取的潜在价值,考虑到每个池中的储备/流动性,并估算交易的燃气成本。
当利用套利机会时,购买输入代币的池的价格将下降,而卖出的池的价格将上升。价格的变动由恒定乘积公式描述。
我们已经在@emileamajar/building-an-arbitrage-bot-automated-market-makers-and-uniswap-2d208215d8c2">第一篇文章中看到了如何计算通过池进行交换的输出,给定该池的储备和输入金额。
为了找到最佳交易规模,我们首先找到了一个公式,用于计算给定一些输入金额和参与交换的两个池的储备的情况下,两次连续交换的输出。
我们假设第一次交换的输入是代币0,第二次交换的输入是代币1,最终产生的输出是代币0。
设 x 为输入金额,(a1, b1) 为第一个池的储备,(a2, b2) 为第二个池的储备。fee 是池收取的费用,并且假设两个池的费用相同(大多数情况下为 0.3%)。
我们定义一个函数来计算交换的输出,给定输入 x 和储备 (a, b):
f(x, a, b) = b (1 - a/(a + x(1-fee)))
然后我们知道第一次交换的输出是:
out1(x) = f(x, a1, b1)
out1(x) = b1 (1 - a1/(a1 + x(1-fee)))
第二次交换的输出是:(注意储备变量的交换)
out2(x) = f(out1(x), b2, a2)
out2(x) = f(f(x, a1, b1), b2, a2)
out2(x) = a2 (1 - b2/(b2 + f(x, a1, b1)(1-fee)))
out2(x) = a2 (1 - b2/(b2 + b1 (1 - a1/(a1 + x (1-fee))) (1-fee)))
我们可以使用 @emileamajar/building-an-arbitrage-bot-automated-market-makers-and-uniswap-2d208215d8c2">Desmos 绘制此函数。通过选择储备值,使得我们模拟第一个池拥有 1 ETH 和 1750 USDC,第二个池拥有 1340 USDC 和 1 ETH,我们得到以下图形:
我们绘制了交易的毛利润作为输入值的函数。
请注意,我们实际上绘制了 out2(x) - x,这是交易的利润减去输入金额。
从图形上可以看出,最佳交易规模为输入值为 0.0607 ETH,获得了 0.0085 ETH 的利润。合约必须至少具有 0.0607 ETH 的 WETH 流动性才能利用这个机会。
这个 0.0085 ETH 的利润值(写这篇文章时约为 16 美元)不是交易的最终利润,因为我们还需要考虑交易的燃气成本。这将在接下来的文章中讨论。
我们想要为我们的 MEV 机器人自动计算这个最佳交易规模。这可以通过基本的微积分来完成。我们有一个关于一个变量 x 的函数,我们想要最大化这个函数。当函数的导数为 0 时,函数达到最大值。
可以使用各种免费和在线工具来符号地计算函数的导数,比如 Wolfram Alpha。
我们的毛利润函数的导数如下:
dout2(x)/dx = (a1b1a2b2(1-fee)^2)/(a1b2 + (1-fee)x(b1(1-fee)+b2))^2
由于我们想要找到最大化利润(即 out2(x) - x)的 x 的值,我们需要找到导数为 1(而不是 0)的 x 的值。
使用 Wolfram Alpha,我们得到了下面方程中 x 的解:
dout2(x)/dx = 1
x = (sqrt(a1b1a2b2(1-fee)^4 (b1(1-fee)+b2)^2) - a1b2(1-fee)(b1(1-fee)+b2)) / ((1-fee) (b1(1-fee) + b2))^2
使用上面图中使用的储备值,我们得到 x_optimal = 0.0607203782551,这验证了我们的公式(与图中的值 0.0607 相比)。
虽然这个公式不太易读,但在代码中很容易实现。以下是一个计算 2 次交换输出和最佳交易规模的公式的 Python 实现:
# 计算最佳交易规模的辅助函数
# 单次交换的输出
def swap_output(x, a, b, fee=0.003):
return b * (1 - a/(a + x*(1-fee)))
# 两次连续交换的毛利润
def trade_profit(x, reserves1, reserves2, fee=0.003):
a1, b1 = reserves1
a2, b2 = reserves2
return swap_output(swap_output(x, a1, b1, fee), b2, a2, fee) - x
# 最佳输入金额
def optimal_trade_size(reserves1, reserves2, fee=0.003):
a1, b1 = reserves1
a2, b2 = reserves2
return (math.sqrt(a1*b1*a2*b2*(1-fee)**4 * (b1*(1-fee)+b2)**2) - a1*b2*(1-fee)*(b1*(1-fee)+b2)) / ((1-fee) * (b1*(1-fee) + b2))**2
现在我们知道如何计算在相同币种的任意两个给定池之间的套利机会的毛利润,我们只需迭代所有币种,并测试所有具有相同币种的池。这将为我们提供所有可能套利机会的毛利润,这些套利机会在我们策略的范围内。
要估算交易的净利润,我们需要估算利用给定机会的燃气成本。这可以通过通过 eth_call 对 RPC 节点模拟交易来精确执行,但需要大量时间,并且每个区块只能执行几十个机会。
我们首先通过假设固定的交易燃气成本(实际上是下限)来对燃气成本进行毛估计,并淘汰不足以覆盖燃气成本的机会。然后我们只对剩下的机会进行燃气成本的精确估算。
以下是遍历所有币种和所有池的代码,并按利润排序的代码:
# [...]
# 获取 pool_dict 中每个池的储备
to_fetch = [] # List of pool addresses for which reserves need to be fetched.
for pair, pool_list in pool_dict.items():
for pair_object in pool_list:
to_fetch.append(pair_object["pair"]) # Add the address of the pool
print(f"Fetching reserves of {len(to_fetch)} pools...")
# getReservesParallel() 是 MEV 机器人系列中的第二篇文章中的一部分
reserveList = asyncio.get_event_loop().run_until_complete(getReservesParallel(to_fetch, providersAsync))
#构建交易机会列表
index = 0
opps = []
for pair, pool_list in pool_dict.items():
# Store the reserves in the pool objects for later use
for pair_object in pool_list:
pair_object["reserves"] = reserveList[index]
index += 1
# 遍历该币种的所有池
for poolA in pool_list:
for poolB in pool_list:
# Skip if it's the same pool
if poolA["pair"] == poolB["pair"]:
continue
# 如果储备中有一个为0(除以0),则跳过
if 0 in poolA["reserves"] or 0 in poolB["reserves"]:
continue
# 重新排序储备,以便 WETH 始终是第一个代币
if poolA["token0"] == WETH:
res_A = (poolA["reserves"][0], poolA["reserves"][1])
res_B = (poolB["reserves"][0], poolB["reserves"][1])
else:
res_A = (poolA["reserves"][1], poolA["reserves"][0])
res_B = (poolB["reserves"][1], poolB["reserves"][0])
# 通过公式计算最佳输入值
x = optimal_trade_size(res_A, res_B)
# 如果最佳输入值为负数(池的顺序被颠倒),则跳过
if x < 0:
continue
# 计算毛利润,以 Wei 为单位(未考虑Gas成本)
profit = trade_profit(x, res_A, res_B)
# 存储套利机会的细节。值以 ETH 为单位。(1e18 Wei = 1 ETH)
opps.append({
"profit": profit / 1e18,
"input": x / 1e18,
"pair": pair,
"poolA": poolA,
"poolB": poolB,
})
print(f"Found {len(opps)} opportunities.")
Fetching reserves of 3081 pools.
Found 1791 opportunities.
现在我们有了所有机会的列表。我们只需要估算它们的利润。现在,我们将简单地假设交易机会的燃气成本是恒定的。
我们必须对在 Uniswap V2 上进行交换的燃气成本使用一个下限。经验上,我们发现这个值接近于 43k 燃气。
利用一个机会需要进行 2 次交换,并在以太坊上执行交易的成本是固定的 21k 燃气,总共是每个机会 107k 燃气。
以下是计算每个机会估计净利润的代码:
# [...]
# 使用每个机会 107k Gas 的硬编码 Gas 成本
gp = w3.eth.gas_price
for opp in opps:
opp["net_profit"] = opp["profit"] - 107000 * gp / 1e18
# 按预计净利润排序
opps.sort(key=lambda x: x["net_profit"], reverse=True)
# 保持积极的机会
positive_opps = [opp for opp in opps if opp["net_profit"] > 0]
### 打印统计数据
# 积极的机会很重要
print(f"Found {len(positive_opps)} positive opportunities.")
# 每个机会的详细信息
ETH_价格=1900年 # 你应该动态获取 ETH 的价格
for opp in positive_opps:
print(f"Profit: {opp['net_profit']} ETH (${opp['net_profit'] * ETH_PRICE})")
print(f"Input: {opp['input']} ETH (${opp['input'] * ETH_PRICE})")
print(f"Pool A: {opp['poolA']['pair']}")
print(f"Pool B: {opp['poolB']['pair']}")
print()
这是脚本的输出:
Found 57 positive opportunities.
Profit: 4.936025725859028 ETH ($9378.448879132153)
Input: 1.7958289984719014 ETH ($3412.075097096613)
Pool A: 0x1498bd576454159Bb81B5Ce532692a8752D163e8
Pool B: 0x7D7E813082eF6c143277c71786e5bE626ec77b20
{‘profit’: 4.9374642090282865, ‘input’: 1.7958(…)
Profit: 4.756587769768892 ETH ($9037.516762560894)
Input: 0.32908348765283796 ETH ($625.2586265403921)
Pool A: 0x486c1609f9605fA14C28E311b7D708B0541cd2f5
Pool B: 0x5e81b946b61F3C7F73Bf84dd961dE3A0A78E8c33
{‘profit’: 4.7580262529381505, ‘input’: 0.329(…)
Profit: 0.8147203063054365 ETH ($1547.9685819803292)
Input: 0.6715171730669338 ETH ($1275.8826288271744)
Pool A: 0x1f1B4836Dde1859e2edE1C6155140318EF5931C2
Pool B: 0x1f7efDcD748F43Fc4BeAe6897e5a6DDd865DcceA
{‘profit’: 0.8161587894746954, ‘input’: 0.671(…)
(…)
这些利润看起来确实很高。首先应该采取的步骤是验证代码是否正确。经过仔细检查代码,我们发现代码是正确的。
这些利润是否真实?事实证明并不是。我们在选择应该考虑在策略中的池时涵盖的范围太广,导致我们手中拿到了一些有毒代币的池。
ERC20 代币标准只描述了一个用于互操作性的接口。任何人都可以部署一个实现了这个接口的代币,并选择实现非正统的行为,这正是这里所涉及的情况。
一些代币创建者设计他们的 ERC20 代币,以便这些代币在交易所上只能购买,而不能出售。一些代币合约甚至具有紧急停止机制,允许创建者取消所有用户的资金。
在我们的 MEV 机器人中,这些有毒代币必须被过滤掉。这将在未来的文章中讨论。
如果我们手动过滤掉明显有毒的代币,我们还剩下以下 42 个机会:
Profit: 0.004126583158496902 ETH ($7.840508001144114)
Input: 0.008369804833786892 ETH ($15.902629184195094)
Pool A: 0xdF42388059692150d0A9De836E4171c7B9c09CBf
Pool B: 0xf98fCEB2DC0Fa2B3f32ABccc5e8495E961370B23
{‘profit’: 0.005565066327755902, (…)
Profit: 0.004092580415474992 ETH ($7.775902789402485)
Input: 0.014696360216108083 ETH ($27.92308441060536)
Pool A: 0xfDBFb4239935A15C2C348400570E34De3b044c5F
Pool B: 0x0F15d69a7E5998252ccC39Ad239Cef67fa2a9369
{‘profit’: 0.005531063584733992, (…)
Profit: 0.003693235163284344 ETH ($7.017146810240254)
Input: 0.1392339178514088 ETH ($264.5444439176767)
Pool A: 0x2957215d0473d2c811A075725Da3C31D2af075F1
Pool B: 0xF110783EbD020DCFBA91Cd1976b79a6E510846AA
{‘profit’: 0.005131718332543344, (…)
Profit: 0.003674128918827048 ETH ($6.980844945771391)
Input: 0.2719041848570484 ETH ($516.617951228392)
Pool A: 0xBa19343ff3E9f496F17C7333cdeeD212D65A8425
Pool B: 0xD30567f1d084f411572f202ebb13261CE9F46325
{‘profit’: 0.005112612088086048, (…)
(…)
请注意,通常利润低于执行交易所需的输入金额。
这些利润更为合理。但请记住,它们仍然是最佳情况下的利润,因为我们对每个机会的燃气成本进行了非常粗略的估计。
在未来的文章中,我们将模拟执行我们的交易,以获取每个机会的燃气成本的精确值。
为了模拟执行,我们首先需要开发执行交易的智能合约。这是下一篇文章的主题。
我们现在对我们的 MEV 套利机器人的范围有了明确的定义。
我们已经探讨了套利策略背后的数学理论,并在 Python 中实现了它。
现在我们有了潜在的套利机会列表,我们需要模拟它们的执行,以获取最终的利润值。为此,我们需要准备好我们的交易智能合约。
在下一篇文章中,我们将使用 Solidity 开发这样的智能合约,并模拟我们的第一笔套利交易。
您可以在与本文相关联的 GitHub 存储库中找到完整的代码。该脚本最好在 Jupyter 笔记本中运行。
如果你的 MEV 设置不是这样的,你就没救了。
这篇文章是构建套利机器人系列的一部分。该系列的目标是提供一个逐步指南,教你如何构建一个能够在热门去中心化交易所上找到并执行套利机会的自动化 MEV 交易机器人。
在这篇文章中,我们对感兴趣的币种进行了预先筛选。然后,我们推导了寻找同一币种两个池之间最佳套利的数学公式。最后,我们将这个公式实现在代码中,并返回一个潜在套利机会的列表。
在开始寻找套利机会之前,我们必须清晰地定义套利机器人的范围。具体来说,我们想要采取哪种类型的套利行动。最安全的套利是涉及 ETH 的池之间的套利。由于 ETH 是我们交易的燃气支付的资产,总是想要在套利之后持有 ETH 是很自然的。但是每个人都会有这种想法。请记住,在交易中,随着越来越多的人参与,一次性机会变得越来越不赚钱。
为了简单起见,我们将专注于涉及 ETH 的池之间的套利机会。我们只会寻找两个相同币种的池之间的机会。我们不会交易涉及多于 2 个池的机会(所谓的多跳机会)。请注意,将这种策略升级为更高风险的策略是改善您的机器人盈利能力的第一步。
要改进这种策略,您可以例如保留一些稳定币库存,并在产生稳定币的套利机会时采取行动。对于更高风险的资产,例如垃圾币(需采取必要的预防措施),也可以这样做,并定期将您的投资组合重新平衡为 ETH 以支付燃气费用。
另一个方向是放弃我们之前做出的原子性假设,并在我们的策略中引入统计推理。例如,当价格朝着有利方向移动超过某个标准差量时在池中购买一个代币,并稍后出售它(均值回归策略)。这对于那些没有被列在更有效的中心化交易所上的垃圾币,或者虽然被列在中心化交易所上但价格在链上没有被正确跟踪的垃圾币来说是理想的。这涉及许多移动部分,超出了本系列的范围。
现在我们已经定义了套利机器人的范围,我们需要选择我们想要交易的币种。以下是我们将使用的两个选择标准:
重新使用第二篇文章中的代码:池价格的高效读取,我们有以下代码,列出了由提供的工厂合约部署的所有币种:
# [...]
# 加载工厂合约的地址
with open("FactoriesV2.json", "r") as f:
factories = json.load(f)
# [...]
WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
pair_pool_dict = {}
for pair_object in pairDataList:
# 检查币种是否包含 ETH(WETH)。
pair = (pair_object['token0'], pair_object['token1'])
if WETH not in pair:
continue
# 确保该币种在字典中被引用。
if pair not in pair_pool_dict:
pair_pool_dict[pair] = []
# 将该池添加到交易此对的池列表中。
pair_pool_dict[pair].append(pair_object)
# 创建最终的将进行交易的池的字典。
pool_dict = {}
for pair, pool_list in pair_pool_dict.items():
if len(pool_list) >= 2:
pool_dict[pair] = pool_list
应打印一些统计数据,以便更好地掌握我们正在处理的数据:
#不同币种的数量
print(f'We have {len(pool_dict)} different pairs.')
# 总池数量
print(f'We have {sum([len(pool_list) for pool_list in pool_dict.values()])} pools in total.')
# 拥有最多池的币种
print(f'The pair with the most pools is {max(pool_dict, key=lambda k: len(pool_dict[k]))} with {len(max(pool_dict.values(), key=len))} pools.')
# 每对币种的池数量分布,十分位
pool_count_list = [len(pool_list) for pool_list in pool_dict.values()]
pool_count_list.sort(reverse=True)
print(f'Number of pools per pair, in deciles: {pool_count_list[::int(len(pool_count_list)/10)]}')
#每个币种的池数量分布,百分位数(第一个百分位数的十分位)
pool_count_list.sort(reverse=True)
print(f'Number of pools per pair, in percentiles: {pool_count_list[::int(len(pool_count_list)/100)][:10]}')
在撰写本文时,输出如下:
我们有第1431章 不同的对。
我们有3081 水池在 全部的。
这对和 最多的池是 (’0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2’,’0xdAC17F958D2ee523a2206206994597C13D831ec7’)和 16 水池。
每对池的数量,在 十分位数:[16,2,2,2,2,2,2,2,2,2,2]
每对池的数量,在 百分位数:[16,5,4,3,3,3,3,3,3,3]
使用公共 RPC 节点可以在不到 1 秒的时间内获取 3000 个池的储备。这是一个合理的时间。
现在,我们已经拥有了所需的所有数据,我们需要开始寻找套利机会。
每当两个交易相同币种的池之间存在价格差异时,就会存在套利机会。然而,并非所有的价格差异都是可利用的:交易的燃气成本设定了交易必须收回的最低价值,而每个池中的流动性限制了可以从给定价格差异中提取的价值。
为了找到对我们可访问的最有利可图的套利机会,我们需要计算每个价格差异可提取的潜在价值,考虑到每个池中的储备/流动性,并估算交易的燃气成本。
当利用套利机会时,购买输入代币的池的价格将下降,而卖出的池的价格将上升。价格的变动由恒定乘积公式描述。
我们已经在@emileamajar/building-an-arbitrage-bot-automated-market-makers-and-uniswap-2d208215d8c2">第一篇文章中看到了如何计算通过池进行交换的输出,给定该池的储备和输入金额。
为了找到最佳交易规模,我们首先找到了一个公式,用于计算给定一些输入金额和参与交换的两个池的储备的情况下,两次连续交换的输出。
我们假设第一次交换的输入是代币0,第二次交换的输入是代币1,最终产生的输出是代币0。
设 x 为输入金额,(a1, b1) 为第一个池的储备,(a2, b2) 为第二个池的储备。fee 是池收取的费用,并且假设两个池的费用相同(大多数情况下为 0.3%)。
我们定义一个函数来计算交换的输出,给定输入 x 和储备 (a, b):
f(x, a, b) = b (1 - a/(a + x(1-fee)))
然后我们知道第一次交换的输出是:
out1(x) = f(x, a1, b1)
out1(x) = b1 (1 - a1/(a1 + x(1-fee)))
第二次交换的输出是:(注意储备变量的交换)
out2(x) = f(out1(x), b2, a2)
out2(x) = f(f(x, a1, b1), b2, a2)
out2(x) = a2 (1 - b2/(b2 + f(x, a1, b1)(1-fee)))
out2(x) = a2 (1 - b2/(b2 + b1 (1 - a1/(a1 + x (1-fee))) (1-fee)))
我们可以使用 @emileamajar/building-an-arbitrage-bot-automated-market-makers-and-uniswap-2d208215d8c2">Desmos 绘制此函数。通过选择储备值,使得我们模拟第一个池拥有 1 ETH 和 1750 USDC,第二个池拥有 1340 USDC 和 1 ETH,我们得到以下图形:
我们绘制了交易的毛利润作为输入值的函数。
请注意,我们实际上绘制了 out2(x) - x,这是交易的利润减去输入金额。
从图形上可以看出,最佳交易规模为输入值为 0.0607 ETH,获得了 0.0085 ETH 的利润。合约必须至少具有 0.0607 ETH 的 WETH 流动性才能利用这个机会。
这个 0.0085 ETH 的利润值(写这篇文章时约为 16 美元)不是交易的最终利润,因为我们还需要考虑交易的燃气成本。这将在接下来的文章中讨论。
我们想要为我们的 MEV 机器人自动计算这个最佳交易规模。这可以通过基本的微积分来完成。我们有一个关于一个变量 x 的函数,我们想要最大化这个函数。当函数的导数为 0 时,函数达到最大值。
可以使用各种免费和在线工具来符号地计算函数的导数,比如 Wolfram Alpha。
我们的毛利润函数的导数如下:
dout2(x)/dx = (a1b1a2b2(1-fee)^2)/(a1b2 + (1-fee)x(b1(1-fee)+b2))^2
由于我们想要找到最大化利润(即 out2(x) - x)的 x 的值,我们需要找到导数为 1(而不是 0)的 x 的值。
使用 Wolfram Alpha,我们得到了下面方程中 x 的解:
dout2(x)/dx = 1
x = (sqrt(a1b1a2b2(1-fee)^4 (b1(1-fee)+b2)^2) - a1b2(1-fee)(b1(1-fee)+b2)) / ((1-fee) (b1(1-fee) + b2))^2
使用上面图中使用的储备值,我们得到 x_optimal = 0.0607203782551,这验证了我们的公式(与图中的值 0.0607 相比)。
虽然这个公式不太易读,但在代码中很容易实现。以下是一个计算 2 次交换输出和最佳交易规模的公式的 Python 实现:
# 计算最佳交易规模的辅助函数
# 单次交换的输出
def swap_output(x, a, b, fee=0.003):
return b * (1 - a/(a + x*(1-fee)))
# 两次连续交换的毛利润
def trade_profit(x, reserves1, reserves2, fee=0.003):
a1, b1 = reserves1
a2, b2 = reserves2
return swap_output(swap_output(x, a1, b1, fee), b2, a2, fee) - x
# 最佳输入金额
def optimal_trade_size(reserves1, reserves2, fee=0.003):
a1, b1 = reserves1
a2, b2 = reserves2
return (math.sqrt(a1*b1*a2*b2*(1-fee)**4 * (b1*(1-fee)+b2)**2) - a1*b2*(1-fee)*(b1*(1-fee)+b2)) / ((1-fee) * (b1*(1-fee) + b2))**2
现在我们知道如何计算在相同币种的任意两个给定池之间的套利机会的毛利润,我们只需迭代所有币种,并测试所有具有相同币种的池。这将为我们提供所有可能套利机会的毛利润,这些套利机会在我们策略的范围内。
要估算交易的净利润,我们需要估算利用给定机会的燃气成本。这可以通过通过 eth_call 对 RPC 节点模拟交易来精确执行,但需要大量时间,并且每个区块只能执行几十个机会。
我们首先通过假设固定的交易燃气成本(实际上是下限)来对燃气成本进行毛估计,并淘汰不足以覆盖燃气成本的机会。然后我们只对剩下的机会进行燃气成本的精确估算。
以下是遍历所有币种和所有池的代码,并按利润排序的代码:
# [...]
# 获取 pool_dict 中每个池的储备
to_fetch = [] # List of pool addresses for which reserves need to be fetched.
for pair, pool_list in pool_dict.items():
for pair_object in pool_list:
to_fetch.append(pair_object["pair"]) # Add the address of the pool
print(f"Fetching reserves of {len(to_fetch)} pools...")
# getReservesParallel() 是 MEV 机器人系列中的第二篇文章中的一部分
reserveList = asyncio.get_event_loop().run_until_complete(getReservesParallel(to_fetch, providersAsync))
#构建交易机会列表
index = 0
opps = []
for pair, pool_list in pool_dict.items():
# Store the reserves in the pool objects for later use
for pair_object in pool_list:
pair_object["reserves"] = reserveList[index]
index += 1
# 遍历该币种的所有池
for poolA in pool_list:
for poolB in pool_list:
# Skip if it's the same pool
if poolA["pair"] == poolB["pair"]:
continue
# 如果储备中有一个为0(除以0),则跳过
if 0 in poolA["reserves"] or 0 in poolB["reserves"]:
continue
# 重新排序储备,以便 WETH 始终是第一个代币
if poolA["token0"] == WETH:
res_A = (poolA["reserves"][0], poolA["reserves"][1])
res_B = (poolB["reserves"][0], poolB["reserves"][1])
else:
res_A = (poolA["reserves"][1], poolA["reserves"][0])
res_B = (poolB["reserves"][1], poolB["reserves"][0])
# 通过公式计算最佳输入值
x = optimal_trade_size(res_A, res_B)
# 如果最佳输入值为负数(池的顺序被颠倒),则跳过
if x < 0:
continue
# 计算毛利润,以 Wei 为单位(未考虑Gas成本)
profit = trade_profit(x, res_A, res_B)
# 存储套利机会的细节。值以 ETH 为单位。(1e18 Wei = 1 ETH)
opps.append({
"profit": profit / 1e18,
"input": x / 1e18,
"pair": pair,
"poolA": poolA,
"poolB": poolB,
})
print(f"Found {len(opps)} opportunities.")
Fetching reserves of 3081 pools.
Found 1791 opportunities.
现在我们有了所有机会的列表。我们只需要估算它们的利润。现在,我们将简单地假设交易机会的燃气成本是恒定的。
我们必须对在 Uniswap V2 上进行交换的燃气成本使用一个下限。经验上,我们发现这个值接近于 43k 燃气。
利用一个机会需要进行 2 次交换,并在以太坊上执行交易的成本是固定的 21k 燃气,总共是每个机会 107k 燃气。
以下是计算每个机会估计净利润的代码:
# [...]
# 使用每个机会 107k Gas 的硬编码 Gas 成本
gp = w3.eth.gas_price
for opp in opps:
opp["net_profit"] = opp["profit"] - 107000 * gp / 1e18
# 按预计净利润排序
opps.sort(key=lambda x: x["net_profit"], reverse=True)
# 保持积极的机会
positive_opps = [opp for opp in opps if opp["net_profit"] > 0]
### 打印统计数据
# 积极的机会很重要
print(f"Found {len(positive_opps)} positive opportunities.")
# 每个机会的详细信息
ETH_价格=1900年 # 你应该动态获取 ETH 的价格
for opp in positive_opps:
print(f"Profit: {opp['net_profit']} ETH (${opp['net_profit'] * ETH_PRICE})")
print(f"Input: {opp['input']} ETH (${opp['input'] * ETH_PRICE})")
print(f"Pool A: {opp['poolA']['pair']}")
print(f"Pool B: {opp['poolB']['pair']}")
print()
这是脚本的输出:
Found 57 positive opportunities.
Profit: 4.936025725859028 ETH ($9378.448879132153)
Input: 1.7958289984719014 ETH ($3412.075097096613)
Pool A: 0x1498bd576454159Bb81B5Ce532692a8752D163e8
Pool B: 0x7D7E813082eF6c143277c71786e5bE626ec77b20
{‘profit’: 4.9374642090282865, ‘input’: 1.7958(…)
Profit: 4.756587769768892 ETH ($9037.516762560894)
Input: 0.32908348765283796 ETH ($625.2586265403921)
Pool A: 0x486c1609f9605fA14C28E311b7D708B0541cd2f5
Pool B: 0x5e81b946b61F3C7F73Bf84dd961dE3A0A78E8c33
{‘profit’: 4.7580262529381505, ‘input’: 0.329(…)
Profit: 0.8147203063054365 ETH ($1547.9685819803292)
Input: 0.6715171730669338 ETH ($1275.8826288271744)
Pool A: 0x1f1B4836Dde1859e2edE1C6155140318EF5931C2
Pool B: 0x1f7efDcD748F43Fc4BeAe6897e5a6DDd865DcceA
{‘profit’: 0.8161587894746954, ‘input’: 0.671(…)
(…)
这些利润看起来确实很高。首先应该采取的步骤是验证代码是否正确。经过仔细检查代码,我们发现代码是正确的。
这些利润是否真实?事实证明并不是。我们在选择应该考虑在策略中的池时涵盖的范围太广,导致我们手中拿到了一些有毒代币的池。
ERC20 代币标准只描述了一个用于互操作性的接口。任何人都可以部署一个实现了这个接口的代币,并选择实现非正统的行为,这正是这里所涉及的情况。
一些代币创建者设计他们的 ERC20 代币,以便这些代币在交易所上只能购买,而不能出售。一些代币合约甚至具有紧急停止机制,允许创建者取消所有用户的资金。
在我们的 MEV 机器人中,这些有毒代币必须被过滤掉。这将在未来的文章中讨论。
如果我们手动过滤掉明显有毒的代币,我们还剩下以下 42 个机会:
Profit: 0.004126583158496902 ETH ($7.840508001144114)
Input: 0.008369804833786892 ETH ($15.902629184195094)
Pool A: 0xdF42388059692150d0A9De836E4171c7B9c09CBf
Pool B: 0xf98fCEB2DC0Fa2B3f32ABccc5e8495E961370B23
{‘profit’: 0.005565066327755902, (…)
Profit: 0.004092580415474992 ETH ($7.775902789402485)
Input: 0.014696360216108083 ETH ($27.92308441060536)
Pool A: 0xfDBFb4239935A15C2C348400570E34De3b044c5F
Pool B: 0x0F15d69a7E5998252ccC39Ad239Cef67fa2a9369
{‘profit’: 0.005531063584733992, (…)
Profit: 0.003693235163284344 ETH ($7.017146810240254)
Input: 0.1392339178514088 ETH ($264.5444439176767)
Pool A: 0x2957215d0473d2c811A075725Da3C31D2af075F1
Pool B: 0xF110783EbD020DCFBA91Cd1976b79a6E510846AA
{‘profit’: 0.005131718332543344, (…)
Profit: 0.003674128918827048 ETH ($6.980844945771391)
Input: 0.2719041848570484 ETH ($516.617951228392)
Pool A: 0xBa19343ff3E9f496F17C7333cdeeD212D65A8425
Pool B: 0xD30567f1d084f411572f202ebb13261CE9F46325
{‘profit’: 0.005112612088086048, (…)
(…)
请注意,通常利润低于执行交易所需的输入金额。
这些利润更为合理。但请记住,它们仍然是最佳情况下的利润,因为我们对每个机会的燃气成本进行了非常粗略的估计。
在未来的文章中,我们将模拟执行我们的交易,以获取每个机会的燃气成本的精确值。
为了模拟执行,我们首先需要开发执行交易的智能合约。这是下一篇文章的主题。
我们现在对我们的 MEV 套利机器人的范围有了明确的定义。
我们已经探讨了套利策略背后的数学理论,并在 Python 中实现了它。
现在我们有了潜在的套利机会列表,我们需要模拟它们的执行,以获取最终的利润值。为此,我们需要准备好我们的交易智能合约。
在下一篇文章中,我们将使用 Solidity 开发这样的智能合约,并模拟我们的第一笔套利交易。
您可以在与本文相关联的 GitHub 存储库中找到完整的代码。该脚本最好在 Jupyter 笔记本中运行。