Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: improve matching speed and separate amm package #163

Merged
merged 28 commits into from
Feb 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
e8abc31
feat: implement HighestTick
hallazzang Feb 9, 2022
2326028
chore: Merge remote-tracking branch 'origin/148-highest-tick' into 14…
hallazzang Feb 10, 2022
124f311
fix: Up/DownTick no more guarantees that the price fits into ticks
hallazzang Feb 10, 2022
4ba4b65
fix: fix bug in BuyAmountOnTick and fix Highest/LowestTick
hallazzang Feb 10, 2022
00530a4
fix: fix Highest/LowestTick
hallazzang Feb 10, 2022
66ccaf7
fix: cache Highest/LowestTick and fix Up/DownTick
hallazzang Feb 10, 2022
752ebd0
fix: fix PoolOrderSource
hallazzang Feb 10, 2022
e6fd64f
fix: use binary search to find match price
hallazzang Feb 10, 2022
ffa9382
feat: add amm package
hallazzang Feb 12, 2022
f771651
feat: implement OrderBook
hallazzang Feb 12, 2022
723982b
feat: add MergeOrderSources
hallazzang Feb 12, 2022
dffa358
refactor: adopt amm package
hallazzang Feb 12, 2022
5360897
chore: Merge branch 'main' of github.com:cosmosquad-labs/squad into 1…
hallazzang Feb 12, 2022
ab6b9e4
fix: do not expire swap request when it is completed
hallazzang Feb 12, 2022
99eacec
fix: fill OfferCoinAmount field of PoolOrder
hallazzang Feb 12, 2022
e243b13
fix: make pool not to make an order when amount is zero
hallazzang Feb 12, 2022
35f93f3
fix: fix minor bugs
hallazzang Feb 12, 2022
25c0149
refactor!: move utility functions into types pacakge
hallazzang Feb 12, 2022
62b959d
test: revert back pool tests
hallazzang Feb 12, 2022
2ac3068
fix: Price doesn't care about pool coin supply
hallazzang Feb 12, 2022
d0ba942
test: add pool tests
hallazzang Feb 12, 2022
52be39e
fix: use pointer of BaseOrder
hallazzang Feb 12, 2022
3475f27
test: add order book tests and OfferCoinAmount helper
hallazzang Feb 12, 2022
25347e2
fix: rename PriceToTick to PriceToDownTick
hallazzang Feb 12, 2022
5307efb
refactor: move matching algorithm to amm package
hallazzang Feb 12, 2022
e3d4c75
refactor: Order holds sdk.Coin instead of sdk.Int
hallazzang Feb 12, 2022
9303fd1
feat: add MockPoolOrderSource
hallazzang Feb 12, 2022
37f0c44
test: add benchmark test for FindMatchPrice
hallazzang Feb 12, 2022
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
41 changes: 41 additions & 0 deletions types/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,44 @@ func MustParseRFC3339(s string) time.Time {
func DateRangesOverlap(startTimeA, endTimeA, startTimeB, endTimeB time.Time) bool {
return startTimeA.Before(endTimeB) && endTimeA.After(startTimeB)
}

// ParseDec is a shortcut for sdk.MustNewDecFromStr.
func ParseDec(s string) sdk.Dec {
return sdk.MustNewDecFromStr(s)
}

// ParseCoin parses and returns sdk.Coin.
func ParseCoin(s string) sdk.Coin {
coin, err := sdk.ParseCoinNormalized(s)
if err != nil {
panic(err)
}
return coin
}

// ParseCoins parses and returns sdk.Coins.
func ParseCoins(s string) sdk.Coins {
coins, err := sdk.ParseCoinsNormalized(s)
if err != nil {
panic(err)
}
return coins
}

// ParseTime parses and returns time.Time in time.RFC3339 format.
func ParseTime(s string) time.Time {
t, err := time.Parse(time.RFC3339, s)
if err != nil {
panic(err)
}
return t
}

// DecApproxEqual returns true if a and b are approximately equal,
// which means the diff ratio is equal or less than 0.1%.
func DecApproxEqual(a, b sdk.Dec) bool {
if b.GT(a) {
a, b = b, a
}
return a.Sub(b).Quo(a).LTE(sdk.NewDecWithPrec(1, 3))
}
178 changes: 178 additions & 0 deletions x/liquidity/amm/match.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package amm

import (
"sort"

sdk "github.com/cosmos/cosmos-sdk/types"
)

type PriceDirection int

const (
PriceStaying PriceDirection = iota + 1
PriceIncreasing
PriceDecreasing
)

func InitialMatchPrice(os OrderSource, tickPrec int) (matchPrice sdk.Dec, dir PriceDirection, matchable bool) {
highest, found := os.HighestBuyPrice()
if !found {
return sdk.Dec{}, 0, false
}
lowest, found := os.LowestSellPrice()
if !found {
return sdk.Dec{}, 0, false
}
if highest.LT(lowest) {
return sdk.Dec{}, 0, false
}

midPrice := highest.Add(lowest).QuoInt64(2)
buyAmt := os.BuyAmountOver(midPrice)
sellAmt := os.SellAmountUnder(midPrice)
switch {
case buyAmt.GT(sellAmt):
dir = PriceIncreasing
case sellAmt.GT(buyAmt):
dir = PriceDecreasing
default:
dir = PriceStaying
}

switch dir {
case PriceStaying:
matchPrice = RoundPrice(midPrice, tickPrec)
case PriceIncreasing:
matchPrice = DownTick(midPrice, tickPrec)
case PriceDecreasing:
matchPrice = UpTick(midPrice, tickPrec)
}

return matchPrice, dir, true
}

func FindMatchPrice(os OrderSource, tickPrec int) (matchPrice sdk.Dec, found bool) {
initialMatchPrice, dir, matchable := InitialMatchPrice(os, tickPrec)
if !matchable {
return sdk.Dec{}, false
}
if dir == PriceStaying {
return initialMatchPrice, true
}

buyAmtOver := func(i int) sdk.Int {
return os.BuyAmountOver(TickFromIndex(i, tickPrec))
}
sellAmtUnder := func(i int) sdk.Int {
return os.SellAmountUnder(TickFromIndex(i, tickPrec))
}

switch dir {
case PriceIncreasing:
start := TickToIndex(initialMatchPrice, tickPrec)
end := TickToIndex(HighestTick(tickPrec), tickPrec)
i := start + sort.Search(end-start+1, func(i int) bool {
i += start
bg := buyAmtOver(i + 1)
return bg.IsZero() || (bg.LTE(sellAmtUnder(i)) && buyAmtOver(i).GT(sellAmtUnder(i-1)))
})
if i > end {
i = end
}
return TickFromIndex(i, tickPrec), true
default: // PriceDecreasing
start := TickToIndex(initialMatchPrice, tickPrec)
end := TickToIndex(LowestTick(tickPrec), tickPrec)
i := start - sort.Search(start-end+1, func(i int) bool {
i = start - i
sl := sellAmtUnder(i - 1)
return sl.IsZero() || (buyAmtOver(i+1).LTE(sellAmtUnder(i)) && buyAmtOver(i).GTE(sl))
})
if i < end {
i = end
}
return TickFromIndex(i, tickPrec), true
}
}

func FindLastMatchableOrders(buyOrders, sellOrders []Order, matchPrice sdk.Dec) (lastBuyIdx, lastSellIdx int, lastBuyPartialMatchAmt, lastSellPartialMatchAmt sdk.Int, found bool) {
if len(buyOrders) == 0 || len(sellOrders) == 0 {
return 0, 0, sdk.Int{}, sdk.Int{}, false
}
type Side struct {
orders []Order
totalOpenAmt sdk.Int
i int
partialMatchAmt sdk.Int
}
buySide := &Side{buyOrders, TotalOpenAmount(buyOrders), len(buyOrders) - 1, sdk.Int{}}
sellSide := &Side{sellOrders, TotalOpenAmount(sellOrders), len(sellOrders) - 1, sdk.Int{}}
sides := map[OrderDirection]*Side{
Buy: buySide,
Sell: sellSide,
}
for {
ok := true
for dir, side := range sides {
i := side.i
order := side.orders[i]
matchAmt := sdk.MinInt(buySide.totalOpenAmt, sellSide.totalOpenAmt)
side.partialMatchAmt = matchAmt.Sub(side.totalOpenAmt.Sub(order.GetOpenAmount()))
if side.totalOpenAmt.Sub(order.GetOpenAmount()).GT(matchAmt) ||
(dir == Sell && matchPrice.MulInt(side.partialMatchAmt).TruncateInt().IsZero()) {
if i == 0 {
return
}
side.totalOpenAmt = side.totalOpenAmt.Sub(order.GetOpenAmount())
side.i--
ok = false
}
}
if ok {
return buySide.i, sellSide.i, buySide.partialMatchAmt, sellSide.partialMatchAmt, true
}
}
}

func MatchOrders(buyOrders, sellOrders []Order, matchPrice sdk.Dec) (quoteCoinDust sdk.Int, matched bool) {
bi, si, pmb, pms, found := FindLastMatchableOrders(buyOrders, sellOrders, matchPrice)
if !found {
return sdk.Int{}, false
}

quoteCoinDust = sdk.ZeroInt()

for i := 0; i <= bi; i++ {
buyOrder := buyOrders[i]
var receivedBaseCoinAmt sdk.Int
if i < bi {
receivedBaseCoinAmt = buyOrder.GetOpenAmount()
} else {
receivedBaseCoinAmt = pmb
}
paidQuoteCoinAmt := matchPrice.MulInt(receivedBaseCoinAmt).Ceil().TruncateInt()
buyOrder.SetOpenAmount(buyOrder.GetOpenAmount().Sub(receivedBaseCoinAmt))
buyOrder.DecrRemainingOfferCoin(paidQuoteCoinAmt)
buyOrder.IncrReceivedDemandCoin(receivedBaseCoinAmt)
buyOrder.SetMatched(true)
quoteCoinDust = quoteCoinDust.Add(paidQuoteCoinAmt)
}

for i := 0; i <= si; i++ {
sellOrder := sellOrders[i]
var paidBaseCoinAmt sdk.Int
if i < si {
paidBaseCoinAmt = sellOrder.GetOpenAmount()
} else {
paidBaseCoinAmt = pms
}
receivedQuoteCoinAmt := matchPrice.MulInt(paidBaseCoinAmt).TruncateInt()
sellOrder.SetOpenAmount(sellOrder.GetOpenAmount().Sub(paidBaseCoinAmt))
sellOrder.DecrRemainingOfferCoin(paidBaseCoinAmt)
sellOrder.IncrReceivedDemandCoin(receivedQuoteCoinAmt)
sellOrder.SetMatched(true)
quoteCoinDust = quoteCoinDust.Sub(receivedQuoteCoinAmt)
}

return quoteCoinDust, true
}
49 changes: 49 additions & 0 deletions x/liquidity/amm/match_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package amm_test

import (
"fmt"
"math/big"
"math/rand"
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"

squad "github.com/cosmosquad-labs/squad/types"
"github.com/cosmosquad-labs/squad/x/liquidity/amm"
)

func randInt(r *rand.Rand, min, max sdk.Int) sdk.Int {
return min.Add(sdk.NewIntFromBigInt(new(big.Int).Rand(r, max.Sub(min).BigInt())))
}

func randDec(r *rand.Rand, min, max sdk.Dec) sdk.Dec {
return min.Add(sdk.NewDecFromBigIntWithPrec(new(big.Int).Rand(r, max.Sub(min).BigInt()), sdk.Precision))
}

func BenchmarkFindMatchPrice(b *testing.B) {
minPrice, maxPrice := squad.ParseDec("0.0000001"), squad.ParseDec("10000000")
minAmt, maxAmt := sdk.NewInt(100), sdk.NewInt(10000000)
minReserveAmt, maxReserveAmt := sdk.NewInt(500), sdk.NewInt(1000000000)

for seed := int64(0); seed < 5; seed++ {
b.Run(fmt.Sprintf("seed/%d", seed), func(b *testing.B) {
r := rand.New(rand.NewSource(seed))
ob := amm.NewOrderBook()
for i := 0; i < 10000; i++ {
ob.Add(newOrder(amm.Buy, randDec(r, minPrice, maxPrice), randInt(r, minAmt, maxAmt)))
ob.Add(newOrder(amm.Sell, randDec(r, minPrice, maxPrice), randInt(r, minAmt, maxAmt)))
}
var poolOrderSources []amm.OrderSource
for i := 0; i < 1000; i++ {
rx, ry := randInt(r, minReserveAmt, maxReserveAmt), randInt(r, minReserveAmt, maxReserveAmt)
pool := amm.NewBasicPool(rx, ry, sdk.ZeroInt())
poolOrderSources = append(poolOrderSources, amm.NewMockPoolOrderSource(pool, "denom1", "denom2"))
}
os := amm.MergeOrderSources(append(poolOrderSources, ob)...)
b.ResetTimer()
for i := 0; i < b.N; i++ {
amm.FindMatchPrice(os, defTickPrec)
}
})
}
}
118 changes: 118 additions & 0 deletions x/liquidity/amm/order.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package amm

import (
"fmt"

sdk "github.com/cosmos/cosmos-sdk/types"
)

var _ Order = (*BaseOrder)(nil)

type OrderDirection int

const (
Buy OrderDirection = iota + 1
Sell
)

func (dir OrderDirection) String() string {
switch dir {
case Buy:
return "buy"
case Sell:
return "sell"
default:
return fmt.Sprintf("OrderDirection(%d)", dir)
}
}

type Order interface {
GetDirection() OrderDirection
GetPrice() sdk.Dec
GetAmount() sdk.Int
GetOpenAmount() sdk.Int
SetOpenAmount(amt sdk.Int) Order
GetRemainingOfferCoin() sdk.Coin
DecrRemainingOfferCoin(amt sdk.Int) Order // Decrement remaining offer coin amount
GetReceivedDemandCoin() sdk.Coin
IncrReceivedDemandCoin(amt sdk.Int) Order // Increment received demand coin amount
IsMatched() bool
SetMatched(matched bool) Order
}

func TotalOpenAmount(orders []Order) sdk.Int {
amt := sdk.ZeroInt()
for _, order := range orders {
amt = amt.Add(order.GetOpenAmount())
}
return amt
}

type BaseOrder struct {
Direction OrderDirection
Price sdk.Dec
Amount sdk.Int
OpenAmount sdk.Int
RemainingOfferCoin sdk.Coin
ReceivedDemandCoin sdk.Coin
Matched bool
}

func NewBaseOrder(dir OrderDirection, price sdk.Dec, amt sdk.Int, offerCoin sdk.Coin, demandCoinDenom string) *BaseOrder {
return &BaseOrder{
Direction: dir,
Price: price,
Amount: amt,
OpenAmount: amt,
RemainingOfferCoin: offerCoin,
ReceivedDemandCoin: sdk.NewCoin(demandCoinDenom, sdk.ZeroInt()),
}
}

func (order *BaseOrder) GetDirection() OrderDirection {
return order.Direction
}

func (order *BaseOrder) GetPrice() sdk.Dec {
return order.Price
}

func (order *BaseOrder) GetAmount() sdk.Int {
return order.Amount
}

func (order *BaseOrder) GetOpenAmount() sdk.Int {
return order.OpenAmount
}

func (order *BaseOrder) SetOpenAmount(amt sdk.Int) Order {
order.OpenAmount = amt
return order
}

func (order *BaseOrder) GetRemainingOfferCoin() sdk.Coin {
return order.RemainingOfferCoin
}

func (order *BaseOrder) DecrRemainingOfferCoin(amt sdk.Int) Order {
order.RemainingOfferCoin = order.RemainingOfferCoin.SubAmount(amt)
return order
}

func (order *BaseOrder) GetReceivedDemandCoin() sdk.Coin {
return order.ReceivedDemandCoin
}

func (order *BaseOrder) IncrReceivedDemandCoin(amt sdk.Int) Order {
order.ReceivedDemandCoin = order.ReceivedDemandCoin.AddAmount(amt)
return order
}

func (order *BaseOrder) IsMatched() bool {
return order.Matched
}

func (order *BaseOrder) SetMatched(matched bool) Order {
order.Matched = matched
return order
}
Loading