A ‘real-world’ framework for backtesting Uniswap V3 strategies

Providing an easy and fast workflow and a webapp for backtesting Uni_V3 positions

JNP
Coinmonks
Published in
6 min readJul 14, 2021

--

With the launch of Uniswap v3, liquidity providers have lacked a good way to estimate the performance of positions with arbitrary ranges. In the following article I will describe a simple workflow to calculate the returns of both dynamic and passive positions. I will also show an example run for the ETH/USDC 0.30% fee pool. The workflow described below has been implemented in the following public webapp.

FIRST PART: A ‘real-world’ backtesting framework

The main difference in Uniswap V3 compared to other LP options is the possibility to set custom prices ranges in which to provide liquidity. This results in a dynamic distribution of the liquidity that changes every time a LPs mint or burn positions, so in order to calculate accrued fees for a position (defined as a price range (pa,pb) and a total liquidity provided) we must calculate the share of active liquidity that this position had at each swap. Given the above, I propose the following workflow for backtesting the performance of Uniswap V3 positions:

1 Download all the swaps of the pools for a given dataframe.

Tool: Thegraph

2 Download historical data of pool liquidity.

In order to get the most accurate results it would be recommended to use a web3 client to query an archive node or the Uniswap v3 Oracle and fetch the historical active liquidity in every block, but in order to speed calculations and making it public, i have used the max granularity available right now in The Graph (hourly data). A few cross-checkings with revert.finance data shows a precision around +/-5% when the testing is done with hourly data and simple positions (without rebalancing).

Tool: Web3 client with archive node access or Thegraph

3 Merge swaps data with liquidity data, in order to know what was the active liquidity when the swap happened. For this example, we assume that the liquidity for the full hour remains constant.

4 Define a position to be simulated. Positions are defined as the combination of (liquidity, tick_lower, tick_upper). Liquidity is calculated using ‘liquidityforamounts’ formula and the following parameters: an initial capital amount ($), price at mint and the range of the position (tick_lower, tick_upper). Liquidity will be constant until an additional add or burn is executed.

Tool: liquidityforamounts@Liquidityamounts.sol or my own wrapper for python.

5 Calculate PNL for the position. For every swap executed in the backtested pool, we calculate the fees accrued by the position and the actual impermanent loss (IL) of the position .

-Fees accrued=Swap_amountIn*fee_tier*(position_liquidity/active_liquidity)

-IL=Value of LP tokens — Value of Hold initial liquidity

-Gas_costs_mint = 500000 gwei * gas_price

-PNL=Acumulated Fees_accrued (dolar value at generation)- IL -Gas_costs_mint

-APR=PNL/Initial_capital*(age of the position / year time)

Tools: amountsforliquidity@Liquidityamounts.sol or my own wrapper for python.

SECOND PART: A real-world test of the workflow

I have implemented the described workflow in a simple, but powerful webapp which I will test against a real position minted by ameen.eth and widely discussed on twitter.

Example of a real not rebalanced position for the UTH/USDC 0,3 % pool

This position only has one mint transaction from which I can take the required parameters for testing in the web app:

-Datetime of the mint: 29/05/2021 3:56PM

-Initial Capital (at the moment of the mint): 251.271,59 $USDC + 229696,3 $ETH = 480967,89$=481.000 $

-Price range: 1844,6164–2858,3641 ETH/USDC

-Gas Costs (at the moment of the mint): 17,01$ (44gwei)

The app will perform the backtest as explained above and will show the evolution for fees accrued, impermanent loss, gas_costs and PNL for the position since it was minted:

As we can see the results are very close to revert.finance data, with a difference of just 0,5% in projected APR with a divergence of -1% for accrued fees and +2% for IL (mostly because of little differences in the price feed used for the calculation).

Screenshot of the webapp for backtesting Uniswap v3 positions

THIRD PART: performing a backtesting with dynamic liquidity provision

So, what if ameen.eth would have decided to be an active liquidity provider instead? to answer this question in this section I’m going to test four symple dynamic strategies from narrow to wide bollinger bands, and see what the results would be compared to the passive strategy (though fight for sure, as ameen.eth’s range choice of parameters has performed really well and he has been in-range most of the time).

The 4 test Bollinger bands strategies choosen are:

1-Range +/- 0,25*STD_7D

2-Range +/- 1*STD_7D

3-Range +/- 2*STD_7D

4-Range +/- 4*STD_7D

Rebalance method:

A rebalance will be triggered anytime our position is out of range and hold just one of the tokens in the pool.

Once a rebalance is triggered:

  • The algorithm will take a new position redeploying exactly the initial capital.
  • The new position will have a price range equivalent to actual price +/- the correspondant bollinger band of the strategy.
  • Taking into account the capital to deploy and the new price range, the amounts of required tokens is calculated. As a swap will be required to adjust the amount relation between tokens, gas costs are added (120000 gas).
  • Gas costs for mint position are added (500000 gas)
  • Impermanent loss is acumulated as we swap the surplus token to adjust the relation between them and mint the new position instead of waiting for a reversal.

Results

Although this analysis is not trying to be a exhaustive analysis of potential market making strategies, the results are very useful to understand the dynamics of different approaches to liquidity providing in Uniswap v3.

One of the main objectives of this analysis is show graphically how the main enemy of an active liquidity provider is the accumulation of impermanent loss and not the accumulation of gas costs.

In first place a comparison for the PNL of the 5 backtests is provided:

PNL comparison of a passive strategy vs 4 dynamic strategies

Allthough the resuts are not necessarily aplicable to other pools, we can see that in the ETH/USDC 0.30% pool, employing a dynamic strategy resulted in losses that increase as the range gets more narrow.

In order to provide a detailed analysis of how this strategies evolved, lets take a detailed view of them:

— 1 Price range of positions

— 2 Detailed results of PNL for the top four strategies

As can been seen in the graphs above, the winning strategy (Fixed Range) gets really close to the min and max prices in the analyzed period and althougth goes out of range for a brief amount of time, it beat the strategy with a wider range (+/- 4*STD_7D) by earning more fees.

Lastly, it is really worth to notice the difference between the impermanent loss curves of two similar strategies as are the fixed range strategy vs the +/- 2*STD_7D strategy. The last mentioned would perform two rebalances resulting in a increase of x5 of the impermanent loss and just a increase of x1,5 fees vs the fixed strategy.

About the webapp

A simple version of the backtester is available in the next link with a guide to used inside.

Reach me at twitter or join LP cafe discord to continue the discussion

--

--