Skip to content

Commit

Permalink
Merge pull request #153 from galacticcouncil/stableswap-IL-4-assets
Browse files Browse the repository at this point in the history
Stableswap il 4 assets
  • Loading branch information
jepidoptera authored Oct 5, 2023
2 parents 0e86675 + b3039d3 commit 339be10
Show file tree
Hide file tree
Showing 6 changed files with 870 additions and 162 deletions.
136 changes: 115 additions & 21 deletions hydradx/model/amm/stableswap_amm.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def __init__(

@property
def ann(self) -> float:
return self.amplification * len(self.asset_list) ** len(self.asset_list)
return self.amplification * self.n_coins

@property
def n_coins(self) -> int:
Expand All @@ -57,6 +57,10 @@ def n_coins(self) -> int:
def d(self) -> float:
return self.calculate_d()

def fail_transaction(self, error: str, **kwargs):
self.fail = error
return self

def update(self):
self.time_step += 1
if self.target_amp_block > self.time_step:
Expand Down Expand Up @@ -120,16 +124,44 @@ def calculate_y(self, reserves: list, d: float, max_iterations=128):
return y

# price is denominated in the first asset
@property
def spot_price(self):
x, y = self.liquidity.values()
return self.price_at_balance([x, y], self.d)
def spot_price(self, i: int = 1):
"""
return the price of TKN denominated in NUMÉRAIRE
"""
balances = list(self.liquidity.values())
if i == 0: # price of the numeraire is always 1
return 1
return self.price_at_balance(balances, self.d, i)

# price is denominated in the first asset
def price_at_balance(self, balances: list, d: float):
x, y = balances
c = self.amplification * self.n_coins ** (2 * self.n_coins)
return (x / y) * (c * x * y ** 2 + d ** 3) / (c * x ** 2 * y + d ** 3)
def price(self, tkn, denomination: str = ''):
"""
return the price of TKN denominated in NUMÉRAIRE
"""
if tkn == denomination:
return 1
i = list(self.liquidity.keys()).index(tkn)
j = list(self.liquidity.keys()).index(denomination)
return self.price_at_balance(
balances=list(self.liquidity.values()),
d=self.d,
i=i, j=j
)

def price_at_balance(self, balances: list, d: float, i: int = 1, j: int = 0):
n = self.n_coins
ann = self.ann

c = d
sorted_bal = sorted(balances)
for x in sorted_bal:
c = c * d / (n * x)

xi = balances[i]
xj = balances[j]

p = xj * (ann * xi + c) / (ann * xj + c) / xi

return p

def modified_balances(self, delta: dict = None, omit: list = ()):
balances = copy.copy(self.liquidity)
Expand Down Expand Up @@ -194,9 +226,9 @@ def swap(
buy_quantity = (self.liquidity[tkn_buy] - self.calculate_y(reserves, self.d)) * (1 - self.trade_fee)

if agent.holdings[tkn_sell] < sell_quantity:
return self.fail_transaction('Agent has insufficient funds.', agent)
return self.fail_transaction('Agent has insufficient funds.')
elif self.liquidity[tkn_buy] <= buy_quantity:
return self.fail_transaction('Pool has insufficient liquidity.', agent)
return self.fail_transaction('Pool has insufficient liquidity.')

new_agent = agent # .copy()
if tkn_buy not in new_agent.holdings:
Expand All @@ -208,6 +240,68 @@ def swap(

return self

def swap_one(
self,
agent: Agent,
quantity: float,
tkn_sell: str = '',
tkn_buy: str = '',
):
"""
This can be used when you want to change the price of one asset without changing the price of the others.
Specify one asset to buy or sell, and the quantity of each of the *other* assets to sell or buy.
The quantity of the specified asset to trade will be determined.
Caution: this will only work correctly if the pool is initially balanced (spot prices equal on all assets).
"""
if tkn_sell and tkn_buy:
raise ValueError('Cannot specify both buy and sell quantities.')

if tkn_buy:
tkns_sell = list(filter(lambda t: t != tkn_buy, self.asset_list))
for tkn in tkns_sell:
if tkn not in agent.holdings:
self.fail_transaction(f'Agent does not have any {tkn}.')
if min([agent.holdings[tkn] for tkn in tkns_sell]) < quantity:
return self.fail_transaction('Agent has insufficient funds.')

sell_quantity = quantity
buy_quantity = (self.liquidity[tkn_buy] - self.calculate_y(
self.modified_balances(delta={tkn: quantity for tkn in tkns_sell}, omit=[tkn_buy]),
self.d
)) * (1 - self.trade_fee)

if self.liquidity[tkn_buy] < buy_quantity:
return self.fail_transaction('Pool has insufficient liquidity.')

for tkn in tkns_sell:
self.liquidity[tkn] += sell_quantity
agent.holdings[tkn] -= sell_quantity
self.liquidity[tkn_buy] -= buy_quantity
agent.holdings[tkn_buy] += buy_quantity

elif tkn_sell:
tkns_buy = list(filter(lambda t: t != tkn_sell, self.asset_list))
buy_quantity = quantity

if min([self.liquidity[tkn] for tkn in tkns_buy]) < buy_quantity:
return self.fail_transaction('Pool has insufficient liquidity.')

sell_quantity = (self.calculate_y(
self.modified_balances(delta={tkn: -quantity for tkn in tkns_buy}, omit=[tkn_sell]),
self.d
) - self.liquidity[tkn_sell]) / (1 - self.trade_fee)
if agent.holdings[tkn_sell] < sell_quantity:
return self.fail_transaction(f'Agent has insufficient funds. {agent.holdings[tkn_sell]} < {quantity}')
for tkn in tkns_buy:
self.liquidity[tkn] -= buy_quantity
if tkn not in agent.holdings:
agent.holdings[tkn] = 0
agent.holdings[tkn] += buy_quantity
self.liquidity[tkn_sell] += sell_quantity
agent.holdings[tkn_sell] -= sell_quantity

return self

def withdraw_asset(
self,
agent: Agent,
Expand All @@ -219,15 +313,15 @@ def withdraw_asset(
Calculate a withdrawal based on the asset quantity rather than the share quantity
"""
if quantity >= self.liquidity[tkn_remove]:
return self.fail_transaction(f'Not enough liquidity in {tkn_remove}.', agent)
return self.fail_transaction(f'Not enough liquidity in {tkn_remove}.')
if quantity <= 0:
raise ValueError('Withdraw quantity must be > 0.')

shares_removed = self.calculate_withdrawal_shares(tkn_remove, quantity)

if shares_removed > agent.holdings[self.unique_id]:
if fail_on_overdraw:
return self.fail_transaction('Agent tried to remove more shares than it owns.', agent)
return self.fail_transaction('Agent tried to remove more shares than it owns.')
else:
# just round down
shares_removed = agent.holdings[self.unique_id]
Expand All @@ -252,9 +346,9 @@ def remove_liquidity(
# * Solve Eqn against y_i for D - _token_amount

if shares_removed > agent.holdings[self.unique_id]:
return self.fail_transaction('Agent has insufficient funds.', agent)
return self.fail_transaction('Agent has insufficient funds.')
elif shares_removed <= 0:
return self.fail_transaction('Withdraw quantity must be > 0.', agent)
return self.fail_transaction('Withdraw quantity must be > 0.')

_fee = self.trade_fee
_fee *= self.n_coins / 4 / (self.n_coins - 1)
Expand Down Expand Up @@ -297,9 +391,9 @@ def add_liquidity(
updated_d = self.calculate_d(self.modified_balances(delta={tkn_add: quantity}))

if updated_d < initial_d:
return self.fail_transaction('invariant decreased for some reason', agent)
return self.fail_transaction('invariant decreased for some reason')
if agent.holdings[tkn_add] < quantity:
return self.fail_transaction(f"Agent doesn't have enough {tkn_add}.", agent)
return self.fail_transaction(f"Agent doesn't have enough {tkn_add}.")

self.liquidity[tkn_add] += quantity
agent.holdings[tkn_add] -= quantity
Expand All @@ -309,7 +403,7 @@ def add_liquidity(
self.shares = updated_d

elif self.shares < 0:
return self.fail_transaction('Shares cannot go below 0.', agent)
return self.fail_transaction('Shares cannot go below 0.')
# why would this possibly happen?

else:
Expand Down Expand Up @@ -339,7 +433,7 @@ def buy_shares(

if delta_tkn > agent.holdings[tkn_add]:
if fail_overdraft:
return self.fail_transaction(f"Agent doesn't have enough {tkn_add}.", agent)
return self.fail_transaction(f"Agent doesn't have enough {tkn_add}.")
else:
# instead of failing, just round down
delta_tkn = agent.holdings[tkn_add]
Expand Down Expand Up @@ -370,7 +464,7 @@ def remove_uniform(
delta_tkns[tkn] = share_fraction * self.liquidity[tkn] # delta_tkn is positive

if delta_tkns[tkn] >= self.liquidity[tkn]:
return self.fail_transaction(f'Not enough liquidity in {tkn}.', agent)
return self.fail_transaction(f'Not enough liquidity in {tkn}.')

if tkn not in agent.holdings:
agent.holdings[tkn] = 0
Expand Down
23 changes: 17 additions & 6 deletions hydradx/model/amm/trade_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,8 @@ def __init__(self, _when):
self.done = False

def execute(self, state: GlobalState, agent_id: str):
if self.done or state.time_step < self.when:
if state.time_step < self.when:
return state
self.done = True
agent: Agent = state.agents[agent_id]
pool = state.pools[pool_id]

Expand Down Expand Up @@ -584,13 +583,21 @@ def price_after_trade(buy_amount: float = 0, sell_amount: float = 0):
balance_in = stable_pool.calculate_y(
stable_pool.modified_balances(delta={buy_asset: -buy_amount}, omit=[sell_asset]), d
)
return stable_pool.price_at_balance([balance_in, balance_out], d)
balances = list(stable_pool.liquidity.values())
balances[list(stable_pool.liquidity.keys()).index(buy_asset)] = balance_out
balances[list(stable_pool.liquidity.keys()).index(sell_asset)] = balance_in
return stable_pool.price_at_balance(
balances,
d=stable_pool.d,
i=list(stable_pool.liquidity.keys()).index(buy_asset),
j=list(stable_pool.liquidity.keys()).index(sell_asset)
)

delta_y = find_agent_delta_y(target_price, price_after_trade, precision=precision)
delta_x = (
stable_pool.liquidity[sell_asset]
- stable_pool.calculate_y(stable_pool.modified_balances(delta={buy_asset: -delta_y}, omit=[sell_asset]), d)
) * (1 + stable_pool.trade_fee)
) / (1 - stable_pool.trade_fee)

projected_profit = (
delta_y * state.price(buy_asset)
Expand All @@ -602,8 +609,12 @@ def price_after_trade(buy_amount: float = 0, sell_amount: float = 0):
# agent.trade_rejected += 1
return state

new_state = state.execute_swap(pool_id, agent_id, sell_asset, buy_asset, buy_quantity=delta_y)
return new_state
agent = state.agents[agent_id]
# old_wealth = sum([state.price(tkn) * agent.holdings[tkn] for tkn in agent.holdings.keys()])
state.pools[pool_id].swap(agent, tkn_sell=sell_asset, tkn_buy=buy_asset, buy_quantity=delta_y)
#
# actual_profit = sum([state.price(tkn) * agent.holdings[tkn] for tkn in agent.holdings.keys()]) - old_wealth
return state

return TradeStrategy(strategy, name='stableswap arbitrage')

Expand Down
Loading

0 comments on commit 339be10

Please sign in to comment.