
Uniswap V3データのクリーニングプロセスを分解する
TechFlow厳選深潮セレクト

Uniswap V3データのクリーニングプロセスを分解する
ユーザーのアドレスの観点から、Uniswap上のユーザー純資産およびリターン率を計算しました。
執筆:Zelos
はじめに
前回は、ユーザーのアドレスの視点から、Uniswapにおける純資産価値(NAV)と収益率を統計しました。今回も同様の目的ですが、各アドレスが保有する現金も含めて、総合的な純資産価値と収益率を求めます。
今回の統計対象は以下の2つのプールです。
-
Polygon上のUSDC-WETH(手数料0.05%)、プールアドレス:0x45dda9cb7c25131df268515131f647d726f50608[1]。これは前回分析でも使用したプールです。
-
Ethereum上のUSDC-ETH(手数料0.05%)、プールアドレス:0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640[2]。このプールはネイティブトークンを含むため、データ処理に若干の複雑さがあります。
最終的に得られるデータは時間単位のものであり、注意してください:各行のデータはその時間帯の最終時点の値を示します。
全体のプロセス
-
Uniswapのデータを取得
-
ユーザーの現金データを取得
-
価格系列(つまりETHの価格)を計算
-
毎分、各ティックで獲得された手数料を取得
-
統計期間内におけるすべてのポジションの一覧を取得
-
アドレスとポジションの対応関係を取得
-
各ポジションの収益率を計算
-
ポジションとアドレスの対応関係に基づき、各ユーザー・アドレスのLPとしての収益率を計算
-
ユーザーの現金とLPを統合し、総合収益率を算出

1. Uniswapのデータ取得
以前、demeter向けにデータソースを提供するために、demeter-fetchというツールを開発しました。このツールは異なるチャネルからUniswapプールのログを取得し、さまざまな形式に解析できます。サポートされているデータソースは以下の通りです。
-
Ethereum RPC:ethクライアントの標準RPCインターフェース。データ取得効率が低く、複数のスレッドを起動する必要があります。
-
Google BigQuery:BigQueryのデータセットからデータをダウンロード。毎日更新されるため、使いやすく安価なのが利点です。
-
Trueblocks chifra:Chifraサービスはチェーン上の取引をスクレイピングし、再構成することで、取引や残高などの情報を簡単にエクスポートできます。ただし、自らノードとサービスを構築する必要があります。
出力フォーマットには以下が含まれます。
-
minute:Uniswapのスワップ取引情報を1分間隔にリサンプリング。バックテスト用。
-
tick:プール内の各取引(スワップおよび流動性操作)を記録。
今回は主にtickデータを取得し、ポジション情報(資金量、毎分の収益、ライフサイクル、保有者など)の統計に使用します。
これらのデータはプールのイベントログ(mint、burn、collect、swapなど)から取得されます。しかし、プールのログにはトークンIDが含まれていないため、どのポジションに対する操作かを特定できません。
実際には、UniswapのLPの権益はNFTによって管理されており、そのNFTの管理者はプロキシコントラクトです。トークンIDはプロキシのイベントログにのみ存在します。そのため、完全なLPポジション情報を得るには、プロキシのイベントログを取得し、プールのイベントログと結合する必要があります。
この取引[3]を例にすると、log index 227と229の2つのログに注目します。これらはそれぞれプールコントラクトが出したmintイベントとプロキシコントラクトが出したIncreaseLiquidityイベントです。両者のamount(すなわち流動性)、amount0、amount1は一致しており、これが関連付けの根拠となります。この2つのログを紐づけることで、当該LPのティックレンジ、流動性、トークンID、および各トークンの金額を得られます。


高度なユーザー、特にファンドなどは、プロキシを経由せず直接プールコントラクトを操作することがあります。このような場合、ポジションにはトークンIDがありません。この場合は、address-LowerTick-UpperTickという形式で仮のIDを作成します。
burnやcollectの場合も同様に、プールのイベントに対応するポジションIDを見つけることができます。ただし、ここに問題があり、2つのイベントの金額が完全に一致しないことがあり、わずかな差異が生じることがあります。例えば次の取引:

amount0とamount1にわずかな差がある場合があります。このようなケースは稀ですが、決して珍しくありません。そのため、burnやcollectのマッチング時には数値に若干の許容誤差を設けています。
次に処理すべき問題は、「誰がこの取引を開始したか」です。撤退(withdrawal)の場合、collectイベントのreceiptをポジションの保有者とみなします。一方、mintについては、poolのmintイベントからsenderを得るしかありません(mintイベント付きの図参照)。
ユーザーがプールコントラクトを直接操作している場合、このsenderはLPプロバイダーになります。しかし、一般のユーザーがプロキシを介して操作している場合、senderはプロキシのアドレスになります(資金が実際にプロキシからプールに転送されるため)。幸いなことに、プロキシにはNFTトークンが生成され、そのNFTトークンは必ずLPプロバイダーに移転されます。したがって、プロキシコントラクト(つまりNFTトークンコントラクト)のtransferイベントを検出することで、mintに対応するLPプロバイダーを特定できます。
また、NFTが譲渡された場合、ポジションの保有者が変化します。これについても統計を行いましたが、そのようなケースはまれです。簡略化のため、mint後のNFTの移転は考慮していません。
2. アドレスの保有現金の取得
この段階の目的は、統計期間中に各アドレスが各時点で保有するトークン数量を取得することです。これを達成するには、以下の2種類のデータが必要です。
-
初期時点でのアドレスの残高
-
統計期間中のアドレスの送金履歴
送金履歴を使って残高を加減することで、各時点の残高を推定できます。
初期時点の残高は、RPCインターフェースで照会可能です。アーカイブノードを使用すれば、パラメータでブロック高を指定して任意の時点の残高を取得できます。ネイティブトークンとERC20の残高の両方でこの方法が利用可能です。
ERC20の送金履歴の取得は比較的簡単で、任意のチャネル(BigQuery、RPC、chifra)から取得可能です。
一方、ETHの送金履歴は取引とtraceから取得する必要があります。取引自体は問題ありませんが、traceの照会と処理には非常に大きな計算量がかかります。幸い、chifraはETH残高のエクスポート機能を提供しており、残高が変更されるたびに記録を出力できます(送金先は記録されませんが、数量の変化だけでも要件を満たせます)。これはコスト面でも最も合理的な方法です。
3. 価格の取得
Uniswapは取引所であり、トークン交換が発生するとswapイベントが生成されます。sqrtPriceX96フィールドからトークン価格を、liquidityフィールドからその時点の総流動性を取得できます。
私たちのプールはいずれもステーブルコインを含んでいるため、ドル建て価格の取得は容易です。ただし、この価格は完全に正確ではありません。まず、取引頻度の影響を受け、swap取引がないと価格が遅延します。また、ステーブルコインがアンカーから外れた場合、実際のドル価格とも乖離します。しかし通常の状況では十分に正確であり、市場研究には問題ありません。
最後に、トークン価格をリサンプリングすることで、毎分の価格リストを得ます。
さらに、eventのliquidityフィールドには現在のプールの総流動性も含まれているため、これを併せて記録します。最終的には以下の表のようになります。

4. 手数料の統計
手数料はポジションの主要な収入源です。ユーザーがプール上でスワップ操作を行うたびに、対応するポジション(現在のティックをlowerとupperの範囲に含むポジション)は一定の手数料を獲得します。収益額は流動性の割合、プールの手数料率、およびティックレンジに依存します。
ユーザーの手数料収入を統計するため、プールが毎分どのティックでいくらのスワップを実行したかを記録します。その後、当該分の当該ティックにおける手数料収益を計算します。

最終的にこのような表が完成します。

この統計方法は、スワップ時に当該ティックの流動性が枯渇するケースを考慮していません。しかし、私たちの統計対象がLP(ティックレンジによる統計)であるため、この誤差はある程度緩和されます。
5. ポジション一覧の取得
ポジション一覧を取得するには、まずポジションの識別子を定義する必要があります。
-
プロキシを通じて投資するLPの場合、各ポジションはNFT(つまりトークンID)を持ち、これがポジションIDとして使用できます。
-
一方、直接プールを操作して投資するLPの場合、
address_LowerTick_UpperTickという形式で仮のIDを付与します。これにより、すべてのポジションに識別子が与えられます。
この識別子を使えば、LPのすべての操作を統合し、ポジションの全ライフサイクルを記述する一覧を作成できます。例:

ただし、今回の統計対象はプール創設時ではなく2023年全年間であるため、2023年1月1日以前の操作が一部のポジションについて取得できない可能性があります。つまり、統計開始時点での流動性量を推定する必要があります。我々は経済的な方法でこれを推定しています。
-
mintとburnの流動性を合計し、数値Lを得る。
-
L > 0(mint > burn)の場合、統計開始前に既に流動性があったとみなし、統計開始時刻(2023年1月1日0:0:0)に補正用のmint操作を追加する。
-
L < 0の場合、統計終了時にも流動性が残っているとみなす。
この方法により、2023年以前のデータをダウンロードせずに済み、コストを節約できます。しかし、沈没流動性(sunk liquidity)の問題に直面します。つまり、LPがその年に一切操作しなければ、そのLPを検出できないということです。しかし、この問題は深刻ではありません。統計期間が1年であるため、ユーザーは通常その期間中にLPを調整すると仮定しています。1年という期間ではETH価格が大きく変動し、LPを調整する理由(ティックレンジの超過、他のDeFiへの資金投入など)が多いためです。したがって、アクティブなユーザーは価格に応じてLPを調整すると考えられ、資金をプールに放置し続けているユーザーは非アクティブとみなし、統計対象外とします。
より厄介なケースは、2023年以前に流動性をmintし、2023年内にいくつかのmint/burn操作を行い、統計終了時にも流動性を完全にburnしていない場合です。この場合、一部の流動性しか統計できず、沈没流動性が手数料の見積もりに影響を与え、収益率が異常になる可能性があります。詳細は後ほど説明します。
最終的な統計では、Polygon上に73,278件、Ethereum上に21,210件のポジションがあり、各チェーンで収益率が異常なのは10件未満であり、この仮定が妥当であることを示しています。
6. アドレスとポジションの対応関係の取得
最終的な統計目標がアドレスの収益であるため、アドレスとポジションの対応関係を取得する必要があります。この関連付けにより、ユーザーの具体的な投資行動を把握できます。
ステップ1で、資金操作(mint/collect)に関連するユーザーを特定する作業を行いました。したがって、mintのsenderとcollectのreceiptを特定すれば、ポジションとアドレスの対応関係を得られます。
7. ポジションの純資産価値と収益率の計算
このステップでは、各ポジションの純資産価値(NAV)を計算し、それから収益率を求めます。
純資産価値(NAV)
ポジションの純資産価値は2つの部分から構成されます。1つはLPの流動性で、これはマーケットメイキングの元本に相当します。ユーザーが資金をポジションに投入した後、流動性の数量は変化しませんが、価格変動により純資産価値は変動します。もう1つは手数料収益で、これは流動性とは独立し、fee0とfee1のフィールドに個別に保存されます。手数料の純資産価値は時間とともに増加します。
したがって、任意の分において、流動性とその分の価格を組み合わせることで、元本部分の純資産価値が得られます。一方、手数料の計算にはステップ4で計算した手数料表が必要です。
まず、このポジションの流動性を、当該分のプールの総流動性で割ったものを分配比率とします。次に、このポジションのティックレンジに含まれるすべてのティックの手数料を合計し、当該分の手数料収益を得ます。
式で表すと:

最後に、fee0とfee1の手数料を合計し、手数料の純資産価値を得ます。これを流動性の純資産価値に加えることで、総純資産価値が得られます。
純資産価値の計算では、mint/burn/collectの取引を基にポジションのライフサイクルを分割します。
-
mint取引が発生したとき、流動性を増加させる。
-
burn取引が発生したとき、流動性を減少させ、流動性の価値を手数料フィールドに折り返す(プールコントラクトのコードも同様に動作)。
-
collect取引が発生したとき、前回collectから現在までの期間を対象に計算をトリガーします。毎分の純資産価値と手数料収入を計算し、リストを作成します。
最後に、各collectで得られた純資産価値のリストを統合し、リサンプリングやその他の統計処理を施して最終結果を得ます。

さらに精度を向上させるために、以下の2点の最適化を行いました。
まず、取引(mint/burn/collect)が発生した時間帯は分単位で統計を行い、取引のない時間帯は時間単位で統計を行います。その後、結果を時間単位にリサンプリングします。
次に、collectイベントでは流動性+手数料の合計値を得られます。したがって、実際にcollectされた値と理論計算値を比較し、理論手数料と実際手数料の差分を得られます(実際にはこの差分にはLP元本の差分も含まれますが、その誤差は極めて小さく、ほぼ0と見なせます)。この手数料差分を各行に補正することで、手数料見積もりの精度を向上させます(前述の表のfee_modify0およびfee_modify1フィールド)。
注意:
-
補正を行う際には、当該時間帯の流動性に応じて手数料の配分を加重平均する必要があります。そうでないと、当該時間帯の手数料が過大評価されることがあります。
-
統計データは2023年全年間のものであり、完全なデータではないため、第5節で述べた「沈没流動性」の現象が発生します。これにより、実際の手数料が理論値よりも大幅に多くなり、収益率が異常に高くなることがあります。
各データ行は当該時間帯の最終時点の値を示すため、完全にクローズしたポジションの純資産価値は0になります。この場合、クローズ時点の純資産価値が失われます。この値を保持するため、ファイル末尾に2038年1月1日00:00:00のデータ行を作成し、ポジションクローズ時の純資産価値などを保存しています。他のプロジェクトの統計ニーズに備えた措置です。
収益率
通常、収益率は初期純資産価値を最終純資産価値で割って計算しますが、ここでは適用できません。理由は以下の通りです。
-
ここで求められる収益率は毎分に細分化される必要がある。
-
ポジションは途中で資金の入出金を行うため、単純に初期と最終の純資産価値を割っただけでは収益状況を正確に反映できない。
問題1に対しては、毎分の純資産価値を前分で割って毎分の収益率を求め、それを累積乗算することで総収益率を得ます。

しかし、このアルゴリズムには重大な問題があります。毎分の収益率のうち1つでも計算ミスがあれば、総収益率に大きな偏りが生じます。統計プロセスが綱渡りのようになり、少しの誤差も許されません。一方で、この性質により、いかなる統計ミスも即座に明らかになります。
問題2に対しては、当該分に資金の入出金がある場合、単純に収益率を割ると極端な値になってしまうため、毎分の収益率アルゴリズムをさらに細分化する必要があります。
最初の試みとして、純資産価値の変化を詳細に分解し、資金の変動を除外することを考えました。純資産価値の変化を以下の要素に分解します。1:価格変動による元本の変化。2:当該分の手数料累計。3:資金の流入流出。明らかに3は統計から除外すべきです。これに対して、以下の計算方法を採用しました。
-
現在の分をn、前の分をn-1とする。
-
当該分のすべての送金操作はn:0.000秒に発生すると仮定する。残りの時間ではLPの純資産価値は変化せず、つまりn:0.001秒の純資産価値はn:59.999秒と同じである。
-
手数料の累計は当該分の末尾、つまりn:59.999秒に発生する。
-
前分の末尾(n-1:59.999)の価格と手数料は、当該分(n:0.000)の開始時点の価格と手数料となる。
以上の仮定に基づき、毎分の収益率は、末尾の流動性/価格/手数料を、末尾の流動性/開始価格/開始手数料で割った値となります。式で表すと以下の通り。fは流動性を純資産価値に換算する関数です。

この方法は一見完璧に見えます。流動性の変化を完全に回避し、価格と手数料の純資産価値への影響を正確に反映します。まさに我々が求めていたものです。しかし、実際には特定の行で極端な収益率が発生することがあります。調査の結果、問題は流動性の引き出し(burn)時に発生していることがわかりました。思い出してください、我々のルールでは各行は当該分/時間の末尾を表します。これはデータ統計に統一的な尺度を提供しますが、各列の意味が異なる点に注意が必要です。
-
純資産価値列は瞬時値、つまり当該分/時間の最終時点の値。
-
一方、手数料列は累計値、つまり当該分/時間内に累計された手数料。
したがって、流動性をburnした時間帯では:
-
LPがburnされ、トークンが移転された場合、当該時間の末尾の純資産価値は0になる。
-
一方、手数料は累計値であるため、当該時間の末尾でも0より大きい値を持つ。
これにより、前述の公式は以下のように退化します。
この現象はポジションのライフサイクル終了時だけでなく、一部の流動性をburnする場合にも発生し、手数料の増加とLPの純資産価値比率に変化をもたらします。
簡略化のため、LPの純資産価値が変化する場合、収益率を1と設定します。これにより収益率の計算結果に誤差が生じますが、正常に継続投資しているポジションにとっては、取引のある時間帯はライフサイクル全体に対してごく少数であるため、影響は限定的です。
8. アドレスのLP総収益の計算
各ポジションの収益率と、ポジションとアドレスの対応関係があれば、ユーザーのアドレスごとの各ポジションの収益率が得られます。
このアルゴリズムは比較的単純です。アドレスが異なる時期に持つポジションを連結します。投資期間のない期間は純資産価値を0、収益率を1とします(前後とも純資産価値が0で変化がないため、収益率は1)。
同一時期に複数のポジションを持つ場合、重複期間の純資産価値を合計し、総純資産価値を得ます。収益率を統合する際には、各ポジションの純資産価値に応じて加重平均します。
9. 現金とLPの総合収益の統合
最後に、ユーザーのアドレスが保有する現金とLP投資の両方を統合すれば、最終結果が得られます。
純資産価値の統合は前のステップ(ポジションの統合)よりさらに単純です。LPの純資産価値の時間範囲を調べ、対応する時間範囲での現金保有量とETH価格を確認すれば、総純資産価値が得られます。
収益率については、毎分の収益率を求めて累積乗算するアルゴリズムを同様に採用します。当初、第7節で述べた誤った収益率アルゴリズムを使用しました。これには当該分の固定部分(現金のcash数量、LPの流動性)と可変部分(価格変動、手数料累計、資金の入出金)を分離する必要があります。ポジションの統計に比べて複雑度がはるかに高く、Uniswapの資金出入りはmintとcollectイベントに注目すれば済みますが、現金の追跡は非常に煩雑です。資金がLPに送られたのか外部に送られたのかを区別する必要があります。LPに送られた場合は元本部分を維持できますが、外部に送られた場合は元本数量を修正する必要があります。これにはERC20およびETHの送金先アドレスを追跡する必要があります。この作業は非常に困難です。まず、mint/collect時に送金先アドレスはプールまたはプロキシのいずれかになります。さらに複雑なのはETHの送金で、ETHはネイティブトークンであるため、一部の送金履歴はtraceでしか確認できません。しかし、traceのデータ量が膨大すぎて、処理能力を超えてしまいます。
最後の決定打となったのは、各行の純資産価値が当該時間の瞬時値である一方、手数料は当該時間の累計値であり、物理的に単純に加算できないことに気づいたことです。この問題は非常に遅れて発覚しました。
そこで、このアルゴリズムを断念し、後ろの分の純資産価値を前の分の純資産価値で割る方法に切り替えました。この方法ははるかに簡単です。しかし、資金の入出金がある場合、依然として不合理的な収益率が発生する問題があります。前述の議論から、資金の流れを分離するのは非常に困難であるため、ここでは精度を犠牲にして、資金移動がある場合の収益率を1と設定します。
残る問題は、「どのようにして当該時間に資金の出入金があったかを識別するか」です。当初のアルゴリズムは単純でした。前時間のトークン残高と現在の価格を使って、もし当該トークンを保有し続けていたら当該時間の純資産価値がいくらになるかを推定し、推定値と実際値の差を取ればよい。差がゼロでなければ資金の出入金があったと判断します。式で表すと:

しかし、このアルゴリズムはUniswap LPの複雑さを無視しています。LPでは、価格変動に伴いトークン数量と純資産価値が変化します。また、この方法は手数料の変化も考慮していません。最終的に推定値と実際値に約0.1%の誤差が生じます。
精度を向上させるため、資金の構成を細分化し、LPの価値変動を個別に計算し、手数料も考慮に入れます。

この方法により、推定値の誤差を0.001%以内に抑えることができます。
また、データの小数点以下桁数を制限し、極端に小さな数字(通常10^-10以下)の除算を避けています。これらの小さな数字は、各種計算およびリサンプリングで蓄積された誤差であり、そのまま除算すると誤差が拡大し、収益率が大きく歪曲されます。
その他の問題
ネイティブトークン
今回の統計では、Ethereum上のUSDC-ETHプールを追加しました。ETHはネイティブトークンであり、特別な処理が必要です。
ETHはDeFiで直接使用できず、WETHに変換する必要があります。したがって、このプールは実際にはUSDC-WETHプールです。プールを直接操作するユーザーは、このプールでWETHを入出金すればよく、通常のプールと同様です。
一方、プロキシを通じてLPを追加するユーザーは、取引のvalueにETHを含めてプロキシコントラクトに送金します。コントラクトはこれらのETHをWETHに交換し、プールに投入します。collectの際、USDCはユーザーに直接送金できますが、ETHは直接送れず、まずプールからプロキシに移動し、プロキシコントラクトがETHに交換した後、内部送金でユーザーに送られます。例はこの取引[4]を参照してください。
したがって、USDC-ETHプールと通常のプールの違いは資金の入出金にのみあり、これはポジションとアドレスの対応付けにのみ影響します。この問題を解決するため、プール創設時からのすべてのNFT送金データを取得し、トークンIDを使って対応するポジションの保有者を特定しました。
欠落するポジション(missing position)
統計中、一部のポジションが最終リストに含まれないことがあります。これらのポジションには何らかの特殊性があります。
その多くはMEV取引です。MEVは純粋な裁定取引であり、通常の投資家ではないため、統計対象外です。また、実際の統計もtraceレベルのデータが必要で非常に困難です。ここでは、開始から終了までの時間が1分未満というシンプルな戦略でMEV取引をフィルタリングしています。実際、私たちのデータの最高精度は1分であるため、存在時間が1分未満のポジションは統計に含まれません。
別の可能性として、collect取引のないポジションがあります。ステップ7からわかるように、収益の計算はcollectイベントによってトリガーされます。collect操作がない場合、それ以前の純資産価値や収益率は計算されません。通常、ユーザーはLPの収益や元本を適宜回収しますが、一部の特殊なユーザーは資産をUniswapプールのfee0やfee1に放置し続けるかもしれません。このようなユーザーも特殊とみなし、統計対象外とします。
TechFlow公式コミュニティへようこそ
Telegram購読グループ:https://t.me/TechFlowDaily
Twitter公式アカウント:https://x.com/TechFlowPost
Twitter英語アカウント:https://x.com/BlockFlow_News










