Skip to content

Commit

Permalink
Merge branch 'main' into model-LP-fees
Browse files Browse the repository at this point in the history
  • Loading branch information
jepidoptera committed Jun 27, 2023
2 parents 91052f4 + c3bea45 commit cc5b0cc
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 91 deletions.
13 changes: 10 additions & 3 deletions hydradx/model/amm/omnipool_amm.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ def __init__(self,
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))
else:
self.oracles['price'] = Oracle(sma_equivalent_length=19, first_block=Block(self), last_values=last_oracle_values['price'])
self.oracles['price'] = Oracle(sma_equivalent_length=19, 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({
name: Oracle(sma_equivalent_length=period, last_values=last_oracle_values[name]
Expand Down Expand Up @@ -901,8 +902,14 @@ def execute_add_liquidity(
) -> tuple[OmnipoolState, Agent]:
"""Compute new state after liquidity addition"""

if quantity <= 0:
return state.fail_transaction('Quantity must be non-negative.', agent)

delta_Q = lrna_price(state, tkn_add) * quantity
if not (state.unique_id, tkn_add) in agent.holdings:
if (state.unique_id, tkn_add) in agent.holdings:
if agent.holdings[(state.unique_id, tkn_add)] != 0:
return state.fail_transaction(f'Agent already has liquidity in pool {tkn_add}.', agent)
else:
agent.holdings[(state.unique_id, tkn_add)] = 0

if agent.holdings[tkn_add] < quantity:
Expand Down Expand Up @@ -1326,4 +1333,4 @@ def cash_out_omnipool(omnipool: OmnipoolState, agent: Agent, prices) -> float:
if agent_holdings[tkn] > 0 and tkn not in prices:
raise ValueError(f'Agent has holdings in {tkn} but no price was given.')

return value_assets(prices, agent_holdings)
return value_assets(prices, agent_holdings)
104 changes: 73 additions & 31 deletions hydradx/model/amm/stableswap_amm.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,44 +186,46 @@ def execute_swap(
return state.fail_transaction('Pool has insufficient liquidity.', agent)

new_agent = agent # .copy()
if tkn_buy not in new_agent.holdings:
new_agent.holdings[tkn_buy] = 0
new_agent.holdings[tkn_buy] += buy_quantity
new_agent.holdings[tkn_sell] -= sell_quantity
state.liquidity[tkn_buy] -= buy_quantity
state.liquidity[tkn_sell] += sell_quantity

return state, new_agent


def execute_remove_liquidity(
state: StableSwapPoolState,
agent: Agent,
shares_removed: float,
tkn_remove: str
):
if shares_removed > agent.holdings[state.unique_id]:
raise ValueError('Agent tried to remove more shares than it owns.')
elif shares_removed <= 0:
raise ValueError('Withdraw quantity must be > 0.')

share_fraction = shares_removed / state.shares

updated_d = state.d * (1 - share_fraction * (1 - state.trade_fee))
delta_tkn = state.calculate_y(
state.modified_balances(delta={}, omit=[tkn_remove]),
updated_d
) - state.liquidity[tkn_remove]

if delta_tkn >= state.liquidity[tkn_remove]:
return state.fail_transaction(f'Not enough liquidity in {tkn_remove}.', agent)

if tkn_remove not in agent.holdings:
agent.holdings[tkn_remove] = 0

state.shares -= shares_removed
agent.holdings[state.unique_id] -= shares_removed
state.liquidity[tkn_remove] += delta_tkn
agent.holdings[tkn_remove] -= delta_tkn # agent is receiving funds, because delta_tkn is a negative number
return state, agent
#
# def execute_remove_liquidity_old(
# state: StableSwapPoolState,
# agent: Agent,
# shares_removed: float,
# tkn_remove: str
# ):
# if shares_removed > agent.holdings[state.unique_id]:
# raise ValueError('Agent tried to remove more shares than it owns.')
# elif shares_removed <= 0:
# raise ValueError('Withdraw quantity must be > 0.')
#
# share_fraction = shares_removed / state.shares
#
# updated_d = state.d * (1 - share_fraction * (1 - state.trade_fee))
# delta_tkn = state.calculate_y(
# state.modified_balances(delta={}, omit=[tkn_remove]),
# updated_d
# ) - state.liquidity[tkn_remove]
#
# if delta_tkn >= state.liquidity[tkn_remove]:
# return state.fail_transaction(f'Not enough liquidity in {tkn_remove}.', agent)
#
# if tkn_remove not in agent.holdings:
# agent.holdings[tkn_remove] = 0
#
# state.shares -= shares_removed
# agent.holdings[state.unique_id] -= shares_removed
# state.liquidity[tkn_remove] += delta_tkn
# agent.holdings[tkn_remove] -= delta_tkn # agent is receiving funds, because delta_tkn is a negative number
# return state, agent


def execute_withdraw_asset(
Expand Down Expand Up @@ -260,6 +262,46 @@ def execute_withdraw_asset(
return state, agent


def execute_remove_liquidity(
state: StableSwapPoolState,
agent: Agent,
shares_removed: float,
tkn_remove: str,
):
# First, need to calculate
# * Get current D
# * Solve Eqn against y_i for D - _token_amount

_fee = state.trade_fee

initial_d = state.calculate_d()
reduced_d = initial_d - shares_removed * initial_d / state.shares

xp_reduced = copy.copy(state.liquidity)
xp_reduced.pop(tkn_remove)

reduced_y = state.calculate_y(state.modified_balances(omit=[tkn_remove]), reduced_d)
asset_reserve = state.liquidity[tkn_remove]

for tkn in state.asset_list:
if tkn == tkn_remove:
dx_expected = state.liquidity[tkn] * reduced_d / initial_d - reduced_y
asset_reserve -= _fee * dx_expected
else:
dx_expected = state.liquidity[tkn] - state.liquidity[tkn] * reduced_d / initial_d
xp_reduced[tkn] -= _fee * dx_expected

dy = asset_reserve - state.calculate_y(list(xp_reduced.values()), reduced_d)

agent.holdings[state.unique_id] -= shares_removed
state.shares -= shares_removed
state.liquidity[tkn_remove] -= dy
if tkn_remove not in agent.holdings:
agent.holdings[tkn_remove] = 0
agent.holdings[tkn_remove] += dy
return state, agent


def execute_add_liquidity(
state: StableSwapPoolState,
agent: Agent,
Expand Down
78 changes: 57 additions & 21 deletions hydradx/tests/test_omnipool_amm.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import copy
import math
import random

import pytest
from hypothesis import given, strategies as st, assume, settings
Expand All @@ -13,7 +12,6 @@
from hydradx.model.amm.trade_strategies import constant_swaps, omnipool_arbitrage
from hydradx.tests.strategies_omnipool import omnipool_reasonable_config, omnipool_config, assets_config


asset_price_strategy = st.floats(min_value=0.0001, max_value=100000)
asset_price_bounded_strategy = st.floats(min_value=0.1, max_value=10)
asset_number_strategy = st.integers(min_value=3, max_value=5)
Expand Down Expand Up @@ -127,6 +125,40 @@ 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.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_quantity_zero_should_fail(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 = 0

new_state, new_agents = oamm.add_liquidity(old_state, old_agent, delta_R, tkn)

if not new_state.fail:
raise AssertionError(f'Adding liquidity with quantity zero should fail.')


@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]
Expand Down Expand Up @@ -1251,52 +1283,56 @@ def test_volatility_limit(omnipool: oamm.OmnipoolState):
def test_LP_limits(omnipool: oamm.OmnipoolState, max_withdrawal_per_block, max_lp_per_block):
omnipool.max_withdrawal_per_block = max_withdrawal_per_block
omnipool.max_lp_per_block = max_lp_per_block
agent = Agent(holdings={'HDX': 10000000000})
state = omnipool.copy()
initial_agent = Agent(holdings={'HDX': 10000000000})
agent = initial_agent.copy()
oamm.execute_add_liquidity(
state=omnipool,
state=state,
agent=agent,
tkn_add='HDX',
quantity=omnipool.liquidity['HDX'] * max_lp_per_block
quantity=state.liquidity['HDX'] * max_lp_per_block
)
if omnipool.fail:
if state.fail:
raise AssertionError('Valid LP operation failed.')
omnipool.update()
state = omnipool.copy()
agent = initial_agent.copy()
oamm.execute_add_liquidity(
state=omnipool,
state=state,
agent=agent,
tkn_add='HDX',
quantity=omnipool.liquidity['HDX'] * max_lp_per_block + 1
quantity=state.liquidity['HDX'] * max_lp_per_block + 1
)
if not omnipool.fail:
if not state.fail:
raise AssertionError('Invalid LP operation succeeded.')
omnipool.update()
state = omnipool.copy()
agent = initial_agent.copy()
# add liquidity again to test remove liquidity
oamm.execute_add_liquidity(
state=omnipool,
state=state,
agent=agent,
tkn_add='HDX',
quantity=omnipool.liquidity['HDX'] * max_lp_per_block
quantity=state.liquidity['HDX'] * max_lp_per_block
)
if omnipool.fail:
if state.fail:
raise AssertionError('Second LP operation failed.')
withdraw_quantity = agent.holdings[('omnipool', 'HDX')]
total_shares = omnipool.shares['HDX']
total_shares = state.shares['HDX']
oamm.execute_remove_liquidity(
state=omnipool,
state=state,
agent=agent,
tkn_remove='HDX',
quantity=withdraw_quantity # agent.holdings[('omnipool', 'HDX')]
)
if withdraw_quantity / total_shares > max_withdrawal_per_block and not omnipool.fail:
if withdraw_quantity / total_shares > max_withdrawal_per_block and not state.fail:
raise AssertionError('Agent was able to remove too much liquidity.')
omnipool.update()
state.update()
oamm.execute_remove_liquidity(
state=omnipool,
state=state,
agent=agent,
tkn_remove='HDX',
quantity=omnipool.shares['HDX'] * max_withdrawal_per_block
quantity=state.shares['HDX'] * max_withdrawal_per_block
)
if omnipool.fail:
if state.fail:
raise AssertionError('Agent was not able to remove liquidity.')


Expand Down
93 changes: 69 additions & 24 deletions hydradx/tests/test_stableswap.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,30 +70,33 @@ def test_round_trip_dy(initial_pool: StableSwapPoolState):
raise AssertionError('Round-trip calculation incorrect.')


@given(stableswap_config(precision=0.000000001))
def test_remove_asset(initial_pool: StableSwapPoolState):
initial_agent = Agent(
holdings={tkn: 0 for tkn in initial_pool.asset_list}
)
# agent holds all the shares
tkn_remove = initial_pool.asset_list[0]
pool_name = initial_pool.unique_id
delta_shares = min(initial_pool.shares / 2, 100)
initial_agent.holdings.update({initial_pool.unique_id: delta_shares + 1})
withdraw_shares_pool, withdraw_shares_agent = stableswap.remove_liquidity(
initial_pool, initial_agent, delta_shares, tkn_remove
)
delta_tkn = withdraw_shares_agent.holdings[tkn_remove] - initial_agent.holdings[tkn_remove]
withdraw_asset_pool, withdraw_asset_agent = stableswap.execute_withdraw_asset(
initial_pool.copy(), initial_agent.copy(), delta_tkn, tkn_remove
)
if (
withdraw_asset_agent.holdings[tkn_remove] != pytest.approx(withdraw_shares_agent.holdings[tkn_remove])
or withdraw_asset_agent.holdings[pool_name] != pytest.approx(withdraw_shares_agent.holdings[pool_name])
or withdraw_shares_pool.liquidity[tkn_remove] != pytest.approx(withdraw_asset_pool.liquidity[tkn_remove])
or withdraw_shares_pool.shares != pytest.approx(withdraw_asset_pool.shares)
):
raise AssertionError("Asset values don't match.")
# commented out because without further work, withdraw_asset and remove_liquidity are not equivalent.
# This is because withdraw_asset was written based remove_liquidity_old, which is not equivalent to remove_liquidity.
#
# @given(stableswap_config(precision=0.000000001))
# def test_remove_asset(initial_pool: StableSwapPoolState):
# initial_agent = Agent(
# holdings={tkn: 0 for tkn in initial_pool.asset_list}
# )
# # agent holds all the shares
# tkn_remove = initial_pool.asset_list[0]
# pool_name = initial_pool.unique_id
# delta_shares = min(initial_pool.shares / 2, 100)
# initial_agent.holdings.update({initial_pool.unique_id: delta_shares + 1})
# withdraw_shares_pool, withdraw_shares_agent = stableswap.remove_liquidity(
# initial_pool, initial_agent, delta_shares, tkn_remove
# )
# delta_tkn = withdraw_shares_agent.holdings[tkn_remove] - initial_agent.holdings[tkn_remove]
# withdraw_asset_pool, withdraw_asset_agent = stableswap.execute_withdraw_asset(
# initial_pool.copy(), initial_agent.copy(), delta_tkn, tkn_remove
# )
# if (
# withdraw_asset_agent.holdings[tkn_remove] != pytest.approx(withdraw_shares_agent.holdings[tkn_remove])
# or withdraw_asset_agent.holdings[pool_name] != pytest.approx(withdraw_shares_agent.holdings[pool_name])
# or withdraw_shares_pool.liquidity[tkn_remove] != pytest.approx(withdraw_asset_pool.liquidity[tkn_remove])
# or withdraw_shares_pool.shares != pytest.approx(withdraw_asset_pool.shares)
# ):
# raise AssertionError("Asset values don't match.")


@given(stableswap_config(precision=0.000000001))
Expand Down Expand Up @@ -197,3 +200,45 @@ def test_add_remove_liquidity(initial_pool: StableSwapPoolState):
raise AssertionError('Stableswap equation does not hold after remove liquidity operation.')
if remove_liquidity_agent.holdings[lp_tkn] != pytest.approx(lp.holdings[lp_tkn]):
raise AssertionError('LP did not get the same balance back when withdrawing liquidity.')


def test_curve_style_withdraw_fees():
initial_state = stableswap.StableSwapPoolState(
tokens={
'USDA': 1000000,
'USDB': 1000000,
'USDC': 1000000,
'USDD': 1000000,
}, amplification=100, trade_fee=0.003,
unique_id='test_pool'
)
initial_agent = Agent(
holdings={'USDA': 100000}
)
test_state, test_agent = stableswap.execute_add_liquidity(
state=initial_state.copy(),
agent=initial_agent.copy(),
quantity=initial_agent.holdings['USDA'],
tkn_add='USDA',
)

stable_state, stable_agent = stableswap.execute_remove_liquidity(
state=test_state.copy(),
agent=test_agent.copy(),
shares_removed=test_agent.holdings['test_pool'],
tkn_remove='USDB'
)
effective_fee_withdraw = 1 - stable_agent.holdings['USDB'] / initial_agent.holdings['USDA']

swap_state, swap_agent = stableswap.execute_swap(
initial_state.copy(),
initial_agent.copy(),
tkn_sell='USDA',
tkn_buy='USDB',
sell_quantity=initial_agent.holdings['USDA']
)
effective_fee_swap = 1 - swap_agent.holdings['USDB'] / initial_agent.holdings['USDA']

if effective_fee_withdraw <= effective_fee_swap:
raise AssertionError('Withdraw fee is not higher than swap fee.')

Loading

0 comments on commit cc5b0cc

Please sign in to comment.