Skip to content

Commit d9872d6

Browse files
committed
moved get_stableswap_bounds to StableswapConstraints
1 parent 8617f1e commit d9872d6

4 files changed

Lines changed: 146 additions & 97 deletions

File tree

hydradx/model/solver/amm_constraints.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,82 @@ def __init__(self, amm: Exchange):
179179
self.k = 2*(len(amm.asset_list) + 1) + self.auxiliary_ct
180180
self.shares = amm.shares
181181
self.liquidity = {tkn: amm.liquidity[tkn] for tkn in amm.asset_list}
182+
self.ann = amm.ann
183+
self.d = amm.d
184+
185+
def get_amm_bounds(self, approx: str, scaling: dict):
186+
B = [0] + [scaling[tkn] for tkn in self.asset_list]
187+
C = [scaling[self.tkn_share]]
188+
ann = self.ann
189+
s0 = self.shares
190+
D0 = self.d
191+
amm_i = self.amm_i
192+
n_amm = len(self.asset_list) + 1
193+
sum_assets = sum([self.liquidity[tkn] for tkn in self.asset_list])
194+
# D0' = D_0 * (1 - 1/ann)
195+
D0_prime = D0 * (1 - 1 / ann)
196+
# a0 ~= -delta_s/s0 + [1 / (sum x_i^0 - D0') * sum delta_x_i - (D0'/s0) / (sum x_i^0 - D0') * delta_s]
197+
denom = sum_assets - D0_prime
198+
if approx[0] == "linear":
199+
A = np.zeros((1, self.k))
200+
A[0, amm_i.aux[0]] = 1 # a_{j,0} coefficient
201+
A[0, amm_i.shares_net] = (1 + D0_prime / denom) * C[0] / s0 # delta_s coefficient
202+
for t in range(1, n_amm):
203+
A[0, amm_i.asset_net[t - 1]] = -B[t] / denom # delta_x_i coefficient
204+
b = np.array([0])
205+
cones = [cb.ZeroConeT(1)]
206+
cone_sizes = [1]
207+
else:
208+
A = np.zeros((3, self.k))
209+
b = np.array([0, 0, 0])
210+
# x = a_{j,0}
211+
A[0, amm_i.aux[0]] = -1 # a_{j,0} coefficient
212+
# y = 1 + C_jS_j / s_0
213+
A[1, amm_i.shares_net] = -C[0] / s0 # delta_s coefficient
214+
b[1] = 1
215+
# z = An^n / D_0 sum(x_i^0 + B_i X_i) + (1 - An^n)(1 + C_jS_j / s_0)
216+
A[2, amm_i.shares_net] = D0_prime * C[0] / denom / s0
217+
for t in range(1, n_amm):
218+
A[2, amm_i.asset_net[t - 1]] = -B[t] / denom
219+
b[2] = 1
220+
cones = [cb.ExponentialConeT()]
221+
cone_sizes = [3]
222+
223+
for t in range(1, n_amm):
224+
x0 = self.liquidity[self.asset_list[t - 1]]
225+
if approx[t] == "linear":
226+
A_t = np.zeros((1, self.k))
227+
A_t[0, amm_i.aux[t]] = 1 # a_{j,t} coefficient
228+
A_t[0, amm_i.shares_net] = C[0] / s0 # delta_s coefficient
229+
A_t[0, amm_i.asset_net[t - 1]] = -B[t] / x0 # delta_x_i coefficient
230+
b_t = np.array([0])
231+
cone_t = cb.ZeroConeT(1)
232+
cone_size_t = 1
233+
else:
234+
A_t = np.zeros((3, self.k))
235+
b_t = np.zeros(3)
236+
# x = a_{j,t}
237+
A_t[0, amm_i.aux[t]] = -1
238+
# y = 1 + C_jS_j / s_0
239+
A_t[1, amm_i.shares_net] = -C[0] / s0
240+
b_t[1] = 1
241+
# z = (x_t^0 + B_t X_t) / D_0
242+
A_t[2, amm_i.asset_net[t - 1]] = -B[t] / x0
243+
b_t[2] = 1
244+
cone_t = cb.ExponentialConeT()
245+
cone_size_t = 3
246+
cones.append(cone_t)
247+
cone_sizes.append(cone_size_t)
248+
A = np.vstack([A, A_t])
249+
b = np.append(b, np.array(b_t))
250+
251+
A_final = np.zeros((1, self.k))
252+
for t in range(n_amm):
253+
A_final[0, amm_i.aux[t]] = -1
254+
b_final = np.array([0])
255+
A = np.vstack([A, A_final])
256+
b = np.append(b, b_final)
257+
cones.append(cb.NonnegativeConeT(1))
258+
cone_sizes.append(1)
259+
260+
return A, b, cones, cone_sizes

hydradx/model/solver/omnix_solver.py

Lines changed: 2 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -686,82 +686,6 @@ def _expand_submatrix(A, k: int, start: int):
686686
return A_limits_i
687687

688688

689-
def _get_stableswap_bounds(amm, amm_i: AmmIndexObject, approx, k, scaling):
690-
B = [0] + [scaling[tkn] for tkn in amm.asset_list]
691-
C = [scaling[amm.unique_id]]
692-
ann = amm.ann
693-
s0 = amm.shares
694-
D0 = amm.d
695-
n_amm = len(amm.asset_list) + 1
696-
sum_assets = sum([amm.liquidity[tkn] for tkn in amm.asset_list])
697-
# D0' = D_0 * (1 - 1/ann)
698-
D0_prime = D0 * (1 - 1 / ann)
699-
# a0 ~= -delta_s/s0 + [1 / (sum x_i^0 - D0') * sum delta_x_i - (D0'/s0) / (sum x_i^0 - D0') * delta_s]
700-
denom = sum_assets - D0_prime
701-
if approx[0] == "linear":
702-
A5j = np.zeros((1, k))
703-
A5j[0, amm_i.aux[0]] = 1 # a_{j,0} coefficient
704-
A5j[0, amm_i.shares_net] = (1 + D0_prime / denom) * C[0] / s0 # delta_s coefficient
705-
for t in range(1, n_amm):
706-
A5j[0, amm_i.asset_net[t - 1]] = -B[t] / denom # delta_x_i coefficient
707-
b5j = np.array([0])
708-
cones5j = [cb.ZeroConeT(1)]
709-
cones_count5j = [1]
710-
else:
711-
A5j = np.zeros((3, k))
712-
b5j = np.array([0, 0, 0])
713-
# x = a_{j,0}
714-
A5j[0, amm_i.aux[0]] = -1 # a_{j,0} coefficient
715-
# y = 1 + C_jS_j / s_0
716-
A5j[1, amm_i.shares_net] = -C[0] / s0 # delta_s coefficient
717-
b5j[1] = 1
718-
# z = An^n / D_0 sum(x_i^0 + B_i X_i) + (1 - An^n)(1 + C_jS_j / s_0)
719-
A5j[2, amm_i.shares_net] = D0_prime * C[0] / denom / s0
720-
for t in range(1, n_amm):
721-
A5j[2, amm_i.asset_net[t - 1]] = -B[t] / denom
722-
b5j[2] = 1
723-
cones5j = [cb.ExponentialConeT()]
724-
cones_count5j = [3]
725-
726-
for t in range(1, n_amm):
727-
x0 = amm.liquidity[amm.asset_list[t - 1]]
728-
if approx[t] == "linear":
729-
A5jt = np.zeros((1, k))
730-
A5jt[0, amm_i.aux[t]] = 1 # a_{j,t} coefficient
731-
A5jt[0, amm_i.shares_net] = C[0] / s0 # delta_s coefficient
732-
A5jt[0, amm_i.asset_net[t - 1]] = -B[t] / x0 # delta_x_i coefficient
733-
b5jt = np.array([0])
734-
cone5jt = cb.ZeroConeT(1)
735-
cone_count5jt = 1
736-
else:
737-
A5jt = np.zeros((3, k))
738-
b5jt = np.zeros(3)
739-
# x = a_{j,t}
740-
A5jt[0, amm_i.aux[t]] = -1
741-
# y = 1 + C_jS_j / s_0
742-
A5jt[1, amm_i.shares_net] = -C[0] / s0
743-
b5jt[1] = 1
744-
# z = (x_t^0 + B_t X_t) / D_0
745-
A5jt[2, amm_i.asset_net[t - 1]] = -B[t] / x0
746-
b5jt[2] = 1
747-
cone5jt = cb.ExponentialConeT()
748-
cone_count5jt = 3
749-
cones5j.append(cone5jt)
750-
cones_count5j.append(cone_count5jt)
751-
A5j = np.vstack([A5j, A5jt])
752-
b5j = np.append(b5j, np.array(b5jt))
753-
754-
A5j_final = np.zeros((1, k))
755-
for t in range(n_amm):
756-
A5j_final[0, amm_i.aux[t]] = -1
757-
b5j_final = np.array([0])
758-
A5j = np.vstack([A5j, A5j_final])
759-
b5j = np.append(b5j, b5j_final)
760-
cones5j.append(cb.NonnegativeConeT(1))
761-
cones_count5j.append(1)
762-
763-
return A5j, b5j, cones5j, cones_count5j
764-
765689
def _get_amm_bounds(p, indices_to_keep=None):
766690
# CFMM invariants must be respected
767691
n, u, m, N = p.n, p.u, p.m, p.N
@@ -772,16 +696,10 @@ def _get_amm_bounds(p, indices_to_keep=None):
772696
cones5 = []
773697
cones_count5 = []
774698
for j, amm in enumerate(p.amm_list):
775-
amm_i = p.amm_i[j]
776699
amm_constraints = p.amm_constraints[j]
777700
approx = p.get_amm_approx(j)
778-
if isinstance(amm, StableSwapPoolState):
779-
A5j, b5j, cones5j, cones_count5j = _get_stableswap_bounds(amm, amm_i, approx, k, p._scaling)
780-
elif isinstance(amm, ConstantProductPoolState):
781-
A5j_small, b5j, cones5j, cones_count5j = amm_constraints.get_amm_bounds(approx, p._scaling)
782-
A5j = _expand_submatrix(A5j_small, k, p.amm_i[j].shares_net)
783-
else:
784-
raise AssertionError("Unrecognized AMM type")
701+
A5j_small, b5j, cones5j, cones_count5j = amm_constraints.get_amm_bounds(approx, p._scaling)
702+
A5j = _expand_submatrix(A5j_small, k, p.amm_i[j].shares_net)
785703
A5 = np.vstack([A5, A5j])
786704
b5 = np.concatenate([b5, b5j])
787705
cones5 = cones5 + cones5j

hydradx/tests/test_solver/test_amm_constraints.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,47 @@ def test_get_xyk_bounds():
233233
s = b - A @ x
234234
if check_all_cone_feasibility(s, cones, cones_sizes, tol=0):
235235
raise AssertionError("Cone feasibility check should fail")
236+
237+
238+
# TODO test with auxiliary variable calculation
239+
# def test_get_stableswap_bounds():
240+
# amm = StableSwapPoolState(tokens={"A": 1_000_000, "B": 2_000_000}, amplification=100)
241+
# constraints = StableswapConstraints(amm)
242+
# scaling = {tkn: 1 for tkn in (amm.asset_list + [amm.unique_id])}
243+
# amm_i = constraints.amm_i
244+
#
245+
# for approx in ["Linear", "None"]:
246+
# A, b, cones, cones_sizes = constraints.get_amm_bounds(approx, scaling)
247+
# x = np.zeros(constraints.k)
248+
# # selling 5 B for 1 A should work
249+
# b_sell_amt, a_buy_amt = 5, 1
250+
# x[amm_i.asset_net[0]] = -a_buy_amt
251+
# x[amm_i.asset_net[1]] = b_sell_amt
252+
# x[amm_i.asset_out[0]] = a_buy_amt
253+
# s = b - A @ x
254+
# if not check_all_cone_feasibility(s, cones, cones_sizes, tol=0):
255+
# raise AssertionError("Cone feasibility check failed for valid Stableswap bounds")
256+
# # selling 1 A for 1 5 should not work
257+
# a_sell_amt, b_buy_amt = 1, 5
258+
# x[amm_i.asset_net[1]] = -b_buy_amt
259+
# x[amm_i.asset_net[0]] = a_sell_amt
260+
# x[amm_i.asset_out[1]] = b_buy_amt
261+
# s = b - A @ x
262+
# if check_all_cone_feasibility(s, cones, cones_sizes, tol=0):
263+
# raise AssertionError("Cone feasibility check should fail")
264+
# # selling 5 A for 1 B should work
265+
# a_sell_amt, b_buy_amt = 5, 1
266+
# x[amm_i.asset_net[1]] = -b_buy_amt
267+
# x[amm_i.asset_net[0]] = a_sell_amt
268+
# x[amm_i.asset_out[1]] = b_buy_amt
269+
# s = b - A @ x
270+
# if not check_all_cone_feasibility(s, cones, cones_sizes, tol=0):
271+
# raise AssertionError("Cone feasibility check should succeed")
272+
# # selling 1 B for 5 A should not work
273+
# b_sell_amt, a_buy_amt = 1, 5
274+
# x[amm_i.asset_net[0]] = -a_buy_amt
275+
# x[amm_i.asset_net[1]] = b_sell_amt
276+
# x[amm_i.asset_out[0]] = a_buy_amt
277+
# s = b - A @ x
278+
# if check_all_cone_feasibility(s, cones, cones_sizes, tol=0):
279+
# raise AssertionError("Cone feasibility check should fail")

hydradx/tests/test_solver/test_solver.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import random
44

55
import pytest
6-
from hypothesis import given, strategies as st, settings, Verbosity, Phase
6+
from hypothesis import given, strategies as st, settings, Verbosity, Phase, reproduce_failure
77

88
from hydradx.model.amm.agents import Agent
99
from hydradx.model.amm.omnipool_amm import OmnipoolState
@@ -343,9 +343,7 @@ def test_no_intent_arbitrage_stableswap_xyk_feeless(ratio):
343343
initial_state = OmnipoolState(tokens={tkn: {'liquidity': liquidity[tkn], 'LRNA': lrna[tkn]} for tkn in lrna})
344344

345345
sp_tokens = {"USDT": 7600000, "USDC": 9200000}
346-
stablepool = StableSwapPoolState(
347-
tokens=sp_tokens, amplification=1000, unique_id="2-Pool"
348-
)
346+
stablepool = StableSwapPoolState(tokens=sp_tokens, amplification=1000, unique_id="2-Pool")
349347

350348
xyk = ConstantProductPoolState(
351349
tokens={
@@ -360,24 +358,34 @@ def test_no_intent_arbitrage_stableswap_xyk_feeless(ratio):
360358
orig_op_price = initial_state.price( "DOT", "2-Pool") * stablepool.share_price("USDT")
361359
orig_xyk_price = xyk.liquidity['USDT'] / xyk.liquidity['DOT']
362360
max_delta = float('inf')
363-
while max_delta > 0:
361+
for i in range(100):
364362
x = find_solution_outer_approx(initial_state, intents, amm_list)
365363

366364
agent = Agent(enforce_holdings=False)
367365
if x['amm_deltas'][1][1] >= 0: # XYK pool is in index 1
368-
assert x['amm_deltas'][1][2] <= 0 # DOT bought from XYK
369-
xyk.swap(agent, tkn_sell = "USDT", tkn_buy = "DOT", sell_quantity = x['amm_deltas'][1][1])
370-
initial_state.swap(agent, tkn_buy = "2-Pool", tkn_sell = "DOT", sell_quantity = agent.get_holdings("DOT"))
371-
stablepool.remove_liquidity(agent, agent.get_holdings("2-Pool"), "USDT")
366+
if x['amm_deltas'][1][2] >= 0: # both deltas with same sign indicates rounding issues
367+
if x['amm_deltas'][1][2] / xyk.liquidity['DOT'] >= 0.0001:
368+
raise AssertionError("XYK pool should not be buying DOT")
369+
x['amm_deltas'] = [[0]]
370+
else:
371+
xyk.swap(agent, tkn_sell = "USDT", tkn_buy = "DOT", sell_quantity = x['amm_deltas'][1][1])
372+
initial_state.swap(agent, tkn_buy = "2-Pool", tkn_sell = "DOT", sell_quantity = agent.get_holdings("DOT"))
373+
stablepool.remove_liquidity(agent, agent.get_holdings("2-Pool"), "USDT")
372374
else:
373-
assert x['amm_deltas'][1][2] >= 0 # DOT sold to AMM
374-
xyk.swap(agent, tkn_sell="DOT", tkn_buy="USDT", sell_quantity=x['amm_deltas'][1][2])
375-
stablepool.add_liquidity(agent, agent.get_holdings("USDT"), "USDT")
376-
initial_state.swap(agent, tkn_buy="DOT", tkn_sell="2-Pool", sell_quantity=agent.get_holdings("2-Pool"))
375+
if x['amm_deltas'][1][2] < 0:
376+
if x['amm_deltas'][1][2] / xyk.liquidity['DOT'] <= -0.0001:
377+
raise AssertionError("XYK pool should not be selling DOT")
378+
x['amm_deltas'] = [[0]]
379+
else:
380+
xyk.swap(agent, tkn_sell="DOT", tkn_buy="USDT", sell_quantity=x['amm_deltas'][1][2])
381+
stablepool.add_liquidity(agent, agent.get_holdings("USDT"), "USDT")
382+
initial_state.swap(agent, tkn_buy="DOT", tkn_sell="2-Pool", sell_quantity=agent.get_holdings("2-Pool"))
377383

378384
op_price = initial_state.price( "DOT", "2-Pool") * stablepool.share_price("USDT")
379385
xyk_price = xyk.liquidity['USDT'] / xyk.liquidity['DOT']
380386
max_delta = max([max(y) for y in x['amm_deltas']])
387+
if max_delta <= 0:
388+
break
381389
if abs((op_price - xyk_price) / op_price) >= 0.005:
382390
raise AssertionError("XYK price should be close to Omnipool price")
383391

0 commit comments

Comments
 (0)