diff --git a/hydradx/model/amm/concentrated_liquidity_pool.py b/hydradx/model/amm/concentrated_liquidity_pool.py new file mode 100644 index 00000000..89f545b2 --- /dev/null +++ b/hydradx/model/amm/concentrated_liquidity_pool.py @@ -0,0 +1,496 @@ +from math import sqrt as sqrt +import math +from .agents import Agent +from .amm import AMM +from mpmath import mp, mpf +mp.dps = 50 + +tick_increment = 1 + 1e-4 +min_tick = -887272 +max_tick = -min_tick +def tick_to_price(tick: int or float): + return tick_increment ** tick + +def price_to_tick(price: float, tick_spacing: int = 0): + raw_tick = math.log(price) / math.log(tick_increment) + if tick_spacing == 0: + return raw_tick + nearest_valid_tick = int(raw_tick / tick_spacing) * tick_spacing + return nearest_valid_tick + +class ConcentratedLiquidityPosition(AMM): + def __init__( + self, + assets: dict = None, + min_tick: int = None, + max_tick: int = None, + liquidity_total: float = None, + price: float = None, + tick_spacing: int = 10, + fee: float = 0.0, + protocol_fee: float = 0.0 + ): + super().__init__() + self.asset_list = list(assets.keys()) + if len(self.asset_list) != 2: + raise ValueError("Expected 2 assets.") + self.tick_spacing = tick_spacing + self.fee = fee + self.protocol_fee = protocol_fee + if liquidity_total and price and min_tick and max_tick: + min_price = tick_to_price(min_tick) + max_price = tick_to_price(max_tick) + x = liquidity_total * (sqrt(price) - sqrt(min_price)) / (sqrt(max_price) * sqrt(price)) + y = liquidity_total * (sqrt(price) - sqrt(min_price)) + f_price = y / x + elif assets: + self.liquidity = {tkn: assets[tkn] for tkn in self.asset_list} + self.asset_x = self.asset_list[0] + self.asset_y = self.asset_list[1] + x = self.liquidity[self.asset_x] + y = self.liquidity[self.asset_y] + price = y / x + price_tick = price_to_tick(price) + + if min_tick is not None and max_tick is not None: + # raise ValueError("Only one of min_tick or max_tick should be provided.") + pass + elif min_tick is not None and max_tick is None: + max_tick = round(2 * price_tick - min_tick) + elif max_tick is not None and min_tick is None: + min_tick = round(2 * price_tick - max_tick) + else: + raise ValueError("Either min_tick or max_tick must be provided.") + else: + raise ValueError("Either assets or liquidity_total, price, min_tick, and max_tick must be provided.") + self.min_price = tick_to_price(min_tick) + self.max_price = tick_to_price(max_tick) + self.min_tick = min_tick + self.max_tick = max_tick + k = (x * sqrt(y / x) * sqrt(self.max_price) / (sqrt(self.max_price) - sqrt(y / x))) ** 2 + a = sqrt(k * x / y) - x + b = sqrt(k * y / x) - y + self.x_offset = a + self.y_offset = b + + if not min_tick <= price_tick <= max_tick: + raise ValueError("Initial price is outside the tick range.") + if min_tick % self.tick_spacing != 0 or max_tick % self.tick_spacing != 0: + raise ValueError(f"Tick values must be multiples of the tick spacing ({self.tick_spacing}).") + self.fees_accrued = {tkn: 0 for tkn in self.asset_list} + + def swap(self, agent: Agent, tkn_buy: str, tkn_sell: str, buy_quantity: float = 0, sell_quantity: float = 0): + if buy_quantity > 0 and sell_quantity > 0: + raise ValueError("Only one of buy_quantity or sell_quantity should be provided.") + + if buy_quantity == 0 and sell_quantity == 0: + raise ValueError("Either buy_quantity or sell_quantity must be provided.") + + if tkn_buy not in self.asset_list or tkn_sell not in self.asset_list: + raise ValueError(f"Invalid token symbols. Token symbols must be {' or '.join(self.asset_list)}.") + + if tkn_buy == tkn_sell: + raise ValueError("Cannot buy and sell the same token.") + + if buy_quantity > 0: + sell_quantity = self.calculate_sell_from_buy(tkn_sell, tkn_buy, buy_quantity) + # vvv what happens to the fee is TBD ^^^ + elif sell_quantity > 0: + buy_quantity = self.calculate_buy_from_sell(tkn_buy, tkn_sell, sell_quantity) + + if agent.holdings[tkn_sell] < sell_quantity: + return self.fail_transaction(f"Agent doesn't have enough {tkn_sell}", agent) + + self.liquidity[tkn_sell] += sell_quantity + self.liquidity[tkn_buy] -= buy_quantity + + if tkn_buy not in agent.holdings: + agent.holdings[tkn_buy] = 0 + agent.holdings[tkn_sell] -= sell_quantity + agent.holdings[tkn_buy] += buy_quantity + + return self + + def calculate_buy_from_sell(self, tkn_buy: str, tkn_sell: str, sell_quantity: float) -> float: + x_virtual, y_virtual = self.get_virtual_reserves() + sell_quantity *= (1 - self.fee) + if tkn_sell == self.asset_x: + buy_quantity = sell_quantity * y_virtual / (x_virtual + sell_quantity) + else: + buy_quantity = sell_quantity * x_virtual / (y_virtual + sell_quantity) + + return buy_quantity + + def calculate_sell_from_buy(self, tkn_sell: str, tkn_buy: str, buy_quantity: float) -> float: + x_virtual, y_virtual = self.get_virtual_reserves() + + if tkn_buy == self.asset_x: + sell_quantity = buy_quantity * y_virtual / (x_virtual - buy_quantity) + else: + sell_quantity = buy_quantity * x_virtual / (y_virtual - buy_quantity) + + return sell_quantity * (1 + self.fee) + + def get_virtual_reserves(self): + x_virtual = self.liquidity[self.asset_x] + self.x_offset + y_virtual = self.liquidity[self.asset_y] + self.y_offset + return x_virtual, y_virtual + + def price(self, tkn: str, denomination: str = '') -> float: + if tkn not in self.asset_list: + raise ValueError(f"Invalid token symbol. Token symbol must be {' or '.join(self.asset_list)}.") + if denomination and denomination not in self.asset_list: + raise ValueError(f"Invalid denomination symbol. Denomination symbol must be {' or '.join(self.asset_list)}.") + if tkn == denomination: + return 1 + + x_virtual, y_virtual = self.get_virtual_reserves() + + if tkn == self.asset_x: + return y_virtual / x_virtual + else: + return x_virtual / y_virtual + + def buy_spot(self, tkn_buy: str, tkn_sell: str, fee: float = None): + if fee is None: + fee = self.fee + return self.price(tkn_buy) * (1 + fee) + + def sell_spot(self, tkn_sell: str, tkn_buy: str, fee: float = None): + if fee is None: + fee = self.fee + return self.price(tkn_sell) * (1 - fee) + + def copy(self): + return ConcentratedLiquidityPosition( + assets=self.liquidity.copy(), + min_tick=self.min_tick, + tick_spacing=self.tick_spacing, + fee=self.fee + ) + + @property + def invariant(self): + return sqrt((self.liquidity[self.asset_x] + self.x_offset) * (self.liquidity[self.asset_y] + self.y_offset)) + + def __str__(self): + return f""" + assets: {', '.join(self.asset_list)} + min_tick: {self.min_tick} ({tick_to_price(self.min_tick)}), + max_tick: {self.max_tick} ({tick_to_price(self.max_tick)}) + """ + + +class Tick: + def __init__(self, liquidity_net: float, sqrt_price: float, index: int): + self.liquidityNet = liquidity_net + self.sqrtPrice = sqrt_price + self.index = index + self.initialized = True + + +class ConcentratedLiquidityPoolState(AMM): + ticks: dict[int, Tick] + def __init__( + self, + asset_list: list[str], + sqrt_price: float, + liquidity: float, + tick_spacing: int = 10, + fee: float = 0.0, + protocol_fee: float = 0.0, + ticks: dict[int, Tick] = None + ): + super().__init__() + self.asset_list = asset_list + self.tick_spacing = tick_spacing + self.fee = fee + self.protocol_fee = protocol_fee + self.fees_accrued = {tkn: 0 for tkn in self.asset_list} + self.ticks: dict[int, Tick] = ticks or {} + self.sqrt_price = sqrt_price + self.liquidity = liquidity + self.tick_crossings = [] + + def initialize_tick(self, tick: int, liquidity_net: float): + if tick % self.tick_spacing != 0: + raise ValueError(f"Tick values must be multiples of the tick spacing ({self.tick_spacing}).") + price = tick_to_price(tick) + new_tick = Tick( + liquidity_net=liquidity_net, + sqrt_price=price ** 0.5, + index=tick + ) + self.ticks[tick] = new_tick + return self + + def initialize_ticks(self, ticks: dict[int, float]): + for tick, liquidity_net in ticks.items(): + self.initialize_tick(tick, liquidity_net) + return self + + @property + def current_tick(self): + return int(price_to_tick(self.sqrt_price ** 2 + 2e-16, self.tick_spacing)) + + def next_initialized_tick(self, zero_for_one): + search_direction = -1 if zero_for_one else 1 + current_tick = self.current_tick + search_direction * self.tick_spacing + + while not current_tick in self.ticks and max_tick > current_tick > min_tick: + current_tick += search_direction * self.tick_spacing + + return self.ticks[current_tick] if current_tick in self.ticks else None + + def getAmount0Delta(self, sqrt_ratio_a, sqrt_ratio_b) -> float: + if sqrt_ratio_a > sqrt_ratio_b: + sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a + return self.liquidity * (sqrt_ratio_b - sqrt_ratio_a) / sqrt_ratio_b / sqrt_ratio_a + + def getAmount1Delta(self, sqrt_ratio_a, sqrt_ratio_b) -> float: + if sqrt_ratio_a > sqrt_ratio_b: + sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a + return self.liquidity * (sqrt_ratio_b - sqrt_ratio_a) + + def swap( + self, + agent: Agent, + tkn_buy: str, + tkn_sell: str, + buy_quantity: float = 0, + sell_quantity: float = 0, + price_limit: float = None + ): + exact_input = sell_quantity > 0 + amountSpecifiedRemaining = sell_quantity or -buy_quantity + amountCalculated = 0 + protocolFee_current = 0 + + zeroForOne = tkn_sell == self.asset_list[0] # zeroForOne means selling x, buying y + if price_limit is None: + sqrt_price_limit = -float('inf') if zeroForOne else float('inf') + else: + sqrt_price_limit = sqrt(price_limit) + + while abs(amountSpecifiedRemaining) > 1e-12: + next_tick: Tick = self.next_initialized_tick(zeroForOne) + + # get the price for the next tick + sqrt_price_next = next_tick.sqrtPrice if next_tick else ( + sqrt(tick_to_price(min_tick)) if zeroForOne else sqrt(tick_to_price(max_tick)) + ) + + # compute values to swap to the target tick, price limit, or point where input/output amount is exhausted + self.sqrt_price, amountIn, amountOut, feeAmount = self.compute_swap_step( + self.sqrt_price, + sqrt_ratio_target=( + sqrt_price_limit if + (sqrt_price_next < sqrt_price_limit if zeroForOne else sqrt_price_next > sqrt_price_limit) + else sqrt_price_next + ), + amount_remaining=amountSpecifiedRemaining + ) + + if exact_input: + amountSpecifiedRemaining -= amountIn + feeAmount + amountCalculated -= amountOut + else: + amountSpecifiedRemaining += amountOut + amountCalculated += amountIn + feeAmount + + # if the protocol fee is on, calculate how much is owed, decrement feeAmount, and increment protocolFee + if self.protocol_fee > 0: + delta = feeAmount / self.protocol_fee + feeAmount -= delta + protocolFee_current += delta + + # update global fee tracker + # if self.liquidity > 0: + # state.feeGrowthGlobalX128 += feeAmount / self.liquidity + + # shift tick if we reached the next price + if self.sqrt_price == sqrt_price_next: + # if the tick is initialized, run the tick transition + liquidity_delta = -next_tick.liquidityNet if zeroForOne else next_tick.liquidityNet + self.liquidity += liquidity_delta + self.tick_crossings.append({ + "net": liquidity_delta, + "total": self.liquidity, + "price": self.sqrt_price ** 2 + }) + + # update the agent's holdings + if tkn_buy not in agent.holdings: + agent.holdings[tkn_buy] = 0 + if exact_input: + agent.holdings[tkn_buy] -= amountCalculated + agent.holdings[tkn_sell] -= sell_quantity + else: + agent.holdings[tkn_buy] += buy_quantity + agent.holdings[tkn_sell] -= amountCalculated + + return self + + + def compute_swap_step( + self, + sqrt_ratio_current: float, + sqrt_ratio_target: float, + amount_remaining: float + ) -> tuple[float, float, float, float]: # sqrt_ratio_nex, amountIn, amountOut, feeAmount + + zeroForOne = sqrt_ratio_current >= sqrt_ratio_target + exactIn = amount_remaining >= 0 + amountIn: float = 0 + amountOut: float = 0 + + if exactIn: + amountRemainingLessFee = amount_remaining * (1 - self.fee) + amountIn = ( # calculate amount that it would take to reach our sqrt price target + self.getAmount0Delta(sqrt_ratio_target, sqrt_ratio_current) + if zeroForOne else + self.getAmount1Delta(sqrt_ratio_current, sqrt_ratio_target) + ) + if amountRemainingLessFee >= amountIn: + sqrt_ratio_next = sqrt_ratio_target + else: + sqrt_ratio_next = self.getNextSqrtPriceFromInput( + amount_in=amountRemainingLessFee, + zero_for_one=zeroForOne + ) + else: + amountOut = ( # calculate amount that it would take to reach our sqrt price target + self.getAmount1Delta(sqrt_ratio_target, sqrt_ratio_current) + if zeroForOne else + self.getAmount0Delta(sqrt_ratio_current, sqrt_ratio_target) + ) + if -amount_remaining >= amountOut: + sqrt_ratio_next = sqrt_ratio_target + else: + sqrt_ratio_next = self.getNextSqrtPriceFromOutput( + amount_out=-amount_remaining, + zero_for_one=zeroForOne + ) + + is_max = sqrt_ratio_target == sqrt_ratio_next + + # get the input/output amounts + if zeroForOne: + if not (is_max and exactIn): + amountIn = self.getAmount0Delta(sqrt_ratio_next, sqrt_ratio_current) + if exactIn or not is_max: + amountOut = self.getAmount1Delta(sqrt_ratio_next, sqrt_ratio_current) + else: + if not(is_max and exactIn): + amountIn = self.getAmount1Delta(sqrt_ratio_current, sqrt_ratio_next) + if exactIn or not is_max: + amountOut = self.getAmount0Delta(sqrt_ratio_current, sqrt_ratio_next) + + # cap the output amount to not exceed the remaining output amount + if not exactIn and amountOut > -amount_remaining: + amountOut = -amount_remaining + + if exactIn and not is_max: + # we didn't reach the target, so take the remainder of the maximum input as fee + feeAmount = amount_remaining - amountIn + else: + feeAmount = amountIn * self.fee + + return ( + sqrt_ratio_next, + amountIn, + amountOut, + feeAmount + ) + + # /// @notice Gets the next sqrt price given an input amount of token0 or token1 + # /// @dev Throws if price or liquidity are 0, or if the next price is out of bounds + # /// @param sqrtPX96 The starting price, i.e., before accounting for the input amount + # /// @param liquidity The amount of usable liquidity + # /// @param amountIn How much of token0, or token1, is being swapped in + # /// @param zeroForOne Whether the amount in is token0 or token1 + # /// @return sqrtQX96 The price after adding the input amount to token0 or token1 + def getNextSqrtPriceFromInput( + self, + amount_in: float, + zero_for_one: bool + ): + # // round to make sure that we don't pass the target price + return ( + self.getNextSqrtPriceFromAmount0(amount=amount_in, add=True) + if zero_for_one else + self.getNextSqrtPriceFromAmount1(amount=amount_in, add=True) + ) + + # /// @notice Gets the next sqrt price given an output amount of token0 or token1 + # /// @dev Throws if price or liquidity are 0 or the next price is out of bounds + # /// @param sqrtPX96 The starting price before accounting for the output amount + # /// @param liquidity The amount of usable liquidity + # /// @param amountOut How much of token0, or token1, is being swapped out + # /// @param zeroForOne Whether the amount out is token0 or token1 + # /// @return sqrtQX96 The price after removing the output amount of token0 or token1 + def getNextSqrtPriceFromOutput( + self, + amount_out: float, + zero_for_one: bool, + ): + # round to make sure that we pass the target price + return ( + self.getNextSqrtPriceFromAmount1(amount=amount_out, add=False) + if zero_for_one else + self.getNextSqrtPriceFromAmount0(amount=amount_out, add=False) + ) + + # Gets the next sqrt price given a delta of token0 + # @param sqrtPX96 The starting price, i.e. before accounting for the token0 delta + # @param liquidity The amount of usable liquidity + # @param amount How much of token0 to add or remove from virtual reserves + # @param add Whether to add or remove the amount of token0 + # @return The price after adding or removing amount, depending on add + def getNextSqrtPriceFromAmount0( + self, + amount: float, + add: bool, + ): + if amount == 0: + # we short circuit amount == 0 because the result is otherwise not guaranteed to equal the input price + return self.sqrt_price + if add: + denominator = self.liquidity + amount * self.sqrt_price + else: + denominator = self.liquidity - amount * self.sqrt_price + return self.liquidity * self.sqrt_price / denominator + + + # Gets the next sqrt price given a delta of token1 + # @param sqrtPX96 The starting price, i.e., before accounting for the token1 delta + # @param liquidity The amount of usable liquidity + # @param amount How much of token1 to add, or remove, from virtual reserves + # @param add Whether to add, or remove, the amount of token1 + # @return The price after adding or removing `amount` + def getNextSqrtPriceFromAmount1( + self, + amount: float, + add: bool + ): + if add: + return self.sqrt_price + amount / self.liquidity + else: + return self.sqrt_price - amount / self.liquidity + + + def buy_spot(self, tkn_buy: str, tkn_sell: str, fee: float = None): + if fee is None: + fee = self.fee + if tkn_buy == self.asset_list[0]: + return self.sqrt_price ** 2 * (1 + fee) + else: + return 1 / (self.sqrt_price ** 2) * (1 + fee) + + def sell_spot(self, tkn_sell: str, tkn_buy: str, fee: float = None): + if fee is None: + fee = self.fee + if tkn_sell == self.asset_list[0]: + return self.sqrt_price ** 2 * (1 - fee) + else: + return 1 / (self.sqrt_price ** 2) * (1 - fee) \ No newline at end of file diff --git a/hydradx/model/amm/liquidations.py b/hydradx/model/amm/liquidations.py index 3ea8b396..488523e6 100644 --- a/hydradx/model/amm/liquidations.py +++ b/hydradx/model/amm/liquidations.py @@ -1,4 +1,4 @@ -from hydradx.model.amm.agents import Agent +from .agents import Agent # Dummy CDP class, in which CDP positions are determined to be in liquidation via a boolean diff --git a/hydradx/model/amm/otc.py b/hydradx/model/amm/otc.py index 013d2329..e9068d33 100644 --- a/hydradx/model/amm/otc.py +++ b/hydradx/model/amm/otc.py @@ -1,4 +1,4 @@ -from hydradx.model.amm.agents import Agent +from .agents import Agent class OTC: diff --git a/hydradx/model/processing.py b/hydradx/model/processing.py index ae64314d..b229ca68 100644 --- a/hydradx/model/processing.py +++ b/hydradx/model/processing.py @@ -1,3 +1,5 @@ +from web3 import Web3 +import math import json from csv import reader import requests @@ -335,8 +337,7 @@ def get_stableswap_data(rpc: str = 'wss://rpc.hydradx.cloud', archive: bool = Fa unique_id=pool_name )) if archive: - for state in pools: - save_stableswap_data(state) + save_stableswap_data(pools) return pools @@ -835,4 +836,153 @@ async def query_sqlPad(query: str): return [] except Exception as e: - print(f"There was a problem with your request: {str(e)}") \ No newline at end of file + print(f"There was a problem with your request: {str(e)}") + + +tkn_pair_abi = json.loads('[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"int24","name":"tickLower","type":"int24"},{"indexed":true,"internalType":"int24","name":"tickUpper","type":"int24"},{"indexed":false,"internalType":"uint128","name":"amount","type":"uint128"},{"indexed":false,"internalType":"uint256","name":"amount0","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount1","type":"uint256"}],"name":"Burn","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"address","name":"recipient","type":"address"},{"indexed":true,"internalType":"int24","name":"tickLower","type":"int24"},{"indexed":true,"internalType":"int24","name":"tickUpper","type":"int24"},{"indexed":false,"internalType":"uint128","name":"amount0","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"amount1","type":"uint128"}],"name":"Collect","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint128","name":"amount0","type":"uint128"},{"indexed":false,"internalType":"uint128","name":"amount1","type":"uint128"}],"name":"CollectProtocol","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount0","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount1","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"paid0","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"paid1","type":"uint256"}],"name":"Flash","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint16","name":"observationCardinalityNextOld","type":"uint16"},{"indexed":false,"internalType":"uint16","name":"observationCardinalityNextNew","type":"uint16"}],"name":"IncreaseObservationCardinalityNext","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint160","name":"sqrtPriceX96","type":"uint160"},{"indexed":false,"internalType":"int24","name":"tick","type":"int24"}],"name":"Initialize","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":true,"internalType":"int24","name":"tickLower","type":"int24"},{"indexed":true,"internalType":"int24","name":"tickUpper","type":"int24"},{"indexed":false,"internalType":"uint128","name":"amount","type":"uint128"},{"indexed":false,"internalType":"uint256","name":"amount0","type":"uint256"},{"indexed":false,"internalType":"uint256","name":"amount1","type":"uint256"}],"name":"Mint","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"uint8","name":"feeProtocol0Old","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"feeProtocol1Old","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"feeProtocol0New","type":"uint8"},{"indexed":false,"internalType":"uint8","name":"feeProtocol1New","type":"uint8"}],"name":"SetFeeProtocol","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"sender","type":"address"},{"indexed":true,"internalType":"address","name":"recipient","type":"address"},{"indexed":false,"internalType":"int256","name":"amount0","type":"int256"},{"indexed":false,"internalType":"int256","name":"amount1","type":"int256"},{"indexed":false,"internalType":"uint160","name":"sqrtPriceX96","type":"uint160"},{"indexed":false,"internalType":"uint128","name":"liquidity","type":"uint128"},{"indexed":false,"internalType":"int24","name":"tick","type":"int24"}],"name":"Swap","type":"event"},{"inputs":[{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint128","name":"amount","type":"uint128"}],"name":"burn","outputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint128","name":"amount0Requested","type":"uint128"},{"internalType":"uint128","name":"amount1Requested","type":"uint128"}],"name":"collect","outputs":[{"internalType":"uint128","name":"amount0","type":"uint128"},{"internalType":"uint128","name":"amount1","type":"uint128"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint128","name":"amount0Requested","type":"uint128"},{"internalType":"uint128","name":"amount1Requested","type":"uint128"}],"name":"collectProtocol","outputs":[{"internalType":"uint128","name":"amount0","type":"uint128"},{"internalType":"uint128","name":"amount1","type":"uint128"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"factory","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"fee","outputs":[{"internalType":"uint24","name":"","type":"uint24"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feeGrowthGlobal0X128","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"feeGrowthGlobal1X128","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"flash","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint16","name":"observationCardinalityNext","type":"uint16"}],"name":"increaseObservationCardinalityNext","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint160","name":"sqrtPriceX96","type":"uint160"}],"name":"initialize","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"liquidity","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"maxLiquidityPerTick","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"},{"internalType":"uint128","name":"amount","type":"uint128"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"mint","outputs":[{"internalType":"uint256","name":"amount0","type":"uint256"},{"internalType":"uint256","name":"amount1","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"observations","outputs":[{"internalType":"uint32","name":"blockTimestamp","type":"uint32"},{"internalType":"int56","name":"tickCumulative","type":"int56"},{"internalType":"uint160","name":"secondsPerLiquidityCumulativeX128","type":"uint160"},{"internalType":"bool","name":"initialized","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint32[]","name":"secondsAgos","type":"uint32[]"}],"name":"observe","outputs":[{"internalType":"int56[]","name":"tickCumulatives","type":"int56[]"},{"internalType":"uint160[]","name":"secondsPerLiquidityCumulativeX128s","type":"uint160[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"bytes32","name":"","type":"bytes32"}],"name":"positions","outputs":[{"internalType":"uint128","name":"liquidity","type":"uint128"},{"internalType":"uint256","name":"feeGrowthInside0LastX128","type":"uint256"},{"internalType":"uint256","name":"feeGrowthInside1LastX128","type":"uint256"},{"internalType":"uint128","name":"tokensOwed0","type":"uint128"},{"internalType":"uint128","name":"tokensOwed1","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"protocolFees","outputs":[{"internalType":"uint128","name":"token0","type":"uint128"},{"internalType":"uint128","name":"token1","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint8","name":"feeProtocol0","type":"uint8"},{"internalType":"uint8","name":"feeProtocol1","type":"uint8"}],"name":"setFeeProtocol","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"slot0","outputs":[{"internalType":"uint160","name":"sqrtPriceX96","type":"uint160"},{"internalType":"int24","name":"tick","type":"int24"},{"internalType":"uint16","name":"observationIndex","type":"uint16"},{"internalType":"uint16","name":"observationCardinality","type":"uint16"},{"internalType":"uint16","name":"observationCardinalityNext","type":"uint16"},{"internalType":"uint8","name":"feeProtocol","type":"uint8"},{"internalType":"bool","name":"unlocked","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"int24","name":"tickLower","type":"int24"},{"internalType":"int24","name":"tickUpper","type":"int24"}],"name":"snapshotCumulativesInside","outputs":[{"internalType":"int56","name":"tickCumulativeInside","type":"int56"},{"internalType":"uint160","name":"secondsPerLiquidityInsideX128","type":"uint160"},{"internalType":"uint32","name":"secondsInside","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"recipient","type":"address"},{"internalType":"bool","name":"zeroForOne","type":"bool"},{"internalType":"int256","name":"amountSpecified","type":"int256"},{"internalType":"uint160","name":"sqrtPriceLimitX96","type":"uint160"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"swap","outputs":[{"internalType":"int256","name":"amount0","type":"int256"},{"internalType":"int256","name":"amount1","type":"int256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"int16","name":"","type":"int16"}],"name":"tickBitmap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"tickSpacing","outputs":[{"internalType":"int24","name":"","type":"int24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"int24","name":"","type":"int24"}],"name":"ticks","outputs":[{"internalType":"uint128","name":"liquidityGross","type":"uint128"},{"internalType":"int128","name":"liquidityNet","type":"int128"},{"internalType":"uint256","name":"feeGrowthOutside0X128","type":"uint256"},{"internalType":"uint256","name":"feeGrowthOutside1X128","type":"uint256"},{"internalType":"int56","name":"tickCumulativeOutside","type":"int56"},{"internalType":"uint160","name":"secondsPerLiquidityOutsideX128","type":"uint160"},{"internalType":"uint32","name":"secondsOutside","type":"uint32"},{"internalType":"bool","name":"initialized","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"token0","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"token1","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]') +erc20_abi = [{"constant": True, "inputs": [], "name": "decimals", "outputs": [{"name": "", "type": "uint8"}], "payable": False, "stateMutability": "view", "type": "function"}] + +class UniswapToken: + def __init__(self, symbol: str, address: str, w3: Web3): + self.address = address + self.symbol = symbol + # print('Getting decimals for token: ', symbol) + self.contract = w3.eth.contract(address=self.address, abi=erc20_abi) + self.decimals = self.contract.functions.decimals().call() + + def __str__(self): + return ( + f"symbol: {self.symbol}" + f"address: ({self.address}" + f"decimals: {self.decimals}" + ) + +class UniswapPool: + def __init__(self, tkn1: UniswapToken, tkn2: UniswapToken, fee: float, address, quoter_contract, w3: Web3): + self.quoter_contract = quoter_contract + self.fee = fee + if tkn1.address.lower() < tkn2.address.lower(): + self.tkn1 = tkn1 + self.tkn2 = tkn2 + else: + self.tkn1 = tkn2 + self.tkn2 = tkn1 + print('new pool: ', tkn1.symbol, tkn2.symbol) + self.name = f"{tkn1.symbol}-{tkn2.symbol}" + self.address = address + print('pool address: ', self.address) + self.contract = w3.eth.contract(address=self.address, abi=tkn_pair_abi) + self.price96 = 0 + self.price = self.get_price() + self.sqrt_price = self.price ** 0.5 + self.tick_spacing = self.contract.functions.tickSpacing().call() + self.current_tick = self.get_active_tick() + + def get_price(self): + slot0 = self.contract.functions.slot0().call() + sqrt_price = slot0[0] + self.price96 = sqrt_price + price = (2 ** 192) / sqrt_price ** 2 # Square the sqrtPriceX96 and adjust for fixed point precision + # account for the different decimals of the tokens + # price = price / (10 ** (self.tkn1.decimals - self.tkn2.decimals)) + return price + + def get_active_tick(self): + price_tick = self.contract.functions.slot0().call()[1] + return price_tick // self.tick_spacing * self.tick_spacing + + def liquidity_at_tick(self, tick): + return_val = self.contract.functions.ticks(tick).call() + return return_val[1] + + def get_active_liquidity(self): + return self.contract.functions.liquidity().call() + + def get_liquidity_distribution(self, low_range: int=-10, high_range: int=10): + """ get liquidity at the current tick and at 10 ticks above and below """ + starting_tick = self.get_active_tick() + liquidity = {} + for i in range(low_range, high_range): + tick = starting_tick + i * self.tick_spacing + liquidity[tick] = self.liquidity_at_tick(tick) + + return {k: v for k, v in sorted(liquidity.items())} + + def get_quote( + self, tkn_sell: str = None, tkn_buy: str = None, sell_quantity: float = 0, buy_quantity: float = 0 + ) -> float: + """ get quote for a token pair """ + if tkn_sell is None: + tkn_sell = self.tkn1.symbol if tkn_buy == self.tkn2.symbol else self.tkn2.symbol + elif tkn_buy is None: + tkn_buy = self.tkn1.symbol if tkn_sell == self.tkn2.symbol else self.tkn2.symbol + if tkn_sell not in (self.tkn1.symbol, self.tkn2.symbol): + raise ValueError(f"Token {tkn_sell} not in pool") + if tkn_buy not in (self.tkn1.symbol, self.tkn2.symbol): + raise ValueError(f"Token {tkn_buy} not in pool") + # sell_quantity *= 10 ** self.tkn1.decimals if tkn_sell == self.tkn1.symbol else 10 ** self.tkn2.decimals + # buy_quantity *= 10 ** self.tkn1.decimals if tkn_buy == self.tkn1.symbol else 10 ** self.tkn2.decimals + if sell_quantity: + if tkn_sell == self.tkn1.symbol: + buy_quantity = self.quoter_contract.functions.quoteExactInputSingle( + self.tkn1.address, self.tkn2.address, self.fee, sell_quantity, 0).call() + else: + buy_quantity = self.quoter_contract.functions.quoteExactInputSingle( + self.tkn2.address, self.tkn1.address, self.fee, sell_quantity, 0).call() + # buy_quantity /= 10 ** self.tkn1.decimals if tkn_buy == self.tkn1.symbol else 10 ** self.tkn2.decimals + return buy_quantity + elif buy_quantity: + if tkn_sell == self.tkn1.symbol: + sell_quantity = self.quoter_contract.functions.quoteExactOutputSingle( + self.tkn1.address, self.tkn2.address, self.fee, buy_quantity, 0).call() + else: + sell_quantity = self.quoter_contract.functions.quoteExactOutputSingle( + self.tkn2.address, self.tkn1.address, self.fee, buy_quantity, 0).call() + # sell_quantity /= 10 ** self.tkn1.decimals if tkn_sell == self.tkn1.symbol else 10 ** self.tkn2.decimals + return sell_quantity + return 0.0 + + +def get_uniswap_pool_data(tkn_pairs: list[tuple]) -> dict[str, UniswapPool]: + """ get pool data from uniswap """ + # Connect to an Ethereum node + provider = 'https://eth-mainnet.g.alchemy.com/v2/wWrLtJw3ZHgitVEI0hOcrQ5UxAuemN6f' + # provider = 'https://moonbeam-rpc.dwellir.com' + w3 = Web3(Web3.HTTPProvider(provider)) + + # Uniswap V3 Pool address and ABI + uniswap_pool_address = "0x1F98431c8aD98523631AE4a59f267346ea31F984" + # stellaswap_factory_address = "0xabE1655110112D0E45EF91e94f8d757e4ddBA59C" + uniswap_pool_abi = json.loads('[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint24","name":"fee","type":"uint24"},{"indexed":true,"internalType":"int24","name":"tickSpacing","type":"int24"}],"name":"FeeAmountEnabled","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"oldOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"token0","type":"address"},{"indexed":true,"internalType":"address","name":"token1","type":"address"},{"indexed":true,"internalType":"uint24","name":"fee","type":"uint24"},{"indexed":false,"internalType":"int24","name":"tickSpacing","type":"int24"},{"indexed":false,"internalType":"address","name":"pool","type":"address"}],"name":"PoolCreated","type":"event"},{"inputs":[{"internalType":"address","name":"tokenA","type":"address"},{"internalType":"address","name":"tokenB","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"}],"name":"createPool","outputs":[{"internalType":"address","name":"pool","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickSpacing","type":"int24"}],"name":"enableFeeAmount","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint24","name":"","type":"uint24"}],"name":"feeAmountTickSpacing","outputs":[{"internalType":"int24","name":"","type":"int24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"},{"internalType":"address","name":"","type":"address"},{"internalType":"uint24","name":"","type":"uint24"}],"name":"getPool","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"parameters","outputs":[{"internalType":"address","name":"factory","type":"address"},{"internalType":"address","name":"token0","type":"address"},{"internalType":"address","name":"token1","type":"address"},{"internalType":"uint24","name":"fee","type":"uint24"},{"internalType":"int24","name":"tickSpacing","type":"int24"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_owner","type":"address"}],"name":"setOwner","outputs":[],"stateMutability":"nonpayable","type":"function"}]') + uniswap_pool_contract = w3.eth.contract(address=uniswap_pool_address, abi=uniswap_pool_abi) + + uniswap_quoter_address = '0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6' + uniswap_quoter_abi = [{"inputs": [{"internalType": "address", "name": "_factory", "type": "address"}, {"internalType": "address", "name": "_WETH9", "type": "address"}],"stateMutability": "nonpayable", "type": "constructor"}, {"inputs": [], "name": "WETH9","outputs": [{"internalType": "address","name": "","type": "address"}],"stateMutability": "view","type": "function"},{"inputs": [], "name": "factory","outputs": [{"internalType": "address", "name": "", "type": "address"}],"stateMutability": "view", "type": "function"}, {"inputs": [{"internalType": "bytes", "name": "path", "type": "bytes"},{"internalType": "uint256", "name": "amountIn", "type": "uint256"}],"name": "quoteExactInput","outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}],"stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "address", "name": "tokenIn", "type": "address"},{"internalType": "address", "name": "tokenOut", "type": "address"},{"internalType": "uint24", "name": "fee", "type": "uint24"},{"internalType": "uint256", "name": "amountIn", "type": "uint256"},{"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}],"name": "quoteExactInputSingle","outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}],"stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "bytes", "name": "path", "type": "bytes"},{"internalType": "uint256", "name": "amountOut", "type": "uint256"}],"name": "quoteExactOutput","outputs": [{"internalType": "uint256", "name": "amountIn", "type": "uint256"}],"stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "address", "name": "tokenIn", "type": "address"},{"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "uint256", "name": "amountOut", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "name": "quoteExactOutputSingle", "outputs": [{"internalType": "uint256", "name": "amountIn", "type": "uint256"}], "stateMutability": "nonpayable", "type": "function"}, {"inputs": [{"internalType": "int256", "name": "amount0Delta", "type": "int256"}, {"internalType": "int256", "name": "amount1Delta", "type": "int256"}, {"internalType": "bytes", "name": "path", "type": "bytes"}], "name": "uniswapV3SwapCallback", "outputs": [], "stateMutability": "view", "type": "function"}] + quoter_contract = w3.eth.contract(address=uniswap_quoter_address, abi=uniswap_quoter_abi) + + tkn_addr = { + "usdc": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "dai": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "eth": "0x0000000000000000000000000000000000000000", + "weth": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "bat": "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + } + + # create token objects + tokens = { + symbol: UniswapToken(symbol, tkn_addr[symbol], w3) + for symbol in {tkn for pair in tkn_pairs for tkn in pair} + } + + # get all available pools for each token pair at every fee level + fee_levels = [100, 500, 3000, 10000] + fee_levels = [3000] + pools = {} + for tkn1, tkn2, fee in [(tkn1, tkn2, fee) for tkn1, tkn2 in tkn_pairs for fee in fee_levels]: + pool_address = uniswap_pool_contract.functions.getPool(tokens[tkn1].address, tokens[tkn2].address, fee).call() + if int(pool_address, 16) > 0: + pools[f"{tkn1}-{tkn2}-{fee}"] = UniswapPool( + tokens[tkn1], tokens[tkn2], fee, pool_address, quoter_contract, w3 + ) + + return pools \ No newline at end of file diff --git a/hydradx/tests/test_concentrated_liquidity.py b/hydradx/tests/test_concentrated_liquidity.py new file mode 100644 index 00000000..3fc47dd6 --- /dev/null +++ b/hydradx/tests/test_concentrated_liquidity.py @@ -0,0 +1,577 @@ +import math +import pytest +from hypothesis import given, strategies as st, assume, settings, Verbosity +from mpmath import mp, mpf +from hydradx.model.processing import get_uniswap_pool_data + +from hydradx.model.amm.concentrated_liquidity_pool import ConcentratedLiquidityPosition, price_to_tick, tick_to_price, \ + ConcentratedLiquidityPoolState, Tick +from hydradx.model.amm.agents import Agent + +mp.dps = 50 + +token_amounts = st.floats(min_value=0.01, max_value=1000.0, allow_nan=False, allow_infinity=False) +price_strategy = st.floats(min_value=0.01, max_value=100.0, allow_nan=False, allow_infinity=False) +fee_strategy = st.floats(min_value=0.0, max_value=0.05, allow_nan=False, allow_infinity=False) + +@given(price_strategy, st.integers(min_value=1, max_value=100), st.integers(min_value=1, max_value=100)) +def test_price_boundaries_no_fee(initial_price, tick_spacing, price_range): + price_range *= tick_spacing + price = tick_to_price(price_to_tick(initial_price, tick_spacing=tick_spacing)) + initial_state = ConcentratedLiquidityPosition( + assets={'A': mpf(1000 / price), 'B': mpf(1000)}, + min_tick=price_to_tick(price, tick_spacing) - price_range, + tick_spacing=tick_spacing, + fee=0 + ) + second_state = ConcentratedLiquidityPosition( + assets=initial_state.liquidity.copy(), + max_tick=initial_state.max_tick, + tick_spacing=tick_spacing, + fee=0 + ) + if initial_state.min_tick != second_state.min_tick: + raise AssertionError('min_tick is not the same') + buy_x_agent = Agent(holdings={'B': 1000000}) + buy_x_state = initial_state.copy().swap( + buy_x_agent, tkn_buy='A', tkn_sell='B', buy_quantity=initial_state.liquidity['A'] + ) + new_price = buy_x_state.price('A') + if new_price != pytest.approx(initial_state.max_price, rel=1e-12): + raise AssertionError(f"Buying all initial liquidity[A] should raise price to max.") + buy_y_agent = Agent(holdings={'A': 1000000}) + buy_y_state = initial_state.copy().swap( + buy_y_agent, tkn_buy='B', tkn_sell='A', buy_quantity=initial_state.liquidity['B'] + ) + new_price = buy_y_state.price('A') + if new_price != pytest.approx(initial_state.min_price, rel=1e-12): + raise AssertionError(f"Buying all initial liquidity[B] should lower price to min.") + + if buy_x_state.invariant != pytest.approx(buy_y_state.invariant, rel=1e12): + raise AssertionError('Invariant is not the same') + + +@given(price_strategy, fee_strategy, st.integers(min_value=1, max_value=100)) +def test_asset_conservation(price, fee, price_range): + tick_spacing = 1 + price = tick_to_price(price_to_tick(price, tick_spacing=tick_spacing)) + initial_state = ConcentratedLiquidityPosition( + assets={'A': mpf(1000 / price), 'B': mpf(1000)}, + min_tick=price_to_tick(price, tick_spacing) - tick_spacing * price_range, + tick_spacing=tick_spacing, + fee=fee + ) + sell_quantity = 1000 + sell_agent = Agent(holdings={'B': 1000000}) + sell_state = initial_state.copy().swap( + sell_agent, tkn_buy='A', tkn_sell='B', sell_quantity=sell_quantity + ) + if sell_state.liquidity['A'] + sell_agent.holdings['A'] != initial_state.liquidity['A']: + raise AssertionError('Asset A was not conserved.') + if sell_state.liquidity['B'] + sell_agent.holdings['B'] != initial_state.liquidity['B'] + sell_agent.initial_holdings['B']: + raise AssertionError('Asset B was not conserved.') + + buy_quantity = initial_state.calculate_buy_from_sell(tkn_buy='A', tkn_sell='B', sell_quantity=sell_quantity) + buy_agent = Agent(holdings={'B': 1000000}) + buy_state = initial_state.copy().swap( + buy_agent, tkn_buy='A', tkn_sell='B', buy_quantity=buy_quantity + ) + if buy_state.liquidity['A'] + buy_agent.holdings['A'] != initial_state.liquidity['A']: + raise AssertionError('Asset A was not conserved.') + if buy_state.liquidity['B'] + buy_agent.holdings['B'] != initial_state.liquidity['B'] + buy_agent.initial_holdings['B']: + raise AssertionError('Asset B was not conserved.') + +@given(price_strategy, st.integers(min_value=1, max_value=100)) +def test_buy_sell_equivalency(price, price_range): + tick_spacing = 10 + price = tick_to_price(price_to_tick(price, tick_spacing=tick_spacing)) + initial_state = ConcentratedLiquidityPosition( + assets={'A': mpf(1000 / price), 'B': mpf(1000)}, + min_tick=price_to_tick(price, tick_spacing) - tick_spacing * price_range, + tick_spacing=tick_spacing, + fee=0 + ) + sell_quantity = 1000 + sell_y_agent = Agent(holdings={'B': 1000000}) + initial_state.copy().swap( + sell_y_agent, tkn_buy='A', tkn_sell='B', sell_quantity=sell_quantity + ) + buy_quantity = initial_state.calculate_buy_from_sell(tkn_buy='A', tkn_sell='B', sell_quantity=sell_quantity) + buy_x_agent = Agent(holdings={'B': 1000000}) + initial_state.copy().swap( + buy_x_agent, tkn_buy='A', tkn_sell='B', buy_quantity=buy_quantity + ) + if buy_x_agent.holdings['A'] != buy_quantity != sell_y_agent.holdings['A']: + raise AssertionError('Buy quantity was not bought correctly.') + if sell_y_agent.holdings['B'] != pytest.approx(buy_x_agent.holdings['B'], rel=1e-12): + raise AssertionError('Sell quantity was not calculated correctly.') + + +@given(price_strategy, fee_strategy, st.integers(min_value=1, max_value=100)) +def test_buy_spot(price, fee, price_range): + tick_spacing = 10 + price = tick_to_price(price_to_tick(price, tick_spacing=tick_spacing)) + initial_state = ConcentratedLiquidityPosition( + assets={'A': mpf(1000 / price), 'B': mpf(1000)}, + min_tick=price_to_tick(price, tick_spacing) - tick_spacing * price_range, + tick_spacing=tick_spacing, + fee=fee + ) + buy_quantity = 1 / mpf(1e20) + agent = Agent(holdings={'B': 1000}) + buy_spot = initial_state.buy_spot(tkn_buy='A', tkn_sell='B', fee=fee) + initial_state.swap( + agent, tkn_buy='A', tkn_sell='B', buy_quantity=buy_quantity + ) + ex_price = (agent.initial_holdings['B'] - agent.holdings['B']) / agent.holdings['A'] + if ex_price != pytest.approx(buy_spot, rel=1e-20): + raise AssertionError('Buy spot price was not calculated correctly.') + + +@given(price_strategy, fee_strategy, st.integers(min_value=1, max_value=100)) +def test_sell_spot(price, fee, price_range): + tick_spacing = 10 + price = tick_to_price(price_to_tick(price, tick_spacing=tick_spacing)) + initial_state = ConcentratedLiquidityPosition( + assets={'A': 1000 / mpf(price), 'B': mpf(1000)}, + min_tick=price_to_tick(price, tick_spacing) - tick_spacing * price_range, + tick_spacing=tick_spacing, + fee=fee + ) + sell_quantity = 1 / mpf(1e20) + agent = Agent(holdings={'A': 1000}) + sell_spot = initial_state.sell_spot(tkn_sell='A', tkn_buy='B', fee=fee) + initial_state.swap( + agent, tkn_buy='B', tkn_sell='A', sell_quantity=sell_quantity + ) + ex_price = agent.holdings['B'] / (agent.initial_holdings['A'] - agent.holdings['A']) + if ex_price != pytest.approx(sell_spot, rel=1e-20): + raise AssertionError('Sell spot price was not calculated correctly.') + + +@given(st.integers(min_value=1, max_value=100), fee_strategy) +def test_buy_x_vs_single_position(initial_tick, fee): + tick_spacing = 100 + price = mpf(tick_to_price(initial_tick * tick_spacing + tick_spacing // 2)) + buy_quantity = mpf(10) + + agent1 = Agent(holdings={'B': 1000}) + one_position = ConcentratedLiquidityPosition( + assets={'A': 10 / mpf(price), 'B': mpf(10)}, + min_tick=price_to_tick(price, tick_spacing), + tick_spacing=tick_spacing, + fee=0.0025 + ).swap( + agent1, tkn_buy='A', tkn_sell='B', buy_quantity=buy_quantity + ) + + agent1_copy = Agent(holdings={'B': 1000}) + one_position_feeless = ConcentratedLiquidityPosition( + assets={'A': 10 / mpf(price), 'B': mpf(10)}, + min_tick=price_to_tick(price, tick_spacing), + tick_spacing=tick_spacing, + fee=0 + ).swap( + agent1_copy, tkn_buy='A', tkn_sell='B', buy_quantity=buy_quantity + ) + + agent2 = Agent(holdings={'B': 1000}) + whole_pool = ConcentratedLiquidityPoolState( + asset_list=['A', 'B'], + sqrt_price=mpf.sqrt(price), + liquidity=one_position.invariant, + tick_spacing = tick_spacing, + fee=0.0025 + ).swap( + agent2, tkn_buy='A', tkn_sell='B', buy_quantity=buy_quantity + ) + + agent2_copy = Agent(holdings={'B': 1000}) + whole_pool_feeless = ConcentratedLiquidityPoolState( + asset_list=['A', 'B'], + sqrt_price=mpf.sqrt(price), + liquidity=one_position.invariant, + tick_spacing = tick_spacing, + fee=0 + ).swap( + agent2_copy, tkn_buy='A', tkn_sell='B', buy_quantity=buy_quantity + ) + + effective_fee_one_pool = (agent1_copy.holdings['B'] - agent1.holdings['B']) / (agent1_copy.initial_holdings['B'] - agent1_copy.holdings['B']) + effective_fee_whole_pool = (agent2_copy.holdings['B'] - agent2.holdings['B']) / (agent2_copy.initial_holdings['B'] - agent2_copy.holdings['B']) + + if agent1.holdings['A'] != agent2.holdings['A']: + raise AssertionError('Buy quantity was not applied correctly.') + if agent1.holdings['B'] != pytest.approx(agent2.holdings['B'], rel=1e-8): + raise AssertionError('Sell quantity was not calculated correctly.') + if effective_fee_whole_pool != pytest.approx(effective_fee_one_pool, rel=1e-8): + raise AssertionError('Fee levels do not match.') + + +@given(st.integers(min_value=1, max_value=100), fee_strategy) +def test_buy_y_vs_single_position(initial_tick, fee): + tick_spacing = 100 + price = mpf(tick_to_price(initial_tick * tick_spacing + tick_spacing // 2)) + buy_quantity = mpf(10) + + agent1 = Agent(holdings={'A': 1000}) + one_position = ConcentratedLiquidityPosition( + assets={'A': 10 / mpf(price), 'B': mpf(10)}, + min_tick=price_to_tick(price, tick_spacing), + tick_spacing=tick_spacing, + fee=fee + ).swap( + agent1, tkn_buy='B', tkn_sell='A', buy_quantity=buy_quantity + ) + + agent1_copy = Agent(holdings={'A': 1000}) + one_position_feeless = ConcentratedLiquidityPosition( + assets={'A': 10 / mpf(price), 'B': mpf(10)}, + min_tick=price_to_tick(price, tick_spacing), + tick_spacing=tick_spacing, + fee=0 + ).swap( + agent1_copy, tkn_buy='B', tkn_sell='A', buy_quantity=buy_quantity + ) + + agent2 = Agent(holdings={'A': 1000}) + whole_pool = ConcentratedLiquidityPoolState( + asset_list=['A', 'B'], + sqrt_price=mpf.sqrt(price), + liquidity=one_position.invariant, + tick_spacing = tick_spacing, + fee=fee + ).swap( + agent2, tkn_buy='B', tkn_sell='A', buy_quantity=buy_quantity + ) + + agent2_copy = Agent(holdings={'A': 1000}) + whole_pool_feeless = ConcentratedLiquidityPoolState( + asset_list=['A', 'B'], + sqrt_price=mpf.sqrt(price), + liquidity=one_position.invariant, + tick_spacing = tick_spacing, + fee=0 + ).swap( + agent2_copy, tkn_buy='B', tkn_sell='A', buy_quantity=buy_quantity + ) + + effective_fee_one_pool = (agent1_copy.holdings['A'] - agent1.holdings['A']) / (agent1_copy.initial_holdings['A'] - agent1_copy.holdings['A']) + effective_fee_whole_pool = (agent2_copy.holdings['A'] - agent2.holdings['A']) / (agent2_copy.initial_holdings['A'] - agent2_copy.holdings['A']) + if agent1.holdings['A'] != pytest.approx(agent2.holdings['A'], rel=1e-8): + raise AssertionError('Sell quantity was not calculated correctly.') + if agent1.holdings['B'] != agent2.holdings['B']: + raise AssertionError('Buy quantity was not applied correctly.') + if effective_fee_whole_pool != pytest.approx(effective_fee_one_pool, rel=1e-8): + raise AssertionError('Fee levels do not match.') + + +@given(st.integers(min_value=1, max_value=100), fee_strategy) +def test_sell_x_vs_single_position(initial_tick, fee): + tick_spacing = 100 + price = mpf(tick_to_price(initial_tick * tick_spacing + tick_spacing // 2)) + sell_quantity = mpf(1) + + agent1 = Agent(holdings={'A': 1000, 'B': 0}) + one_position = ConcentratedLiquidityPosition( + assets={'A':10 / mpf(price), 'B': mpf(10)}, + min_tick=price_to_tick(price, tick_spacing), + tick_spacing=tick_spacing, + fee=fee + ).swap( + agent1, tkn_buy='B', tkn_sell='A', sell_quantity=sell_quantity + ) + + agent1_copy = Agent(holdings={'A': 1000, 'B': 0}) + one_position_feeless = ConcentratedLiquidityPosition( + assets={'A': 10 / mpf(price), 'B': mpf(10)}, + min_tick=price_to_tick(price, tick_spacing), + tick_spacing=tick_spacing, + fee=0 + ).swap( + agent1_copy, tkn_buy='B', tkn_sell='A', sell_quantity=sell_quantity + ) + + agent2 = Agent(holdings={'A': 1000, 'B': 0}) + whole_pool = ConcentratedLiquidityPoolState( + asset_list=['A', 'B'], + sqrt_price=mpf.sqrt(price), + liquidity=one_position.invariant, + tick_spacing=tick_spacing, + fee=fee + ).swap( + agent2, tkn_buy='B', tkn_sell='A', sell_quantity=sell_quantity + ) + + agent2_copy = Agent(holdings={'A': 1000, 'B': 0}) + whole_pool_feeless = ConcentratedLiquidityPoolState( + asset_list=['A', 'B'], + sqrt_price=mpf.sqrt(price), + liquidity=one_position.invariant, + tick_spacing=tick_spacing, + fee=0 + ).swap( + agent2_copy, tkn_buy='B', tkn_sell='A', sell_quantity=sell_quantity + ) + + effective_fee_one_pool = (agent1_copy.holdings['B'] - agent1.holdings['B']) / ( + agent1_copy.initial_holdings['B'] - agent1_copy.holdings['B']) + effective_fee_whole_pool = (agent2_copy.holdings['B'] - agent2.holdings['B']) / ( + agent2_copy.initial_holdings['B'] - agent2_copy.holdings['B']) + if agent1.holdings['A'] != agent2.holdings['A']: + raise AssertionError('Sell quantity was not applied correctly.') + if agent1.holdings['B'] != pytest.approx(agent2.holdings['B'], rel=1e-6): + raise AssertionError('Buy quantity was not calculated correctly.') + if effective_fee_whole_pool != pytest.approx(effective_fee_one_pool, rel=1e-5): + raise AssertionError('Fee levels do not match.') + + +@given(st.integers(min_value=1, max_value=100), fee_strategy) +def test_sell_y_vs_single_position(initial_tick, fee): + tick_spacing = 100 + price = mpf(tick_to_price(initial_tick * tick_spacing + tick_spacing // 2)) + sell_quantity = mpf(10) + + agent1 = Agent(holdings={'A': 0, 'B': 1000}) + one_position = ConcentratedLiquidityPosition( + assets={'A': mpf(10 / price), 'B': mpf(10)}, + min_tick=price_to_tick(price, tick_spacing), + tick_spacing=tick_spacing, + fee=fee + ).swap( + agent1, tkn_buy='A', tkn_sell='B', sell_quantity=sell_quantity + ) + + agent1_copy = Agent(holdings={'A': 0, 'B': 1000}) + one_position_feeless = ConcentratedLiquidityPosition( + assets={'A': mpf(10 / price), 'B': mpf(10)}, + min_tick=price_to_tick(price, tick_spacing), + tick_spacing=tick_spacing, + fee=0 + ).swap( + agent1_copy, tkn_buy='A', tkn_sell='B', sell_quantity=sell_quantity + ) + + agent2 = Agent(holdings={'A': 0, 'B': 1000}) + whole_pool = ConcentratedLiquidityPoolState( + asset_list=['A', 'B'], + sqrt_price=mpf.sqrt(price), + liquidity=one_position.invariant, + tick_spacing=tick_spacing, + fee=fee + ).swap( + agent2, tkn_buy='A', tkn_sell='B', sell_quantity=sell_quantity + ) + + agent2_copy = Agent(holdings={'A': 0, 'B': 1000}) + whole_pool_feeless = ConcentratedLiquidityPoolState( + asset_list=['A', 'B'], + sqrt_price=mpf.sqrt(price), + liquidity=one_position.invariant, + tick_spacing=tick_spacing, + fee=0 + ).swap( + agent2_copy, tkn_buy='A', tkn_sell='B', sell_quantity=sell_quantity + ) + + effective_fee_one_pool = (agent1_copy.holdings['A'] - agent1.holdings['A']) / ( + agent1_copy.initial_holdings['A'] - agent1_copy.holdings['A']) + effective_fee_whole_pool = (agent2_copy.holdings['A'] - agent2.holdings['A']) / ( + agent2_copy.initial_holdings['A'] - agent2_copy.holdings['A']) + if agent1.holdings['B'] != agent2.holdings['B']: + raise AssertionError('Sell quantity was not applied correctly.') + if agent1.holdings['A'] != pytest.approx(agent2.holdings['A'], rel=1e-6): + raise AssertionError('Buy quantity was not calculated correctly.') + if effective_fee_whole_pool != pytest.approx(effective_fee_one_pool, rel=1e-6): + raise AssertionError('Fee levels do not match.') + + +@given(st.integers(min_value=1, max_value=100), fee_strategy) +def test_pool_sell_spot(initial_tick, fee): + tick_spacing = 100 + price = mpf(tick_to_price(initial_tick * tick_spacing + tick_spacing // 2)) + sell_quantity = mpf(1) / 10000000000 + + initial_state = ConcentratedLiquidityPoolState( + asset_list=['A', 'B'], + sqrt_price=mpf.sqrt(price), + liquidity=mpf(1000), + tick_spacing=tick_spacing, + fee=fee + ) + + agent = Agent(holdings={'A': 1000, 'B': 0}) + sell_spot = initial_state.sell_spot(tkn_sell='A', tkn_buy='B', fee=fee) + initial_state.swap( + agent, tkn_buy='B', tkn_sell='A', sell_quantity=sell_quantity + ) + ex_price = agent.holdings['B'] / (agent.initial_holdings['A'] - agent.holdings['A']) + if ex_price != pytest.approx(sell_spot, rel=1e-20): + raise AssertionError('Sell spot price was not calculated correctly.') + + +@given(st.integers(min_value=1, max_value=100), fee_strategy) +def test_pool_buy_spot(initial_tick, fee): + tick_spacing = 100 + price = mpf(tick_to_price(initial_tick * tick_spacing + tick_spacing // 2)) + buy_quantity = mpf(1) / 10000000000 + + initial_state = ConcentratedLiquidityPoolState( + asset_list=['A', 'B'], + sqrt_price=mpf.sqrt(price), + liquidity=mpf(1000), + tick_spacing=tick_spacing, + fee=fee + ) + + agent = Agent(holdings={'B': 1000, 'A': 0}) + buy_spot = initial_state.buy_spot(tkn_buy='A', tkn_sell='B', fee=fee) + initial_state.swap( + agent, tkn_buy='A', tkn_sell='B', buy_quantity=buy_quantity + ) + ex_price = (agent.initial_holdings['B'] - agent.holdings['B']) / agent.holdings['A'] + if ex_price != pytest.approx(buy_spot, rel=1e-20): + raise AssertionError('Buy spot price was not calculated correctly.') + +def test_vs_uniswap_quote(): + swap_size = 10 ** 20 + fee = 0.003 + + uniswap = get_uniswap_pool_data([('weth', 'usdc')]) + weth_usdc = uniswap[f'weth-usdc-{round(fee * 1000000)}'] + + ticks = weth_usdc.get_liquidity_distribution(0, 30) + uniswap_liquidity = weth_usdc.get_active_liquidity() + liquidity = mpf(uniswap_liquidity) + + uniswap_quote = weth_usdc.get_quote('weth', 'usdc', sell_quantity=swap_size) + weth_usdc.get_price() + + local_clone = ConcentratedLiquidityPoolState( + asset_list=['usdc', 'weth'], + sqrt_price=mpf.sqrt(mpf(1 / weth_usdc.price)), + liquidity=liquidity, + tick_spacing=weth_usdc.tick_spacing, + fee=fee + ).initialize_ticks(ticks) + agent = Agent(holdings={'weth': 1000}) + local_clone.swap( + agent, tkn_buy='usdc', tkn_sell='weth', sell_quantity=swap_size + ) + ex_price = agent.holdings['usdc'] + + print(f'Swap executed at {ex_price} vs Uniswap quote of {uniswap_quote}.') + print(f'Execution price deviated from quote by {int(ex_price) / uniswap_quote - 1:.12%}.') + # print(local_clone.tick_crossings) + diff = int(ex_price) / uniswap_quote - 1 + + if uniswap_quote != pytest.approx(ex_price, rel=1e-5): + raise AssertionError('Simulated pool did not match quote from Uniswap.') + + + + +def test_tick_crossing(): + tick_spacing = 60 + initial_tick = 6000 + individual_positions: dict[int: ConcentratedLiquidityPosition] = { + tick: ConcentratedLiquidityPosition( + assets={'A': 1000 / tick_to_price(mpf(tick + tick_spacing / 2)), 'B': mpf(1000)}, + min_tick=tick, + tick_spacing=tick_spacing, + fee=0 + ) + .swap( + agent=Agent(holdings={'A': float('inf')}), + tkn_buy='B', tkn_sell='A', buy_quantity=1000 + ) + for tick in range(initial_tick, initial_tick + tick_spacing * 10 + 1, tick_spacing) + } # every individual position is now at the bottom of its price range + initial_state = ConcentratedLiquidityPoolState( + asset_list=['A', 'B'], + sqrt_price=mpf.sqrt(individual_positions[initial_tick].price('A')), + liquidity=individual_positions[initial_tick].invariant, + tick_spacing=tick_spacing, + fee=0 + ).initialize_ticks({ + tick: individual_positions[tick].invariant - individual_positions[tick - tick_spacing].invariant + for tick in range(initial_tick + tick_spacing, initial_tick + tick_spacing * 10 + 1, tick_spacing) + }) # {current_tick + i * tick_spacing: -100 * (1 if i > 0 else -1) for i in range(-20, 20)}) + agent1 = Agent(holdings={'B': 10000}) + agent2 = agent1.copy() + + for position in list(individual_positions.values()): + position.swap( + agent1, tkn_buy='A', tkn_sell='B', buy_quantity=position.liquidity['A'] + ) + + initial_state.swap( + agent2, tkn_buy='A', tkn_sell='B', buy_quantity=agent1.holdings['A'] + ) + + if agent1.holdings['B'] != pytest.approx(agent2.holdings['B'], rel=1e-8): + raise AssertionError('Sell quantity was not applied correctly.') + + +def test_get_next_sqrt_price_from_amount_0(): + price = tick_to_price(mpf(6030)) + single_position = ConcentratedLiquidityPosition( + assets={'A': 1000 / price, 'B': 1000}, + min_tick=6000, + tick_spacing=60, + fee=0 + ) + initial_state = ConcentratedLiquidityPoolState( + liquidity=single_position.invariant, + asset_list=['A', 'B'], + sqrt_price=mpf.sqrt(single_position.price('A')) + ) + sell_quantity = mpf(100) + agent = Agent(holdings={'A': 1000}) + single_position.swap( + agent, tkn_buy='B', tkn_sell='A', sell_quantity=sell_quantity + ) + expected_sqrt_price = initial_state.getNextSqrtPriceFromAmount0(sell_quantity, True) + if expected_sqrt_price ** 2 != pytest.approx(single_position.price('A'), rel=1e-12): + raise AssertionError('Price was not calculated correctly.') + + if initial_state.getAmount0Delta(initial_state.sqrt_price, expected_sqrt_price) != pytest.approx(sell_quantity, rel=1e-12): + raise AssertionError('Amount0 delta was not calculated correctly.') + + if ( + initial_state.getAmount1Delta(initial_state.sqrt_price, expected_sqrt_price) + != pytest.approx(agent.holdings['B'], rel=1e-12) + ): + raise AssertionError('Amount1 delta was not calculated correctly.') + + +def test_get_next_sqrt_price_from_amount_1(): + price = tick_to_price(mpf(6030)) + single_position = ConcentratedLiquidityPosition( + assets={'A': 1000 / price, 'B': 1000}, + min_tick=6000, + tick_spacing=60, + fee=0 + ) + initial_state = ConcentratedLiquidityPoolState( + liquidity=single_position.invariant, + asset_list=['A', 'B'], + sqrt_price=mpf.sqrt(single_position.price('A')) + ) + buy_quantity = mpf(100) + agent = Agent(holdings={'A': 1000}) + single_position.swap( + agent, tkn_buy='B', tkn_sell='A', buy_quantity=buy_quantity + ) + expected_sqrt_price = initial_state.getNextSqrtPriceFromAmount1(buy_quantity, False) + if expected_sqrt_price ** 2 != pytest.approx(single_position.price('A'), rel=1e-12): + raise AssertionError('Price was not calculated correctly.') + + if initial_state.getAmount1Delta(initial_state.sqrt_price, expected_sqrt_price) != pytest.approx(buy_quantity, rel=1e-12): + raise AssertionError('Amount1 delta was not calculated correctly.') + + if ( + initial_state.getAmount0Delta(initial_state.sqrt_price, expected_sqrt_price) + != pytest.approx(agent.initial_holdings['A'] - agent.holdings['A'], rel=1e-12) + ): + raise AssertionError('Amount0 delta was not calculated correctly.') diff --git a/hydradx/tests/test_router.py b/hydradx/tests/test_router.py index bc7120b3..731ae5b0 100644 --- a/hydradx/tests/test_router.py +++ b/hydradx/tests/test_router.py @@ -654,51 +654,37 @@ def test_sell_spot_buy_stableswap_sell_stableswap(assets, lrna_fee, asset_fee, t tokens={"stable1": mpf(1000000), "stable2": mpf(1000000)}, amplification=100, trade_fee=trade_fee, unique_id="stablepool1", - precision=1e-08 + precision=1e-08, + spot_price_precision=mpf(1e-12) ) stablepool2 = StableSwapPoolState( tokens={"stable3": mpf(1000000), "stable4": mpf(1000000)}, amplification=1000, trade_fee=trade_fee, unique_id="stablepool2", - precision=1e-08 + precision=1e-08, + spot_price_precision=mpf(1e-12) ) - initial_agent = Agent( + agent = Agent( holdings={"stable1": mpf(1), "stable3": mpf(0)} ) tkn_sell = "stable1" tkn_buy = "stable3" - trade_size = mpf(1e-07) - - # debugging stuff - # step_1_sell_spot = 1 / stablepool1.add_liquidity_spot(tkn_add=tkn_sell) - # step_1_agent = initial_agent.copy() - # stablepool1.copy().add_liquidity(step_1_agent, quantity=trade_size, tkn_add=tkn_sell) - # step_1_sell_ex = step_1_agent.holdings['stablepool1'] / trade_size - # - # step_2_sell_spot = omnipool.sell_spot(tkn_sell='stablepool1', tkn_buy='stablepool2') - # step_2_agent = step_1_agent.copy() - # omnipool.copy().swap(step_2_agent, tkn_sell='stablepool1', tkn_buy='stablepool2', sell_quantity=step_1_agent.holdings['stablepool1']) - # step_2_sell_ex = step_2_agent.holdings['stablepool2'] / step_1_agent.holdings['stablepool1'] - # - # step_3_sell_spot = stablepool2.remove_liquidity_spot(tkn_remove=tkn_buy) - # step_3_agent = step_2_agent.copy() - # stablepool2.copy().remove_liquidity(step_3_agent, shares_removed=step_2_agent.holdings['stablepool2'], tkn_remove=tkn_buy) - # step_3_sell_ex = step_3_agent.holdings[tkn_buy] / step_2_agent.holdings['stablepool2'] + trade_size = mpf(1e-10) router = OmnipoolRouter({"omnipool": omnipool, "stablepool1": stablepool1, "stablepool2": stablepool2}) - test_router, test_agent = router.simulate_swap( - initial_agent, tkn_buy, tkn_sell, sell_quantity=trade_size + router.swap( + agent, tkn_buy, tkn_sell, sell_quantity=trade_size ) sell_spot = router.sell_spot(tkn_sell=tkn_sell, tkn_buy=tkn_buy) - sell_quantity = initial_agent.holdings[tkn_sell] - test_agent.holdings[tkn_sell] - buy_quantity = test_agent.holdings[tkn_buy] - initial_agent.holdings[tkn_buy] + sell_quantity = agent.initial_holdings[tkn_sell] - agent.holdings[tkn_sell] + buy_quantity = agent.holdings[tkn_buy] - agent.initial_holdings[tkn_buy] sell_ex = buy_quantity / sell_quantity if sell_quantity != trade_size: raise ValueError(f"actually sold {sell_quantity} != trade size {trade_size}") - if sell_spot != pytest.approx(sell_ex, rel=1e-06): + if sell_spot != pytest.approx(sell_ex, rel=1e-12): raise ValueError(f"spot price {sell_spot} != execution price {sell_ex}") diff --git a/requirements.txt b/requirements.txt index b4a8be10..e3e0702b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ zipp requests~=2.31.0 python-dotenv hydradx-api~=0.3.0 +web3~=4.10.0