如果你的 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 筆記本中運行。