Part I of the Convex Optimization series introduced convex functions, gave a brief introduction to CVXPY, and capped the lesson with a demonstration of modeling the Uniswap x*y=k invariant to reproduce an exact input swap calc, and to optimize a two-pool arbitrage.
To simplify the concept, I did not model fees and used discrete variables for the three relevant amounts (forward token quantity, profit token in, profit token out).
If we’re going to use convex optimization for more complex problems, we should make the calculation accurate and generalize the method wherever possible.
Modeling Fees
The standard 0.3% fee for a Uniswap V2 pool is applied to the swap input amount. Before completing the swap, the pool verifies that the new reserves (with the fee amount withheld) satisfy the invariant.
The previous lesson presented the geo_mean()
constraint as a way to enforce the invariant. It is still valid here, but there is some subtlety involved. The invariant is applied to the post-swap amounts, not the final reserves. The previous lesson ignored fees completely, so the post-swap k is equal to the final k. But when accounting for non-zero fees, the relationship is:
We can model this within a constraint by multiplying the input amount by a fee scalar before evaluating the invariant:
uniswap_v2_convex_amount_out.py
import cvxpy
fee = 0.997
x0 = 100_000
y0 = 50_000
delta_x = 1_000
delta_y = cvxpy.Variable() # Amount of Y to withdraw
# Define objective (maximize profit in terms of token X)
objective = cvxpy.Maximize(delta_y)
# Define constraints
constraints = [
(x0 + fee * delta_x) * (y0 - delta_y) >= (x0 * y0),
]
# Define and solve the problem
problem = cvxpy.Problem(objective, constraints)
problem.solve(verbose=False)
print(f"Solved. delta_y={delta_y.value}")
0.3% can be converted to a decimal value by dividing by 100, so the fee withheld is 0.003. Thus the input amount applied to the swap is (1 - 0.003) = 0.997.
The 0.997 float value is fine for this demonstration, but I prefer to use Python’s Fraction class when working with fees and will do so from here on.
Running again gives the following result:
Solved. delta_y=493.5790171949477
This is slightly lower than the result from the previous lesson (495.04).
Vectorizing Deposits and Withdrawals
In the last lesson, I used CVXPY variables to represent scalar values. But a variable can be multi-dimensional — a vector or a matrix.
If you’re unfamiliar with these terms, spend time reading the first few sections of the Wikipedia page.
The idea of a swap can be expressed with six discrete values:
We typically swap one token for another, so one of the deposits will be zero and one of the withdrawls will be zero.
Instead of six values, these values can be collapsed into three two-dimensional values, with the x axis used for amounts of token0, and the y axis used for amounts of token1.
Rewritten as row vectors:
A vector variable in CVXPY is created by passing the shape
argument, which can be a number or a tuple. A number n will create a row vector of length n, and a tuple (m,n) will create a matrix of dimensions m x n.
A CVXPY variable can be created with at most one special built-in constraint. We want to enforce the assumption that the withdrawal vector cannot contain any negative values, but zeros are acceptable. Thus the nonneg=True
constraint is appropriate.
Let’s rewrite the exact input calculation from the previous lesson using numpy arrays for constants, and CVXPY variables of shape 2 for the withdrawal vector: