From 839da048742144ec0c56633c8351075a08b9d07e Mon Sep 17 00:00:00 2001 From: poliwop Date: Thu, 31 Oct 2024 16:44:28 -0500 Subject: [PATCH] solver now takes profit in some asset in Omnipool --- hydradx/model/amm/omnix.py | 63 +++++---- hydradx/model/amm/omnix_solver_simple.py | 83 ++++++----- hydradx/tests/test_solver.py | 169 +++++++++++++++++++---- 3 files changed, 221 insertions(+), 94 deletions(-) diff --git a/hydradx/model/amm/omnix.py b/hydradx/model/amm/omnix.py index 805086ed..1518ea77 100644 --- a/hydradx/model/amm/omnix.py +++ b/hydradx/model/amm/omnix.py @@ -48,19 +48,25 @@ def calculate_transfers( def validate_and_execute_solution( omnipool: OmnipoolState, intents: list, # swap desired to be processed - intent_deltas: list # list of deltas for each intent + intent_deltas: list, # list of deltas for each intent + tkn_profit: str = None ): validate_intents(intents, intent_deltas) transfers, deltas = calculate_transfers(intents, intent_deltas) validate_transfer_amounts(transfers) - pool_agent, lrna_deltas = execute_solution(omnipool, transfers, deltas) + pool_agent, fee_agent, lrna_deltas = execute_solution(omnipool, transfers, deltas) if not validate_remainder(pool_agent): raise Exception("agent has negative holdings") update_intents(intents, transfers) - - return True + if tkn_profit is not None: + tkn_list = [tkn for tkn in pool_agent.holdings if tkn != tkn_profit] + for tkn in tkn_list: + omnipool.swap(pool_agent, tkn_profit, tkn, sell_quantity=pool_agent.holdings[tkn]) + return True, (pool_agent.holdings[tkn_profit] if tkn_profit in pool_agent.holdings else 0) + else: + return True def validate_intents(intents: list, intent_deltas: list): @@ -92,9 +98,11 @@ def validate_transfer_amounts(transfers: list): def execute_solution( omnipool: OmnipoolState, transfers: list, - deltas: dict # note that net_deltas can be easily reconstructed from transfers + deltas: dict, # note that net_deltas can be easily reconstructed from transfers + fee_match: float = 0.0 ): pool_agent = Agent() + fee_agent = Agent() # transfer assets in from agents whose intents are being executed for transfer in transfers: @@ -108,24 +116,31 @@ def execute_solution( init_lrna = {tkn: omnipool.lrna[tkn] for tkn in deltas if tkn != 'LRNA'} for tkn_buy in deltas: - for tkn_sell in deltas: - # try to sell tkn_buy for tkn_sell, if it is the direction we need to go. - if tkn_buy != tkn_sell and deltas[tkn_buy]["in"] < deltas[tkn_buy]["out"] and deltas[tkn_sell]["in"] > deltas[tkn_sell]["out"]: - max_buy_amt = math.nextafter(deltas[tkn_buy]["out"] - deltas[tkn_buy]["in"], math.inf) - max_sell_amt = math.nextafter(deltas[tkn_sell]["in"] - deltas[tkn_sell]["out"], -math.inf) - test_state, test_agent = simulate_swap(omnipool, pool_agent, tkn_buy, tkn_sell, sell_quantity=max_sell_amt) - buy_given_max_sell = test_agent.holdings[tkn_buy] - (pool_agent.holdings[tkn_buy] if tkn_buy in pool_agent.holdings else 0) - if buy_given_max_sell > max_buy_amt: # can't do max sell, do max buy instead - init_sell_holdings = pool_agent.holdings[tkn_sell] - omnipool.swap(pool_agent, tkn_buy, tkn_sell, buy_quantity=max_buy_amt) - deltas[tkn_buy]["out"] -= max_buy_amt - deltas[tkn_sell]["in"] -= init_sell_holdings - pool_agent.holdings[tkn_sell] - else: - init_buy_liquidity = pool_agent.holdings[tkn_buy] if tkn_buy in pool_agent.holdings else 0 - omnipool.swap(pool_agent, tkn_buy, tkn_sell, sell_quantity=max_sell_amt) - deltas[tkn_sell]["in"] -= max_sell_amt - deltas[tkn_buy]["out"] -= pool_agent.holdings[tkn_buy] - init_buy_liquidity - lrna_deltas = {tkn: omnipool.lrna[tkn] - init_lrna[tkn] for tkn in init_lrna} + if deltas[tkn_buy]["in"] < deltas[tkn_buy]["out"]: + for tkn_sell in deltas: + # try to sell tkn_buy for tkn_sell, if it is the direction we need to go. + if tkn_buy != tkn_sell and deltas[tkn_sell]["in"] > deltas[tkn_sell]["out"]: + max_buy_amt = math.nextafter(deltas[tkn_buy]["out"] - deltas[tkn_buy]["in"], math.inf) + max_sell_amt = math.nextafter(deltas[tkn_sell]["in"] - deltas[tkn_sell]["out"], -math.inf) + test_state, test_agent = simulate_swap(omnipool, pool_agent, tkn_buy, tkn_sell, sell_quantity=max_sell_amt) + buy_given_max_sell = test_agent.holdings[tkn_buy] - (pool_agent.holdings[tkn_buy] if tkn_buy in pool_agent.holdings else 0) + if buy_given_max_sell > max_buy_amt: # can't do max sell, do max buy instead + init_sell_holdings = pool_agent.holdings[tkn_sell] + omnipool.swap(pool_agent, tkn_buy, tkn_sell, buy_quantity=max_buy_amt) + deltas[tkn_buy]["out"] -= max_buy_amt + deltas[tkn_sell]["in"] -= init_sell_holdings - pool_agent.holdings[tkn_sell] + else: + init_buy_liquidity = pool_agent.holdings[tkn_buy] if tkn_buy in pool_agent.holdings else 0 + omnipool.swap(pool_agent, tkn_buy, tkn_sell, sell_quantity=max_sell_amt) + deltas[tkn_sell]["in"] -= max_sell_amt + deltas[tkn_buy]["out"] -= pool_agent.holdings[tkn_buy] - init_buy_liquidity + lrna_deltas = {tkn: omnipool.lrna[tkn] - init_lrna[tkn] for tkn in init_lrna} + # transfer matched fees to fee agent + matched_amt = min(deltas[tkn_buy]["in"], deltas[tkn_buy]["out"]) + fee_amt = matched_amt * fee_match + if fee_amt > 0: + pool_agent.holdings[tkn_buy] -= fee_amt + fee_agent.holdings[tkn_buy] = fee_amt # transfer assets out to intent agents for transfer in transfers: @@ -137,7 +152,7 @@ def execute_solution( elif transfer['buy_quantity'] < 0: raise Exception("buy quantity is negative") - return pool_agent, lrna_deltas + return pool_agent, fee_agent, lrna_deltas def validate_remainder(pool_agent: Agent): diff --git a/hydradx/model/amm/omnix_solver_simple.py b/hydradx/model/amm/omnix_solver_simple.py index f1bd79dd..8c267445 100644 --- a/hydradx/model/amm/omnix_solver_simple.py +++ b/hydradx/model/amm/omnix_solver_simple.py @@ -3,7 +3,6 @@ import clarabel import numpy as np -import cvxpy as cp import clarabel as cb import highspy from scipy import sparse @@ -84,12 +83,12 @@ def clear(self): def _set_known_flow(self): self._known_flow = {tkn: {'in': 0, 'out': 0} for tkn in ["LRNA"] + self.asset_list} - # if self.I is not None: # full intent executions are known - assert len(self.I) == len(self.full_intents) - for i, intent in enumerate(self.full_intents): - if self.I[i] > 0.5: - self._known_flow[intent['tkn_sell']]['in'] += intent["sell_quantity"] - self._known_flow[intent['tkn_buy']]['out'] += intent["buy_quantity"] + if self.I is not None: # full intent executions are known + assert len(self.I) == len(self.full_intents) + for i, intent in enumerate(self.full_intents): + if self.I[i] > 0.5: + self._known_flow[intent['tkn_sell']]['in'] += intent["sell_quantity"] + self._known_flow[intent['tkn_buy']]['out'] += intent["buy_quantity"] # note that max out is not enforced in Omnipool, it's used to scale variables and use good estimates for AMMs # in particular, the max_out for tkn_profit does not reflect that the solver will buy it with any leftover @@ -119,13 +118,16 @@ def _set_scaling(self): self._scaling["LRNA"] = 0 for tkn in self.asset_list: self._scaling[tkn] = max(self._max_in[tkn], self._max_out[tkn]) - if self._scaling[tkn] == 0: + if self._scaling[tkn] == 0 and tkn != self.tkn_profit: self._scaling[tkn] = 1 else: self._scaling[tkn] = min(self._scaling[tkn], self.omnipool.liquidity[tkn]) # set scaling for LRNA equal to scaling for asset, adjusted by spot price scalar = self._scaling[tkn] * self.omnipool.lrna[tkn] / self.omnipool.liquidity[tkn] self._scaling["LRNA"] = max(self._scaling["LRNA"], scalar) + # raise scaling for tkn_profit to scaling for asset, adjusted by spot price, if needed + scalar_profit = self._scaling[tkn] * self.omnipool.price(self.omnipool, tkn, self.tkn_profit) + self._scaling[self.tkn_profit] = max(self._scaling[self.tkn_profit], scalar_profit) def _set_omnipool_directions(self): known_intent_directions = {self.tkn_profit: 'buy'} # solver collects profits in tkn_profit @@ -227,19 +229,21 @@ def _set_coefficients(self): self._profit_A = sparse.vstack([profit_A_LRNA, profit_A_assets], format='csc') profit_i = self.asset_list.index(self.tkn_profit) - self._q = self._profit_A[profit_i, :].toarray().flatten() + self._q = self._profit_A[profit_i + 1, :].toarray().flatten() - def _recalculate(self): + def _recalculate(self, rescale: bool = True): self._set_known_flow() self._set_max_in_out() - self._set_scaling() + if rescale: + self._set_scaling() + self._set_amm_coefs() self._set_omnipool_directions() self._set_tau_phi() - self._set_amm_coefs() self._set_coefficients() - def set_up_problem(self, I: list, flags: dict = None, sell_maxes: list = None, clear_sell_maxes: bool = True): - assert len(I) == len(self.full_intents) + def set_up_problem(self, I: list = None, flags: dict = None, sell_maxes: list = None, rescale: bool = True, clear_sell_maxes: bool = True): + if I is not None: + assert len(I) == len(self.full_intents) self.I = I if sell_maxes is not None: self.partial_sell_maxs = sell_maxes @@ -247,7 +251,7 @@ def set_up_problem(self, I: list, flags: dict = None, sell_maxes: list = None, c self.partial_sell_maxs = [intent['sell_quantity'] for intent in self.partial_intents] if flags is not None: self._directional_flags = flags - self._recalculate() + self._recalculate(rescale) def get_amm_lrna_coefs(self): return {k: v for k, v in self._amm_lrna_coefs.items()} @@ -281,8 +285,8 @@ def get_partial_intent_prices(self): def get_partial_sell_maxs_scaled(self): return [self.partial_sell_maxs[j] / self._scaling[intent['tkn_sell']] for j, intent in enumerate(self.partial_intents)] - def scale_LRNA_amt(self, amt): - return amt * self._scaling["LRNA"] + def scale_obj_amt(self, amt): + return amt * self._scaling[self.tkn_profit] def get_real_x(self, x): ''' @@ -351,7 +355,7 @@ def scale_down_partial_intents(p, trade_pcts): intent_sell_maxs = [] for i, m in enumerate(p.partial_sell_maxs): # we allow new solution to find trade size up to 10x old solution - new_sell_quantity = min([m * trade_pcts[i] * 10, m]) + new_sell_quantity = m / 10 tkn = p.partial_intents[i]['tkn_sell'] sell_amt_lrna_value = new_sell_quantity * p.omnipool.price(p.omnipool, tkn) # if we are scaling lower than min_partial, we eliminate the intent from execution @@ -444,7 +448,7 @@ def _find_solution_unrounded( epsilon_tkn = p.get_epsilon_tkn() for i in range(n): tkn = asset_list[i] - if epsilon_tkn[tkn] <= 1e-6: # linearize the AMM constraint + if epsilon_tkn[tkn] <= 1e-6 and tkn != p.tkn_profit: # linearize the AMM constraint if tkn not in directions: c1 = 1 / (1 + epsilon_tkn[tkn]) c2 = 1 / (1 - epsilon_tkn[tkn]) @@ -552,9 +556,11 @@ def _find_solution_unrounded( for j in range(len(partial_intents)): exec_intent_deltas[j] = -x_scaled[4 * n + j] - fixed_profit = objective_I_coefs @ I if I is not None else 0 - return (new_amm_deltas, exec_intent_deltas, x_expanded, p.scale_LRNA_amt(solution.obj_val + fixed_profit), - p.scale_LRNA_amt(solution.obj_val_dual + fixed_profit), str(solution.status)) + full_sell_tkns = [intent['tkn_sell'] for intent in full_intents] + objective_I_coefs_scaled = np.array([objective_I_coefs[l] * p._scaling[full_sell_tkns[l]] for l in range(r)]) + fixed_profit = objective_I_coefs_scaled @ I if I is not None else 0 + return (new_amm_deltas, exec_intent_deltas, x_expanded, p.scale_obj_amt(solution.obj_val + fixed_profit), + p.scale_obj_amt(solution.obj_val_dual + fixed_profit), str(solution.status)) def _solve_inclusion_problem( @@ -671,6 +677,7 @@ def _solve_inclusion_problem( h.passModel(lp) h.run() + status = h.getModelStatus() solution = h.getSolution() info = h.getInfo() basis = h.getBasis() @@ -693,7 +700,7 @@ def _solve_inclusion_problem( save_A_upper = np.concatenate([old_A_upper, S_upper]) save_A_lower = np.concatenate([old_A_lower, S_lower]) - return new_amm_deltas, exec_partial_intent_deltas, exec_full_intent_flags, save_A, save_A_upper, save_A_lower, -q @ x_expanded * scaling[p.tkn_profit], solution.value_valid + return new_amm_deltas, exec_partial_intent_deltas, exec_full_intent_flags, save_A, save_A_upper, save_A_lower, -q @ x_expanded * scaling[p.tkn_profit], solution.value_valid, status def round_solution(intents, intent_deltas, tolerance=0.0001): @@ -757,20 +764,20 @@ def find_solution_outer_approx(state: OmnipoolState, init_intents: list, min_par Z_U = inf best_status = "Not Solved" y_best = indicators - best_amm_deltas = [0]*n + best_amm_deltas = {tkn: 0 for tkn in p.asset_list} best_intent_deltas = [0]*m milp_obj = -inf new_A, new_A_upper, new_A_lower = np.zeros((0, k_milp)), np.array([]), np.array([]) + p.set_up_problem() # scale problem including all intents # loop until MILP has no solution: for _i in range(50): # - update I^(K+1), Z_L Z_L = max(Z_L, milp_obj) # - do NLP solve given I values, update x^K - p.set_up_problem(I=indicators) + p.set_up_problem(I=indicators, rescale=False) amm_deltas, intent_deltas, x, obj, dual_obj, status = _find_solution_unrounded(p) - if obj < Z_U and dual_obj < 0: # - update Z_U, y*, x* + if obj < Z_U and dual_obj <= 0: # - update Z_U, y*, x* Z_U = obj - x_best = x y_best = indicators best_amm_deltas = amm_deltas best_intent_deltas = intent_deltas @@ -790,7 +797,7 @@ def find_solution_outer_approx(state: OmnipoolState, init_intents: list, min_par A_lower = np.concatenate([new_A_lower, IC_lower]) # - do MILP solve - amm_deltas, partial_intent_deltas, indicators, new_A, new_A_upper, new_A_lower, milp_obj, valid = _solve_inclusion_problem(p, x, Z_U, Z_L, A, A_upper, A_lower) + amm_deltas, partial_intent_deltas, indicators, new_A, new_A_upper, new_A_lower, milp_obj, valid, status = _solve_inclusion_problem(p, x, Z_U, Z_L, A, A_upper, A_lower) if not valid: break @@ -800,7 +807,8 @@ def find_solution_outer_approx(state: OmnipoolState, init_intents: list, min_par trade_pcts = [-best_intent_deltas[i] / m for i, m in enumerate(p.partial_sell_maxs)] # if solution is not good yet, try scaling down partial intent sizes, to get scaling better - while len(p.partial_intents) > 0 and (best_status != "Solved" or Z_U > 0) and min(trade_pcts) < 0.05: + # while len(p.partial_intents) > 0 and (best_status != "Solved" or Z_U > 0) and min(trade_pcts) < 0.05: + while len(p.partial_intents) > 0 and min(trade_pcts) < 0.05: new_maxes, zero_ct = scale_down_partial_intents(p, trade_pcts) p.set_up_problem(I=y_best, sell_maxes=new_maxes) if zero_ct == m: @@ -816,27 +824,14 @@ def find_solution_outer_approx(state: OmnipoolState, init_intents: list, min_par best_x = x Z_U = obj status = temp_status - else: - break # break if no improvement in solution + # else: + # break # break if no improvement in solution trade_pcts = [-best_intent_deltas[i] / m if m > 0 else 0 for i, m in enumerate(p.partial_sell_maxs)] flags = get_directional_flags(best_amm_deltas) p.set_up_problem(I=y_best, flags=flags, clear_sell_maxes=False) best_amm_deltas, best_intent_deltas, x, obj, dual_obj, status = _find_solution_unrounded(p) - # linearize = [] - # _, max_in, max_out = _calculate_scaling(new_partial_intents, full_intents, y_best, state, asset_list) - # epsilon_tkn_ls = [(max([abs(max_in[t]), abs(max_out[t])]) / state.liquidity[t], t) for t in asset_list] - # epsilon_tkn_ls.sort() - # loc = bisect.bisect_right([x[0] for x in epsilon_tkn_ls], epsilon) - # while status != "Solved" and loc < len(epsilon_tkn_ls) and epsilon_tkn_ls[loc][0] < 0: - # # force linearization of asset with smallest epsilon - # linearize.append(epsilon_tkn_ls[loc][1]) - # loc += 1 - # best_amm_deltas, best_intent_deltas, x, obj, dual_obj, status = _find_solution_unrounded3(state, new_partial_intents, - # full_intents, I=y_best, - # flags=flags, epsilon=epsilon, - # force_linear = linearize) if status not in ["Solved", "AlmostSolved"]: if obj > 0: return [[0,0]]*(m+r), 0 # no solution found diff --git a/hydradx/tests/test_solver.py b/hydradx/tests/test_solver.py index 8960bb5f..1bc3e74f 100644 --- a/hydradx/tests/test_solver.py +++ b/hydradx/tests/test_solver.py @@ -7,6 +7,8 @@ from hydradx.model.amm.agents import Agent from hydradx.model.amm.omnipool_amm import OmnipoolState from mpmath import mp, mpf +import highspy +import numpy as np from hydradx.model.amm.omnix import validate_and_execute_solution from hydradx.model.amm.omnix_solver_simple import find_solution, \ @@ -45,25 +47,25 @@ def test_single_trade_settles(): initial_state.last_lrna_fee = {tkn: mpf(0.0005) for tkn in lrna} intents = copy.deepcopy(init_intents_partial) - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, _ = find_solution_outer_approx(initial_state, intents) assert validate_and_execute_solution(initial_state.copy(), intents, intent_deltas) assert intent_deltas[0][0] == -init_intents_partial[0]['sell_quantity'] assert intent_deltas[0][1] == init_intents_partial[0]['buy_quantity'] intents = copy.deepcopy(init_intents_full) - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, _ = find_solution_outer_approx(initial_state, intents) assert validate_and_execute_solution(initial_state.copy(), intents, intent_deltas) assert intent_deltas[0][0] == -init_intents_full[0]['sell_quantity'] assert intent_deltas[0][1] == init_intents_full[0]['buy_quantity'] intents = copy.deepcopy(init_intents_partial_lrna) - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, _ = find_solution_outer_approx(initial_state, intents) assert validate_and_execute_solution(initial_state.copy(), intents, intent_deltas) assert intent_deltas[0][0] == -init_intents_partial_lrna[0]['sell_quantity'] assert intent_deltas[0][1] == init_intents_partial_lrna[0]['buy_quantity'] intents = copy.deepcopy(init_intents_full_lrna) - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, _ = find_solution_outer_approx(initial_state, intents) assert validate_and_execute_solution(initial_state.copy(), intents, intent_deltas) assert intent_deltas[0][0] == -init_intents_full_lrna[0]['sell_quantity'] assert intent_deltas[0][1] == init_intents_full_lrna[0]['buy_quantity'] @@ -100,25 +102,25 @@ def test_single_trade_does_not_settle(): initial_state.last_lrna_fee = {tkn: mpf(0.0005) for tkn in lrna} intents = copy.deepcopy(init_intents_partial) - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, _ = find_solution_outer_approx(initial_state, intents) assert validate_and_execute_solution(initial_state.copy(), intents, intent_deltas) assert intent_deltas[0][0] == 0 assert intent_deltas[0][1] == 0 intents = copy.deepcopy(init_intents_full) - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, _ = find_solution_outer_approx(initial_state, intents) assert validate_and_execute_solution(initial_state.copy(), intents, intent_deltas) assert intent_deltas[0][0] == 0 assert intent_deltas[0][1] == 0 intents = copy.deepcopy(init_intents_partial_lrna) - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, _ = find_solution_outer_approx(initial_state, intents) assert validate_and_execute_solution(initial_state.copy(), intents, intent_deltas) assert intent_deltas[0][0] == 0 assert intent_deltas[0][1] == 0 intents = copy.deepcopy(init_intents_full_lrna) - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, _ = find_solution_outer_approx(initial_state, intents) assert validate_and_execute_solution(initial_state.copy(), intents, intent_deltas) assert intent_deltas[0][0] == 0 assert intent_deltas[0][1] == 0 @@ -154,19 +156,19 @@ def test_matching_trades_execute_more(): # do the DOT sale alone state_sale = initial_state.copy() intents_sale = [copy.deepcopy(intent1)] - sale_deltas = find_solution_outer_approx(state_sale, intents_sale) + sale_deltas, _ = find_solution_outer_approx(state_sale, intents_sale) assert validate_and_execute_solution(state_sale, intents_sale, sale_deltas) # do the DOT buy alone state_buy = initial_state.copy() intents_buy = [copy.deepcopy(intent2)] - buy_deltas = find_solution_outer_approx(state_buy, intents_buy) + buy_deltas, _ = find_solution_outer_approx(state_buy, intents_buy) assert validate_and_execute_solution(state_buy, intents_buy, buy_deltas) # do both trades together state_match = initial_state.copy() intents_match = [copy.deepcopy(intent1), copy.deepcopy(intent2)] - match_deltas = find_solution_outer_approx(state_match, intents_match) + match_deltas, _ = find_solution_outer_approx(state_match, intents_match) assert validate_and_execute_solution(state_match, intents_match, match_deltas) # check that matching trades caused more execution than executing either alone @@ -178,13 +180,13 @@ def test_matching_trades_execute_more(): # do the LRNA sale alone state_sale = initial_state.copy() intents_sale = [copy.deepcopy(intent1_lrna)] - sale_deltas = find_solution_outer_approx(state_sale, intents_sale) + sale_deltas, _ = find_solution_outer_approx(state_sale, intents_sale) assert validate_and_execute_solution(state_sale, intents_sale, sale_deltas) # do both LRNA sale & DOT buy together state_match = initial_state.copy() intents_match = [copy.deepcopy(intent1_lrna), copy.deepcopy(intent2)] - match_deltas = find_solution_outer_approx(state_match, intents_match) + match_deltas, _ = find_solution_outer_approx(state_match, intents_match) assert validate_and_execute_solution(state_match, intents_match, match_deltas) # check that matching trades caused more execution than executing either alone @@ -223,19 +225,19 @@ def test_matching_trades_execute_more_full_execution(): # do the DOT sale alone state_sale = initial_state.copy() intents_sale = [copy.deepcopy(intent1)] - sale_deltas = find_solution_outer_approx(state_sale, intents_sale) + sale_deltas, _ = find_solution_outer_approx(state_sale, intents_sale) assert validate_and_execute_solution(state_sale, intents_sale, sale_deltas) # do the DOT buy alone state_buy = initial_state.copy() intents_buy = [copy.deepcopy(intent2)] - buy_deltas = find_solution_outer_approx(state_buy, intents_buy) + buy_deltas, _ = find_solution_outer_approx(state_buy, intents_buy) assert validate_and_execute_solution(state_buy, intents_buy, buy_deltas) # do both trades together state_match = initial_state.copy() intents_match = [copy.deepcopy(intent1), copy.deepcopy(intent2)] - match_deltas = find_solution_outer_approx(state_match, intents_match) + match_deltas, _ = find_solution_outer_approx(state_match, intents_match) assert validate_and_execute_solution(state_match, intents_match, match_deltas) # check that matching trades caused more execution than executing either alone @@ -247,13 +249,13 @@ def test_matching_trades_execute_more_full_execution(): # do the LRNA sale alone state_sale = initial_state.copy() intents_sale = [copy.deepcopy(intent1_lrna)] - sale_deltas = find_solution_outer_approx(state_sale, intents_sale) + sale_deltas, _ = find_solution_outer_approx(state_sale, intents_sale) assert validate_and_execute_solution(state_sale, intents_sale, sale_deltas) # do both LRNA sale & DOT buy together state_match = initial_state.copy() intents_match = [copy.deepcopy(intent1_lrna), copy.deepcopy(intent2)] - match_deltas = find_solution_outer_approx(state_match, intents_match) + match_deltas, _ = find_solution_outer_approx(state_match, intents_match) assert validate_and_execute_solution(state_match, intents_match, match_deltas) # check that matching trades caused more execution than executing either alone @@ -290,7 +292,7 @@ def test_convex(): initial_state.last_fee = {tkn: mpf(0.0025) for tkn in lrna} initial_state.last_lrna_fee = {tkn: mpf(0.0005) for tkn in lrna} - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, _ = find_solution_outer_approx(initial_state, intents) assert validate_and_execute_solution(initial_state, intents, intent_deltas) @@ -328,7 +330,7 @@ def test_with_lrna_intent(): initial_state.last_fee = {tkn: mpf(0.0025) for tkn in lrna} initial_state.last_lrna_fee = {tkn: mpf(0.0005) for tkn in lrna} - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, _ = find_solution_outer_approx(initial_state, intents) assert validate_and_execute_solution(initial_state, intents, intent_deltas) @@ -369,7 +371,7 @@ def test_small_trade(): # this is to test that rounding errors don't screw up s initial_state.last_fee = {tkn: mpf(0.0025) for tkn in lrna} initial_state.last_lrna_fee = {tkn: mpf(0.0005) for tkn in lrna} - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, _ = find_solution_outer_approx(initial_state, intents) assert validate_and_execute_solution(initial_state.copy(), copy.deepcopy(intents), intent_deltas) assert intent_deltas[0][0] == -intents[0]['sell_quantity'] @@ -377,7 +379,76 @@ def test_small_trade(): # this is to test that rounding errors don't screw up s assert intent_deltas[1][0] == 0 assert intent_deltas[1][1] == 0 -@given(st.floats(min_value=1e-10, max_value=1e-3)) +@given(st.floats(min_value=1e-7, max_value=1e-3)) +@settings(verbosity=Verbosity.verbose, print_blob=True) +def test_inclusion_problem_small_trade_fuzz(trade_size_pct: float): + liquidity = {'4-Pool': mpf(1392263.9295618401), 'HDX': mpf(140474254.46393022), 'KILT': mpf(1941765.8700688032), + 'WETH': mpf(897.820372708098), '2-Pool': mpf(80.37640742108785), 'GLMR': mpf(7389788.325282889), + 'BNC': mpf(5294190.655262755), 'RING': mpf(30608622.54045291), 'vASTR': mpf(1709768.9093601815), + 'vDOT': mpf(851755.7840315843), 'CFG': mpf(3497639.0397717496), 'CRU': mpf(337868.26827475097), + '2-Pool': mpf(14626788.977583803), 'DOT': mpf(2369965.4990946855), 'PHA': mpf(6002455.470581388), + 'ZTG': mpf(9707643.829161936), 'INTR': mpf(52756928.48950746), 'ASTR': mpf(31837859.71273387), } + lrna = {'4-Pool': mpf(50483.454258911326), 'HDX': mpf(24725.8021660851), 'KILT': mpf(10802.301353604526), + 'WETH': mpf(82979.9927924809), '2-Pool': mpf(197326.54331209575), 'GLMR': mpf(44400.11377262768), + 'BNC': mpf(35968.10763198863), 'RING': mpf(1996.48438233777), 'vASTR': mpf(4292.819030020081), + 'vDOT': mpf(182410.99000727307), 'CFG': mpf(41595.57689216696), 'CRU': mpf(4744.442135139952), + '2-Pool': mpf(523282.70722423657), 'DOT': mpf(363516.4838824808), 'PHA': mpf(24099.247547699764), + 'ZTG': mpf(4208.90365804613), 'INTR': mpf(19516.483401186168), 'ASTR': mpf(68571.5237579274), } + + initial_state = OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + }, + asset_fee=mpf(0.0025), + lrna_fee=mpf(0.0005) + ) + initial_state.last_fee = {tkn: mpf(0.0025) for tkn in lrna} + initial_state.last_lrna_fee = {tkn: mpf(0.0005) for tkn in lrna} + + buy_tkn = 'DOT' + selL_tkn = '2-Pool' + buy_amt = trade_size_pct * liquidity[buy_tkn] + # buy_amt = mpf(.01) + price = initial_state.price(initial_state, buy_tkn, selL_tkn) + sell_amt = buy_amt * price * 1.01 + # sell_amt = mpf(.05) + agents = [Agent(holdings={selL_tkn: sell_amt})] + + intents = [ + {'sell_quantity': sell_amt, 'buy_quantity': buy_amt, 'tkn_sell': selL_tkn, 'tkn_buy': buy_tkn, 'agent': agents[0], 'partial': False}, + ] + + # intent_deltas, _ = find_solution_outer_approx(initial_state, intents) + p = ICEProblem(initial_state, intents) + p.set_up_problem() + + inf = highspy.kHighsInf + Z_L = -inf + Z_U = 0 + x = np.zeros(13) + A, A_upper, A_lower = np.zeros((0, 13)), np.array([]), np.array([]) + # - get new cone constraint from I^K + indicators = [0] + BK = np.where(np.array(indicators) == 1)[0] + 12 + NK = np.where(np.array(indicators) == 0)[0] + 12 + IC_A = np.zeros((1, 13)) + IC_A[0, BK] = 1 + IC_A[0, NK] = -1 + IC_upper = np.array([len(BK) - 1]) + IC_lower = np.array([-inf]) + + # - add cone constraint to A, A_upper, A_lower + A = np.vstack([A, IC_A]) + A_upper = np.concatenate([A_upper, IC_upper]) + A_lower = np.concatenate([A_lower, IC_lower]) + amm_deltas, partial_intent_deltas, indicators, new_A, new_A_upper, new_A_lower, milp_obj, valid, status = _solve_inclusion_problem(p, x, Z_U, Z_L, A, A_upper, A_lower) + assert indicators[0] == 1 + assert str(status) == 'HighsModelStatus.kOptimal' + # assert validate_and_execute_solution(initial_state.copy(), copy.deepcopy(intents), intent_deltas) + # assert intent_deltas[0][0] == -intents[0]['sell_quantity'] + # assert intent_deltas[0][1] == pytest.approx(intents[0]['buy_quantity'], rel=1e-10) + +@given(st.floats(min_value=1e-7, max_value=1e-3)) @settings(verbosity=Verbosity.verbose, print_blob=True) def test_small_trade_fuzz(trade_size_pct: float): # this is to test that rounding errors don't screw up small trades @@ -415,7 +486,7 @@ def test_small_trade_fuzz(trade_size_pct: float): # this is to test that roundi {'sell_quantity': sell_amt, 'buy_quantity': buy_amt, 'tkn_sell': selL_tkn, 'tkn_buy': buy_tkn, 'agent': agents[0], 'partial': True}, ] - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, _ = find_solution_outer_approx(initial_state, intents) assert validate_and_execute_solution(initial_state.copy(), copy.deepcopy(intents), intent_deltas) assert intent_deltas[0][0] == -intents[0]['sell_quantity'] @@ -520,7 +591,7 @@ def test_full_solver(): initial_state.last_fee = {tkn: mpf(0.0025) for tkn in lrna} initial_state.last_lrna_fee = {tkn: mpf(0.0005) for tkn in lrna} - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, _ = find_solution_outer_approx(initial_state, intents) assert validate_and_execute_solution(initial_state.copy(), copy.deepcopy(intents), intent_deltas) @@ -532,6 +603,7 @@ def test_full_solver(): st.lists(st.integers(min_value=0, max_value=17), min_size=3, max_size=3), st.lists(st.booleans(), min_size=3, max_size=3) ) +@reproduce_failure('6.39.6', b'AXic4wg0jOIoKWcgAQAAVxkB2A==') @settings(print_blob=True, verbosity=Verbosity.verbose, deadline=None, phases=(Phase.explicit, Phase.reuse, Phase.generate, Phase.target)) def test_solver_random_intents(sell_ratios, price_ratios, sell_is, buy_is, partial_flags): @@ -572,8 +644,53 @@ def test_solver_random_intents(sell_ratios, price_ratios, sell_is, buy_is, parti intents.append({'sell_quantity': sell_quantity, 'buy_quantity': buy_quantity, 'tkn_sell': sell_tkn, 'tkn_buy': buy_tkn, 'agent': agent, 'partial': partial_flags[i]}) - intent_deltas = find_solution_outer_approx(initial_state, intents) + intent_deltas, predicted_profit = find_solution_outer_approx(initial_state, intents) - assert validate_and_execute_solution(initial_state.copy(), copy.deepcopy(intents), intent_deltas) + valid, profit = validate_and_execute_solution(initial_state.copy(), copy.deepcopy(intents), intent_deltas, "HDX") + assert valid + abs_error = abs(profit - predicted_profit) + if profit > 0: + pct_error = abs_error/profit + assert pct_error < 0.01 or abs_error < 100 + else: + assert abs_error == 0 + # assert abs_error < 100 pprint(intent_deltas) + + +def test_case_Martin(): + + liquidity = {'4-Pool': mpf(1392263.9295618401), 'HDX': mpf(140474254.46393022), 'KILT': mpf(1941765.8700688032), + 'WETH': mpf(897.820372708098), '2-Pool-btc': mpf(80.37640742108785), 'GLMR': mpf(7389788.325282889), + 'BNC': mpf(5294190.655262755), 'RING': mpf(30608622.54045291), 'vASTR': mpf(1709768.9093601815), + 'vDOT': mpf(851755.7840315843), 'CFG': mpf(3497639.0397717496), 'CRU': mpf(337868.26827475097), + '2-Pool': mpf(14626788.977583803), 'DOT': mpf(2369965.4990946855), 'PHA': mpf(6002455.470581388), + 'ZTG': mpf(9707643.829161936), 'INTR': mpf(52756928.48950746), 'ASTR': mpf(31837859.71273387), } + lrna = {'4-Pool': mpf(50483.454258911326), 'HDX': mpf(24725.8021660851), 'KILT': mpf(10802.301353604526), + 'WETH': mpf(82979.9927924809), '2-Pool-btc': mpf(197326.54331209575), 'GLMR': mpf(44400.11377262768), + 'BNC': mpf(35968.10763198863), 'RING': mpf(1996.48438233777), 'vASTR': mpf(4292.819030020081), + 'vDOT': mpf(182410.99000727307), 'CFG': mpf(41595.57689216696), 'CRU': mpf(4744.442135139952), + '2-Pool': mpf(523282.70722423657), 'DOT': mpf(363516.4838824808), 'PHA': mpf(24099.247547699764), + 'ZTG': mpf(4208.90365804613), 'INTR': mpf(19516.483401186168), 'ASTR': mpf(68571.5237579274), } + + agent = Agent(holdings={'GLMR': 1001500}) + + intents = [ + {'sell_quantity': mpf(1001497.604662274886037302), 'buy_quantity': mpf(1081639.587746551400027), 'tkn_sell': 'GLMR', 'tkn_buy': 'KILT', 'agent': agent, 'partial': True}, + ] + + initial_state = OmnipoolState( + tokens={ + tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna + }, + asset_fee=mpf(0.0025), + lrna_fee=mpf(0.0005) + ) + initial_state.last_fee = {tkn: mpf(0.0025) for tkn in lrna} + initial_state.last_lrna_fee = {tkn: mpf(0.0005) for tkn in lrna} + + intent_deltas, predicted_profit = find_solution_outer_approx(initial_state, intents) + valid, profit = validate_and_execute_solution(initial_state.copy(), copy.deepcopy(intents), intent_deltas, "HDX") + assert valid + assert profit == 0 \ No newline at end of file