output | ||||
---|---|---|---|---|
|
This R Package calculates the retroactive "perfect" liquidity position given a budget (e.g. 100 ETH) and a set of trades in a liquidity pool (e.g., all ETH-WBTC trades between Blocks 16,000,000 and 16,010,000 in the 0.3% fee tier). These trades implicitly include starting and ending price details but can be overriden if desired.
Data included in this package include:
ETH-WBTC 0.3% Fee Tier Uniswap v3 Net Liquidity between Ticks as of a Block 17,000,000.
This data was taken with the following query using Flipside Crypto Uniswap liquidity_actions table.
Note: Liquidity_Adjusted
is the raw liquidity divided by 1e13
. This doesn't affect calculations for
proportions of trade fees a position earns. But for sizing swaps it must match.
{"
/*
ETH WBTC 0.3% fee Ethereum Mainnet
Up to April 7, 2023 (Block 17M)
Liquidity Actions -> Net liquidity as block N
NOTE: liquidity adjusted is Liquidity / 1e13 to scale it down
*/
with ethwbtc_003_lp_history AS (
select
block_number,
action,
amount0_adjusted, token0_symbol,
amount1_adjusted, token1_symbol,
liquidity_adjusted,
IFF(action = 'DECREASE_LIQUIDITY', -1*liquidity_adjusted, liquidity_adjusted) as liquidity_signed,
tick_lower, tick_upper
from ETHEREUM.UNISWAPV3.EZ_LP_ACTIONS
WHERE pool_address = '0xcbcdf9626bc03e24f779434178a73a0b4bad62ed'
AND block_number <= 17000000
AND liquidity_adjusted > 0
ORDER BY block_number
),
block_aggregated AS (
select block_number,
tick_lower, tick_upper,
sum(liquidity_signed) as block_range_liq_sign
from ethwbtc_003_lp_history
group by block_number, tick_lower, tick_upper
)
select block_number, tick_lower, tick_upper,
sum(block_range_liq_sign) OVER (
partition by tick_lower, tick_upper ORDER BY block_number ASC
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) as net_adjusted_liquidity
from block_aggregated
ORDER BY block_number asc
"
}
## [1] "\n/*\nETH WBTC 0.3% fee Ethereum Mainnet \nUp to April 7, 2023 (Block 17M) \nLiquidity Actions -> Net liquidity as block N\nNOTE: liquidity adjusted is Liquidity / 1e13 to scale it down\n*/\n\nwith ethwbtc_003_lp_history AS (\nselect \nblock_number, \naction,\namount0_adjusted, token0_symbol,\namount1_adjusted, token1_symbol,\nliquidity_adjusted, \nIFF(action = 'DECREASE_LIQUIDITY', -1*liquidity_adjusted, liquidity_adjusted) as liquidity_signed,\ntick_lower, tick_upper\nfrom ETHEREUM.UNISWAPV3.EZ_LP_ACTIONS \nWHERE pool_address = '0xcbcdf9626bc03e24f779434178a73a0b4bad62ed'\nAND block_number <= 17000000\nAND liquidity_adjusted > 0 \nORDER BY block_number \n), \n\nblock_aggregated AS (\n select block_number,\ntick_lower, tick_upper,\nsum(liquidity_signed) as block_range_liq_sign\nfrom ethwbtc_003_lp_history\n group by block_number, tick_lower, tick_upper\n)\n\nselect block_number, tick_lower, tick_upper,\nsum(block_range_liq_sign) OVER (\npartition by tick_lower, tick_upper ORDER BY block_number ASC \nROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW\n) as net_adjusted_liquidity\nfrom block_aggregated\nORDER BY block_number asc\n"
This timestamp of data is accessible in the package via data("ethwbtc_net_liquidity")
, for more info use
?ethwbtc_net_liquidity
. Use ?liquidity_asof_block
to get the net liquidity
at a block height (i.e., dropping historical net liquidity for a given range if
it later has an updated net liquidity closer the desired blockheight).
ETH-WBTC 0.3% Fee Tier Uniswap v3 Trade History from inception (May 4, 2021) to Block 17,000,000.
This data was taken with the following query using Flipside Crypto Uniswap ez_swaps table.
Note: Liquidity_Adjusted
is the raw liquidity divided by 1e13
. This doesn't affect calculations for
proportions of trade fees a position earns.
{
"
/*
ETH WBTC 0.3% fee Trades Ethereum Mainnet
All trades up to April 7, 2023 (Block 17M)
NOTE: liquidity adjusted is Liquidity / 1e13 to scale it down
*/
select block_number,
liquidity_adjusted,
tick, amount0_adjusted, token0_symbol,
amount1_adjusted, token1_symbol
from ethereum.uniswapv3.ez_swaps
WHERE pool_address = '0xcbcdf9626bc03e24f779434178a73a0b4bad62ed'
AND block_number <= 17000000
ORDER BY BLOCK_NUMBER ASC, event_index ASC;
"
}
## [1] "\n/*\nETH WBTC 0.3% fee Trades Ethereum Mainnet \nAll trades up to April 7, 2023 (Block 17M) \nNOTE: liquidity adjusted is Liquidity / 1e13 to scale it down\n*/\n\nselect block_number, \nliquidity_adjusted,\ntick, amount0_adjusted, token0_symbol,\namount1_adjusted, token1_symbol\nfrom ethereum.uniswapv3.ez_swaps\nWHERE pool_address = '0xcbcdf9626bc03e24f779434178a73a0b4bad62ed'\nAND block_number <= 17000000 \nORDER BY BLOCK_NUMBER ASC, event_index ASC;\n "
This timestamp of data is accessible in the package via data("ethwbtc_trade_history")
, for more info use
?ethwbtc_trade_history
.
A detailed guide/presentation is available in the repo as Beating_Uniswap.pdf
, here is a shortened summary.
-
A liquidity position is comprised of a balance of assets
[token0, token1]
added to a liquidity pool at acurrent_price
with a[tick_lower, tick_upper]
indicating the concentration of liquidity between those assets (i.e., when prices are below tick_lower or above tick_upper, the position is 100% in the lower valued asset). -
This means providing liquidity is the act of selling winners (rising assets) to buy losers (automatically market making for traders) and taking a fee for doing so.
-
This creates a standard accounting/business dilemma. The liquidity provider (LP) seeks to earn revenue (fees from market making) above their costs (price divergence between the winning and losing asset).
-
Going a level deeper, this is actually a constrained optimization problem. Liquidity of a position is an actual number calculable from the inputs
[token0, token1, current_price, tick_lower, tick_upper]
(see:?get_liquidity
). Given the same assets at the same price (here, current_price means the price when position is created) the more narrow the range (i.e., the closer tick_lower and tick_upper are around current_price) the higher the marginal liquidity is and thus the higher percent of trading fees the position gets (this is called capital efficiency. Same assets, but more revenue). -
But when prices fall out of the range, the position is both getting no fees for those trades and is taking on all the price divergence risk.
-
Thus, given a
budget
(e.g., 100 ETH) at a starting point (e.g., Block 16M) and the exact series of trades that occur between the starting point and a user defined end point (e.g., Block 16,010,000), it is possible to calculate what would have been the absolute best asset breakdown and range for a given target outcome (e.g., growing from 100 ETH to >100 ETH). In some cases the optimal may be to not participate in market making during the time period at all (price divergence was significantly more than fees). In other cases the optimal may be an extremely narrow (capital efficient) range because price divergence (cost) was minimal so fees (revenue) should be maximized.
The goal of this package is to calculate optimal ranges in hindsight for a given budget. Forecasts, predictive models, and strategy development are avoided as a separate endeavor (optimal ranges are very dependent on initial conditions and may or may not be autoregressive).
naive_search.R
breaks down an example optimization on the ETH-WBTC pool.
Given:
-
100 ETH
-
169 trades between Blocks 16,000,000 - 16,010,000
-
A starting price (current_price) of the first trade's tick & ending price of the last trade's tick
-
Generate 81 potential
[token1, tick_lower]
pairs under the constraints that token1 < budget and tick_lower < current_price. -
Identify the most profitable
[token0, token1, tick_lower, tick_upper]
combination of the 81 initial options. Where profitable is determined by having an end value (fees + balance after price divergence) - budget (i.e., everything denominated in ETH at the ending price). -
Use this as the initial parameters of a formal continuous optimization using L-BFGS-B (a constrained optimization algorithm) to find the actual most profitable position (across 100,000s of potential options).
The result here may not follow pool specific requirements. For example,the 0.3% fee tier requires tick_spacing of at least 60, whereas the optimal is calculated at a tick_spacing of 1 (see: ?get_closest_tick
for more details) which is only the tick_spacing of 0.01% fee tiers.
Nonetheless, the difference in calculated liquidity and fees will generally be minimal - correction for fee ticks simply withholds some assets from the pool (i.e., not using the entire budget) which can be accounted for as revenue (priced at end_price of course).