code.vegaprotocol.io/vega@v0.79.0/core/matching/orderbook_iceberg_uncrossing_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  	"time"
    21  
    22  	"code.vegaprotocol.io/vega/core/types"
    23  	"code.vegaprotocol.io/vega/libs/crypto"
    24  	vgrand "code.vegaprotocol.io/vega/libs/rand"
    25  	"code.vegaprotocol.io/vega/logging"
    26  
    27  	"github.com/stretchr/testify/assert"
    28  	"github.com/stretchr/testify/require"
    29  )
    30  
    31  func makeIceberg(t *testing.T, orderbook *tstOB, market string, id string, side types.Side, price uint64, partyid string, size uint64) *types.Order {
    32  	t.Helper()
    33  	order := getOrder(t, market, id, side, price, partyid, size)
    34  	order.IcebergOrder = &types.IcebergOrder{
    35  		PeakSize:           1,
    36  		MinimumVisibleSize: 1,
    37  	}
    38  	_, err := orderbook.ob.SubmitOrder(order)
    39  	assert.NoError(t, err)
    40  	return order
    41  }
    42  
    43  func requireUncrossedBook(t *testing.T, book *tstOB) {
    44  	t.Helper()
    45  
    46  	ask, err := book.ob.GetBestAskPrice()
    47  	require.NoError(t, err)
    48  
    49  	bid, err := book.ob.GetBestBidPrice()
    50  	require.NoError(t, err)
    51  	require.True(t, bid.LT(ask))
    52  }
    53  
    54  func assertTradeSizes(t *testing.T, trades []*types.Trade, sizes ...uint64) {
    55  	t.Helper()
    56  	require.Equal(t, len(sizes), len(trades))
    57  	for i := range trades {
    58  		assert.Equal(t, trades[i].Size, sizes[i])
    59  	}
    60  }
    61  
    62  func makeIcebergForPanic(t *testing.T, orderbook *tstOB, market string, id string, side types.Side, price uint64, partyid string, size uint64) *types.Order {
    63  	t.Helper()
    64  	order := getOrder(t, market, id, side, price, partyid, size)
    65  	order.IcebergOrder = &types.IcebergOrder{
    66  		PeakSize:           3832,
    67  		MinimumVisibleSize: 493,
    68  	}
    69  	_, err := orderbook.ob.SubmitOrder(order)
    70  	assert.NoError(t, err)
    71  	return order
    72  }
    73  
    74  // TestIcebergPanic is reproducing a bug observed in the market sim. It's skipping a few steps
    75  // to make it minimal but it it's close enough. In summary there are 3 steps below:
    76  // 1. the iceberg order is submitted with peak size of 3832 and size of 8400
    77  // 2. the order is amended to decrease the size and change the price - hence going through ReplaceOrder
    78  // 3. size offset only amendment - no price change - done with amendOrder.
    79  func TestIcebergPanic(t *testing.T) {
    80  	market := vgrand.RandomStr(5)
    81  	book := getTestOrderBook(t, market)
    82  	defer book.Finish()
    83  
    84  	logger := logging.NewTestLogger()
    85  	defer logger.Sync()
    86  
    87  	// Switch to auction mode
    88  	book.ob.EnterAuction()
    89  
    90  	o1 := makeIcebergForPanic(t, book, market, crypto.RandomHash(), types.SideBuy, 101, "party01", 8400)
    91  	makeOrder(t, book, market, "SellOrder01", types.SideSell, 101, "party02", 10000)
    92  
    93  	book.ob.GetIndicativeTrades()
    94  
    95  	// decrease the size - not changing the peak size = 3832
    96  	o2 := o1.Clone()
    97  	o2.Size = 1569
    98  	o2.Remaining = 1569
    99  	o2.IcebergOrder.ReservedRemaining = 0
   100  	book.ob.ReplaceOrder(o1, o2)
   101  
   102  	// size offset of -512
   103  	o3 := o2.Clone()
   104  	o3.Size = 1057
   105  	o3.Remaining = 1057
   106  	o3.IcebergOrder.ReservedRemaining = 0
   107  	book.ob.AmendOrder(o2, o3)
   108  
   109  	book.ob.GetIndicativeTrades()
   110  }
   111  
   112  func TestIcebergExtractedSide(t *testing.T) {
   113  	market := vgrand.RandomStr(5)
   114  	book := getTestOrderBook(t, market)
   115  	defer book.Finish()
   116  
   117  	logger := logging.NewTestLogger()
   118  	defer logger.Sync()
   119  
   120  	// Switch to auction mode
   121  	book.ob.EnterAuction()
   122  
   123  	// the iceberg order is on the side with the smallest uncrossing volume and should be
   124  	// fully consumed after uncrossing
   125  	o := makeIceberg(t, book, market, "BuyOrder01", types.SideBuy, 101, "party01", 20)
   126  	makeOrder(t, book, market, "BuyOrder02", types.SideBuy, 100, "party01", 10)
   127  	makeOrder(t, book, market, "BuyOrder03", types.SideBuy, 99, "party01", 20)
   128  	makeOrder(t, book, market, "BuyOrder04", types.SideBuy, 98, "party01", 10)
   129  
   130  	sell1 := makeOrder(t, book, market, "SellOrder01", types.SideSell, 100, "party02", 10)
   131  	sell2 := makeOrder(t, book, market, "SellOrder02", types.SideSell, 101, "party02", 15)
   132  	makeOrder(t, book, market, "SellOrder03", types.SideSell, 102, "party02", 5)
   133  	makeOrder(t, book, market, "SellOrder04", types.SideSell, 103, "party02", 10)
   134  
   135  	// Get indicative auction price and volume
   136  	price, volume, side := book.ob.GetIndicativePriceAndVolume()
   137  	assert.Equal(t, price.Uint64(), uint64(101))
   138  	assert.Equal(t, volume, uint64(20))
   139  	assert.Equal(t, side, types.SideBuy)
   140  	price = book.ob.GetIndicativePrice()
   141  	assert.Equal(t, price.Uint64(), uint64(101))
   142  
   143  	// Get indicative trades
   144  	trades, err := book.ob.GetIndicativeTrades()
   145  	assert.NoError(t, err)
   146  	assertTradeSizes(t, trades, 10, 10)
   147  
   148  	// Leave auction and uncross the book
   149  	uncrossedOrders, cancels, err := book.ob.LeaveAuction(time.Now())
   150  	assert.Nil(t, err)
   151  	requireUncrossedBook(t, book)
   152  	assert.Equal(t, len(uncrossedOrders), 1)
   153  	assert.Equal(t, len(cancels), 0)
   154  
   155  	// the uncrossed order should be the iceberg and it is fully filled and traded
   156  	// fully with sellf order 1, and half of sell order 2
   157  	trades = uncrossedOrders[0].Trades
   158  	assert.Equal(t, o.ID, uncrossedOrders[0].Order.ID)
   159  	assertTradeSizes(t, trades, 10, 10)
   160  
   161  	assert.Equal(t, types.OrderStatusFilled, sell1.Status)
   162  	assert.Equal(t, types.OrderStatusActive, sell2.Status)
   163  	assert.Equal(t, uint64(5), sell2.Remaining)
   164  	assert.Equal(t, uint64(10), trades[0].Size)
   165  	assert.Equal(t, uint64(10), trades[1].Size)
   166  }
   167  
   168  func TestIcebergAllPriceLevel(t *testing.T) {
   169  	market := vgrand.RandomStr(5)
   170  	book := getTestOrderBook(t, market)
   171  	defer book.Finish()
   172  
   173  	logger := logging.NewTestLogger()
   174  	defer logger.Sync()
   175  
   176  	// Switch to auction mode
   177  	book.ob.EnterAuction()
   178  
   179  	// this order will be big enough to eat into all of the first two icebergs and some of a third at a different pricelevel
   180  	makeOrder(t, book, market, "BuyOrder01", types.SideBuy, 101, "party01", 20)
   181  	makeOrder(t, book, market, "BuyOrder02", types.SideBuy, 100, "party01", 10)
   182  	makeOrder(t, book, market, "BuyOrder03", types.SideBuy, 99, "party01", 20)
   183  	makeOrder(t, book, market, "BuyOrder04", types.SideBuy, 98, "party01", 10)
   184  
   185  	// We have two icebergs at one price level with a small peak
   186  	sell1 := makeIceberg(t, book, market, "SellOrder01", types.SideSell, 100, "party02", 5)
   187  	sell2 := makeIceberg(t, book, market, "SellOrder02", types.SideSell, 100, "party02", 5)
   188  	sell3 := makeIceberg(t, book, market, "SellOrder03", types.SideSell, 101, "party02", 15)
   189  	makeOrder(t, book, market, "SellOrder04", types.SideSell, 102, "party02", 5)
   190  	makeOrder(t, book, market, "SellOrder05", types.SideSell, 103, "party02", 10)
   191  
   192  	// Get indicative auction price and volume
   193  	price, volume, side := book.ob.GetIndicativePriceAndVolume()
   194  	assert.Equal(t, price.Uint64(), uint64(101))
   195  	assert.Equal(t, volume, uint64(20))
   196  	assert.Equal(t, side, types.SideBuy)
   197  
   198  	// Get indicative trades
   199  	trades, err := book.ob.GetIndicativeTrades()
   200  	assert.NoError(t, err)
   201  	assertTradeSizes(t, trades, 5, 5, 10)
   202  	assert.Equal(t, 3, len(trades))
   203  
   204  	// Leave auction and uncross the book
   205  	uncrossedOrders, cancels, err := book.ob.LeaveAuction(time.Now())
   206  	assert.Nil(t, err)
   207  	requireUncrossedBook(t, book)
   208  	assert.Equal(t, 1, len(uncrossedOrders))
   209  	assert.Equal(t, 0, len(cancels))
   210  	assertTradeSizes(t, uncrossedOrders[0].Trades, 5, 5, 10)
   211  
   212  	// first two sell icebergs should be fully filled
   213  	assert.Equal(t, types.OrderStatusFilled, sell1.Status)
   214  	assert.Equal(t, uint64(0), sell1.TrueRemaining())
   215  	assert.Equal(t, types.OrderStatusFilled, sell2.Status)
   216  	assert.Equal(t, uint64(0), sell2.TrueRemaining())
   217  
   218  	// and the third iceberg should be refreshed
   219  	assert.Equal(t, types.OrderStatusActive, sell3.Status)
   220  	assert.Equal(t, uint64(5), sell3.TrueRemaining())
   221  	assert.Equal(t, uint64(1), sell3.Remaining)
   222  
   223  	// check pricelevel count to be sure the sell side at 100 was removed
   224  	assert.Equal(t, uint64(6), book.ob.GetOrderBookLevelCount())
   225  }
   226  
   227  func TestIcebergsDoubleProrata(t *testing.T) {
   228  	market := vgrand.RandomStr(5)
   229  	book := getTestOrderBook(t, market)
   230  	defer book.Finish()
   231  
   232  	logger := logging.NewTestLogger()
   233  	defer logger.Sync()
   234  
   235  	// Switch to auction mode
   236  	book.ob.EnterAuction()
   237  
   238  	// this first order will take off all their peaks, and then 1 off each reserve
   239  	_ = makeOrder(t, book, market, "BuyOrder01", types.SideBuy, 101, "party01", 6)
   240  	// this will then pro-rated take 1 of each reserve again, the icebergs won't refresh in between
   241  	_ = makeOrder(t, book, market, "BuyOrder02", types.SideBuy, 101, "party01", 6)
   242  	makeOrder(t, book, market, "BuyOrder04", types.SideBuy, 99, "party01", 20)
   243  	makeOrder(t, book, market, "BuyOrder05", types.SideBuy, 98, "party01", 10)
   244  
   245  	// Populate sell side the three icebergs will be matched pro-rated, twice
   246  	_ = makeIceberg(t, book, market, "SellOrder01", types.SideSell, 100, "party02", 10)
   247  	_ = makeIceberg(t, book, market, "SellOrder02", types.SideSell, 100, "party02", 10)
   248  	_ = makeIceberg(t, book, market, "SellOrder03", types.SideSell, 100, "party02", 10)
   249  	makeOrder(t, book, market, "SellOrder04", types.SideSell, 102, "party02", 5)
   250  	makeOrder(t, book, market, "SellOrder05", types.SideSell, 103, "party02", 10)
   251  
   252  	// Get indicative auction price and volume
   253  	price, volume, side := book.ob.GetIndicativePriceAndVolume()
   254  	assert.Equal(t, uint64(100), price.Uint64())
   255  	assert.Equal(t, volume, uint64(12))
   256  	assert.Equal(t, side, types.SideBuy)
   257  
   258  	// Get indicative trades
   259  	trades, err := book.ob.GetIndicativeTrades()
   260  	assert.NoError(t, err)
   261  	assertTradeSizes(t, trades, 2, 2, 2, 2, 2, 2)
   262  
   263  	// Leave auction and uncross the book
   264  	uncrossedOrders, cancels, err := book.ob.LeaveAuction(time.Now())
   265  	assert.Nil(t, err)
   266  	requireUncrossedBook(t, book)
   267  	assert.Equal(t, len(uncrossedOrders), 2)
   268  	assert.Equal(t, len(cancels), 0)
   269  	assertTradeSizes(t, uncrossedOrders[0].Trades, 2, 2, 2)
   270  	assertTradeSizes(t, uncrossedOrders[1].Trades, 2, 2, 2)
   271  	assert.Equal(t, 3, len(uncrossedOrders[0].PassiveOrdersAffected))
   272  	assert.Equal(t, 3, len(uncrossedOrders[1].PassiveOrdersAffected))
   273  
   274  	// check pricelevel count to be sure the empty ones were removed
   275  	assert.Equal(t, uint64(5), book.ob.GetOrderBookLevelCount())
   276  }
   277  
   278  func TestIcebergsAndNormalOrders(t *testing.T) {
   279  	// this is basically TestIcebergsDoubleProrata with some non-iceberg orders thrown into the uncrossing too
   280  	market := vgrand.RandomStr(5)
   281  	book := getTestOrderBook(t, market)
   282  	defer book.Finish()
   283  
   284  	logger := logging.NewTestLogger()
   285  	defer logger.Sync()
   286  
   287  	// Switch to auction mode
   288  	book.ob.EnterAuction()
   289  
   290  	// this first order will take off all their peaks, the non-iceberg order, and then 1 off each reserve
   291  	_ = makeOrder(t, book, market, "BuyOrder01", types.SideBuy, 101, "party01", 16)
   292  	// this will then pro-rated take 1 of each reserve again, the icebergs won't refresh in between
   293  	_ = makeOrder(t, book, market, "BuyOrder02", types.SideBuy, 101, "party01", 6)
   294  	makeOrder(t, book, market, "BuyOrder04", types.SideBuy, 99, "party01", 20)
   295  	makeOrder(t, book, market, "BuyOrder05", types.SideBuy, 98, "party01", 10)
   296  
   297  	// Populate sell side the three icebergs will be matched pro-rated, twice
   298  	_ = makeIceberg(t, book, market, "SellOrder01", types.SideSell, 100, "party02", 10)
   299  	_ = makeIceberg(t, book, market, "SellOrder02", types.SideSell, 100, "party02", 10)
   300  	_ = makeIceberg(t, book, market, "SellOrder03", types.SideSell, 100, "party02", 10)
   301  	_ = makeOrder(t, book, market, "SellOrder04", types.SideSell, 100, "party02", 10)
   302  	makeOrder(t, book, market, "SellOrder05", types.SideSell, 102, "party02", 5)
   303  	makeOrder(t, book, market, "SellOrder06", types.SideSell, 103, "party02", 10)
   304  
   305  	// Get indicative auction price and volume
   306  	price, volume, side := book.ob.GetIndicativePriceAndVolume()
   307  	assert.Equal(t, uint64(100), price.Uint64())
   308  	assert.Equal(t, volume, uint64(22))
   309  	assert.Equal(t, side, types.SideBuy)
   310  
   311  	// Get indicative trades
   312  	trades, err := book.ob.GetIndicativeTrades()
   313  	assert.NoError(t, err)
   314  	assertTradeSizes(t, trades, 2, 2, 2, 10, 2, 2, 2)
   315  
   316  	// Leave auction and uncross the book
   317  	uncrossedOrders, cancels, err := book.ob.LeaveAuction(time.Now())
   318  	assert.Nil(t, err)
   319  	requireUncrossedBook(t, book)
   320  	assert.Equal(t, len(uncrossedOrders), 2)
   321  	assert.Equal(t, len(cancels), 0)
   322  	assertTradeSizes(t, uncrossedOrders[0].Trades, 2, 2, 2, 10)
   323  	assertTradeSizes(t, uncrossedOrders[1].Trades, 2, 2, 2)
   324  	assert.Equal(t, 4, len(uncrossedOrders[0].PassiveOrdersAffected))
   325  	assert.Equal(t, 3, len(uncrossedOrders[1].PassiveOrdersAffected))
   326  
   327  	// check pricelevel count to be sure the empty ones were removed
   328  	assert.Equal(t, uint64(5), book.ob.GetOrderBookLevelCount())
   329  }
   330  
   331  func TestIcebergsAndNormalOrders2(t *testing.T) {
   332  	// this is basically TestIcebergsDoubleProrata with some non-iceberg orders thrown into the uncrossing too
   333  	market := vgrand.RandomStr(5)
   334  	book := getTestOrderBook(t, market)
   335  	defer book.Finish()
   336  
   337  	logger := logging.NewTestLogger()
   338  	defer logger.Sync()
   339  
   340  	// Switch to auction mode
   341  	book.ob.EnterAuction()
   342  
   343  	// this first order will take off all their peaks, the non-iceberg order, and then 1 off each reserve
   344  	_ = makeOrder(t, book, market, "BuyOrder01", types.SideBuy, 101, "party01", 40)
   345  	makeOrder(t, book, market, "BuyOrder04", types.SideBuy, 99, "party01", 20)
   346  	makeOrder(t, book, market, "BuyOrder05", types.SideBuy, 98, "party01", 10)
   347  
   348  	// Populate sell side the three icebergs will be matched pro-rated, twice
   349  	_ = makeIceberg(t, book, market, "SellOrder01", types.SideSell, 100, "party02", 10)
   350  	_ = makeIceberg(t, book, market, "SellOrder02", types.SideSell, 100, "party02", 10)
   351  	_ = makeIceberg(t, book, market, "SellOrder03", types.SideSell, 100, "party02", 10)
   352  	_ = makeOrder(t, book, market, "SellOrder04", types.SideSell, 100, "party02", 10)
   353  	makeOrder(t, book, market, "SellOrder05", types.SideSell, 102, "party02", 5)
   354  	makeOrder(t, book, market, "SellOrder06", types.SideSell, 103, "party02", 10)
   355  
   356  	// Get indicative auction price and volume
   357  	price, volume, side := book.ob.GetIndicativePriceAndVolume()
   358  	assert.Equal(t, uint64(100), price.Uint64())
   359  	assert.Equal(t, volume, uint64(40))
   360  	assert.Equal(t, side, types.SideBuy)
   361  
   362  	// Get indicative trades
   363  	trades, err := book.ob.GetIndicativeTrades()
   364  	assert.NoError(t, err)
   365  	assertTradeSizes(t, trades, 10, 10, 10, 10)
   366  
   367  	// Leave auction and uncross the book
   368  	uncrossedOrders, cancels, err := book.ob.LeaveAuction(time.Now())
   369  	assert.Nil(t, err)
   370  	requireUncrossedBook(t, book)
   371  	assert.Equal(t, 1, len(uncrossedOrders))
   372  	assert.Equal(t, len(cancels), 0)
   373  	assertTradeSizes(t, uncrossedOrders[0].Trades, 10, 10, 10, 10)
   374  	assert.Equal(t, 4, len(uncrossedOrders[0].PassiveOrdersAffected))
   375  
   376  	// check pricelevel count to be sure the empty ones were removed
   377  	assert.Equal(t, uint64(4), book.ob.GetOrderBookLevelCount())
   378  }