Uniswap v3 OHCLV candle price analysis

In this notebook we will show how to download price events from Uniswap V3 to your computer as CSV files and use them to analyse price in each pool. This will generate an OHLCV price chart.

  • The notebook will fetch price data for all pairs on Uniswap v3 Ethereum which traded at a specific period, or a block range.

  • Running this notebook will do ~1M API requests on your JSON-RPC provider. You can reduce the scanned block range to decrease the number of API requests.

  • Running this notebook will be slow (hours), depending how high quality your JSON-RPC provider is. With a local JSON-RPC node it completes in few minutes.

Prerequisites

Running

Run the notebook in Visual Studio Code or similar.

To run from the command line using IPython command:

ipython docs/source/tutorials/uniswap-v3-price-analysis.ipynb

To reset the scan state delete /tmp/uniswap-v3-price-scan.json and related CSV files:

rm /tmp/uniswap-v3-*

Extracting and transforming the data

This will extract Uniswap v3 events, save them in temporary CSV files, and then transforms them to OHLCV data.

Download the raw data from Ethereum blockchain

You can sign up for free access to an Ethereum node, see ethereumnodes.com for details. If you run your local run your own Ethereum full node node with transaction receipts retained, the speed up of fetching data is 100x - 1000x.

[10]:
from eth_defi.provider.multi_provider import create_multi_provider_web3
import os
from web3 import Web3, HTTPProvider

# Get your node JSON-RPC URL
# interactively when you run the notebook.
# The actual UI prompt will depend on your environment (Visual Studio Code, Jupyter Notebook, etc.).
# If you are running from command line you can also pass this as JSON_RPC_ETHEREUM environment
# variable
json_rpc_url = os.environ.get("JSON_RPC_ETHEREUM")
if not json_rpc_url:
    json_rpc_url = input("Please enter your Ethereum mainnet JSON-RPC URL here: ")
web3 = create_multi_provider_web3(json_rpc_url)

# Configure logging for diagnostics if needed
#import logging
#import sys
#logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)

As an example, here we download raw events from first few blocks after Uniswap V3 was deployed. The events will be stored in several CSV files at /tmp folder.

Depends on your internet connection and latency to the Ethereum node, the scan might take hours. However it can resume in case there is a crash, as we save the last scanned block in a JSON state file.

See the source code of fetch_events_to_csv.

Note: Progress bar might be slow to update at the start.

[11]:
from eth_defi.uniswap_v3.constants import UNISWAP_V3_FACTORY_CREATED_AT_BLOCK
from eth_defi.uniswap_v3.events import fetch_events_to_csv
from eth_defi.event_reader.json_state import JSONFileScanState

# Take a snapshot of 250,000 blocks after Uniswap v3 deployment
start_block = UNISWAP_V3_FACTORY_CREATED_AT_BLOCK
end_block = UNISWAP_V3_FACTORY_CREATED_AT_BLOCK + 250_000

# Stores the last block number of event data we store
state = JSONFileScanState("/tmp/uniswap-v3-price-scan.json")

print(f"Data snapshot range set to {start_block:,} - {end_block:,}")

# Load the events and write them into a CSV file.
# Several different CSV files are created,
# each for one event type: swap, pool created, mint, burn
web3 = fetch_events_to_csv(
    json_rpc_url,
    state,
    start_block=start_block,
    end_block=end_block,
    output_folder="/tmp",
    # Configure depending on what's eth_getLogs
    # limit of your JSON-RPC provider and also
    # how often you want to see progress bar updates
    max_blocks_once=222,
    # Do reading and decoding in parallel threads
    max_threads=8,
)


Data snapshot range set to 12,369,621 - 12,619,621
Restored previous scan state, data until block 12,619,593, we are skipping 249,972 blocks out of 250,000 total
Saving Uniswap v3 data for block range 12,619,593 - 12,619,621
Wrote 0 PoolCreated events to /tmp/uniswap-v3-poolcreated.csv
Wrote 226 Swap events to /tmp/uniswap-v3-swap.csv
Wrote 9 Mint events to /tmp/uniswap-v3-mint.csv
Wrote 8 Burn events to /tmp/uniswap-v3-burn.csv

JSON-RPC API usage

Show how many API calls we made to our JSON-RPC provider.

It’s important to follow your API usage counts, as most commercial vendors charge or limit you be the API request count.

[12]:
import pandas as pd

api_call_counts = web3.get_api_call_counts()

data = [(k, v) for k, v in api_call_counts.items()]

df = pd.DataFrame(data, columns=["Endpoint", "Number of JSON-RPC API calls",])
df = df.set_index("Endpoint")

display(df)
Number of JSON-RPC API calls
Endpoint
eth_chainId 1

Analysing Uniswap v3 price data

In Uniswap V3, you can get the current price of any pool from any given moment using swap events.

[13]:
swap_df = pd.read_csv("/tmp/uniswap-v3-swap.csv")

print(f"We have total {len(swap_df):,} Uniswap swap events in the loaded dataset")
column_names = ", ".join([n for n in swap_df.columns])
print("Swap data columns are:", column_names)
We have total 1,221,012 Uniswap swap events in the loaded dataset
Swap data columns are: block_number, timestamp, tx_hash, log_index, pool_contract_address, amount0, amount1, sqrt_price_x96, liquidity, tick

Choose a pool to analyse, for example: USDC/ETH 0.3%. You can find pool addresses on Uniswap v3 info site.

[14]:
from eth_defi.uniswap_v3.pool import fetch_pool_details

pool_address = "0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8"
pool_details = fetch_pool_details(web3, pool_address)

print(pool_details)
print("token0 is", pool_details.token0)
print("token1 is", pool_details.token1)

Pool 0x8ad599c3A0ff1De082011EFDDc58f1908eb6e6D8 is USDC-WETH, with the fee 0.3000%
token0 is <USD Coin (USDC) at 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48, 6 decimals, on chain 1>
token1 is <Wrapped Ether (WETH) at 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2, 18 decimals, on chain 1>

Extract the swaps of this pool from the Uniswap v3 full dataset of all pools.

[15]:
df = swap_df.loc[swap_df["pool_contract_address"] == pool_address.lower()]
df
[15]:
block_number timestamp tx_hash log_index pool_contract_address amount0 amount1 sqrt_price_x96 liquidity tick
19 12371376 2021-05-05 01:56:23 0xce7c3c307d820785caa12938012372fc9366a614a6aa... 26 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 -329608 100000000000000 1377932816571815120446551350158799 4303369674465501 195285
21 12373132 2021-05-05 08:23:26 0x9a1c51b0bffbf840948f3b6e3f3e495ba1cd3fa64854... 192 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 -164694492 50000000000000000 1378850591292581266780357299649652 4303369674465501 195298
25 12373520 2021-05-05 09:50:51 0xc58715c62a5bf70a6ca09f0e51546d6cad76c8d4fff0... 8 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 -329169 100000000000000 1378852426842022799073024911548633 4303369674465501 195298
26 12374077 2021-05-05 11:59:57 0x288c21b8b4fbf449b1d086a06e43b124ac2bc088c3f5... 86 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 2 -329169 1378852426842016741051966412054516 4304946248093346 195298
27 12374320 2021-05-05 12:56:56 0x67502d8ba373287f6d301f6baa77c5a5f4c80d0753c3... 257 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 1559137299 -467880854065813753 1370241555019945317645788135487819 4304946248093346 195173
... ... ... ... ... ... ... ... ... ... ...
1220816 12619596 2021-06-12 12:16:13 0x8c1f6c5639b304eb0087c2b6d5b65e32ee4f1a51e8b0... 112 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 92432639474 -38329545577539549888 1615736234513681125600943820478722 25998097729965548918 198469
1220835 12619598 2021-06-12 12:16:25 0xac8e34aa704a1ab4068bd93d4b5aef0fb7655d292fc9... 3 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 76760509615 -31826486546079303210 1615639241428978886102798535892145 25997255949450154096 198468
1220837 12619598 2021-06-12 12:16:25 0x0752c6529f939f046c7f9648e8d27d76b1cf767d6902... 8 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 482579746068 -200000000000000000000 1615029729696907486273078115160620 25997255949450154096 198460
1220841 12619598 2021-06-12 12:16:25 0xbd9193ff70e4828a43679f955c9707fa63fe1dabeed3... 45 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 679981136491 -281554906013205614934 1614171674604720940565192048438458 25997255949450154096 198449
1221001 12619619 2021-06-12 12:21:51 0x91f7b948333f452324270b09d6cb074b69dda7647f4c... 281 0x8ad599c3a0ff1de082011efddc58f1908eb6e6d8 8388626 -3471572647773651 1614171664025625771996673894396803 25999040327671324243 198449

63657 rows × 10 columns

In Uniswap V3, we can get pool’s price from pool’s tick.

[16]:
def convert_price(row):
    # USDC/WETH pool has reverse token order, so let's flip it WETH/USDC
    tick = row["tick"]
    return float(pool_details.convert_price_to_human(tick, reverse_token_order=True))

def convert_value(row):
    # USDC is token0 and amount0
    return abs(float(row["amount0"])) / (10**pool_details.token0.decimals)

df = df.copy(deep=True)  # https://stackoverflow.com/a/60885847/315168
df["price"] = df.apply(convert_price, axis=1)
df["value"] = df.apply(convert_value, axis=1)

display(df[["block_number", "timestamp", "tick", "price", "value"]])

block_number timestamp tick price value
19 12371376 2021-05-05 01:56:23 195285 3306.010571 0.329608
21 12373132 2021-05-05 08:23:26 195298 3301.715764 164.694492
25 12373520 2021-05-05 09:50:51 195298 3301.715764 0.329169
26 12374077 2021-05-05 11:59:57 195298 3301.715764 0.000002
27 12374320 2021-05-05 12:56:56 195173 3343.244146 1559.137299
... ... ... ... ... ...
1220816 12619596 2021-06-12 12:16:13 198469 2404.538795 92432.639474
1220835 12619598 2021-06-12 12:16:25 198468 2404.779249 76760.509615
1220837 12619598 2021-06-12 12:16:25 198460 2406.703745 482579.746068
1220841 12619598 2021-06-12 12:16:25 198449 2409.352444 679981.136491
1221001 12619619 2021-06-12 12:21:51 198449 2409.352444 8.388626

63657 rows × 5 columns

Then we convert individual swap events to OHLC candles. We use 4h time frame here.

[17]:
from eth_defi.research.candle import convert_to_ohlcv_candles

candles = convert_to_ohlcv_candles(df, pd.Timedelta("4h"))

# Show only 100 first candles
candles = candles.head(100)

display(candles)
open high low close volume
timestamp
2021-05-05 00:00:00 3306.010571 3306.010571 3306.010571 3306.010571 3.296080e-01
2021-05-05 04:00:00 NaN NaN NaN NaN 0.000000e+00
2021-05-05 08:00:00 3301.715764 3301.715764 3301.715764 3301.715764 1.650237e+02
2021-05-05 12:00:00 3343.244146 3343.244146 3343.244146 3343.244146 1.559137e+03
2021-05-05 16:00:00 3343.244146 3476.895080 3343.244146 3430.617750 1.381694e+05
... ... ... ... ... ...
2021-05-20 20:00:00 2789.467427 2890.845172 2668.863966 2775.833150 1.082562e+08
2021-05-21 00:00:00 2775.833150 2924.572530 2775.833150 2797.008788 4.026791e+07
2021-05-21 04:00:00 2792.816630 2809.342124 2694.068302 2694.068302 4.344308e+07
2021-05-21 08:00:00 2691.375715 2771.395590 2638.085543 2707.571699 3.918836e+07
2021-05-21 12:00:00 2709.738515 2750.138850 2388.005409 2420.944555 1.261962e+08

100 rows × 5 columns

Drawing OHLCV chart

Now we can plot the OHLC chart using Plotly.

[18]:
import plotly.graph_objects as go
from plotly.subplots import make_subplots

candlesticks = go.Candlestick(
    x=candles.index,
    open=candles['open'],
    high=candles['high'],
    low=candles['low'],
    close=candles['close'],
    showlegend=False,

)

volume_bars = go.Bar(
    x=candles.index,
    y=candles['volume'],
    showlegend=False,
    marker={
        "color": "rgba(128,128,128,0.5)",
    }
)

fig = go.Figure(candlesticks)
fig = make_subplots(specs=[[{"secondary_y": True}]])
fig.add_trace(candlesticks, secondary_y=True)
fig.add_trace(volume_bars, secondary_y=False)
fig.update_layout(
    title="ETH/USDC pool price data at the very beginning of Uniswap v3",
    height=800,
    # Hide Plotly scrolling minimap below the price chart
    xaxis={"rangeslider": {"visible": False}},
)
fig.update_yaxes(title="Price $", secondary_y=True, showgrid=True)
fig.update_yaxes(title="Volume $", secondary_y=False, showgrid=False)
fig.show()