あなたのMEVセットアップがこれと異なる場合、あなたはngmiです
この記事はアービトラージボットの構築に関するシリーズの一部です。このシリーズの目標は、人気のある分散型取引所でアービトラージの機会を見つけて実行できる自動化されたMEV取引ロボットの構築について、段階的なガイドを提供することです。
この記事では、興味のあるトークンペアの事前選択を行います。その後、同じトークンペアの2つのプール間で最適なアービトラージを見つけるための数式を導出します。最後に、その数式をコードで実装し、潜在的なアービトラージの機会のリストを返します。
アービトラージの機会を探し始める前に、アービトラージボットの範囲を明確に定義する必要があります。具体的には、どのようなアービトラージに対応したいのかを指定します。最も安全なアービトラージの種類は、ETHを含むプール間です。ETHは取引のガスが支払われる資産であるため、常にアービトラージの結果としてETHを持つことを望むのは自然です。しかし、皆がこのように考えたくなる誘惑に駆られます。取引では、時間通りの機会は他の人が行動するほど利益が少なくなりますことを心に留めておいてください。
簡単のため、ETHを含むプール間の裁定機会に焦点を当てます。同じトークンペアの2つのプールの間の機会のみを探します。取引ルートに2つ以上のプールを含む機会(いわゆるマルチホップ機会)には取引しません。この戦略をリスキーなものにアップグレードすることは、ボットの収益性を向上させるために取るべき最初のステップです。
この戦略を改善するためには、例えば一部の在庫をステーブルコインで保持し、ステーブルコインを収益をもたらすアービトラージの機会に活用することができます。同様のことは、必要な注意を払いながらリスクの高いアセット(shitcoins)にも行うことができ、定期的にポートフォリオをETHにリバランスしてガス料金を支払うことができます。
別の方向性は、私たちが行った原子性の暗黙の前提を放棄し、戦略に統計的推論を導入することになります。たとえば、価格が有利に動いたときにプール内のトークンを1つ購入し、後で売却する(平均回帰戦略)。これは、効率的な中央集権的取引所には上場していないシットコインや、上場しているが価格が正しく追跡されていないものに最適です。これにはさらに多くの要素が関与し、本シリーズの範囲外です。
今、私たちがアービトラージボットの範囲を定義したので、取引したいトークンペアを選択する必要があります。ここには、使用する2つの選択基準があります。
再利用ゲートからのコード記事 2: プール価格の効率的な読み取り,ファクトリ契約によって提供されたすべてのトークンペアをリストアップする以下のコードがあります:
# [...]# ファクトリコントラクトのアドレスを読み込むwith open("FactoriesV2.json", "r") as f:factories = json.load(f)# [...]# 各ファクトリコントラクトのプールリストを取得pairDataList = []for factoryName, factoryData in factories.items():events = getPairEvents(w3.eth.contract(address=factoryData['factory'], abi=factory_abi), 0, w3.eth.block_number)print(f'{factoryName} には {len(events)} 個のプールが見つかりました')for e in events: pairDataList.append({ "token0": e["args"]["token0"], "token1": e["args"]["token1"], "pair": e["args"]["pair"], "factory": factoryName })
pairDataListを単に辞書に反転させ、キーがトークンペアで値がこのペアを取引するプールのリストであるようにします。リストをループ処理する際、ETHを含まないペアは無視されます。ループが終了すると、少なくとも2つのプールを持つペアが選択され、少なくとも2つの要素を持つリストに格納されます。
# [...]WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"pair_pool_dict = {}for pair_object in pairDataList:# Check for ETH (WETH) in the pair.pair = (pair_object['token0'], pair_object['token1'])if WETH not in pair: continue# Make sure the pair is referenced in the dictionary. if pair not in pair_pool_dict: pair_pool_dict[pair] = []# Add the pool to the list of pools that trade this pair.pair_pool_dict[pair].append(pair_object)# Create the final dictionnary of pools that will be traded on.pool_dict = {}for pair, pool_list in pair_pool_dict.items():if len(pool_list) >= 2: pool_dict[pair] = pool_list
いくつかの統計情報を印刷すると、作業しているデータをよりよく把握できます:
# 異なるペアの数print(f'異なるペアは{len(pool_dict)}あります。')# 総プール数print(f'合計{sum([len(pool_list) for pool_list in pool_dict.values()])}個のプールがあります。')# 最も多くのプールを持つペアprint(f'最も多くのプールを持つペアは{max(pool_dict, key=lambda k: len(pool_dict[k]))}で、{len(max(pool_dict.values(), key=len))}個のプールがあります。')# ペアごとのプール数の分布、デシルspool_count_list = [len(pool_list) for pool_list in pool_dict.values()]pool_count_list.sort(reverse=True)print(f'ペアごとのプール数、デシル: {pool_count_list[::int(len(pool_count_list)/10)]}')# ペアごとのプール数の分布、パーセンタイル(第1デシルのデシル)pool_count_list.sort(reverse=True)print(f'ペアごとのプール数、パーセンタイル: {pool_count_list[::int(len(pool_count_list)/100)][:10]}')
執筆時点では、これは次のように出力されます:
私たちは1431種類の異なるペアを持っています。
合計で3081のプールを持っています。
最もプールの数が多いペアは('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'、 '0xdAC17F958D2ee523a2206206994597C13D831ec7')で、16個のプールがあります。
ペアごとのプールの数、10分位数で表すと:[16、2、2、2、2、2、2、2、2、2、2]
ペアごとのプール数、パーセンタイルで:[16、5、4、3、3、3、3、3、3、3]
公開RPCノードを使用して3000のプールのリザーブを取得することは、1秒未満で行うことができます。これは合理的な時間です。
今、必要なデータをすべて取得したので、裁定取引の機会を見つけ始める必要があります。
同じペアを取引する2つのプールの間に価格の不一致がある場合、アービトラージの機会があります。ただし、すべての価格の違いが活用可能とは限りません:取引のガスコストは、取引で回収される必要がある最小値を設定し、各プールの流動性は価格の違いから抽出できる価値を制限します。
私たちにアクセス可能な最も利益を上げるアービトラージ機会を見つけるためには、各価格の差から抽出可能な潜在価値を計算し、各プールの準備金/流動性を考慮し、取引のガスコストを見積もる必要があります。
アービトラージ機会が利用されると、入力トークンを購入するプールの価格は下がり、売却するプールの価格は上がります。価格の変動は定数乗積式によって記述されます。
すでに見ました@emileamajar/building-an-arbitrage-bot-automated-market-makers-and-uniswap-2d208215d8c2">article 1 how to compute the output of a swap through a pool, given the reserves of that pool and the input amount.
最適な取引サイズを見つけるためには、まず、いくつかの入力量とスワップに関与する2つのプールの準備金を考慮して、2回の連続したスワップの出力のための公式を見つけます。
最初のスワップの入力がtoken0であり、2番目のスワップの入力がtoken1であると仮定し、最終的にtoken0で出力されるとします。
xを入力量とし、(a1、b1)を最初のプールのリザーブ、(a2、b2)を2番目のプールのリザーブとします。 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)))
第2回のスワップのアウトプットは次のようになります:(リザーブ変数がスワップされたことに注意)
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)))
この関数をプロットすることができますdesmos予約値を選択して、最初のプールが1 ETHと1750 USDC、2番目のプールが1340 USDCと1 ETHをシミュレートするようにすると、次のグラフが得られます。
取引の粗利益のプロットは、入力値の関数としての機能
取引の利益であるトレードの利益から入力金額を差し引いたものであるout2(x) - xを実際にプロットしていることに注意してください。
グラフィック的に、最適な取引量は入力時に0.0607 ETHであり、これにより0.0085 ETHの利益が生じます。この取引を利用するには、契約には少なくとも0.0607 ETHのWETH流動性が必要です。
この0.0085 ETHの利益(この記事を書いている時点で約$16)は、取引の最終利益ではありません。なぜなら、取引のガスコストを考慮する必要があります。この点については、後続の記事で議論されます。
私たちはMEVボットのためにこの最適取引サイズを自動的に計算したいと考えています。これは初等微積分を通じて行うことができます。最大化したい1つの変数xの関数があります。関数は、その導関数が0になるxの値で最大になります。
さまざまな無料でオンラインのツールを使用して、関数の導関数を記号的に計算することができます。Gatewolfram alpha.
私たちの総利益関数の導関数を求める。
Wolfram Alphaを使用すれば、そのような導関数を見つけることは非常に簡単です。数学のスキルに自信がない場合は、手作業でも行うことができます。
Wolfram Alphaは次の導関数を生み出します:
dout2(x)/dx = (a1b1a2b2(1-fee)^2)/(a1b2 + (1-fee)x(b1(1-fee)+b2))^2
利益を最大化するためにxの値を見つけたいので(これはout2(x) - xです)、導関数が1になるxの値を見つける必要があります(0ではなく1)。
Wolfram Alphaは、方程式dout2(x)/dx = 1におけるxの解を次のように出力します:
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)))# 2つの連続したスワップの総利益def trade_profit(x, reserves1, reserves2, fee=0.003): a1, b1 = reserves1a2, b2 = reserves2return swap_output(swap_output(x, a1, b1, fee), b2, a2, fee) - x# 最適な入力量def optimal_trade_size(reserves1, reserves2, fee=0.003):a1, b1 = reserves1a2, b2 = reserves2return (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
Gate
取引の純利益を推定するには、特定の機会を利用するためのガスコストを推定する必要があります。これはRPCノードへのeth_callを介して取引をシミュレートすることで正確に行うことができますが、多くの時間を要し、1ブロックあたり数十の機会にしか行えません。
最初に、固定の取引ガスコスト(実際には下限)を仮定して、ガスコストをカバーするために十分に利益を上げていない機会を排除することによって、ガスコストの粗い見積もりを行います。その後で、残りの機会についてガスコストの正確な見積もりを行います。
ここには、すべてのペアとすべてのプールを通過し、利益によって機会をソートするコードがあります。
# [...] # 各プールのリザーブを取得 in pool_dictto_fetch = [] # リザーブをフェッチする必要があるプールアドレスのリスト pool_dict.items() でpool_listペア:for pair_object in pool_list: to_fetch.append(pair_object["pair"]) # プールのアドレスを追加 print(f"Fetching reserves of {len(to_fetch)} pools...")# getReservesParallel() は MEV ボットの記事 2 からのものです seriesreserveList = asyncio.get_event_loop().run_until_complete(getReservesParallel(to_fetch, providersAsync))# 取引機会のリストを構築index = 0opps = []for pair, pool_list in pool_dict.items():# 準備金を後で使用するためにプールオブジェクトに格納for pair_object in pool_list: pair_object["reserves"] = reserveList[index] index += 1# ペアのすべてのプールを反復処理するfor poolA in pool_list: for poolB in pool_list: # 同じプールの場合はスキップ if poolA["pair"] == poolB["pair"]: continue # リザーブの 1 つが 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の粗利益を計算(ガス代前) 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.")
次の出力を生成します:
3081プールのリザーブを取得しています。
1791の機会を見つけました。
現在、すべての機会のリストを持っています。利益を見積もる必要があります。現時点では、単純に機会での取引のための一定のガスコストを想定することにします。
Uniswap V2のスワップのガスコストには下限値を使用する必要があります。実験的に、この値は約43kガスに近いことがわかりました。
機会を活かすには2回のスワップが必要で、Ethereumで取引を実行するにはフラットな21kガスがかかり、機会ごとに合計107kガスが必要です。
ここには、各機会の見積もり純利益を計算するコードがあります:
# [...]# 1機会あたりのハードコーディングされたガスコストを使用するgp = w3.eth.gas_pricefor 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]
# Positive opportunities countprint(f"{len(positive_opps)}件のポジティブな機会が見つかりました。")# 各機会の詳細 ETH_PRICE = 1900 # ETHの価格を動的に取得する必要がありますfor opp in positive_opps:print(f"利益: {opp['net_profit']} ETH (${opp['net_profit'] * ETH_PRICE})")print(f"入力: {opp['input']} ETH (${opp['input'] * ETH_PRICE})")print(f"プールA: {opp['poolA']['pair']}")print(f"プールB: {opp['poolB']['pair']}")print()
スクリプトの出力はこちらです:
57のポジティブな機会を発見しました。
利益:4.936025725859028 ETH($9378.448879132153)
Input: 1.7958289984719014 ETH ($3412.075097096613)
Pool A: 0x1498bd576454159Bb81B5Ce532692a8752D163e8
Pool B: 0x7D7E813082eF6c143277c71786e5bE626ec77b20
{'profit': 4.9374642090282865, 'input': 1.7958(…)
利益: 4.756587769768892 ETH ($9037.516762560894)
Input: 0.32908348765283796 ETH ($625.2586265403921)
Pool A: 0x486c1609f9605fA14C28E311b7D708B0541cd2f5
Pool B: 0x5e81b946b61F3C7F73Bf84dd961dE3A0A78E8c33
{'profit': 4.7580262529381505, 'input': 0.329(…)
利益: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の機会が残ります。
利益:0.004126583158496902 ETH($7.840508001144114)
Input: 0.008369804833786892 ETH ($15.902629184195094)
Pool A: 0xdF42388059692150d0A9De836E4171c7B9c09CBf
Pool B: 0xf98fCEB2DC0Fa2B3f32ABccc5e8495E961370B23
{'profit': 0.005565066327755902, (...)
利益: 0.004092580415474992 ETH ($7.775902789402485)
Input: 0.014696360216108083 ETH ($27.92308441060536)
Pool A: 0xfDBFb4239935A15C2C348400570E34De3b044c5F
Pool B: 0x0F15d69a7E5998252ccC39Ad239Cef67fa2a9369
{'profit': 0.005531063584733992, (...)}
利益:0.003693235163284344 ETH($7.017146810240254)
Input: 0.1392339178514088 ETH ($264.5444439176767)
Pool A: 0x2957215d0473d2c811A075725Da3C31D2af075F1
プールB:0xF110783EbD020DCFBA91Cd1976b79a6E510846AA
{'profit': 0.005131718332543344, (...)
利益:0.003674128918827048 ETH($6.980844945771391)
Input: 0.2719041848570484 ETH ($516.617951228392)
Pool A: 0xBa19343ff3E9f496F17C7333cdeeD212D65A8425
Pool B: 0xD30567f1d084f411572f202ebb13261CE9F46325
{'profit': 0.005112612088086048, (...)
(…)
一般的に利益が取引を実行するために必要な入力金額よりも低いことに注意してください。
これらの利益ははるかに合理的です。ただし、それでもベストケースシナリオの利益であることを覚えておいてください。なぜなら、各機会のガスコストの非常に粗略な推定値を使用しているからです。
将来の記事では、各機会のガスコストの正確な値を得るために、取引の実行をシミュレートします。
実行をシミュレートするためには、まず取引を実行するスマートコントラクトを開発する必要があります。次の記事のトピックです。
私たちのMEVアービトラージボットの範囲については、今は明確な定義があります。
アービトラージ戦略の背後にある数学理論を探求し、Pythonで実装しました。
今、潜在的な裁定取引のリストを持っており、最終的な利益値を得るためにそれらの実行をシミュレートする必要があります。そのためには、取引スマートコントラクトを準備する必要があります。
次の記事では、Solidityでそのようなスマートコントラクトを開発し、最初の裁定取引をシミュレートします。
完全なコードはGate.ioで見つけることができますこの記事に関連するgithubリポジトリ. スクリプトはJupyterノートブックで実行するのが最適です。
Bagikan
あなたのMEVセットアップがこれと異なる場合、あなたはngmiです
この記事はアービトラージボットの構築に関するシリーズの一部です。このシリーズの目標は、人気のある分散型取引所でアービトラージの機会を見つけて実行できる自動化されたMEV取引ロボットの構築について、段階的なガイドを提供することです。
この記事では、興味のあるトークンペアの事前選択を行います。その後、同じトークンペアの2つのプール間で最適なアービトラージを見つけるための数式を導出します。最後に、その数式をコードで実装し、潜在的なアービトラージの機会のリストを返します。
アービトラージの機会を探し始める前に、アービトラージボットの範囲を明確に定義する必要があります。具体的には、どのようなアービトラージに対応したいのかを指定します。最も安全なアービトラージの種類は、ETHを含むプール間です。ETHは取引のガスが支払われる資産であるため、常にアービトラージの結果としてETHを持つことを望むのは自然です。しかし、皆がこのように考えたくなる誘惑に駆られます。取引では、時間通りの機会は他の人が行動するほど利益が少なくなりますことを心に留めておいてください。
簡単のため、ETHを含むプール間の裁定機会に焦点を当てます。同じトークンペアの2つのプールの間の機会のみを探します。取引ルートに2つ以上のプールを含む機会(いわゆるマルチホップ機会)には取引しません。この戦略をリスキーなものにアップグレードすることは、ボットの収益性を向上させるために取るべき最初のステップです。
この戦略を改善するためには、例えば一部の在庫をステーブルコインで保持し、ステーブルコインを収益をもたらすアービトラージの機会に活用することができます。同様のことは、必要な注意を払いながらリスクの高いアセット(shitcoins)にも行うことができ、定期的にポートフォリオをETHにリバランスしてガス料金を支払うことができます。
別の方向性は、私たちが行った原子性の暗黙の前提を放棄し、戦略に統計的推論を導入することになります。たとえば、価格が有利に動いたときにプール内のトークンを1つ購入し、後で売却する(平均回帰戦略)。これは、効率的な中央集権的取引所には上場していないシットコインや、上場しているが価格が正しく追跡されていないものに最適です。これにはさらに多くの要素が関与し、本シリーズの範囲外です。
今、私たちがアービトラージボットの範囲を定義したので、取引したいトークンペアを選択する必要があります。ここには、使用する2つの選択基準があります。
再利用ゲートからのコード記事 2: プール価格の効率的な読み取り,ファクトリ契約によって提供されたすべてのトークンペアをリストアップする以下のコードがあります:
# [...]# ファクトリコントラクトのアドレスを読み込むwith open("FactoriesV2.json", "r") as f:factories = json.load(f)# [...]# 各ファクトリコントラクトのプールリストを取得pairDataList = []for factoryName, factoryData in factories.items():events = getPairEvents(w3.eth.contract(address=factoryData['factory'], abi=factory_abi), 0, w3.eth.block_number)print(f'{factoryName} には {len(events)} 個のプールが見つかりました')for e in events: pairDataList.append({ "token0": e["args"]["token0"], "token1": e["args"]["token1"], "pair": e["args"]["pair"], "factory": factoryName })
pairDataListを単に辞書に反転させ、キーがトークンペアで値がこのペアを取引するプールのリストであるようにします。リストをループ処理する際、ETHを含まないペアは無視されます。ループが終了すると、少なくとも2つのプールを持つペアが選択され、少なくとも2つの要素を持つリストに格納されます。
# [...]WETH = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"pair_pool_dict = {}for pair_object in pairDataList:# Check for ETH (WETH) in the pair.pair = (pair_object['token0'], pair_object['token1'])if WETH not in pair: continue# Make sure the pair is referenced in the dictionary. if pair not in pair_pool_dict: pair_pool_dict[pair] = []# Add the pool to the list of pools that trade this pair.pair_pool_dict[pair].append(pair_object)# Create the final dictionnary of pools that will be traded on.pool_dict = {}for pair, pool_list in pair_pool_dict.items():if len(pool_list) >= 2: pool_dict[pair] = pool_list
いくつかの統計情報を印刷すると、作業しているデータをよりよく把握できます:
# 異なるペアの数print(f'異なるペアは{len(pool_dict)}あります。')# 総プール数print(f'合計{sum([len(pool_list) for pool_list in pool_dict.values()])}個のプールがあります。')# 最も多くのプールを持つペアprint(f'最も多くのプールを持つペアは{max(pool_dict, key=lambda k: len(pool_dict[k]))}で、{len(max(pool_dict.values(), key=len))}個のプールがあります。')# ペアごとのプール数の分布、デシルspool_count_list = [len(pool_list) for pool_list in pool_dict.values()]pool_count_list.sort(reverse=True)print(f'ペアごとのプール数、デシル: {pool_count_list[::int(len(pool_count_list)/10)]}')# ペアごとのプール数の分布、パーセンタイル(第1デシルのデシル)pool_count_list.sort(reverse=True)print(f'ペアごとのプール数、パーセンタイル: {pool_count_list[::int(len(pool_count_list)/100)][:10]}')
執筆時点では、これは次のように出力されます:
私たちは1431種類の異なるペアを持っています。
合計で3081のプールを持っています。
最もプールの数が多いペアは('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2'、 '0xdAC17F958D2ee523a2206206994597C13D831ec7')で、16個のプールがあります。
ペアごとのプールの数、10分位数で表すと:[16、2、2、2、2、2、2、2、2、2、2]
ペアごとのプール数、パーセンタイルで:[16、5、4、3、3、3、3、3、3、3]
公開RPCノードを使用して3000のプールのリザーブを取得することは、1秒未満で行うことができます。これは合理的な時間です。
今、必要なデータをすべて取得したので、裁定取引の機会を見つけ始める必要があります。
同じペアを取引する2つのプールの間に価格の不一致がある場合、アービトラージの機会があります。ただし、すべての価格の違いが活用可能とは限りません:取引のガスコストは、取引で回収される必要がある最小値を設定し、各プールの流動性は価格の違いから抽出できる価値を制限します。
私たちにアクセス可能な最も利益を上げるアービトラージ機会を見つけるためには、各価格の差から抽出可能な潜在価値を計算し、各プールの準備金/流動性を考慮し、取引のガスコストを見積もる必要があります。
アービトラージ機会が利用されると、入力トークンを購入するプールの価格は下がり、売却するプールの価格は上がります。価格の変動は定数乗積式によって記述されます。
すでに見ました@emileamajar/building-an-arbitrage-bot-automated-market-makers-and-uniswap-2d208215d8c2">article 1 how to compute the output of a swap through a pool, given the reserves of that pool and the input amount.
最適な取引サイズを見つけるためには、まず、いくつかの入力量とスワップに関与する2つのプールの準備金を考慮して、2回の連続したスワップの出力のための公式を見つけます。
最初のスワップの入力がtoken0であり、2番目のスワップの入力がtoken1であると仮定し、最終的にtoken0で出力されるとします。
xを入力量とし、(a1、b1)を最初のプールのリザーブ、(a2、b2)を2番目のプールのリザーブとします。 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)))
第2回のスワップのアウトプットは次のようになります:(リザーブ変数がスワップされたことに注意)
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)))
この関数をプロットすることができますdesmos予約値を選択して、最初のプールが1 ETHと1750 USDC、2番目のプールが1340 USDCと1 ETHをシミュレートするようにすると、次のグラフが得られます。
取引の粗利益のプロットは、入力値の関数としての機能
取引の利益であるトレードの利益から入力金額を差し引いたものであるout2(x) - xを実際にプロットしていることに注意してください。
グラフィック的に、最適な取引量は入力時に0.0607 ETHであり、これにより0.0085 ETHの利益が生じます。この取引を利用するには、契約には少なくとも0.0607 ETHのWETH流動性が必要です。
この0.0085 ETHの利益(この記事を書いている時点で約$16)は、取引の最終利益ではありません。なぜなら、取引のガスコストを考慮する必要があります。この点については、後続の記事で議論されます。
私たちはMEVボットのためにこの最適取引サイズを自動的に計算したいと考えています。これは初等微積分を通じて行うことができます。最大化したい1つの変数xの関数があります。関数は、その導関数が0になるxの値で最大になります。
さまざまな無料でオンラインのツールを使用して、関数の導関数を記号的に計算することができます。Gatewolfram alpha.
私たちの総利益関数の導関数を求める。
Wolfram Alphaを使用すれば、そのような導関数を見つけることは非常に簡単です。数学のスキルに自信がない場合は、手作業でも行うことができます。
Wolfram Alphaは次の導関数を生み出します:
dout2(x)/dx = (a1b1a2b2(1-fee)^2)/(a1b2 + (1-fee)x(b1(1-fee)+b2))^2
利益を最大化するためにxの値を見つけたいので(これはout2(x) - xです)、導関数が1になるxの値を見つける必要があります(0ではなく1)。
Wolfram Alphaは、方程式dout2(x)/dx = 1におけるxの解を次のように出力します:
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)))# 2つの連続したスワップの総利益def trade_profit(x, reserves1, reserves2, fee=0.003): a1, b1 = reserves1a2, b2 = reserves2return swap_output(swap_output(x, a1, b1, fee), b2, a2, fee) - x# 最適な入力量def optimal_trade_size(reserves1, reserves2, fee=0.003):a1, b1 = reserves1a2, b2 = reserves2return (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
Gate
取引の純利益を推定するには、特定の機会を利用するためのガスコストを推定する必要があります。これはRPCノードへのeth_callを介して取引をシミュレートすることで正確に行うことができますが、多くの時間を要し、1ブロックあたり数十の機会にしか行えません。
最初に、固定の取引ガスコスト(実際には下限)を仮定して、ガスコストをカバーするために十分に利益を上げていない機会を排除することによって、ガスコストの粗い見積もりを行います。その後で、残りの機会についてガスコストの正確な見積もりを行います。
ここには、すべてのペアとすべてのプールを通過し、利益によって機会をソートするコードがあります。
# [...] # 各プールのリザーブを取得 in pool_dictto_fetch = [] # リザーブをフェッチする必要があるプールアドレスのリスト pool_dict.items() でpool_listペア:for pair_object in pool_list: to_fetch.append(pair_object["pair"]) # プールのアドレスを追加 print(f"Fetching reserves of {len(to_fetch)} pools...")# getReservesParallel() は MEV ボットの記事 2 からのものです seriesreserveList = asyncio.get_event_loop().run_until_complete(getReservesParallel(to_fetch, providersAsync))# 取引機会のリストを構築index = 0opps = []for pair, pool_list in pool_dict.items():# 準備金を後で使用するためにプールオブジェクトに格納for pair_object in pool_list: pair_object["reserves"] = reserveList[index] index += 1# ペアのすべてのプールを反復処理するfor poolA in pool_list: for poolB in pool_list: # 同じプールの場合はスキップ if poolA["pair"] == poolB["pair"]: continue # リザーブの 1 つが 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の粗利益を計算(ガス代前) 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.")
次の出力を生成します:
3081プールのリザーブを取得しています。
1791の機会を見つけました。
現在、すべての機会のリストを持っています。利益を見積もる必要があります。現時点では、単純に機会での取引のための一定のガスコストを想定することにします。
Uniswap V2のスワップのガスコストには下限値を使用する必要があります。実験的に、この値は約43kガスに近いことがわかりました。
機会を活かすには2回のスワップが必要で、Ethereumで取引を実行するにはフラットな21kガスがかかり、機会ごとに合計107kガスが必要です。
ここには、各機会の見積もり純利益を計算するコードがあります:
# [...]# 1機会あたりのハードコーディングされたガスコストを使用するgp = w3.eth.gas_pricefor 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]
# Positive opportunities countprint(f"{len(positive_opps)}件のポジティブな機会が見つかりました。")# 各機会の詳細 ETH_PRICE = 1900 # ETHの価格を動的に取得する必要がありますfor opp in positive_opps:print(f"利益: {opp['net_profit']} ETH (${opp['net_profit'] * ETH_PRICE})")print(f"入力: {opp['input']} ETH (${opp['input'] * ETH_PRICE})")print(f"プールA: {opp['poolA']['pair']}")print(f"プールB: {opp['poolB']['pair']}")print()
スクリプトの出力はこちらです:
57のポジティブな機会を発見しました。
利益:4.936025725859028 ETH($9378.448879132153)
Input: 1.7958289984719014 ETH ($3412.075097096613)
Pool A: 0x1498bd576454159Bb81B5Ce532692a8752D163e8
Pool B: 0x7D7E813082eF6c143277c71786e5bE626ec77b20
{'profit': 4.9374642090282865, 'input': 1.7958(…)
利益: 4.756587769768892 ETH ($9037.516762560894)
Input: 0.32908348765283796 ETH ($625.2586265403921)
Pool A: 0x486c1609f9605fA14C28E311b7D708B0541cd2f5
Pool B: 0x5e81b946b61F3C7F73Bf84dd961dE3A0A78E8c33
{'profit': 4.7580262529381505, 'input': 0.329(…)
利益: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の機会が残ります。
利益:0.004126583158496902 ETH($7.840508001144114)
Input: 0.008369804833786892 ETH ($15.902629184195094)
Pool A: 0xdF42388059692150d0A9De836E4171c7B9c09CBf
Pool B: 0xf98fCEB2DC0Fa2B3f32ABccc5e8495E961370B23
{'profit': 0.005565066327755902, (...)
利益: 0.004092580415474992 ETH ($7.775902789402485)
Input: 0.014696360216108083 ETH ($27.92308441060536)
Pool A: 0xfDBFb4239935A15C2C348400570E34De3b044c5F
Pool B: 0x0F15d69a7E5998252ccC39Ad239Cef67fa2a9369
{'profit': 0.005531063584733992, (...)}
利益:0.003693235163284344 ETH($7.017146810240254)
Input: 0.1392339178514088 ETH ($264.5444439176767)
Pool A: 0x2957215d0473d2c811A075725Da3C31D2af075F1
プールB:0xF110783EbD020DCFBA91Cd1976b79a6E510846AA
{'profit': 0.005131718332543344, (...)
利益:0.003674128918827048 ETH($6.980844945771391)
Input: 0.2719041848570484 ETH ($516.617951228392)
Pool A: 0xBa19343ff3E9f496F17C7333cdeeD212D65A8425
Pool B: 0xD30567f1d084f411572f202ebb13261CE9F46325
{'profit': 0.005112612088086048, (...)
(…)
一般的に利益が取引を実行するために必要な入力金額よりも低いことに注意してください。
これらの利益ははるかに合理的です。ただし、それでもベストケースシナリオの利益であることを覚えておいてください。なぜなら、各機会のガスコストの非常に粗略な推定値を使用しているからです。
将来の記事では、各機会のガスコストの正確な値を得るために、取引の実行をシミュレートします。
実行をシミュレートするためには、まず取引を実行するスマートコントラクトを開発する必要があります。次の記事のトピックです。
私たちのMEVアービトラージボットの範囲については、今は明確な定義があります。
アービトラージ戦略の背後にある数学理論を探求し、Pythonで実装しました。
今、潜在的な裁定取引のリストを持っており、最終的な利益値を得るためにそれらの実行をシミュレートする必要があります。そのためには、取引スマートコントラクトを準備する必要があります。
次の記事では、Solidityでそのようなスマートコントラクトを開発し、最初の裁定取引をシミュレートします。
完全なコードはGate.ioで見つけることができますこの記事に関連するgithubリポジトリ. スクリプトはJupyterノートブックで実行するのが最適です。