code.vegaprotocol.io/vega@v0.79.0/core/liquidity/v2/sla_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 liquidity_test
    17  
    18  import (
    19  	"context"
    20  	"encoding/hex"
    21  	"fmt"
    22  	"testing"
    23  	"time"
    24  
    25  	bmocks "code.vegaprotocol.io/vega/core/broker/mocks"
    26  	mmocks "code.vegaprotocol.io/vega/core/execution/common/mocks"
    27  	"code.vegaprotocol.io/vega/core/integration/stubs"
    28  	"code.vegaprotocol.io/vega/core/liquidity/v2"
    29  	"code.vegaprotocol.io/vega/core/liquidity/v2/mocks"
    30  	"code.vegaprotocol.io/vega/core/types"
    31  	"code.vegaprotocol.io/vega/libs/num"
    32  	"code.vegaprotocol.io/vega/logging"
    33  
    34  	"github.com/golang/mock/gomock"
    35  	"github.com/stretchr/testify/require"
    36  )
    37  
    38  const partyID = "lp-party-1"
    39  
    40  type testEngine struct {
    41  	ctrl             *gomock.Controller
    42  	marketID         string
    43  	tsvc             *stubs.TimeStub
    44  	broker           *bmocks.MockBroker
    45  	riskModel        *mocks.MockRiskModel
    46  	priceMonitor     *mocks.MockPriceMonitor
    47  	orderbook        *mocks.MockOrderBook
    48  	auctionState     *mmocks.MockAuctionState
    49  	engine           *liquidity.SnapshotEngine
    50  	stateVar         *stubs.StateVarStub
    51  	defaultSLAParams *types.LiquiditySLAParams
    52  }
    53  
    54  func newTestEngine(t *testing.T) *testEngine {
    55  	t.Helper()
    56  	ctrl := gomock.NewController(t)
    57  
    58  	log := logging.NewTestLogger()
    59  	tsvc := stubs.NewTimeStub()
    60  
    61  	broker := bmocks.NewMockBroker(ctrl)
    62  	risk := mocks.NewMockRiskModel(ctrl)
    63  	monitor := mocks.NewMockPriceMonitor(ctrl)
    64  	orderbook := mocks.NewMockOrderBook(ctrl)
    65  	market := "market-id"
    66  	asset := "asset-id"
    67  	liquidityConfig := liquidity.NewDefaultConfig()
    68  	stateVarEngine := stubs.NewStateVar()
    69  	risk.EXPECT().GetProjectionHorizon().AnyTimes()
    70  
    71  	auctionState := mmocks.NewMockAuctionState(ctrl)
    72  
    73  	defaultSLAParams := &types.LiquiditySLAParams{
    74  		PriceRange:                  num.DecimalFromFloat(0.2), // priceRange
    75  		CommitmentMinTimeFraction:   num.DecimalFromFloat(0.5), // commitmentMinTimeFraction
    76  		SlaCompetitionFactor:        num.DecimalFromFloat(1),   // slaCompetitionFactor,
    77  		PerformanceHysteresisEpochs: 4,                         // performanceHysteresisEpochs
    78  	}
    79  
    80  	engine := liquidity.NewSnapshotEngine(
    81  		liquidityConfig,
    82  		log,
    83  		tsvc,
    84  		broker,
    85  		risk,
    86  		monitor,
    87  		orderbook,
    88  		auctionState,
    89  		asset,
    90  		market,
    91  		stateVarEngine,
    92  		num.NewDecimalFromFloat(1), // positionFactor
    93  		defaultSLAParams,
    94  	)
    95  
    96  	engine.OnNonPerformanceBondPenaltyMaxUpdate(num.DecimalFromFloat(0.5)) // nonPerformanceBondPenaltyMax
    97  	engine.OnNonPerformanceBondPenaltySlopeUpdate(num.DecimalFromFloat(2)) // nonPerformanceBondPenaltySlope
    98  	engine.OnStakeToCcyVolumeUpdate(num.DecimalFromInt64(1))
    99  
   100  	return &testEngine{
   101  		ctrl:             ctrl,
   102  		marketID:         market,
   103  		tsvc:             tsvc,
   104  		broker:           broker,
   105  		riskModel:        risk,
   106  		priceMonitor:     monitor,
   107  		orderbook:        orderbook,
   108  		auctionState:     auctionState,
   109  		engine:           engine,
   110  		stateVar:         stateVarEngine,
   111  		defaultSLAParams: defaultSLAParams,
   112  	}
   113  }
   114  
   115  type stubIDGen struct {
   116  	calls int
   117  }
   118  
   119  func (s *stubIDGen) NextID() string {
   120  	s.calls++
   121  	return hex.EncodeToString([]byte(fmt.Sprintf("deadb33f%d", s.calls)))
   122  }
   123  
   124  func toPoint[T any](v T) *T {
   125  	return &v
   126  }
   127  
   128  func generateOrders(idGen stubIDGen, marketID string, buys, sells []uint64) []*types.Order {
   129  	newOrder := func(price uint64, side types.Side) *types.Order {
   130  		return &types.Order{
   131  			ID:        idGen.NextID(),
   132  			MarketID:  marketID,
   133  			Party:     partyID,
   134  			Side:      side,
   135  			Price:     num.NewUint(price),
   136  			Remaining: price,
   137  			Status:    types.OrderStatusActive,
   138  		}
   139  	}
   140  
   141  	orders := []*types.Order{}
   142  	for _, price := range buys {
   143  		orders = append(orders, newOrder(price, types.SideBuy))
   144  	}
   145  
   146  	for _, price := range sells {
   147  		orders = append(orders, newOrder(price, types.SideSell))
   148  	}
   149  
   150  	return orders
   151  }
   152  
   153  func TestSLAPerformanceSingleEpochFeePenalty(t *testing.T) {
   154  	testCases := []struct {
   155  		desc string
   156  
   157  		// represents list of active orders by a party on a book in a given block
   158  		buyOrdersPerBlock   [][]uint64
   159  		sellsOrdersPerBlock [][]uint64
   160  
   161  		epochLength int
   162  
   163  		// optional net params to set
   164  		slaCompetitionFactor        *num.Decimal
   165  		commitmentMinTimeFraction   *num.Decimal
   166  		priceRange                  *num.Decimal
   167  		performanceHysteresisEpochs *uint64
   168  
   169  		// expected result
   170  		expectedPenalty num.Decimal
   171  	}{
   172  		{
   173  			desc:                 "Meets commitment with fraction_of_time_on_book=0.75 and slaCompetitionFactor=1, 0042-LIQF-037",
   174  			epochLength:          4,
   175  			buyOrdersPerBlock:    [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {}},
   176  			sellsOrdersPerBlock:  [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {}},
   177  			slaCompetitionFactor: toPoint(num.DecimalFromFloat(1)),
   178  			expectedPenalty:      num.DecimalFromFloat(0.5),
   179  		},
   180  		{
   181  			desc:                 "Meets commitment with fraction_of_time_on_book=0.75 and slaCompetitionFactor=1, 0042-LIQF-038",
   182  			epochLength:          4,
   183  			buyOrdersPerBlock:    [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}},
   184  			sellsOrdersPerBlock:  [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}},
   185  			slaCompetitionFactor: toPoint(num.DecimalFromFloat(1)),
   186  			expectedPenalty:      num.DecimalFromFloat(0.5),
   187  		},
   188  		{
   189  			desc:                 "Meets commitment with fraction_of_time_on_book=0.75 and slaCompetitionFactor=0, 0042-LIQF-041",
   190  			epochLength:          4,
   191  			buyOrdersPerBlock:    [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {}},
   192  			sellsOrdersPerBlock:  [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {}},
   193  			slaCompetitionFactor: toPoint(num.DecimalFromFloat(0)),
   194  			expectedPenalty:      num.DecimalFromFloat(0.0),
   195  		},
   196  		{
   197  			desc:                 "Meets commitment with fraction_of_time_on_book=0.75 and slaCompetitionFactor=0.5, 0042-LIQF-042",
   198  			epochLength:          4,
   199  			buyOrdersPerBlock:    [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {}},
   200  			sellsOrdersPerBlock:  [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {}},
   201  			slaCompetitionFactor: toPoint(num.DecimalFromFloat(0.5)),
   202  			expectedPenalty:      num.DecimalFromFloat(0.25),
   203  		},
   204  		{
   205  			desc:                        "Meets commitment with fraction_of_time_on_book=1 and performanceHysteresisEpochs=0, 0042-LIQF-035",
   206  			performanceHysteresisEpochs: toPoint[uint64](0),
   207  			epochLength:                 3,
   208  			buyOrdersPerBlock:           [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}},
   209  			sellsOrdersPerBlock:         [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}},
   210  			expectedPenalty:             num.DecimalFromFloat(0),
   211  		},
   212  		{
   213  			desc:                        "Does not meet commitment with fraction_of_time_on_book=0.5 and performanceHysteresisEpochs=0, 0042-LIQF-036",
   214  			performanceHysteresisEpochs: toPoint[uint64](0),
   215  			epochLength:                 6,
   216  			buyOrdersPerBlock:           [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {}, {15, 15, 17, 18, 12, 12, 12}, {}, {}, {15, 15, 17, 18, 12, 12, 12}},
   217  			sellsOrdersPerBlock:         [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {}, {15, 15, 17, 18, 12, 12, 12}, {}, {}, {15, 15, 17, 18, 12, 12, 12}},
   218  			expectedPenalty:             num.DecimalFromFloat(1),
   219  		},
   220  	}
   221  
   222  	for i := 0; i < 2; i++ {
   223  		inAuction := i != 0
   224  
   225  		for _, tC := range testCases {
   226  			desc := tC.desc
   227  			if inAuction {
   228  				desc = fmt.Sprintf("%s in auction", tC.desc)
   229  			}
   230  			t.Run(desc, func(t *testing.T) {
   231  				te := newTestEngine(t)
   232  
   233  				slaParams := te.defaultSLAParams.DeepClone()
   234  
   235  				// set the net params
   236  				if tC.slaCompetitionFactor != nil {
   237  					slaParams.SlaCompetitionFactor = *tC.slaCompetitionFactor
   238  				}
   239  				if tC.commitmentMinTimeFraction != nil {
   240  					slaParams.CommitmentMinTimeFraction = *tC.commitmentMinTimeFraction
   241  				}
   242  				if tC.priceRange != nil {
   243  					slaParams.PriceRange = *tC.priceRange
   244  				}
   245  				if tC.performanceHysteresisEpochs != nil {
   246  					slaParams.PerformanceHysteresisEpochs = *tC.performanceHysteresisEpochs
   247  				}
   248  
   249  				te.engine.UpdateMarketConfig(te.riskModel, te.priceMonitor)
   250  				te.engine.UpdateSLAParameters(slaParams)
   251  
   252  				idGen := &stubIDGen{}
   253  				ctx := context.Background()
   254  				party := "lp-party-1"
   255  
   256  				te.broker.EXPECT().Send(gomock.Any()).AnyTimes()
   257  				te.auctionState.EXPECT().IsOpeningAuction().Return(false).AnyTimes()
   258  				te.auctionState.EXPECT().InAuction().Return(inAuction).AnyTimes()
   259  
   260  				lps := &types.LiquidityProvisionSubmission{
   261  					MarketID:         te.marketID,
   262  					CommitmentAmount: num.NewUint(100),
   263  					Fee:              num.NewDecimalFromFloat(0.5),
   264  					Reference:        fmt.Sprintf("provision-by-%s", party),
   265  				}
   266  
   267  				_, err := te.engine.SubmitLiquidityProvision(ctx, lps, party, idGen)
   268  				require.NoError(t, err)
   269  
   270  				te.orderbook.EXPECT().GetLastTradedPrice().Return(num.NewUint(15)).AnyTimes()
   271  				te.orderbook.EXPECT().GetIndicativePrice().Return(num.NewUint(15)).AnyTimes()
   272  
   273  				orders := []*types.Order{}
   274  				te.orderbook.EXPECT().GetOrdersPerParty(party).DoAndReturn(func(party string) []*types.Order {
   275  					return orders
   276  				}).AnyTimes()
   277  
   278  				epochLength := time.Duration(tC.epochLength) * time.Second
   279  				epochStart := time.Now().Add(-epochLength)
   280  				epochEnd := epochStart.Add(epochLength)
   281  
   282  				orders = generateOrders(*idGen, te.marketID, tC.buyOrdersPerBlock[0], tC.sellsOrdersPerBlock[0])
   283  
   284  				one := num.UintOne()
   285  				positionFactor := num.DecimalOne()
   286  				midPrice := num.NewUint(15)
   287  
   288  				te.engine.ResetSLAEpoch(epochStart, one, midPrice, positionFactor)
   289  				te.engine.ApplyPendingProvisions(ctx, time.Now())
   290  
   291  				for i := 0; i < tC.epochLength; i++ {
   292  					orders = generateOrders(*idGen, te.marketID, tC.buyOrdersPerBlock[i], tC.sellsOrdersPerBlock[i])
   293  
   294  					te.tsvc.SetTime(epochStart.Add(time.Duration(i) * time.Second))
   295  					te.engine.EndBlock(one, midPrice, positionFactor)
   296  				}
   297  
   298  				penalties := te.engine.CalculateSLAPenalties(epochEnd)
   299  				sla := penalties.PenaltiesPerParty[party]
   300  
   301  				require.Truef(t, sla.Fee.Equal(tC.expectedPenalty), "actual penalty: %s, expected penalty: %s \n", sla.Fee, tC.expectedPenalty)
   302  			})
   303  		}
   304  	}
   305  }
   306  
   307  func TestSLAPerformanceMultiEpochFeePenalty(t *testing.T) {
   308  	testCases := []struct {
   309  		desc            string
   310  		epochsOffBook   int
   311  		epochsOnBook    int
   312  		startWithOnBook bool
   313  		expectedPenalty num.Decimal
   314  	}{
   315  		{
   316  			desc:            "Selects average hysteresis period penalty (3 epochs) over lower current penalty, 0042-LIQF-039",
   317  			epochsOffBook:   3,
   318  			epochsOnBook:    1,
   319  			expectedPenalty: num.DecimalFromFloat(0.75),
   320  		},
   321  		{
   322  			desc:            "Selects average hysteresis period penalty (2 epochs) of 0.5 over 2 epochs, 0042-LIQF-039",
   323  			epochsOffBook:   2,
   324  			epochsOnBook:    2,
   325  			expectedPenalty: num.DecimalFromFloat(0.5),
   326  		},
   327  		{
   328  			desc:            "Selects current higher penalty over hysteresis average period, 0042-LIQF-040",
   329  			epochsOnBook:    4,
   330  			startWithOnBook: true,
   331  			epochsOffBook:   1,
   332  			expectedPenalty: num.DecimalFromFloat(1),
   333  		},
   334  	}
   335  	for _, tC := range testCases {
   336  		t.Run(tC.desc, func(t *testing.T) {
   337  			te := newTestEngine(t)
   338  
   339  			slaParams := te.defaultSLAParams.DeepClone()
   340  			slaParams.PerformanceHysteresisEpochs = 4
   341  			te.engine.UpdateMarketConfig(te.riskModel, te.priceMonitor)
   342  			te.engine.UpdateSLAParameters(slaParams)
   343  
   344  			idGen := &stubIDGen{}
   345  			ctx := context.Background()
   346  
   347  			te.broker.EXPECT().Send(gomock.Any()).AnyTimes()
   348  			te.auctionState.EXPECT().IsOpeningAuction().Return(false).AnyTimes()
   349  
   350  			lps := &types.LiquidityProvisionSubmission{
   351  				MarketID:         te.marketID,
   352  				CommitmentAmount: num.NewUint(100),
   353  				Fee:              num.NewDecimalFromFloat(0.5),
   354  				Reference:        fmt.Sprintf("provision-by-%s", partyID),
   355  			}
   356  
   357  			_, err := te.engine.SubmitLiquidityProvision(ctx, lps, partyID, idGen)
   358  			require.NoError(t, err)
   359  
   360  			te.auctionState.EXPECT().InAuction().Return(false).AnyTimes()
   361  
   362  			te.orderbook.EXPECT().GetLastTradedPrice().Return(num.NewUint(15)).AnyTimes()
   363  			te.orderbook.EXPECT().GetIndicativePrice().Return(num.NewUint(15)).AnyTimes()
   364  
   365  			orders := []*types.Order{}
   366  			te.orderbook.EXPECT().GetOrdersPerParty(partyID).DoAndReturn(func(party string) []*types.Order {
   367  				return orders
   368  			}).AnyTimes()
   369  
   370  			epochLength := time.Duration(4) * time.Second
   371  			epochStart := time.Now().Add(-epochLength)
   372  			epochEnd := epochStart.Add(epochLength)
   373  
   374  			firstEpochIters := tC.epochsOffBook
   375  			secondEpochIters := tC.epochsOnBook
   376  
   377  			if tC.startWithOnBook {
   378  				orders = generateOrders(*idGen, te.marketID, []uint64{15, 15, 17, 18, 12, 12, 12}, []uint64{15, 15, 17, 18, 12, 12, 12})
   379  				firstEpochIters = tC.epochsOnBook
   380  				secondEpochIters = tC.epochsOffBook
   381  			}
   382  
   383  			one := num.UintOne()
   384  			positionFactor := num.DecimalOne()
   385  			midPrice := num.NewUint(15)
   386  
   387  			for i := 0; i < firstEpochIters; i++ {
   388  				te.engine.ResetSLAEpoch(epochStart, one, midPrice, positionFactor)
   389  				te.engine.ApplyPendingProvisions(ctx, time.Now())
   390  
   391  				for j := 0; j < int(epochLength.Seconds()); j++ {
   392  					te.tsvc.SetTime(epochStart.Add(time.Duration(j) * time.Second))
   393  					te.engine.EndBlock(one, midPrice, positionFactor)
   394  				}
   395  
   396  				te.engine.CalculateSLAPenalties(epochEnd)
   397  			}
   398  
   399  			if tC.startWithOnBook {
   400  				orders = []*types.Order{}
   401  			} else {
   402  				orders = generateOrders(*idGen, te.marketID, []uint64{15, 15, 17, 18, 12, 12, 12}, []uint64{15, 15, 17, 18, 12, 12, 12})
   403  			}
   404  
   405  			for i := 0; i < secondEpochIters; i++ {
   406  				te.engine.ResetSLAEpoch(epochStart, one, midPrice, positionFactor)
   407  				te.engine.ApplyPendingProvisions(ctx, time.Now())
   408  
   409  				for j := 0; j < int(epochLength.Seconds()); j++ {
   410  					te.tsvc.SetTime(epochStart.Add(time.Duration(j) * time.Second))
   411  					te.engine.EndBlock(one, midPrice, positionFactor)
   412  				}
   413  
   414  				te.engine.CalculateSLAPenalties(epochEnd)
   415  			}
   416  
   417  			penalties := te.engine.CalculateSLAPenalties(epochEnd)
   418  			sla := penalties.PenaltiesPerParty[partyID]
   419  
   420  			require.Truef(t, sla.Fee.Equal(tC.expectedPenalty), "actual penalty: %s, expected penalty: %s \n", sla.Fee, tC.expectedPenalty)
   421  		})
   422  	}
   423  }
   424  
   425  func TestSLAPerformanceBondPenalty(t *testing.T) {
   426  	testCases := []struct {
   427  		desc string
   428  
   429  		// represents list of active orders by a party on a book in a given block
   430  		buyOrdersPerBlock   [][]uint64
   431  		sellsOrdersPerBlock [][]uint64
   432  
   433  		epochLength int
   434  
   435  		// optional net params to set
   436  		commitmentMinTimeFraction      *num.Decimal
   437  		nonPerformanceBondPenaltySlope *num.Decimal
   438  		nonPerformanceBondPenaltyMax   *num.Decimal
   439  
   440  		// expected result
   441  		expectedPenalty num.Decimal
   442  	}{
   443  		{
   444  			desc:                      "Bond account penalty is 0 when commitment is met, 0044-LIME-013",
   445  			epochLength:               3,
   446  			buyOrdersPerBlock:         [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}},
   447  			sellsOrdersPerBlock:       [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}},
   448  			commitmentMinTimeFraction: toPoint(num.NewDecimalFromFloat(0.6)),
   449  			expectedPenalty:           num.DecimalFromFloat(0),
   450  		},
   451  		{
   452  			desc:        "Bond account penalty is 35%, 0044-LIME-014",
   453  			epochLength: 10,
   454  			buyOrdersPerBlock: [][]uint64{
   455  				{}, {}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {}, {}, {}, {}, {},
   456  			},
   457  			sellsOrdersPerBlock: [][]uint64{
   458  				{}, {}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {}, {}, {}, {}, {},
   459  			},
   460  			commitmentMinTimeFraction:      toPoint(num.NewDecimalFromFloat(0.6)),
   461  			nonPerformanceBondPenaltySlope: toPoint(num.NewDecimalFromFloat(0.7)),
   462  			nonPerformanceBondPenaltyMax:   toPoint(num.NewDecimalFromFloat(0.6)),
   463  			expectedPenalty:                num.DecimalFromFloat(0.35),
   464  		},
   465  		{
   466  			desc:                           "Bond account penalty is 60%, 0044-LIME-015",
   467  			epochLength:                    3,
   468  			buyOrdersPerBlock:              [][]uint64{{}, {}, {}},
   469  			sellsOrdersPerBlock:            [][]uint64{{}, {}, {}},
   470  			commitmentMinTimeFraction:      toPoint(num.NewDecimalFromFloat(0.6)),
   471  			nonPerformanceBondPenaltySlope: toPoint(num.NewDecimalFromFloat(0.7)),
   472  			nonPerformanceBondPenaltyMax:   toPoint(num.NewDecimalFromFloat(0.6)),
   473  			expectedPenalty:                num.DecimalFromFloat(0.6),
   474  		},
   475  		{
   476  			desc:                           "Bond account penalty is 20%, 0044-LIME-016",
   477  			epochLength:                    3,
   478  			buyOrdersPerBlock:              [][]uint64{{}, {}, {}},
   479  			sellsOrdersPerBlock:            [][]uint64{{}, {}, {}},
   480  			commitmentMinTimeFraction:      toPoint(num.NewDecimalFromFloat(0.6)),
   481  			nonPerformanceBondPenaltySlope: toPoint(num.NewDecimalFromFloat(0.2)),
   482  			nonPerformanceBondPenaltyMax:   toPoint(num.NewDecimalFromFloat(0.6)),
   483  			expectedPenalty:                num.DecimalFromFloat(0.2),
   484  		},
   485  	}
   486  
   487  	for _, tC := range testCases {
   488  		t.Run(tC.desc, func(t *testing.T) {
   489  			te := newTestEngine(t)
   490  			slaParams := te.defaultSLAParams.DeepClone()
   491  			if tC.commitmentMinTimeFraction != nil {
   492  				slaParams.CommitmentMinTimeFraction = *tC.commitmentMinTimeFraction
   493  			}
   494  			te.engine.UpdateMarketConfig(te.riskModel, te.priceMonitor)
   495  			te.engine.UpdateSLAParameters(slaParams)
   496  
   497  			if tC.nonPerformanceBondPenaltySlope != nil {
   498  				te.engine.OnNonPerformanceBondPenaltySlopeUpdate(*tC.nonPerformanceBondPenaltySlope)
   499  			}
   500  			if tC.nonPerformanceBondPenaltyMax != nil {
   501  				te.engine.OnNonPerformanceBondPenaltyMaxUpdate(*tC.nonPerformanceBondPenaltyMax)
   502  			}
   503  
   504  			idGen := &stubIDGen{}
   505  			ctx := context.Background()
   506  			party := "lp-party-1"
   507  
   508  			te.broker.EXPECT().Send(gomock.Any()).AnyTimes()
   509  			te.auctionState.EXPECT().IsOpeningAuction().Return(false).AnyTimes()
   510  
   511  			lps := &types.LiquidityProvisionSubmission{
   512  				MarketID:         te.marketID,
   513  				CommitmentAmount: num.NewUint(100),
   514  				Fee:              num.NewDecimalFromFloat(0.5),
   515  				Reference:        fmt.Sprintf("provision-by-%s", party),
   516  			}
   517  
   518  			_, err := te.engine.SubmitLiquidityProvision(ctx, lps, party, idGen)
   519  			require.NoError(t, err)
   520  
   521  			te.auctionState.EXPECT().InAuction().Return(false).AnyTimes()
   522  
   523  			te.orderbook.EXPECT().GetLastTradedPrice().Return(num.NewUint(15)).AnyTimes()
   524  			te.orderbook.EXPECT().GetIndicativePrice().Return(num.NewUint(15)).AnyTimes()
   525  
   526  			orders := []*types.Order{}
   527  			te.orderbook.EXPECT().GetOrdersPerParty(party).DoAndReturn(func(party string) []*types.Order {
   528  				return orders
   529  			}).AnyTimes()
   530  
   531  			epochLength := time.Duration(tC.epochLength) * time.Second
   532  			epochStart := time.Now().Add(-epochLength)
   533  			epochEnd := epochStart.Add(epochLength)
   534  
   535  			orders = generateOrders(*idGen, te.marketID, tC.buyOrdersPerBlock[0], tC.sellsOrdersPerBlock[0])
   536  
   537  			one := num.UintOne()
   538  			positionFactor := num.DecimalOne()
   539  			midPrice := num.NewUint(15)
   540  
   541  			te.engine.ResetSLAEpoch(epochStart, one, midPrice, positionFactor)
   542  			te.engine.ApplyPendingProvisions(ctx, time.Now())
   543  
   544  			for i := 0; i < tC.epochLength; i++ {
   545  				orders = generateOrders(*idGen, te.marketID, tC.buyOrdersPerBlock[i], tC.sellsOrdersPerBlock[i])
   546  
   547  				te.tsvc.SetTime(epochStart.Add(time.Duration(i) * time.Second))
   548  				te.engine.EndBlock(one, midPrice, positionFactor)
   549  			}
   550  
   551  			penalties := te.engine.CalculateSLAPenalties(epochEnd)
   552  			sla := penalties.PenaltiesPerParty[party]
   553  
   554  			require.Truef(t, sla.Bond.Equal(tC.expectedPenalty), "actual penalty: %s, expected penalty: %s \n", sla.Bond, tC.expectedPenalty)
   555  		})
   556  	}
   557  }
   558  
   559  func TestSLAParamChangePushesOutOfCommitment(t *testing.T) {
   560  	te := newTestEngine(t)
   561  	slaParams := te.defaultSLAParams.DeepClone()
   562  	te.engine.UpdateMarketConfig(te.riskModel, te.priceMonitor)
   563  	te.engine.UpdateSLAParameters(slaParams)
   564  
   565  	idGen := &stubIDGen{}
   566  	ctx := context.Background()
   567  	party := "lp-party-1"
   568  
   569  	te.broker.EXPECT().Send(gomock.Any()).AnyTimes()
   570  	te.auctionState.EXPECT().IsOpeningAuction().Return(false).AnyTimes()
   571  
   572  	lps := &types.LiquidityProvisionSubmission{
   573  		MarketID:         te.marketID,
   574  		CommitmentAmount: num.NewUint(100),
   575  		Fee:              num.NewDecimalFromFloat(0.5),
   576  		Reference:        fmt.Sprintf("provision-by-%s", party),
   577  	}
   578  
   579  	buyOrdersPerBlock := [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}}
   580  	sellsOrdersPerBlock := [][]uint64{{15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}, {15, 15, 17, 18, 12, 12, 12}}
   581  
   582  	_, err := te.engine.SubmitLiquidityProvision(ctx, lps, party, idGen)
   583  	require.NoError(t, err)
   584  
   585  	one := num.UintOne()
   586  	positionFactor := num.DecimalOne()
   587  	midPrice := num.NewUint(15)
   588  
   589  	epochLength := time.Duration(100) * time.Second
   590  	epochStart := time.Now()
   591  	epochEnd := time.Now().Add(epochLength)
   592  	epochEndAgain := time.Now().Add(2 * epochLength)
   593  
   594  	te.engine.ResetSLAEpoch(epochStart, one, midPrice, positionFactor)
   595  
   596  	te.engine.ApplyPendingProvisions(ctx, epochStart)
   597  
   598  	te.auctionState.EXPECT().InAuction().Return(false).AnyTimes()
   599  
   600  	te.orderbook.EXPECT().GetLastTradedPrice().Return(num.NewUint(15)).AnyTimes()
   601  	te.orderbook.EXPECT().GetIndicativePrice().Return(num.NewUint(15)).AnyTimes()
   602  
   603  	orders := []*types.Order{}
   604  	te.orderbook.EXPECT().GetOrdersPerParty(party).DoAndReturn(func(party string) []*types.Order {
   605  		return orders
   606  	}).AnyTimes()
   607  
   608  	orders = generateOrders(*idGen, te.marketID, buyOrdersPerBlock[0], sellsOrdersPerBlock[0])
   609  
   610  	te.tsvc.SetTime(epochStart)
   611  	te.engine.EndBlock(one, midPrice, positionFactor)
   612  
   613  	// LP meets commitment right up til the end of the epoch
   614  	te.tsvc.SetTime(epochEnd)
   615  	te.engine.EndBlock(one, midPrice, positionFactor)
   616  	te.engine.CalculateSLAPenalties(epochEnd)
   617  	stats := te.engine.LiquidityProviderSLAStats(epochEnd)
   618  	require.Equal(t, "1", stats[0].CurrentEpochFractionOfTimeOnBook)
   619  
   620  	// now update the params so that the new update parameters mean they do not meet the commitment
   621  	te.engine.ResetSLAEpoch(epochEnd, one, num.NewUint(15000), positionFactor)
   622  
   623  	// at the end of the next epoch the time on book should be 0
   624  	te.engine.CalculateSLAPenalties(epochEndAgain)
   625  	stats = te.engine.LiquidityProviderSLAStats(epochEndAgain)
   626  	require.Equal(t, "0", stats[0].CurrentEpochFractionOfTimeOnBook)
   627  }