code.vegaprotocol.io/vega@v0.79.0/core/execution/liquidation/engine_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 liquidation_test 17 18 import ( 19 "context" 20 "fmt" 21 "testing" 22 "time" 23 24 bmocks "code.vegaprotocol.io/vega/core/broker/mocks" 25 "code.vegaprotocol.io/vega/core/events" 26 cmocks "code.vegaprotocol.io/vega/core/execution/common/mocks" 27 "code.vegaprotocol.io/vega/core/execution/liquidation" 28 "code.vegaprotocol.io/vega/core/execution/liquidation/mocks" 29 "code.vegaprotocol.io/vega/core/types" 30 vegacontext "code.vegaprotocol.io/vega/libs/context" 31 vgcrypto "code.vegaprotocol.io/vega/libs/crypto" 32 "code.vegaprotocol.io/vega/libs/num" 33 "code.vegaprotocol.io/vega/logging" 34 35 "github.com/golang/mock/gomock" 36 "github.com/stretchr/testify/require" 37 ) 38 39 type tstEngine struct { 40 *liquidation.Engine 41 ctrl *gomock.Controller 42 book *mocks.MockBook 43 idgen *mocks.MockIDGen 44 as *cmocks.MockAuctionState 45 broker *bmocks.MockBroker 46 tSvc *cmocks.MockTimeService 47 pos *mocks.MockPositions 48 pmon *mocks.MockPriceMonitor 49 amm *mocks.MockAMM 50 } 51 52 type marginStub struct { 53 party string 54 size int64 55 market string 56 } 57 58 type SliceLenMatcher[T any] int 59 60 func TestOrderbookPriceLimits(t *testing.T) { 61 t.Run("orderbook has no volume", testOrderbookHasNoVolume) 62 t.Run("orderbook has no volume, but vAMM's provide volume", testOrderbookEmptyButAMMVolume) 63 t.Run("orderbook has a volume of one (consumed fraction rounding)", testOrderbookFractionRounding) 64 t.Run("orderbook has plenty of volume (should not increase order size)", testOrderbookExceedsVolume) 65 t.Run("orderbook only has volume above price monitoring bounds", testOrderCappedByPriceMonitor) 66 } 67 68 func TestNetworkReducesOverTime(t *testing.T) { 69 // basic setup can be shared across these tests 70 mID := "intervalMkt" 71 ctx := vegacontext.WithTraceID(context.Background(), vgcrypto.RandomHash()) 72 config := &types.LiquidationStrategy{ 73 DisposalTimeStep: 5 * time.Second, // decrease volume every 5 seconds 74 DisposalFraction: num.DecimalFromFloat(0.1), // remove 10% each step 75 FullDisposalSize: 10, // a volume of 10 or less can be removed in one go 76 MaxFractionConsumed: num.DecimalFromFloat(0.2), // never use more than 20% of the available volume 77 } 78 eng := getTestEngine(t, mID, config.DeepClone()) 79 defer eng.Finish() 80 81 eng.pmon.EXPECT().GetValidPriceRange().AnyTimes().Return( 82 num.NewWrappedDecimal(num.UintZero(), num.DecimalZero()), 83 num.NewWrappedDecimal(num.MaxUint(), num.MaxDecimal()), 84 ) 85 86 // setup: create a party with volume of 10 long as the distressed party 87 closed := []events.Margin{ 88 createMarginEvent("party1", mID, 10), 89 createMarginEvent("party2", mID, 10), 90 createMarginEvent("party3", mID, 10), 91 createMarginEvent("party4", mID, 10), 92 createMarginEvent("party5", mID, 10), 93 } 94 totalSize := uint64(50) 95 now := time.Now() 96 eng.tSvc.EXPECT().GetTimeNow().Times(2).Return(now) 97 idCount := len(closed) * 3 98 eng.idgen.EXPECT().NextID().Times(idCount).Return("nextID") 99 // 2 orders per closed position 100 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](2 * len(closed))).Times(1) 101 // 1 trade per closed position 102 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](1 * len(closed))).Times(1) 103 eng.pos.EXPECT().RegisterOrder(gomock.Any(), gomock.Any()).Times(2 * len(closed)) 104 eng.pos.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(len(closed)) 105 pos, parties, trades := eng.ClearDistressedParties(ctx, eng.idgen, closed, num.UintZero(), num.UintZero()) 106 require.Equal(t, len(closed), len(trades)) 107 require.Equal(t, len(closed), len(pos)) 108 require.Equal(t, len(closed), len(parties)) 109 require.Equal(t, closed[0].Party(), parties[0]) 110 midPrice := num.NewUint(100) 111 112 t.Run("call to ontick within the time step does nothing", func(t *testing.T) { 113 next := now.Add(config.DisposalTimeStep) 114 now = now.Add(2 * time.Second) 115 eng.as.EXPECT().InAuction().Times(1).Return(false) 116 order, err := eng.OnTick(ctx, now, midPrice) 117 require.Nil(t, order) 118 require.NoError(t, err) 119 ns := eng.GetNextCloseoutTS() 120 require.Equal(t, ns, next.UnixNano()) 121 }) 122 123 t.Run("after the time step passes, the first batch is disposed of", func(t *testing.T) { 124 now = now.Add(3 * time.Second) 125 eng.as.EXPECT().InAuction().Times(1).Return(false) 126 // return a large volume so the full step is disposed 127 eng.book.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(1000)) 128 eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0)) 129 order, err := eng.OnTick(ctx, now, midPrice) 130 require.NoError(t, err) 131 require.NotNil(t, order) 132 require.Equal(t, uint64(5), order.Size) 133 }) 134 135 t.Run("ensure the next time step is set", func(t *testing.T) { 136 now = now.Add(2 * time.Second) 137 eng.as.EXPECT().InAuction().Times(1).Return(false) 138 order, err := eng.OnTick(ctx, now, midPrice) 139 require.Nil(t, order) 140 require.NoError(t, err) 141 }) 142 143 // ready to dispose again from here on 144 t.Run("while in auction, the position is not reduced", func(t *testing.T) { 145 // pass another step 146 now = now.Add(3 * time.Second) 147 eng.as.EXPECT().InAuction().Times(1).Return(true) 148 order, err := eng.OnTick(ctx, now, midPrice) 149 require.Nil(t, order) 150 require.NoError(t, err) 151 }) 152 153 t.Run("No longer in auction and we have a price range finally generates the order", func(t *testing.T) { 154 eng.as.EXPECT().InAuction().Times(1).Return(false) 155 // return a large volume so the full step is disposed 156 eng.book.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(1000)) 157 eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0)) 158 order, err := eng.OnTick(ctx, now, midPrice) 159 require.NoError(t, err) 160 require.NotNil(t, order) 161 require.Equal(t, uint64(5), order.Size) 162 }) 163 164 t.Run("increasing the position of the network does not change the time step", func(t *testing.T) { 165 now = now.Add(time.Second) 166 closed := []events.Margin{ 167 createMarginEvent("party", mID, 1), 168 } 169 eng.tSvc.EXPECT().GetTimeNow().Times(1).Return(now) 170 idCount := len(closed) * 3 171 eng.idgen.EXPECT().NextID().Times(idCount).Return("nextID") 172 // 2 orders per closed position 173 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](2 * len(closed))).Times(1) 174 // 1 trade per closed position 175 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](1 * len(closed))).Times(1) 176 eng.pos.EXPECT().RegisterOrder(gomock.Any(), gomock.Any()).Times(len(closed) * 2) 177 eng.pos.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(len(closed)) 178 pos, parties, trades := eng.ClearDistressedParties(ctx, eng.idgen, closed, num.UintZero(), num.UintZero()) 179 require.Equal(t, len(closed), len(trades)) 180 require.Equal(t, len(closed), len(pos)) 181 require.Equal(t, len(closed), len(parties)) 182 require.Equal(t, closed[0].Party(), parties[0]) 183 totalSize++ 184 // now increase time by 4 seconds should dispose 5.1 -> 5 185 now = now.Add(4 * time.Second) 186 eng.as.EXPECT().InAuction().Times(1).Return(false) 187 // return a large volume so the full step is disposed 188 eng.book.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(1000)) 189 eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0)) 190 order, err := eng.OnTick(ctx, now, midPrice) 191 require.NoError(t, err) 192 require.NotNil(t, order) 193 require.Equal(t, uint64(num.DecimalFromFloat(float64(totalSize)).Div(num.DecimalFromFloat(float64(10))).Ceil().IntPart()), order.Size) 194 }) 195 196 t.Run("Updating the config changes the time left until the next step", func(t *testing.T) { 197 now = now.Add(time.Second) 198 eng.as.EXPECT().InAuction().Times(1).Return(false) 199 order, err := eng.OnTick(ctx, now, midPrice) 200 require.Nil(t, order) 201 require.NoError(t, err) 202 // 4s to go, but... 203 config.DisposalTimeStep = 3 * time.Second 204 eng.Update(config.DeepClone()) 205 now = now.Add(2 * time.Second) 206 // only 3 seconds later and we dispose of the next batch 207 eng.as.EXPECT().InAuction().Times(1).Return(false) 208 // return a large volume so the full step is disposed 209 eng.book.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(1000)) 210 eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0)) 211 order, err = eng.OnTick(ctx, now, midPrice) 212 require.NoError(t, err) 213 require.NotNil(t, order) 214 require.Equal(t, uint64(num.DecimalFromFloat(float64(totalSize)).Div(num.DecimalFromFloat(float64(10))).Ceil().IntPart()), order.Size) 215 }) 216 217 t.Run("Once the remaining volume of the network is LTE full disposal position, the network creates an order for its full position", func(t *testing.T) { 218 // use trades to reduce its position 219 size := uint64(eng.GetNetworkPosition().Size()) - config.FullDisposalSize 220 eng.UpdateNetworkPosition([]*types.Trade{ 221 { 222 ID: "someTrade", 223 MarketID: mID, 224 Size: size, 225 }, 226 }) 227 require.True(t, uint64(eng.GetNetworkPosition().Size()) <= config.FullDisposalSize) 228 now = now.Add(3 * time.Second) 229 // only 3 seconds later and we dispose of the next batch 230 eng.as.EXPECT().InAuction().Times(1).Return(false) 231 // return a large volume so the full step is disposed 232 eng.book.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(1000)) 233 eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0)) 234 order, err := eng.OnTick(ctx, now, midPrice) 235 require.NoError(t, err) 236 require.NotNil(t, order) 237 require.Equal(t, config.FullDisposalSize, order.Size) 238 }) 239 } 240 241 func testOrderbookHasNoVolume(t *testing.T) { 242 mID := "market" 243 ctx := vegacontext.WithTraceID(context.Background(), vgcrypto.RandomHash()) 244 eng := getTestEngine(t, mID, nil) 245 defer eng.Finish() 246 247 minP, midPrice := num.NewUint(90), num.NewUint(100) 248 // make sure the lower bound does not override the price range 249 eng.pmon.EXPECT().GetValidPriceRange().AnyTimes().Return( 250 num.NewWrappedDecimal(num.NewUint(90), num.DecimalFromFloat(90.0)), 251 num.NewWrappedDecimal(num.MaxUint(), num.MaxDecimal()), 252 ) 253 254 // setup: create a party with volume of 10 long as the distressed party 255 closed := []events.Margin{ 256 createMarginEvent("party", mID, 10), 257 } 258 now := time.Now() 259 eng.tSvc.EXPECT().GetTimeNow().Times(2).Return(now) 260 idCount := len(closed) * 3 261 eng.idgen.EXPECT().NextID().Times(idCount).Return("nextID") 262 // 2 orders per closed position 263 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](2 * len(closed))).Times(1) 264 // 1 trade per closed position 265 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](1 * len(closed))).Times(1) 266 eng.pos.EXPECT().RegisterOrder(gomock.Any(), gomock.Any()).Times(len(closed) * 2) 267 eng.pos.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(len(closed)) 268 pos, parties, trades := eng.ClearDistressedParties(ctx, eng.idgen, closed, num.UintZero(), num.UintZero()) 269 require.Equal(t, len(closed), len(trades)) 270 require.Equal(t, len(closed), len(pos)) 271 require.Equal(t, len(closed), len(parties)) 272 require.Equal(t, closed[0].Party(), parties[0]) 273 // now when we close out, the book returns a volume of 0 is available 274 eng.as.EXPECT().InAuction().Times(1).Return(false) 275 eng.book.EXPECT().GetVolumeAtPrice(minP, types.SideBuy).Times(1).Return(uint64(0)) 276 // the side should represent the side of the order the network places. 277 eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), types.SideSell).Times(1).Return(uint64(0)) 278 order, err := eng.OnTick(ctx, now, midPrice) 279 require.NoError(t, err) 280 require.Nil(t, order) 281 } 282 283 func testOrderbookFractionRounding(t *testing.T) { 284 mID := "smallMkt" 285 ctx := vegacontext.WithTraceID(context.Background(), vgcrypto.RandomHash()) 286 config := types.LiquidationStrategy{ 287 DisposalTimeStep: 0, 288 DisposalFraction: num.DecimalOne(), 289 FullDisposalSize: 1000000, // plenty 290 MaxFractionConsumed: num.DecimalFromFloat(0.5), 291 DisposalSlippage: num.DecimalFromFloat(10), 292 } 293 eng := getTestEngine(t, mID, &config) 294 defer eng.Finish() 295 296 eng.pmon.EXPECT().GetValidPriceRange().AnyTimes().Return( 297 num.NewWrappedDecimal(num.UintZero(), num.DecimalZero()), 298 num.NewWrappedDecimal(num.MaxUint(), num.MaxDecimal()), 299 ) 300 301 closed := []events.Margin{ 302 createMarginEvent("party", mID, 10), 303 } 304 var netVol int64 305 for _, c := range closed { 306 netVol += c.Size() 307 } 308 now := time.Now() 309 eng.tSvc.EXPECT().GetTimeNow().Times(2).Return(now) 310 idCount := len(closed) * 3 311 eng.idgen.EXPECT().NextID().Times(idCount).Return("nextID") 312 // 2 orders per closed position 313 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](2 * len(closed))).Times(1) 314 // 1 trade per closed position 315 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](1 * len(closed))).Times(1) 316 eng.pos.EXPECT().RegisterOrder(gomock.Any(), gomock.Any()).Times(len(closed) * 2) 317 eng.pos.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(len(closed)) 318 pos, parties, trades := eng.ClearDistressedParties(ctx, eng.idgen, closed, num.UintZero(), num.UintZero()) 319 require.Equal(t, len(closed), len(trades)) 320 require.Equal(t, len(closed), len(pos)) 321 require.Equal(t, len(closed), len(parties)) 322 require.Equal(t, closed[0].Party(), parties[0]) 323 // now the available volume on the book is 1, with the fraction that gets rounded to 0.5 324 // which should be rounded UP to 1. 325 minP, midPrice := num.UintZero(), num.NewUint(100) 326 eng.as.EXPECT().InAuction().Times(1).Return(false) 327 eng.book.EXPECT().GetVolumeAtPrice(minP, types.SideBuy).Times(1).Return(uint64(1)) 328 eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0)) 329 order, err := eng.OnTick(ctx, now, midPrice) 330 require.NoError(t, err) 331 require.Equal(t, uint64(1), order.Size) 332 } 333 334 func testOrderbookEmptyButAMMVolume(t *testing.T) { 335 mID := "market" 336 ctx := vegacontext.WithTraceID(context.Background(), vgcrypto.RandomHash()) 337 config := types.LiquidationStrategy{ 338 DisposalTimeStep: 0, 339 DisposalFraction: num.DecimalOne(), 340 FullDisposalSize: 1000000, // plenty 341 MaxFractionConsumed: num.DecimalFromFloat(0.5), 342 DisposalSlippage: num.DecimalFromFloat(10), 343 } 344 eng := getTestEngine(t, mID, &config) 345 require.Zero(t, eng.GetNextCloseoutTS()) 346 defer eng.Finish() 347 348 eng.pmon.EXPECT().GetValidPriceRange().AnyTimes().Return( 349 num.NewWrappedDecimal(num.UintZero(), num.DecimalZero()), 350 num.NewWrappedDecimal(num.MaxUint(), num.MaxDecimal()), 351 ) 352 353 closed := []events.Margin{ 354 createMarginEvent("party", mID, 10), 355 } 356 var netVol int64 357 for _, c := range closed { 358 netVol += c.Size() 359 } 360 now := time.Now() 361 eng.tSvc.EXPECT().GetTimeNow().Times(2).Return(now) 362 idCount := len(closed) * 3 363 eng.idgen.EXPECT().NextID().Times(idCount).Return("nextID") 364 // 2 orders per closed position 365 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](2 * len(closed))).Times(1) 366 // 1 trade per closed position 367 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](1 * len(closed))).Times(1) 368 eng.pos.EXPECT().RegisterOrder(gomock.Any(), gomock.Any()).Times(len(closed) * 2) 369 eng.pos.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(len(closed)) 370 pos, parties, trades := eng.ClearDistressedParties(ctx, eng.idgen, closed, num.UintZero(), num.UintZero()) 371 require.Equal(t, len(closed), len(trades)) 372 require.Equal(t, len(closed), len(pos)) 373 require.Equal(t, len(closed), len(parties)) 374 require.Equal(t, closed[0].Party(), parties[0]) 375 minP, midPrice := num.UintZero(), num.NewUint(100) 376 eng.as.EXPECT().InAuction().Times(1).Return(false) 377 // no volume on the book 378 eng.book.EXPECT().GetVolumeAtPrice(minP, types.SideBuy).Times(1).Return(uint64(0)) 379 // vAMM's have 100x the available volume, with a factor of 0.5, that's still 50x 380 eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), types.SideSell).Times(1).Return(uint64(netVol * 10)) 381 order, err := eng.OnTick(ctx, now, midPrice) 382 require.NoError(t, err) 383 require.Equal(t, uint64(netVol), order.Size) 384 } 385 386 func testOrderbookExceedsVolume(t *testing.T) { 387 mID := "market" 388 ctx := vegacontext.WithTraceID(context.Background(), vgcrypto.RandomHash()) 389 config := types.LiquidationStrategy{ 390 DisposalTimeStep: 0, 391 DisposalFraction: num.DecimalOne(), 392 FullDisposalSize: 1000000, // plenty 393 MaxFractionConsumed: num.DecimalFromFloat(0.5), 394 DisposalSlippage: num.DecimalFromFloat(10), 395 } 396 eng := getTestEngine(t, mID, &config) 397 defer eng.Finish() 398 399 eng.pmon.EXPECT().GetValidPriceRange().AnyTimes().Return( 400 num.NewWrappedDecimal(num.UintZero(), num.DecimalZero()), 401 num.NewWrappedDecimal(num.MaxUint(), num.MaxDecimal()), 402 ) 403 404 closed := []events.Margin{ 405 createMarginEvent("party", mID, 10), 406 } 407 var netVol int64 408 for _, c := range closed { 409 netVol += c.Size() 410 } 411 now := time.Now() 412 eng.tSvc.EXPECT().GetTimeNow().Times(2).Return(now) 413 idCount := len(closed) * 3 414 eng.idgen.EXPECT().NextID().Times(idCount).Return("nextID") 415 // 2 orders per closed position 416 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](2 * len(closed))).Times(1) 417 // 1 trade per closed position 418 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](1 * len(closed))).Times(1) 419 eng.pos.EXPECT().RegisterOrder(gomock.Any(), gomock.Any()).Times(len(closed) * 2) 420 eng.pos.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(len(closed)) 421 pos, parties, trades := eng.ClearDistressedParties(ctx, eng.idgen, closed, num.UintZero(), num.UintZero()) 422 require.Equal(t, len(closed), len(trades)) 423 require.Equal(t, len(closed), len(pos)) 424 require.Equal(t, len(closed), len(parties)) 425 require.Equal(t, closed[0].Party(), parties[0]) 426 minP, midPrice := num.UintZero(), num.NewUint(100) 427 eng.as.EXPECT().InAuction().Times(1).Return(false) 428 // orderbook has 100x the available volume, with a factor of 0.5, that's still 50x 429 eng.book.EXPECT().GetVolumeAtPrice(minP, types.SideBuy).Times(1).Return(uint64(netVol * 10)) 430 eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0)) 431 order, err := eng.OnTick(ctx, now, midPrice) 432 require.NoError(t, err) 433 require.Equal(t, uint64(netVol), order.Size) 434 } 435 436 func testOrderCappedByPriceMonitor(t *testing.T) { 437 mID := "market" 438 ctx := vegacontext.WithTraceID(context.Background(), vgcrypto.RandomHash()) 439 config := types.LiquidationStrategy{ 440 DisposalTimeStep: 0, 441 DisposalFraction: num.DecimalOne(), 442 FullDisposalSize: 1000000, // plenty 443 MaxFractionConsumed: num.DecimalFromFloat(0.5), 444 DisposalSlippage: num.DecimalFromFloat(10), 445 } 446 eng := getTestEngine(t, mID, &config) 447 defer eng.Finish() 448 449 // these are the bounds given by the order book 450 midPrice := num.NewUint(150) 451 452 // these are the price monitoring bounds 453 minB := num.NewUint(150) 454 eng.pmon.EXPECT().GetValidPriceRange().AnyTimes().Return( 455 num.NewWrappedDecimal(minB.Clone(), num.DecimalFromInt64(150)), 456 num.NewWrappedDecimal(num.NewUint(300), num.DecimalFromInt64(300)), 457 ) 458 459 closed := []events.Margin{ 460 createMarginEvent("party", mID, 10), 461 } 462 var netVol int64 463 for _, c := range closed { 464 netVol += c.Size() 465 } 466 now := time.Now() 467 eng.tSvc.EXPECT().GetTimeNow().Times(2).Return(now) 468 idCount := len(closed) * 3 469 eng.idgen.EXPECT().NextID().Times(idCount).Return("nextID") 470 // 2 orders per closed position 471 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](2 * len(closed))).Times(1) 472 // 1 trade per closed position 473 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](1 * len(closed))).Times(1) 474 eng.pos.EXPECT().RegisterOrder(gomock.Any(), gomock.Any()).Times(len(closed) * 2) 475 eng.pos.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(len(closed)) 476 pos, parties, trades := eng.ClearDistressedParties(ctx, eng.idgen, closed, num.UintZero(), num.UintZero()) 477 require.Equal(t, len(closed), len(trades)) 478 require.Equal(t, len(closed), len(pos)) 479 require.Equal(t, len(closed), len(parties)) 480 require.Equal(t, closed[0].Party(), parties[0]) 481 482 eng.as.EXPECT().InAuction().Times(1).Return(false) 483 484 // we will check for volume at the price monitoring minimum 485 eng.book.EXPECT().GetVolumeAtPrice(minB, types.SideBuy).Times(1).Return(uint64(netVol * 10)) 486 eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0)) 487 order, err := eng.OnTick(ctx, now, midPrice) 488 require.NoError(t, err) 489 require.Equal(t, uint64(netVol), order.Size) 490 } 491 492 func TestLegacySupport(t *testing.T) { 493 // simple test to make sure that passing nil for the config does not cause issues. 494 mID := "market" 495 ctx := vegacontext.WithTraceID(context.Background(), vgcrypto.RandomHash()) 496 eng := getTestEngine(t, mID, nil) 497 defer eng.Finish() 498 499 eng.pmon.EXPECT().GetValidPriceRange().AnyTimes().Return( 500 num.NewWrappedDecimal(num.UintZero(), num.DecimalZero()), 501 num.NewWrappedDecimal(num.MaxUint(), num.MaxDecimal()), 502 ) 503 504 require.False(t, eng.Stopped()) 505 // let's check if we get back an order, create the margin events 506 closed := []events.Margin{ 507 createMarginEvent("party", mID, 10), 508 } 509 var netVol int64 510 for _, c := range closed { 511 netVol += c.Size() 512 } 513 now := time.Now() 514 eng.tSvc.EXPECT().GetTimeNow().Times(2).Return(now) 515 idCount := len(closed) * 3 516 eng.idgen.EXPECT().NextID().Times(idCount).Return("nextID") 517 // 2 orders per closed position 518 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](2 * len(closed))).Times(1) 519 // 1 trade per closed position 520 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](1 * len(closed))).Times(1) 521 eng.pos.EXPECT().RegisterOrder(gomock.Any(), gomock.Any()).Times(len(closed) * 2) 522 eng.pos.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(len(closed)) 523 pos, parties, trades := eng.ClearDistressedParties(ctx, eng.idgen, closed, num.UintZero(), num.UintZero()) 524 require.Equal(t, len(closed), len(trades)) 525 require.Equal(t, len(closed), len(pos)) 526 require.Equal(t, len(closed), len(parties)) 527 require.Equal(t, closed[0].Party(), parties[0]) 528 // now that the network has a position, do the same thing, we should see the time service gets called only once 529 closed = []events.Margin{ 530 createMarginEvent("another party", mID, 5), 531 } 532 for _, c := range closed { 533 netVol += c.Size() 534 } 535 eng.tSvc.EXPECT().GetTimeNow().Times(1).Return(now) 536 idCount = len(closed) * 3 537 eng.idgen.EXPECT().NextID().Times(idCount).Return("nextID") 538 // 2 orders per closed position 539 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](2 * len(closed))).Times(1) 540 // 1 trade per closed position 541 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](1 * len(closed))).Times(1) 542 eng.pos.EXPECT().RegisterOrder(gomock.Any(), gomock.Any()).Times(len(closed) * 2) 543 eng.pos.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(len(closed)) 544 pos, parties, trades = eng.ClearDistressedParties(ctx, eng.idgen, closed, num.UintZero(), num.UintZero()) 545 require.Equal(t, len(closed), len(trades)) 546 require.Equal(t, len(closed), len(pos)) 547 require.Equal(t, len(closed), len(parties)) 548 require.Equal(t, closed[0].Party(), parties[0]) 549 // now we should see an order for size 15 returned 550 minP, midPrice := num.UintZero(), num.NewUint(100) 551 eng.as.EXPECT().InAuction().Times(1).Return(false) 552 eng.book.EXPECT().GetVolumeAtPrice(minP, types.SideBuy).Times(1).Return(uint64(netVol)) 553 // the side should be the side of the order placed by the network, the side used to call the matching engine is the opposite side 554 eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0)) 555 order, err := eng.OnTick(ctx, now, midPrice) 556 require.NoError(t, err) 557 require.Equal(t, uint64(netVol), order.Size) 558 // now reduce the network size through distressed short position 559 closed = []events.Margin{ 560 createMarginEvent("another party", mID, -netVol), 561 } 562 for _, c := range closed { 563 netVol += c.Size() 564 } 565 require.Equal(t, int64(0), netVol) 566 // just check the margin position event we return, too 567 eng.tSvc.EXPECT().GetTimeNow().Times(1).Return(now) 568 idCount = len(closed) * 3 569 eng.idgen.EXPECT().NextID().Times(idCount).Return("nextID") 570 // 2 orders per closed position 571 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](2 * len(closed))).Times(1) 572 // 1 trade per closed position 573 eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](1 * len(closed))).Times(1) 574 eng.pos.EXPECT().RegisterOrder(gomock.Any(), gomock.Any()).Times(len(closed) * 2) 575 eng.pos.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(len(closed)) 576 pos, parties, trades = eng.ClearDistressedParties(ctx, eng.idgen, closed, num.UintZero(), num.UintZero()) 577 require.Equal(t, len(closed), len(trades)) 578 require.Equal(t, len(closed), len(pos)) 579 require.Equal(t, len(closed), len(parties)) 580 require.Equal(t, closed[0].Party(), parties[0]) 581 require.Equal(t, netVol, eng.GetNetworkPosition().Size()) 582 // now we should see no error, and no order returned 583 order, err = eng.OnTick(ctx, now, midPrice) 584 require.NoError(t, err) 585 require.Nil(t, order) 586 // now just make sure stopping for snapshots works as expected 587 eng.StopSnapshots() 588 require.True(t, eng.Stopped()) 589 } 590 591 func createMarginEvent(party, market string, size int64) events.Margin { 592 return &marginStub{ 593 party: party, 594 market: market, 595 size: size, 596 } 597 } 598 599 func (m *marginStub) Party() string { 600 return m.party 601 } 602 603 func (m *marginStub) Size() int64 { 604 return m.size 605 } 606 607 func (m *marginStub) Buy() int64 { 608 return 0 609 } 610 611 func (m *marginStub) Sell() int64 { 612 return 0 613 } 614 615 func (m *marginStub) Price() *num.Uint { 616 return nil 617 } 618 619 func (m *marginStub) BuySumProduct() *num.Uint { 620 return nil 621 } 622 623 func (m *marginStub) SellSumProduct() *num.Uint { 624 return nil 625 } 626 627 func (m *marginStub) VWBuy() *num.Uint { 628 return nil 629 } 630 631 func (m *marginStub) VWSell() *num.Uint { 632 return nil 633 } 634 635 func (m *marginStub) AverageEntryPrice() *num.Uint { 636 return nil 637 } 638 639 func (m *marginStub) Asset() string { 640 return "" 641 } 642 643 func (m *marginStub) MarginBalance() *num.Uint { 644 return nil 645 } 646 647 func (m *marginStub) OrderMarginBalance() *num.Uint { 648 return nil 649 } 650 651 func (m *marginStub) GeneralBalance() *num.Uint { 652 return nil 653 } 654 655 func (m *marginStub) GeneralAccountBalance() *num.Uint { 656 return nil 657 } 658 659 func (m *marginStub) BondBalance() *num.Uint { 660 return nil 661 } 662 663 func (m *marginStub) MarketID() string { 664 return m.market 665 } 666 667 func (m *marginStub) MarginShortFall() *num.Uint { 668 return nil 669 } 670 671 func getTestEngine(t *testing.T, marketID string, config *types.LiquidationStrategy) *tstEngine { 672 t.Helper() 673 ctrl := gomock.NewController(t) 674 book := mocks.NewMockBook(ctrl) 675 idgen := mocks.NewMockIDGen(ctrl) 676 as := cmocks.NewMockAuctionState(ctrl) 677 broker := bmocks.NewMockBroker(ctrl) 678 tSvc := cmocks.NewMockTimeService(ctrl) 679 pe := mocks.NewMockPositions(ctrl) 680 pmon := mocks.NewMockPriceMonitor(ctrl) 681 amm := mocks.NewMockAMM(ctrl) 682 engine := liquidation.New(logging.NewDevLogger(), config, marketID, broker, book, as, tSvc, pe, pmon, amm) 683 return &tstEngine{ 684 Engine: engine, 685 ctrl: ctrl, 686 book: book, 687 idgen: idgen, 688 as: as, 689 broker: broker, 690 tSvc: tSvc, 691 pos: pe, 692 pmon: pmon, 693 amm: amm, 694 } 695 } 696 697 func (t *tstEngine) Finish() { 698 t.ctrl.Finish() 699 } 700 701 func (l SliceLenMatcher[T]) Matches(v any) bool { 702 sv, ok := v.([]T) 703 if !ok { 704 return false 705 } 706 return len(sv) == int(l) 707 } 708 709 func (l SliceLenMatcher[T]) String() string { 710 return fmt.Sprintf("matches slice of length %d", int(l)) 711 }