Skip to content

Commit

Permalink
Merge pull request #196 from cosmosquad-labs/testing-2
Browse files Browse the repository at this point in the history
test: add x/liquidity tests
  • Loading branch information
hallazzang authored Feb 21, 2022
2 parents f1e1995 + 2a0fc49 commit 40bd560
Show file tree
Hide file tree
Showing 9 changed files with 576 additions and 80 deletions.
167 changes: 167 additions & 0 deletions x/liquidity/amm/match_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package amm_test

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

sdk "github.com/cosmos/cosmos-sdk/types"
Expand All @@ -10,6 +12,54 @@ import (
"github.com/cosmosquad-labs/squad/x/liquidity/amm"
)

func TestFindMatchPrice(t *testing.T) {
for _, tc := range []struct {
name string
os amm.OrderSource
found bool
matchPrice sdk.Dec
}{
{
"happy case",
amm.NewOrderBook(
newOrder(amm.Buy, squad.ParseDec("1.1"), sdk.NewInt(10000)),
newOrder(amm.Sell, squad.ParseDec("0.9"), sdk.NewInt(10000)),
),
true,
squad.ParseDec("1.0"),
},
{
"buy order only",
amm.NewOrderBook(newOrder(amm.Buy, squad.ParseDec("1.0"), sdk.NewInt(10000))),
false,
sdk.Dec{},
},
{
"sell order only",
amm.NewOrderBook(newOrder(amm.Sell, squad.ParseDec("1.0"), sdk.NewInt(10000))),
false,
sdk.Dec{},
},
{
"highest buy price is lower than lowest sell price",
amm.NewOrderBook(
newOrder(amm.Buy, squad.ParseDec("0.9"), sdk.NewInt(10000)),
newOrder(amm.Sell, squad.ParseDec("1.1"), sdk.NewInt(10000)),
),
false,
sdk.Dec{},
},
} {
t.Run(tc.name, func(t *testing.T) {
matchPrice, found := amm.FindMatchPrice(tc.os, int(defTickPrec))
require.Equal(t, tc.found, found)
if found {
require.Equal(t, tc.matchPrice, matchPrice)
}
})
}
}

func TestFindMatchPrice_Rounding(t *testing.T) {
basePrice := squad.ParseDec("0.9990")

Expand All @@ -29,3 +79,120 @@ func TestFindMatchPrice_Rounding(t *testing.T) {
basePrice = defTickPrec.UpTick(basePrice)
}
}

func TestFindLastMatchableOrders(t *testing.T) {
_, _, _, _, found := amm.FindLastMatchableOrders(nil, nil, squad.ParseDec("1.0"))
require.False(t, found)

for seed := int64(0); seed < 100; seed++ {
r := rand.New(rand.NewSource(seed))

minPrice, maxPrice := squad.ParseDec("0.01"), squad.ParseDec("1.0")
minAmt, maxAmt := sdk.NewInt(30), sdk.NewInt(300)

for i := 0; i < 100; i++ {
var buyOrders, sellOrders []amm.Order
numBuyOrders := 1 + r.Intn(5)
numSellOrders := 1 + r.Intn(5)
for j := 0; j < numBuyOrders; j++ {
price := squad.ParseDec("1.0") // Price is not important.
amt := squad.RandomInt(r, minAmt, maxAmt)
buyOrders = append(buyOrders, newOrder(amm.Buy, price, amt))
}
for j := 0; j < numSellOrders; j++ {
price := squad.ParseDec("1.0") // Price is not important.
amt := squad.RandomInt(r, minAmt, maxAmt)
sellOrders = append(sellOrders, newOrder(amm.Sell, price, amt))
}
matchPrice := defTickPrec.PriceToDownTick(squad.RandomDec(r, minPrice, maxPrice))
// We don't sort orders like in real situations, and it doesn't
// actually matter.
bi, si, pmb, pms, found := amm.FindLastMatchableOrders(buyOrders, sellOrders, matchPrice)
if found {
buyAmt := amm.TotalOpenAmount(buyOrders[:bi]).Add(pmb)
sellAmt := amm.TotalOpenAmount(sellOrders[:si]).Add(pms)
require.True(sdk.IntEq(t, buyAmt, sellAmt))
require.False(t, matchPrice.MulInt(pms).TruncateInt().IsZero())
}
}
}
}

func TestMatchOrders(t *testing.T) {
_, matched := amm.MatchOrders(nil, nil, squad.ParseDec("1.0"))
require.False(t, matched)

for _, tc := range []struct {
name string
os amm.OrderSource
matchPrice sdk.Dec
matched bool
quoteCoinDust sdk.Int
}{
{
"happy case",
amm.NewOrderBook(
newOrder(amm.Buy, squad.ParseDec("1.0"), sdk.NewInt(10000)),
newOrder(amm.Sell, squad.ParseDec("1.0"), sdk.NewInt(10000)),
),
squad.ParseDec("1.0"),
true,
sdk.ZeroInt(),
},
{
"happy case #2",
amm.NewOrderBook(
newOrder(amm.Buy, squad.ParseDec("1.1"), sdk.NewInt(10000)),
newOrder(amm.Sell, squad.ParseDec("0.9"), sdk.NewInt(10000)),
),
squad.ParseDec("1.0"),
true,
sdk.ZeroInt(),
},
{
"positive quote coin dust",
amm.NewOrderBook(
newOrder(amm.Buy, squad.ParseDec("0.9999"), sdk.NewInt(1000)),
newOrder(amm.Buy, squad.ParseDec("0.9999"), sdk.NewInt(1000)),
newOrder(amm.Sell, squad.ParseDec("0.9999"), sdk.NewInt(1000)),
newOrder(amm.Sell, squad.ParseDec("0.9999"), sdk.NewInt(1000)),
),
squad.ParseDec("0.9999"),
true,
sdk.NewInt(2),
},
} {
t.Run(tc.name, func(t *testing.T) {
buyOrders := tc.os.BuyOrdersOver(tc.matchPrice)
sellOrders := tc.os.SellOrdersUnder(tc.matchPrice)
matchPrice, found := amm.FindMatchPrice(tc.os, int(defTickPrec))
if tc.matched {
require.True(t, found)
} else {
require.False(t, found)
return
}
require.True(sdk.DecEq(t, tc.matchPrice, matchPrice))
quoteCoinDust, matched := amm.MatchOrders(buyOrders, sellOrders, tc.matchPrice)
require.Equal(t, tc.matched, matched)
if matched {
require.True(sdk.IntEq(t, tc.quoteCoinDust, quoteCoinDust))
for _, order := range append(buyOrders, sellOrders...) {
if order.IsMatched() {
paid := order.GetOfferCoin().Sub(order.GetRemainingOfferCoin())
received := order.GetReceivedDemandCoin()
var effPrice sdk.Dec // Effective swap price
switch order.GetDirection() {
case amm.Buy:
effPrice = paid.Amount.ToDec().QuoInt(received.Amount)
case amm.Sell:
effPrice = received.Amount.ToDec().QuoInt(paid.Amount)
}
fmt.Println(paid, received, effPrice, tc.matchPrice)
require.True(t, squad.DecApproxEqual(tc.matchPrice, effPrice))
}
}
}
})
}
}
11 changes: 9 additions & 2 deletions x/liquidity/amm/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ const (
func (dir OrderDirection) String() string {
switch dir {
case Buy:
return "buy"
return "Buy"
case Sell:
return "sell"
return "Sell"
default:
return fmt.Sprintf("OrderDirection(%d)", dir)
}
Expand All @@ -35,6 +35,7 @@ type Order interface {
GetAmount() sdk.Int // The original order amount
GetOpenAmount() sdk.Int
SetOpenAmount(amt sdk.Int)
GetOfferCoin() sdk.Coin
GetRemainingOfferCoin() sdk.Coin
DecrRemainingOfferCoin(amt sdk.Int) // Decrement remaining offer coin amount
GetReceivedDemandCoin() sdk.Coin
Expand All @@ -58,6 +59,7 @@ type BaseOrder struct {
Price sdk.Dec
Amount sdk.Int
OpenAmount sdk.Int
OfferCoin sdk.Coin
RemainingOfferCoin sdk.Coin
ReceivedDemandCoin sdk.Coin
Matched bool
Expand All @@ -70,6 +72,7 @@ func NewBaseOrder(dir OrderDirection, price sdk.Dec, amt sdk.Int, offerCoin sdk.
Price: price,
Amount: amt,
OpenAmount: amt,
OfferCoin: offerCoin,
RemainingOfferCoin: offerCoin,
ReceivedDemandCoin: sdk.NewCoin(demandCoinDenom, sdk.ZeroInt()),
}
Expand Down Expand Up @@ -100,6 +103,10 @@ func (order *BaseOrder) SetOpenAmount(amt sdk.Int) {
order.OpenAmount = amt
}

func (order *BaseOrder) GetOfferCoin() sdk.Coin {
return order.OfferCoin
}

// GetRemainingOfferCoin returns remaining offer coin of the order.
func (order *BaseOrder) GetRemainingOfferCoin() sdk.Coin {
return order.RemainingOfferCoin
Expand Down
48 changes: 23 additions & 25 deletions x/liquidity/amm/orderbook.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ type OrderBook struct {
// NewOrderBook returns a new OrderBook.
func NewOrderBook(orders ...Order) *OrderBook {
ob := &OrderBook{}
for _, order := range orders {
ob.Add(order)
}
ob.Add(orders...)
return ob
}

Expand Down Expand Up @@ -73,11 +71,11 @@ func (ob *OrderBook) SellOrdersUnder(price sdk.Dec) []Order {
// This type is used for both buy/sell sides of OrderBook.
type orderBookTicks []*orderBookTick

func (ticks *orderBookTicks) findPrice(price sdk.Dec) (i int, exact bool) {
i = sort.Search(len(*ticks), func(i int) bool {
return (*ticks)[i].price.LTE(price)
func (ticks orderBookTicks) findPrice(price sdk.Dec) (i int, exact bool) {
i = sort.Search(len(ticks), func(i int) bool {
return ticks[i].price.LTE(price)
})
if i < len(*ticks) && (*ticks)[i].price.Equal(price) {
if i < len(ticks) && ticks[i].price.Equal(price) {
exact = true
}
return
Expand All @@ -98,68 +96,68 @@ func (ticks *orderBookTicks) add(order Order) {
}
}

func (ticks *orderBookTicks) highestPrice() (sdk.Dec, bool) {
if len(*ticks) == 0 {
func (ticks orderBookTicks) highestPrice() (sdk.Dec, bool) {
if len(ticks) == 0 {
return sdk.Dec{}, false
}
for _, tick := range *ticks {
for _, tick := range ticks {
if TotalOpenAmount(tick.orders).IsPositive() {
return tick.price, true
}
}
return sdk.Dec{}, false
}

func (ticks *orderBookTicks) lowestPrice() (sdk.Dec, bool) {
if len(*ticks) == 0 {
func (ticks orderBookTicks) lowestPrice() (sdk.Dec, bool) {
if len(ticks) == 0 {
return sdk.Dec{}, false
}
for i := len(*ticks) - 1; i >= 0; i-- {
if TotalOpenAmount((*ticks)[i].orders).IsPositive() {
return (*ticks)[i].price, true
for i := len(ticks) - 1; i >= 0; i-- {
if TotalOpenAmount(ticks[i].orders).IsPositive() {
return ticks[i].price, true
}
}
return sdk.Dec{}, false
}

func (ticks *orderBookTicks) amountOver(price sdk.Dec) sdk.Int {
func (ticks orderBookTicks) amountOver(price sdk.Dec) sdk.Int {
i, exact := ticks.findPrice(price)
if !exact {
i--
}
amt := sdk.ZeroInt()
for ; i >= 0; i-- {
amt = amt.Add(TotalOpenAmount((*ticks)[i].orders))
amt = amt.Add(TotalOpenAmount(ticks[i].orders))
}
return amt
}

func (ticks *orderBookTicks) amountUnder(price sdk.Dec) sdk.Int {
func (ticks orderBookTicks) amountUnder(price sdk.Dec) sdk.Int {
i, _ := ticks.findPrice(price)
amt := sdk.ZeroInt()
for ; i < len(*ticks); i++ {
amt = amt.Add(TotalOpenAmount((*ticks)[i].orders))
for ; i < len(ticks); i++ {
amt = amt.Add(TotalOpenAmount(ticks[i].orders))
}
return amt
}

func (ticks *orderBookTicks) ordersOver(price sdk.Dec) []Order {
func (ticks orderBookTicks) ordersOver(price sdk.Dec) []Order {
i, exact := ticks.findPrice(price)
if !exact {
i--
}
var orders []Order
for ; i >= 0; i-- {
orders = append(orders, (*ticks)[i].orders...)
orders = append(orders, ticks[i].orders...)
}
return orders
}

func (ticks *orderBookTicks) ordersUnder(price sdk.Dec) []Order {
func (ticks orderBookTicks) ordersUnder(price sdk.Dec) []Order {
i, _ := ticks.findPrice(price)
var orders []Order
for ; i < len(*ticks); i++ {
orders = append(orders, (*ticks)[i].orders...)
for ; i < len(ticks); i++ {
orders = append(orders, ticks[i].orders...)
}
return orders
}
Expand Down
53 changes: 53 additions & 0 deletions x/liquidity/amm/orderbook_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package amm

import (
"sort"
"testing"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/stretchr/testify/require"

squad "github.com/cosmosquad-labs/squad/types"
)

// Copied from orderbook_test.go
func newOrder(dir OrderDirection, price sdk.Dec, amt sdk.Int) *BaseOrder {
var offerCoinDenom, demandCoinDenom string
switch dir {
case Buy:
offerCoinDenom, demandCoinDenom = "denom2", "denom1"
case Sell:
offerCoinDenom, demandCoinDenom = "denom1", "denom2"
}
return NewBaseOrder(dir, price, amt, sdk.NewCoin(offerCoinDenom, OfferCoinAmount(dir, price, amt)), demandCoinDenom)
}

func TestOrderBookTicks_add(t *testing.T) {
prices := []sdk.Dec{
squad.ParseDec("1.0"),
squad.ParseDec("1.1"),
squad.ParseDec("1.05"),
squad.ParseDec("1.1"),
squad.ParseDec("1.2"),
squad.ParseDec("0.9"),
squad.ParseDec("0.9"),
}
var ticks orderBookTicks
for _, price := range prices {
ticks.add(newOrder(Buy, price, sdk.NewInt(10000)))
}
pricesSet := map[string]struct{}{}
for _, price := range prices {
pricesSet[price.String()] = struct{}{}
}
prices = nil
for priceStr := range pricesSet {
prices = append(prices, squad.ParseDec(priceStr))
}
sort.Slice(prices, func(i, j int) bool {
return prices[i].GT(prices[j])
})
for i, price := range prices {
require.True(sdk.DecEq(t, price, ticks[i].price))
}
}
Loading

0 comments on commit 40bd560

Please sign in to comment.