diff --git a/changelog.d/20231009_163013_philiplu97_crypto_bonding_curve.rst b/changelog.d/20231009_163013_philiplu97_crypto_bonding_curve.rst new file mode 100644 index 000000000..f2223faba --- /dev/null +++ b/changelog.d/20231009_163013_philiplu97_crypto_bonding_curve.rst @@ -0,0 +1,5 @@ +Changed +------- + +- Updated curvesim.bonding_curve to output bonding curve graphs for Cryptoswap pools. + diff --git a/curvesim/tools/bonding_curve.py b/curvesim/tools/bonding_curve.py index bbd7565e1..2d3a70b9c 100644 --- a/curvesim/tools/bonding_curve.py +++ b/curvesim/tools/bonding_curve.py @@ -4,28 +4,39 @@ plots the curves using Matplotlib. """ from itertools import combinations +from typing import Dict, List, Tuple, Union import matplotlib.pyplot as plt from numpy import linspace -from curvesim.pool import CurveMetaPool +from curvesim.pool import CurveCryptoPool, CurveMetaPool, CurvePool, CurveRaiPool D_UNIT = 10**18 +Stableswap = Union[CurvePool, CurveMetaPool, CurveRaiPool] +Cryptoswap = Union[CurveCryptoPool] + +IndexPair = Tuple[int, int] +NormalizedPoint = Tuple[int, int] +Point = Tuple[float, float] + # pylint: disable-next=too-many-locals -def bonding_curve(pool, *, truncate=0.0005, resolution=1000, plot=False): +def bonding_curve( # noqa: C901 + pool: Union[Stableswap, Cryptoswap], *, truncate=None, resolution=1000, plot=False +) -> Dict[IndexPair, List[Point]]: """ Computes and optionally plots a pool's bonding curve and current reserves. Parameters ---------- - pool : CurvePool or CurveMetaPool + pool : CurvePool, CurveMetaPool, CurveRaiPool, or CurveCryptoPool The pool object for which the bonding curve is computed. - truncate : float, optional (default=0.0005) + truncate : float, int, or None, optional (default=None) Determines where to truncate the bonding curve. The truncation point is given - by D*truncate, where D is the total supply of tokens in the pool. + by D*truncate, where D is the total supply of tokens in the pool. Stableswap + pools apply 0.0005 by default, and Cryptoswap pools apply 1 by default. resolution : int, optional (default=1000) The number of points to compute along the bonding curve. @@ -47,51 +58,119 @@ def bonding_curve(pool, *, truncate=0.0005, resolution=1000, plot=False): >>> pool = curvesim.pool.get(pool_address) >>> pair_to_curve = curvesim.bonding_curve(pool, plot=True) """ - if isinstance(pool, CurveMetaPool): - combos = [(0, 1)] + combos: List[IndexPair] = [(0, 1)] else: - combos = combinations(range(pool.n), 2) - - D = pool.D() - xp = pool._xp() # pylint: disable=protected-access + combos = list(combinations(range(pool.n), 2)) + + xp: List[int] = pool._xp() # pylint: disable=protected-access + + if isinstance(pool, (CurveMetaPool, CurvePool, CurveRaiPool)): + D: int = pool.D() + if truncate is None: + # This default value works for Stableswap, but will break Cryptoswap. + # At this value, the graph usually cuts off cleanly around the points where + # the Stableswap pool would depeg, as one stablecoin balance has reached + # almost 100% of pool assets. + + truncate = 0.0005 + elif isinstance(pool, CurveCryptoPool): + D = pool.D # Don't recalcuate D - it will rebalance the bonding curve(s) + if truncate is None: + # 1 (int) "just works" for Cryptoswap. It extends the graph to the + # point at which one coin, when valued at the price scale around which + # liquidity is centered, is pushed to 100% of deposits `D` after starting + # at (1 / pool.n) from the most recent rebalance. That should cover a + # sufficient domain. The further away from (1 / pool.n) truncate is, the + # more imbalanced the pool is at the ends of the graph. + + truncate = 1 + else: + raise TypeError(f"Bonding curve calculation not supported for {type(pool)}") - pair_to_curve = {} + pair_to_curve: Dict[IndexPair, List[Point]] = {} + current_points: Dict[IndexPair, Point] = {} for (i, j) in combos: - truncated_D = int(D * truncate) - x_max = pool.get_y(j, i, truncated_D, xp) - xs = linspace(truncated_D, x_max, resolution).round() + truncated_D: int = int(D * truncate) + x_limit: int = pool.get_y(j, i, truncated_D, xp) + xs: List[int] = list( + map(int, linspace(truncated_D, x_limit, resolution).round()) + ) - curve = [] + curve: List[Point] = [] for x in xs: - y = pool.get_y(i, j, int(x), xp) - curve.append((x, y)) - curve = [(x / D_UNIT, y / D_UNIT) for x, y in curve] + y: int = pool.get_y(i, j, x, xp) + x_float, y_float = _denormalize((x, y), (i, j), pool) + + curve.append((x_float, y_float)) + pair_to_curve[(i, j)] = curve + current_x: int = xp[i] + current_y: int = xp[j] + + current_x_float, current_y_float = _denormalize( + (current_x, current_y), (i, j), pool + ) + + current_points[(i, j)] = (current_x_float, current_y_float) + if plot: - labels = pool.coin_names + labels: List[str] = pool.coin_names if not labels: labels = [f"Coin {str(label)}" for label in range(pool.n)] - _plot_bonding_curve(pair_to_curve, labels, xp) + _plot_bonding_curve(pair_to_curve, current_points, labels) return pair_to_curve -def _plot_bonding_curve(pair_to_curve, labels, xp): - n = len(pair_to_curve) +def _denormalize( + normalized_point: NormalizedPoint, + index_pair: IndexPair, + pool: Union[Stableswap, Cryptoswap], +) -> Point: + """ + Converts a point made of integer balances (as if following EVM rules) to + human-readable float form. + """ + x, y = normalized_point + i, j = index_pair + + assert i != j # dev: x and y axes must use coins of different indices + + if isinstance(pool, (CurveMetaPool, CurvePool, CurveRaiPool)): + x_factor: int = D_UNIT + y_factor: int = D_UNIT + elif isinstance(pool, CurveCryptoPool): + x_factor = D_UNIT if i == 0 else pool.price_scale[i - 1] + y_factor = D_UNIT if j == 0 else pool.price_scale[j - 1] + + x_float: float = x / x_factor + y_float: float = y / y_factor + point: Point = (x_float, y_float) + + return point + + +def _plot_bonding_curve( + pair_to_curve: Dict[IndexPair, List[Point]], + current_points: Dict[IndexPair, Point], + labels: List[str], +) -> None: + n: int = len(pair_to_curve) _, axs = plt.subplots(1, n, constrained_layout=True) if n == 1: axs = [axs] for pair, ax in zip(pair_to_curve, axs): - curve = pair_to_curve[pair] + curve: List[Point] = pair_to_curve[pair] xs, ys = zip(*curve) - ax.plot(xs, ys, color="black") + ax.plot(xs, ys, color="black") # the entire bonding curve i, j = pair - ax.scatter(xp[i] / D_UNIT, xp[j] / D_UNIT, s=40, color="black") + x, y = current_points[(i, j)] + ax.scatter(x, y, s=40, color="black") # A single dot at the current point ax.set_xlabel(labels[i]) ax.set_ylabel(labels[j]) diff --git a/test/integration/test_bonding_curve.py b/test/integration/test_bonding_curve.py index 0513b707a..3f2023ce2 100644 --- a/test/integration/test_bonding_curve.py +++ b/test/integration/test_bonding_curve.py @@ -78,3 +78,69 @@ def test_bonding_curve_metapool(): } assert pair_to_curve == expected_result + + +def test_bonding_curve_cryptoswap(): + """ + Simple test of the bonding curve for a regular cryptoswap. + + Parameters taken from 0xd51a44d3fae010294c616388b506acda1bfaae46 + (Tricrypto-2 pool) on Oct. 10, 2023, ~4 PM EDT. + """ + A = 1707629 + gamma = 11809167828997 + n = 3 + precisions = [1000000000000, 10000000000, 1] + mid_fee = 3000000 + out_fee = 30000000 + allowed_extra_profit = 2000000000000 + fee_gamma = 500000000000000 + adjustment_step = 490000000000000 + ma_half_time = 600 + price_scale = [27823549548207248490238, 1580164282540758832038] + balances = [21282026780687, 77735630688, 13482330187707402680192] + D = 64214523455757010937592598 + + pool = curvesim.pool.CurveCryptoPool( + A, + gamma, + n, + precisions, + mid_fee, + out_fee, + allowed_extra_profit, + fee_gamma, + adjustment_step, + ma_half_time, + price_scale, + balances=balances, + D=D, + ) + + pair_to_curve = bonding_curve(pool, resolution=5) + + expected_result = { + (0, 1): [ + (64214523.455757014, 257.0830793764587), + (49949133.541076906, 330.62933900193525), + (35683743.626396805, 463.0556701281358), + (21418353.711716697, 772.4361029340506), + (7152963.7970365975, 2307.919891547214), + ], + (0, 2): [ + (64214523.455757014, 4458.778590637992), + (49922293.25998866, 5737.424624762623), + (35630063.064220294, 8043.1971137980945), + (21337832.868451938, 13447.046511020473), + (7045602.672683579, 40637.87807714902), + ], + (1, 2): [ + (2307.9198915472143, 4531.460155851949), + (1795.2779668453447, 5827.600890376516), + (1282.6360421434752, 8161.168801564149), + (769.9941174416056, 13611.631993297762), + (257.3521927397361, 40637.87807714902), + ], + } + + assert pair_to_curve == expected_result