code.vegaprotocol.io/vega@v0.79.0/core/matching/orderbook_amm_test.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package matching_test
    17  
    18  import (
    19  	"testing"
    20  
    21  	"code.vegaprotocol.io/vega/core/matching"
    22  	"code.vegaprotocol.io/vega/core/matching/mocks"
    23  	"code.vegaprotocol.io/vega/core/types"
    24  	vgcrypto "code.vegaprotocol.io/vega/libs/crypto"
    25  	"code.vegaprotocol.io/vega/libs/num"
    26  	"code.vegaprotocol.io/vega/logging"
    27  
    28  	"github.com/golang/mock/gomock"
    29  	"github.com/stretchr/testify/assert"
    30  	"github.com/stretchr/testify/require"
    31  )
    32  
    33  func TestOrderbookAMM(t *testing.T) {
    34  	t.Run("test empty book and AMM", testEmptyBookAndAMM)
    35  	t.Run("test empty book and matching AMM", testEmptyBookMatchingAMM)
    36  	t.Run("test empty book and matching AMM with incoming FOK", testEmptyBookMatchingAMMFOK)
    37  	t.Run("test matching between price levels", testMatchBetweenPriceLevels)
    38  	t.Run("test matching with orders on both sides", testMatchOrdersBothSide)
    39  	t.Run("test check book accounts for AMM volumes", testOnlyAMMOrders)
    40  }
    41  
    42  func testOnlyAMMOrders(t *testing.T) {
    43  	tst := getTestOrderBookWithAMM(t)
    44  	defer tst.ctrl.Finish()
    45  	sellPrice := num.NewUint(111)
    46  	buyPrice := num.NewUint(95)
    47  	// create some regular orders
    48  	sell := createOrder(t, tst, 10, sellPrice)
    49  	buy := createOrder(t, tst, 10, buyPrice)
    50  	buy.Side = types.SideBuy
    51  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(4).Return(nil)
    52  	tst.obs.EXPECT().NotifyFinished().Times(3)
    53  	_, err := tst.book.SubmitOrder(sell)
    54  	require.NoError(t, err)
    55  	_, err = tst.book.SubmitOrder(buy)
    56  	require.NoError(t, err)
    57  	// create some pegged orders
    58  	pob := createOrder(t, tst, 100, nil)
    59  	pob.Party = "B"
    60  	pob.Side = types.SideBuy
    61  	pob.PeggedOrder = &types.PeggedOrder{
    62  		Reference: types.PeggedReferenceBestBid,
    63  		Offset:    num.NewUint(10),
    64  	}
    65  	pos := createOrder(t, tst, 100, nil)
    66  	pos.Party = "S"
    67  	pos.PeggedOrder = &types.PeggedOrder{
    68  		Reference: types.PeggedReferenceBestAsk,
    69  		Offset:    num.NewUint(10),
    70  	}
    71  	require.NoError(t, err)
    72  	_, err = tst.book.SubmitOrder(pos)
    73  	require.NoError(t, err)
    74  	require.Equal(t, uint64(1), tst.book.GetPeggedOrdersCount())
    75  	// now cancel the non-pegged orders
    76  	require.NoError(t, err)
    77  	one, zero := uint64(1), uint64(0)
    78  
    79  	// only buy orders
    80  	tst.obs.EXPECT().BestPricesAndVolumes().Times(1).Return(num.UintOne(), one, nil, zero)
    81  	check := tst.book.CheckBook()
    82  	require.False(t, check)
    83  
    84  	// buy and sell orders
    85  	tst.obs.EXPECT().BestPricesAndVolumes().Times(1).Return(num.UintOne(), one, num.UintOne(), one)
    86  	check = tst.book.CheckBook()
    87  	require.True(t, check)
    88  }
    89  
    90  func testEmptyBookAndAMM(t *testing.T) {
    91  	tst := getTestOrderBookWithAMM(t)
    92  	defer tst.ctrl.Finish()
    93  	price := num.NewUint(100)
    94  
    95  	// fake uncross
    96  	o := createOrder(t, tst, 100, price)
    97  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), nil, price).Times(1)
    98  	tst.obs.EXPECT().NotifyFinished().Times(1)
    99  	trades, err := tst.book.GetTrades(o)
   100  	assert.NoError(t, err)
   101  	assert.Len(t, trades, 0)
   102  
   103  	// uncross
   104  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), nil, price).Times(1)
   105  	tst.obs.EXPECT().NotifyFinished().Times(1)
   106  	conf, err := tst.book.SubmitOrder(o)
   107  	assert.NoError(t, err)
   108  	assert.Len(t, conf.PassiveOrdersAffected, 0)
   109  	assert.Len(t, conf.Trades, 0)
   110  }
   111  
   112  func testEmptyBookMatchingAMM(t *testing.T) {
   113  	tst := getTestOrderBookWithAMM(t)
   114  	defer tst.ctrl.Finish()
   115  	price := num.NewUint(100)
   116  
   117  	o := createOrder(t, tst, 1000, price)
   118  	generated := createGeneratedOrders(t, tst, price)
   119  
   120  	// fake uncross
   121  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), nil, price).Times(1).Return(generated)
   122  	tst.obs.EXPECT().NotifyFinished().Times(1)
   123  	trades, err := tst.book.GetTrades(o)
   124  	assert.NoError(t, err)
   125  	assert.Len(t, trades, 2)
   126  
   127  	// uncross
   128  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), nil, price).Times(1).Return(generated)
   129  	tst.obs.EXPECT().NotifyFinished().Times(1)
   130  	conf, err := tst.book.SubmitOrder(o)
   131  	assert.NoError(t, err)
   132  	assertConf(t, conf, 2, 10)
   133  }
   134  
   135  func testEmptyBookMatchingAMMFOK(t *testing.T) {
   136  	tst := getTestOrderBookWithAMM(t)
   137  	defer tst.ctrl.Finish()
   138  	price := num.NewUint(100)
   139  
   140  	o := createOrder(t, tst, 20, price)
   141  	generated := createGeneratedOrders(t, tst, price)
   142  
   143  	o.TimeInForce = types.OrderTimeInForceFOK
   144  
   145  	// fake uncross
   146  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), nil, price).Times(2).Return(generated)
   147  	tst.obs.EXPECT().NotifyFinished().Times(2)
   148  	trades, err := tst.book.GetTrades(o)
   149  	assert.NoError(t, err)
   150  	assert.Len(t, trades, 2)
   151  
   152  	// uncross
   153  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), nil, price).Times(2).Return(generated)
   154  	tst.obs.EXPECT().NotifyFinished().Times(2)
   155  	conf, err := tst.book.SubmitOrder(o)
   156  	assert.NoError(t, err)
   157  	assertConf(t, conf, 2, 10)
   158  }
   159  
   160  func testMatchBetweenPriceLevels(t *testing.T) {
   161  	tst := getTestOrderBookWithAMM(t)
   162  	defer tst.ctrl.Finish()
   163  
   164  	createPriceLevels(t, tst, 10,
   165  		num.NewUint(100),
   166  		num.NewUint(110),
   167  		num.NewUint(120),
   168  	)
   169  
   170  	price := num.NewUint(90)
   171  	size := uint64(1000)
   172  
   173  	o := createOrder(t, tst, size, price)
   174  	generated := createGeneratedOrders(t, tst, price)
   175  
   176  	// price levels at 100, 110, 120, incoming order at 100
   177  	// expect it to consume all volume at the three levels, and between each level we'll submit to offbook
   178  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(4).Return(generated)
   179  	tst.obs.EXPECT().NotifyFinished().Times(1)
   180  	trades, err := tst.book.GetTrades(o)
   181  	assert.NoError(t, err)
   182  
   183  	// 3 trades with each price level, and then 2 trades from AMM in the intervals
   184  	// (nil, 120) (120, 110) (110, 100) (100, 90)
   185  	// so 3 + (2 * 4) = 11
   186  	assert.Len(t, trades, 11)
   187  
   188  	// uncross
   189  	expectOffbookOrders(t, tst, price, nil, num.NewUint(120))
   190  	expectOffbookOrders(t, tst, price, num.NewUint(120), num.NewUint(110))
   191  	expectOffbookOrders(t, tst, price, num.NewUint(110), num.NewUint(100))
   192  	expectOffbookOrders(t, tst, price, num.NewUint(100), num.NewUint(90))
   193  	tst.obs.EXPECT().NotifyFinished().Times(1)
   194  
   195  	conf, err := tst.book.SubmitOrder(o)
   196  	assert.NoError(t, err)
   197  	assertConf(t, conf, 11, 10)
   198  }
   199  
   200  func testMatchOrdersBothSide(t *testing.T) {
   201  	tst := getTestOrderBookWithAMM(t)
   202  	defer tst.ctrl.Finish()
   203  
   204  	createPriceLevels(t, tst, 10,
   205  		num.NewUint(120),
   206  		num.NewUint(110),
   207  	)
   208  
   209  	// this one will be on the opposite side of the book as price levels
   210  	// sell order willing to sell at 130
   211  	oPrice := uint64(130)
   212  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(1)
   213  	tst.obs.EXPECT().NotifyFinished().Times(1)
   214  	o := createOrder(t, tst, 10, num.NewUint(oPrice))
   215  
   216  	conf, err := tst.book.SubmitOrder(o)
   217  	assert.NoError(t, err)
   218  	assertConf(t, conf, 0, 0)
   219  
   220  	price := num.NewUint(90)
   221  	size := uint64(1000)
   222  	o = createOrder(t, tst, size, price)
   223  	generated := createGeneratedOrders(t, tst, price)
   224  
   225  	// price levels at 100, 110, 120, incoming order at 100
   226  	// expect it to consume all volume at the three levels, and between each level we'll submit to offbook
   227  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(3).Return(generated)
   228  	tst.obs.EXPECT().NotifyFinished().Times(1)
   229  	trades, err := tst.book.GetTrades(o)
   230  	assert.NoError(t, err)
   231  
   232  	// 3 trades with each price level, and then 2 trades from AMM in the intervals
   233  	// (nil, 120) (120, 110) (110, 100) (100, 90)
   234  	// so 3 + (2 * 4) = 11
   235  	assert.Len(t, trades, 8)
   236  
   237  	// uncross
   238  	expectOffbookOrders(t, tst, price, nil, num.NewUint(120))
   239  	expectOffbookOrders(t, tst, price, num.NewUint(120), num.NewUint(110))
   240  	expectOffbookOrders(t, tst, price, num.NewUint(110), num.NewUint(90))
   241  	tst.obs.EXPECT().NotifyFinished().Times(1)
   242  
   243  	conf, err = tst.book.SubmitOrder(o)
   244  	assert.NoError(t, err)
   245  	assertConf(t, conf, 8, 10)
   246  }
   247  
   248  func TestAMMOnlyBestPrices(t *testing.T) {
   249  	tst := getTestOrderBookWithAMM(t)
   250  	defer tst.ctrl.Finish()
   251  
   252  	tst.obs.EXPECT().BestPricesAndVolumes().Return(
   253  		num.NewUint(1999),
   254  		uint64(10),
   255  		num.NewUint(2001),
   256  		uint64(9),
   257  	).AnyTimes()
   258  
   259  	// Best
   260  	price, err := tst.book.GetBestAskPrice()
   261  	require.NoError(t, err)
   262  	assert.Equal(t, "2001", price.String())
   263  
   264  	price, err = tst.book.GetBestBidPrice()
   265  	require.NoError(t, err)
   266  	assert.Equal(t, "1999", price.String())
   267  
   268  	// Best and volume
   269  	price, volume, err := tst.book.BestOfferPriceAndVolume()
   270  	require.NoError(t, err)
   271  	assert.Equal(t, "2001", price.String())
   272  	assert.Equal(t, uint64(9), volume)
   273  
   274  	price, volume, err = tst.book.BestBidPriceAndVolume()
   275  	require.NoError(t, err)
   276  	assert.Equal(t, "1999", price.String())
   277  	assert.Equal(t, uint64(10), volume)
   278  
   279  	// Best static
   280  	price, err = tst.book.GetBestStaticAskPrice()
   281  	require.NoError(t, err)
   282  	assert.Equal(t, "2001", price.String())
   283  
   284  	price, err = tst.book.GetBestStaticBidPrice()
   285  	require.NoError(t, err)
   286  	assert.Equal(t, "1999", price.String())
   287  
   288  	// Best static and volume
   289  	price, volume, err = tst.book.GetBestStaticAskPriceAndVolume()
   290  	require.NoError(t, err)
   291  	assert.Equal(t, "2001", price.String())
   292  	assert.Equal(t, uint64(9), volume)
   293  
   294  	price, volume, err = tst.book.GetBestStaticBidPriceAndVolume()
   295  	require.NoError(t, err)
   296  	assert.Equal(t, "1999", price.String())
   297  	assert.Equal(t, uint64(10), volume)
   298  }
   299  
   300  func TestIndicativeTradesAMMOnly(t *testing.T) {
   301  	tst := getTestOrderBookWithAMM(t)
   302  	defer tst.ctrl.Finish()
   303  	tst.obs.EXPECT().NotifyFinished().Times(1)
   304  
   305  	expectCrossedAMMs(t, tst, 100, 150)
   306  	tst.book.EnterAuction()
   307  
   308  	ret := createOrder(t, tst, 10, num.NewUint(100))
   309  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(
   310  		func(o *types.Order, _, _ *num.Uint) []*types.Order {
   311  			ret.Size = o.Remaining
   312  			ret.Remaining = o.Remaining
   313  			return []*types.Order{ret.Clone()}
   314  		},
   315  	)
   316  
   317  	trades, err := tst.book.GetIndicativeTrades()
   318  	require.NoError(t, err)
   319  	assert.Equal(t, 1, len(trades))
   320  	assert.Equal(t, 260, int(trades[0].Size))
   321  }
   322  
   323  func TestIndicativeTradesAMMOrderbookNotCrosses(t *testing.T) {
   324  	tst := getTestOrderBookWithAMM(t)
   325  	defer tst.ctrl.Finish()
   326  	tst.obs.EXPECT().NotifyFinished().Times(1)
   327  
   328  	expectCrossedAMMs(t, tst, 100, 150)
   329  	tst.book.EnterAuction()
   330  
   331  	// submit an order each side outside of the crossed region
   332  	o := createOrder(t, tst, 10, num.NewUint(90))
   333  	o.Side = types.SideBuy
   334  	_, err := tst.book.SubmitOrder(o)
   335  	require.NoError(t, err)
   336  
   337  	o = createOrder(t, tst, 10, num.NewUint(160))
   338  	o.Side = types.SideSell
   339  	_, err = tst.book.SubmitOrder(o)
   340  	require.NoError(t, err)
   341  
   342  	ret := createOrder(t, tst, 10, num.NewUint(100))
   343  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(
   344  		func(o *types.Order, _, _ *num.Uint) []*types.Order {
   345  			ret.Size = o.Remaining
   346  			ret.Remaining = o.Remaining
   347  			return []*types.Order{ret.Clone()}
   348  		},
   349  	)
   350  
   351  	trades, err := tst.book.GetIndicativeTrades()
   352  	require.NoError(t, err)
   353  	assert.Equal(t, 1, len(trades))
   354  	assert.Equal(t, 260, int(trades[0].Size))
   355  }
   356  
   357  func TestIndicativeTradesAMMCrossedOrders(t *testing.T) {
   358  	tst := getTestOrderBookWithAMM(t)
   359  	defer tst.ctrl.Finish()
   360  	tst.obs.EXPECT().NotifyFinished().Times(1)
   361  
   362  	expectCrossedAMMs(t, tst, 100, 150)
   363  	tst.book.EnterAuction()
   364  
   365  	o := createOrder(t, tst, 5, num.NewUint(125))
   366  	o.Side = types.SideSell
   367  	_, err := tst.book.SubmitOrder(o)
   368  	require.NoError(t, err)
   369  
   370  	o = createOrder(t, tst, 5, num.NewUint(126))
   371  	o.Side = types.SideSell
   372  	_, err = tst.book.SubmitOrder(o)
   373  	require.NoError(t, err)
   374  
   375  	ret := createOrder(t, tst, 250, num.NewUint(100))
   376  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return([]*types.Order{ret.Clone()})
   377  
   378  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(nil)
   379  
   380  	trades, err := tst.book.GetIndicativeTrades()
   381  	require.NoError(t, err)
   382  	assert.Equal(t, 3, len(trades))
   383  	var total int
   384  	for _, t := range trades {
   385  		total += int(t.Size)
   386  	}
   387  	assert.Equal(t, 260, total)
   388  }
   389  
   390  func TestUncrossedBookDoesNotExpandAMMs(t *testing.T) {
   391  	tst := getTestOrderBookWithAMM(t)
   392  	defer tst.ctrl.Finish()
   393  
   394  	// AMM with buy at 99 and SELL at 101
   395  	tst.obs.EXPECT().BestPricesAndVolumes().Return(num.NewUint(uint64(99)), uint64(10), num.NewUint(uint64(101)), uint64(10)).AnyTimes()
   396  
   397  	// enter auction when not crossed we should not try to expand AMM's
   398  	tst.book.EnterAuction()
   399  	assert.Equal(t, "0", tst.book.GetIndicativePrice().String())
   400  }
   401  
   402  func assertConf(t *testing.T, conf *types.OrderConfirmation, n int, size uint64) {
   403  	t.Helper()
   404  	assert.Len(t, conf.PassiveOrdersAffected, n)
   405  	assert.Len(t, conf.Trades, n)
   406  	for i := range conf.Trades {
   407  		assert.Equal(t, conf.Trades[i].Size, size)
   408  		assert.Equal(t, conf.PassiveOrdersAffected[i].Remaining, uint64(0))
   409  	}
   410  }
   411  
   412  func expectOffbookOrders(t *testing.T, tst *tstOrderbook, price, first, last *num.Uint) {
   413  	t.Helper()
   414  	generated := createGeneratedOrders(t, tst, price)
   415  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), first, last).Times(1).Return(generated)
   416  }
   417  
   418  func expectCrossedAMMs(t *testing.T, tst *tstOrderbook, min, max int) {
   419  	t.Helper()
   420  	tst.obs.EXPECT().BestPricesAndVolumes().Return(num.NewUint(uint64(max)), uint64(10), num.NewUint(uint64(min)), uint64(10)).AnyTimes()
   421  
   422  	orders1 := createOrderbookShape(t, tst, min, max, types.SideBuy, "A")
   423  	orders2 := createOrderbookShape(t, tst, min, max, types.SideSell, "B")
   424  	res := []*types.OrderbookShapeResult{
   425  		{
   426  			Buys:  orders1,
   427  			Sells: orders2,
   428  		},
   429  	}
   430  
   431  	tst.obs.EXPECT().OrderbookShape(gomock.Any(), gomock.Any(), gomock.Any()).Return(res)
   432  }
   433  
   434  type tstOrderbook struct {
   435  	ctrl     *gomock.Controller
   436  	book     *matching.CachedOrderBook
   437  	obs      *mocks.MockOffbookSource
   438  	marketID string
   439  }
   440  
   441  func createOrder(t *testing.T, tst *tstOrderbook, size uint64, price *num.Uint) *types.Order {
   442  	t.Helper()
   443  	return &types.Order{
   444  		ID:            vgcrypto.RandomHash(),
   445  		Status:        types.OrderStatusActive,
   446  		MarketID:      tst.marketID,
   447  		Party:         "A",
   448  		Side:          types.SideSell,
   449  		Price:         price,
   450  		OriginalPrice: price,
   451  		Size:          size,
   452  		Remaining:     size,
   453  		TimeInForce:   types.OrderTimeInForceGTC,
   454  		Type:          types.OrderTypeLimit,
   455  	}
   456  }
   457  
   458  func createGeneratedOrders(t *testing.T, tst *tstOrderbook, price *num.Uint) []*types.Order {
   459  	t.Helper()
   460  
   461  	orders := []*types.Order{}
   462  	for i := 0; i < 2; i++ {
   463  		o := createOrder(t, tst, 10, price)
   464  		o.Side = types.OtherSide(o.Side)
   465  		o.Party = "C"
   466  		orders = append(orders, o)
   467  	}
   468  
   469  	return orders
   470  }
   471  
   472  func createPriceLevels(t *testing.T, tst *tstOrderbook, size uint64, levels ...*num.Uint) {
   473  	t.Helper()
   474  
   475  	tst.obs.EXPECT().SubmitOrder(gomock.Any(), gomock.Any(), gomock.Any()).Times(len(levels))
   476  	tst.obs.EXPECT().NotifyFinished().Times(len(levels))
   477  	for _, l := range levels {
   478  		o := createOrder(t, tst, size, l)
   479  		o.Side = types.OtherSide(o.Side)
   480  		o.Party = "B"
   481  		conf, err := tst.book.SubmitOrder(o)
   482  		require.NoError(t, err)
   483  		require.Len(t, conf.Trades, 0)
   484  	}
   485  }
   486  
   487  func createOrderbookShape(t *testing.T, tst *tstOrderbook, from, to int, side types.Side, party string) []*types.Order {
   488  	t.Helper()
   489  
   490  	orders := []*types.Order{}
   491  	for i := from; i <= to; i++ {
   492  		o := createOrder(t, tst, 10, num.NewUint(uint64(i)))
   493  		o.GeneratedOffbook = true
   494  		o.Side = side
   495  		o.Party = party
   496  		orders = append(orders, o)
   497  	}
   498  	return orders
   499  }
   500  
   501  func getTestOrderBookWithAMM(t *testing.T) *tstOrderbook {
   502  	t.Helper()
   503  
   504  	ctrl := gomock.NewController(t)
   505  	obs := mocks.NewMockOffbookSource(ctrl)
   506  
   507  	marketID := "testMarket"
   508  	book := matching.NewCachedOrderBook(logging.NewTestLogger(), matching.NewDefaultConfig(), "testMarket", false, peggedOrderCounterForTest)
   509  	book.SetOffbookSource(obs)
   510  
   511  	return &tstOrderbook{
   512  		ctrl:     ctrl,
   513  		book:     book,
   514  		obs:      obs,
   515  		marketID: marketID,
   516  	}
   517  }