Skip to content

Commit

Permalink
Merge branch 'main' into test_stableswap_amp_change
Browse files Browse the repository at this point in the history
  • Loading branch information
jepidoptera authored Oct 17, 2023
2 parents 6a9b0ea + b03954a commit 2eff53c
Show file tree
Hide file tree
Showing 4 changed files with 957 additions and 53 deletions.
62 changes: 39 additions & 23 deletions hydradx/model/amm/stableswap_amm.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import copy

from .amm import AMM
from .agents import Agent
from .amm import AMM


class StableSwapPoolState(AMM):
Expand Down Expand Up @@ -163,6 +163,22 @@ def price_at_balance(self, balances: list, d: float, i: int = 1, j: int = 0):

return p

def share_price(self, numeraire: str = ''):
i = 0 if numeraire == '' else list(self.liquidity.keys()).index(numeraire)
d = self.calculate_d()
s = self.shares
a = self.amplification
n = self.n_coins

c = d
sorted_liq = sorted(self.liquidity.values())
for x in sorted_liq:
c = c * d / (n * x)
xi = self.liquidity[self.asset_list[i]]
ann = self.ann
p = (d * xi * ann + xi * (n + 1) * c - xi * d) / (xi * ann + c) / s
return p

def modified_balances(self, delta: dict = None, omit: list = ()):
balances = copy.copy(self.liquidity)
if delta:
Expand All @@ -186,29 +202,29 @@ def __repr__(self):
liquidity = {tkn: round(self.liquidity[tkn], precision) for tkn in self.asset_list}
shares = round(self.shares, precision)
return (
f'Stable Swap Pool: {self.unique_id}\n'
f'********************************\n'
f'trade fee: {self.trade_fee}\n'
f'shares: {shares}\n'
f'amplification constant: {self.amplification}\n'
f'tokens: (\n\n'
) + '\n'.join(
f'Stable Swap Pool: {self.unique_id}\n'
f'********************************\n'
f'trade fee: {self.trade_fee}\n'
f'shares: {shares}\n'
f'amplification constant: {self.amplification}\n'
f'tokens: (\n\n'
) + '\n'.join(
[(
f' {token}\n'
f' quantity: {liquidity[token]}\n'
f' weight: {liquidity[token] / sum(liquidity.values())}\n'
+ (
f' conversion metrics:\n'
f' price: {self.conversion_metrics[token]["price"]}\n'
f' old shares: {self.conversion_metrics[token]["old_shares"]}\n'
f' Omnipool shares: {self.conversion_metrics[token]["omnipool_shares"]}\n'
f' subpool shares: {self.conversion_metrics[token]["subpool_shares"]}\n'
if token in self.conversion_metrics else ""
)
f' {token}\n'
f' quantity: {liquidity[token]}\n'
f' weight: {liquidity[token] / sum(liquidity.values())}\n'
+ (
f' conversion metrics:\n'
f' price: {self.conversion_metrics[token]["price"]}\n'
f' old shares: {self.conversion_metrics[token]["old_shares"]}\n'
f' Omnipool shares: {self.conversion_metrics[token]["omnipool_shares"]}\n'
f' subpool shares: {self.conversion_metrics[token]["subpool_shares"]}\n'
if token in self.conversion_metrics else ""
)
) for token in self.asset_list]
) + '\n)\n' + (
f'error message:{self.fail or "none"}'
)
f'error message:{self.fail or "none"}'
)

def swap(
self,
Expand Down Expand Up @@ -443,9 +459,9 @@ def buy_shares(
asset_reserve = 0
for tkn, balance in self.liquidity.items():
dx_expected = (
balance * d1 / initial_d - balance
balance * d1 / initial_d - balance
) if tkn != tkn_add else (
y - balance * d1 / initial_d
y - balance * d1 / initial_d
)
reduced_balance = balance - fee * dx_expected
if tkn == tkn_add:
Expand Down
329 changes: 329 additions & 0 deletions hydradx/notebooks/Stableswap/PriceByQuantityRatio.ipynb

Large diffs are not rendered by default.

511 changes: 511 additions & 0 deletions hydradx/notebooks/Stableswap/PriceByQuantityRatioMultiAsset.ipynb

Large diffs are not rendered by default.

108 changes: 78 additions & 30 deletions hydradx/tests/test_stableswap.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import pytest
import copy
import functools

import pytest
from hypothesis import given, strategies as st
from mpmath import mp, mpf

from hydradx.model import run
from hydradx.model.amm import stableswap_amm as stableswap
from hydradx.model.amm.stableswap_amm import StableSwapPoolState, simulate_swap
from hydradx.model.amm.agents import Agent
from hydradx.model.amm.trade_strategies import random_swaps, stableswap_arbitrage
from hydradx.model.amm.global_state import GlobalState
from hydradx.model import run
from hypothesis import given, strategies as st, settings, reproduce_failure
from mpmath import mp, mpf
from hydradx.model.amm.stableswap_amm import StableSwapPoolState
from hydradx.model.amm.trade_strategies import random_swaps, stableswap_arbitrage
from hydradx.tests.strategies_omnipool import stableswap_config

mp.dps = 50

asset_price_strategy = st.floats(min_value=0.01, max_value=1000)
Expand Down Expand Up @@ -61,10 +66,10 @@ def test_swap_invariant(initial_pool: StableSwapPoolState):
raise AssertionError('Some assets were lost along the way.')


@given(st.integers(min_value=1000,max_value=1000000),
@given(st.integers(min_value=1000, max_value=1000000),
st.integers(min_value=1000, max_value=1000000),
st.integers(min_value=10, max_value=1000)
)
)
def test_spot_price_two_assets(token_a: int, token_b: int, amp: int):
initial_pool = StableSwapPoolState(
tokens={"A": token_a, "B": token_b},
Expand All @@ -86,13 +91,13 @@ def test_spot_price_two_assets(token_a: int, token_b: int, amp: int):
exec_price = delta_a / delta_b

spot_price_final = swap_state.spot_price()
if spot_price_initial > exec_price and (spot_price_initial - exec_price)/spot_price_initial > 10e-10:

if spot_price_initial > exec_price and (spot_price_initial - exec_price) / spot_price_initial > 10e-10:
raise AssertionError('Initial spot price should be lower than execution price.')
if exec_price > spot_price_final and (exec_price - spot_price_final)/spot_price_final > 10e-10:
if exec_price > spot_price_final and (exec_price - spot_price_final) / spot_price_final > 10e-10:
raise AssertionError('Execution price should be lower than final spot price.')


@given(st.integers(min_value=1000, max_value=1000000),
st.integers(min_value=1000, max_value=1000000),
st.integers(min_value=1000, max_value=1000000),
Expand Down Expand Up @@ -120,12 +125,54 @@ def test_spot_price(token_a: int, token_b: int, token_c: int, token_d: int, amp:

spot_price_final = swap_pool.price(tkns[i], "A")

if spot_price_initial > exec_price and (spot_price_initial - exec_price)/spot_price_initial > 10e-10:
if spot_price_initial > exec_price and (spot_price_initial - exec_price) / spot_price_initial > 10e-10:
raise AssertionError('Initial spot price should be lower than execution price.')
if exec_price > spot_price_final and (exec_price - spot_price_final)/spot_price_final > 10e-10:
if exec_price > spot_price_final and (exec_price - spot_price_final) / spot_price_final > 10e-10:
raise AssertionError('Execution price should be lower than final spot price.')


@given(st.integers(min_value=1000, max_value=1000000),
st.integers(min_value=1000, max_value=1000000),
st.integers(min_value=10, max_value=1000)
)
def test_share_price(token_a: int, token_b: int, amp: int):
tokens = {"A": token_a, "B": token_b}
initial_pool = StableSwapPoolState(
tokens=tokens,
amplification=amp,
trade_fee=0.0,
unique_id='stableswap'
)

share_price_initial = initial_pool.share_price()

agent = Agent(holdings={"A": 100000000, "B": 100000000})
delta_tkn = 1
shares_initial = initial_pool.shares
add_pool = initial_pool.copy()
add_pool.add_liquidity(agent, quantity=delta_tkn, tkn_add="A")
shares_final = add_pool.shares
delta_a = add_pool.liquidity["A"] - tokens["A"]
delta_s = shares_final - shares_initial
exec_price = delta_a / delta_s

if share_price_initial > exec_price and (share_price_initial - exec_price) / share_price_initial > 10e-10:
raise AssertionError('Initial share price should be lower than execution price.')

# now we test withdraw

delta_s = agent.holdings['stableswap']
share_price_initial = add_pool.share_price()
a_initial = add_pool.liquidity['A']
withdraw_pool = add_pool.copy()
withdraw_pool.remove_liquidity(agent, shares_removed=delta_s, tkn_remove='A')
a_final = withdraw_pool.liquidity['A']
exec_price = (a_initial - a_final) / delta_s

if share_price_initial < exec_price and (exec_price - share_price_initial) / share_price_initial > 10e-10:
raise AssertionError('Initial share price should be higher than execution price.')


@given(stableswap_config(trade_fee=0))
def test_round_trip_dy(initial_pool: StableSwapPoolState):
d = initial_pool.calculate_d()
Expand All @@ -135,7 +182,7 @@ def test_round_trip_dy(initial_pool: StableSwapPoolState):
if y != pytest.approx(initial_pool.liquidity[asset_a]) or y < initial_pool.liquidity[asset_a]:
raise AssertionError('Round-trip calculation incorrect.')
modified_d = initial_pool.calculate_d(initial_pool.modified_balances(delta={asset_a: 1}))
if initial_pool.calculate_y(reserves=other_reserves, d=modified_d) != pytest.approx(y+1):
if initial_pool.calculate_y(reserves=other_reserves, d=modified_d) != pytest.approx(y + 1):
raise AssertionError('Round-trip calculation incorrect.')


Expand All @@ -158,10 +205,10 @@ def test_remove_asset(initial_pool: StableSwapPoolState):
withdraw_asset_agent, 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)
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.")

Expand All @@ -186,11 +233,11 @@ def test_buy_shares(initial_pool: StableSwapPoolState):
)

if (
add_liquidity_agent.holdings[tkn_add] != pytest.approx(buy_shares_agent.holdings[tkn_add])
or add_liquidity_agent.holdings[pool_name] != pytest.approx(buy_shares_agent.holdings[pool_name])
or add_liquidity_pool.liquidity[tkn_add] != pytest.approx(buy_shares_pool.liquidity[tkn_add])
or add_liquidity_pool.shares != pytest.approx(buy_shares_pool.shares)
or add_liquidity_pool.calculate_d() != pytest.approx(buy_shares_pool.calculate_d())
add_liquidity_agent.holdings[tkn_add] != pytest.approx(buy_shares_agent.holdings[tkn_add])
or add_liquidity_agent.holdings[pool_name] != pytest.approx(buy_shares_agent.holdings[pool_name])
or add_liquidity_pool.liquidity[tkn_add] != pytest.approx(buy_shares_pool.liquidity[tkn_add])
or add_liquidity_pool.shares != pytest.approx(buy_shares_pool.shares)
or add_liquidity_pool.calculate_d() != pytest.approx(buy_shares_pool.calculate_d())
):
raise AssertionError("Asset values don't match.")

Expand Down Expand Up @@ -304,6 +351,7 @@ def test_add_remove_liquidity(initial_pool: StableSwapPoolState):
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(
Expand Down Expand Up @@ -442,9 +490,9 @@ def test_swap_one(amplification, swap_fraction):

for tkn in initial_state.asset_list:
if (
sell_state.price(tkn, stablecoin)
!= pytest.approx(initial_state.price(tkn, stablecoin))
and tkn != tkn_sell
sell_state.price(tkn, stablecoin)
!= pytest.approx(initial_state.price(tkn, stablecoin))
and tkn != tkn_sell
):
raise AssertionError('Spot price changed for non-swapped token.')

Expand All @@ -466,9 +514,9 @@ def test_swap_one(amplification, swap_fraction):

for tkn in initial_state.asset_list:
if (
buy_state.price(tkn, stablecoin)
!= pytest.approx(initial_state.price(tkn, stablecoin))
and tkn != tkn_buy
buy_state.price(tkn, stablecoin)
!= pytest.approx(initial_state.price(tkn, stablecoin))
and tkn != tkn_buy
):
raise AssertionError('Spot price changed for non-swapped token.')

Expand Down

0 comments on commit 2eff53c

Please sign in to comment.