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 }