Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions examples/Univ3.jl
Original file line number Diff line number Diff line change
@@ -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)

161 changes: 160 additions & 1 deletion src/cfmms.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export CFMM, ProductTwoCoin, GeometricMeanTwoCoin
export CFMM, ProductTwoCoin, GeometricMeanTwoCoin, UniV3
export find_arb!
export update_reserves!

abstract type CFMM{T} end

Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion src/objectives.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions src/router.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
159 changes: 159 additions & 0 deletions test/cfmms.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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