From 535df3c0345a964116e7e5341328d10459a92a93 Mon Sep 17 00:00:00 2001 From: poliwop Date: Thu, 29 Jun 2023 16:23:11 -0500 Subject: [PATCH] Making trade strategies into mutators that do not return state, but mutate the input state. --- hydradx/model/amm/global_state.py | 27 +- hydradx/model/amm/trade_strategies.py | 181 ++++++------ hydradx/model/run.py | 6 +- hydradx/tests/strategies_omnipool.py | 6 +- hydradx/tests/test_amm.py | 93 +++--- hydradx/tests/test_omnipool_agents.py | 7 +- hydradx/tests/test_omnipool_amm.py | 402 ++++++++++++++------------ hydradx/tests/test_stableswap.py | 58 ++-- 8 files changed, 424 insertions(+), 356 deletions(-) diff --git a/hydradx/model/amm/global_state.py b/hydradx/model/amm/global_state.py index 7d713df3..a192918c 100644 --- a/hydradx/model/amm/global_state.py +++ b/hydradx/model/amm/global_state.py @@ -1,11 +1,10 @@ import copy import random from typing import Callable -from .omnipool_amm import OmnipoolState, calculate_remove_liquidity from .agents import Agent, AgentArchiveState from .amm import AMM, FeeMechanism -from .omnipool_amm import OmnipoolState, calculate_remove_liquidity, OmnipoolArchiveState +from .omnipool_amm import OmnipoolState, calculate_remove_liquidity class GlobalState: @@ -424,14 +423,28 @@ def external_market_trade( # do a trade at spot price on the external market # this should maybe only work in USD, because we're probably talking about coinbase or something new_state = old_state.copy() - agent = new_state.agents[agent_id] + execute_external_market_trade(new_state, agent_id, tkn_buy, tkn_sell, buy_quantity, sell_quantity) + return new_state + + +def execute_external_market_trade( + state: GlobalState, + agent_id: str, + tkn_buy: str, + tkn_sell: str, + buy_quantity: float = 0, + sell_quantity: float = 0 +) -> None: + # do a trade at spot price on the external market + # this should maybe only work in USD, because we're probably talking about coinbase or something + agent = state.agents[agent_id] if buy_quantity: - sell_quantity = buy_quantity * new_state.price(tkn_buy) / new_state.price(tkn_sell) + sell_quantity = buy_quantity * state.price(tkn_buy) / state.price(tkn_sell) elif sell_quantity: - buy_quantity = sell_quantity * new_state.price(tkn_sell) / new_state.price(tkn_buy) + buy_quantity = sell_quantity * state.price(tkn_sell) / state.price(tkn_buy) else: # raise TypeError('Must include either buy_quantity or sell_quantity.') - return old_state + return if tkn_buy not in agent.holdings: agent.holdings[tkn_buy] = 0 @@ -447,8 +460,6 @@ def external_market_trade( agent.holdings[tkn_buy] += buy_quantity agent.holdings[tkn_sell] -= sell_quantity - return new_state - def migrate( old_state: GlobalState, diff --git a/hydradx/model/amm/trade_strategies.py b/hydradx/model/amm/trade_strategies.py index 875f88fd..e6d90365 100644 --- a/hydradx/model/amm/trade_strategies.py +++ b/hydradx/model/amm/trade_strategies.py @@ -1,16 +1,20 @@ -import math import copy -from .global_state import GlobalState, swap, add_liquidity, external_market_trade, withdraw_all_liquidity +import math +import random +from typing import Callable + +import numpy as np + +from . import omnipool_amm as oamm from .agents import Agent from .basilisk_amm import ConstantProductPoolState +from .global_state import GlobalState, withdraw_all_liquidity, \ + execute_external_market_trade from .omnipool_amm import OmnipoolState, execute_swap, execute_add_liquidity -from . import omnipool_amm as oamm from .stableswap_amm import StableSwapPoolState -from typing import Callable -import random -# from numbers import Number -import numpy as np + +# from numbers import Number class TradeStrategy: @@ -37,9 +41,9 @@ def combo_function(state, agent_id) -> None: def random_swaps( - pool_id: str, - amount: dict[str: float] or float, - randomize_amount: bool = True, + pool_id: str, + amount: dict[str: float] or float, + randomize_amount: bool = True, ) -> TradeStrategy: """ amount should be a dict in the form of: @@ -49,16 +53,16 @@ def random_swaps( """ def strategy(state: GlobalState, agent_id: str) -> None: - buy_asset = random.choice(list(amount.keys())) sell_asset = random.choice(list(amount.keys())) sell_quantity = ( - amount[sell_asset] / (state.price(sell_asset) or 1) * (random.random() if randomize_amount else 1) - ) or 1 + amount[sell_asset] / (state.price(sell_asset) or 1) * ( + random.random() if randomize_amount else 1) + ) or 1 if buy_asset != sell_asset: - execute_swap( - state=state.pools['omnipool'], + state.pools[pool_id].execute_swap( + state=state.pools[pool_id], agent=state.agents[agent_id], tkn_buy=buy_asset, tkn_sell=sell_asset, @@ -69,13 +73,11 @@ def strategy(state: GlobalState, agent_id: str) -> None: def steady_swaps( - pool_id: str, - usd_amount: float, - asset_list: list = () + pool_id: str, + usd_amount: float, + asset_list: list = () ) -> TradeStrategy: - def strategy(state: GlobalState, agent_id: str) -> None: - strategy.buy_index = getattr(strategy, 'buy_index', -1) strategy.buy_index += 1 agent = state.agents[agent_id] @@ -99,14 +101,12 @@ def strategy(state: GlobalState, agent_id: str) -> None: def constant_swaps( - pool_id: str, - sell_quantity: float, - sell_asset: str, - buy_asset: str + pool_id: str, + sell_quantity: float, + sell_asset: str, + buy_asset: str ) -> TradeStrategy: - def strategy(state: GlobalState, agent_id: str) -> None: - state.execute_swap( pool_id=pool_id, agent_id=agent_id, @@ -119,10 +119,9 @@ def strategy(state: GlobalState, agent_id: str) -> None: def back_and_forth( - pool_id: str, - percentage: float # percentage of TVL to trade each block + pool_id: str, + percentage: float # percentage of TVL to trade each block ) -> TradeStrategy: - def strategy(state: GlobalState, agent_id: str) -> None: omnipool: OmnipoolState = state.pools[pool_id] agent: Agent = state.agents[agent_id] @@ -138,19 +137,19 @@ def strategy(state: GlobalState, agent_id: str) -> None: def invest_all(pool_id: str, assets: list or str = None) -> TradeStrategy: - if assets and not isinstance(assets, list): assets = [assets] def strategy(state: GlobalState, agent_id: str) -> None: agent: Agent = state.agents[agent_id] + agent_assets = [k for k in agent.holdings] - for asset in assets or agent.holdings.keys(): + for asset in assets or agent_assets: if asset in state.pools[pool_id].asset_list: - state = execute_add_liquidity( - state=state.pools['omnipool'], + state.pools[pool_id].execute_add_liquidity( + state=state.pools[pool_id], agent=agent, quantity=state.agents[agent_id].holdings[asset], tkn_add=asset @@ -162,7 +161,6 @@ def strategy(state: GlobalState, agent_id: str) -> None: def withdraw_all(when: int) -> TradeStrategy: - def strategy(state: GlobalState, agent_id: str) -> None: if state.time_step == when: return withdraw_all_liquidity(state, agent_id) @@ -173,7 +171,6 @@ def strategy(state: GlobalState, agent_id: str) -> None: def sell_all(pool_id: str, sell_asset: str, buy_asset: str): - def strategy(state: GlobalState, agent_id: str) -> None: agent = state.agents[agent_id] if agent.holdings[sell_asset] != 0: @@ -182,7 +179,7 @@ def strategy(state: GlobalState, agent_id: str) -> None: return TradeStrategy(strategy, name=f'sell all {sell_asset} for {buy_asset}') -def invest_and_withdraw(frequency: float = 0.001, pool_id: str = 'omnipool', sell_lrna: bool = False) -> TradeStrategy: +def invest_and_withdraw(frequency: float = 0.001, pool_id: str = 'Omnipool', sell_lrna: bool = False) -> TradeStrategy: class Strategy: def __init__(self): self.last_move = 0 @@ -232,11 +229,11 @@ def __call__(self, state: GlobalState, agent_id: str) -> None: # iterative arbitrage method def find_agent_delta_y( - target_price: float, - price_after_trade: Callable, - starting_bid: float = 1, - precision: float = 0.000000001, - max_iterations: int = 50 + target_price: float, + price_after_trade: Callable, + starting_bid: float = 1, + precision: float = 0.000000001, + max_iterations: int = 50 ): b = starting_bid previous_change = 1 @@ -258,11 +255,10 @@ def find_agent_delta_y( def constant_product_arbitrage(pool_id: str, minimum_profit: float = 0, direct_calc: bool = True) -> TradeStrategy: - def strategy(state: GlobalState, agent_id: str): pool: ConstantProductPoolState = state.pools[pool_id] - if not(isinstance(pool, ConstantProductPoolState)): + if not (isinstance(pool, ConstantProductPoolState)): raise TypeError(f'{pool_id} is not compatible with constant product arbitrage.') x = pool.asset_list[0] @@ -280,8 +276,8 @@ def strategy(state: GlobalState, agent_id: str): agent_delta_x *= 1 - pool.trade_fee.compute(y, abs(agent_delta_y)) projected_profit = ( - agent_delta_y * state.price(y) - + agent_delta_x * state.price(x) + agent_delta_y * state.price(y) + + agent_delta_x * state.price(x) ) # in case we want to graph this later @@ -291,12 +287,12 @@ def strategy(state: GlobalState, agent_id: str): if projected_profit <= minimum_profit: # don't do it # agent.trade_rejected += 1 - return state + return # buy just enough of non-USD asset if agent_delta_y > 0 and x != 'USD' or agent_delta_y < 0 and y != 'USD': - state = external_market_trade( - old_state=state, + execute_external_market_trade( + state=state, agent_id=agent_id, tkn_buy=x if agent_delta_y > 0 else 'USD', tkn_sell=y if agent_delta_y < 0 else 'USD', @@ -305,18 +301,18 @@ def strategy(state: GlobalState, agent_id: str): ) # swap - new_state = state.execute_swap(pool_id, agent_id, tkn_sell=x, tkn_buy=y, buy_quantity=agent_delta_y) + state.pools[pool_id].execute_swap(state.pools[pool_id], state.agents[agent_id], tkn_sell=x, tkn_buy=y, + buy_quantity=agent_delta_y) # immediately cash out everything for USD - new_agent = state.agents[agent_id] - for tkn, quantity in new_agent.holdings.items(): - if new_agent.holdings[tkn] > 0 and tkn != 'USD': - new_state = external_market_trade( + tkns = [tkn for tkn in state.agents[agent_id].holdings if tkn != 'USD'] + for tkn in tkns: + quantity = state.agents[agent_id].holdings[tkn] + if state.agents[agent_id].holdings[tkn] > 0: + execute_external_market_trade( state, agent_id, tkn_buy='USD', tkn_sell=tkn, sell_quantity=quantity ) - return new_state - def direct_calculation(state: GlobalState, tkn_sell: str, tkn_buy: str): pool = state.pools[pool_id] @@ -324,7 +320,7 @@ def direct_calculation(state: GlobalState, tkn_sell: str, tkn_buy: str): x = pool.liquidity[tkn_sell] y = pool.liquidity[tkn_buy] f = pool.trade_fee.compute('', 0) - if p < x/y * (1 - f): + if p < x / y * (1 - f): # agent can profit by selling y to AMM b = 2 * y - (f / p) * x * (1 - f) c = y ** 2 - x * y / p * (1 - f) @@ -334,7 +330,7 @@ def direct_calculation(state: GlobalState, tkn_sell: str, tkn_buy: str): else: dY = (-b - t) / 2 return -dY - elif p > x/y * (1 + f): + elif p > x / y * (1 + f): # agent can profit by selling x to AMM b = 2 * y + (f / p) * x / (1 - f) c = y ** 2 - x * y / p / (1 - f) @@ -482,10 +478,10 @@ def strategy(state: GlobalState, agent_id: str) -> None: q = omnipool.lrna for j in range(arb_precision): - dr = [0]*len(dq) + dr = [0] * len(dq) for i in range(len(prices)): asset = asset_list[i] - delta_Qi = dq[i] * (j+1) / arb_precision + delta_Qi = dq[i] * (j + 1) / arb_precision if delta_Qi > 0: dr[i] = r[asset] * delta_Qi / (q[asset] + delta_Qi) * (1 - asset_fees[i]) else: @@ -496,10 +492,10 @@ def strategy(state: GlobalState, agent_id: str) -> None: for i in range(len(asset_list)): if dq[i] > 0: oamm.execute_swap(state=omnipool, agent=agent, tkn_sell="LRNA", tkn_buy=asset_list[i], - sell_quantity=dq[i] * j/arb_precision, modify_imbalance=False) + sell_quantity=dq[i] * j / arb_precision, modify_imbalance=False) else: oamm.execute_swap(state=omnipool, agent=agent, tkn_sell=asset_list[i], tkn_buy="LRNA", - buy_quantity=-dq[i] * j/arb_precision, modify_imbalance=False) + buy_quantity=-dq[i] * j / arb_precision, modify_imbalance=False) break elif j == arb_precision - 1: for i in range(len(asset_list)): @@ -515,7 +511,6 @@ def strategy(state: GlobalState, agent_id: str) -> None: def stableswap_arbitrage(pool_id: str, minimum_profit: float = 1, precision: float = 0.00001): - def strategy(state: GlobalState, agent_id: str) -> None: stable_pool: StableSwapPoolState = state.pools[pool_id] @@ -540,13 +535,14 @@ def price_after_trade(buy_amount: float = 0, sell_amount: float = 0): 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) + 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) projected_profit = ( - delta_y * state.price(buy_asset) - + delta_x * state.price(sell_asset) + delta_y * state.price(buy_asset) + + delta_x * state.price(sell_asset) ) if projected_profit > minimum_profit: @@ -556,7 +552,6 @@ def price_after_trade(buy_amount: float = 0, sell_amount: float = 0): def toxic_asset_attack(pool_id: str, asset_name: str, trade_size: float, start_timestep: int = 0) -> TradeStrategy: - def strategy(state: GlobalState, agent_id: str) -> None: if state.time_step < start_timestep: return @@ -571,13 +566,13 @@ def strategy(state: GlobalState, agent_id: str) -> None: if usd_price <= 0: return quantity = ( - (omnipool.lrna_total - omnipool.lrna[asset_name]) - * omnipool.weight_cap[asset_name] / (1 - omnipool.weight_cap[asset_name]) - - omnipool.lrna[asset_name] - ) / current_price - 0.001 # because rounding errors + (omnipool.lrna_total - omnipool.lrna[asset_name]) + * omnipool.weight_cap[asset_name] / (1 - omnipool.weight_cap[asset_name]) + - omnipool.lrna[asset_name] + ) / current_price - 0.001 # because rounding errors execute_add_liquidity( - state=state.pools['omnipool'], + state=state.pools[pool_id], agent=state.agents[agent_id], quantity=quantity, tkn_add=asset_name @@ -590,7 +585,7 @@ def strategy(state: GlobalState, agent_id: str) -> None: # pool is maxed return execute_swap( - state=state.pools['omnipool'], + state=state.pools[pool_id], agent=state.agents[agent_id], tkn_buy='USD', tkn_sell=asset_name, @@ -624,10 +619,11 @@ def price_manipulation_strategy(state: GlobalState, agent_id: str) -> None: ) # here we want to bring the prices back in line with the market delta_r = (math.sqrt(( - omnipool.lrna[asset2] * omnipool.lrna[asset1] * omnipool.liquidity[asset2] * omnipool.liquidity[asset1] - ) / (state.external_market[asset2] / state.external_market[asset1])) - ( - omnipool.lrna[asset1] * omnipool.liquidity[asset2] - )) / (omnipool.lrna[asset2] + omnipool.lrna[asset1]) + omnipool.lrna[asset2] * omnipool.lrna[asset1] * omnipool.liquidity[asset2] * + omnipool.liquidity[asset1] + ) / (state.external_market[asset2] / state.external_market[asset1])) - ( + omnipool.lrna[asset1] * omnipool.liquidity[asset2] + )) / (omnipool.lrna[asset2] + omnipool.lrna[asset1]) oamm.execute_swap( state=omnipool, agent=agent, @@ -655,10 +651,11 @@ def price_manipulation_strategy(state: GlobalState, agent_id: str) -> None: tkn_add=asset1 ) delta_r = (math.sqrt(( - omnipool.lrna[asset1] * omnipool.lrna[asset2] * omnipool.liquidity[asset1] * omnipool.liquidity[asset2] - ) / (state.external_market[asset1] / state.external_market[asset2])) - ( - omnipool.lrna[asset2] * omnipool.liquidity[asset1] - )) / (omnipool.lrna[asset1] + omnipool.lrna[asset2]) + omnipool.lrna[asset1] * omnipool.lrna[asset2] * omnipool.liquidity[asset1] * + omnipool.liquidity[asset2] + ) / (state.external_market[asset1] / state.external_market[asset2])) - ( + omnipool.lrna[asset2] * omnipool.liquidity[asset1] + )) / (omnipool.lrna[asset1] + omnipool.lrna[asset2]) oamm.execute_swap( state=omnipool, agent=agent, @@ -728,7 +725,7 @@ def execute(self, state: GlobalState, agent_id: str) -> None: self.attack_asset = (omnipool.asset_list[ (attack_asset_index + len(omnipool.asset_list) - 1) % len(omnipool.asset_list) - ]) + ]) self.asset_pairs.append({ 'attack asset': self.attack_asset, 'trade asset': self.trade_asset, @@ -821,11 +818,11 @@ def execute(self, state: GlobalState, agent_id: str) -> None: tkn_buy = self.trade_asset tkn_sell = self.attack_asset delta_r = (math.sqrt(( - omnipool.lrna[tkn_sell] * omnipool.lrna[tkn_buy] - * omnipool.liquidity[tkn_sell] * omnipool.liquidity[tkn_buy] - ) / (state.external_market[tkn_sell] / state.external_market[tkn_buy])) - ( - omnipool.lrna[tkn_buy] * omnipool.liquidity[tkn_sell] - )) / (omnipool.lrna[tkn_sell] + omnipool.lrna[tkn_buy]) + omnipool.lrna[tkn_sell] * omnipool.lrna[tkn_buy] + * omnipool.liquidity[tkn_sell] * omnipool.liquidity[tkn_buy] + ) / (state.external_market[tkn_sell] / state.external_market[tkn_buy])) - ( + omnipool.lrna[tkn_buy] * omnipool.liquidity[tkn_sell] + )) / (omnipool.lrna[tkn_sell] + omnipool.lrna[tkn_buy]) # find the max that we can (or want to) actually sell sell_quantity = min( @@ -903,10 +900,10 @@ def strategy(state: GlobalState, agent_id: str) -> None: if tkn_buy is None: buy = random.choice(options) slip_rate = ( - oamm.calculate_sell_from_buy(pool, buy, sell, max_volume_usd / state.external_market[buy]) - / (state.external_market[buy] / state.external_market[sell]) - / (max_volume_usd / state.external_market[buy]) - ) - 1 # find the price of buying from the pool vs. buying from the market + oamm.calculate_sell_from_buy(pool, buy, sell, max_volume_usd / state.external_market[buy]) + / (state.external_market[buy] / state.external_market[sell]) + / (max_volume_usd / state.external_market[buy]) + ) - 1 # find the price of buying from the pool vs. buying from the market trade_volume = max( max_volume_usd / state.external_market[sell] * max(min(1 - price_sensitivity * slip_rate, 1), 0) ** 10, 0 diff --git a/hydradx/model/run.py b/hydradx/model/run.py index ae9d79a8..e2a5cf4e 100644 --- a/hydradx/model/run.py +++ b/hydradx/model/run.py @@ -1,7 +1,7 @@ import time +from copy import deepcopy from .amm.global_state import GlobalState -from copy import deepcopy def run(initial_state: GlobalState, time_steps: int, silent: bool = False) -> list: @@ -22,11 +22,11 @@ def run(initial_state: GlobalState, time_steps: int, silent: bool = False) -> li # market evolutions new_global_state.evolve() - agent_ids = deepcopy(new_global_state.agents.keys()) + agent_ids = deepcopy(list(new_global_state.agents.keys())) for agent_id in agent_ids: agent = new_global_state.agents[agent_id] if agent.trade_strategy: - new_global_state = agent.trade_strategy.execute(new_global_state, agent.unique_id) + agent.trade_strategy.execute(new_global_state, agent.unique_id) events.append(new_global_state.archive()) diff --git a/hydradx/tests/strategies_omnipool.py b/hydradx/tests/strategies_omnipool.py index a9d4a45b..61c5df89 100644 --- a/hydradx/tests/strategies_omnipool.py +++ b/hydradx/tests/strategies_omnipool.py @@ -1,10 +1,11 @@ import random from hypothesis import strategies as st +from mpmath import mp, mpf import hydradx.model.amm.omnipool_amm as oamm import hydradx.model.amm.stableswap_amm as ssamm -from mpmath import mp, mpf + mp.dps = 50 asset_price_strategy = st.floats(min_value=0.0001, max_value=100000) @@ -81,7 +82,8 @@ def omnipool_reasonable_config( asset_fee=None, tvl_cap_usd=0, imbalance=None, - remove_liquidity_volatility_threshold: float = 0 + remove_liquidity_volatility_threshold: float = 0, + withdrawal_fee: bool = True, ): asset_dict: dict = asset_dict or draw(assets_reasonable_config(token_count)) diff --git a/hydradx/tests/test_amm.py b/hydradx/tests/test_amm.py index f7751e03..aafabcf7 100644 --- a/hydradx/tests/test_amm.py +++ b/hydradx/tests/test_amm.py @@ -1,17 +1,18 @@ -import random import copy +import random + import pytest from hypothesis import given, strategies as st, settings -from hydradx.tests.test_omnipool_amm import omnipool_config -from hydradx.tests.test_basilisk_amm import constant_product_pool_config -from hydradx.model.amm.basilisk_amm import ConstantProductPoolState -from hydradx.model.amm.global_state import GlobalState, fluctuate_prices, swap -from hydradx.model.amm.agents import Agent -from hydradx.model import run from hydradx.model import processing -from hydradx.model.amm.trade_strategies import steady_swaps, invest_all, constant_product_arbitrage, invest_and_withdraw +from hydradx.model import run +from hydradx.model.amm.agents import Agent from hydradx.model.amm.amm import AMM +from hydradx.model.amm.basilisk_amm import ConstantProductPoolState +from hydradx.model.amm.global_state import GlobalState, fluctuate_prices, swap +from hydradx.model.amm.trade_strategies import steady_swaps, invest_all, constant_product_arbitrage, invest_and_withdraw +from hydradx.tests.test_basilisk_amm import constant_product_pool_config +from hydradx.tests.test_omnipool_amm import omnipool_config asset_price_strategy = st.floats(min_value=0.01, max_value=1000) asset_number_strategy = st.integers(min_value=3, max_value=5) @@ -35,10 +36,10 @@ def assets_config(draw, token_count: int = 0) -> dict: @st.composite def agent_config( - draw, - holdings: dict = None, - asset_list: list = None, - trade_strategy: any = None + draw, + holdings: dict = None, + asset_list: list = None, + trade_strategy: any = None ): return Agent( holdings=holdings or { @@ -56,7 +57,8 @@ def global_state_config( pools=None, agents=None, evolve_function=None, - skip_omnipool=False + skip_omnipool=False, + omnipool_withdrawal_fee=True, ) -> GlobalState: # if asset_dict was not provided, generate one market_prices = external_market or draw(assets_config()) @@ -75,13 +77,13 @@ def global_state_config( x_quantity = draw(asset_quantity_strategy) pools.update({ f'{asset_list[x]}/{asset_list[y]}': - draw(constant_product_pool_config( - asset_dict={ - asset_list[x]: x_quantity, - asset_list[y]: x_quantity * market_prices[asset_list[x]] / market_prices[asset_list[y]] - }, - trade_fee=draw(fee_strategy) - )) + draw(constant_product_pool_config( + asset_dict={ + asset_list[x]: x_quantity, + asset_list[y]: x_quantity * market_prices[asset_list[x]] / market_prices[asset_list[y]] + }, + trade_fee=draw(fee_strategy) + )) }) if not skip_omnipool: @@ -99,7 +101,8 @@ def global_state_config( for tkn in asset_list }, lrna_fee=draw(fee_strategy), - asset_fee=draw(fee_strategy) + asset_fee=draw(fee_strategy), + withdrawal_fee=omnipool_withdrawal_fee )) }) else: @@ -182,7 +185,8 @@ def test_simulation(initial_state: GlobalState): }, trade_strategy=steady_swaps('HDX/USD', 100, asset_list=['HDX', 'USD']) ) - } + }, + omnipool_withdrawal_fee=False )) def test_LP(initial_state: GlobalState): initial_state.agents['LP'].holdings = { @@ -220,7 +224,8 @@ def test_LP(initial_state: GlobalState): }, external_market={'HDX': 0, 'BSX': 0}, # config function will fill these in with random values evolve_function=fluctuate_prices(volatility={'HDX': 1, 'BSX': 1}), - skip_omnipool=True + skip_omnipool=True, + omnipool_withdrawal_fee=False )) def test_arbitrage_pool_balance(initial_state): # there are actually two things we would like to test: @@ -231,6 +236,8 @@ def test_arbitrage_pool_balance(initial_state): events = run.run(initial_state, time_steps=50, silent=True) final_state = events[-1] final_pool_state = final_state.pools['HDX/BSX'] + test1 = final_pool_state.liquidity['HDX'] / final_pool_state.liquidity['BSX'] + test2 = final_state.price('BSX') / final_state.price('HDX') if (pytest.approx(final_pool_state.liquidity['HDX'] / final_pool_state.liquidity['BSX']) != final_state.price('BSX') / final_state.price('HDX')): raise AssertionError('Price ratio does not match ratio in the pool!') @@ -301,8 +308,12 @@ def buy_spot(state: GlobalState): / (1 - state.pools['X/Y'].trade_fee.compute('', 0)) ) - algebraic_state: GlobalState = algebraic_function.execute(initial_state.copy(), 'arbitrager') - recursive_state: GlobalState = recursive_function.execute(initial_state.copy(), 'arbitrager') + algebraic_state = initial_state.copy() + algebraic_function.execute(algebraic_state, 'arbitrager') + + recursive_state = initial_state.copy() + recursive_function.execute(recursive_state, 'arbitrager') + algebraic_result = (algebraic_state.pools['X/Y'].liquidity['X'] / algebraic_state.pools['X/Y'].liquidity['Y']) recursive_result = (recursive_state.pools['X/Y'].liquidity['X'] @@ -341,9 +352,9 @@ def test_buy_fee_derivation(initial_state: GlobalState): feeless_buy_amount = 1 next_feeless_state = [feeless_state.copy()] next_state = [initial_state.copy()] - for i in range(len(asset_path)-1): + for i in range(len(asset_path) - 1): tkn_buy = asset_path[i] - tkn_sell = asset_path[i+1] + tkn_sell = asset_path[i + 1] pool_id = f'{tkn_buy}/{tkn_sell}' if f'{tkn_buy}/{tkn_sell}' in initial_state.pools else f'{tkn_sell}/{tkn_buy}' pool_path.append(pool_id) next_feeless_state.append(swap( @@ -355,8 +366,8 @@ def test_buy_fee_derivation(initial_state: GlobalState): buy_quantity=feeless_buy_amount )) feeless_buy_amount = ( - next_feeless_state[-2].agents['trader'].holdings[tkn_sell] - - next_feeless_state[-1].agents['trader'].holdings[tkn_sell] + next_feeless_state[-2].agents['trader'].holdings[tkn_sell] + - next_feeless_state[-1].agents['trader'].holdings[tkn_sell] ) next_state.append(swap( next_state[-1], @@ -367,8 +378,8 @@ def test_buy_fee_derivation(initial_state: GlobalState): buy_quantity=buy_amount )) buy_amount = ( - next_state[-2].agents['trader'].holdings[tkn_sell] - - next_state[-1].agents['trader'].holdings[tkn_sell] + next_state[-2].agents['trader'].holdings[tkn_sell] + - next_state[-1].agents['trader'].holdings[tkn_sell] ) if buy_amount == 0: return @@ -378,7 +389,7 @@ def test_buy_fee_derivation(initial_state: GlobalState): expected_fee_amount /= (1 - initial_state.pools[pool_id].trade_fee.compute('', 0)) expected_fee_amount -= 1 if fee_amount != pytest.approx(expected_fee_amount): - raise ValueError(f'off by {abs(1-expected_fee_amount/fee_amount)}') + raise ValueError(f'off by {abs(1 - expected_fee_amount / fee_amount)}') # if fee_amount < expected_fee_amount: # raise ValueError(f'fee is lower than expected') @@ -402,9 +413,9 @@ def test_sell_fee_derivation(initial_state: GlobalState): feeless_sell_amount = 1 next_feeless_state = [feeless_state.copy()] next_state = [initial_state.copy()] - for i in range(len(asset_path)-1): + for i in range(len(asset_path) - 1): tkn_buy = asset_path[i] - tkn_sell = asset_path[i+1] + tkn_sell = asset_path[i + 1] pool_id = f'{tkn_buy}/{tkn_sell}' if f'{tkn_buy}/{tkn_sell}' in initial_state.pools else f'{tkn_sell}/{tkn_buy}' pool_path.append(pool_id) next_feeless_state.append(swap( @@ -416,8 +427,8 @@ def test_sell_fee_derivation(initial_state: GlobalState): sell_quantity=feeless_sell_amount )) feeless_sell_amount = ( - next_feeless_state[-1].agents['trader'].holdings[tkn_buy] - - next_feeless_state[-2].agents['trader'].holdings[tkn_buy] + next_feeless_state[-1].agents['trader'].holdings[tkn_buy] + - next_feeless_state[-2].agents['trader'].holdings[tkn_buy] ) next_state.append(swap( next_state[-1], @@ -428,8 +439,8 @@ def test_sell_fee_derivation(initial_state: GlobalState): sell_quantity=sell_amount )) sell_amount = ( - next_state[-1].agents['trader'].holdings[tkn_buy] - - next_state[-2].agents['trader'].holdings[tkn_buy] + next_state[-1].agents['trader'].holdings[tkn_buy] + - next_state[-2].agents['trader'].holdings[tkn_buy] ) if sell_amount == 0: return @@ -440,7 +451,7 @@ def test_sell_fee_derivation(initial_state: GlobalState): expected_fee_amount *= (1 - initial_state.pools[pool_id].trade_fee.compute('', 0)) expected_fee_amount = 1 - expected_fee_amount if fee_amount != pytest.approx(expected_fee_amount): - raise ValueError(f'off by {abs(1-expected_fee_amount/fee_amount)}') + raise ValueError(f'off by {abs(1 - expected_fee_amount / fee_amount)}') # if fee_amount > expected_fee_amount: # raise ValueError('fee is higher than expected') @@ -535,8 +546,8 @@ def test_omnipool_arbitrage(): {tkn: arb_holdings[tkn][i] for tkn in arb_holdings} for i in range(len(arb_holdings['LRNA'])) ] profit = ( - GlobalState.value_assets(arb_holdings[-1], initial_state.external_market) - - GlobalState.value_assets(arb_holdings[0], initial_state.external_market) + GlobalState.value_assets(arb_holdings[-1], initial_state.external_market) + - GlobalState.value_assets(arb_holdings[0], initial_state.external_market) ) if profit < 0: raise ValueError(f'Arbitrageur lost {profit} USD') diff --git a/hydradx/tests/test_omnipool_agents.py b/hydradx/tests/test_omnipool_agents.py index b7fcf8e3..29c15afc 100644 --- a/hydradx/tests/test_omnipool_agents.py +++ b/hydradx/tests/test_omnipool_agents.py @@ -1,5 +1,4 @@ import copy -import math import pytest from hypothesis import given, strategies as st # , settings @@ -139,15 +138,17 @@ def test_omnipool_LP(omnipool: oamm.OmnipoolState): holdings = {asset: 10000 for asset in omnipool.asset_list} initial_agent = Agent(holdings=holdings, trade_strategy=omnipool_arbitrage) initial_state = GlobalState(pools={'omnipool': omnipool}, agents={'agent': initial_agent}) + new_state = initial_state.copy() - new_state = invest_all('omnipool').execute(initial_state, 'agent') + invest_all('omnipool').execute(new_state, 'agent') for tkn in omnipool.asset_list: if new_state.agents['agent'].holdings[tkn] != 0: raise AssertionError(f'Failed to LP {tkn}') if new_state.agents['agent'].holdings[('omnipool', tkn)] == 0: raise AssertionError(f'Did not receive shares for {tkn}') - hdx_state = invest_all('omnipool', 'HDX').execute(initial_state.copy(), 'agent') + hdx_state = initial_state.copy() + invest_all('omnipool', 'HDX').execute(hdx_state, 'agent') if hdx_state.agents['agent'].holdings['HDX'] != 0: raise AssertionError('HDX not reinvested.') diff --git a/hydradx/tests/test_omnipool_amm.py b/hydradx/tests/test_omnipool_amm.py index d1932106..ad7b9bd5 100644 --- a/hydradx/tests/test_omnipool_amm.py +++ b/hydradx/tests/test_omnipool_amm.py @@ -1448,46 +1448,49 @@ def test_lowering_price(lp_multiplier, price_movement, oracle_mult): market_prices = {tkn: oamm.usd_price(omnipool, tkn) for tkn in omnipool.asset_list} holdings = {tkn: 1000000000 for tkn in omnipool.asset_list} - agent = Agent(holdings=holdings) + initial_agent = Agent(holdings=holdings) - swap_state, swap_agent = oamm.execute_swap( - state=omnipool.copy(), - agent=agent.copy(), + state = omnipool.copy() + agent = initial_agent.copy() + + oamm.execute_swap( + state=state, + agent=agent, tkn_sell='DOT', tkn_buy='DAI', sell_quantity=trade_size ) - add_state, add_agent = oamm.execute_add_liquidity( - state=swap_state.copy(), - agent=swap_agent.copy(), + oamm.execute_add_liquidity( + state=state, + agent=agent, tkn_add='DOT', - quantity=swap_state.liquidity['DOT'] * lp_multiplier + quantity=state.liquidity['DOT'] * lp_multiplier ) global_state = GlobalState( - pools={'omnipool': add_state}, - agents={'attacker': add_agent}, + pools={'omnipool': state}, + agents={'attacker': agent}, external_market=market_prices ) - arb_state = omnipool_arbitrage('omnipool', 20).execute( - state=global_state.copy(), + omnipool_arbitrage('omnipool', 20).execute( + state=global_state, agent_id='attacker' ) - arbed_pool = arb_state.pools['omnipool'] - arbed_agent = arb_state.agents['attacker'] + arbed_pool = global_state.pools['omnipool'] + arbed_agent = global_state.agents['attacker'] - remove_state, remove_agent = oamm.execute_remove_liquidity( - state=arbed_pool.copy(), - agent=arbed_agent.copy(), + oamm.execute_remove_liquidity( + state=arbed_pool, + agent=arbed_agent, tkn_remove='DOT', quantity=arbed_agent.holdings[('omnipool', 'DOT')] ) - initial_value = oamm.cash_out_omnipool(omnipool, agent, market_prices) - final_value = oamm.cash_out_omnipool(remove_state, remove_agent, market_prices) + initial_value = oamm.cash_out_omnipool(omnipool, initial_agent, market_prices) + final_value = oamm.cash_out_omnipool(arbed_pool, arbed_agent, market_prices) profit = final_value - initial_value if profit > 0: raise @@ -1527,24 +1530,27 @@ def test_add_and_remove_liquidity(): market_prices = {tkn: oamm.usd_price(omnipool, tkn) for tkn in omnipool.asset_list} holdings = {tkn: 1000000000 for tkn in omnipool.asset_list} - agent = Agent(holdings=holdings) + initial_agent = Agent(holdings=holdings) - add_state, add_agent = oamm.execute_add_liquidity( - state=omnipool.copy(), - agent=agent.copy(), + state = omnipool.copy() + agent = initial_agent.copy() + + oamm.execute_add_liquidity( + state=state, + agent=agent, tkn_add='DOT', - quantity=omnipool.liquidity['DOT'] * lp_multiplier + quantity=state.liquidity['DOT'] * lp_multiplier ) - remove_state, remove_agent = oamm.execute_remove_liquidity( - state=add_state.copy(), - agent=add_agent.copy(), + oamm.execute_remove_liquidity( + state=state, + agent=agent, tkn_remove='DOT', - quantity=add_agent.holdings[('omnipool', 'DOT')] + quantity=agent.holdings[('omnipool', 'DOT')] ) - initial_value = oamm.cash_out_omnipool(omnipool, agent, market_prices) - final_value = oamm.cash_out_omnipool(remove_state, remove_agent, market_prices) + initial_value = oamm.cash_out_omnipool(omnipool, initial_agent, market_prices) + final_value = oamm.cash_out_omnipool(state, agent, market_prices) profit = final_value - initial_value if profit > 0: raise @@ -1552,14 +1558,14 @@ def test_add_and_remove_liquidity(): # @settings(max_examples=1) @given( - st.floats(min_value=0, max_value=0.10, exclude_min=True), + st.floats(min_value=0, max_value=0.05, exclude_min=True), st.floats(min_value=0, max_value=0.01, exclude_min=True), # st.floats(min_value=0.90, max_value=1.1) ) def test_add_liquidity_exploit(lp_multiplier, trade_mult): oracle_mult = 1.0 - # lp_multiplier = 0.5 - # trade_mult = 0.5 + # lp_multiplier = 0.00 + # trade_mult = 0.01 tokens = { 'HDX': {'liquidity': 44000000, 'LRNA': 275143}, @@ -1594,46 +1600,49 @@ def test_add_liquidity_exploit(lp_multiplier, trade_mult): market_prices = {tkn: oamm.usd_price(omnipool, tkn) for tkn in omnipool.asset_list} holdings = {tkn: 1000000000 for tkn in omnipool.asset_list} - agent = Agent(holdings=holdings) + initial_agent = Agent(holdings=holdings) - swap_state, swap_agent = oamm.execute_swap( - state=omnipool.copy(), - agent=agent.copy(), + state = omnipool.copy() + agent = initial_agent.copy() + + oamm.execute_swap( + state=state, + agent=agent, tkn_sell='DAI', tkn_buy='DOT', buy_quantity=trade_size ) - add_state, add_agent = oamm.execute_add_liquidity( - state=swap_state.copy(), - agent=swap_agent.copy(), + oamm.execute_add_liquidity( + state=state, + agent=agent, tkn_add='DOT', - quantity=swap_state.liquidity['DOT'] * lp_multiplier + quantity=state.liquidity['DOT'] * lp_multiplier ) global_state = GlobalState( - pools={'omnipool': add_state}, - agents={'attacker': add_agent}, + pools={'omnipool': state}, + agents={'attacker': agent}, external_market=market_prices ) - arb_state = omnipool_arbitrage('omnipool', 20).execute( - state=global_state.copy(), + omnipool_arbitrage('omnipool', 20).execute( + state=global_state, agent_id='attacker' ) - arbed_pool = arb_state.pools['omnipool'] - arbed_agent = arb_state.agents['attacker'] + arbed_pool = global_state.pools['omnipool'] + arbed_agent = global_state.agents['attacker'] - remove_state, remove_agent = oamm.execute_remove_liquidity( - state=arbed_pool.copy(), - agent=arbed_agent.copy(), + oamm.execute_remove_liquidity( + state=arbed_pool, + agent=arbed_agent, tkn_remove='DOT', quantity=arbed_agent.holdings[('omnipool', 'DOT')] ) - initial_value = oamm.cash_out_omnipool(omnipool, agent, market_prices) - final_value = oamm.cash_out_omnipool(remove_state, remove_agent, market_prices) + initial_value = oamm.cash_out_omnipool(omnipool, initial_agent, market_prices) + final_value = oamm.cash_out_omnipool(arbed_pool, arbed_agent, market_prices) profit = final_value - initial_value if profit > 0: raise @@ -1682,46 +1691,49 @@ def test_add_liquidity_exploit_sell(lp_multiplier, trade_mult): market_prices = {tkn: oamm.usd_price(omnipool, tkn) for tkn in omnipool.asset_list} holdings = {tkn: 1000000000 for tkn in omnipool.asset_list} - agent = Agent(holdings=holdings) + initial_agent = Agent(holdings=holdings) - swap_state, swap_agent = oamm.execute_swap( - state=omnipool.copy(), - agent=agent.copy(), + state = omnipool.copy() + agent = initial_agent.copy() + + oamm.execute_swap( + state=state, + agent=agent, tkn_sell='DOT', tkn_buy='DAI', sell_quantity=trade_size ) - add_state, add_agent = oamm.execute_add_liquidity( - state=swap_state.copy(), - agent=swap_agent.copy(), + oamm.execute_add_liquidity( + state=state, + agent=agent, tkn_add='DOT', - quantity=swap_state.liquidity['DOT'] * lp_multiplier + quantity=state.liquidity['DOT'] * lp_multiplier ) global_state = GlobalState( - pools={'omnipool': add_state}, - agents={'attacker': add_agent}, + pools={'omnipool': state}, + agents={'attacker': agent}, external_market=market_prices ) - arb_state = omnipool_arbitrage('omnipool', 20).execute( - state=global_state.copy(), + omnipool_arbitrage('omnipool', 20).execute( + state=global_state, agent_id='attacker' ) - arbed_pool = arb_state.pools['omnipool'] - arbed_agent = arb_state.agents['attacker'] + arbed_pool = global_state.pools['omnipool'] + arbed_agent = global_state.agents['attacker'] - remove_state, remove_agent = oamm.execute_remove_liquidity( - state=arbed_pool.copy(), - agent=arbed_agent.copy(), + oamm.execute_remove_liquidity( + state=arbed_pool, + agent=arbed_agent, tkn_remove='DOT', quantity=arbed_agent.holdings[('omnipool', 'DOT')] ) - initial_value = oamm.cash_out_omnipool(omnipool, agent, market_prices) - final_value = oamm.cash_out_omnipool(remove_state, remove_agent, market_prices) + initial_value = oamm.cash_out_omnipool(omnipool, initial_agent, market_prices) + final_value = oamm.cash_out_omnipool(arbed_pool, arbed_agent, market_prices) profit = final_value - initial_value if profit > 0: raise @@ -1730,7 +1742,7 @@ def test_add_liquidity_exploit_sell(lp_multiplier, trade_mult): def test_withdraw_exploit(): oracle_mult = 1.0 lp_multiplier = 0.1 - trade_mult = 0.01 + trade_mult = 0.02 tokens = { 'HDX': {'liquidity': 44000000, 'LRNA': 275143}, @@ -1765,61 +1777,64 @@ def test_withdraw_exploit(): market_prices = {tkn: oamm.usd_price(omnipool, tkn) for tkn in omnipool.asset_list} holdings = {tkn: 1000000000 for tkn in omnipool.asset_list} - agent = Agent(holdings=holdings) + initial_agent = Agent(holdings=holdings) - add_state, add_agent = oamm.execute_add_liquidity( - state=omnipool.copy(), - agent=agent.copy(), + state = omnipool.copy() + agent = initial_agent.copy() + + oamm.execute_add_liquidity( + state=state, + agent=agent, tkn_add='DOT', - quantity=omnipool.liquidity['DOT'] * lp_multiplier + quantity=state.liquidity['DOT'] * lp_multiplier ) - swap_state, swap_agent = oamm.execute_swap( - state=add_state.copy(), - agent=add_agent.copy(), + oamm.execute_swap( + state=state, + agent=agent, tkn_sell='DAI', tkn_buy='DOT', buy_quantity=trade_size ) - remove_state, remove_agent = oamm.execute_remove_liquidity( - state=swap_state.copy(), - agent=swap_agent.copy(), + oamm.execute_remove_liquidity( + state=state, + agent=agent, tkn_remove='DOT', - quantity=swap_agent.holdings[('omnipool', 'DOT')] + quantity=agent.holdings[('omnipool', 'DOT')] ) global_state = GlobalState( - pools={'omnipool': remove_state}, - agents={'attacker': remove_agent}, + pools={'omnipool': state}, + agents={'attacker': agent}, external_market=market_prices ) - arb_state = omnipool_arbitrage('omnipool', 20).execute( - state=global_state.copy(), + omnipool_arbitrage('omnipool', 20).execute( + state=global_state, agent_id='attacker' ) - arbed_pool = arb_state.pools['omnipool'] - arbed_agent = arb_state.agents['attacker'] + arbed_pool = global_state.pools['omnipool'] + arbed_agent = global_state.agents['attacker'] - initial_value = oamm.cash_out_omnipool(omnipool, agent, market_prices) + initial_value = oamm.cash_out_omnipool(omnipool, initial_agent, market_prices) final_value = oamm.cash_out_omnipool(arbed_pool, arbed_agent, market_prices) profit = final_value - initial_value if profit > 0: raise -@settings(max_examples=1) +# @settings(max_examples=1) @given( st.floats(min_value=0, max_value=0.05, exclude_min=True), st.floats(min_value=0, max_value=0.1, exclude_min=True), st.floats(min_value=0.50, max_value=1.5) ) def test_swap_exploit(lp_multiplier, trade_mult, oracle_mult): - lp_multiplier = 0.2 - trade_mult = 0.01 - oracle_mult = 0.99 + # lp_multiplier = 0.2 + # trade_mult = 0.01 + # oracle_mult = 0.99 tokens = { 'HDX': {'liquidity': 44000000, 'LRNA': 275143}, @@ -1855,40 +1870,46 @@ def test_swap_exploit(lp_multiplier, trade_mult, oracle_mult): market_prices = {tkn: oamm.usd_price(omnipool, tkn) for tkn in omnipool.asset_list} holdings = {tkn: 1000000000 for tkn in omnipool.asset_list} - agent = Agent(holdings=holdings) + initial_agent = Agent(holdings=holdings) - add_state, add_agent = oamm.execute_add_liquidity( - state=omnipool.copy(), - agent=agent.copy(), + state = omnipool.copy() + agent = initial_agent.copy() + + oamm.execute_add_liquidity( + state=state, + agent=agent, tkn_add='DOT', quantity=omnipool.liquidity['DOT'] * lp_multiplier ) - swap_state, swap_agent = oamm.execute_swap( - state=add_state.copy(), - agent=add_agent.copy(), + oamm.execute_swap( + state=state, + agent=agent, tkn_sell='DOT', tkn_buy='DAI', sell_quantity=trade_size ) - remove_state, remove_agent = oamm.execute_remove_liquidity( - state=swap_state.copy(), - agent=swap_agent.copy(), + oamm.execute_remove_liquidity( + state=state, + agent=agent, tkn_remove='DOT', - quantity=swap_agent.holdings[('omnipool', 'DOT')] + quantity=agent.holdings[('omnipool', 'DOT')] ) - swap_alone_state, swap_alone_agent = oamm.execute_swap( - state=omnipool.copy(), - agent=agent.copy(), + swap_alone_state = omnipool.copy() + swap_alone_agent = initial_agent.copy() + + oamm.execute_swap( + state=swap_alone_state, + agent=swap_alone_agent, tkn_sell='DOT', tkn_buy='DAI', sell_quantity=trade_size ) swap_alone_dai = oamm.cash_out_omnipool(swap_alone_state, swap_alone_agent, market_prices) - manipulated_dai = oamm.cash_out_omnipool(remove_state, remove_agent, market_prices) + manipulated_dai = oamm.cash_out_omnipool(state, agent, market_prices) profit = manipulated_dai - swap_alone_dai if profit > 0: raise @@ -1937,38 +1958,42 @@ def test_withdraw_manipulation( # trade to manipulate the price signed_price_move = price_move if price_move_is_up else -price_move first_trade = initial_state.liquidity[lp_token] * (1 - 1 / math.sqrt(1 + signed_price_move)) - trade_state, trade_agent = oamm.execute_swap( - state=initial_state.copy(), - agent=initial_agent.copy(), + + state = initial_state.copy() + agent = initial_agent.copy() + + oamm.execute_swap( + state=state, + agent=agent, tkn_sell=trade_token, tkn_buy=lp_token, buy_quantity=first_trade ) - withdraw_state, withdraw_agent = oamm.execute_remove_liquidity( - state=trade_state.copy(), - agent=trade_agent.copy(), - quantity=trade_agent.holdings[('omnipool', lp_token)], + oamm.execute_remove_liquidity( + state=state, + agent=agent, + quantity=agent.holdings[('omnipool', lp_token)], tkn_remove=lp_token ) - glob = omnipool_arbitrage(pool_id='omnipool').execute( - state=GlobalState( - pools={ - 'omnipool': withdraw_state.copy() - }, - agents={ - 'agent': withdraw_agent.copy() - }, - external_market=market_prices - ), - agent_id='agent' + global_state = GlobalState( + pools={ + 'omnipool': state + }, + agents={ + 'agent': agent + }, + external_market=market_prices ) - final_state, final_agent = glob.pools['omnipool'], glob.agents['agent'] + omnipool_arbitrage(pool_id='omnipool').execute( + state=global_state, + agent_id='agent' + ) profit = ( - oamm.cash_out_omnipool(final_state, final_agent, market_prices) + oamm.cash_out_omnipool(state, agent, market_prices) - oamm.cash_out_omnipool(initial_state, initial_agent, market_prices) ) @@ -2013,9 +2038,13 @@ def test_add_manipulation( # trade to manipulate the price first_trade = initial_state.liquidity[asset1] * (1 - 1 / math.sqrt(1 + price_move)) - trade_state, trade_agent = oamm.execute_swap( - state=initial_state.copy(), - agent=initial_agent.copy(), + + state = initial_state.copy() + agent = initial_agent.copy() + + oamm.execute_swap( + state=state, + agent=agent, tkn_sell=asset2, tkn_buy=asset1, buy_quantity=first_trade @@ -2023,38 +2052,38 @@ def test_add_manipulation( # add liquidity lp_quantity = lp_percent * initial_agent.holdings[asset1] - add_state, add_agent = oamm.execute_add_liquidity( - state=trade_state.copy(), - agent=trade_agent.copy(), + oamm.execute_add_liquidity( + state=state, + agent=agent, tkn_add=asset1, - quantity=min(lp_quantity, trade_state.liquidity[asset1] * trade_state.max_lp_per_block) + quantity=min(lp_quantity, state.liquidity[asset1] * state.max_lp_per_block) ) lp_quantity = lp_percent * initial_agent.holdings[asset2] - add_state, add_agent = oamm.execute_add_liquidity( - state=add_state, - agent=add_agent, + oamm.execute_add_liquidity( + state=state, + agent=agent, tkn_add=asset2, quantity=lp_quantity ) - glob = omnipool_arbitrage(pool_id='omnipool').execute( - state=GlobalState( - pools={ - 'omnipool': add_state.copy() - }, - agents={ - 'agent': add_agent.copy() - }, - external_market=market_prices - ), - agent_id='agent' + global_state = GlobalState( + pools={ + 'omnipool': state + }, + agents={ + 'agent': agent + }, + external_market=market_prices ) - sell_state, sell_agent = glob.pools['omnipool'], glob.agents['agent'] + omnipool_arbitrage(pool_id='omnipool').execute( + state=global_state, + agent_id='agent' + ) profit = ( - oamm.cash_out_omnipool(sell_state, sell_agent, market_prices) + oamm.cash_out_omnipool(state, agent, market_prices) - oamm.cash_out_omnipool(initial_state, initial_agent, market_prices) ) @@ -2089,53 +2118,68 @@ def test_trade_manipulation( asset2 = options[asset_index % len(options)] market_prices = {tkn: oamm.usd_price(initial_state, tkn) for tkn in initial_state.asset_list} - lp1_state, lp1_agent = oamm.execute_add_liquidity( - state=initial_state.copy(), - agent=initial_agent.copy(), - tkn_add=asset1, - quantity=min( - lp_percent * initial_state.liquidity[asset1], - initial_state.liquidity[asset1] * initial_state.max_lp_per_block - ) - ) + state = initial_state.copy() + agent = initial_agent.copy() - lp2_state, lp2_agent = oamm.execute_add_liquidity( - state=initial_state.copy(), - agent=initial_agent.copy(), - tkn_add=asset2, + oamm.execute_add_liquidity( + state=state, + agent=agent, + tkn_add=asset1, quantity=min( - lp_percent * initial_state.liquidity[asset2], - initial_state.liquidity[asset2] * initial_state.max_lp_per_block + lp_percent * state.liquidity[asset1], + state.liquidity[asset1] * state.max_lp_per_block ) ) - trade_state_1, trade_agent_1 = oamm.execute_remove_liquidity( + oamm.execute_remove_liquidity( *oamm.execute_swap( - state=lp1_state.copy(), - agent=lp1_agent.copy(), + state=state, + agent=agent, tkn_sell=asset1, tkn_buy=asset2, sell_quantity=sell_quantity ), tkn_remove=asset1, - quantity=lp1_agent.holdings[('omnipool', asset1)] + quantity=agent.holdings[('omnipool', asset1)] + ) + + trade_state_1 = state.copy() + trade_agent_1 = agent.copy() + + state = initial_state.copy() + agent = initial_agent.copy() + + oamm.execute_add_liquidity( + state=state, + agent=agent, + tkn_add=asset2, + quantity=min( + lp_percent * state.liquidity[asset2], + state.liquidity[asset2] * state.max_lp_per_block + ) ) - trade_state_2, trade_agent_2 = oamm.execute_remove_liquidity( + oamm.execute_remove_liquidity( *oamm.execute_swap( - state=lp2_state.copy(), - agent=lp2_agent.copy(), + state=state, + agent=agent, tkn_sell=asset1, tkn_buy=asset2, sell_quantity=sell_quantity ), tkn_remove=asset2, - quantity=lp2_agent.holdings[('omnipool', asset2)] + quantity=agent.holdings[('omnipool', asset2)] ) - trade_state_3, trade_agent_3 = oamm.execute_swap( - state=initial_state.copy(), - agent=initial_agent.copy(), + trade_state_2 = state.copy() + trade_agent_2 = agent.copy() + + trade_state_3 = initial_state.copy() + trade_agent_3 = initial_agent.copy() + + oamm.execute_swap( + state=trade_state_3, + agent=trade_agent_3, tkn_sell=asset1, tkn_buy=asset2, sell_quantity=sell_quantity diff --git a/hydradx/tests/test_stableswap.py b/hydradx/tests/test_stableswap.py index 9ac9f498..01e1e654 100644 --- a/hydradx/tests/test_stableswap.py +++ b/hydradx/tests/test_stableswap.py @@ -1,14 +1,17 @@ -import pytest import functools + +import pytest +from hypothesis import given, strategies as st +from mpmath import mp + +from hydradx.model import run from hydradx.model.amm import stableswap_amm as stableswap -from hydradx.model.amm.stableswap_amm import StableSwapPoolState 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 -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) @@ -49,7 +52,7 @@ def test_swap_invariant(initial_pool: StableSwapPoolState): new_state = initial_state.copy() d = new_state.pools['stableswap'].calculate_d() for n in range(10): - new_state = new_state.agents['trader'].trade_strategy.execute(new_state, agent_id='trader') + new_state.agents['trader'].trade_strategy.execute(new_state, agent_id='trader') new_d = new_state.pools['stableswap'].calculate_d() if new_d != pytest.approx(d): raise AssertionError('Invariant has varied.') @@ -66,7 +69,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.') @@ -118,11 +121,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.") @@ -152,17 +155,17 @@ def test_arbitrage(stable_pool): events = run.run(initial_state, time_steps=10, silent=True) # print(events[0].pools['R1/R2'].spot_price, events[-1].pools['R1/R2'].spot_price) if ( - events[0].pools['R1/R2'].spot_price - != pytest.approx(events[-1].pools['R1/R2'].spot_price) + events[0].pools['R1/R2'].spot_price + != pytest.approx(events[-1].pools['R1/R2'].spot_price) ): raise AssertionError(f"Arbitrageur didn't keep the price stable." f"({events[0].pools['R1/R2'].spot_price})" f"{events[-1].pools['R1/R2'].spot_price}") if ( - events[0].agents['Arbitrageur'].holdings['R1'] - + events[0].agents['Arbitrageur'].holdings['R2'] - > events[-1].agents['Arbitrageur'].holdings['R1'] - + events[-1].agents['Arbitrageur'].holdings['R2'] + events[0].agents['Arbitrageur'].holdings['R1'] + + events[0].agents['Arbitrageur'].holdings['R2'] + > events[-1].agents['Arbitrageur'].holdings['R1'] + + events[-1].agents['Arbitrageur'].holdings['R2'] ): raise AssertionError("Arbitrageur didn't make money.") @@ -178,10 +181,10 @@ def test_add_remove_liquidity(initial_pool: StableSwapPoolState): initial_pool, old_agent=lp, quantity=10000, tkn_add=lp_tkn ) if not stable_swap_equation( - add_liquidity_state.calculate_d(), - add_liquidity_state.amplification, - add_liquidity_state.n_coins, - add_liquidity_state.liquidity.values() + add_liquidity_state.calculate_d(), + add_liquidity_state.amplification, + add_liquidity_state.n_coins, + add_liquidity_state.liquidity.values() ): raise AssertionError('Stableswap equation does not hold after add liquidity operation.') @@ -192,10 +195,10 @@ def test_add_remove_liquidity(initial_pool: StableSwapPoolState): tkn_remove=lp_tkn ) if not stable_swap_equation( - remove_liquidity_state.calculate_d(), - remove_liquidity_state.amplification, - remove_liquidity_state.n_coins, - remove_liquidity_state.liquidity.values() + remove_liquidity_state.calculate_d(), + remove_liquidity_state.amplification, + remove_liquidity_state.n_coins, + remove_liquidity_state.liquidity.values() ): raise AssertionError('Stableswap equation does not hold after remove liquidity operation.') if remove_liquidity_agent.holdings[lp_tkn] != pytest.approx(lp.holdings[lp_tkn]): @@ -241,4 +244,3 @@ def test_curve_style_withdraw_fees(): if effective_fee_withdraw <= effective_fee_swap: raise AssertionError('Withdraw fee is not higher than swap fee.') -