Skip to content

Commit

Permalink
Merge branch 'main' into stableswap-add-liquidity-fee
Browse files Browse the repository at this point in the history
  • Loading branch information
jepidoptera authored Oct 5, 2023
2 parents 9d0122c + 339be10 commit 5b8af6d
Show file tree
Hide file tree
Showing 7 changed files with 1,013 additions and 125 deletions.
103 changes: 91 additions & 12 deletions hydradx/model/amm/stableswap_amm.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ def __init__(
raise ValueError('Too many tokens (limit 5)')

self.amplification = amplification
self.amp_change_step = 0
self.target_amp_block = 0
self.time_step = 0
self.precision = precision
self.liquidity = dict()
self.asset_list: list[str] = []
Expand All @@ -44,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 @@ -54,6 +57,19 @@ 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:
self.amplification += self.amp_change_step

def set_amplification(self, amplification: float, duration: float):
self.target_amp_block = self.time_step + duration
self.amp_change_step = (amplification - self.amplification) / duration

def has_converged(self, v0, v1) -> bool:
diff = abs(v0 - v1)
if (v1 <= v0 and diff < self.precision) or (v1 > v0 and diff <= self.precision):
Expand Down Expand Up @@ -210,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 @@ -224,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 @@ -235,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 @@ -268,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 @@ -314,7 +392,9 @@ def add_liquidity(
initial_d = self.calculate_d()
updated_d = self.calculate_d(tuple(updated_reserves.values()))
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}.")

fixed_fee = self.trade_fee
fee = fixed_fee * self.n_coins / (4 * (self.n_coins - 1))
Expand All @@ -327,8 +407,7 @@ def add_liquidity(
abs(updated_reserves[tkn] - d1 * self.liquidity[tkn] / d0) * fee
for tkn in self.asset_list
]
if self.shares > 0 else
updated_reserves
if self.shares > 0 else updated_reserves
)

adjusted_d = self.calculate_d(adjusted_balances)
Expand Down Expand Up @@ -383,7 +462,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 @@ -414,7 +493,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
153 changes: 153 additions & 0 deletions hydradx/notebooks/Stableswap/AmplificationChange.ipynb

Large diffs are not rendered by default.

272 changes: 200 additions & 72 deletions hydradx/notebooks/Stableswap/ImpermanentLossAnalysis.ipynb

Large diffs are not rendered by default.

Loading

0 comments on commit 5b8af6d

Please sign in to comment.