Skip to content

Commit

Permalink
WIP added an initial version of a MIP solver to handle trades that ca…
Browse files Browse the repository at this point in the history
…nnot be partially executed. Will need integration with our existing solver.
  • Loading branch information
poliwop committed Oct 22, 2024
1 parent 8564d09 commit a55c262
Show file tree
Hide file tree
Showing 2 changed files with 271 additions and 1 deletion.
163 changes: 163 additions & 0 deletions hydradx/model/amm/omnix_solver_simple.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import copy
import math

import clarabel
import numpy as np
import cvxpy as cp
import clarabel as cb
import highspy
from scipy import sparse

from hydradx.model.amm.omnipool_amm import OmnipoolState
Expand Down Expand Up @@ -592,6 +594,167 @@ def _find_solution_unrounded3(
return new_amm_deltas, exec_intent_deltas


def _solve_inclusion_problem(
state: OmnipoolState,
intents: list,
# old_IC,
# old_IC_upper,
# old_S,
# old_s
):

asset_list = []
for intent in intents:
if intent['tkn_sell'] != "LRNA" and intent['tkn_sell'] not in asset_list:
asset_list.append(intent['tkn_sell'])
if intent['tkn_sell'] != "LRNA" and intent['tkn_buy'] not in asset_list:
asset_list.append(intent['tkn_buy'])
tkn_list = ["LRNA"] + asset_list

partial_intents = [i for i in intents if i['partial']]
full_intents = [i for i in intents if not i['partial']]
n = len(asset_list)
m = len(partial_intents)
r = len(full_intents)
k = 4 * n + m + r

fees = [float(state.last_fee[tkn]) for tkn in asset_list] # f_i
lrna_fees = [float(state.last_lrna_fee[tkn]) for tkn in asset_list] # l_i
fee_match = 0.0005
assert fee_match <= min(fees) # breaks otherwise

partial_intent_prices = [float(intent['buy_quantity'] / intent['sell_quantity']) for intent in partial_intents]
full_intent_prices = [float(intent['buy_quantity'] / intent['sell_quantity']) for intent in full_intents]

# calculate tau, phi
scaling, net_supply, net_demand = _calculate_scaling(intents, state, asset_list)
tau1, phi1 = _calculate_tau_phi(partial_intents, tkn_list, scaling)
tau2, phi2 = _calculate_tau_phi(full_intents, tkn_list, scaling)
tau = sparse.hstack([tau1, tau2]).toarray()
phi = sparse.hstack([phi1, phi2]).toarray()
# epsilon_tkn = {t: max([net_supply[t], net_demand[t]]) / state.liquidity[t] for t in asset_list}

# we start with the 4n + m variables from the initial problem
# then we add r indicator variables for the r non-partial intents
#----------------------------#
# OBJECTIVE #
#----------------------------#

y_coefs = np.ones(n)
x_coefs = np.zeros(n)
lrna_lambda_coefs = np.array(lrna_fees)
lambda_coefs = np.zeros(n)
d_coefs = np.array([-tau[0,j] for j in range(m)])
I_coefs = np.array([-tau[0,m+l]*full_intents[l]['sell_quantity']/scaling["LRNA"] for l in range(r)])
c = np.concatenate([y_coefs, x_coefs, lrna_lambda_coefs, lambda_coefs, d_coefs, I_coefs])

# bounds on variables
# y, x are unbounded
# lrna_lambda, lambda >= 0
# 0 <= d <= max
# 0 <= I <= 1

inf = highspy.kHighsInf

lower = np.array([-inf] * 2 * n + [0] * (2 * n + m + r))
partial_intent_sell_amts = [i['sell_quantity']/scaling[i['tkn_sell']] for i in partial_intents]
upper = np.array([inf] * 4 * n + partial_intent_sell_amts + [1] * r)

# we will temporarily assume a 0 solution is latest, and linearize g() around that.
s = np.zeros(n)
S = np.zeros((n, k))
S_upper = np.zeros(n)
S_lower = np.array([-inf]*n)
for i in range(n):
S[i, i] = -scaling["LRNA"] * state.liquidity[asset_list[i]]
S[i, n+i] = -scaling[asset_list[i]] * state.lrna[asset_list[i]]

# for now we will not include any integer cuts

# asset leftover must be above zero
A3 = np.zeros((n+1, k))
A3[0, :] = c
for i in range(n):
A3[i+1, n+i] = 1
A3[i+1, 3*n+i] = fees[i] - fee_match
for j in range(m):
A3[i+1, 4*n+j] = 1/(1-fee_match)*phi[i+1, j] * partial_intent_prices[j] - tau[i+1, j]
for l in range(r):
buy_amt = 1 / (1 - fee_match) * phi[i+1, l] * full_intents[l]['buy_quantity'] * scaling[full_intents[l]['tkn_buy']] / scaling[full_intents[l]['tkn_sell']]
sell_amt = tau[i + 1, l] * full_intents[l]['sell_quantity']
A3[i+1, 4*n+m+l] = (buy_amt - sell_amt)/scaling[asset_list[i]]
A3_upper = np.zeros(n+1)
A3_lower = np.array([-inf]*(n+1))

# sum of lrna_lambda and y_i should be non-negative
# sum of lambda and x_i should be non-negative
A5 = np.zeros((2 * n, k))
for i in range(n):
A5[i, i] = 1
A5[i, 2 * n + i] = 1
A5[n + i, n + i] = 1
A5[n + i, 3 * n + i] = 1
A5_upper = np.array([inf] * 2 * n)
A5_lower = np.zeros(2 * n)

A = np.vstack([S, A3, A5])
A_upper = np.concatenate([S_upper, A3_upper, A5_upper])
A_lower = np.concatenate([S_lower, A3_lower, A5_lower])

nonzeros = []
start = [0]
a = []
for i in range(A.shape[0]):
row_nonzeros = np.where(A[i, :] != 0)[0]
nonzeros.extend(row_nonzeros)
start.append(len(nonzeros))
a.extend(A[i, row_nonzeros])
h = highspy.Highs()
lp = highspy.HighsLp()

lp.num_col_ = k
lp.num_row_ = A.shape[0]

lp.col_cost_ = c
lp.col_lower_ = lower
lp.col_upper_ = upper
lp.row_lower_ = A_lower
lp.row_upper_ = A_upper

lp.a_matrix_.format_ = highspy.MatrixFormat.kRowwise
lp.a_matrix_.start_ = start
lp.a_matrix_.index_ = nonzeros
lp.a_matrix_.value_ = a

lp.integrality_ = np.array([highspy.HighsVarType.kContinuous] * (4*n + m) + [highspy.HighsVarType.kInteger] * r)

h.passModel(lp)
h.run()
solution = h.getSolution()
info = h.getInfo()
basis = h.getBasis()

x_expanded = solution.col_value

new_amm_deltas = {}
exec_partial_intent_deltas = [None] * len(partial_intents)
exec_full_intent_flags = [None] * len(full_intents)

for i in range(n):
tkn = tkn_list[i+1]
new_amm_deltas[tkn] = x_expanded[n+i] * scaling[tkn]

for i in range(m):
exec_partial_intent_deltas[i] = -x_expanded[4 * n + i] * scaling[intents[i]['tkn_sell']]
for i in range(r):
exec_full_intent_flags[i] = x_expanded[4 * n + m + i]

return new_amm_deltas, exec_partial_intent_deltas, exec_full_intent_flags





def round_solution(intents, intent_deltas, tolerance=0.0001):
deltas = []
for i in range(len(intent_deltas)):
Expand Down
109 changes: 108 additions & 1 deletion hydradx/tests/test_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,4 +262,111 @@ def test_solver_with_real_omnipool():
intent_deltas2 = [[-mpf(100), mpf(1.149)], [0, 0]]
assert validate_and_execute_solution(initial_state.copy(), copy.deepcopy(intents), intent_deltas2)

pprint(intent_deltas)
pprint(intent_deltas)


def test_MIP_solver_with_real_omnipool():
from hydradx.model.amm.omnix_solver_simple import _solve_inclusion_problem
agents = [
Agent(holdings={'HDX': 100}),
Agent(holdings={'HDX': 100}),
]

intents = [
# {'sell_quantity': mpf(100), 'buy_quantity': mpf(1.149711278057), 'tkn_sell': 'HDX', 'tkn_buy': 'CRU', 'agent': agents[0]},
# {'sell_quantity': mpf(1.149711278057), 'buy_quantity': mpf(100), 'tkn_sell': 'CRU', 'tkn_buy': 'HDX', 'agent': agents[1]},
{'sell_quantity': mpf(100), 'buy_quantity': mpf(1.149), 'tkn_sell': 'HDX', 'tkn_buy': 'CRU', 'agent': agents[0], 'partial': False},
{'sell_quantity': mpf(100), 'buy_quantity': mpf(1.149), 'tkn_sell': 'CRU', 'tkn_buy': 'HDX', 'agent': agents[1], 'partial': True},
# {'sell_quantity': mpf(100), 'buy_quantity': mpf(200.0), 'tkn_sell': 'HDX', 'tkn_buy': 'CRU', 'agent': agents[1],
# 'partial': True},
# {'sell_quantity': mpf(100), 'buy_quantity': mpf(1.25359), 'tkn_sell': 'HDX', 'tkn_buy': 'CRU',
# 'agent': agents[0]},
# {'sell_quantity': mpf(1.25361), 'buy_quantity': mpf(100), 'tkn_sell': 'CRU', 'tkn_buy': 'HDX',
# 'agent': agents[1]}
]

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}

intent_deltas = _solve_inclusion_problem(initial_state, intents)

assert validate_and_execute_solution(initial_state.copy(), copy.deepcopy(intents), intent_deltas)
intent_deltas2 = [[-mpf(100), mpf(1.149)], [0, 0]]
assert validate_and_execute_solution(initial_state.copy(), copy.deepcopy(intents), intent_deltas2)

pprint(intent_deltas)


def test_MIP_solver():
import highspy
import numpy as np

# Create HiGHS model
h = highspy.Highs()
lp = highspy.HighsLp()
inf = highspy.kHighsInf

lp.num_col_ = 2
lp.num_row_ = 3

lp.col_cost_ = np.array([1, 1], dtype=np.double) # c^T
lp.col_lower_ = np.array([0, 1], dtype=np.double) # l
lp.col_upper_ = np.array([4, inf], dtype=np.double) # u
lp.row_lower_ = np.array([-inf, 5, 6], dtype=np.double) # L
lp.row_upper_ = np.array([7, 15, inf], dtype=np.double) # U
# In a HighsLp instsance, the number of nonzeros is given by a fictitious final start
lp.a_matrix_.start_ = np.array([0, 2, 5])
lp.a_matrix_.index_ = np.array([1, 2, 0, 1, 2])
lp.a_matrix_.value_ = np.array([1, 3, 1, 2, 2], dtype=np.double)
h.passModel(lp)

h.version()

output_flag = h.getOptionValue("output_flag")
print("output_flag:", output_flag)

output_flag = h.getOptionValue("log_to_console")
print("log_to_console:", output_flag)

output_flag = h.getOptionValue("log_dev_level")
print("log_dev_level:", output_flag)

h.setOptionValue('output_flag', False)

h.setOptionValue('log_dev_level', 1)

h.setOptionValue('output_flag', True)

h.run()

solution = h.getSolution()
basis = h.getBasis()
info = h.getInfo()
model_status = h.getModelStatus()
print('Model status = ', h.modelStatusToString(model_status))
print()
print('Optimal objective = ', info.objective_function_value)
print('Iteration count = ', info.simplex_iteration_count)
print('Primal solution status = ', h.solutionStatusToString(info.primal_solution_status))
print('Dual solution status = ', h.solutionStatusToString(info.dual_solution_status))
print('Basis validity = ', h.basisValidityToString(info.basis_validity))

0 comments on commit a55c262

Please sign in to comment.