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
For more background information, see this blog post about the topic
See here a tutorial how to set up your local development environment to run this notebook
For easier access to data, see also Trading Strategy historical datasets where you can batch download historical data in one file
You need to understand Jupyter Notebook and Pandas basics
You need to understand Ethereum and Web3.py basics
You know how to use Plotly charting library for Python
You will need to have Ethereum API node and its JSON-RPC URL in order to pull out the data from Ethereum blockchain. The notebook will interactively ask you for your API key.
You will need to install web3-ethereum-defi Python package
This notebook uses UNIX style paths and may not run on Microsoft Windows unless modified.
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()