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  }