From 8476e3aaba990049d2c93e56dc0f039cd55273a9 Mon Sep 17 00:00:00 2001 From: Mikko Ohtamaa Date: Tue, 1 Aug 2023 10:03:13 +0300 Subject: [PATCH] Adding slippage and price impact tutorial --- docs/source/tutorials/index.rst | 5 + .../tutorials/slippage-and-price-impact.rst | 29 ++++ eth_defi/uniswap_v3/price.py | 134 +++++------------- scripts/pancakeswap-live-swaps-minimal.py | 2 - scripts/slippage-and-price-impact.py | 107 ++++++++++++++ scripts/uniswap-v2-swaps-live.py | 8 +- 6 files changed, 180 insertions(+), 105 deletions(-) create mode 100644 docs/source/tutorials/slippage-and-price-impact.rst create mode 100644 scripts/slippage-and-price-impact.py diff --git a/docs/source/tutorials/index.rst b/docs/source/tutorials/index.rst index 3465d5d9..ae536533 100644 --- a/docs/source/tutorials/index.rst +++ b/docs/source/tutorials/index.rst @@ -1,3 +1,7 @@ +.. meta:: + :description: Python and Pandas examples for blockchain data research + + Tutorials ========= @@ -43,6 +47,7 @@ Example tutorials pancakeswap-live-minimal live-swap aave-v3-interest-analysis + slippage-and-price-impact `For more examples, browse tests folder on Github `__. You can also search function names in `the repository `__ diff --git a/docs/source/tutorials/slippage-and-price-impact.rst b/docs/source/tutorials/slippage-and-price-impact.rst new file mode 100644 index 00000000..49603729 --- /dev/null +++ b/docs/source/tutorials/slippage-and-price-impact.rst @@ -0,0 +1,29 @@ +.. meta:: + :description: Python example to calculate slippage and price impact + +.. _slippage and price impact: + +Slippage and price impact +------------------------- + +Below is an example Python script that analyses the historical Uniswap v3 trade. +It does some calculations around `slippage `__ and `price impact `__ +analysis. + +- We know the block number when the trade decision was made + (time and block number at the time when the price impact was estimated) + +- We know the block number when the trade was actually executed + +- We use a Polygon JSON-RPC archive node to check Uniswap WMATIC-USDC + pool state at both blocks and compare the results + +- Slippage is assumed execution price vs. realised execution price + +- We also double check some other numbers like `TVL `__ + and `mid price `__ of the underlying Uniswap v3 pool + +- See :py:mod:`eth_defi.uniswap_v3.price` module for more information + +.. literalinclude:: ../../../scripts/slippage-and-price-impact.py + :language: python diff --git a/eth_defi/uniswap_v3/price.py b/eth_defi/uniswap_v3/price.py index 1a7a035b..19467dd8 100644 --- a/eth_defi/uniswap_v3/price.py +++ b/eth_defi/uniswap_v3/price.py @@ -1,95 +1,6 @@ """Uniswap v3 price calculations. -- You can estimate the future price w/slippage - -- You can check for the past price impact and slippage - -Here is an example how to use price calculation to calculate the historical price impact of WMATIC->USDC trade on Polygon using 5 BPS fee tier -for two different blocks: - -.. code-block:: python - - import os - from decimal import Decimal - - from web3 import Web3, HTTPProvider - - from eth_defi.token import fetch_erc20_details - from eth_defi.uniswap_v3.deployment import fetch_deployment - from eth_defi.uniswap_v3.price import get_onchain_price, estimate_sell_received_amount - - params = {"path":"0x0d500b1d8e8ef31e21c99d1db9a6444d3adf12700001f42791bca1f2de4661ed88a30c99a7a9449aa84174","recipient":"0x19f61a2cdebccbf500b24a1330c46b15e5f54cbc","deadline":"9223372036854775808","amountIn":"14975601230579683413","amountOutMinimum":"10799953"} - - amount_in = 14975601230579683413 - path = params["path"] - # https://tradingstrategy.ai/trading-view/polygon/uniswap-v3/matic-usdc-fee-5 - pool_address = "0xa374094527e1673a86de625aa59517c5de346d32" - block_estimated = 45_583_631 - block_executed = 45_583_635 - - wmatic_address = "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270" - usdc_address = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" - wmatic_amount = Decimal("14.975601230579683413") - fee_tier = 0.0005 # BPS - - # What is the max slippage value for Uniswap, - # as slippage is irrelevant in our - # calculations - max_slippage = 10000 - - json_rpc_url = os.environ["JSON_RPC_POLYGON"] - web3 = Web3(HTTPProvider(json_rpc_url)) - - wmatic = fetch_erc20_details(web3, wmatic_address) - usdc = fetch_erc20_details(web3, usdc_address) - - wmatic_amount_raw = wmatic.convert_to_raw(wmatic_amount) - - mid_price_estimated = get_onchain_price(web3, pool_address, block_identifier=block_estimated) - mid_price_executed = get_onchain_price(web3, pool_address, block_identifier=block_executed) - - print(f"Mid price when estimate at block {block_estimated:,}:", mid_price_estimated) - print(f"Mid price at the time of execution at block {block_executed:,}:", mid_price_executed) - print(f"Price difference {(mid_price_executed - mid_price_estimated) / mid_price_estimated * 100:.2f}%") - - # Uniswap v4 deployment addresses are the same across the chains - # https://docs.uniswap.org/contracts/v3/reference/deployments - uniswap = fetch_deployment( - web3, - "0x1F98431c8aD98523631AE4a59f267346ea31F984", - "0xE592427A0AEce92De3Edee1F18E0157C05861564", - "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", - "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", - ) - - estimated_sell_raw = estimate_sell_received_amount( - uniswap, - base_token_address=wmatic_address, - quote_token_address=usdc_address, - quantity=wmatic_amount_raw, - target_pair_fee=int(fee_tier * 1_000_000), - block_identifier=block_estimated, - slippage=max_slippage, - ) - estimated_sell = usdc.convert_to_decimals(estimated_sell_raw) - - print(f"Estimated quantity: {estimated_sell}") - - executed_sell_raw = estimate_sell_received_amount( - uniswap, - base_token_address=wmatic_address, - quote_token_address=usdc_address, - quantity=wmatic_amount_raw, - target_pair_fee=int(fee_tier * 1_000_000), - block_identifier=block_executed, - slippage=max_slippage, - ) - executed_sell = usdc.convert_to_decimals(executed_sell_raw) - - print(f"Executed quantity: {executed_sell}") - - print(f"Supposed price impact {(executed_sell - estimated_sell) / estimated_sell * 100:.2f}%") - +See :ref:`slippage and price impact` tutorial. """ from decimal import Decimal @@ -103,6 +14,8 @@ class UniswapV3PriceHelper: + """Internal helper class for price calculations.""" + def __init__(self, uniswap_v3: UniswapV3Deployment): self.deployment = uniswap_v3 @@ -231,7 +144,11 @@ def estimate_buy_received_amount( :param base_token_address: Base token address of the trading pair :param quote_token_address: Quote token address of the trading pair :param target_pair_fee: Trading fee of the target pair in raw format - :param slippage: Slippage express in bps + + :param slippage: + Slippage express in bps. + The amount will be estimated for the maximum slippage. + :param block_identifier: A specific block to estimate price :param verbose: If True, return more debug info :return: Expected base token amount to receive @@ -285,7 +202,11 @@ def estimate_sell_received_amount( :param base_token_address: Base token address of the trading pair :param quote_token_address: Quote token address of the trading pair :param target_pair_fee: Trading fee of the target pair in raw format - :param slippage: Slippage express in bps + + :param slippage: + Slippage express in bps. + The amount will be estimated for the maximum slippage. + :param block_identifier: A specific block to estimate price :param verbose: If True, return more debug info :return: Expected quote token amount to receive @@ -325,11 +246,30 @@ def get_onchain_price( ): """Get the current price of a Uniswap pool. - :param web3: Web3 instance - :param pool_contract_address: Contract address of the pool - :param block_identifier: A specific block to query price - :param reverse_token_order: If set, assume quote token is token0 - :return: Current price + Reads Uniswap v3 "slot 0" price. + + TODO: Does this include fees or not? + + - See `mid price `__ + + :param web3: + Web3 instance + + :param pool_contract_address: + Contract address of the Uniswap v3 pool + + :param block_identifier: + A specific block to query price. + + Block number or block hash. + + :param reverse_token_order: + For switching the pair ticker around to make it human readable. + + If set, assume quote token is token0 + + :return: + Current price """ pool_details = fetch_pool_details(web3, pool_contract_address) _, tick, *_ = pool_details.pool.functions.slot0().call(block_identifier=block_identifier) diff --git a/scripts/pancakeswap-live-swaps-minimal.py b/scripts/pancakeswap-live-swaps-minimal.py index 3d10f689..b4f0556f 100644 --- a/scripts/pancakeswap-live-swaps-minimal.py +++ b/scripts/pancakeswap-live-swaps-minimal.py @@ -72,7 +72,6 @@ def main(): # Keep reading events as they land while True: - start = next_block end = web3.eth.block_number @@ -83,7 +82,6 @@ def main(): end_block=end, filter=filter, ): - decoded = decode_log(evt) # Swap() events are generated by UniswapV2Pool contracts diff --git a/scripts/slippage-and-price-impact.py b/scripts/slippage-and-price-impact.py new file mode 100644 index 00000000..41d47b7c --- /dev/null +++ b/scripts/slippage-and-price-impact.py @@ -0,0 +1,107 @@ +"""Check the Uniswap v3 price estimation at a historical block. + +- Understand why slippage was what it was + +- Check what was the estimated and executed sell WMATIC->USDC on Uniswap v3 + +- See the TX https://polygonscan.com/tx/0x5b76bf15bce4de5f5d6db8d929f13e28b11816f282ecd1522e4ec6eca3a1655e + +""" +import os +from decimal import Decimal + +from web3 import Web3, HTTPProvider + +from eth_defi.token import fetch_erc20_details +from eth_defi.uniswap_v3.deployment import fetch_deployment +from eth_defi.uniswap_v3.pool import fetch_pool_details +from eth_defi.uniswap_v3.price import get_onchain_price, estimate_sell_received_amount +from eth_defi.uniswap_v3.tvl import fetch_uniswap_v3_pool_tvl + +# The test amount of WMATIC for which selling +# we calculate price impact and slippage numbers +wmatic_amount = Decimal("14.975601230579683413") + +# WMATIC-USDC 5 BPS pool address +# https://tradingstrategy.ai/trading-view/polygon/uniswap-v3/matic-usdc-fee-5 +pool_address = "0xa374094527e1673a86de625aa59517c5de346d32" +block_estimated = 45_583_631 # Assume this was when the trade was deciced +block_executed = 45_583_635 # Assume this was then the trade was executed +wmatic_address = "0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270" +usdc_address = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" +fee_tier = 0.0005 # BPS + +# Give Polygon archive node JSON-RPC access +json_rpc_url = os.environ["JSON_RPC_POLYGON"] +web3 = Web3(HTTPProvider(json_rpc_url)) + +wmatic = fetch_erc20_details(web3, wmatic_address) +usdc = fetch_erc20_details(web3, usdc_address) + +pool = fetch_pool_details(web3, pool_address) + +wmatic_amount_raw = wmatic.convert_to_raw(wmatic_amount) + +mid_price_estimated = get_onchain_price(web3, pool_address, block_identifier=block_estimated) +mid_price_executed = get_onchain_price(web3, pool_address, block_identifier=block_executed) + +tvl_estimated = fetch_uniswap_v3_pool_tvl( + pool, + quote_token=usdc, + block_identifier=block_estimated, +) + +tvl_executed = fetch_uniswap_v3_pool_tvl( + pool, + quote_token=usdc, + block_identifier=block_executed, +) + +print(f"WMATIC sold {wmatic_amount}") +print(f"TVL during estimation: {tvl_estimated:,} USDC at block {block_estimated:,}") +print(f"TVL during execution: {tvl_executed:,} USDC") +print(f"Mid price when estimate at block {block_estimated:,} USDC/MATIC:", mid_price_estimated) +print(f"Mid price at the time of execution at block {block_executed:,} USDC/MATIC:", mid_price_executed) +print(f"Mid price movement {(mid_price_executed - mid_price_estimated) / mid_price_estimated * 100:.2f}%") + +# Uniswap v3 deployment addresses are the same across the chains +# https://docs.uniswap.org/contracts/v3/reference/deployments +uniswap = fetch_deployment( + web3, + "0x1F98431c8aD98523631AE4a59f267346ea31F984", + "0xE592427A0AEce92De3Edee1F18E0157C05861564", + "0xC36442b4a4522E871399CD717aBDD847Ab11FE88", + "0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6", +) + +# Get the amount of price impact +estimated_sell_raw = estimate_sell_received_amount( + uniswap, + base_token_address=wmatic_address, + quote_token_address=usdc_address, + quantity=wmatic_amount_raw, + target_pair_fee=int(fee_tier * 1_000_000), + block_identifier=block_estimated, + slippage=0, +) +estimated_sell = usdc.convert_to_decimals(estimated_sell_raw) + +print(f"Estimated received quantity: {estimated_sell} USDC") + +executed_sell_raw = estimate_sell_received_amount( + uniswap, + base_token_address=wmatic_address, + quote_token_address=usdc_address, + quantity=wmatic_amount_raw, + target_pair_fee=int(fee_tier * 1_000_000), + block_identifier=block_executed, + slippage=0, +) +executed_sell = usdc.convert_to_decimals(executed_sell_raw) + +executed_sell_price = executed_sell / wmatic_amount + +print(f"Executed received quantity: {executed_sell} USDC") +print(f"Executed sell price: {executed_sell_price} USDC/MATIC") +print(f"Executed price impact (includes fees) {(executed_sell_price - mid_price_executed) / mid_price_executed * 100:.2f}%") +print(f"Slippage {(executed_sell - estimated_sell) / estimated_sell * 100:.2f}%") diff --git a/scripts/uniswap-v2-swaps-live.py b/scripts/uniswap-v2-swaps-live.py index 50f05c8b..bd2fea8e 100644 --- a/scripts/uniswap-v2-swaps-live.py +++ b/scripts/uniswap-v2-swaps-live.py @@ -212,8 +212,7 @@ def main(): # Start from the existing save point block_header_df = block_store.load() reorg_mon.load_pandas(block_header_df) - logger.info("Loaded %d existing blocks from %s.\n" - "If the save checkpoint was long time ago, we need to catch up all blocks and it could be slow.", len(block_header_df), block_store.path) + logger.info("Loaded %d existing blocks from %s.\n" "If the save checkpoint was long time ago, we need to catch up all blocks and it could be slow.", len(block_header_df), block_store.path) else: # Start from the scratch, # use tqdm progess bar for interactive progress @@ -262,10 +261,7 @@ def main(): # Dump stats to the output regularly if time.time() > next_stat_print: req_count = api_request_counter["total"] - logger.info("**STATS** Reorgs detected: %d, block headers buffered: %d, API requests made: %d", - total_reorgs, - len(reorg_mon.block_map), - req_count) + logger.info("**STATS** Reorgs detected: %d, block headers buffered: %d, API requests made: %d", total_reorgs, len(reorg_mon.block_map), req_count) next_stat_print = time.time() + stat_delay # Save the current block headers on disk