Skip to content

Commit 2c02a0e

Browse files
committed
refactored some of the approximation logic
1 parent 69d4efe commit 2c02a0e

3 files changed

Lines changed: 137 additions & 87 deletions

File tree

hydradx/model/solver/amm_constraints.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,19 @@ def get_milp_constraints(self, x_list, global_asset_list, scaling_mat):
156156
A_upper = np.concatenate([A1_upper, A2_upper])
157157
return A, A_lower, A_upper
158158

159+
@abstractmethod
160+
def get_approx(self, deltas: list):
161+
pass
162+
163+
@abstractmethod
164+
def upgrade_approx(self, deltas: list, current_approx):
165+
pass
166+
167+
@abstractmethod
168+
def get_linear_approx(self):
169+
pass
170+
171+
159172
class XykConstraints(AmmConstraints):
160173
def __init__(self, amm: ConstantProductPoolState):
161174
super().__init__(amm)
@@ -237,6 +250,20 @@ def get_linearized_amm_constraints(self, x_list, global_asset_list: list, scalin
237250
S_lower = np.array([-INF] * len(S_upper)) # no lower bounds for linearized constraints
238251
return S, S_lower, S_upper
239252

253+
def get_approx(self, deltas: list) -> str:
254+
pcts = [abs(deltas[0]) / self.shares] # first shares size constraint, delta_s / s_0 <= epsilon
255+
pcts.extend([abs(deltas[j + 1]) / self.liquidity[tkn] for j, tkn in enumerate(self.asset_list)])
256+
approx = "linear" if max(pcts) <= 1e-5 else "full"
257+
return approx
258+
259+
def upgrade_approx(self, deltas: list, current_approx: str) -> str:
260+
if current_approx == "full":
261+
return "full" # we don't want to downgrade full constraints to linear approximations
262+
return self.get_approx(deltas)
263+
264+
def get_linear_approx(self): # generate a linear approximation object
265+
return "linear"
266+
240267

241268
class StableswapConstraints(AmmConstraints):
242269
def __init__(self, amm: Exchange):
@@ -407,4 +434,24 @@ def get_milp_constraints(self, x_list, global_asset_list, scaling_mat):
407434
A = np.vstack([A1, A2])
408435
A_lower = np.concatenate([A1_lower, A2_lower])
409436
A_upper = np.concatenate([A1_upper, A2_upper])
410-
return A, A_lower, A_upper
437+
return A, A_lower, A_upper
438+
439+
def get_approx(self, deltas: list) -> list[str]:
440+
pcts = [abs(deltas[0]) / self.shares] # first shares size constraint, delta_s / s_0 <= epsilon
441+
sum_delta_x = sum([abs(deltas[j + 1]) for j in range(len(self.asset_list))])
442+
pcts.append(sum_delta_x / self.d)
443+
pcts.extend([abs(deltas[j + 1]) / self.liquidity[tkn] for j, tkn in enumerate(self.asset_list)])
444+
approx = ["linear" if max(pcts[0], pcts[1]) <= 1e-5 else "full"]
445+
for pct in pcts[2:]:
446+
approx.append("linear" if pct <= 1e-5 else "full")
447+
return approx
448+
449+
def upgrade_approx(self, deltas: list, current_approx: list[str]) -> list[str]:
450+
new_approx = self.get_approx(deltas)
451+
for i, approx in enumerate(current_approx):
452+
if approx == "full" and new_approx[i] == "linear":
453+
new_approx[i] = "full" # we don't want to downgrade full constraints to linear approximations
454+
return new_approx
455+
456+
def get_linear_approx(self): # generate a linear approximation object
457+
return ["linear"] * (len(self.asset_list) + 1)

hydradx/model/solver/omnix_solver.py

Lines changed: 21 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -813,8 +813,6 @@ def _find_solution_unrounded(
813813
approx = p.get_omnipool_approx(tkn)
814814
# if approx == "none" and epsilon_tkn[tkn] <= 1e-6 and tkn != p.tkn_profit:
815815
# approx = "linear"
816-
# elif approx == "none" and epsilon_tkn[tkn] <= 1e-3:
817-
# approx = "quadratic"
818816
if tkn == p.tkn_profit:
819817
approx = "none"
820818
if approx == "linear": # linearize the AMM constraint
@@ -858,14 +856,6 @@ def _find_solution_unrounded(
858856
# A4i = np.vstack([A4i, A4i_bounds])
859857
# b4i = np.append(b4i, b4i_bounds)
860858
# cones4.append(cb.NonnegativeConeT(4))
861-
elif approx == "quadratic": # quadratic approximation to in-given-out function
862-
A4i = np.zeros((3, k))
863-
A4i[1,i] = -omnipool_lrna_coefs[tkn]
864-
A4i[1,n+i] = -omnipool_asset_coefs[tkn]
865-
A4i[2,n+i] = -omnipool_asset_coefs[tkn]
866-
b4i = np.array([1, 0, 0])
867-
cones4.append(cb.PowerConeT(0.5))
868-
cone_sizes4.append(3)
869859
else: # full AMM constraint
870860
A4i = np.zeros((3, k))
871861
b4i = np.ones(3)
@@ -991,11 +981,8 @@ def _find_good_solution(
991981
n, m, r = p.n, p.m, p.r
992982
force_omnipool_approx = {tkn: "linear" for tkn in p.omnipool.asset_list}
993983
amm_approx_list = []
994-
for amm in p.amm_list:
995-
if isinstance(amm, StableSwapPoolState):
996-
amm_approx_list.append(["linear" for _ in range(len(amm.asset_list) + 1)])
997-
elif isinstance(amm, ConstantProductPoolState):
998-
amm_approx_list.append("linear")
984+
for amm_constraints in p.amm_constraints:
985+
amm_approx_list.append(amm_constraints.get_linear_approx())
999986
p.set_up_problem(clear_I=False, force_omnipool_approx=force_omnipool_approx, amm_approx_list=amm_approx_list)
1000987
omnipool_deltas, intent_deltas, x, obj, dual_obj, status, amm_deltas = _find_solution_unrounded(p, allow_loss=allow_loss)
1001988
# if partial trade size is much higher than executed trade, lower trade max
@@ -1021,40 +1008,19 @@ def _find_good_solution(
10211008
# stableswap_pcts.append(pcts)
10221009

10231010
# force_omnipool_approx = None
1024-
approx_adjusted_ct = 0
1011+
approx_adjusted = False
10251012
if approx_amm_eqs and status not in ['PrimalInfeasible', 'DualInfeasible']: # update approximations if necessary
10261013
omnipool_pcts = {tkn: abs(omnipool_deltas[tkn]) / p.omnipool.liquidity[tkn] for tkn in p.omnipool.asset_list}
10271014
for tkn in p.omnipool.asset_list:
10281015
if force_omnipool_approx[tkn] == "linear" and omnipool_pcts[tkn] > 1e-6:
1029-
force_omnipool_approx[tkn] = "quadratic" # don't actually want to force linear approximation
1030-
approx_adjusted_ct += 1
1031-
if force_omnipool_approx[tkn] == "quadratic" and omnipool_pcts[tkn] > 1e-3:
1032-
force_omnipool_approx[tkn] = "full" # don't actually want to force quadratic approximation
1033-
approx_adjusted_ct += 1
1034-
1035-
amm_pcts = []
1036-
for _i, amm in enumerate(p.amm_list):
1037-
pcts = [abs(amm_deltas[_i][0]) / amm.shares] # first shares size constraint, delta_s / s_0 <= epsilon
1038-
if isinstance(amm, StableSwapPoolState):
1039-
sum_delta_x = sum([abs(amm_deltas[_i][j + 1]) for j in range(len(amm.asset_list))])
1040-
pcts.append(sum_delta_x / amm.d)
1041-
pcts.extend([abs(amm_deltas[_i][j + 1]) / amm.liquidity[tkn] for j, tkn in enumerate(amm.asset_list)])
1042-
amm_pcts.append(pcts)
1043-
for s, amm in enumerate(p.amm_list):
1044-
if isinstance(amm, StableSwapPoolState):
1045-
if amm_approx_list[s][0] == "linear" and max(amm_pcts[s][0], amm_pcts[s][1]) > 1e-5:
1046-
amm_approx_list[s][0] = "full"
1047-
approx_adjusted_ct += 1
1048-
for j in range(len(amm.asset_list)): # evaluate each asset constraint
1049-
if amm_approx_list[s][j + 1] == "linear" and amm_pcts[s][j + 2] > 1e-5:
1050-
amm_approx_list[s][j + 1] = "full"
1051-
approx_adjusted_ct += 1
1052-
elif isinstance(amm, ConstantProductPoolState):
1053-
if amm_approx_list[s][0] == "linear" and max(amm_pcts[s]) > 1e-5:
1054-
amm_approx_list[s][0] = "full"
1055-
approx_adjusted_ct += 1
1056-
else:
1057-
raise AssertionError("Unrecognized AMM type")
1016+
force_omnipool_approx[tkn] = "full" # don't actually want to force linear approximation
1017+
approx_adjusted = True
1018+
1019+
for _i, amm_constraints in enumerate(p.amm_constraints):
1020+
old_approx = amm_approx_list[_i]
1021+
amm_approx_list[_i] = amm_constraints.upgrade_approx(amm_deltas[_i], old_approx)
1022+
if old_approx != amm_approx_list[_i]:
1023+
approx_adjusted = True
10581024

10591025
for i in range(100):
10601026
# lower maxes for intents
@@ -1066,7 +1032,7 @@ def _find_good_solution(
10661032
# so we should be looking at trade_pcts where max is nonzero
10671033
trade_pcts_nonzero_max = [max(-intent_deltas[i],0) / m for i, m in enumerate(p.partial_sell_maxs) if m > 0]
10681034

1069-
if (len(trade_pcts_nonzero_max) == 0 or min(trade_pcts_nonzero_max) >= 0.1) and approx_adjusted_ct == 0:
1035+
if (len(trade_pcts_nonzero_max) == 0 or min(trade_pcts_nonzero_max) >= 0.1) and (not approx_adjusted):
10701036
break # no changes to problem were made
10711037
if len(trade_pcts_nonzero_max) > 0 and min(trade_pcts_nonzero_max) < 0.1:
10721038
new_maxes, zero_ct = scale_down_partial_intents(p, trade_pcts, 10)
@@ -1086,50 +1052,19 @@ def _find_good_solution(
10861052
if scale_trade_max: # update trade_pcts
10871053
trade_pcts = [-intent_deltas[i] / m if m > 0 else 0 for i, m in enumerate(p.partial_sell_maxs)]
10881054
# trade_pcts + [1 for _ in range(r)]
1055+
approx_adjusted = False
10891056
if approx_amm_eqs and status not in ['PrimalInfeasible', 'DualInfeasible']: # update approximations if necessary
10901057
omnipool_pcts = {tkn: abs(omnipool_deltas[tkn]) / p.omnipool.liquidity[tkn] for tkn in p.omnipool.asset_list}
1091-
approx_adjusted_ct = 0
10921058
for tkn in p.omnipool.asset_list:
10931059
if force_omnipool_approx[tkn] == "linear" and omnipool_pcts[tkn] > 1e-6:
1094-
force_omnipool_approx[tkn] = "quadratic" # don't actually want to force linear approximation
1095-
approx_adjusted_ct += 1
1096-
if force_omnipool_approx[tkn] == "quadratic" and omnipool_pcts[tkn] > 1e-3:
1097-
force_omnipool_approx[tkn] = "full" # don't actually want to force quadratic approximation
1098-
approx_adjusted_ct += 1
1099-
# elif omnipool_pcts[tkn] <= 1e-6: # force linear
1100-
# force_omnipool_approx[tkn] = "linear"
1101-
# approx_adjusted_ct += 1
1102-
# else:
1103-
# if omnipool_pcts[tkn] <= 1e-6: # force linear
1104-
# force_omnipool_approx[tkn] = "linear"
1105-
# approx_adjusted_ct += 1
1106-
# elif omnipool_pcts[tkn] <= 1e-3: # force quadratic
1107-
# force_omnipool_approx[tkn] = "quadratic"
1108-
# approx_adjusted_ct += 1
1109-
1110-
amm_pcts = []
1111-
for _i, amm in enumerate(p.amm_list):
1112-
pcts = [abs(amm_deltas[_i][0]) / amm.shares] # first shares size constraint, delta_s / s_0 <= epsilon
1113-
if isinstance(amm, StableSwapPoolState):
1114-
sum_delta_x = sum([abs(amm_deltas[_i][j + 1]) for j in range(len(amm.asset_list))])
1115-
pcts.append(sum_delta_x / amm.d)
1116-
pcts.extend([abs(amm_deltas[_i][j + 1]) / amm.liquidity[tkn] for j, tkn in enumerate(amm.asset_list)])
1117-
amm_pcts.append(pcts)
1118-
for s, amm in enumerate(p.amm_list):
1119-
if isinstance(amm, StableSwapPoolState):
1120-
if amm_approx_list[s][0] == "linear" and max(amm_pcts[s][0], amm_pcts[s][1]) > 1e-5:
1121-
amm_approx_list[s][0] = "full"
1122-
approx_adjusted_ct += 1
1123-
for j in range(len(amm.asset_list)): # evaluate each asset constraint
1124-
if amm_approx_list[s][j + 1] == "linear" and amm_pcts[s][j + 2] > 1e-5:
1125-
amm_approx_list[s][j + 1] = "full"
1126-
approx_adjusted_ct += 1
1127-
elif isinstance(amm, ConstantProductPoolState):
1128-
if amm_approx_list[s][0] == "linear" and max(amm_pcts[s]) > 1e-5:
1129-
amm_approx_list[s][0] = "full"
1130-
approx_adjusted_ct += 1
1131-
else:
1132-
raise AssertionError("Unrecognized AMM type")
1060+
force_omnipool_approx[tkn] = "full" # don't actually want to force linear approximation
1061+
approx_adjusted = True
1062+
1063+
for _i, amm_constraints in enumerate(p.amm_constraints):
1064+
old_approx = amm_approx_list[_i]
1065+
amm_approx_list[_i] = amm_constraints.upgrade_approx(amm_deltas[_i], old_approx)
1066+
if old_approx != amm_approx_list[_i]:
1067+
approx_adjusted = True
11331068

11341069
# once solution is found, re-run with directional flags
11351070
if do_directional_run:

hydradx/tests/test_solver/test_amm_constraints.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,74 @@ def test_get_xyk_bounds():
235235
raise AssertionError("Cone feasibility check should fail")
236236

237237

238+
def test_xyk_upgrade_approx():
239+
amm = ConstantProductPoolState(tokens={"A": 1_000_000, "B": 2_000_000}) # spot price is 2 B = 1 A
240+
constraints = XykConstraints(amm)
241+
242+
# current_approx is linear, we need to upgrade to full because of shares
243+
current_approx = "linear"
244+
deltas = [amm.shares / 10000, amm.liquidity["A"] / 1000000, amm.liquidity["B"] / 1000000]
245+
if constraints.upgrade_approx(deltas, current_approx) != "full":
246+
raise AssertionError("Upgrade to full approximation should be required due to shares delta")
247+
# current_approx is linear, we need to upgrade to full because of an asset
248+
current_approx = "linear"
249+
deltas = [amm.shares / 1000000, amm.liquidity["A"] / 10000, amm.liquidity["B"] / 1000000]
250+
if constraints.upgrade_approx(deltas, current_approx) != "full":
251+
raise AssertionError("Upgrade to full approximation should be required due to asset delta")
252+
# current_approx is full, all deltas are low enough for linear approximation
253+
current_approx = "full"
254+
deltas = [amm.shares / 10000, amm.liquidity["A"] / 10000, amm.liquidity["B"] / 10000]
255+
if constraints.upgrade_approx(deltas, current_approx) != "full":
256+
raise AssertionError("Full approximation should never be downgraded to linear")
257+
# current_approx is linear, all deltas are low enough for linear approximation
258+
current_approx = "linear"
259+
deltas = [amm.shares / 1000000, amm.liquidity["A"] / 1000000, amm.liquidity["B"] / 1000000]
260+
if constraints.upgrade_approx(deltas, current_approx) != "linear":
261+
raise AssertionError("Linear approximation should be kept as deltas are low enough")
262+
# current_approx is full, correct approximation is full
263+
current_approx = "full"
264+
deltas = [amm.shares / 10000, amm.liquidity["A"] / 1000000, amm.liquidity["B"] / 1000000]
265+
if constraints.upgrade_approx(deltas, current_approx) != "full":
266+
raise AssertionError("Full approximation should always be kept")
267+
268+
269+
def test_stableswap_upgrade_approx():
270+
amm = StableSwapPoolState(tokens={"A": 1_000_000, "B": 2_000_000}, amplification=100) # spot price is 2 B = 1 A
271+
constraints = StableswapConstraints(amm)
272+
273+
# current_approx is entirely linear
274+
current_approx = ["linear", "linear", "linear"]
275+
examples = [ # [delta_mults, expected_approx]
276+
[[1e-4, 1e-6, 1e-6], ["full", "linear", "linear"]],
277+
[[1e-6, 1e-4, 1e-6], ["full", "full", "linear"]],
278+
[[1e-6, 1e-6, 1e-4], ["full", "linear", "full"]],
279+
[[1e-4, 1e-4, 1e-4], ["full", "full", "full"]],
280+
[[1e-6, 1e-6, 1e-6], ["linear", "linear", "linear"]]
281+
]
282+
for delta_mults, expected_approx in examples:
283+
deltas = [amm.shares * delta_mults[0], amm.liquidity["A"] * delta_mults[1], amm.liquidity["B"] * delta_mults[2]]
284+
real_approx = constraints.upgrade_approx(deltas, current_approx)
285+
for i, approx in enumerate(real_approx):
286+
if approx != expected_approx[i]:
287+
raise AssertionError(f"Expected {expected_approx[i]} for {amm.asset_list[i]} but got {approx}")
288+
289+
# current_approx is mixed
290+
current_approx = ["full", "linear", "full"]
291+
examples = [ # [delta_mults, expected_approx]
292+
[[1e-4, 1e-6, 1e-6], ["full", "linear", "full"]],
293+
[[1e-6, 1e-4, 1e-6], ["full", "full", "full"]],
294+
[[1e-6, 1e-6, 1e-4], ["full", "linear", "full"]],
295+
[[1e-4, 1e-4, 1e-4], ["full", "full", "full"]],
296+
[[1e-6, 1e-6, 1e-6], ["full", "linear", "full"]]
297+
]
298+
for delta_mults, expected_approx in examples:
299+
deltas = [amm.shares * delta_mults[0], amm.liquidity["A"] * delta_mults[1], amm.liquidity["B"] * delta_mults[2]]
300+
real_approx = constraints.upgrade_approx(deltas, current_approx)
301+
for i, approx in enumerate(real_approx):
302+
if approx != expected_approx[i]:
303+
raise AssertionError(f"Expected {expected_approx[i]} for {amm.asset_list[i]} but got {approx}")
304+
305+
238306
# TODO test with auxiliary variable calculation
239307
# def test_get_stableswap_bounds():
240308
# amm = StableSwapPoolState(tokens={"A": 1_000_000, "B": 2_000_000}, amplification=100)

0 commit comments

Comments
 (0)