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 }