diff --git a/examples/Univ3.jl b/examples/Univ3.jl new file mode 100644 index 0000000..6c4f059 --- /dev/null +++ b/examples/Univ3.jl @@ -0,0 +1,31 @@ +#= +# Uniswap V3 Router +This example illustrates how to setup uniswapv3 pools +=# + +using CFMMRouter +using LinearAlgebra, SparseArrays, StaticArrays + +#= + The price in each tick refers to the lower bound on the interval + so the intervals are [t_i,t_i+1] with liquidity L_i +=# + +tick_list = [Dict("price" => .1, "liquidity" => 1.0), + Dict("price" => .5, "liquidity" => 1.0), + Dict("price" => 1.0, "liquidity" => 100.0), + Dict("price" => 2.0, "liquidity" => 1.0), + Dict("price" => 4.0, "liquidity" => 1.0)] + +pool = UniV3([100,100],1,[1,2],1.0,3,tick_list) +Δ = [0.0,0.0] +Λ = [0.0,0.0] + +find_arb!(Δ,Λ,pool, [1.0,2.0]) + +print(Δ,Λ) + +update_reserves!(pool, Δ, Λ, [1.0,2.0]) + +println(pool.current_price, pool.current_tick_index) + diff --git a/src/cfmms.jl b/src/cfmms.jl index d623529..67b09e2 100644 --- a/src/cfmms.jl +++ b/src/cfmms.jl @@ -1,5 +1,6 @@ -export CFMM, ProductTwoCoin, GeometricMeanTwoCoin +export CFMM, ProductTwoCoin, GeometricMeanTwoCoin, UniV3 export find_arb! +export update_reserves! abstract type CFMM{T} end @@ -190,3 +191,161 @@ function find_arb!(Δ::VT, Λ::VT, cfmm::GeometricMeanTwoCoin{T}, v::VT) where { Λ[2] = geom_arb_λ(v[2]/v[1], R[2], R[1], η, γ) return nothing end + + +#The price in each tick refers to the lower bound on the interval +#so the intervals are [t_i,t_i+1] with liquidity L_i +# p2--------- +# p1------ | p3------- +# | | | +# L1 L2 L3 +# | | | + +mutable struct UniV3{T} <: CFMM{T} + @add_two_coin_fields + current_price :: T + current_tick_index :: Int #This is the index of the maximal tick with price lower than current_price in the ticks dictionary + ticks #This is the tick mapping sorted by price + function UniV3(R,γ,idx,current_price,current_tick_index,ticks) + γ_T, idx_uint, T = two_coin_check_cast(R, γ, idx) + return new{T}( + MVector{2,T}(R), + γ_T, + MVector{2,UInt}(idx_uint), + current_price, + current_tick_index, + ticks + ) + end +end + +## See univ3 whitepaper +function virtual_reserves(P,L) + sP = sqrt(P) + x = L/sP + y = L*sP + return x,y +end + +function find_arb!(Δ::VT, Λ::VT, cfmm::UniV3{T}, v::VT) where {T, VT<:AbstractVector{T}} + current_price, current_tick_index, γ, ticks = cfmm.current_price, cfmm.current_tick_index, cfmm.γ, cfmm.ticks + Δ[1] = 0 + Δ[2] = 0 + + Λ[1] = 0 + Λ[2] = 0 + + target_price = v[1]/v[2] + # if (current_price*γ < target_price) && (target_price < current_price * 1/(γ)) + # return nothing + # end + #target_price = target_price/γ + if target_price >= current_price #iterate forwards in the tick mapping + i = 1 + while true + next_tick_price = 0.0 + try + next_tick_price = ticks[current_tick_index + i]["price"] + catch + ## Ran out of liquidity + break + end + if next_tick_price >= target_price ## so now we know that current_price <= target_price < next_tick_price + R = virtual_reserves( + max(current_price,ticks[current_tick_index + i - 1]["price"]), + ticks[current_tick_index + i - 1]["liquidity"] + ) + + k = R[1]*R[2] + + Δ[1] += prod_arb_δ(1/target_price, R[1], k, 1) + Δ[2] += prod_arb_δ(target_price, R[2], k, 1) + + Λ[1] += prod_arb_λ(target_price, R[1], k, 1) + Λ[2] += prod_arb_λ(1/target_price, R[2], k, 1) + + break + + elseif next_tick_price < target_price ## so now we know that current_price <= next_tick_price <= target_price + R = virtual_reserves(max(current_price,ticks[current_tick_index + i - 1]["price"]),ticks[current_tick_index + i - 1]["liquidity"]) + k = R[1]*R[2] + + Δ[1] += prod_arb_δ(1/next_tick_price, R[1], k, 1) + Δ[2] += prod_arb_δ(next_tick_price, R[2], k, 1) + + Λ[1] += prod_arb_λ(next_tick_price, R[1], k, 1) + Λ[2] += prod_arb_λ(1/next_tick_price, R[2], k, 1) + end + i += 1 + end + + elseif target_price < current_price #iterate backwards in the tick mapping + i = 1 + while true + prev_tick_price = 0.0 + try + prev_tick_price = ticks[current_tick_index - i + 1]["price"] + catch + # Ran out of liquidity + break + end + if prev_tick_price <= target_price ## so now we know that prev_tick_price < target_price <= current_price + R = [0.0,0.0] + try #it can happen that we are beyond the last tick and then this will have an indexing error in which case there is no liquidity + R = virtual_reserves(min(current_price,ticks[current_tick_index - i + 2]["price"]),ticks[current_tick_index - i + 1]["liquidity"]) + catch + break + end + k = R[1]*R[2] + + Δ[1] += prod_arb_δ(1/target_price, R[1], k, 1) + Δ[2] += prod_arb_δ(target_price, R[2], k, 1) + + Λ[1] += prod_arb_λ(target_price, R[1], k, 1) + Λ[2] += prod_arb_λ(1/target_price, R[2], k, 1) + + break + + elseif prev_tick_price > target_price ## so now we know that current_price <= next_tick_price <= target_price + R = [0.0,0.0] + try #it can happen that we are beyond the last tick and then this will have an indexing error error in which case there is no liquidity + R = virtual_reserves(min(current_price,ticks[current_tick_index - i + 2]["price"]),ticks[current_tick_index - i + 1]["liquidity"]) + catch + break + end + k = R[1]*R[2] + + Δ[1] += prod_arb_δ(1/prev_tick_price, R[1], k, 1) + Δ[2] += prod_arb_δ(prev_tick_price, R[2], k, 1) + + Λ[1] += prod_arb_λ(prev_tick_price, R[1], k, 1) + Λ[2] += prod_arb_λ(1/prev_tick_price, R[2], k, 1) + end + i += 1 + end + end + #Δ = Δ ./ γ + #Λ = Λ ./ γ + return nothing +end + +function update_reserves!(c :: CFMM, Δ, Λ, V) + c.R .+= Δ - Λ +end + +function update_reserves!(c :: UniV3, Δ, Λ, V) + c.R .+= Δ - Λ #update reserves + + if any(Δ .!= 0) || any(Λ .!= 0) #current_tick & current_price only if outside the no arb interval + target_price = V[1]/V[2] + c.current_price = target_price + + #there are much more efficient ways to do this e.g. binary search but this should work for now + for i in eachindex(c.ticks) + if (c.ticks[i]["price"]<= c.current_price) & (c.current_price < c.ticks[i+1]["price"]) + c.current_tick_index = i + break + end + end + end +end \ No newline at end of file diff --git a/src/objectives.jl b/src/objectives.jl index 2f16e9b..08a6da1 100644 --- a/src/objectives.jl +++ b/src/objectives.jl @@ -94,7 +94,7 @@ struct BasketLiquidation{T} <: Objective Δin::Vector{T} function BasketLiquidation(i::Integer, Δin::Vector{T}) where {T<:AbstractFloat} - !(i > 0 && i < length(Δin)) && throw(ArgumentError("Invalid index i")) + !(i > 0 && i <= length(Δin)) && throw(ArgumentError("Invalid index i")) return new{T}( i, Δin, diff --git a/src/router.jl b/src/router.jl index ed722db..c3916ec 100644 --- a/src/router.jl +++ b/src/router.jl @@ -127,8 +127,7 @@ end function update_reserves!(r::Router) for (Δ, Λ, c) in zip(r.Δs, r.Λs, r.cfmms) - c.R .+= Δ - Λ + update_reserves!(c, Δ,Λ,r.v[c.Ai]) end - return nothing end diff --git a/test/cfmms.jl b/test/cfmms.jl index 9b18b12..fb1225f 100644 --- a/test/cfmms.jl +++ b/test/cfmms.jl @@ -20,6 +20,7 @@ function optimality_conditions_met(c, Δ, Λ, cfmm; cache=nothing) opt = maximum(i -> γ * ∇ϕR[i] / c[i], 1:n) ≤ minimum(i -> ∇ϕR[i] / c[i], 1:n) + sqrt(eps()) return pfeas && cfmm_sat && opt end + @testset "CFMMs" begin @testset "arbitrage checks: two coins" begin Δ = MVector{2, Float64}(undef) @@ -70,6 +71,164 @@ end @test optimality_conditions_met(ν, Δ, Λ, cfmm; cache=cache) end end + + +end end +@testset "univ3" begin + + + #this test is going to be one were there is no arbitrage because the current_price = target_price + #there can be some slight numerical error when calculating virtual_reserves + + # #reserves dont matter for univ3 pools, only tick data so just setting to 1 + R = [1.0,1.0] + # #no fees for now + γ = .997 + # # + ids = [1.0,2.0] + current_price = 15.0 + current_tick_index = 3.0 + ticks = [Dict("price" => 1/2.0^64,"liquidity" => 0.0), + Dict("price" => 5.0,"liquidity" => 1.0), + Dict("price" => 10.0, "liquidity" => 2.0), + Dict("price" => 20.0, "liquidity" => 1.0), + Dict("price" => 30.0, "liquidity" => 0.0), + Dict("price" => 2.0^64, "liquidity" => 0)] + + cfmm = UniV3(R,γ,ids, current_price, current_tick_index, ticks) + + Δ = [0.0,0.0] + Λ = [0.0,0.0] + v = [15.0,1.0] + find_arb!(Δ,Λ,cfmm,v) + + @test (norm(Δ) <= 1e-12) && (norm(Λ) <= 1e-12) #nothing happens + + + #In this test the target price is higher than the current_price but we are still within one tick + + Δ = [0.0,0.0] + Λ = [0.0,0.0] + v = [18.0,1.0] + find_arb!(Δ,Λ,cfmm,v) + + @test (Δ[1] == 0) # no token 0 in + @test (Δ[2] > 0) # posiive token 1 in + @test (Λ[1] > 0) # positive token 0 out + @test (Λ[2] == 0) # no token 1 out + @test (16 <= Δ[2]/Λ[1]) && (Δ[2]/Λ[1] <= 17) #Average Price paid is between 16 and 17 + + # now we cross a tick going up + + Δ = [0.0,0.0] + Λ = [0.0,0.0] + v = [22.0,1.0] + find_arb!(Δ,Λ,cfmm,v) + + @test (Δ[1] == 0) # no token 0 in + @test (Δ[2] > 0) # posiive token 1 in + @test (Λ[1] > 0) # positive token 0 out + @test (Λ[2] == 0) # no token 1 out + + @test (17 <= Δ[2]/Λ[1]) && (Δ[2]/Λ[1] <= 18) #Average Price paid is between 17 and 18 + + + #out of bounds test upper + Δ = [0.0,0.0] + Λ = [0.0,0.0] + v = [29.99,1.0] + find_arb!(Δ,Λ,cfmm,v) + + @test (Δ[1] == 0) # no token 0 in + @test (Δ[2] > 0) # posiive token 1 in + @test (Λ[1] > 0) # positive token 0 out + @test (Λ[2] == 0) # no token 1 out + + + @test (19.5 <= Δ[2]/Λ[1]) && (Δ[2]/Λ[1] <= 20.5) + + Δ = [0.0,0.0] + Λ = [0.0,0.0] + v = [35,1.0] + find_arb!(Δ,Λ,cfmm,v) + + @test (Δ[1] == 0) # no token 0 in + @test (Δ[2] > 0) # posiive token 1 in + @test (Λ[1] > 0) # positive token 0 out + @test (Λ[2] == 0) # no token 1 out + + + @test (19.5 <= Δ[2]/Λ[1]) && (Δ[2]/Λ[1] <= 20.5) + + + #Now we test when target price is below current price but within current tick + + #starting with just below current price + Δ = [0.0,0.0] + Λ = [0.0,0.0] + v = [14.99999,1.0] #basically current price but less than so we check the other branch + find_arb!(Δ,Λ,cfmm,v) + + + + @test (norm(Δ) <= 1e-3) && (norm(Λ) <= 1e-3) #nothing happens + + + #now below current price but not below current tick price + + Δ = [0.0,0.0] + Λ = [0.0,0.0] + v = [12.0,1.0] + find_arb!(Δ,Λ,cfmm,v) + + @test (Δ[1] > 0) # positive token 0 in + @test (Δ[2] == 0) # no token 1 in + @test (Λ[1] == 0) # no 0 out + @test (Λ[2] > 0) # positive 1 out + + @test (13 <= Λ[2]/Δ[1]) && (Λ[2]/Δ[1] <= 14) #Average Price paid is between 13 and 14 + + #now below current price and crossing a tick + + Δ = [0.0,0.0] + Λ = [0.0,0.0] + v = [5.01,1.0] + find_arb!(Δ,Λ,cfmm,v) + + + @test (Δ[1] > 0) # positive token 0 in + @test (Δ[2] == 0) # no token 1 in + @test (Λ[1] == 0) # no 0 out + @test (Λ[2] > 0) # positive 1 out + + @test (9 <= Λ[2]/Δ[1]) && (Λ[2]/Δ[1] <= 10) #Average Price paid is between 9 and 10 + + #now out of bounds test + + Δ = [0.0,0.0] + Λ = [0.0,0.0] + v = [4.0,1.0] + find_arb!(Δ,Λ,cfmm,v) + + + @test (Δ[1] > 0) # positive token 0 in + @test (Δ[2] == 0) # no token 1 in + @test (Λ[1] == 0) # no 0 out + @test (Λ[2] > 0) # positive 1 out + + @test (9 <= Λ[2]/Δ[1]) && (Λ[2]/Δ[1] <= 10) #Average Price paid is between 13 and 14 + + + #update_reserves for univ3 test + Δ = [0.0,0.0] + Λ = [0.0,0.0] + v = [18.0,1.0] + find_arb!(Δ,Λ,cfmm,v) + + update_reserves!(cfmm, Δ, Λ, v) + + @test (cfmm.current_price == 18.0) + @test (cfmm.current_tick_index == 3) end \ No newline at end of file