From d78684845aa5f87ddeafe9fe6e9891ac62925f0f Mon Sep 17 00:00:00 2001 From: Mikko Ohtamaa Date: Fri, 4 Oct 2024 21:14:10 +0200 Subject: [PATCH] Add price impact example (#229) - [Add a tutorial for Uniswap v3 price impact estimation](https://web3-ethereum-defi.readthedocs.io/tutorials/) --- docs/source/tutorials/index.rst | 1 + .../tutorials/uniswap-v3-price-impact.rst | 54 ++++++ eth_defi/uniswap_v3/pool.py | 4 + eth_defi/uniswap_v3/price.py | 183 ++++++++++++++++-- scripts/uniswap-v3-price-impact.py | 135 +++++++++++++ tests/enzyme/test_arbitrum_trade.py | 1 - 6 files changed, 360 insertions(+), 18 deletions(-) create mode 100644 docs/source/tutorials/uniswap-v3-price-impact.rst create mode 100644 scripts/uniswap-v3-price-impact.py diff --git a/docs/source/tutorials/index.rst b/docs/source/tutorials/index.rst index 737e65c0..78033ea8 100644 --- a/docs/source/tutorials/index.rst +++ b/docs/source/tutorials/index.rst @@ -49,6 +49,7 @@ Tutorials and examples chainlink-price-feed chainlink-native-token uniswap-v3-price-simple + uniswap-v3-price-impact live-price uniswap-v3-liquidity-analysis uniswap-v3-price-analysis diff --git a/docs/source/tutorials/uniswap-v3-price-impact.rst b/docs/source/tutorials/uniswap-v3-price-impact.rst new file mode 100644 index 00000000..6afe1fb7 --- /dev/null +++ b/docs/source/tutorials/uniswap-v3-price-impact.rst @@ -0,0 +1,54 @@ +.. meta:: + :description: Tutorial to estimate Uniswap v3 price impact for a swap + :title: Uniswap v3 price impact using Python + +Uniswap v3 price impact estimation +---------------------------------- + +This is a minimal example code for estimating Uniswap v3 price impact. + +- This example runs on a free Polygon JSON-RPC node. + +- It will print out live price for a chosen pool + +- It will estimate the price impact for the given pool, for the given swap buy amount + +- In this example we are buying WETH with $1,000,000.50 USDC cash in hand + +- See :py:mod:`eth_defi.uniswap_v3` for Uniswap v3 API documentation + +.. note:: + + `Price impact `__ and `slippage `__ are two different things. + +To run: + +.. code-block:: shell + + python scripts/uniswap-v3-price-impact.py + +Example output: + +.. code-block:: text + + -------------------------------------------------------------------------------- + Uniswap pool details + Chain 137 + Pool 0x45dda9cb7c25131df268515131f647d726f50608 + Token0 USDC + Token1 WETH + Base token WETH + Quote token USDC + Fee (BPS) 5 + -------------------------------------------------------------------------------- + Block: 62,632,744 + Swap size: 1,000,000.50 USDC + Pool base token TVL: 739.37 WETH + Pool quote token TVL: 558,088.84 USDC + Mid price WETH / USDC: 2,423.61 + Quoted amount to received: 354.87 WETH + Quoted price (no execution slippage): 2,817.91 USDC + Price impact: 16.27% + +.. literalinclude:: ../../../scripts/uniswap-v3-price-impact.py + :language: python diff --git a/eth_defi/uniswap_v3/pool.py b/eth_defi/uniswap_v3/pool.py index 2549ccbd..389b0826 100644 --- a/eth_defi/uniswap_v3/pool.py +++ b/eth_defi/uniswap_v3/pool.py @@ -40,6 +40,10 @@ class PoolDetails: def __repr__(self): return f"Pool {self.address} is {self.token0.symbol}-{self.token1.symbol}, with the fee {self.fee * 100:.04f}%" + def get_fee_bps(self) -> int: + """Return pool fee in Basis Points""" + return int(self.fee * 10_000) + def convert_price_to_human(self, tick: int, reverse_token_order=False) -> Decimal: """Convert the price obtained through diff --git a/eth_defi/uniswap_v3/price.py b/eth_defi/uniswap_v3/price.py index 10bb2f07..0afbcf21 100644 --- a/eth_defi/uniswap_v3/price.py +++ b/eth_defi/uniswap_v3/price.py @@ -1,14 +1,143 @@ """Uniswap v3 price calculations. -See :ref:`slippage and price impact` tutorial. - Helpers to calculate -- `price impact `__ +- `Price impact `__ + +- `Slippage `__ + +- `Mid price `__ + +Example: + +.. code-block:: python + + import os + from decimal import Decimal + + from eth_defi.provider.multi_provider import create_multi_provider_web3 + from eth_defi.uniswap_v3.constants import UNISWAP_V3_DEPLOYMENTS + 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_buy_received_amount + from eth_defi.uniswap_v3.tvl import fetch_uniswap_v3_pool_tvl -- `slippage `__ -- `mid price `__ + def main(): + # You can pass your own endpoint in an environment variable + json_rpc_url = os.environ.get("JSON_RPC_POLYGON", "https://polygon-rpc.com") + + # Search pair contract addresses using Trading Strategy search: https://tradingstrategy.ai/search + # This one is: + # https://tradingstrategy.ai/trading-view/polygon/uniswap-v3/eth-usdc-fee-5 + pool_address = os.environ.get("PAIR_ADDRESS", "0x45dda9cb7c25131df268515131f647d726f50608") + + # Create web3 connection instance + web3 = create_multi_provider_web3(json_rpc_url) + + contract_details = UNISWAP_V3_DEPLOYMENTS["polygon"] + uniswap = fetch_deployment( + web3, + factory_address=contract_details["factory"], + router_address=contract_details["router"], + position_manager_address=contract_details["position_manager"], + quoter_address=contract_details["quoter"], + ) + + # Get Pool contract ABI file, prepackaged in eth_defi Python package + # and convert it to a wrapped Python object + pool = fetch_pool_details(web3, pool_address) + + inverse = True + + # Manually resolve token order from random Uniswap v3 order + if inverse: + base_token = pool.token1 + quote_token = pool.token0 + else: + base_token = pool.token0 + quote_token = pool.token1 + + # Print out pool details + # token0 and token1 will be always in a random order + # and may inverse the price + print("-" * 80) + print("Uniswap pool details") + print("Chain", web3.eth.chain_id) + print("Pool", pool_address) + print("Token0", pool.token0.symbol) + print("Token1", pool.token1.symbol) + print("Base token", base_token.symbol) + print("Quote token", quote_token.symbol) + print("Fee (BPS)", pool.get_fee_bps()) + print("-" * 80) + + inverse = True # Is price inverted for output + + # Record the block number close to our timestamp + block_num = web3.eth.get_block_number() + + # Use get_onchain_price() to get a human readable price + # in Python Decimal + mid_price = get_onchain_price( + web3, + pool.address, + ) + + if inverse: + mid_price = 1 / mid_price + + target_pair_fee_bps = 5 + + # Attempt to buy ETH wit $1,000,000.50 + swap_amount = Decimal("1_000_000.50") + swap_amount_raw = quote_token.convert_to_raw(swap_amount) + + received_amount_raw = estimate_buy_received_amount( + uniswap=uniswap, + base_token_address=base_token.address, + quote_token_address=quote_token.address, + quantity=swap_amount_raw, + target_pair_fee=target_pair_fee_bps * 100, # Uniswap v3 units + block_identifier=block_num, + ) + + received_amount = base_token.convert_to_decimals(received_amount_raw) + + quoted_price = received_amount / swap_amount + + if inverse: + quoted_price = 1 / quoted_price + + price_impact = (quoted_price - mid_price) / mid_price + + tvl_quote = fetch_uniswap_v3_pool_tvl( + pool, + quote_token, + block_identifier=block_num, + ) + + tvl_base = fetch_uniswap_v3_pool_tvl( + pool, + base_token, + block_identifier=block_num, + ) + + print(f"Block: {block_num:,}") + print(f"Swap size: {swap_amount:,.2f} {quote_token.symbol}") + print(f"Pool base token TVL: {tvl_base:,.2f} {base_token.symbol}") + print(f"Pool quote token TVL: {tvl_quote:,.2f} {quote_token.symbol}") + print(f"Mid price {base_token.symbol} / {quote_token.symbol}: {mid_price:,.2f}") + print(f"Quoted amount to received: {received_amount:,.2f} {base_token.symbol}") + print(f"Quoted price (no execution slippage): {quoted_price:,.2f} {quote_token.symbol}") + print(f"Price impact: {price_impact * 100:.2f}%") + + if __name__ == "__main__": + main() + + + +See :ref:`slippage and price impact` tutorial for more information. """ @@ -136,8 +265,8 @@ def estimate_buy_received_amount( uniswap_v3, weth.address, usdc.address, - 1650 * 10**18, - 500, + 1650 * 10**18, # Must be raw token units + 500, # 100 Uniswap v3 fee units = 1 BPS, this is 5 BPS ) assert eth_received / (10**18) == pytest.approx(0.9667409780905836) @@ -146,22 +275,42 @@ def estimate_buy_received_amount( price = (1650*10**18) / eth_received assert price == pytest.approx(Decimal(1706.7653460381143)) - See another example in :py:mod:`eth_defi.uniswap_v3.price`. + :param quantity: + How much of the base token we want to buy. - :param quantity: How much of the base token we want to buy - :param uniswap: Uniswap v3 deployment - :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 + Expressed in raw token. + + :param uniswap: + Uniswap v3 deployment + + :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 Uniswap v3 fee units. + + 100 units = 1 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 - :raise TokenDetailError: If we have an issue with ERC-20 contracts + :param block_identifier: + A specific block to estimate price. + + Either block number or a block hash. + + :param verbose: + If True, return more debug info + + :return: + Expected base token amount to receive + + :raise TokenDetailError: + If we have an issue with ERC-20 contracts """ price_helper = UniswapV3PriceHelper(uniswap) diff --git a/scripts/uniswap-v3-price-impact.py b/scripts/uniswap-v3-price-impact.py new file mode 100644 index 00000000..abbfa4e7 --- /dev/null +++ b/scripts/uniswap-v3-price-impact.py @@ -0,0 +1,135 @@ +"""Uniswap v3 price impact example. + +- Price impact is the difference between mid price and quoted/filled price + +To run: + +.. code-block:: shell + + python scripts/uniswap-v3-price-impact.py + +""" + + +import os +from decimal import Decimal + +from eth_defi.provider.multi_provider import create_multi_provider_web3 +from eth_defi.uniswap_v3.constants import UNISWAP_V3_DEPLOYMENTS +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_buy_received_amount +from eth_defi.uniswap_v3.tvl import fetch_uniswap_v3_pool_tvl + + +def main(): + # You can pass your own endpoint in an environment variable + json_rpc_url = os.environ.get("JSON_RPC_POLYGON", "https://polygon-rpc.com") + + # Search pair contract addresses using Trading Strategy search: https://tradingstrategy.ai/search + # This one is: + # https://tradingstrategy.ai/trading-view/polygon/uniswap-v3/eth-usdc-fee-5 + pool_address = os.environ.get("PAIR_ADDRESS", "0x45dda9cb7c25131df268515131f647d726f50608") + + # Create web3 connection instance + web3 = create_multi_provider_web3(json_rpc_url) + + contract_details = UNISWAP_V3_DEPLOYMENTS["polygon"] + uniswap = fetch_deployment( + web3, + factory_address=contract_details["factory"], + router_address=contract_details["router"], + position_manager_address=contract_details["position_manager"], + quoter_address=contract_details["quoter"], + ) + + # Get Pool contract ABI file, prepackaged in eth_defi Python package + # and convert it to a wrapped Python object + pool = fetch_pool_details(web3, pool_address) + + inverse = True + + # Manually resolve token order from random Uniswap v3 order + if inverse: + base_token = pool.token1 + quote_token = pool.token0 + else: + base_token = pool.token0 + quote_token = pool.token1 + + # Print out pool details + # token0 and token1 will be always in a random order + # and may inverse the price + print("-" * 80) + print("Uniswap pool details") + print("Chain", web3.eth.chain_id) + print("Pool", pool_address) + print("Token0", pool.token0.symbol) + print("Token1", pool.token1.symbol) + print("Base token", base_token.symbol) + print("Quote token", quote_token.symbol) + print("Fee (BPS)", pool.get_fee_bps()) + print("-" * 80) + + inverse = True # Is price inverted for output + + # Record the block number close to our timestamp + block_num = web3.eth.get_block_number() + + # Use get_onchain_price() to get a human readable price + # in Python Decimal + mid_price = get_onchain_price( + web3, + pool.address, + ) + + if inverse: + mid_price = 1 / mid_price + + target_pair_fee_bps = 5 + + # Attempt to buy ETH wit $1,000,000.50 + swap_amount = Decimal("1_000_000.50") + swap_amount_raw = quote_token.convert_to_raw(swap_amount) + + received_amount_raw = estimate_buy_received_amount( + uniswap=uniswap, + base_token_address=base_token.address, + quote_token_address=quote_token.address, + quantity=swap_amount_raw, + target_pair_fee=target_pair_fee_bps * 100, # Uniswap v3 units + block_identifier=block_num, + ) + + received_amount = base_token.convert_to_decimals(received_amount_raw) + + quoted_price = received_amount / swap_amount + + if inverse: + quoted_price = 1 / quoted_price + + price_impact = (quoted_price - mid_price) / mid_price + + tvl_quote = fetch_uniswap_v3_pool_tvl( + pool, + quote_token, + block_identifier=block_num, + ) + + tvl_base = fetch_uniswap_v3_pool_tvl( + pool, + base_token, + block_identifier=block_num, + ) + + print(f"Block: {block_num:,}") + print(f"Swap size: {swap_amount:,.2f} {quote_token.symbol}") + print(f"Pool base token TVL: {tvl_base:,.2f} {base_token.symbol}") + print(f"Pool quote token TVL: {tvl_quote:,.2f} {quote_token.symbol}") + print(f"Mid price {base_token.symbol} / {quote_token.symbol}: {mid_price:,.2f}") + print(f"Quoted amount to received: {received_amount:,.2f} {base_token.symbol}") + print(f"Quoted price (no execution slippage): {quoted_price:,.2f} {quote_token.symbol}") + print(f"Price impact: {price_impact * 100:.2f}%") + +if __name__ == "__main__": + main() diff --git a/tests/enzyme/test_arbitrum_trade.py b/tests/enzyme/test_arbitrum_trade.py index 3c35b9dd..f301fb71 100644 --- a/tests/enzyme/test_arbitrum_trade.py +++ b/tests/enzyme/test_arbitrum_trade.py @@ -11,7 +11,6 @@ import pytest from eth_account import Account -from eth_account.signers.local import LocalAccount from eth_typing import HexAddress from web3 import Web3