Liquidity analysis#

This is an example how to analyse Automated Market Market liquidity on a blockchain. It is similar of order book depth analysis on centralised exchanges.

We will

  • Download pair and exchange map (“the trading universe”)

  • Plot a liquidity of a single trading pair on SushiSwap

  • Compare the historical liquidity of all pairs on SushiSwap and Uniswap v2

  • Calculate the slippage for a hypotethical trade that would have happened at a specific timepoint

About liquidity data collection on AMM exchanges#

Trading Strategy maintains the data of Uniswap v2 style, x*y = k bonding curve, liquidity in a similar format as it maintains OHLC price data. In more traditional finance, the equivalent analysis would be an order book depth - how much you can buy or sell and how much it would move the price.

For each time bucket you get the available liquidity

  • At open (start of the time bucket period)

  • At close (end of the time bucket period)

  • At peak (high of the time bucket period)

  • At bottom (low of the time bucket period)

The liquidity is expressed as the US dollar depth of the one side of the pool. E.g. the liquidity of a pool with 50 USDC and 0.001 ETH would be 50 USD.

There is also something very swap liquidity pool specific measurements

  • Added and removed liquidity, as volume and as number of transactions

The liquidity data can be very effectively used to analyse or predict the trading fees of a swap.

Read API documentation

[ ]:

Getting started#

First, let’s create Trading Strategy dataset client.

[1]:
from tradingstrategy.client import Client

client = Client.create_jupyter_client()
Started Trading Strategy in Jupyter notebook environment, configuration is stored in /Users/moo/.tradingstrategy

Fetching datasets#

Get the map of exchanges and pairs we are working on

[2]:
from pyarrow import Table
from tradingstrategy.exchange import ExchangeUniverse
from tradingstrategy.pair import PandasPairUniverse
from tradingstrategy.timebucket import TimeBucket
from tradingstrategy.liquidity import GroupedLiquidityUniverse

# Exchange map data is so small it does not need any decompression
exchange_universe: ExchangeUniverse = client.fetch_exchange_universe()

# Fetch all trading pairs across all exchanges
pair_table: Table = client.fetch_pair_universe()
pair_universe = PandasPairUniverse(pair_table.to_pandas())

# GroupedLiquidityUniverse is a helper class that
# encapsulates Pandas grouped array
liquidity_table: Table = client.fetch_all_liquidity_samples(TimeBucket.d1)
liquidity_universe = GroupedLiquidityUniverse(liquidity_table.to_pandas())

Single pair liquidity#

Here we first narrow down our data to single trading

[3]:
from tradingstrategy.chain import ChainId
from tradingstrategy.pair import DEXPair

# Filter down to pairs that only trade on Sushiswap
sushi_swap = exchange_universe.get_by_chain_and_slug(ChainId.ethereum, "sushi")

pair: DEXPair = pair_universe.get_one_pair_from_pandas_universe(
    sushi_swap.exchange_id,
    "WETH",
    "USDC")

eth_usdc_liquidity = liquidity_universe.get_liquidity_samples_by_pair(pair.pair_id)

Let’s create a table to explore the liquidity for a specific month

[4]:
import datetime

start = datetime.datetime(2020, 10, 1)
end = datetime.datetime(2020, 11, 1)

df = eth_usdc_liquidity[["open", "high", "low", "close"]]

def format(x):
    return "${:.1f}M".format(x / 1_000_000)

df = df.applymap(format)

df[start:end]
[4]:
open high low close
timestamp
2020-10-01 $24.1M $28.3M $13.2M $14.6M
2020-10-02 $14.5M $23.2M $14.5M $23.1M
2020-10-03 $23.1M $23.4M $22.3M $22.7M
2020-10-04 $22.7M $22.8M $22.1M $22.3M
2020-10-05 $22.3M $22.4M $16.9M $17.1M
2020-10-06 $17.1M $17.7M $16.2M $16.7M
2020-10-07 $16.7M $16.7M $15.1M $16.3M
2020-10-08 $16.4M $16.8M $14.7M $15.0M
2020-10-09 $15.0M $19.5M $15.0M $19.4M
2020-10-10 $19.4M $20.4M $19.4M $19.7M
2020-10-11 $19.7M $20.0M $19.5M $19.6M
2020-10-12 $19.6M $20.3M $19.3M $19.5M
2020-10-13 $19.5M $20.3M $18.0M $18.3M
2020-10-14 $18.3M $19.1M $18.3M $19.1M
2020-10-15 $19.1M $19.8M $19.0M $19.7M
2020-10-16 $19.7M $19.9M $19.4M $19.6M
2020-10-17 $19.6M $19.8M $19.2M $19.8M
2020-10-18 $19.8M $20.9M $19.8M $20.9M
2020-10-19 $20.9M $21.5M $20.8M $20.8M
2020-10-20 $20.8M $20.8M $20.0M $20.0M
2020-10-21 $20.0M $20.8M $20.0M $20.6M
2020-10-22 $20.7M $21.2M $20.0M $20.8M
2020-10-23 $20.8M $21.0M $11.6M $15.4M
2020-10-24 $15.4M $16.6M $15.4M $16.5M
2020-10-25 $16.5M $16.6M $16.4M $16.4M
2020-10-26 $16.4M $19.5M $16.3M $19.4M
2020-10-27 $19.5M $20.1M $19.2M $19.8M
2020-10-28 $19.7M $19.9M $18.2M $18.4M
2020-10-29 $18.4M $18.5M $17.9M $18.0M
2020-10-30 $18.0M $18.0M $17.7M $17.9M
2020-10-31 $17.9M $18.2M $17.8M $18.1M
2020-11-01 $18.1M $18.3M $18.1M $18.3M

Now, let’s plot how the liquidity has developed over the time. For OHLC liquidity values, we use high (peak) liquidity.

We also cut off the launch of the SushiSwap away from the time series, because the launch date was special due to SushiSwap’s vampire attack on Uniswap liquidity (100x more liquidity available).

[5]:
df = eth_usdc_liquidity[["close"]]

# Convert to millions
df["close"] = df["close"] / 1_000_00
/var/folders/12/pbc59svn70q_9dfz1kjl3zww0000gn/T/ipykernel_82533/112040287.py:4: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame.
Try using .loc[row_indexer,col_indexer] = value instead

See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
  df["close"] = df["close"] / 1_000_00

Let’s plot the liquidity using Pandas internal plotting functions for Jupyter notebook

[6]:
from tradingstrategy.frameworks.matplotlib import render_figure_in_docs

axes_subplot = df.plot(title="ETH-USDC liquidity on SushiSwap, all-time")
axes_subplot.set_xlabel("Day")
axes_subplot.set_ylabel("Liquidity (millions of  dollars)")

axes_subplot
[6]:
<AxesSubplot: title={'center': 'ETH-USDC liquidity on SushiSwap, all-time'}, xlabel='Day', ylabel='Liquidity (millions of  dollars)'>
../../_images/programming_code-examples_liquidity-analysis_14_1.png

Comparing liquidity across exchanges#

In this example, we compare the liquidity across Uniswap and Sushiswap for WETH-AAVE pair. We see that AAVE traders prefer Sushiswap.

[7]:
import pandas as pd

uniswap_v2 = exchange_universe.get_by_chain_and_slug(ChainId.ethereum, "uniswap-v2")
sushi_swap = exchange_universe.get_by_chain_and_slug(ChainId.ethereum, "sushi")

pair1: DEXPair = pair_universe.get_one_pair_from_pandas_universe(
    sushi_swap.exchange_id,
    "AAVE",
    "WETH")

# Uniswap has fake listings for AAVE-WETH, and
# pick_by_highest_vol=True will work around this by
# using the highest volume pair of the same name.
# Usually the real pair has the highest volume and
# scam tokens have ~0 volume.
pair2: DEXPair = pair_universe.get_one_pair_from_pandas_universe(
    uniswap_v2.exchange_id,
    "AAVE",
    "WETH",
    pick_by_highest_vol=True)

liq1 = liquidity_universe.get_liquidity_samples_by_pair(pair1.pair_id)
liq2 = liquidity_universe.get_liquidity_samples_by_pair(pair2.pair_id)

Now construct a chart that shows the both liquidities, stacked.

[8]:
# Scale liquidity candles on the both series to $1M
sushi = liq1[["close"]] / 1_000_000
uni = liq2[["close"]] / 1_000_000

# Merge using timestamp index
df = pd.merge_ordered(sushi, uni, fill_method="ffill", on="timestamp")
df = df.set_index("timestamp")
df = df.rename(columns={"close_x": "Sushi", "close_y": "Uni"})

axes_subplot = df.plot.area(title="AAVE-WETH liquidity, all-time")
axes_subplot.set_xlabel("Day")
axes_subplot.set_ylabel("Liquidity (millions of  dollars)")

axes_subplot

[8]:
<AxesSubplot: title={'center': 'AAVE-WETH liquidity, all-time'}, xlabel='Day', ylabel='Liquidity (millions of  dollars)'>
../../_images/programming_code-examples_liquidity-analysis_18_1.png