The Real 3pool

Introduction

I am a visual learner. Especially when it comes to data. Seeing shapes and colors helps me understand what is going on with a dataset as mathematics are being applied to it. I wanted to apply this to liquidity pools on Uniswap as a way of understanding how the liquidity in these pools behave at a fundamental level and to eventually test a hypothesis that I cannot answer without a visual aid. Uniswap pool visualizations are nothing new; graphs like the below are quite common:

But uniswap is in version 3, right? So why not map the liquidity represented by these curves into three dimensions? For my purposes I realized this is exactly what must be done.

Background - Concentrated Liquidity

Uniswap v3 introduced concentrated liquidity. In a normal v1 or v2 pool, liquidity exists within the price space {0,∞}. Because the price of an asset tends to not fluctuate across this entire range, only a small fraction of liquidity is used at any given time. V3 allows for liquidity providers to concentrate liquidity within predefined ranges. This greatly increases overall capital efficiency, reduces price slippage, and introduces new complexities for users. Active liquidity management has become something of a cottage industry, with a number of protocols providing yield via active management strategies.

Why I’m Interested

In order to better understand these pools a visual representation in three dimensions is required (at least for me). In this article I will demonstrate how I got the data, how to map the normal two dimensional representations to three, and what I intend to do with these data.

Part 1 - Get Data

Uniswap pools are hosted on-chain. Pulling on-chain data about smart contract state is by far the easiest if done via The Graph. Uniswap, thankfully, has a number of subgraphs deployed that index the contents of the thousands of pools at every block. So, naturally, being a lover of The Graph, I opted for this as my first approach. Even If I needed to pay for queries on mainnet, I was ok with doing so if need be. The official Uniswap V3 subgraph was my first stop back in April 2022. Sadly, however, the subgraph at that time was out of sync! As of today, about one month later, the same is true. As for subgraphs on the hosted service, which The Graph still maintains, I had no options. Messari’s standardized subgraph is nice (and I will be using it for other projects) but does not provide the granularity I needed so was not usable. What I am searching for is not just general information about a certain pool, such as price and TVL, but the data on each and every individual position within the pool. So, I continued the search elsewhere.

After searching I came across the Mellow Protocol. The project seems like your standard yield tool; the protocol gives users yield by using their funds to execute a number of strategies they have developed. Earning yield, however, was not why Mellow was a good find. Instead what drew me in was their python SDK, which comes along with access to an S3 bucket with a bunch of raw UniV3 pool data. They provide this, to the best of my knowledge, to give developers of strategies a way to backtest and visualize the performance of their strategy. It also happens to be exactly the data I want. Sadly, the only tokens with any data are WETH, WBTC, stETH and some stables. Given the size of these datasets it is understandable why they do not provide free access to many more tokens, as this could quickly become very expensive.

The dataset as it comes is actually much more information than I need; it contains a record of all burns, mints (of position NFTs) as well as swaps. So to clean the data I filtered these unnecessary parts.

Part 1.5 - 3D

To get these data into a 3d form I need to perform a mapping of the data onto a coordinate system that I want. Normally, pools are represented with the amount of token0 as the x-axis, and the amount of token1 as the y-axis. This is a nice way to show a pool because then a function drawn on the graph can represent the price of asset 1. In my system I remap to a coordinate system where x = ticks, y = amount0 and z = amount1. A tick in Uniswap V3 is a unit within price-space where each tick equates to a 1 basis point change in the price quoted by the pool for asset1. In order to map to three dimensions I wrote a (very messy) algorithm to do the following:

  • Traverse all rows points in the downloaded data
  • If the row is not of the desired type, discard.
  • Else,
    • traverse the tick range of this row (each position has an upper and lower tick value, between which the liquidity is concentrated)
    • add the liquidity of token0 (in token0 units) to a dictionary at dict[currentTick][token0]
    • repeat for token1
  • end

Here it is implemented in Python. Note: to run this you need to first set up the Mellow finance virtual environment and install all required dependencies. I also use pandas here for convenience.

# the code for this (takes awhile to run; not optimized at all)
from mellow_sdk.data import RawDataUniV3
from mellow_sdk.primitives import Pool, Token, Fee
import pandas as pd

# returns a dataframe
def download_data(t0, t1):
    pool = Pool(t0, t1, Fee.MIDDLE)
    data = RawDataUniV3(pool, 'data', reload_data=False).load_from_folder()
    return data.full_df

# returns the cleaned dataframe
def filter_data(df):
    surface_df = df[['tx_hash', 'owner', 'timestamp', 'amount0', 'amount1', 'event', 'tick_lower', 'tick_upper']]
    surface_df = surface_df.to_pandas()
    surface_df.set_index('tx_hash',drop=True,inplace=True)
    surface_df = surface_df[(surface_df['event'] == 'mint') | (surface_df['event'] == 'burn')]
    surface_df['tick_lower'] = surface_df['tick_lower'].astype(int)
    surface_df['tick_upper'] = surface_df['tick_upper'].astype(int)
    result = {}
    for key, row in surface_df.iterrows():
        for i in range(row['tick_lower'],row['tick_upper']):
            try:
                if result[i] != None:
                    result[i]['a0'] += row['amount0']
                    result[i]['a1'] += row['amount1']
            except KeyError:
                liquidity_point = {'a0':row['amount0'], 'a1':row['amount1']}
                result[i] = liquidity_point
    df_map = pd.DataFrame(result)
    df_map = df_map.transpose()
    df_map.reset_index(inplace=True)

The result of doing this, after a few minutes, is a dictionary that is indexed by tick (our x axis) with the total amount of liquidity, across all providers, at each tick. I exported this to a csv file with about 1.7 million rows.

Part 2 - Visualizing

I naively assumed that I could just pipe these data into matplotlib quickly and have a nice looking graph made for me all within python. I found out this is possible most of the time, but 1.7 million data points is simply too much for the system to handle. Matplotlib in fact performs best, I learned with less than 500,000 datapoints (thanks stack overflow!). This is because ultimately matplotlib is not a big data software package. It was never made for stresses like this, even if your computer can handle it. So I turned to more serious (I think) scientific software. Typically there is a poorly documented, open source package from the 90s that has terrible UX that can get the job done (if you can get it to compile). So I went to stack overflow again and found someone was asking the same question. Thankfully a number of solutions were provided and I settled on VisIt, a more advanced visualization software than what I had been trying to use before. Since my data were already in csv format I decided to just use the GUI version to quickly see what I was working with. With some experimentation and my computer crashing on me a handful of times I finally got it to work.

Part 2.5 - The Result

First impression of the WBTC-WETH pool is that it looks like what I would expect: Really flat for a long time, then a sudden spike upwards in the middle which must represent the current price of the asset being traded. Makes sense for this pool since BTC-ETH tend to trade in a range due to their mutual correlation.

BTC per ETH (notice it hasn't fluctuated much in ETH terms) (source: Spookyswap)
BTC per ETH (notice it hasn't fluctuated much in ETH terms) (source: Spookyswap)
excuse the red square
excuse the red square

From far away it looks like how one would expect, but zooming in yielded some more interesting information. When looked at from the other direction, it becomes clear there is a sort of loop at work:

You can reduce the plot to two dimensions to see it better:

For the pair USDC/WETH, which is far more volatile than ETH/BTC, the looping effect is far more pronounced. In fact, it forms almost an infinity symbol by doubling back on itself. What was also interesting about this one compared with WETH/WBTC was that the tails of the pool were far shorter. So much shorter, in fact, that zooming in to capture the screenshot with the somewhat touchy zoom function in this program became something of a challenge:

It is important to remember that inside this space that I have defined, each dot does not represent a single LP position but rather what the token0 and token1 balances are for any given tick. So, the transform that has taken place remaps our thinking from user terms to tick terms. And makes a pretty neat looking graph. Ultimately I hope to explore more deeply what causes different types of shapes and eventually have a comprehensive collection of various liquidity pools, from high to low liquidity, to understand what these geometries mean.

Conclusion and Future Work

My hypothesis, which inspired doing this in the first place, was the following: Liquidity pools in Uniswap v3 can be represented as three dimensional surfaces in tick-liquidity space. Trading volume can then be represented as a collection of vectors that are orthogonal to the tick axis (the tick value of a swap is undefined) moving through the surface of liquidity, generating a flux F. The enclosed loop shape of the pool seems indicative of this happening, but I still need to find another way to represent the volume in this software. Then, F should correspond to some property of the liquidity pool. Flux mathematically in this case would be the surface integral of the volume function V over the surface of liquidity. Qualitatively, Flux should be related in some way to the utilization or efficiency of the liquidity pool. In other words, if all the flux passes in and out of a tiny loop, the pool is efficient, and else it is inefficient. Is any of this true? No idea. Next time, I will attempt to model the flux and see what happens.

DISCLAIMER: Nothing about this article is an endorsement of Mellow Protocol.

Subscribe to Christian
Receive the latest updates directly to your inbox.
Verification
This entry has been permanently stored onchain and signed by its creator.