Skip to content

Commit

Permalink
Add cryptoswap liquidity density metric
Browse files Browse the repository at this point in the history
- Update LD estimate for robustness across pool types
- Use pool.get_min_trade_size() for cryptoswap LD estimate
- Add virtual price and block timestamp to cryptoswap state log
- Reorganize PriceDepth metric to minimize duplication
  • Loading branch information
nagakingg committed Nov 16, 2023
1 parent d794f51 commit 9a000db
Show file tree
Hide file tree
Showing 7 changed files with 237 additions and 51 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

Added
-----

- Added PriceDepth (liquidity density) metric for Cryptoswap pools
- Added virtual price and block timestamp to get_cryptoswap_pool_state()
- Added detailed testing of liquidity density calculations

Changed
-------

- Improved liquidity density calculation for robustness across pool types
127 changes: 77 additions & 50 deletions curvesim/metrics/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from copy import deepcopy

from altair import Axis, Scale
from numpy import array, exp, log, timedelta64
from numpy import array, exp, inf, log, timedelta64
from pandas import DataFrame, concat

from curvesim.pool.sim_interface import (
Expand Down Expand Up @@ -522,14 +522,11 @@ class PriceDepth(PoolMetric):
liquidity density, % change in reserves per % change in price.
"""

__slots__ = ["_factor"]

@property
@cache
def pool_config(self):
ss_config = {
base = {
"functions": {
"metrics": self.get_curve_LD,
"summary": {"liquidity_density": ["median", "min"]},
},
"plot": {
Expand All @@ -552,73 +549,103 @@ def pool_config(self):
},
}

return dict.fromkeys(
[SimCurveMetaPool, SimCurvePool, SimCurveRaiPool, SimCurveCryptoPool],
ss_config,
)
functions = {
SimCurvePool: self.get_stableswap_LD,
SimCurveMetaPool: self.get_stableswap_LD,
SimCurveRaiPool: self.get_stableswap_LD,
SimCurveCryptoPool: self.get_cryptoswap_LD,
}

config = {}
for pool, fn in functions.items():
config[pool] = deepcopy(base)
config[pool]["functions"]["metrics"] = fn

def __init__(self, pool, factor=10**8, **kwargs):
self._factor = factor
super().__init__(pool, **kwargs)
return config

def get_curve_LD(self, **kwargs):
def get_stableswap_LD(self, **kwargs):
"""
Computes liquidity density for each timestamp in an individual run.
Used for all Curve pools.
Used for all Curve stableswap pools.
"""
pool_state = kwargs["pool_state"]

coin_pairs = get_pairs(
self._pool.coin_names
) # for metapool, uses only meta assets
LD = pool_state.apply(self._get_curve_LD_by_row, axis=1, coin_pairs=coin_pairs)
return DataFrame(LD, columns=["liquidity_density"])
def trade_size_function(coin_in):
x_per_dx = 10**8
return self._pool.asset_balances[coin_in] // x_per_dx

return self._get_LD(pool_state, trade_size_function)

def _get_curve_LD_by_row(self, pool_state_row, coin_pairs):
def get_cryptoswap_LD(self, **kwargs):
"""
Computes liquidity density for a single row of data (i.e., a single timestamp).
Used for all Curve pools.
Computes liquidity density for each timestamp in an individual run.
Used for all Curve crpytoswap pools.
"""
self.set_pool_state(pool_state_row)
pool_state = kwargs["pool_state"]

LD = []
for pair in coin_pairs:
ld = self._compute_liquidity_density(*pair)
LD.append(ld)
return sum(LD) / len(LD)
extra_profit = self._pool.allowed_extra_profit # disable price_scale updates
self._pool.allowed_extra_profit = inf

def trade_size_function(coin_in):
return self._pool.get_min_trade_size(coin_in)

def _compute_liquidity_density(self, coin_in, coin_out):
LD = self._get_LD(pool_state, trade_size_function)
self._pool.allowed_extra_profit = extra_profit
return LD

def _get_LD(self, pool_state, trade_size_function):
"""
Computes liquidity density for a single pair of coins.
Computes liquidity density for each timestamp in an individual run.
Used for any sim pool.
"""
factor = self._factor
pool = self._pool
post_trade_price = self._post_trade_price
coin_pairs = get_pairs(self._pool.coin_names) # only meta assets for metapools

price_pre = pool.price(coin_in, coin_out, use_fee=False)
price_post = post_trade_price(pool, coin_in, coin_out, factor)
LD1 = price_pre / ((price_pre - price_post) * factor)

price_pre = pool.price(coin_out, coin_in, use_fee=False)
# pylint: disable-next=arguments-out-of-order
price_post = post_trade_price(pool, coin_out, coin_in, factor)
LD2 = price_pre / ((price_pre - price_post) * factor)
LD = pool_state.apply(
self._get_LD_by_row,
axis=1,
coin_pairs=coin_pairs,
trade_size_function=trade_size_function,
)

return (LD1 + LD2) / 2
return DataFrame(LD, columns=["liquidity_density"])

@staticmethod
def _post_trade_price(pool, coin_in, coin_out, factor, use_fee=False):
def _get_LD_by_row(self, pool_state_row, coin_pairs, trade_size_function):
"""
Computes price after executing a trade of size coin_in balances / factor.
Computes liquidity density for a single row of data (i.e., a single timestamp).
Used for any sim pool.
"""
self.set_pool_state(pool_state_row)
pool = self._pool

LD = []
for pair in coin_pairs:
amount_in = [trade_size_function(coin) for coin in pair]
LD_i = _compute_liquidity_density(pool, *pair, amount_in[0])
LD_j = _compute_liquidity_density(pool, *reversed(pair), amount_in[1])
LD += [LD_i, LD_j]
return sum(LD) / len(LD)

size = pool.asset_balances[coin_in] // factor

with pool.use_snapshot_context():
pool.trade(coin_in, coin_out, size)
price = pool.price(coin_in, coin_out, use_fee=use_fee)
def _compute_liquidity_density(pool, coin_in, coin_out, amount_in):
"""
Computes liquidity density for a single pair of coins.
"""
x_avg = pool.asset_balances[coin_in] + amount_in / 2
price_pre = pool.price(coin_in, coin_out, use_fee=False)
price_post = _post_trade_price(pool, coin_in, coin_out, amount_in)
LD = amount_in * (price_pre + price_post) / (2 * (price_pre - price_post) * x_avg)
return LD


def _post_trade_price(pool, coin_in, coin_out, amount_in, use_fee=False):
"""
Computes price after executing a trade of size amount_in.
"""
with pool.use_snapshot_context():
pool.trade(coin_in, coin_out, amount_in)
price = pool.price(coin_in, coin_out, use_fee=use_fee)

return price
return price


class Timestamp(Metric):
Expand Down
2 changes: 2 additions & 0 deletions curvesim/metrics/state_log/pool_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ def get_cryptoswap_pool_state(pool):
"xcp_profit_a": pool.xcp_profit_a,
"last_prices": pool.last_prices.copy(),
"last_prices_timestamp": pool.last_prices_timestamp,
"_block_timestamp": pool._block_timestamp, # pylint: disable=protected-access
"not_adjusted": pool.not_adjusted,
"virtual_price": pool.virtual_price,
}


Expand Down
2 changes: 1 addition & 1 deletion curvesim/pipelines/common/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
Metrics.Timestamp,
Metrics.PoolValue,
Metrics.PoolBalance,
# Metrics.PriceDepth,
Metrics.PriceDepth,
Metrics.PoolVolume,
Metrics.ArbMetrics,
]
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/sim_pools.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ def sim_curve_pool():
return SimCurvePool(A=250, D=1000000 * 10**18, n=2, admin_fee=5 * 10**9)


@pytest.fixture(scope="function")
def sim_curve_tripool():
return SimCurvePool(A=250, D=1000000 * 10**18, n=3, admin_fee=5 * 10**9)


@pytest.fixture(scope="function")
def sim_curve_meta_pool():
basepool = SimCurvePool(A=250, D=1000000 * 10**18, n=2, admin_fee=5 * 10**9)
Expand Down
138 changes: 138 additions & 0 deletions test/unit/test_metrics_price_depth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
from numpy import inf, mean

from curvesim.metrics.metrics import _compute_liquidity_density
from curvesim.utils import get_pairs


def test_liquidity_density_sim_curve_pool(sim_curve_pool):
_test_liquidity_density_stableswap(sim_curve_pool)


def test_liquidity_density_sim_curve_tripool(sim_curve_tripool):
_test_liquidity_density_stableswap(sim_curve_tripool)


def test_liquidity_density_sim_curve_meta_pool(sim_curve_meta_pool):
basepool = sim_curve_meta_pool.basepool
coin_names = ["BP_SYM" + str(i) for i in range(basepool.n)]
basepool.metadata = {"coins": {"names": coin_names}}
_test_liquidity_density_stableswap(sim_curve_meta_pool)


def test_liquidity_density_sim_curve_crypto_pool(sim_curve_crypto_pool):
_test_liquidity_density_cryptoswap(sim_curve_crypto_pool)


def test_liquidity_density_sim_curve_tricrypto_pool(sim_curve_tricrypto_pool):
_test_liquidity_density_cryptoswap(sim_curve_tricrypto_pool)


def _test_liquidity_density_stableswap(pool):
"""
Tests liquidity density for stableswap pools:
- Ensure LD is max at center
- Ensure LDs ~equal at equivalent curve positions for every trading pair/direction
- Ensure LD at center is ~correct
"""

def trade_size_function(pool, coin_in):
x_per_dx = 10**8
return pool.asset_balances[coin_in] // x_per_dx

A_list = [10, 50, 100, 500, 1000, 5000]
for A in A_list:
pool.A = A
LD_range = _test_compute_liquidity_density(pool, trade_size_function)

# Ensure LD at center is ~correct
LD_max_expected = (A + 1) / 2
assert abs(LD_range[0] - LD_max_expected) / LD_max_expected < 0.001


def _test_liquidity_density_cryptoswap(pool):
"""
Tests liquidity density for cryptoswap pools:
- Ensure LD is max at center
- Ensure LDs ~equal at equivalent curve positions for every trading pair/direction
- Ensure LD at center is ~correct
- Ensure LD near tail is ~.5 (constant product LD)
"""

def trade_size_function(pool, coin_in):
return pool.get_min_trade_size(coin_in)

pool.allowed_extra_profit = inf # disable _tweak_price
pool.balances = pool._convert_D_to_balances(pool.D) # balance pool
n = pool.n

A_list = [10, 50, 100, 500]
gamma_list = [10**x for x in range(13, 16)]
for A in A_list:
for gamma in gamma_list:
pool.A = A * n**n * 10000
pool.gamma = gamma
LD_range = _test_compute_liquidity_density(pool, trade_size_function)

# Ensure LD at center is ~correct
A_v1 = A * n ** (n - 1)
LD_max_expected = (A_v1 + 1) / 2
assert abs(LD_range[0] - LD_max_expected) / LD_max_expected < 0.0015

# Ensure LD near tail is ~.5 (constant product LD)
assert 0.47 < LD_range[-1] < 0.501


def _test_compute_liquidity_density(pool, trade_size_fn):
"""
Computes liquidity density ranges for each trading pair/direction and tests that:
- LD is max at center
- LDs ~equal at equivalent curve positions
"""
coin_names = ["SYM" + str(i) for i in range(pool.n)]
pool.metadata = {"coins": {"names": coin_names}}

# Compute LD range for each trading pair/direction
coin_pairs = get_pairs(pool.coin_names)
LD_ranges = []
for pair in coin_pairs:
coin_in, coin_out = pair
LD0 = _compute_liquidity_density_range(pool, coin_in, coin_out, trade_size_fn)
LD1 = _compute_liquidity_density_range(pool, coin_out, coin_in, trade_size_fn)
LD_ranges += [LD0, LD1]

# Ensure LD is max at center
for LD_range in LD_ranges:
assert LD_range[0] == max(LD_range)

# Ensure LDs ~equal at equivalent curve positions for every trading pair/direction
LD_ranges_aligned = list(zip(*LD_ranges))
LD_ranges_means = [mean(LDs) for LDs in LD_ranges_aligned]
for LDs, LD_mean in zip(LD_ranges_aligned, LD_ranges_means):
percent_deviations = [abs(LD - LD_mean) / LD_mean for LD in LDs]
assert max(percent_deviations) < 1e-4

return LD_ranges_means


def _compute_liquidity_density_range(pool, coin_in, coin_out, trade_size_function):
"""Computes liquidity density at a range of locations along the bonding curve."""

pre_trade_size = 0
pre_trade_step_size = pool.asset_balances[coin_in] // 250

coin_out_balances = pool.asset_balances[coin_out]
coin_out_balances_limit = coin_out_balances * 0.05

LD_range = []
while coin_out_balances > coin_out_balances_limit:
with pool.use_snapshot_context():
if pre_trade_size > 0:
pool.trade(coin_in, coin_out, pre_trade_size)
LD_trade_size = trade_size_function(pool, coin_in)
LD = _compute_liquidity_density(pool, coin_in, coin_out, LD_trade_size)
coin_out_balances = pool.asset_balances[coin_out]

LD_range.append(LD)
pre_trade_size += pre_trade_step_size

return LD_range
2 changes: 2 additions & 0 deletions test/unit/test_state_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,9 @@ def test_get_pool_state_curve_crypto_pool(sim_curve_crypto_pool):
"xcp_profit_a": pool.xcp_profit_a,
"last_prices": pool.last_prices,
"last_prices_timestamp": pool.last_prices_timestamp,
"_block_timestamp": pool._block_timestamp,
"not_adjusted": pool.not_adjusted,
"virtual_price": pool.virtual_price,
}

state = get_pool_state(pool)
Expand Down

0 comments on commit 9a000db

Please sign in to comment.