From 84e39cce9fe172e682b9c518296d82a68ad312ea Mon Sep 17 00:00:00 2001 From: poliwop Date: Thu, 8 Aug 2024 15:18:54 -0500 Subject: [PATCH 01/28] initial changes --- hydradx/model/amm/global_state.py | 2 +- hydradx/model/amm/omnipool_amm.py | 116 ++++++++---- hydradx/tests/test_omnipool_amm.py | 271 ++++++++++++++++++++++++++++- 3 files changed, 351 insertions(+), 38 deletions(-) diff --git a/hydradx/model/amm/global_state.py b/hydradx/model/amm/global_state.py index cc5f2e27..bf2928b2 100644 --- a/hydradx/model/amm/global_state.py +++ b/hydradx/model/amm/global_state.py @@ -143,7 +143,7 @@ def market_prices(self, shares: dict) -> dict: for share_id in shares: # if shares are for a specific asset in a specific pool, get prices according to that pool if isinstance(share_id, tuple): - pool_id = share_id[0] + pool_id = share_id[0].split('_')[0] tkn_id = share_id[1] prices[share_id] = self.pools[pool_id].usd_price(self.pools[pool_id], tkn_id) diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index 87895b6f..5750f1fa 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -105,9 +105,9 @@ def __init__(self, if oracles is None or 'price' not in oracles: if last_oracle_values is None or 'price' not in last_oracle_values: - self.oracles['price'] = Oracle(sma_equivalent_length=19, first_block=Block(self)) + self.oracles['price'] = Oracle(sma_equivalent_length=9, first_block=Block(self)) else: - self.oracles['price'] = Oracle(sma_equivalent_length=19, first_block=Block(self), + self.oracles['price'] = Oracle(sma_equivalent_length=9, first_block=Block(self), last_values=last_oracle_values['price']) if last_oracle_values is not None and oracles is not None: self.oracles.update({ @@ -885,8 +885,31 @@ def migrate_lp( agent.holdings[old_pool_id] = 0 return self + + def calculate_remove_all_liquidity(self, agent: Agent, tkn_remove): + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = 0, 0, 0, 0, 0, 0 + for k in agent.holdings: + if len(k) > 1 and k[1] == tkn_remove: + k_split = k[0].split("_") + if len(k_split) == 1: + qa, r, q, s, b, l = self.calculate_remove_liquidity(agent, agent.holdings[k], tkn_remove) + delta_qa += qa + delta_r += r + delta_q += q + delta_s += s + delta_b += b + delta_l += l + elif k_split[0] == self.unique_id: + qa, r, q, s, b, l = self.calculate_remove_liquidity(agent, agent.holdings[k], tkn_remove, int(k_split[1])) + delta_qa += qa + delta_r += r + delta_q += q + delta_s += s + delta_b += b + delta_l += l + return delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l - def calculate_remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: str): + def calculate_remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: str, i: int = None): """ calculated the pool and agent deltas for removing liquidity from a sub pool return as a tuple in this order: @@ -899,6 +922,11 @@ def calculate_remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: delta_b (protocol shares) delta_l (LRNA imbalance) """ + if i is None: + k = (self.unique_id, tkn_remove) + else: + k = (self.unique_id + "_" + str(i), tkn_remove) + quantity = -abs(quantity) assert quantity <= 0, f"delta_S cannot be positive: {quantity}" # assert tkn_remove in self.asset_list, f"invalid token name: {tkn_remove}" @@ -906,13 +934,13 @@ def calculate_remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: raise AssertionError(f"Invalid token name: {tkn_remove}") # return 0, 0, 0, 0, 0, 0 - if (self.unique_id, tkn_remove) not in agent.share_prices: + if k not in agent.share_prices: raise AssertionError(f"Agent does not have a share price for {tkn_remove}") # return 0, 0, 0, 0, 0, 0 # determine if they should get some LRNA back as well as the asset they invested piq = lrna_price(self, tkn_remove) - p0 = agent.share_prices[(self.unique_id, tkn_remove)] + p0 = agent.share_prices[k] mult = (piq - p0) / (piq + p0) # Share update @@ -959,11 +987,12 @@ def add_liquidity( return self.fail_transaction('Quantity must be non-negative.', agent) delta_Q = lrna_price(self, tkn_add) * quantity - if (self.unique_id, tkn_add) in agent.holdings: - if agent.holdings[(self.unique_id, tkn_add)] != 0: - return self.fail_transaction(f'Agent already has liquidity in pool {tkn_add}.', agent) - else: - agent.holdings[(self.unique_id, tkn_add)] = 0 + k = (self.unique_id, tkn_add) + i = 0 + while k in agent.holdings: + i += 1 + k = (self.unique_id + "_" + str(i), tkn_add) + agent.holdings[k] = 0 if agent.holdings[tkn_add] < quantity: return self.fail_transaction( @@ -1013,8 +1042,8 @@ def add_liquidity( self.shares[tkn_add] += shares_added # shares go to provisioning agent - agent.holdings[(self.unique_id, tkn_add)] += shares_added - + agent.holdings[k] += shares_added + # L update: LRNA fees to be burned before they will start to accumulate again delta_L = quantity * lrna_price(self, tkn_add) * self.lrna_imbalance / self.lrna_total self.lrna_imbalance += delta_L @@ -1028,19 +1057,35 @@ def add_liquidity( # set price at which liquidity was added # use the minimum of the oracle price and the current price - agent.share_prices[(self.unique_id, tkn_add)] = lrna_price(self, tkn_add) - agent.delta_r[(self.unique_id, tkn_add)] = quantity + agent.share_prices[k] = lrna_price(self, tkn_add) + agent.delta_r[k] = quantity # update block self.current_block.lps[tkn_add] += quantity return self + + def remove_all_liquidity(self, agent: Agent, tkn_remove: str): + agent_assets = list(agent.holdings.keys()) + for k in agent_assets: + if len(k) > 1 and k[1] == tkn_remove: + k_split = k[0].split("_") + if len(k_split) == 1: + self.remove_liquidity(agent, agent.holdings[k], tkn_remove) + elif k[0].split("_")[0] == self.unique_id: + self.remove_liquidity(agent, agent.holdings[k], tkn_remove, int(k[0].split("_")[1])) + return self - def remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: str): + def remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: str, i: int = None): """ Remove liquidity from a sub pool. """ - + + if i is None: + k = (self.unique_id, tkn_remove) + else: + k = (self.unique_id + "_" + str(i), tkn_remove) + if tkn_remove not in self.asset_list: for sub_pool in self.sub_pools.values(): if tkn_remove in sub_pool.asset_list: @@ -1056,7 +1101,7 @@ def remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: str): if quantity == 0: return self - if not agent.is_holding((self.unique_id, tkn_remove)): + if not agent.is_holding(k): return self.fail_transaction('Agent does not have liquidity in this pool.', agent) if self.remove_liquidity_volatility_threshold: @@ -1079,13 +1124,18 @@ def remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: str): return self.fail_transaction( f"Transaction rejected because it would exceed the withdrawal limit: {quantity} > {max_remove}", agent ) - - delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = self.calculate_remove_liquidity( - agent, quantity, tkn_remove - ) + + if i is None: + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = self.calculate_remove_liquidity( + agent, quantity, tkn_remove + ) + else: + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = self.calculate_remove_liquidity( + agent, quantity, tkn_remove, i + ) if delta_r + self.liquidity[tkn_remove] < 0: return self.fail_transaction('Cannot remove more liquidity than exists in the pool.', agent) - elif quantity > agent.holdings[(self.unique_id, tkn_remove)]: + elif quantity > agent.holdings[k]: return self.fail_transaction('Cannot remove more liquidity than the agent has invested.', agent) self.liquidity[tkn_remove] += delta_r @@ -1097,9 +1147,11 @@ def remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: str): if 'LRNA' not in agent.holdings: agent.holdings['LRNA'] = 0 agent.holdings['LRNA'] += delta_qa - agent.holdings[(self.unique_id, tkn_remove)] -= quantity - if agent.holdings[(self.unique_id, tkn_remove)] == 0: - agent.share_prices[(self.unique_id, tkn_remove)] = 0 + agent.holdings[k] -= quantity + if agent.holdings[k] == 0: + agent.share_prices[k] = 0 + if tkn_remove not in agent.holdings: + agent.holdings[tkn_remove] = 0 agent.holdings[tkn_remove] -= delta_r self.current_block.withdrawals[tkn_remove] += quantity @@ -1438,11 +1490,12 @@ def cash_out_omnipool(omnipool: OmnipoolState, agent: Agent, prices) -> float: del agent_holdings[key] if agent.holdings[key] > 0: - delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = omnipool.calculate_remove_liquidity( - agent, - agent.holdings[key], - tkn_remove=tkn - ) + k_split = key[0].split("_") + # delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = omnipool.calculate_remove_all_liquidity(agent, tkn_remove=tkn) + if len(k_split) > 1: + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = omnipool.calculate_remove_liquidity(agent, agent.holdings[key], tkn, int(k_split[1])) + else: + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = omnipool.calculate_remove_liquidity(agent, agent.holdings[key], tkn) agent_holdings['LRNA'] += delta_qa if tkn not in agent_holdings: agent_holdings[tkn] = 0 @@ -1450,6 +1503,9 @@ def cash_out_omnipool(omnipool: OmnipoolState, agent: Agent, prices) -> float: liquidity_removed[tkn] -= delta_r lrna_removed[tkn] -= delta_q + if 'LRNA' in prices: + raise ValueError('LRNA price should not be given.') + if 'LRNA' not in prices and agent_holdings['LRNA'] > 0: lrna_total = omnipool.lrna_total - sum(lrna_removed.values()) lrna_sells = { diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index 86264d1c..86b5da36 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -9,7 +9,8 @@ from hydradx.model.amm import omnipool_amm as oamm from hydradx.model.amm.agents import Agent from hydradx.model.amm.global_state import GlobalState -from hydradx.model.amm.omnipool_amm import price, dynamicadd_asset_fee, dynamicadd_lrna_fee, OmnipoolState +from hydradx.model.amm.omnipool_amm import price, dynamicadd_asset_fee, dynamicadd_lrna_fee, OmnipoolState, \ + cash_out_omnipool from hydradx.model.amm.trade_strategies import constant_swaps, omnipool_arbitrage from hydradx.tests.strategies_omnipool import omnipool_reasonable_config, omnipool_config, assets_config @@ -150,19 +151,95 @@ def test_add_liquidity(initial_state: oamm.OmnipoolState): @settings(max_examples=1) @given(omnipool_config(token_count=3, lrna_fee=0, asset_fee=0)) -def test_add_liquidity_with_existing_position_fails(initial_state: oamm.OmnipoolState): +def test_add_liquidity_with_existing_position_succeeds(initial_state: oamm.OmnipoolState): old_state = initial_state tkn = old_state.asset_list[0] old_agent = Agent( - holdings={tkn: old_state.liquidity[tkn] / 10, (old_state.unique_id, tkn): old_state.shares[tkn] / 10} + holdings={tkn: old_state.liquidity[tkn], (old_state.unique_id, tkn): old_state.shares[tkn] / 10} ) - delta_R = old_agent.holdings[tkn] + delta_R = old_agent.holdings[tkn] / 10 - new_state, new_agents = oamm.simulate_add_liquidity(old_state, old_agent, delta_R, tkn) + new_state, new_agent = oamm.simulate_add_liquidity(old_state, old_agent, delta_R, tkn) - if not new_state.fail: - raise AssertionError(f'Adding liquidity to an existing position should fail.') + if new_state.fail: + raise AssertionError(f'Adding liquidity to an existing position should not fail.') + new_key = (old_state.unique_id + "_1", tkn) + if new_key not in new_agent.holdings or new_agent.holdings[new_key] <= 0: + raise AssertionError(f'New position not added to agent holdings.') + if len(new_agent.holdings) != len(old_agent.holdings) + 1: + raise AssertionError(f'Agent holdings have wrong length.') + + new_state2, new_agent2 = oamm.simulate_add_liquidity(new_state, new_agent, delta_R, tkn) + if new_state2.fail: + raise AssertionError(f'Adding liquidity to an existing position should not fail.') + new_key = (old_state.unique_id + "_2", tkn) + if new_key not in new_agent2.holdings or new_agent2.holdings[new_key] <= 0: + raise AssertionError(f'New position not added to agent holdings.') + if len(new_agent2.holdings) != len(new_agent.holdings) + 1: + raise AssertionError(f'Agent holdings have wrong length.') + + +@given(st.integers(min_value=1, max_value=10)) +def test_compare_several_lp_adds_to_single(n): + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + } + ) + tkn = initial_state.asset_list[0] + init_agent = Agent(holdings={tkn: initial_state.liquidity[tkn]}) + delta_R = init_agent.holdings[tkn] / 2 + + single_add_state, single_add_agent = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn) + multi_add_state, multi_add_agent = initial_state, init_agent + for i in range(n): + multi_add_state, multi_add_agent = oamm.simulate_add_liquidity(multi_add_state, multi_add_agent, delta_R / n, tkn) + + if single_add_state.liquidity[tkn] != pytest.approx(multi_add_state.liquidity[tkn], rel=1e-20): + raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') + if single_add_state.shares[tkn] != pytest.approx(multi_add_state.shares[tkn], rel=1e-20): + raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') + if single_add_state.lrna[tkn] != pytest.approx(multi_add_state.lrna[tkn], rel=1e-20): + raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') + total_multi_shares = multi_add_agent.holdings[(multi_add_state.unique_id, tkn)] + total_multi_shares += sum([multi_add_agent.holdings[(multi_add_state.unique_id + f'_{i}', tkn)] for i in range(1, n)]) + if total_multi_shares != pytest.approx(single_add_agent.holdings[(single_add_state.unique_id, tkn)], rel=1e-20): + raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') + + +def test_add_additional_positions_at_new_price(): + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + } + ) + tkn = 'DOT' + holdings = {tkn: initial_state.liquidity[tkn], (initial_state.unique_id, tkn): initial_state.shares[tkn] / 10} + share_prices = {(initial_state.unique_id, tkn): 8} + init_agent = Agent(holdings=holdings, share_prices=share_prices) + delta_R = init_agent.holdings[tkn] / 2 + + new_state, new_agent = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn) + + if new_agent.holdings[(initial_state.unique_id, tkn)] != init_agent.holdings[(initial_state.unique_id, tkn)]: + raise AssertionError(f'Adding liquidity should not change the shares in existing position') + if new_agent.share_prices[(initial_state.unique_id, tkn)] != init_agent.share_prices[(initial_state.unique_id, tkn)]: + raise AssertionError(f'Adding liquidity should not change the share price in existing position') + if new_agent.share_prices[(initial_state.unique_id + '_1', tkn)] != initial_state.price(initial_state, tkn): + raise AssertionError(f'Adding liquidity should set the share price in new position to the pool price') + + new_state2, new_agent2 = oamm.simulate_add_liquidity(new_state, new_agent, delta_R, tkn) + if new_agent2.holdings[(initial_state.unique_id + '_1', tkn)] != new_agent.holdings[(initial_state.unique_id + '_1', tkn)]: + raise AssertionError(f'Adding liquidity should not change the shares in existing position') + if new_agent2.share_prices[(initial_state.unique_id, tkn)] != new_agent.share_prices[(initial_state.unique_id, tkn)]: + raise AssertionError(f'Adding liquidity should not change the share price in existing position') + if new_agent2.share_prices[(initial_state.unique_id + '_1', tkn)] != new_state.price(initial_state, tkn): + raise AssertionError(f'Adding liquidity should set the share price in new position to the pool price') @settings(max_examples=1) @@ -356,6 +433,131 @@ def test_remove_liquidity_no_fee_different_price(initial_state: oamm.OmnipoolSta raise AssertionError(f'LRNA imbalance did not remain constant.') +@given(st.integers(min_value=1, max_value=2)) +def test_remove_liquidity_one_of_two(n): + + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + } + ) + tkn = 'DOT' + holdings = { + tkn: initial_state.liquidity[tkn], + (initial_state.unique_id, tkn): initial_state.shares[tkn] / 10, + (initial_state.unique_id + '_1', tkn): initial_state.shares[tkn] / 10 + } + share_prices = {(initial_state.unique_id, tkn): 8, (initial_state.unique_id + '_1', tkn): 12} + init_agent = Agent(holdings=holdings, share_prices=share_prices) + if n == 0: + k = (initial_state.unique_id, tkn) + k2 = (initial_state.unique_id + '_1', tkn) + else: + k = (initial_state.unique_id + '_1', tkn) + k2 = (initial_state.unique_id, tkn) + + quantity = init_agent.holdings[k] + + comp_holdings = {k: v for k, v in holdings.items()} + del comp_holdings[k2] + comp_prices = {k: v for k, v in share_prices.items()} + del comp_prices[k2] + comp_agent = Agent(holdings=comp_holdings, share_prices=comp_prices) + + agent = init_agent.copy() + pool = initial_state.copy() + comp_pool = initial_state.copy() + if n == 0: + pool.remove_liquidity(agent, quantity, tkn) + comp_pool.remove_liquidity(comp_agent, quantity, tkn) + else: + pool.remove_liquidity(agent, quantity, tkn, n) + comp_pool.remove_liquidity(comp_agent, quantity, tkn, n) + + assert pool.liquidity[tkn] == comp_pool.liquidity[tkn] + + +@given(st.floats(min_value=1, max_value=100)) +def test_remove_liquidity_split(price: float): + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + }, + withdrawal_fee=False + ) + tkn = 'DOT' + amt1 = initial_state.shares[tkn] / 5 + amt2 = amt1 / 2 + amt3 = amt1 - amt2 + holdings1 = {(initial_state.unique_id, tkn): amt1} + holdings2 = {(initial_state.unique_id, tkn): amt2, (initial_state.unique_id + '_1', tkn): amt3} + prices1 = {(initial_state.unique_id, tkn): price} + prices2 = {(initial_state.unique_id, tkn): price, (initial_state.unique_id + '_1', tkn): price} + state1 = initial_state.copy() + state2 = initial_state.copy() + agent1 = Agent(holdings=holdings1, share_prices=prices1) + agent2 = Agent(holdings=holdings2, share_prices=prices2) + state1.remove_all_liquidity(agent1, tkn) + state2.remove_all_liquidity(agent2, tkn) + + assert state1.liquidity[tkn] == pytest.approx(state2.liquidity[tkn], rel=1e-20) + assert state1.shares[tkn] == pytest.approx(state2.shares[tkn], rel=1e-20) + assert agent1.holdings[tkn] == pytest.approx(agent2.holdings[tkn], rel=1e-20) + assert agent1.holdings[(initial_state.unique_id, tkn)] == 0 + assert agent2.holdings[(initial_state.unique_id, tkn)] == 0 + assert agent2.holdings[(initial_state.unique_id + '_1', tkn)] == 0 + + # test if this holds with remove_liquidity as well + holdings3 = {(initial_state.unique_id, tkn): amt2, (initial_state.unique_id + '_1', tkn): amt3} + prices3 = {(initial_state.unique_id, tkn): price, (initial_state.unique_id + '_1', tkn): price} + state3 = initial_state.copy() + agent3 = Agent(holdings=holdings3, share_prices=prices3) + state3.remove_liquidity(agent3, amt2, tkn) + state3.remove_liquidity(agent3, amt3, tkn, 1) + + assert agent3.holdings[tkn] == pytest.approx(agent2.holdings[tkn], rel=1e-20) + assert agent3.holdings[(initial_state.unique_id, tkn)] == 0 + assert agent3.holdings[(initial_state.unique_id + '_1', tkn)] == 0 + + +@given(st.floats(min_value=1, max_value=100), st.floats(min_value=1, max_value=100), + st.floats(min_value=0.1, max_value = 0.9)) +def test_remove_all_liquidity(price1: float, price2: float, r: float): + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + }, + withdrawal_fee=False + ) + tkn = 'DOT' + amt1 = r * initial_state.shares[tkn] / 5 + amt2 = initial_state.shares[tkn] / 5 - amt1 + holdings1 = {(initial_state.unique_id, tkn): amt1, (initial_state.unique_id + '_1', tkn): amt2} + holdings2 = {k: v for k, v in holdings1.items()} + prices1 = {(initial_state.unique_id, tkn): price1, (initial_state.unique_id + '_1', tkn): price2} + prices2 = {k: v for k, v in prices1.items()} + state1 = initial_state.copy() + state2 = initial_state.copy() + agent1 = Agent(holdings=holdings1, share_prices=prices1) + agent2 = Agent(holdings=holdings2, share_prices=prices2) + state1.remove_all_liquidity(agent1, tkn) + state2.remove_liquidity(agent2, amt1, tkn) + state2.remove_liquidity(agent2, amt2, tkn, 1) + + assert state1.liquidity[tkn] == pytest.approx(state2.liquidity[tkn], rel=1e-20) + assert state1.shares[tkn] == pytest.approx(state2.shares[tkn], rel=1e-20) + assert agent1.holdings[tkn] == pytest.approx(agent2.holdings[tkn], rel=1e-20) + assert agent1.holdings[(initial_state.unique_id, tkn)] == 0 + assert agent2.holdings[(initial_state.unique_id, tkn)] == 0 + assert agent2.holdings[(initial_state.unique_id + '_1', tkn)] == 0 + + @given(omnipool_config(token_count=3)) def test_swap_lrna(initial_state: oamm.OmnipoolState): old_state = initial_state @@ -2669,3 +2871,58 @@ def test_fee_application(): buy_quantity_2 = sell_lrna_agent.holdings['USD'] if buy_quantity != pytest.approx(buy_quantity_2, rel=1e-12): raise AssertionError("Direct swap was not equivalent to LRNA swap with fees applied manually.") + + +@given(st.floats(min_value=10.1, max_value=100), + st.floats(min_value=10.1, max_value=100), + st.floats(min_value=0.1, max_value=0.9)) +def test_cash_out_multiple_positions_works_no_lrna(price1: float, price2: float, r: float): + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + }, + withdrawal_fee=False + ) + dot_spot_price = initial_state.price(initial_state, 'DOT', 'USD') + tkn = 'DOT' + amt1 = r * initial_state.shares[tkn] / 5 + amt2 = initial_state.shares[tkn] / 5 - amt1 + holdings1 = {(initial_state.unique_id, tkn): amt1, (initial_state.unique_id + '_1', tkn): amt2} + prices1 = {(initial_state.unique_id, tkn): price1, (initial_state.unique_id + '_1', tkn): price2} + agent = Agent(holdings=holdings1, share_prices=prices1) + cash_out = cash_out_omnipool(initial_state, agent, {'DOT': dot_spot_price}) + + state = initial_state.copy() + state.remove_all_liquidity(agent, tkn) + dot_value = agent.holdings['DOT'] * dot_spot_price + assert cash_out == pytest.approx(dot_value, rel=1e-20) + + +@given(st.floats(min_value=0.1, max_value=9.9), + st.floats(min_value=0.1, max_value=9.9), + st.floats(min_value=0.1, max_value=0.9)) +def test_cash_out_multiple_positions_works_with_lrna(price1: float, price2: float, r: float): + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + }, + withdrawal_fee=False + ) + tkn = 'DOT' + amt1 = r * initial_state.shares[tkn] / 5 + amt2 = initial_state.shares[tkn] / 5 - amt1 + holdings1 = {(initial_state.unique_id, tkn): amt1, (initial_state.unique_id + '_1', tkn): amt2} + prices1 = {(initial_state.unique_id, tkn): price1, (initial_state.unique_id + '_1', tkn): price2} + agent = Agent(holdings=holdings1, share_prices=prices1) + spot_prices = {tkn: initial_state.price(initial_state, tkn, 'USD') for tkn in initial_state.asset_list} + cash_out = cash_out_omnipool(initial_state, agent, spot_prices) + + state = initial_state.copy() + state.remove_all_liquidity(agent, tkn) + dot_value = agent.holdings['DOT'] * spot_prices['DOT'] + lrna_value = agent.holdings['LRNA'] * initial_state.price(initial_state, 'LRNA', 'USD') + assert dot_value < cash_out < dot_value + lrna_value # cash_out will be less than dot + lrna due to slippage From e7e2e68d69aa4cf8c74537a0982ab247012d64a8 Mon Sep 17 00:00:00 2001 From: poliwop Date: Thu, 8 Aug 2024 16:51:04 -0500 Subject: [PATCH 02/28] WIP, reimplementing multiple LP positions in same asset to use agent.nfts --- hydradx/model/amm/agents.py | 7 +- hydradx/model/amm/omnipool_amm.py | 60 +++++++---- hydradx/tests/test_omnipool_amm.py | 159 ++++++++++++++++------------- 3 files changed, 135 insertions(+), 91 deletions(-) diff --git a/hydradx/model/amm/agents.py b/hydradx/model/amm/agents.py index 3178e2a7..edfa4620 100644 --- a/hydradx/model/amm/agents.py +++ b/hydradx/model/amm/agents.py @@ -9,7 +9,8 @@ def __init__(self, share_prices: dict[str: float] = None, delta_r: dict[str: float] = None, trade_strategy: any = None, - unique_id: str = 'agent' + unique_id: str = 'agent', + nfts: dict[str: any] = None, ): """ holdings should be in the form of: @@ -29,6 +30,7 @@ def __init__(self, self.trade_strategy = trade_strategy self.asset_list = list(self.holdings.keys()) self.unique_id = unique_id + self.nfts = nfts or {} def __repr__(self): precision = 10 @@ -52,7 +54,8 @@ def copy(self): share_prices={k: v for k, v in self.share_prices.items()}, delta_r={k: v for k, v in self.delta_r.items()}, trade_strategy=self.trade_strategy, - unique_id=self.unique_id + unique_id=self.unique_id, + nfts={id: copy.deepcopy(nft) for id, nft in self.nfts.items()} ) copy_self.initial_holdings = {k: v for k, v in self.initial_holdings.items()} copy_self.asset_list = [tkn for tkn in self.asset_list] diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index 5750f1fa..e2149e0d 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -979,20 +979,23 @@ def add_liquidity( self, agent: Agent = None, quantity: float = 0, - tkn_add: str = '' + tkn_add: str = '', + nft_id: str = None ): """Compute new state after liquidity addition""" if quantity <= 0: return self.fail_transaction('Quantity must be non-negative.', agent) - + delta_Q = lrna_price(self, tkn_add) * quantity - k = (self.unique_id, tkn_add) - i = 0 - while k in agent.holdings: - i += 1 - k = (self.unique_id + "_" + str(i), tkn_add) - agent.holdings[k] = 0 + + if nft_id is None and (self.unique_id, tkn_add) in agent.holdings: + return self.fail_transaction( + 'Agent already has liquidity in this pool. Try using nft_id input.', agent + ) + + if nft_id is not None and nft_id in agent.nfts: + raise AssertionError('Agent already has an NFT with this ID.') if agent.holdings[tkn_add] < quantity: return self.fail_transaction( @@ -1024,7 +1027,8 @@ def add_liquidity( return self.add_liquidity( agent=agent, quantity=agent.holdings[sub_pool.unique_id] - old_agent_holdings, - tkn_add=sub_pool.unique_id + tkn_add=sub_pool.unique_id, + nft_id=nft_id ) raise AssertionError(f"invalid value for i: {tkn_add}") else: @@ -1040,9 +1044,6 @@ def add_liquidity( else: shares_added = delta_Q self.shares[tkn_add] += shares_added - - # shares go to provisioning agent - agent.holdings[k] += shares_added # L update: LRNA fees to be burned before they will start to accumulate again delta_L = quantity * lrna_price(self, tkn_add) * self.lrna_imbalance / self.lrna_total @@ -1053,12 +1054,20 @@ def add_liquidity( # Token amounts update self.liquidity[tkn_add] += quantity + + # agent update agent.holdings[tkn_add] -= quantity - - # set price at which liquidity was added - # use the minimum of the oracle price and the current price - agent.share_prices[k] = lrna_price(self, tkn_add) - agent.delta_r[k] = quantity + if nft_id is None: + k = (self.unique_id, tkn_add) + agent.holdings[k] = 0 + # shares go to provisioning agent + agent.holdings[k] += shares_added + # set price at which liquidity was added + agent.share_prices[k] = lrna_price(self, tkn_add) + agent.delta_r[k] = quantity + else: + lp_position = OmnipoolLiquidityPosition(tkn_add, lrna_price(self, tkn_add), shares_added, quantity) + agent.nfts[nft_id] = lp_position # update block self.current_block.lps[tkn_add] += quantity @@ -1191,6 +1200,18 @@ def value_assets(self, assets: dict[str, float], equivalency_map: dict[str, str] return value +class OmnipoolLiquidityPosition: + def __init__(self, tkn: str, price: float, shares: float, delta_r: float): + self.tkn = tkn + self.price = price + self.shares = shares + self.delta_r = delta_r + + def copy(self): + return OmnipoolLiquidityPosition(self.tkn, self.price, self.shares, self.delta_r) + + + class OmnipoolArchiveState: def __init__(self, state: OmnipoolState): self.asset_list = [tkn for tkn in state.asset_list] @@ -1325,13 +1346,14 @@ def simulate_add_liquidity( old_state: OmnipoolState, old_agent: Agent, quantity: float = 0, - tkn_add: str = '' + tkn_add: str = '', + nft_id: str = None ) -> tuple[OmnipoolState, Agent]: """Copy state, then add liquidity and return new state""" new_state = old_state.copy() new_agent = old_agent.copy() - new_state.add_liquidity(new_agent, quantity, tkn_add) + new_state.add_liquidity(new_agent, quantity, tkn_add, nft_id) return new_state, new_agent diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index 86b5da36..ae9c8f9b 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -149,6 +149,23 @@ def test_add_liquidity(initial_state: oamm.OmnipoolState): raise AssertionError(f'legal transaction failed against weight limit in {i} ({new_state.fail})') +@settings(max_examples=1) +@given(omnipool_config(token_count=3, lrna_fee=0, asset_fee=0)) +def test_add_liquidity_with_existing_position_fails(initial_state: oamm.OmnipoolState): + old_state = initial_state + tkn = old_state.asset_list[0] + old_agent = Agent( + holdings={tkn: old_state.liquidity[tkn] / 10, (old_state.unique_id, tkn): old_state.shares[tkn] / 10} + ) + + delta_R = old_agent.holdings[tkn] + + new_state, new_agents = oamm.simulate_add_liquidity(old_state, old_agent, delta_R, tkn) + + if not new_state.fail: + raise AssertionError(f'Adding liquidity to an existing position should fail.') + + @settings(max_examples=1) @given(omnipool_config(token_count=3, lrna_fee=0, asset_fee=0)) def test_add_liquidity_with_existing_position_succeeds(initial_state: oamm.OmnipoolState): @@ -160,86 +177,88 @@ def test_add_liquidity_with_existing_position_succeeds(initial_state: oamm.Omnip delta_R = old_agent.holdings[tkn] / 10 - new_state, new_agent = oamm.simulate_add_liquidity(old_state, old_agent, delta_R, tkn) + nft_id = "first_position" + new_state, new_agent = oamm.simulate_add_liquidity(old_state, old_agent, delta_R, tkn, nft_id) if new_state.fail: raise AssertionError(f'Adding liquidity to an existing position should not fail.') - new_key = (old_state.unique_id + "_1", tkn) - if new_key not in new_agent.holdings or new_agent.holdings[new_key] <= 0: - raise AssertionError(f'New position not added to agent holdings.') - if len(new_agent.holdings) != len(old_agent.holdings) + 1: + if nft_id not in new_agent.nfts: + raise AssertionError(f'LP position not added to agent NFTs.') + if len(new_agent.holdings) != len(old_agent.holdings): raise AssertionError(f'Agent holdings have wrong length.') - new_state2, new_agent2 = oamm.simulate_add_liquidity(new_state, new_agent, delta_R, tkn) + nft_id2 = "second_position" + new_state2, new_agent2 = oamm.simulate_add_liquidity(new_state, new_agent, delta_R, tkn, nft_id2) if new_state2.fail: raise AssertionError(f'Adding liquidity to an existing position should not fail.') - new_key = (old_state.unique_id + "_2", tkn) - if new_key not in new_agent2.holdings or new_agent2.holdings[new_key] <= 0: - raise AssertionError(f'New position not added to agent holdings.') - if len(new_agent2.holdings) != len(new_agent.holdings) + 1: + if nft_id2 not in new_agent2.nfts: + raise AssertionError(f'LP position not added to agent NFTs.') + if len(new_agent2.holdings) != len(old_agent.holdings): raise AssertionError(f'Agent holdings have wrong length.') -@given(st.integers(min_value=1, max_value=10)) -def test_compare_several_lp_adds_to_single(n): - liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} - lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} - initial_state = oamm.OmnipoolState( - tokens={ - tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna - } - ) - tkn = initial_state.asset_list[0] - init_agent = Agent(holdings={tkn: initial_state.liquidity[tkn]}) - delta_R = init_agent.holdings[tkn] / 2 - - single_add_state, single_add_agent = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn) - multi_add_state, multi_add_agent = initial_state, init_agent - for i in range(n): - multi_add_state, multi_add_agent = oamm.simulate_add_liquidity(multi_add_state, multi_add_agent, delta_R / n, tkn) - - if single_add_state.liquidity[tkn] != pytest.approx(multi_add_state.liquidity[tkn], rel=1e-20): - raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') - if single_add_state.shares[tkn] != pytest.approx(multi_add_state.shares[tkn], rel=1e-20): - raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') - if single_add_state.lrna[tkn] != pytest.approx(multi_add_state.lrna[tkn], rel=1e-20): - raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') - total_multi_shares = multi_add_agent.holdings[(multi_add_state.unique_id, tkn)] - total_multi_shares += sum([multi_add_agent.holdings[(multi_add_state.unique_id + f'_{i}', tkn)] for i in range(1, n)]) - if total_multi_shares != pytest.approx(single_add_agent.holdings[(single_add_state.unique_id, tkn)], rel=1e-20): - raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') - - -def test_add_additional_positions_at_new_price(): - liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} - lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} - initial_state = oamm.OmnipoolState( - tokens={ - tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna - } - ) - tkn = 'DOT' - holdings = {tkn: initial_state.liquidity[tkn], (initial_state.unique_id, tkn): initial_state.shares[tkn] / 10} - share_prices = {(initial_state.unique_id, tkn): 8} - init_agent = Agent(holdings=holdings, share_prices=share_prices) - delta_R = init_agent.holdings[tkn] / 2 - - new_state, new_agent = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn) - - if new_agent.holdings[(initial_state.unique_id, tkn)] != init_agent.holdings[(initial_state.unique_id, tkn)]: - raise AssertionError(f'Adding liquidity should not change the shares in existing position') - if new_agent.share_prices[(initial_state.unique_id, tkn)] != init_agent.share_prices[(initial_state.unique_id, tkn)]: - raise AssertionError(f'Adding liquidity should not change the share price in existing position') - if new_agent.share_prices[(initial_state.unique_id + '_1', tkn)] != initial_state.price(initial_state, tkn): - raise AssertionError(f'Adding liquidity should set the share price in new position to the pool price') - - new_state2, new_agent2 = oamm.simulate_add_liquidity(new_state, new_agent, delta_R, tkn) - if new_agent2.holdings[(initial_state.unique_id + '_1', tkn)] != new_agent.holdings[(initial_state.unique_id + '_1', tkn)]: - raise AssertionError(f'Adding liquidity should not change the shares in existing position') - if new_agent2.share_prices[(initial_state.unique_id, tkn)] != new_agent.share_prices[(initial_state.unique_id, tkn)]: - raise AssertionError(f'Adding liquidity should not change the share price in existing position') - if new_agent2.share_prices[(initial_state.unique_id + '_1', tkn)] != new_state.price(initial_state, tkn): - raise AssertionError(f'Adding liquidity should set the share price in new position to the pool price') +# TODO adjust these tests using nft_id input + +# @given(st.integers(min_value=1, max_value=10)) +# def test_compare_several_lp_adds_to_single(n): +# liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} +# lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} +# initial_state = oamm.OmnipoolState( +# tokens={ +# tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna +# } +# ) +# tkn = initial_state.asset_list[0] +# init_agent = Agent(holdings={tkn: initial_state.liquidity[tkn]}) +# delta_R = init_agent.holdings[tkn] / 2 +# +# single_add_state, single_add_agent = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn) +# multi_add_state, multi_add_agent = initial_state, init_agent +# for i in range(n): +# multi_add_state, multi_add_agent = oamm.simulate_add_liquidity(multi_add_state, multi_add_agent, delta_R / n, tkn) +# +# if single_add_state.liquidity[tkn] != pytest.approx(multi_add_state.liquidity[tkn], rel=1e-20): +# raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') +# if single_add_state.shares[tkn] != pytest.approx(multi_add_state.shares[tkn], rel=1e-20): +# raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') +# if single_add_state.lrna[tkn] != pytest.approx(multi_add_state.lrna[tkn], rel=1e-20): +# raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') +# total_multi_shares = multi_add_agent.holdings[(multi_add_state.unique_id, tkn)] +# total_multi_shares += sum([multi_add_agent.holdings[(multi_add_state.unique_id + f'_{i}', tkn)] for i in range(1, n)]) +# if total_multi_shares != pytest.approx(single_add_agent.holdings[(single_add_state.unique_id, tkn)], rel=1e-20): +# raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') +# +# +# def test_add_additional_positions_at_new_price(): +# liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} +# lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} +# initial_state = oamm.OmnipoolState( +# tokens={ +# tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna +# } +# ) +# tkn = 'DOT' +# holdings = {tkn: initial_state.liquidity[tkn], (initial_state.unique_id, tkn): initial_state.shares[tkn] / 10} +# share_prices = {(initial_state.unique_id, tkn): 8} +# init_agent = Agent(holdings=holdings, share_prices=share_prices) +# delta_R = init_agent.holdings[tkn] / 2 +# +# new_state, new_agent = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn) +# +# if new_agent.holdings[(initial_state.unique_id, tkn)] != init_agent.holdings[(initial_state.unique_id, tkn)]: +# raise AssertionError(f'Adding liquidity should not change the shares in existing position') +# if new_agent.share_prices[(initial_state.unique_id, tkn)] != init_agent.share_prices[(initial_state.unique_id, tkn)]: +# raise AssertionError(f'Adding liquidity should not change the share price in existing position') +# if new_agent.share_prices[(initial_state.unique_id + '_1', tkn)] != initial_state.price(initial_state, tkn): +# raise AssertionError(f'Adding liquidity should set the share price in new position to the pool price') +# +# new_state2, new_agent2 = oamm.simulate_add_liquidity(new_state, new_agent, delta_R, tkn) +# if new_agent2.holdings[(initial_state.unique_id + '_1', tkn)] != new_agent.holdings[(initial_state.unique_id + '_1', tkn)]: +# raise AssertionError(f'Adding liquidity should not change the shares in existing position') +# if new_agent2.share_prices[(initial_state.unique_id, tkn)] != new_agent.share_prices[(initial_state.unique_id, tkn)]: +# raise AssertionError(f'Adding liquidity should not change the share price in existing position') +# if new_agent2.share_prices[(initial_state.unique_id + '_1', tkn)] != new_state.price(initial_state, tkn): +# raise AssertionError(f'Adding liquidity should set the share price in new position to the pool price') @settings(max_examples=1) From 909ab4bb3860faa7ea05916a26bb979ea5533af5 Mon Sep 17 00:00:00 2001 From: poliwop Date: Fri, 9 Aug 2024 15:13:41 -0500 Subject: [PATCH 03/28] WIP, reimplementing multiple LP positions in same asset to use agent.nfts --- hydradx/model/amm/omnipool_amm.py | 242 ++++++++++++++++++----------- hydradx/tests/test_omnipool_amm.py | 97 +++++++----- 2 files changed, 216 insertions(+), 123 deletions(-) diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index e2149e0d..ccf837a5 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -886,30 +886,73 @@ def migrate_lp( return self - def calculate_remove_all_liquidity(self, agent: Agent, tkn_remove): - delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = 0, 0, 0, 0, 0, 0 - for k in agent.holdings: - if len(k) > 1 and k[1] == tkn_remove: - k_split = k[0].split("_") - if len(k_split) == 1: - qa, r, q, s, b, l = self.calculate_remove_liquidity(agent, agent.holdings[k], tkn_remove) - delta_qa += qa - delta_r += r - delta_q += q - delta_s += s - delta_b += b - delta_l += l - elif k_split[0] == self.unique_id: - qa, r, q, s, b, l = self.calculate_remove_liquidity(agent, agent.holdings[k], tkn_remove, int(k_split[1])) - delta_qa += qa - delta_r += r - delta_q += q - delta_s += s - delta_b += b - delta_l += l - return delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l - - def calculate_remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: str, i: int = None): + # def calculate_remove_all_liquidity(self, agent: Agent, tkn_remove): + # delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = 0, 0, 0, 0, 0, 0 + # for k in agent.holdings: + # if len(k) > 1 and k[1] == tkn_remove: + # k_split = k[0].split("_") + # if len(k_split) == 1: + # qa, r, q, s, b, l = self.calculate_remove_liquidity(agent, agent.holdings[k], tkn_remove) + # delta_qa += qa + # delta_r += r + # delta_q += q + # delta_s += s + # delta_b += b + # delta_l += l + # elif k_split[0] == self.unique_id: + # qa, r, q, s, b, l = self.calculate_remove_liquidity(agent, agent.holdings[k], tkn_remove, int(k_split[1])) + # delta_qa += qa + # delta_r += r + # delta_q += q + # delta_s += s + # delta_b += b + # delta_l += l + # return delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l + + def calculate_remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str = None, nft_id: str = None): + """ + If quantity is specified and nft_id is specified, remove specified quantity of shares from specified position. + If quantity is specified and nft_id is unspecified, remove specified quantity of shares from holdings. + If quantity is unspecified and nft_id is specified, remove specified position. + If quantity is unspecified and nft_id is unspecified, remove all liquidity. + """ + + if tkn_remove is None: + if nft_id is None: + raise AssertionError('tkn_remove must be specified if nft_id is not provided.') + else: + tkn_remove = agent.nfts[nft_id].tkn + + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l, nft_ids = 0, 0, 0, 0, 0, 0, [] + if quantity is not None: + if nft_id is None: # remove specified quantity of shares from holdings + k = (self.unique_id, tkn_remove) + return self._calculate_remove_one_position( + quantity=quantity, tkn_remove=tkn_remove, share_price=agent.share_prices[k] + ) + else: # remove specified quantity of shares from specified position + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = self._calculate_remove_one_position( + quantity=quantity, tkn_remove=tkn_remove, share_price=agent.nfts[nft_id].price + ) + nft_ids = [nft_id] + else: + if nft_id is not None: # remove specified position + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = self._calculate_remove_one_position( + quantity=agent.nfts[nft_id].shares, tkn_remove=tkn_remove, share_price=agent.nfts[nft_id].price + ) + nft_ids = [nft_id] + else: # remove all liquidity + for nft_id in agent.nfts: + nft = agent.nfts[nft_id] + if isinstance(nft, OmnipoolLiquidityPosition): + if nft.pool_id == self.unique_id and nft.tkn == tkn_remove: + nft_ids.append(nft_id) + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = self._calculate_remove_one_position( + quantity=nft.shares, tkn_remove=tkn_remove, share_price=nft.price + ) + return delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l, nft_ids + + def _calculate_remove_one_position(self, quantity, tkn_remove, share_price): """ calculated the pool and agent deltas for removing liquidity from a sub pool return as a tuple in this order: @@ -922,25 +965,16 @@ def calculate_remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: delta_b (protocol shares) delta_l (LRNA imbalance) """ - if i is None: - k = (self.unique_id, tkn_remove) - else: - k = (self.unique_id + "_" + str(i), tkn_remove) + k = (self.unique_id, tkn_remove) quantity = -abs(quantity) assert quantity <= 0, f"delta_S cannot be positive: {quantity}" - # assert tkn_remove in self.asset_list, f"invalid token name: {tkn_remove}" if tkn_remove not in self.asset_list: raise AssertionError(f"Invalid token name: {tkn_remove}") - # return 0, 0, 0, 0, 0, 0 - - if k not in agent.share_prices: - raise AssertionError(f"Agent does not have a share price for {tkn_remove}") - # return 0, 0, 0, 0, 0, 0 # determine if they should get some LRNA back as well as the asset they invested piq = lrna_price(self, tkn_remove) - p0 = agent.share_prices[k] + p0 = share_price mult = (piq - p0) / (piq + p0) # Share update @@ -952,8 +986,6 @@ def calculate_remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: delta_r = delta_q / piq if piq > p0: # prevents rounding errors - if 'LRNA' not in agent.holdings: - agent.holdings['LRNA'] = 0 delta_qa = -piq * ( 2 * piq / (piq + p0) * quantity / self.shares[tkn_remove] * self.liquidity[tkn_remove] - delta_r @@ -1066,7 +1098,7 @@ def add_liquidity( agent.share_prices[k] = lrna_price(self, tkn_add) agent.delta_r[k] = quantity else: - lp_position = OmnipoolLiquidityPosition(tkn_add, lrna_price(self, tkn_add), shares_added, quantity) + lp_position = OmnipoolLiquidityPosition(tkn_add, lrna_price(self, tkn_add), shares_added, quantity, self.unique_id) agent.nfts[nft_id] = lp_position # update block @@ -1074,26 +1106,32 @@ def add_liquidity( return self - def remove_all_liquidity(self, agent: Agent, tkn_remove: str): - agent_assets = list(agent.holdings.keys()) - for k in agent_assets: - if len(k) > 1 and k[1] == tkn_remove: - k_split = k[0].split("_") - if len(k_split) == 1: - self.remove_liquidity(agent, agent.holdings[k], tkn_remove) - elif k[0].split("_")[0] == self.unique_id: - self.remove_liquidity(agent, agent.holdings[k], tkn_remove, int(k[0].split("_")[1])) - return self + # def remove_all_liquidity(self, agent: Agent, tkn_remove: str): + # agent_assets = list(agent.holdings.keys()) + # for k in agent_assets: + # if len(k) > 1 and k[1] == tkn_remove: + # k_split = k[0].split("_") + # if len(k_split) == 1: + # self.remove_liquidity(agent, agent.holdings[k], tkn_remove) + # elif k[0].split("_")[0] == self.unique_id: + # self.remove_liquidity(agent, agent.holdings[k], tkn_remove, int(k[0].split("_")[1])) + # return self - def remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: str, i: int = None): + def remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str = '', nft_id: str = None): """ Remove liquidity from a sub pool. + If quantity is specified and nft_id is specified, remove specified quantity of shares from specified position. + If quantity is specified and nft_id is unspecified, remove specified quantity of shares from holdings. + If quantity is unspecified and nft_id is specified, remove specified position. + If quantity is unspecified and nft_id is unspecified, remove all liquidity. """ - if i is None: - k = (self.unique_id, tkn_remove) - else: - k = (self.unique_id + "_" + str(i), tkn_remove) + # if i is None: + # k = (self.unique_id, tkn_remove) + # else: + # k = (self.unique_id + "_" + str(i), tkn_remove) + + k = (self.unique_id, tkn_remove) if tkn_remove not in self.asset_list: for sub_pool in self.sub_pools.values(): @@ -1110,8 +1148,20 @@ def remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: str, i: in if quantity == 0: return self - if not agent.is_holding(k): - return self.fail_transaction('Agent does not have liquidity in this pool.', agent) + # if nft_id is None and not agent.is_holding(k): + # return self.fail_transaction('Agent does not have liquidity in this pool.', agent) + if nft_id is None: + if quantity is not None and not agent.is_holding(k, quantity): + return self.fail_transaction('Agent does not have enough liquidity in this pool.', agent) + else: + if nft_id not in agent.nfts: + return self.fail_transaction('Agent does not have liquidity position with specified nft_id.', agent) + elif agent.nfts[nft_id].pool_id != self.unique_id: + return self.fail_transaction('Specified position is wrong pool.', agent) + elif agent.nfts[nft_id].tkn != tkn_remove: + return self.fail_transaction('Specified position is wrong asset.', agent) + elif quantity is not None and agent.nfts[nft_id].shares < quantity: + return self.fail_transaction('Agent does not have enough shares in specified position.', agent) if self.remove_liquidity_volatility_threshold: if self.oracles['price']: @@ -1123,47 +1173,57 @@ def remove_liquidity(self, agent: Agent, quantity: float, tkn_remove: str, i: in f"Withdrawal rejected because the oracle volatility is too high: {volatility} > " f"{self.remove_liquidity_volatility_threshold}", agent ) - - quantity = abs(quantity) + + val = self.calculate_remove_liquidity(agent, quantity, tkn_remove, nft_id) + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = val[:6] + if len(val) == 7: + nft_ids = val[6] + max_remove = ( self.max_withdrawal_per_block * self.shares[tkn_remove] - self.current_block.withdrawals[tkn_remove] ) - if quantity > max_remove: - # quantity = max_remove + if abs(delta_s) > max_remove: return self.fail_transaction( - f"Transaction rejected because it would exceed the withdrawal limit: {quantity} > {max_remove}", agent + f"Transaction rejected because it would exceed the withdrawal limit: {abs(delta_s)} > {max_remove}", agent ) - if i is None: - delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = self.calculate_remove_liquidity( - agent, quantity, tkn_remove - ) - else: - delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = self.calculate_remove_liquidity( - agent, quantity, tkn_remove, i - ) if delta_r + self.liquidity[tkn_remove] < 0: return self.fail_transaction('Cannot remove more liquidity than exists in the pool.', agent) - elif quantity > agent.holdings[k]: - return self.fail_transaction('Cannot remove more liquidity than the agent has invested.', agent) self.liquidity[tkn_remove] += delta_r self.shares[tkn_remove] += delta_s self.protocol_shares[tkn_remove] += delta_b self.lrna[tkn_remove] += delta_q self.lrna_imbalance += delta_l - # agent.delta_r[(self.unique_id, tkn_remove)] *= quantity / agent.holdings[(self.unique_id, tkn_remove)] - if 'LRNA' not in agent.holdings: - agent.holdings['LRNA'] = 0 - agent.holdings['LRNA'] += delta_qa - agent.holdings[k] -= quantity - if agent.holdings[k] == 0: - agent.share_prices[k] = 0 + + # distribute tokens to agent + if delta_qa > 0: + if 'LRNA' not in agent.holdings: + agent.holdings['LRNA'] = 0 + agent.holdings['LRNA'] += delta_qa if tkn_remove not in agent.holdings: agent.holdings[tkn_remove] = 0 agent.holdings[tkn_remove] -= delta_r - - self.current_block.withdrawals[tkn_remove] += quantity + + # remove lp position(s) + if nft_id is None: + if quantity is not None: + agent.holdings[k] -= quantity + if agent.holdings[k] == 0: + agent.share_prices[k] = 0 + else: + if k in agent.holdings: + agent.holdings[k] = 0 + agent.share_prices[k] = 0 + for nft_id in nft_ids: + del agent.nfts[nft_id] + else: + if quantity is not None: + agent.nfts[nft_id].shares -= quantity + if quantity is None or agent.nfts[nft_id].shares == 0: + del agent.nfts[nft_id] + + self.current_block.withdrawals[tkn_remove] += abs(delta_s) return self def value_assets(self, assets: dict[str, float], equivalency_map: dict[str, str] = None, stablecoin: str = None) -> float: @@ -1201,14 +1261,15 @@ def value_assets(self, assets: dict[str, float], equivalency_map: dict[str, str] class OmnipoolLiquidityPosition: - def __init__(self, tkn: str, price: float, shares: float, delta_r: float): + def __init__(self, tkn: str, price: float, shares: float, delta_r: float, pool_id: str = None): self.tkn = tkn self.price = price self.shares = shares self.delta_r = delta_r + self.pool_id = pool_id def copy(self): - return OmnipoolLiquidityPosition(self.tkn, self.price, self.shares, self.delta_r) + return OmnipoolLiquidityPosition(self.tkn, self.price, self.shares, self.delta_r, self.pool_id) @@ -1512,18 +1573,25 @@ def cash_out_omnipool(omnipool: OmnipoolState, agent: Agent, prices) -> float: del agent_holdings[key] if agent.holdings[key] > 0: - k_split = key[0].split("_") - # delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = omnipool.calculate_remove_all_liquidity(agent, tkn_remove=tkn) - if len(k_split) > 1: - delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = omnipool.calculate_remove_liquidity(agent, agent.holdings[key], tkn, int(k_split[1])) - else: - delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = omnipool.calculate_remove_liquidity(agent, agent.holdings[key], tkn) + val = omnipool.calculate_remove_liquidity(agent, tkn_remove=tkn) + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = val[:6] agent_holdings['LRNA'] += delta_qa if tkn not in agent_holdings: agent_holdings[tkn] = 0 agent_holdings[tkn] -= delta_r liquidity_removed[tkn] -= delta_r lrna_removed[tkn] -= delta_q + for nft_id in agent.nfts: + nft = agent.nfts[nft_id] + if isinstance(nft, OmnipoolLiquidityPosition): + val = omnipool.calculate_remove_liquidity(agent, nft_id=nft_id) + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = val[:6] + agent_holdings['LRNA'] += delta_qa + if nft.tkn not in agent_holdings: + agent_holdings[nft.tkn] = 0 + agent_holdings[nft.tkn] -= delta_r + liquidity_removed[nft.tkn] -= delta_r + lrna_removed[nft.tkn] -= delta_q if 'LRNA' in prices: raise ValueError('LRNA price should not be given.') diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index ae9c8f9b..0eda9ed8 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -10,7 +10,7 @@ from hydradx.model.amm.agents import Agent from hydradx.model.amm.global_state import GlobalState from hydradx.model.amm.omnipool_amm import price, dynamicadd_asset_fee, dynamicadd_lrna_fee, OmnipoolState, \ - cash_out_omnipool + cash_out_omnipool, OmnipoolLiquidityPosition from hydradx.model.amm.trade_strategies import constant_swaps, omnipool_arbitrage from hydradx.tests.strategies_omnipool import omnipool_reasonable_config, omnipool_config, assets_config @@ -199,36 +199,36 @@ def test_add_liquidity_with_existing_position_succeeds(initial_state: oamm.Omnip # TODO adjust these tests using nft_id input -# @given(st.integers(min_value=1, max_value=10)) -# def test_compare_several_lp_adds_to_single(n): -# liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} -# lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} -# initial_state = oamm.OmnipoolState( -# tokens={ -# tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna -# } -# ) -# tkn = initial_state.asset_list[0] -# init_agent = Agent(holdings={tkn: initial_state.liquidity[tkn]}) -# delta_R = init_agent.holdings[tkn] / 2 -# -# single_add_state, single_add_agent = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn) -# multi_add_state, multi_add_agent = initial_state, init_agent -# for i in range(n): -# multi_add_state, multi_add_agent = oamm.simulate_add_liquidity(multi_add_state, multi_add_agent, delta_R / n, tkn) -# -# if single_add_state.liquidity[tkn] != pytest.approx(multi_add_state.liquidity[tkn], rel=1e-20): -# raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') -# if single_add_state.shares[tkn] != pytest.approx(multi_add_state.shares[tkn], rel=1e-20): -# raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') -# if single_add_state.lrna[tkn] != pytest.approx(multi_add_state.lrna[tkn], rel=1e-20): -# raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') -# total_multi_shares = multi_add_agent.holdings[(multi_add_state.unique_id, tkn)] -# total_multi_shares += sum([multi_add_agent.holdings[(multi_add_state.unique_id + f'_{i}', tkn)] for i in range(1, n)]) -# if total_multi_shares != pytest.approx(single_add_agent.holdings[(single_add_state.unique_id, tkn)], rel=1e-20): -# raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') -# -# +@given(st.integers(min_value=1, max_value=10)) +def test_compare_several_lp_adds_to_single(n): + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + } + ) + tkn = initial_state.asset_list[0] + init_agent = Agent(holdings={tkn: initial_state.liquidity[tkn]}) + delta_R = init_agent.holdings[tkn] / 2 + + single_add_state, single_add_agent = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn) + multi_add_state, multi_add_agent = initial_state, init_agent + for i in range(n): + multi_add_state, multi_add_agent = oamm.simulate_add_liquidity(multi_add_state, multi_add_agent, delta_R / n, tkn) + + if single_add_state.liquidity[tkn] != pytest.approx(multi_add_state.liquidity[tkn], rel=1e-20): + raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') + if single_add_state.shares[tkn] != pytest.approx(multi_add_state.shares[tkn], rel=1e-20): + raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') + if single_add_state.lrna[tkn] != pytest.approx(multi_add_state.lrna[tkn], rel=1e-20): + raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') + total_multi_shares = multi_add_agent.holdings[(multi_add_state.unique_id, tkn)] + total_multi_shares += sum([multi_add_agent.holdings[(multi_add_state.unique_id + f'_{i}', tkn)] for i in range(1, n)]) + if total_multi_shares != pytest.approx(single_add_agent.holdings[(single_add_state.unique_id, tkn)], rel=1e-20): + raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') + + # def test_add_additional_positions_at_new_price(): # liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} # lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} @@ -2892,10 +2892,34 @@ def test_fee_application(): raise AssertionError("Direct swap was not equivalent to LRNA swap with fees applied manually.") +@given(st.floats(min_value=10.1, max_value=100)) +def test_cash_out_nft_position(price1: float): + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + }, + withdrawal_fee=False + ) + dot_spot_price = initial_state.price(initial_state, 'DOT', 'USD') + tkn = 'DOT' + amt1 = initial_state.shares[tkn] / 5 + delta_r = initial_state.liquidity[tkn] / 5 + nft = OmnipoolLiquidityPosition(tkn, price1, amt1, delta_r, initial_state.unique_id) + agent = Agent(holdings={}, nfts={'pos1': nft}) + cash_out = cash_out_omnipool(initial_state, agent, {'DOT': dot_spot_price}) + + state = initial_state.copy() + state.remove_liquidity(agent, tkn_remove=tkn) + dot_value = agent.holdings['DOT'] * dot_spot_price + assert cash_out == pytest.approx(dot_value, rel=1e-20) + + @given(st.floats(min_value=10.1, max_value=100), st.floats(min_value=10.1, max_value=100), st.floats(min_value=0.1, max_value=0.9)) -def test_cash_out_multiple_positions_works_no_lrna(price1: float, price2: float, r: float): +def test_cash_out_nft_position_with_holdings(price1: float, price2: float, r: float): liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} initial_state = oamm.OmnipoolState( @@ -2908,13 +2932,14 @@ def test_cash_out_multiple_positions_works_no_lrna(price1: float, price2: float, tkn = 'DOT' amt1 = r * initial_state.shares[tkn] / 5 amt2 = initial_state.shares[tkn] / 5 - amt1 - holdings1 = {(initial_state.unique_id, tkn): amt1, (initial_state.unique_id + '_1', tkn): amt2} - prices1 = {(initial_state.unique_id, tkn): price1, (initial_state.unique_id + '_1', tkn): price2} - agent = Agent(holdings=holdings1, share_prices=prices1) + holdings1 = {(initial_state.unique_id, tkn): amt1} + prices1 = {(initial_state.unique_id, tkn): price1} + nft = OmnipoolLiquidityPosition(tkn, price2, amt2, 0, initial_state.unique_id) + agent = Agent(holdings=holdings1, share_prices=prices1, nfts={'pos1': nft}) cash_out = cash_out_omnipool(initial_state, agent, {'DOT': dot_spot_price}) state = initial_state.copy() - state.remove_all_liquidity(agent, tkn) + state.remove_liquidity(agent, tkn_remove=tkn) dot_value = agent.holdings['DOT'] * dot_spot_price assert cash_out == pytest.approx(dot_value, rel=1e-20) From 692a1b166e6e254551c5b3385a0b6a31e225da94 Mon Sep 17 00:00:00 2001 From: poliwop Date: Fri, 23 Aug 2024 15:12:59 -0500 Subject: [PATCH 04/28] Fixed dca_with_lping strategy to take advantage of new LPing features --- hydradx/model/amm/trade_strategies.py | 19 +++---------------- hydradx/tests/test_omnipool_agents.py | 4 +--- 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/hydradx/model/amm/trade_strategies.py b/hydradx/model/amm/trade_strategies.py index 7065b111..fa999ce0 100644 --- a/hydradx/model/amm/trade_strategies.py +++ b/hydradx/model/amm/trade_strategies.py @@ -1031,26 +1031,13 @@ def strategy(state: GlobalState, agent_id: str) -> GlobalState: sell_quantity=agent.holdings[sell_asset] - init_sell_amt ) - # we turn off the withdrawal fee for this remove_liquidity - # this is because removing liquidity here is due to a limitation in our implementation, - # agents can have only one LP position each in one asset in Omnipool. - # In reality someone executing this strategy would simply add liquidity and get a new LP position. - withdrawal_fee_cache = pool.withdrawal_fee # hack to temporarily zero out withdrawal fee - pool.withdrawal_fee = False - if agent.is_holding((pool.unique_id, buy_asset)): - # remove all liquidity in buy_asset - pool.remove_liquidity( - agent=agent, - quantity=agent.holdings[(pool.unique_id, buy_asset)], - tkn_remove=buy_asset - ) - pool.withdrawal_fee = withdrawal_fee_cache - # LP the buy asset + nft_id = str(len(agent.nfts) + 1) pool.add_liquidity( agent=agent, quantity=agent.holdings[buy_asset] - init_buy_amt, - tkn_add=buy_asset + tkn_add=buy_asset, + nft_id=nft_id ) return state diff --git a/hydradx/tests/test_omnipool_agents.py b/hydradx/tests/test_omnipool_agents.py index 7de3141c..cb409de7 100644 --- a/hydradx/tests/test_omnipool_agents.py +++ b/hydradx/tests/test_omnipool_agents.py @@ -238,10 +238,8 @@ def test_dca_with_lping( strategy.execute(state, trader_id) if init_sell_tkn_lped > 0: - if ('omnipool', buy_tkn) not in agent.holdings: + if len(agent.nfts) == 0: raise AssertionError('Agent does not have shares for buy_tkn.') - if agent.holdings[('omnipool', buy_tkn)] == init_buy_tkn_lped: - raise AssertionError('Agent did not receive shares for buy_tkn.') lp_diff = init_sell_tkn_lped if ('omnipool', sell_tkn) in agent.holdings: From b6f519e2aa9b0b61645b3edc741aa4f1ba2f30af Mon Sep 17 00:00:00 2001 From: poliwop Date: Fri, 23 Aug 2024 15:35:28 -0500 Subject: [PATCH 05/28] Fixed test_compare_several_lp_adds_to_single --- hydradx/tests/test_omnipool_amm.py | 33 +++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index 0eda9ed8..ad2981e5 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -197,8 +197,6 @@ def test_add_liquidity_with_existing_position_succeeds(initial_state: oamm.Omnip raise AssertionError(f'Agent holdings have wrong length.') -# TODO adjust these tests using nft_id input - @given(st.integers(min_value=1, max_value=10)) def test_compare_several_lp_adds_to_single(n): liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} @@ -212,23 +210,38 @@ def test_compare_several_lp_adds_to_single(n): init_agent = Agent(holdings={tkn: initial_state.liquidity[tkn]}) delta_R = init_agent.holdings[tkn] / 2 - single_add_state, single_add_agent = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn) + single_add_state_1, single_add_agent_1 = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn) + single_add_state_2, single_add_agent_2 = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn, nft_id="id001") multi_add_state, multi_add_agent = initial_state, init_agent for i in range(n): - multi_add_state, multi_add_agent = oamm.simulate_add_liquidity(multi_add_state, multi_add_agent, delta_R / n, tkn) + nft_id = str(i) + multi_add_state, multi_add_agent = oamm.simulate_add_liquidity(multi_add_state, multi_add_agent, delta_R / n, tkn, nft_id=nft_id) + + if single_add_state_1.liquidity[tkn] != pytest.approx(multi_add_state.liquidity[tkn], rel=1e-20): + raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') + if single_add_state_1.shares[tkn] != pytest.approx(multi_add_state.shares[tkn], rel=1e-20): + raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') + if single_add_state_1.lrna[tkn] != pytest.approx(multi_add_state.lrna[tkn], rel=1e-20): + raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') + total_multi_shares = sum([multi_add_agent.nfts[nft_id].shares for nft_id in multi_add_agent.nfts]) + shares_1 = single_add_agent_1.holdings[(single_add_state_1.unique_id, tkn)] + if total_multi_shares != pytest.approx(shares_1, rel=1e-20): + raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') - if single_add_state.liquidity[tkn] != pytest.approx(multi_add_state.liquidity[tkn], rel=1e-20): + if single_add_state_2.liquidity[tkn] != pytest.approx(multi_add_state.liquidity[tkn], rel=1e-20): raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') - if single_add_state.shares[tkn] != pytest.approx(multi_add_state.shares[tkn], rel=1e-20): + if single_add_state_2.shares[tkn] != pytest.approx(multi_add_state.shares[tkn], rel=1e-20): raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') - if single_add_state.lrna[tkn] != pytest.approx(multi_add_state.lrna[tkn], rel=1e-20): + if single_add_state_2.lrna[tkn] != pytest.approx(multi_add_state.lrna[tkn], rel=1e-20): raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') - total_multi_shares = multi_add_agent.holdings[(multi_add_state.unique_id, tkn)] - total_multi_shares += sum([multi_add_agent.holdings[(multi_add_state.unique_id + f'_{i}', tkn)] for i in range(1, n)]) - if total_multi_shares != pytest.approx(single_add_agent.holdings[(single_add_state.unique_id, tkn)], rel=1e-20): + shares_2 = single_add_agent_2.nfts["id001"].shares + if shares_1 != pytest.approx(shares_2, rel=1e-20): raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') +# TODO adjust these tests using nft_id input + + # def test_add_additional_positions_at_new_price(): # liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} # lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} From ef0d9672f6b397faad8778568270a87b34891275 Mon Sep 17 00:00:00 2001 From: poliwop Date: Wed, 28 Aug 2024 16:49:58 -0500 Subject: [PATCH 06/28] Fixed remove_liquidity handling of removing all liquidity when quantity is not specified --- hydradx/model/amm/omnipool_amm.py | 19 ++++++++++- hydradx/tests/test_omnipool_amm.py | 51 ++++++++++++++---------------- 2 files changed, 42 insertions(+), 28 deletions(-) diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index ccf837a5..fa733444 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -947,9 +947,26 @@ def calculate_remove_liquidity(self, agent: Agent, quantity: float = None, tkn_r if isinstance(nft, OmnipoolLiquidityPosition): if nft.pool_id == self.unique_id and nft.tkn == tkn_remove: nft_ids.append(nft_id) - delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = self._calculate_remove_one_position( + dqa, dr, dq, ds, db, dl = self._calculate_remove_one_position( quantity=nft.shares, tkn_remove=tkn_remove, share_price=nft.price ) + delta_qa += dqa + delta_r += dr + delta_q += dq + delta_s += ds + delta_b += db + delta_l += dl + if (self.unique_id, tkn_remove) in agent.holdings: + dqa, dr, dq, ds, db, dl = self._calculate_remove_one_position( + quantity=agent.holdings[(self.unique_id, tkn_remove)], tkn_remove=tkn_remove, + share_price=agent.share_prices[(self.unique_id, tkn_remove)] + ) + delta_qa += dqa + delta_r += dr + delta_q += dq + delta_s += ds + delta_b += db + delta_l += dl return delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l, nft_ids def _calculate_remove_one_position(self, quantity, tkn_remove, share_price): diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index ad2981e5..bf853228 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -511,8 +511,9 @@ def test_remove_liquidity_one_of_two(n): assert pool.liquidity[tkn] == comp_pool.liquidity[tkn] -@given(st.floats(min_value=1, max_value=100)) -def test_remove_liquidity_split(price: float): +@given(st.floats(min_value=1, max_value=100), +st.floats(min_value=0.1, max_value=0.9)) +def test_remove_liquidity_split(price: float, split: float): liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} initial_state = oamm.OmnipoolState( @@ -523,37 +524,33 @@ def test_remove_liquidity_split(price: float): ) tkn = 'DOT' amt1 = initial_state.shares[tkn] / 5 - amt2 = amt1 / 2 + amt2 = amt1 * split amt3 = amt1 - amt2 holdings1 = {(initial_state.unique_id, tkn): amt1} - holdings2 = {(initial_state.unique_id, tkn): amt2, (initial_state.unique_id + '_1', tkn): amt3} prices1 = {(initial_state.unique_id, tkn): price} - prices2 = {(initial_state.unique_id, tkn): price, (initial_state.unique_id + '_1', tkn): price} - state1 = initial_state.copy() - state2 = initial_state.copy() agent1 = Agent(holdings=holdings1, share_prices=prices1) - agent2 = Agent(holdings=holdings2, share_prices=prices2) - state1.remove_all_liquidity(agent1, tkn) - state2.remove_all_liquidity(agent2, tkn) - assert state1.liquidity[tkn] == pytest.approx(state2.liquidity[tkn], rel=1e-20) - assert state1.shares[tkn] == pytest.approx(state2.shares[tkn], rel=1e-20) - assert agent1.holdings[tkn] == pytest.approx(agent2.holdings[tkn], rel=1e-20) - assert agent1.holdings[(initial_state.unique_id, tkn)] == 0 - assert agent2.holdings[(initial_state.unique_id, tkn)] == 0 - assert agent2.holdings[(initial_state.unique_id + '_1', tkn)] == 0 + # holdings2 = {(initial_state.unique_id, tkn): amt2, (initial_state.unique_id + '_1', tkn): amt3} + nft1 = OmnipoolLiquidityPosition(tkn, price, amt2, 0, initial_state.unique_id) + nft2 = OmnipoolLiquidityPosition(tkn, price, amt3, 0, initial_state.unique_id) + nfts = {'nft001': nft1, 'nft002': nft2} + agent2 = Agent(nfts=nfts) - # test if this holds with remove_liquidity as well - holdings3 = {(initial_state.unique_id, tkn): amt2, (initial_state.unique_id + '_1', tkn): amt3} - prices3 = {(initial_state.unique_id, tkn): price, (initial_state.unique_id + '_1', tkn): price} - state3 = initial_state.copy() - agent3 = Agent(holdings=holdings3, share_prices=prices3) - state3.remove_liquidity(agent3, amt2, tkn) - state3.remove_liquidity(agent3, amt3, tkn, 1) - - assert agent3.holdings[tkn] == pytest.approx(agent2.holdings[tkn], rel=1e-20) - assert agent3.holdings[(initial_state.unique_id, tkn)] == 0 - assert agent3.holdings[(initial_state.unique_id + '_1', tkn)] == 0 + state1 = initial_state.copy() + state2 = initial_state.copy() + state1.remove_liquidity(agent1, tkn_remove=tkn) + state2.remove_liquidity(agent2, tkn_remove=tkn) + + if state1.liquidity[tkn] != pytest.approx(state2.liquidity[tkn], rel=1e-20): + raise AssertionError('liquidity should match') + if state1.shares[tkn] != pytest.approx(state2.shares[tkn], rel=1e-20): + raise AssertionError('shares should match') + if agent1.holdings[tkn] != pytest.approx(agent2.holdings[tkn], rel=1e-20): + raise AssertionError('holdings should match') + if agent1.holdings[(initial_state.unique_id, tkn)] != 0: + raise AssertionError('holdings of shares should be zero') + if len(agent2.nfts) != 0: + raise AssertionError('LP positions should be removed') @given(st.floats(min_value=1, max_value=100), st.floats(min_value=1, max_value=100), From 6e6d8ae9969bc162688e242a12a195ab721f7e86 Mon Sep 17 00:00:00 2001 From: poliwop Date: Thu, 5 Sep 2024 14:39:13 -0500 Subject: [PATCH 07/28] Extended simulate_remove_liquidity Added basic test for remove_liquidity at exact added price --- hydradx/model/amm/omnipool_amm.py | 7 ++++--- hydradx/tests/test_omnipool_amm.py | 22 +++++++++++++++++++++- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index fa733444..77bd9e4d 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -1438,14 +1438,15 @@ def simulate_add_liquidity( def simulate_remove_liquidity( old_state: OmnipoolState, old_agent: Agent, - quantity: float, - tkn_remove: str + quantity: float = None, + tkn_remove: str = '', + nft_id: str = None ) -> tuple[OmnipoolState, Agent]: """Compute new state after liquidity removal""" new_state = old_state.copy() new_agent = old_agent.copy() - new_state.remove_liquidity(new_agent, quantity, tkn_remove) + new_state.remove_liquidity(new_agent, quantity, tkn_remove, nft_id) return new_state, new_agent diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index bf853228..4d1d5234 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -291,6 +291,26 @@ def test_add_liquidity_with_quantity_zero_should_fail(initial_state: oamm.Omnipo raise AssertionError(f'Adding liquidity with quantity zero should fail.') +def test_remove_liquidity_at_add_price_exact(): + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + } + ) + tkn = 'DOT' + p = initial_state.price(initial_state, tkn, 'LRNA') + s = initial_state.shares[tkn] / 10 + expected_r = initial_state.liquidity[tkn] / 10 * (1 - initial_state.min_withdrawal_fee) + position = OmnipoolLiquidityPosition(tkn, p, s, 0, initial_state.unique_id) + init_agent = Agent(nfts={'position': position}) + new_state, new_agent = oamm.simulate_remove_liquidity(initial_state, init_agent, s, tkn, 'position') + delta_r = initial_state.liquidity[tkn] - new_state.liquidity[tkn] + if delta_r != pytest.approx(expected_r, rel=1e-20): + raise AssertionError(f'Removed liquidity should be equal to initial liquidity minus final liquidity.') + + @given(omnipool_config(token_count=3, withdrawal_fee=False)) def test_remove_liquidity_no_fee(initial_state: oamm.OmnipoolState): i = initial_state.asset_list[2] @@ -485,7 +505,7 @@ def test_remove_liquidity_one_of_two(n): init_agent = Agent(holdings=holdings, share_prices=share_prices) if n == 0: k = (initial_state.unique_id, tkn) - k2 = (initial_state.unique_id + '_1', tkn) + k2 = (initial_state.unique_id + '_1', tkn) #TODO fix this else: k = (initial_state.unique_id + '_1', tkn) k2 = (initial_state.unique_id, tkn) From bcfa1a1905dac98c3cab10e256b15af30f7d1fdb Mon Sep 17 00:00:00 2001 From: poliwop Date: Fri, 6 Sep 2024 15:46:03 -0500 Subject: [PATCH 08/28] Extended exact test for remove liquidity --- hydradx/tests/test_omnipool_amm.py | 58 ++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index 4d1d5234..f3783459 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -300,6 +300,7 @@ def test_remove_liquidity_at_add_price_exact(): } ) tkn = 'DOT' + p = initial_state.price(initial_state, tkn, 'LRNA') s = initial_state.shares[tkn] / 10 expected_r = initial_state.liquidity[tkn] / 10 * (1 - initial_state.min_withdrawal_fee) @@ -310,6 +311,63 @@ def test_remove_liquidity_at_add_price_exact(): if delta_r != pytest.approx(expected_r, rel=1e-20): raise AssertionError(f'Removed liquidity should be equal to initial liquidity minus final liquidity.') + p = initial_state.price(initial_state, tkn, 'LRNA') / 2 + s = initial_state.shares[tkn] / 10 + position = OmnipoolLiquidityPosition(tkn, p, s, 0, initial_state.unique_id) + init_agent = Agent(nfts={'position': position}) + new_state, new_agent = oamm.simulate_remove_liquidity(initial_state, init_agent, s, tkn, 'position') + + expected_dq_pct = mpf(1) / 10 * (1 - initial_state.min_withdrawal_fee) + expected_agent_dq_pct = mpf(1) / 30 * (1 - initial_state.min_withdrawal_fee) + expected_dr_pct = mpf(1) / 10 * (1 - initial_state.min_withdrawal_fee) + expected_ds_pct = mpf(1) / 10 + + actual_dq_pct = (initial_state.lrna[tkn] - new_state.lrna[tkn]) / initial_state.lrna[tkn] + actual_agent_dq_pct = new_agent.holdings['LRNA'] / initial_state.lrna[tkn] + actual_dr_pct = (initial_state.liquidity[tkn] - new_state.liquidity[tkn]) / initial_state.liquidity[tkn] + actual_ds_pct = (initial_state.shares[tkn] - new_state.shares[tkn]) / initial_state.shares[tkn] + actual_db = initial_state.protocol_shares[tkn] - new_state.protocol_shares[tkn] + + if actual_dq_pct != pytest.approx(expected_dq_pct, rel=1e-20): + raise AssertionError(f'LRNA change incorrect') + if actual_agent_dq_pct != pytest.approx(expected_agent_dq_pct, rel=1e-20): + raise AssertionError(f'LRNA given to agent incorrect') + if actual_dr_pct != pytest.approx(expected_dr_pct, rel=1e-20): + raise AssertionError(f'Liquidity change incorrect') + if actual_ds_pct != pytest.approx(expected_ds_pct, rel=1e-20): + raise AssertionError(f'Shares change incorrect') + if actual_db != 0: + raise AssertionError(f'Protocol should not earn shares') + + p = initial_state.price(initial_state, tkn, 'LRNA') * 2 + s = initial_state.shares[tkn] / 10 + position = OmnipoolLiquidityPosition(tkn, p, s, 0, initial_state.unique_id) + init_agent = Agent(nfts={'position': position}) + new_state, new_agent = oamm.simulate_remove_liquidity(initial_state, init_agent, s, tkn, 'position') + + expected_dq_pct = mpf(2) / 30 * (1 - initial_state.min_withdrawal_fee) + expected_dr_pct = mpf(2) / 30 * (1 - initial_state.min_withdrawal_fee) + expected_ds_pct = mpf(2) / 30 + expected_db_pct = mpf(1) / 30 + + actual_dq_pct = (initial_state.lrna[tkn] - new_state.lrna[tkn]) / initial_state.lrna[tkn] + actual_agent_dq = new_agent.holdings['LRNA'] if 'LRNA' in new_agent.holdings else 0 + actual_dr_pct = (initial_state.liquidity[tkn] - new_state.liquidity[tkn]) / initial_state.liquidity[tkn] + actual_ds_pct = (initial_state.shares[tkn] - new_state.shares[tkn]) / initial_state.shares[tkn] + actual_db_pct = (new_state.protocol_shares[tkn] - initial_state.protocol_shares[tkn]) / initial_state.shares[tkn] + + if actual_dq_pct != pytest.approx(expected_dq_pct, rel=1e-20): + raise AssertionError(f'LRNA change incorrect') + if actual_agent_dq != pytest.approx(0, rel=1e-20): + raise AssertionError(f'LRNA given to agent incorrect') + if actual_dr_pct != pytest.approx(expected_dr_pct, rel=1e-20): + raise AssertionError(f'Liquidity change incorrect') + if actual_ds_pct != pytest.approx(expected_ds_pct, rel=1e-20): + raise AssertionError(f'Shares change incorrect') + if actual_db_pct != pytest.approx(expected_db_pct, rel=1e-20): + raise AssertionError(f'Protocol shares incorrect') + + @given(omnipool_config(token_count=3, withdrawal_fee=False)) def test_remove_liquidity_no_fee(initial_state: oamm.OmnipoolState): From d224f4a268532949cb1dfba445e0de0b227e4bdd Mon Sep 17 00:00:00 2001 From: poliwop Date: Fri, 6 Sep 2024 16:10:57 -0500 Subject: [PATCH 09/28] Added test to compare basic case with case where shares are in holdings --- hydradx/tests/test_omnipool_amm.py | 53 +++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index f3783459..297cadbf 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -291,7 +291,7 @@ def test_add_liquidity_with_quantity_zero_should_fail(initial_state: oamm.Omnipo raise AssertionError(f'Adding liquidity with quantity zero should fail.') -def test_remove_liquidity_at_add_price_exact(): +def test_remove_liquidity_exact(): liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} initial_state = oamm.OmnipoolState( @@ -368,6 +368,57 @@ def test_remove_liquidity_at_add_price_exact(): raise AssertionError(f'Protocol shares incorrect') +@given(st.floats(min_value=0.1, max_value=10)) +def test_remove_liquidity_specified_quantity_unspecified_nft(price_mult: float): + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + } + ) + tkn = 'DOT' + + p = price_mult * initial_state.price(initial_state, tkn, 'LRNA') + s = initial_state.shares[tkn] / 10 + holdings = {(initial_state.unique_id, tkn): s} + share_prices = {(initial_state.unique_id, tkn): p} + init_agent = Agent(holdings=holdings, share_prices=share_prices) + + position = OmnipoolLiquidityPosition(tkn, p, s, 0, initial_state.unique_id) + base_agent = Agent(nfts={'position': position}) + + quantity = s + new_state, new_agent = oamm.simulate_remove_liquidity(initial_state, init_agent, quantity, tkn) + comp_state, comp_agent = oamm.simulate_remove_liquidity(initial_state, base_agent, quantity, tkn, 'position') + if new_state.liquidity[tkn] != pytest.approx(comp_state.liquidity[tkn], rel=1e-20): + raise AssertionError(f'Remaining liquidity doesn\'t match.') + if new_state.shares[tkn] != pytest.approx(comp_state.shares[tkn], rel=1e-20): + raise AssertionError(f'Remaining shares doesn\'t match.') + if new_state.lrna[tkn] != pytest.approx(comp_state.lrna[tkn], rel=1e-20): + raise AssertionError(f'Remaining LRNA doesn\'t match.') + if new_state.protocol_shares[tkn] != pytest.approx(comp_state.protocol_shares[tkn], rel=1e-20): + raise AssertionError(f'Remaining protocol shares doesn\'t match.') + + quantity = s / 2 + new_state, new_agent = oamm.simulate_remove_liquidity(initial_state, init_agent, quantity, tkn) + comp_state, comp_agent = oamm.simulate_remove_liquidity(initial_state, base_agent, quantity, tkn, 'position') + if new_state.liquidity[tkn] != pytest.approx(comp_state.liquidity[tkn], rel=1e-20): + raise AssertionError(f'Remaining liquidity doesn\'t match.') + if new_state.shares[tkn] != pytest.approx(comp_state.shares[tkn], rel=1e-20): + raise AssertionError(f'Remaining shares doesn\'t match.') + if new_state.lrna[tkn] != pytest.approx(comp_state.lrna[tkn], rel=1e-20): + raise AssertionError(f'Remaining LRNA doesn\'t match.') + if new_state.protocol_shares[tkn] != pytest.approx(comp_state.protocol_shares[tkn], rel=1e-20): + raise AssertionError(f'Remaining protocol shares doesn\'t match.') + + quantity = s * 2 + new_state, new_agent = oamm.simulate_remove_liquidity(initial_state, init_agent, quantity, tkn) + if not new_state.fail: + raise AssertionError(f'Removing liquidity with quantity greater than holdings should fail.') + + + @given(omnipool_config(token_count=3, withdrawal_fee=False)) def test_remove_liquidity_no_fee(initial_state: oamm.OmnipoolState): From 76bc198dee4918b1362cd4d380e5ee16831a46d3 Mon Sep 17 00:00:00 2001 From: poliwop Date: Fri, 6 Sep 2024 16:18:21 -0500 Subject: [PATCH 10/28] Added test to compare basic usage when quantity is unspecified --- hydradx/tests/test_omnipool_amm.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index 297cadbf..f81ffc75 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -417,7 +417,32 @@ def test_remove_liquidity_specified_quantity_unspecified_nft(price_mult: float): if not new_state.fail: raise AssertionError(f'Removing liquidity with quantity greater than holdings should fail.') +@given(st.floats(min_value=0.1, max_value=10)) +def test_remove_liquidity_unspecified_quantity_specified_nft(price_mult: float): + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + } + ) + tkn = 'DOT' + + p = price_mult * initial_state.price(initial_state, tkn, 'LRNA') + s = initial_state.shares[tkn] / 10 + position = OmnipoolLiquidityPosition(tkn, p, s, 0, initial_state.unique_id) + init_agent = Agent(nfts={'position': position}) + new_state, new_agent = oamm.simulate_remove_liquidity(initial_state, init_agent, tkn_remove=tkn, nft_id='position') + comp_state, comp_agent = oamm.simulate_remove_liquidity(initial_state, init_agent, s, tkn, 'position') + if new_state.liquidity[tkn] != pytest.approx(comp_state.liquidity[tkn], rel=1e-20): + raise AssertionError(f'Remaining liquidity doesn\'t match.') + if new_state.shares[tkn] != pytest.approx(comp_state.shares[tkn], rel=1e-20): + raise AssertionError(f'Remaining shares doesn\'t match.') + if new_state.lrna[tkn] != pytest.approx(comp_state.lrna[tkn], rel=1e-20): + raise AssertionError(f'Remaining LRNA doesn\'t match.') + if new_state.protocol_shares[tkn] != pytest.approx(comp_state.protocol_shares[tkn], rel=1e-20): + raise AssertionError(f'Remaining protocol shares doesn\'t match.') @given(omnipool_config(token_count=3, withdrawal_fee=False)) From 86c272565c1dcce402aaf0e73e36eaf76122e28c Mon Sep 17 00:00:00 2001 From: poliwop Date: Fri, 6 Sep 2024 16:29:54 -0500 Subject: [PATCH 11/28] Added test to compare base case to case where both quantity and nft_id are unspecified --- hydradx/tests/test_omnipool_amm.py | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index f81ffc75..a08ff1d8 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -444,6 +444,38 @@ def test_remove_liquidity_unspecified_quantity_specified_nft(price_mult: float): if new_state.protocol_shares[tkn] != pytest.approx(comp_state.protocol_shares[tkn], rel=1e-20): raise AssertionError(f'Remaining protocol shares doesn\'t match.') +@given(st.floats(min_value=0.1, max_value=10)) +def test_remove_liquidity_unspecified_quantity_unspecified_nft(price_mult: float): + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + } + ) + tkn = 'DOT' + + p = price_mult * initial_state.price(initial_state, tkn, 'LRNA') + s = initial_state.shares[tkn] / 10 + holdings = {(initial_state.unique_id, tkn): s/2} + share_prices = {(initial_state.unique_id, tkn): p} + position = OmnipoolLiquidityPosition(tkn, p, s/2, 0, initial_state.unique_id) + init_agent = Agent(holdings=holdings, share_prices=share_prices, nfts={'position': position}) + + position = OmnipoolLiquidityPosition(tkn, p, s, 0, initial_state.unique_id) + base_agent = Agent(nfts={'position': position}) + + new_state, new_agent = oamm.simulate_remove_liquidity(initial_state, init_agent, tkn_remove=tkn) + comp_state, comp_agent = oamm.simulate_remove_liquidity(initial_state, base_agent, s, tkn, 'position') + if new_state.liquidity[tkn] != pytest.approx(comp_state.liquidity[tkn], rel=1e-20): + raise AssertionError(f'Remaining liquidity doesn\'t match.') + if new_state.shares[tkn] != pytest.approx(comp_state.shares[tkn], rel=1e-20): + raise AssertionError(f'Remaining shares doesn\'t match.') + if new_state.lrna[tkn] != pytest.approx(comp_state.lrna[tkn], rel=1e-20): + raise AssertionError(f'Remaining LRNA doesn\'t match.') + if new_state.protocol_shares[tkn] != pytest.approx(comp_state.protocol_shares[tkn], rel=1e-20): + raise AssertionError(f'Remaining protocol shares doesn\'t match.') + @given(omnipool_config(token_count=3, withdrawal_fee=False)) def test_remove_liquidity_no_fee(initial_state: oamm.OmnipoolState): From 672020ad9c7ebb02afb542a62bcfe68c4a60c710 Mon Sep 17 00:00:00 2001 From: poliwop Date: Mon, 9 Sep 2024 15:18:03 -0500 Subject: [PATCH 12/28] Removed outdated test --- hydradx/tests/test_omnipool_amm.py | 34 ------------------------------ 1 file changed, 34 deletions(-) diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index a08ff1d8..0eee5912 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -739,40 +739,6 @@ def test_remove_liquidity_split(price: float, split: float): raise AssertionError('LP positions should be removed') -@given(st.floats(min_value=1, max_value=100), st.floats(min_value=1, max_value=100), - st.floats(min_value=0.1, max_value = 0.9)) -def test_remove_all_liquidity(price1: float, price2: float, r: float): - liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} - lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} - initial_state = oamm.OmnipoolState( - tokens={ - tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna - }, - withdrawal_fee=False - ) - tkn = 'DOT' - amt1 = r * initial_state.shares[tkn] / 5 - amt2 = initial_state.shares[tkn] / 5 - amt1 - holdings1 = {(initial_state.unique_id, tkn): amt1, (initial_state.unique_id + '_1', tkn): amt2} - holdings2 = {k: v for k, v in holdings1.items()} - prices1 = {(initial_state.unique_id, tkn): price1, (initial_state.unique_id + '_1', tkn): price2} - prices2 = {k: v for k, v in prices1.items()} - state1 = initial_state.copy() - state2 = initial_state.copy() - agent1 = Agent(holdings=holdings1, share_prices=prices1) - agent2 = Agent(holdings=holdings2, share_prices=prices2) - state1.remove_all_liquidity(agent1, tkn) - state2.remove_liquidity(agent2, amt1, tkn) - state2.remove_liquidity(agent2, amt2, tkn, 1) - - assert state1.liquidity[tkn] == pytest.approx(state2.liquidity[tkn], rel=1e-20) - assert state1.shares[tkn] == pytest.approx(state2.shares[tkn], rel=1e-20) - assert agent1.holdings[tkn] == pytest.approx(agent2.holdings[tkn], rel=1e-20) - assert agent1.holdings[(initial_state.unique_id, tkn)] == 0 - assert agent2.holdings[(initial_state.unique_id, tkn)] == 0 - assert agent2.holdings[(initial_state.unique_id + '_1', tkn)] == 0 - - @given(omnipool_config(token_count=3)) def test_swap_lrna(initial_state: oamm.OmnipoolState): old_state = initial_state From af32d65a1027237ec9ca473675d69c5efec8c413 Mon Sep 17 00:00:00 2001 From: poliwop Date: Tue, 10 Sep 2024 10:56:07 -0500 Subject: [PATCH 13/28] reverted change --- hydradx/model/amm/global_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hydradx/model/amm/global_state.py b/hydradx/model/amm/global_state.py index bf2928b2..cc5f2e27 100644 --- a/hydradx/model/amm/global_state.py +++ b/hydradx/model/amm/global_state.py @@ -143,7 +143,7 @@ def market_prices(self, shares: dict) -> dict: for share_id in shares: # if shares are for a specific asset in a specific pool, get prices according to that pool if isinstance(share_id, tuple): - pool_id = share_id[0].split('_')[0] + pool_id = share_id[0] tkn_id = share_id[1] prices[share_id] = self.pools[pool_id].usd_price(self.pools[pool_id], tkn_id) From fb69dbd32c2397a6a19c12e7b17434da5e368a46 Mon Sep 17 00:00:00 2001 From: poliwop Date: Tue, 10 Sep 2024 11:02:21 -0500 Subject: [PATCH 14/28] Added logic to pull tkn_remove from nft in remove_liquidity if necessary --- hydradx/model/amm/omnipool_amm.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index 77bd9e4d..2ae68cdf 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -1149,6 +1149,10 @@ def remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str # k = (self.unique_id + "_" + str(i), tkn_remove) k = (self.unique_id, tkn_remove) + if nft_id is not None: + if nft_id not in agent.nfts: + return self.fail_transaction('Agent does not have liquidity position with specified nft_id.', agent) + tkn_remove = agent.nfts[nft_id].tkn if tkn_remove not in self.asset_list: for sub_pool in self.sub_pools.values(): @@ -1161,7 +1165,7 @@ def remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str else: return self - raise AssertionError(f"invalid value for i: {tkn_remove}") + raise AssertionError(f"invalid value for tkn_remove: {tkn_remove}") if quantity == 0: return self From 1431339c0676f2cd2b19f6efba7a0ed0147b345e Mon Sep 17 00:00:00 2001 From: poliwop Date: Tue, 10 Sep 2024 16:26:39 -0500 Subject: [PATCH 15/28] Fixed some tests --- hydradx/model/amm/omnipool_amm.py | 49 ++++++++++------------------ hydradx/tests/test_omnipool_amm.py | 9 ++--- hydradx/tests/test_omnipool_state.py | 2 +- 3 files changed, 23 insertions(+), 37 deletions(-) diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index 2ae68cdf..007416fa 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -1184,7 +1184,7 @@ def remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str elif quantity is not None and agent.nfts[nft_id].shares < quantity: return self.fail_transaction('Agent does not have enough shares in specified position.', agent) - if self.remove_liquidity_volatility_threshold: + if self.remove_liquidity_volatility_threshold and self.remove_liquidity_volatility_threshold < float('inf'): if self.oracles['price']: volatility = abs( self.oracles['price'].price[tkn_remove] / self.current_block.price[tkn_remove] - 1 @@ -1577,43 +1577,28 @@ def value_assets(prices: dict, assets: dict) -> float: ]) +def _turn_off_validations(omnipool: OmnipoolState) -> OmnipoolState: + new_state = omnipool.copy() + new_state.remove_liquidity_volatility_threshold = float('inf') + new_state.max_withdrawal_per_block = float('inf') + return new_state + + def cash_out_omnipool(omnipool: OmnipoolState, agent: Agent, prices) -> float: """ return the value of the agent's holdings if they withdraw all liquidity and then sell at current spot prices """ - if 'LRNA' not in agent.holdings: - agent.holdings['LRNA'] = 0 - agent_holdings = {tkn: agent.holdings[tkn] for tkn in list(agent.holdings.keys())} - liquidity_removed = {tkn: 0 for tkn in omnipool.asset_list} - lrna_removed = {tkn: 0 for tkn in omnipool.asset_list} - - for key in agent.holdings.keys(): - if isinstance(key, tuple): - tkn = key[1] - del agent_holdings[key] - - if agent.holdings[key] > 0: - val = omnipool.calculate_remove_liquidity(agent, tkn_remove=tkn) - delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = val[:6] - agent_holdings['LRNA'] += delta_qa - if tkn not in agent_holdings: - agent_holdings[tkn] = 0 - agent_holdings[tkn] -= delta_r - liquidity_removed[tkn] -= delta_r - lrna_removed[tkn] -= delta_q - for nft_id in agent.nfts: - nft = agent.nfts[nft_id] - if isinstance(nft, OmnipoolLiquidityPosition): - val = omnipool.calculate_remove_liquidity(agent, nft_id=nft_id) - delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = val[:6] - agent_holdings['LRNA'] += delta_qa - if nft.tkn not in agent_holdings: - agent_holdings[nft.tkn] = 0 - agent_holdings[nft.tkn] -= delta_r - liquidity_removed[nft.tkn] -= delta_r - lrna_removed[nft.tkn] -= delta_q + new_state, new_agent = _turn_off_validations(omnipool), agent.copy() + if 'LRNA' not in new_agent.holdings: + new_agent.holdings['LRNA'] = 0 + for tkn in omnipool.asset_list: + new_state, new_agent = simulate_remove_liquidity(new_state, new_agent, tkn_remove=tkn) + + agent_holdings = new_agent.holdings + lrna_removed = {tkn: omnipool.lrna[tkn] - new_state.lrna[tkn] for tkn in omnipool.lrna} + liquidity_removed = {tkn: omnipool.liquidity[tkn] - new_state.liquidity[tkn] for tkn in omnipool.liquidity} if 'LRNA' in prices: raise ValueError('LRNA price should not be given.') diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index 0eee5912..570d7d30 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -3121,14 +3121,15 @@ def test_cash_out_multiple_positions_works_with_lrna(price1: float, price2: floa tkn = 'DOT' amt1 = r * initial_state.shares[tkn] / 5 amt2 = initial_state.shares[tkn] / 5 - amt1 - holdings1 = {(initial_state.unique_id, tkn): amt1, (initial_state.unique_id + '_1', tkn): amt2} - prices1 = {(initial_state.unique_id, tkn): price1, (initial_state.unique_id + '_1', tkn): price2} - agent = Agent(holdings=holdings1, share_prices=prices1) + holdings1 = {(initial_state.unique_id, tkn): amt1} + prices1 = {(initial_state.unique_id, tkn): price1} + nft = OmnipoolLiquidityPosition(tkn, price2, amt2, 0, initial_state.unique_id) + agent = Agent(holdings=holdings1, share_prices=prices1, nfts={'pos1': nft}) spot_prices = {tkn: initial_state.price(initial_state, tkn, 'USD') for tkn in initial_state.asset_list} cash_out = cash_out_omnipool(initial_state, agent, spot_prices) state = initial_state.copy() - state.remove_all_liquidity(agent, tkn) + state.remove_liquidity(agent, tkn_remove=tkn) dot_value = agent.holdings['DOT'] * spot_prices['DOT'] lrna_value = agent.holdings['LRNA'] * initial_state.price(initial_state, 'LRNA', 'USD') assert dot_value < cash_out < dot_value + lrna_value # cash_out will be less than dot + lrna due to slippage diff --git a/hydradx/tests/test_omnipool_state.py b/hydradx/tests/test_omnipool_state.py index aed0aa52..40b1c8e2 100644 --- a/hydradx/tests/test_omnipool_state.py +++ b/hydradx/tests/test_omnipool_state.py @@ -311,7 +311,7 @@ def test_cash_out_accuracy(omnipool: oamm.OmnipoolState, share_price_ratio, lp_i ) lrna_profits[tkn] = withdraw_agent.holdings[tkn] - agent_holdings - del withdraw_agent.holdings['LRNA'] + del withdraw_agent.holdings['LRNA'] cash_count = sum([market_prices[tkn] * withdraw_agent.holdings[tkn] for tkn in withdraw_agent.holdings]) if cash_count != pytest.approx(cash_out, rel=1e-15): raise AssertionError('Cash out calculation is not accurate.') From 02befd5866b6dfd0ef59c2c5fe44f99cbe7b7e62 Mon Sep 17 00:00:00 2001 From: poliwop Date: Wed, 11 Sep 2024 13:58:42 -0500 Subject: [PATCH 16/28] removed commented out test --- hydradx/tests/test_omnipool_amm.py | 35 ------------------------------ 1 file changed, 35 deletions(-) diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index 570d7d30..9255c429 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -239,41 +239,6 @@ def test_compare_several_lp_adds_to_single(n): raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') -# TODO adjust these tests using nft_id input - - -# def test_add_additional_positions_at_new_price(): -# liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} -# lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} -# initial_state = oamm.OmnipoolState( -# tokens={ -# tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna -# } -# ) -# tkn = 'DOT' -# holdings = {tkn: initial_state.liquidity[tkn], (initial_state.unique_id, tkn): initial_state.shares[tkn] / 10} -# share_prices = {(initial_state.unique_id, tkn): 8} -# init_agent = Agent(holdings=holdings, share_prices=share_prices) -# delta_R = init_agent.holdings[tkn] / 2 -# -# new_state, new_agent = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn) -# -# if new_agent.holdings[(initial_state.unique_id, tkn)] != init_agent.holdings[(initial_state.unique_id, tkn)]: -# raise AssertionError(f'Adding liquidity should not change the shares in existing position') -# if new_agent.share_prices[(initial_state.unique_id, tkn)] != init_agent.share_prices[(initial_state.unique_id, tkn)]: -# raise AssertionError(f'Adding liquidity should not change the share price in existing position') -# if new_agent.share_prices[(initial_state.unique_id + '_1', tkn)] != initial_state.price(initial_state, tkn): -# raise AssertionError(f'Adding liquidity should set the share price in new position to the pool price') -# -# new_state2, new_agent2 = oamm.simulate_add_liquidity(new_state, new_agent, delta_R, tkn) -# if new_agent2.holdings[(initial_state.unique_id + '_1', tkn)] != new_agent.holdings[(initial_state.unique_id + '_1', tkn)]: -# raise AssertionError(f'Adding liquidity should not change the shares in existing position') -# if new_agent2.share_prices[(initial_state.unique_id, tkn)] != new_agent.share_prices[(initial_state.unique_id, tkn)]: -# raise AssertionError(f'Adding liquidity should not change the share price in existing position') -# if new_agent2.share_prices[(initial_state.unique_id + '_1', tkn)] != new_state.price(initial_state, tkn): -# raise AssertionError(f'Adding liquidity should set the share price in new position to the pool price') - - @settings(max_examples=1) @given(omnipool_config(token_count=3, lrna_fee=0, asset_fee=0)) def test_add_liquidity_with_quantity_zero_should_fail(initial_state: oamm.OmnipoolState): From 74291d70e5173c6f0ff59c21cef9fc470d4db94e Mon Sep 17 00:00:00 2001 From: poliwop Date: Wed, 11 Sep 2024 14:02:11 -0500 Subject: [PATCH 17/28] Removed commented out function --- hydradx/model/amm/omnipool_amm.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index 007416fa..a394060f 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -886,29 +886,6 @@ def migrate_lp( return self - # def calculate_remove_all_liquidity(self, agent: Agent, tkn_remove): - # delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = 0, 0, 0, 0, 0, 0 - # for k in agent.holdings: - # if len(k) > 1 and k[1] == tkn_remove: - # k_split = k[0].split("_") - # if len(k_split) == 1: - # qa, r, q, s, b, l = self.calculate_remove_liquidity(agent, agent.holdings[k], tkn_remove) - # delta_qa += qa - # delta_r += r - # delta_q += q - # delta_s += s - # delta_b += b - # delta_l += l - # elif k_split[0] == self.unique_id: - # qa, r, q, s, b, l = self.calculate_remove_liquidity(agent, agent.holdings[k], tkn_remove, int(k_split[1])) - # delta_qa += qa - # delta_r += r - # delta_q += q - # delta_s += s - # delta_b += b - # delta_l += l - # return delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l - def calculate_remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str = None, nft_id: str = None): """ If quantity is specified and nft_id is specified, remove specified quantity of shares from specified position. From 0d2b83a1a0f37afba9f2d770aa9d21f0558dc475 Mon Sep 17 00:00:00 2001 From: poliwop Date: Wed, 11 Sep 2024 14:05:32 -0500 Subject: [PATCH 18/28] Removed commented out function --- hydradx/model/amm/omnipool_amm.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index a394060f..1bfda6e7 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -1099,17 +1099,6 @@ def add_liquidity( self.current_block.lps[tkn_add] += quantity return self - - # def remove_all_liquidity(self, agent: Agent, tkn_remove: str): - # agent_assets = list(agent.holdings.keys()) - # for k in agent_assets: - # if len(k) > 1 and k[1] == tkn_remove: - # k_split = k[0].split("_") - # if len(k_split) == 1: - # self.remove_liquidity(agent, agent.holdings[k], tkn_remove) - # elif k[0].split("_")[0] == self.unique_id: - # self.remove_liquidity(agent, agent.holdings[k], tkn_remove, int(k[0].split("_")[1])) - # return self def remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str = '', nft_id: str = None): """ From cfec790a7e7b2e59bedd0f7059969dd1ff9fb16b Mon Sep 17 00:00:00 2001 From: poliwop Date: Wed, 11 Sep 2024 14:09:56 -0500 Subject: [PATCH 19/28] Removed old test --- hydradx/tests/test_omnipool_amm.py | 47 ------------------------------ 1 file changed, 47 deletions(-) diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index 9255c429..699c7274 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -616,52 +616,6 @@ def test_remove_liquidity_no_fee_different_price(initial_state: oamm.OmnipoolSta raise AssertionError(f'LRNA imbalance did not remain constant.') -@given(st.integers(min_value=1, max_value=2)) -def test_remove_liquidity_one_of_two(n): - - liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} - lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} - initial_state = oamm.OmnipoolState( - tokens={ - tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna - } - ) - tkn = 'DOT' - holdings = { - tkn: initial_state.liquidity[tkn], - (initial_state.unique_id, tkn): initial_state.shares[tkn] / 10, - (initial_state.unique_id + '_1', tkn): initial_state.shares[tkn] / 10 - } - share_prices = {(initial_state.unique_id, tkn): 8, (initial_state.unique_id + '_1', tkn): 12} - init_agent = Agent(holdings=holdings, share_prices=share_prices) - if n == 0: - k = (initial_state.unique_id, tkn) - k2 = (initial_state.unique_id + '_1', tkn) #TODO fix this - else: - k = (initial_state.unique_id + '_1', tkn) - k2 = (initial_state.unique_id, tkn) - - quantity = init_agent.holdings[k] - - comp_holdings = {k: v for k, v in holdings.items()} - del comp_holdings[k2] - comp_prices = {k: v for k, v in share_prices.items()} - del comp_prices[k2] - comp_agent = Agent(holdings=comp_holdings, share_prices=comp_prices) - - agent = init_agent.copy() - pool = initial_state.copy() - comp_pool = initial_state.copy() - if n == 0: - pool.remove_liquidity(agent, quantity, tkn) - comp_pool.remove_liquidity(comp_agent, quantity, tkn) - else: - pool.remove_liquidity(agent, quantity, tkn, n) - comp_pool.remove_liquidity(comp_agent, quantity, tkn, n) - - assert pool.liquidity[tkn] == comp_pool.liquidity[tkn] - - @given(st.floats(min_value=1, max_value=100), st.floats(min_value=0.1, max_value=0.9)) def test_remove_liquidity_split(price: float, split: float): @@ -681,7 +635,6 @@ def test_remove_liquidity_split(price: float, split: float): prices1 = {(initial_state.unique_id, tkn): price} agent1 = Agent(holdings=holdings1, share_prices=prices1) - # holdings2 = {(initial_state.unique_id, tkn): amt2, (initial_state.unique_id + '_1', tkn): amt3} nft1 = OmnipoolLiquidityPosition(tkn, price, amt2, 0, initial_state.unique_id) nft2 = OmnipoolLiquidityPosition(tkn, price, amt3, 0, initial_state.unique_id) nfts = {'nft001': nft1, 'nft002': nft2} From c18afa9231a711a78c3ce084db37c8c33bd14c1e Mon Sep 17 00:00:00 2001 From: poliwop Date: Wed, 11 Sep 2024 14:21:24 -0500 Subject: [PATCH 20/28] Formatting --- hydradx/model/amm/omnipool_amm.py | 118 +++++++++++++++-------------- hydradx/tests/test_omnipool_amm.py | 18 +++-- 2 files changed, 72 insertions(+), 64 deletions(-) diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index 1bfda6e7..6dea0ebf 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -415,7 +415,7 @@ def get_sub_pool(self, tkn: str): for pool in self.sub_pools.values(): if tkn in pool.asset_list: return pool.unique_id - + def swap( self, agent: Agent, @@ -445,12 +445,12 @@ def swap( buy_quantity=buy_quantity, sell_quantity=sell_quantity ) - + elif tkn_sell == 'LRNA': return_val = self.lrna_swap(agent, buy_quantity, -sell_quantity, tkn_buy, modify_imbalance) elif tkn_buy == 'LRNA': return_val = self.lrna_swap(agent, -sell_quantity, buy_quantity, tkn_sell, modify_imbalance) - + elif buy_quantity and not sell_quantity: # back into correct delta_Ri, then execute sell delta_Ri = self.calculate_sell_from_buy(tkn_buy, tkn_sell, buy_quantity) @@ -489,7 +489,7 @@ def swap( if self.liquidity[i] + sell_quantity > 10 ** 12: return self.fail_transaction('Asset liquidity cannot exceed 10 ^ 12.', agent) - + # per-block trade limits if ( -delta_Rj - self.current_block.volume_in[tkn_buy] + self.current_block.volume_out[tkn_buy] @@ -511,14 +511,14 @@ def swap( self.liquidity[j] += -buy_quantity or delta_Rj self.lrna['HDX'] += delta_QH self.lrna_imbalance += delta_L - + if j not in agent.holdings: agent.holdings[j] = 0 agent.holdings[i] -= delta_Ri agent.holdings[j] -= -buy_quantity or delta_Rj - + return_val = self - + # update oracle if tkn_buy in self.current_block.asset_list: buy_quantity = old_buy_liquidity - self.liquidity[tkn_buy] @@ -529,7 +529,7 @@ def swap( self.current_block.volume_in[tkn_sell] += sell_quantity self.current_block.price[tkn_sell] = self.lrna[tkn_sell] / self.liquidity[tkn_sell] return return_val - + def lrna_swap( self, agent: Agent, @@ -541,7 +541,7 @@ def lrna_swap( """ Execute LRNA swap in place (modify and return) """ - + if delta_qa < 0: asset_fee = self.asset_fee[tkn].compute() if -delta_qa + self.lrna[tkn] <= 0: @@ -557,7 +557,7 @@ def lrna_swap( self.lrna[tkn] += delta_q self.liquidity[tkn] += -delta_ra - + elif delta_ra > 0: asset_fee = self.asset_fee[tkn].compute() if -delta_ra + self.liquidity[tkn] <= 0: @@ -573,7 +573,7 @@ def lrna_swap( self.lrna[tkn] += delta_q self.liquidity[tkn] -= delta_ra - + # buying LRNA elif delta_qa > 0: lrna_fee = self.lrna_fee[tkn].compute() @@ -587,13 +587,13 @@ def lrna_swap( self.liquidity[tkn] += -delta_ra # if modify_imbalance: # self.lrna_imbalance += - delta_qi * (q + l) / (q + delta_qi) - delta_qi - + # we assume, for now, that buying LRNA is only possible when modify_imbalance = False lrna_fee_amt = -(delta_qa + delta_qi) delta_l = min(-self.lrna_imbalance, lrna_fee_amt) self.lrna_imbalance += delta_l self.lrna["HDX"] += lrna_fee_amt - delta_l - + elif delta_ra < 0: lrna_fee = self.lrna_fee[tkn].compute() # delta_ri = -delta_ra @@ -605,16 +605,16 @@ def lrna_swap( self.liquidity[tkn] -= delta_ra # if modify_imbalance: # self.lrna_imbalance += - delta_qi * (q + l) / (q + delta_qi) - delta_qi - + # we assume, for now, that buying LRNA is only possible when modify_imbalance = False lrna_fee_amt = -(delta_qa + delta_qi) delta_l = min(-self.lrna_imbalance, lrna_fee_amt) self.lrna_imbalance += delta_l self.lrna["HDX"] += lrna_fee_amt - delta_l - + else: return self.fail_transaction('All deltas are zero.', agent) - + if 'LRNA' not in agent.holdings: agent.holdings['LRNA'] = 0 if tkn not in agent.holdings: @@ -622,9 +622,9 @@ def lrna_swap( agent.holdings['LRNA'] += delta_qa agent.holdings[tkn] += delta_ra - + return self - + def stable_swap( self, agent: Agent, @@ -659,7 +659,7 @@ def stable_swap( delta_shares = agent.holdings[sub_pool.unique_id] - agent_shares sub_pool.remove_liquidity(agent, delta_shares, tkn_buy) return self - + elif sub_pool_sell_id and tkn_buy in self.asset_list: sub_pool: StableSwapPoolState = self.sub_pools[sub_pool_sell_id] if sell_quantity: @@ -687,13 +687,13 @@ def stable_swap( return self.fail_transaction(sub_pool.fail, agent) self.swap(agent, tkn_buy, sub_pool.unique_id, buy_quantity) return self - + elif sub_pool_buy_id and tkn_sell in self.asset_list: sub_pool: StableSwapPoolState = self.sub_pools[sub_pool_buy_id] if buy_quantity: # buy a stableswap asset with an omnipool asset shares_traded = sub_pool.calculate_withdrawal_shares(tkn_buy, buy_quantity) - + # buy shares in the subpool self.swap(agent, tkn_buy=sub_pool.unique_id, tkn_sell=tkn_sell, buy_quantity=shares_traded) if self.fail: @@ -753,7 +753,7 @@ def stable_swap( ) if pool_buy.fail: return self.fail_transaction(pool_buy.fail, agent) - + # if all three parts succeeded, then we're good! return self elif sell_quantity: @@ -806,7 +806,8 @@ def create_sub_pool( 'subpool_shares': self.liquidity[tkn] * tkns_migrate[tkn] / self.liquidity[tkn] } for tkn in tkns_migrate } - new_sub_pool.shares = sum([self.liquidity[tkn] * tkns_migrate[tkn] / self.liquidity[tkn] for tkn in tkns_migrate]) + new_sub_pool.shares = sum( + [self.liquidity[tkn] * tkns_migrate[tkn] / self.liquidity[tkn] for tkn in tkns_migrate]) self.sub_pools[unique_id] = new_sub_pool self.add_token( unique_id, @@ -818,7 +819,7 @@ def create_sub_pool( for tkn in tkns_migrate ]) ) - + # remove assets from Omnipool for tkn in tkns_migrate: self.lrna[tkn] -= self.lrna[tkn] * tkns_migrate[tkn] / self.liquidity[tkn] @@ -826,7 +827,7 @@ def create_sub_pool( if self.liquidity[tkn] == 0: self.asset_list.remove(tkn) return self - + def migrate_asset(self, tkn_migrate: str, sub_pool_id: str): """ Move an asset from the Omnipool into a stableswap subpool. @@ -840,26 +841,26 @@ def migrate_asset(self, tkn_migrate: str, sub_pool_id: str): self.protocol_shares[s] += ( self.shares[s] * self.lrna[i] / self.lrna[s] * self.protocol_shares[i] / self.shares[i] ) - + sub_pool.conversion_metrics[i] = { 'price': self.lrna[i] / self.lrna[s] * sub_pool.shares / self.liquidity[i], 'old_shares': self.shares[i], 'omnipool_shares': self.lrna[i] * self.shares[s] / self.lrna[s], 'subpool_shares': self.lrna[i] * sub_pool.shares / self.lrna[s] } - + self.shares[s] += self.lrna[i] * self.shares[s] / self.lrna[s] self.liquidity[s] += self.lrna[i] * sub_pool.shares / self.lrna[s] sub_pool.shares += self.lrna[i] * sub_pool.shares / self.lrna[s] self.lrna[s] += self.lrna[i] - + # remove asset from omnipool and add it to subpool self.lrna[i] = 0 self.liquidity[i] = 0 self.asset_list.remove(i) sub_pool.asset_list.append(i) return self - + def migrate_lp( self, agent: Agent, @@ -883,10 +884,11 @@ def migrate_lp( agent.holdings[old_pool_id] / conversions['old_shares'] * conversions['subpool_shares'] ) agent.holdings[old_pool_id] = 0 - + return self - def calculate_remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str = None, nft_id: str = None): + def calculate_remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str = None, + nft_id: str = None): """ If quantity is specified and nft_id is specified, remove specified quantity of shares from specified position. If quantity is specified and nft_id is unspecified, remove specified quantity of shares from holdings. @@ -965,20 +967,20 @@ def _calculate_remove_one_position(self, quantity, tkn_remove, share_price): assert quantity <= 0, f"delta_S cannot be positive: {quantity}" if tkn_remove not in self.asset_list: raise AssertionError(f"Invalid token name: {tkn_remove}") - + # determine if they should get some LRNA back as well as the asset they invested piq = lrna_price(self, tkn_remove) p0 = share_price mult = (piq - p0) / (piq + p0) - + # Share update delta_b = max(mult * quantity, 0) delta_s = quantity + delta_b - + # Token amounts update delta_q = self.lrna[tkn_remove] / self.shares[tkn_remove] * delta_s delta_r = delta_q / piq - + if piq > p0: # prevents rounding errors delta_qa = -piq * ( 2 * piq / (piq + p0) * quantity / self.shares[tkn_remove] @@ -986,21 +988,21 @@ def _calculate_remove_one_position(self, quantity, tkn_remove, share_price): ) else: delta_qa = 0 - + if hasattr(self, 'withdrawal_fee') and self.withdrawal_fee > 0: # calculate withdraw fee diff = abs(self.oracles['price'].price[tkn_remove] - piq) / self.oracles['price'].price[tkn_remove] fee = max(min(diff, 1), self.min_withdrawal_fee) - + delta_r *= 1 - fee delta_qa *= 1 - fee delta_q *= 1 - fee - + # L update: LRNA fees to be burned before they will start to accumulate again delta_l = delta_r * piq * self.lrna_imbalance / self.lrna_total - + return delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l - + def add_liquidity( self, agent: Agent = None, @@ -1009,7 +1011,7 @@ def add_liquidity( nft_id: str = None ): """Compute new state after liquidity addition""" - + if quantity <= 0: return self.fail_transaction('Quantity must be non-negative.', agent) @@ -1022,12 +1024,12 @@ def add_liquidity( if nft_id is not None and nft_id in agent.nfts: raise AssertionError('Agent already has an NFT with this ID.') - + if agent.holdings[tkn_add] < quantity: return self.fail_transaction( f'Agent has insufficient funds ({agent.holdings[tkn_add]} < {quantity}).', agent ) - + if (self.lrna[tkn_add] + delta_Q) / (self.lrna_total + delta_Q) > self.weight_cap[tkn_add]: return self.fail_transaction( 'Transaction rejected because it would exceed the weight cap in pool[{i}].', agent @@ -1036,7 +1038,7 @@ def add_liquidity( if self.tvl_cap < float('inf'): if (self.total_value_locked() + quantity * usd_price(self, tkn_add)) > self.tvl_cap: return self.fail_transaction('Transaction rejected because it would exceed the TVL cap.', agent) - + # assert quantity > 0, f"delta_R must be positive: {quantity}" if tkn_add not in self.asset_list: for sub_pool in self.sub_pools.values(): @@ -1063,7 +1065,7 @@ def add_liquidity( return self.fail_transaction( 'Transaction rejected because it would exceed the max LP per block.', agent ) - + # Share update if self.shares[tkn_add]: shares_added = (delta_Q / self.lrna[tkn_add]) * self.shares[tkn_add] @@ -1074,10 +1076,10 @@ def add_liquidity( # L update: LRNA fees to be burned before they will start to accumulate again delta_L = quantity * lrna_price(self, tkn_add) * self.lrna_imbalance / self.lrna_total self.lrna_imbalance += delta_L - + # LRNA add (mint) self.lrna[tkn_add] += delta_Q - + # Token amounts update self.liquidity[tkn_add] += quantity @@ -1092,14 +1094,15 @@ def add_liquidity( agent.share_prices[k] = lrna_price(self, tkn_add) agent.delta_r[k] = quantity else: - lp_position = OmnipoolLiquidityPosition(tkn_add, lrna_price(self, tkn_add), shares_added, quantity, self.unique_id) + lp_position = OmnipoolLiquidityPosition(tkn_add, lrna_price(self, tkn_add), shares_added, quantity, + self.unique_id) agent.nfts[nft_id] = lp_position - + # update block self.current_block.lps[tkn_add] += quantity - + return self - + def remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str = '', nft_id: str = None): """ Remove liquidity from a sub pool. @@ -1130,7 +1133,7 @@ def remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str return self.fail_transaction(sub_pool.fail, agent) else: return self - + raise AssertionError(f"invalid value for tkn_remove: {tkn_remove}") if quantity == 0: @@ -1149,7 +1152,7 @@ def remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str return self.fail_transaction('Specified position is wrong asset.', agent) elif quantity is not None and agent.nfts[nft_id].shares < quantity: return self.fail_transaction('Agent does not have enough shares in specified position.', agent) - + if self.remove_liquidity_volatility_threshold and self.remove_liquidity_volatility_threshold < float('inf'): if self.oracles['price']: volatility = abs( @@ -1171,12 +1174,13 @@ def remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str ) if abs(delta_s) > max_remove: return self.fail_transaction( - f"Transaction rejected because it would exceed the withdrawal limit: {abs(delta_s)} > {max_remove}", agent + f"Transaction rejected because it would exceed the withdrawal limit: {abs(delta_s)} > {max_remove}", + agent ) if delta_r + self.liquidity[tkn_remove] < 0: return self.fail_transaction('Cannot remove more liquidity than exists in the pool.', agent) - + self.liquidity[tkn_remove] += delta_r self.shares[tkn_remove] += delta_s self.protocol_shares[tkn_remove] += delta_b @@ -1213,7 +1217,8 @@ def remove_liquidity(self, agent: Agent, quantity: float = None, tkn_remove: str self.current_block.withdrawals[tkn_remove] += abs(delta_s) return self - def value_assets(self, assets: dict[str, float], equivalency_map: dict[str, str] = None, stablecoin: str = None) -> float: + def value_assets(self, assets: dict[str, float], equivalency_map: dict[str, str] = None, + stablecoin: str = None) -> float: # assets is a dict of token: quantity # returns the value of the assets in USD if stablecoin is None: @@ -1259,7 +1264,6 @@ def copy(self): return OmnipoolLiquidityPosition(self.tkn, self.price, self.shares, self.delta_r, self.pool_id) - class OmnipoolArchiveState: def __init__(self, state: OmnipoolState): self.asset_list = [tkn for tkn in state.asset_list] diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index 699c7274..95aee13f 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -211,11 +211,13 @@ def test_compare_several_lp_adds_to_single(n): delta_R = init_agent.holdings[tkn] / 2 single_add_state_1, single_add_agent_1 = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn) - single_add_state_2, single_add_agent_2 = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn, nft_id="id001") + single_add_state_2, single_add_agent_2 = oamm.simulate_add_liquidity(initial_state, init_agent, delta_R, tkn, + nft_id="id001") multi_add_state, multi_add_agent = initial_state, init_agent for i in range(n): nft_id = str(i) - multi_add_state, multi_add_agent = oamm.simulate_add_liquidity(multi_add_state, multi_add_agent, delta_R / n, tkn, nft_id=nft_id) + multi_add_state, multi_add_agent = oamm.simulate_add_liquidity(multi_add_state, multi_add_agent, delta_R / n, + tkn, nft_id=nft_id) if single_add_state_1.liquidity[tkn] != pytest.approx(multi_add_state.liquidity[tkn], rel=1e-20): raise AssertionError(f'Adding liquidity in one go should be equivalent to adding it in {n} steps.') @@ -382,6 +384,7 @@ def test_remove_liquidity_specified_quantity_unspecified_nft(price_mult: float): if not new_state.fail: raise AssertionError(f'Removing liquidity with quantity greater than holdings should fail.') + @given(st.floats(min_value=0.1, max_value=10)) def test_remove_liquidity_unspecified_quantity_specified_nft(price_mult: float): liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} @@ -409,6 +412,7 @@ def test_remove_liquidity_unspecified_quantity_specified_nft(price_mult: float): if new_state.protocol_shares[tkn] != pytest.approx(comp_state.protocol_shares[tkn], rel=1e-20): raise AssertionError(f'Remaining protocol shares doesn\'t match.') + @given(st.floats(min_value=0.1, max_value=10)) def test_remove_liquidity_unspecified_quantity_unspecified_nft(price_mult: float): liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} @@ -422,9 +426,9 @@ def test_remove_liquidity_unspecified_quantity_unspecified_nft(price_mult: float p = price_mult * initial_state.price(initial_state, tkn, 'LRNA') s = initial_state.shares[tkn] / 10 - holdings = {(initial_state.unique_id, tkn): s/2} + holdings = {(initial_state.unique_id, tkn): s / 2} share_prices = {(initial_state.unique_id, tkn): p} - position = OmnipoolLiquidityPosition(tkn, p, s/2, 0, initial_state.unique_id) + position = OmnipoolLiquidityPosition(tkn, p, s / 2, 0, initial_state.unique_id) init_agent = Agent(holdings=holdings, share_prices=share_prices, nfts={'position': position}) position = OmnipoolLiquidityPosition(tkn, p, s, 0, initial_state.unique_id) @@ -617,7 +621,7 @@ def test_remove_liquidity_no_fee_different_price(initial_state: oamm.OmnipoolSta @given(st.floats(min_value=1, max_value=100), -st.floats(min_value=0.1, max_value=0.9)) + st.floats(min_value=0.1, max_value=0.9)) def test_remove_liquidity_split(price: float, split: float): liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} @@ -1666,7 +1670,6 @@ def test_dynamic_fees_with_trade(liquidity: list[float], lrna: list[float], orac oracle_volume_in: list[float], oracle_volume_out: list[float], oracle_prices: list[float], n, trade_size: float, lrna_fees: list[float], asset_fees: list[float], amp: list[float], decay: list[float]): - init_liquidity = { 'HDX': {'liquidity': liquidity[0], 'LRNA': lrna[0]}, 'USD': {'liquidity': liquidity[1], 'LRNA': lrna[1]}, @@ -2759,7 +2762,8 @@ def test_calculate_buy_from_sell(omnipool: oamm.OmnipoolState): usd_lrna_fee=st.floats(min_value=0, max_value=0.1) ) def test_buy_sell_spot( - hdx_lrna: float, usd_lrna: float, hdx_asset_fee: float, hdx_lrna_fee: float, usd_asset_fee: float, usd_lrna_fee: float + hdx_lrna: float, usd_lrna: float, hdx_asset_fee: float, hdx_lrna_fee: float, usd_asset_fee: float, + usd_lrna_fee: float ): tokens = { 'HDX': {'liquidity': mpf(1000000000), 'LRNA': hdx_lrna}, From 560ad7e411458ca67eb3d3b2569cf1571fed6192 Mon Sep 17 00:00:00 2001 From: poliwop Date: Wed, 18 Sep 2024 16:24:47 -0500 Subject: [PATCH 21/28] Optimized cash_out --- hydradx/model/amm/omnipool_amm.py | 63 ++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index 6dea0ebf..2390450a 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -1560,33 +1560,52 @@ def cash_out_omnipool(omnipool: OmnipoolState, agent: Agent, prices) -> float: and then sell at current spot prices """ - new_state, new_agent = _turn_off_validations(omnipool), agent.copy() - if 'LRNA' not in new_agent.holdings: - new_agent.holdings['LRNA'] = 0 - for tkn in omnipool.asset_list: - new_state, new_agent = simulate_remove_liquidity(new_state, new_agent, tkn_remove=tkn) - - agent_holdings = new_agent.holdings - lrna_removed = {tkn: omnipool.lrna[tkn] - new_state.lrna[tkn] for tkn in omnipool.lrna} - liquidity_removed = {tkn: omnipool.liquidity[tkn] - new_state.liquidity[tkn] for tkn in omnipool.liquidity} + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = 0, {}, {}, {}, {}, 0 + nft_ids = [] + + for k in agent.holdings: + if isinstance(k, tuple) and len(k) == 2 and k[0] == omnipool.unique_id: # LP shares of correct pool + tkn = k[1] + dqa, dr, dq, ds, db, dl, ids = omnipool.calculate_remove_liquidity(agent, tkn_remove=tkn) + delta_qa += dqa + delta_r[tkn] = dr + (delta_r[tkn] if tkn in delta_r else 0) + delta_q[tkn] = dq + (delta_q[tkn] if tkn in delta_q else 0) + delta_s[tkn] = ds + (delta_s[tkn] if tkn in delta_s else 0) + delta_b[tkn] = db + (delta_b[tkn] if tkn in delta_b else 0) + delta_l += dl + nft_ids += ids + + for nft_id in agent.nfts: + if nft_id not in nft_ids and agent.nfts[nft_id].pool_id == omnipool.unique_id: + tkn = agent.nfts[nft_id].tkn + dqa, dr, dq, ds, db, dl, _ = omnipool.calculate_remove_liquidity(agent, nft_id=nft_id) + delta_qa += dqa + delta_r[tkn] = dr + (delta_r[tkn] if tkn in delta_r else 0) + delta_q[tkn] = dq + (delta_q[tkn] if tkn in delta_q else 0) + delta_s[tkn] = ds + (delta_s[tkn] if tkn in delta_s else 0) + delta_b[tkn] = db + (delta_b[tkn] if tkn in delta_b else 0) + delta_l += dl + + # agent_holdings = new_agent.holdings + lrna_removed = {tkn: -delta_q[tkn] if tkn in delta_q else 0 for tkn in omnipool.asset_list} + liquidity_removed = {tkn: -delta_r[tkn] if tkn in delta_r else 0 for tkn in omnipool.asset_list} if 'LRNA' in prices: raise ValueError('LRNA price should not be given.') - - if 'LRNA' not in prices and agent_holdings['LRNA'] > 0: + lrna_to_sell = delta_qa + if 'LRNA' in agent.holdings: + lrna_to_sell += agent.holdings['LRNA'] + if 'LRNA' not in prices and lrna_to_sell > 0: lrna_total = omnipool.lrna_total - sum(lrna_removed.values()) lrna_sells = { - tkn: -(omnipool.lrna[tkn] - lrna_removed[tkn]) / lrna_total * agent_holdings['LRNA'] + tkn: -(omnipool.lrna[tkn] - lrna_removed[tkn]) / lrna_total * lrna_to_sell for tkn in omnipool.asset_list } - agent_holdings['LRNA'] = 0 lrna_profits = dict() # sell LRNA optimally back to the pool for tkn, delta_qa in lrna_sells.items(): - if tkn not in agent_holdings: - agent_holdings[tkn] = 0 asset_fee = ( omnipool.asset_fee[tkn].compute() if hasattr(omnipool, 'asset_fee') @@ -1597,12 +1616,12 @@ def cash_out_omnipool(omnipool: OmnipoolState, agent: Agent, prices) -> float: / (-delta_qa + omnipool.lrna[tkn] - lrna_removed[tkn]) * (1 - asset_fee) ) - agent_holdings[tkn] += lrna_profits[tkn] - - del agent_holdings['LRNA'] + liquidity_removed[tkn] += lrna_profits[tkn] - for tkn in agent_holdings.keys(): - if agent_holdings[tkn] > 0 and tkn not in prices: - raise ValueError(f'Agent has holdings in {tkn} but no price was given.') + new_holdings = {tkn: agent.holdings[tkn] for tkn in agent.holdings} + for tkn in liquidity_removed: + if tkn not in new_holdings: + new_holdings[tkn] = 0 + new_holdings[tkn] += liquidity_removed[tkn] - return value_assets(prices, agent_holdings) + return value_assets(prices, new_holdings) From 7b6b1e7538c8ced3b32017213fbf5d77e4b7250a Mon Sep 17 00:00:00 2001 From: poliwop Date: Wed, 18 Sep 2024 16:57:53 -0500 Subject: [PATCH 22/28] Added test_cash_out_omnipool_exact --- hydradx/tests/test_omnipool_amm.py | 59 ++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index 95aee13f..cd7508d1 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -2976,6 +2976,65 @@ def test_fee_application(): raise AssertionError("Direct swap was not equivalent to LRNA swap with fees applied manually.") +def test_cash_out_omnipool_exact(): + liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + } + ) + tkn = 'DOT' + + p = initial_state.price(initial_state, tkn, 'LRNA') + s = initial_state.shares[tkn] / 10 + prices = {tkn: initial_state.price(initial_state, tkn, 'USD') for tkn in initial_state.asset_list} + expected_r = initial_state.liquidity[tkn] / 10 * (1 - initial_state.min_withdrawal_fee) + expected_cash = expected_r * prices[tkn] + position = OmnipoolLiquidityPosition(tkn, p, s, 0, initial_state.unique_id) + init_agent = Agent(nfts={'position': position}) + cash = cash_out_omnipool(initial_state, init_agent, prices) + if cash != pytest.approx(expected_cash, rel=1e-20): + raise AssertionError(f'Removed liquidity should be equal to initial liquidity minus final liquidity.') + + p = initial_state.price(initial_state, tkn, 'LRNA') / 2 + s = initial_state.shares[tkn] / 10 + position = OmnipoolLiquidityPosition(tkn, p, s, 0, initial_state.unique_id) + init_agent = Agent(nfts={'position': position}) + cash = cash_out_omnipool(initial_state, init_agent, prices) + + expected_agent_dq_pct = mpf(1) / 30 * (1 - initial_state.min_withdrawal_fee) + expected_agent_dq = expected_agent_dq_pct * initial_state.lrna[tkn] + + expected_dr_pct = mpf(1) / 10 * (1 - initial_state.min_withdrawal_fee) + expected_dr = expected_dr_pct * initial_state.liquidity[tkn] + expected_min_cash = expected_dr * prices[tkn] + expected_max_cash = expected_min_cash + expected_agent_dq + initial_state.price(initial_state, 'LRNA', 'USD') + expected_cash_out_lrna = 32954 + + if expected_agent_dq <= 0: + raise AssertionError(f'LRNA change incorrect') + if cash <= expected_min_cash: + raise AssertionError(f'Cash out should be at least the minimum amount') + if cash >= expected_max_cash: + raise AssertionError(f'Cash out should be at most the maximum amount') + if cash - expected_min_cash != pytest.approx(expected_cash_out_lrna, rel=1e-4): + raise AssertionError(f'cash incorrect') + + p = initial_state.price(initial_state, tkn, 'LRNA') * 2 + s = initial_state.shares[tkn] / 10 + position = OmnipoolLiquidityPosition(tkn, p, s, 0, initial_state.unique_id) + init_agent = Agent(nfts={'position': position}) + cash = cash_out_omnipool(initial_state, init_agent, prices) + + expected_dr_pct = mpf(2) / 30 * (1 - initial_state.min_withdrawal_fee) + expected_dr = expected_dr_pct * initial_state.liquidity[tkn] + expected_cash = expected_dr * prices[tkn] + + if cash != pytest.approx(expected_cash, rel=1e-20): + raise AssertionError(f'cash incorrect') + + @given(st.floats(min_value=10.1, max_value=100)) def test_cash_out_nft_position(price1: float): liquidity = {'HDX': mpf(10000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} From 5b7838a5237180e6ff6304ad826612356fd381cb Mon Sep 17 00:00:00 2001 From: jepidoptera Date: Fri, 20 Sep 2024 13:48:40 -0500 Subject: [PATCH 23/28] add test_cash_out_multiple_positions --- hydradx/tests/test_omnipool_amm.py | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index cd7508d1..2209ee91 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -3114,3 +3114,34 @@ def test_cash_out_multiple_positions_works_with_lrna(price1: float, price2: floa dot_value = agent.holdings['DOT'] * spot_prices['DOT'] lrna_value = agent.holdings['LRNA'] * initial_state.price(initial_state, 'LRNA', 'USD') assert dot_value < cash_out < dot_value + lrna_value # cash_out will be less than dot + lrna due to slippage + + +@given(st.lists(st.floats(min_value=-100000, max_value=100000), min_size=3, max_size=3)) +def test_cash_out_multiple_positions(trade_sizes: list[float]): + liquidity = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(100000)} + lrna = {'HDX': mpf(1000000), 'USD': mpf(1000000), 'DOT': mpf(1000000)} + initial_state = oamm.OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + }, + withdrawal_fee=False + ) + + lp_quantity = 10000 + agent1 = Agent(holdings={'DOT': lp_quantity * len(trade_sizes)}) + agent2 = Agent(holdings={'DOT': 10000000, 'HDX': 10000000}) + for i, trade in enumerate(trade_sizes): + initial_state.add_liquidity(agent1, tkn_add='DOT', quantity=lp_quantity, nft_id=str(i)) + if trade > 0: + initial_state.swap(agent2, tkn_buy='HDX', tkn_sell='DOT', sell_quantity=trade) + else: + initial_state.swap(agent2, tkn_buy='DOT', tkn_sell='HDX', sell_quantity=-trade) + + spot_prices = {tkn: initial_state.price(initial_state, tkn, 'USD') for tkn in initial_state.asset_list} + cash_out_value = cash_out_omnipool(initial_state, agent1, spot_prices) + cash_out_state = initial_state.copy() + cash_out_agent = agent1.copy() + cash_out_state.remove_liquidity(cash_out_agent, tkn_remove='DOT') + reference_value = oamm.value_assets(spot_prices, cash_out_agent.holdings) + if cash_out_value != pytest.approx(reference_value, 1e-20): + raise AssertionError("Cash out not computed correctly.") From c9b542f385d002776bf7fb47ed09c276b6aae1c0 Mon Sep 17 00:00:00 2001 From: poliwop Date: Fri, 20 Sep 2024 14:32:15 -0500 Subject: [PATCH 24/28] Allowing LRNA price in cash_out_omnipool. Fixed test_cash_out_multiple_positions --- hydradx/model/amm/omnipool_amm.py | 14 ++++++++------ hydradx/tests/test_omnipool_amm.py | 12 +++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index 2390450a..54447cfe 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -1590,15 +1590,16 @@ def cash_out_omnipool(omnipool: OmnipoolState, agent: Agent, prices) -> float: lrna_removed = {tkn: -delta_q[tkn] if tkn in delta_q else 0 for tkn in omnipool.asset_list} liquidity_removed = {tkn: -delta_r[tkn] if tkn in delta_r else 0 for tkn in omnipool.asset_list} - if 'LRNA' in prices: - raise ValueError('LRNA price should not be given.') - lrna_to_sell = delta_qa + # if 'LRNA' in prices: + # raise ValueError('LRNA price should not be given.') + agent_lrna = delta_qa if 'LRNA' in agent.holdings: - lrna_to_sell += agent.holdings['LRNA'] - if 'LRNA' not in prices and lrna_to_sell > 0: + agent_lrna += agent.holdings['LRNA'] + + if 'LRNA' not in prices and agent_lrna > 0: lrna_total = omnipool.lrna_total - sum(lrna_removed.values()) lrna_sells = { - tkn: -(omnipool.lrna[tkn] - lrna_removed[tkn]) / lrna_total * lrna_to_sell + tkn: -(omnipool.lrna[tkn] - lrna_removed[tkn]) / lrna_total * agent_lrna for tkn in omnipool.asset_list } @@ -1619,6 +1620,7 @@ def cash_out_omnipool(omnipool: OmnipoolState, agent: Agent, prices) -> float: liquidity_removed[tkn] += lrna_profits[tkn] new_holdings = {tkn: agent.holdings[tkn] for tkn in agent.holdings} + new_holdings['LRNA'] = agent_lrna for tkn in liquidity_removed: if tkn not in new_holdings: new_holdings[tkn] = 0 diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index 2209ee91..a17d569b 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -3127,17 +3127,19 @@ def test_cash_out_multiple_positions(trade_sizes: list[float]): withdrawal_fee=False ) - lp_quantity = 10000 + lp_quantity = mpf(10000) agent1 = Agent(holdings={'DOT': lp_quantity * len(trade_sizes)}) - agent2 = Agent(holdings={'DOT': 10000000, 'HDX': 10000000}) + agent2 = Agent(holdings={'DOT': mpf(10000000), 'HDX': mpf(10000000)}) for i, trade in enumerate(trade_sizes): initial_state.add_liquidity(agent1, tkn_add='DOT', quantity=lp_quantity, nft_id=str(i)) if trade > 0: - initial_state.swap(agent2, tkn_buy='HDX', tkn_sell='DOT', sell_quantity=trade) - else: - initial_state.swap(agent2, tkn_buy='DOT', tkn_sell='HDX', sell_quantity=-trade) + initial_state.swap(agent2, tkn_buy='HDX', tkn_sell='DOT', sell_quantity=mpf(trade)) + elif trade < 0: + initial_state.swap(agent2, tkn_buy='DOT', tkn_sell='HDX', sell_quantity=-mpf(trade)) spot_prices = {tkn: initial_state.price(initial_state, tkn, 'USD') for tkn in initial_state.asset_list} + spot_prices['LRNA'] = oamm.usd_price(initial_state, 'LRNA') + cash_out_value = cash_out_omnipool(initial_state, agent1, spot_prices) cash_out_state = initial_state.copy() cash_out_agent = agent1.copy() From 758f27da489cda875fc60e726556a99986d33737 Mon Sep 17 00:00:00 2001 From: poliwop Date: Fri, 20 Sep 2024 15:16:46 -0500 Subject: [PATCH 25/28] fixed liquidation test --- hydradx/tests/test_liquidations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hydradx/tests/test_liquidations.py b/hydradx/tests/test_liquidations.py index a7abe9f2..cc3a3892 100644 --- a/hydradx/tests/test_liquidations.py +++ b/hydradx/tests/test_liquidations.py @@ -713,7 +713,7 @@ def test_liquidate_against_omnipool_fuzz(collateral_amt1: float, ratio1: float, if cdp1.debt_amt == 0: # fully liquidated assert ratio1 >= full_liq_threshold elif cdp1.collateral_amt == 0: # fully liquidated, bad debt remaining - assert ratio1 > 1 + assert ratio1 > 1/(1 + mm.liquidation_penalty['DOT']) elif cdp1.debt_amt == debt_amt1: # not liquidated if ratio1 < liq_threshold: # 1. overcollateralized pass From a5b56b1204480ab6a42a1ce5ecaaf331bb0a8efb Mon Sep 17 00:00:00 2001 From: poliwop Date: Fri, 20 Sep 2024 15:44:06 -0500 Subject: [PATCH 26/28] fixed stableswap test --- hydradx/tests/test_stableswap.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hydradx/tests/test_stableswap.py b/hydradx/tests/test_stableswap.py index 4fe30c06..d46388e5 100644 --- a/hydradx/tests/test_stableswap.py +++ b/hydradx/tests/test_stableswap.py @@ -459,6 +459,7 @@ def test_exploitability(initial_lp: int, trade_size: int): break +@settings(deadline=timedelta(milliseconds=500)) @given( st.integers(min_value=1, max_value=1000000), st.floats(min_value=0.00001, max_value=0.99999) From e807eb590b4b41cebe2dcb10a635262c86a3cb3b Mon Sep 17 00:00:00 2001 From: poliwop Date: Mon, 23 Sep 2024 13:00:03 -0500 Subject: [PATCH 27/28] Removed unused function --- hydradx/model/amm/omnipool_amm.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index 54447cfe..3602b69b 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -1547,13 +1547,6 @@ def value_assets(prices: dict, assets: dict) -> float: ]) -def _turn_off_validations(omnipool: OmnipoolState) -> OmnipoolState: - new_state = omnipool.copy() - new_state.remove_liquidity_volatility_threshold = float('inf') - new_state.max_withdrawal_per_block = float('inf') - return new_state - - def cash_out_omnipool(omnipool: OmnipoolState, agent: Agent, prices) -> float: """ return the value of the agent's holdings if they withdraw all liquidity From 2a9b7b47db6bd64c1f9c724aa9de27d3c7b9e356 Mon Sep 17 00:00:00 2001 From: poliwop Date: Mon, 23 Sep 2024 13:03:55 -0500 Subject: [PATCH 28/28] Fixed calculate_remove_liquidity so it always returns nft_ids --- hydradx/model/amm/omnipool_amm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hydradx/model/amm/omnipool_amm.py b/hydradx/model/amm/omnipool_amm.py index 3602b69b..b5d5070b 100644 --- a/hydradx/model/amm/omnipool_amm.py +++ b/hydradx/model/amm/omnipool_amm.py @@ -906,7 +906,7 @@ def calculate_remove_liquidity(self, agent: Agent, quantity: float = None, tkn_r if quantity is not None: if nft_id is None: # remove specified quantity of shares from holdings k = (self.unique_id, tkn_remove) - return self._calculate_remove_one_position( + delta_qa, delta_r, delta_q, delta_s, delta_b, delta_l = self._calculate_remove_one_position( quantity=quantity, tkn_remove=tkn_remove, share_price=agent.share_prices[k] ) else: # remove specified quantity of shares from specified position