code.vegaprotocol.io/vega@v0.79.0/core/volumediscount/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 volumediscount_test 17 18 import ( 19 "bytes" 20 "context" 21 "testing" 22 "time" 23 24 "code.vegaprotocol.io/vega/core/events" 25 "code.vegaprotocol.io/vega/core/types" 26 "code.vegaprotocol.io/vega/core/volumediscount" 27 "code.vegaprotocol.io/vega/core/volumediscount/mocks" 28 "code.vegaprotocol.io/vega/libs/num" 29 "code.vegaprotocol.io/vega/libs/proto" 30 "code.vegaprotocol.io/vega/protos/vega" 31 snapshotpb "code.vegaprotocol.io/vega/protos/vega/snapshot/v1" 32 33 "github.com/golang/mock/gomock" 34 "github.com/stretchr/testify/require" 35 ) 36 37 func assertSnapshotMatches(t *testing.T, key string, expectedHash []byte) *volumediscount.SnapshottedEngine { 38 t.Helper() 39 40 loadCtrl := gomock.NewController(t) 41 loadBroker := mocks.NewMockBroker(loadCtrl) 42 loadMarketActivityTracker := mocks.NewMockMarketActivityTracker(loadCtrl) 43 loadEngine := volumediscount.NewSnapshottedEngine(loadBroker, loadMarketActivityTracker) 44 45 pl := snapshotpb.Payload{} 46 require.NoError(t, proto.Unmarshal(expectedHash, &pl)) 47 48 loadEngine.LoadState(context.Background(), types.PayloadFromProto(&pl)) 49 loadedHashEmpty, _, err := loadEngine.GetState(key) 50 require.NoError(t, err) 51 require.True(t, bytes.Equal(expectedHash, loadedHashEmpty)) 52 return loadEngine 53 } 54 55 func TestVolumeDiscountProgramLifecycle(t *testing.T) { 56 key := (&types.PayloadVolumeDiscountProgram{}).Key() 57 ctrl := gomock.NewController(t) 58 broker := mocks.NewMockBroker(ctrl) 59 marketActivityTracker := mocks.NewMockMarketActivityTracker(ctrl) 60 engine := volumediscount.NewSnapshottedEngine(broker, marketActivityTracker) 61 62 // test snapshot with empty engine 63 hashEmpty, _, err := engine.GetState(key) 64 require.NoError(t, err) 65 assertSnapshotMatches(t, key, hashEmpty) 66 67 now := time.Now() 68 69 p1 := &types.VolumeDiscountProgram{ 70 ID: "1", 71 Version: 0, 72 EndOfProgramTimestamp: now.Add(time.Hour * 1), 73 WindowLength: 1, 74 VolumeBenefitTiers: []*types.VolumeBenefitTier{ 75 {MinimumRunningNotionalTakerVolume: num.NewUint(1000), VolumeDiscountFactors: types.Factors{ 76 Infra: num.DecimalFromFloat(0.1), 77 Maker: num.DecimalFromFloat(0.1), 78 Liquidity: num.DecimalFromFloat(0.1), 79 }}, 80 {MinimumRunningNotionalTakerVolume: num.NewUint(2000), VolumeDiscountFactors: types.Factors{ 81 Infra: num.DecimalFromFloat(0.2), 82 Maker: num.DecimalFromFloat(0.2), 83 Liquidity: num.DecimalFromFloat(0.2), 84 }}, 85 {MinimumRunningNotionalTakerVolume: num.NewUint(3000), VolumeDiscountFactors: types.Factors{ 86 Infra: num.DecimalFromFloat(0.5), 87 Maker: num.DecimalFromFloat(0.5), 88 Liquidity: num.DecimalFromFloat(0.5), 89 }}, 90 {MinimumRunningNotionalTakerVolume: num.NewUint(4000), VolumeDiscountFactors: types.Factors{ 91 Infra: num.DecimalFromFloat(1), 92 Maker: num.DecimalFromFloat(1), 93 Liquidity: num.DecimalFromFloat(1), 94 }}, 95 }, 96 } 97 // add the program 98 engine.UpdateProgram(p1) 99 100 // expect an event for the started program 101 broker.EXPECT().Send(eventMatcher[*events.VolumeDiscountProgramStarted]{}).DoAndReturn(func(evt events.Event) { 102 e := evt.(*events.VolumeDiscountProgramStarted) 103 require.Equal(t, p1.IntoProto(), e.GetVolumeDiscountProgramStarted().Program) 104 }).Times(1) 105 // we expect the stats to be updated when a new program starts 106 broker.EXPECT().Send(eventMatcher[*events.VolumeDiscountStatsUpdated]{}).Times(1) 107 108 // activate the program 109 engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now}) 110 111 // check snapshot with new program 112 hashWithNew, _, err := engine.GetState(key) 113 require.NoError(t, err) 114 assertSnapshotMatches(t, key, hashWithNew) 115 116 // add a new program 117 p2 := &types.VolumeDiscountProgram{ 118 ID: "1", 119 Version: 1, 120 EndOfProgramTimestamp: now.Add(time.Hour * 2), 121 WindowLength: 1, 122 VolumeBenefitTiers: []*types.VolumeBenefitTier{ 123 {MinimumRunningNotionalTakerVolume: num.NewUint(2000), VolumeDiscountFactors: types.Factors{ 124 Maker: num.DecimalFromFloat(0.2), 125 Infra: num.DecimalFromFloat(0.2), 126 Liquidity: num.DecimalFromFloat(0.2), 127 }}, 128 {MinimumRunningNotionalTakerVolume: num.NewUint(3000), VolumeDiscountFactors: types.Factors{ 129 Maker: num.DecimalFromFloat(0.5), 130 Infra: num.DecimalFromFloat(0.5), 131 Liquidity: num.DecimalFromFloat(0.5), 132 }}, 133 {MinimumRunningNotionalTakerVolume: num.NewUint(1000), VolumeDiscountFactors: types.Factors{ 134 Maker: num.DecimalFromFloat(0.1), 135 Infra: num.DecimalFromFloat(0.1), 136 Liquidity: num.DecimalFromFloat(0.1), 137 }}, 138 {MinimumRunningNotionalTakerVolume: num.NewUint(4000), VolumeDiscountFactors: types.Factors{ 139 Maker: num.DecimalFromFloat(1), 140 Infra: num.DecimalFromFloat(1), 141 Liquidity: num.DecimalFromFloat(1), 142 }}, 143 }, 144 } 145 // add the new program 146 engine.UpdateProgram(p2) 147 148 // check snapshot with new program and current 149 hashWithNewAndCurrent, _, err := engine.GetState(key) 150 require.NoError(t, err) 151 assertSnapshotMatches(t, key, hashWithNewAndCurrent) 152 153 // // expect a program updated event 154 broker.EXPECT().Send(eventMatcher[*events.VolumeDiscountProgramUpdated]{}).DoAndReturn(func(evt events.Event) { 155 e := evt.(*events.VolumeDiscountProgramUpdated) 156 require.Equal(t, p2.IntoProto(), e.GetVolumeDiscountProgramUpdated().Program) 157 }).Times(1) 158 // expect the stats updated event 159 expectStatsUpdated(t, broker) 160 engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now.Add(time.Hour * 1)}) 161 162 // // expire the program 163 broker.EXPECT().Send(eventMatcher[*events.VolumeDiscountProgramEnded]{}).DoAndReturn(func(evt events.Event) { 164 e := evt.(*events.VolumeDiscountProgramEnded) 165 require.Equal(t, p2.Version, e.GetVolumeDiscountProgramEnded().Version) 166 }).Times(1) 167 engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now.Add(time.Hour * 2)}) 168 169 // check snapshot with terminated program 170 hashWithPostTermination, _, err := engine.GetState(key) 171 require.NoError(t, err) 172 assertSnapshotMatches(t, key, hashWithPostTermination) 173 } 174 175 func TestDiscountFactor(t *testing.T) { 176 key := (&types.PayloadVolumeDiscountProgram{}).Key() 177 ctrl := gomock.NewController(t) 178 broker := mocks.NewMockBroker(ctrl) 179 marketActivityTracker := mocks.NewMockMarketActivityTracker(ctrl) 180 engine := volumediscount.NewSnapshottedEngine(broker, marketActivityTracker) 181 182 currentTime := time.Now() 183 184 p1 := &types.VolumeDiscountProgram{ 185 ID: "1", 186 Version: 0, 187 EndOfProgramTimestamp: currentTime.Add(time.Hour * 1), 188 WindowLength: 1, 189 VolumeBenefitTiers: []*types.VolumeBenefitTier{ 190 {MinimumRunningNotionalTakerVolume: num.NewUint(1000), VolumeDiscountFactors: types.Factors{ 191 Maker: num.DecimalFromFloat(0.1), 192 Infra: num.DecimalFromFloat(0.1), 193 Liquidity: num.DecimalFromFloat(0.1), 194 }}, 195 {MinimumRunningNotionalTakerVolume: num.NewUint(2000), VolumeDiscountFactors: types.Factors{ 196 Maker: num.DecimalFromFloat(0.2), 197 Infra: num.DecimalFromFloat(0.2), 198 Liquidity: num.DecimalFromFloat(0.2), 199 }}, 200 {MinimumRunningNotionalTakerVolume: num.NewUint(3000), VolumeDiscountFactors: types.Factors{ 201 Maker: num.DecimalFromFloat(0.5), 202 Infra: num.DecimalFromFloat(0.5), 203 Liquidity: num.DecimalFromFloat(0.5), 204 }}, 205 {MinimumRunningNotionalTakerVolume: num.NewUint(4000), VolumeDiscountFactors: types.Factors{ 206 Maker: num.DecimalFromFloat(1), 207 Infra: num.DecimalFromFloat(1), 208 Liquidity: num.DecimalFromFloat(1), 209 }}, 210 }, 211 } 212 // add the program 213 engine.UpdateProgram(p1) 214 215 // activate the program 216 currentEpoch := uint64(1) 217 expectProgramStarted(t, broker, p1) 218 expectStatsUpdated(t, broker) 219 startEpoch(t, engine, currentEpoch, currentTime) 220 221 // so now we have a program active so at the end of the epoch lets return for some parties some notional 222 marketActivityTracker.EXPECT().NotionalTakerVolumeForAllParties().Return(map[types.PartyID]*num.Uint{ 223 "p1": num.NewUint(900), 224 "p2": num.NewUint(1000), 225 "p3": num.NewUint(1001), 226 "p4": num.NewUint(2000), 227 "p5": num.NewUint(3000), 228 "p6": num.NewUint(4000), 229 "p7": num.NewUint(5000), 230 }).Times(1) 231 232 // end the epoch to get the market activity recorded 233 expectStatsUpdatedWithUnqualifiedParties(t, broker) 234 currentTime = currentTime.Add(1 * time.Minute) 235 endEpoch(t, engine, currentEpoch, currentTime.Add(1*time.Minute)) 236 237 // start a new epoch for the discount factors to be in place 238 currentEpoch += 1 239 startEpoch(t, engine, currentEpoch, currentTime) 240 241 // check snapshot with terminated program 242 hashWithEpochNotionalsData, _, err := engine.GetState(key) 243 require.NoError(t, err) 244 loadedEngine := assertSnapshotMatches(t, key, hashWithEpochNotionalsData) 245 246 // party does not exist 247 require.Equal(t, num.DecimalZero(), engine.VolumeDiscountFactorForParty("p8").Infra) 248 require.Equal(t, num.DecimalZero(), loadedEngine.VolumeDiscountFactorForParty("p8").Infra) 249 // party is not eligible 250 require.Equal(t, num.DecimalZero(), engine.VolumeDiscountFactorForParty("p1").Infra) 251 require.Equal(t, num.DecimalZero(), loadedEngine.VolumeDiscountFactorForParty("p1").Infra) 252 // volume between 1000/2000 253 require.Equal(t, "0.1", engine.VolumeDiscountFactorForParty("p2").Infra.String()) 254 require.Equal(t, "0.1", loadedEngine.VolumeDiscountFactorForParty("p2").Infra.String()) 255 require.Equal(t, "0.1", engine.VolumeDiscountFactorForParty("p3").Infra.String()) 256 require.Equal(t, "0.1", loadedEngine.VolumeDiscountFactorForParty("p3").Infra.String()) 257 258 // volume 2000<=x<3000 259 require.Equal(t, "0.2", engine.VolumeDiscountFactorForParty("p4").Infra.String()) 260 require.Equal(t, "0.2", loadedEngine.VolumeDiscountFactorForParty("p4").Infra.String()) 261 262 // volume 3000<=x<4000 263 require.Equal(t, "0.5", engine.VolumeDiscountFactorForParty("p5").Infra.String()) 264 require.Equal(t, "0.5", loadedEngine.VolumeDiscountFactorForParty("p5").Infra.String()) 265 266 // volume >=4000 267 require.Equal(t, "1", engine.VolumeDiscountFactorForParty("p6").Infra.String()) 268 require.Equal(t, "1", loadedEngine.VolumeDiscountFactorForParty("p6").Infra.String()) 269 require.Equal(t, "1", engine.VolumeDiscountFactorForParty("p7").Infra.String()) 270 require.Equal(t, "1", loadedEngine.VolumeDiscountFactorForParty("p7").Infra.String()) 271 272 marketActivityTracker.EXPECT().NotionalTakerVolumeForAllParties().Return(map[types.PartyID]*num.Uint{}).Times(1) 273 274 expectStatsUpdated(t, broker) 275 currentTime = p1.EndOfProgramTimestamp 276 endEpoch(t, engine, currentEpoch, currentTime) 277 278 // terminate the program 279 currentEpoch += 1 280 expectProgramEnded(t, broker, p1) 281 startEpoch(t, engine, currentEpoch, currentTime) 282 283 hashAfterProgramEnded, _, err := engine.GetState(key) 284 require.NoError(t, err) 285 loadedEngine = assertSnapshotMatches(t, key, hashAfterProgramEnded) 286 287 // no discount for terminated program 288 for _, p := range []string{"p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8"} { 289 require.Equal(t, num.DecimalZero(), engine.VolumeDiscountFactorForParty(types.PartyID(p)).Infra) 290 require.Equal(t, num.DecimalZero(), loadedEngine.VolumeDiscountFactorForParty(types.PartyID(p)).Infra) 291 } 292 } 293 294 func TestDiscountFactorWithWindow(t *testing.T) { 295 key := (&types.PayloadVolumeDiscountProgram{}).Key() 296 ctrl := gomock.NewController(t) 297 broker := mocks.NewMockBroker(ctrl) 298 marketActivityTracker := mocks.NewMockMarketActivityTracker(ctrl) 299 engine := volumediscount.NewSnapshottedEngine(broker, marketActivityTracker) 300 301 currentTime := time.Now() 302 303 p1 := &types.VolumeDiscountProgram{ 304 ID: "1", 305 Version: 0, 306 EndOfProgramTimestamp: currentTime.Add(time.Hour * 1), 307 WindowLength: 2, 308 VolumeBenefitTiers: []*types.VolumeBenefitTier{ 309 {MinimumRunningNotionalTakerVolume: num.NewUint(1000), VolumeDiscountFactors: types.Factors{ 310 Maker: num.DecimalFromFloat(0.1), 311 Infra: num.DecimalFromFloat(0.1), 312 Liquidity: num.DecimalFromFloat(0.1), 313 }}, 314 {MinimumRunningNotionalTakerVolume: num.NewUint(2000), VolumeDiscountFactors: types.Factors{ 315 Maker: num.DecimalFromFloat(0.2), 316 Infra: num.DecimalFromFloat(0.2), 317 Liquidity: num.DecimalFromFloat(0.2), 318 }}, 319 {MinimumRunningNotionalTakerVolume: num.NewUint(3000), VolumeDiscountFactors: types.Factors{ 320 Maker: num.DecimalFromFloat(0.5), 321 Infra: num.DecimalFromFloat(0.5), 322 Liquidity: num.DecimalFromFloat(0.5), 323 }}, 324 {MinimumRunningNotionalTakerVolume: num.NewUint(4000), VolumeDiscountFactors: types.Factors{ 325 Maker: num.DecimalFromFloat(1), 326 Infra: num.DecimalFromFloat(1), 327 Liquidity: num.DecimalFromFloat(1), 328 }}, 329 }, 330 } 331 // add the program 332 engine.UpdateProgram(p1) 333 334 // expect an event for the started program 335 expectProgramStarted(t, broker, p1) 336 expectStatsUpdated(t, broker) 337 // activate the program 338 currentEpoch := uint64(1) 339 startEpoch(t, engine, currentEpoch, currentTime) 340 341 // so now we have a program active so at the end of the epoch lets return for some parties some notional 342 marketActivityTracker.EXPECT().NotionalTakerVolumeForAllParties().Return(map[types.PartyID]*num.Uint{ 343 "p1": num.NewUint(900), 344 "p2": num.NewUint(1000), 345 "p3": num.NewUint(1001), 346 "p4": num.NewUint(2000), 347 "p5": num.NewUint(3000), 348 "p6": num.NewUint(4000), 349 "p7": num.NewUint(5000), 350 }).Times(1) 351 352 expectStatsUpdatedWithUnqualifiedParties(t, broker) 353 currentTime = currentTime.Add(1 * time.Minute) 354 endEpoch(t, engine, currentEpoch, currentTime) 355 // start a new epoch for the discount factors to be in place 356 357 // party does not exist 358 require.Equal(t, num.DecimalZero(), engine.VolumeDiscountFactorForParty("p8").Infra) 359 // volume 900 360 require.Equal(t, num.DecimalZero(), engine.VolumeDiscountFactorForParty("p1").Infra) 361 // volume 1000 362 require.Equal(t, "0.1", engine.VolumeDiscountFactorForParty("p2").Infra.String()) 363 // volume 1001 364 require.Equal(t, "0.1", engine.VolumeDiscountFactorForParty("p3").Infra.String()) 365 // volume 2000 366 require.Equal(t, "0.2", engine.VolumeDiscountFactorForParty("p4").Infra.String()) 367 // volume 3000 368 require.Equal(t, "0.5", engine.VolumeDiscountFactorForParty("p5").Infra.String()) 369 // volume 4000 370 require.Equal(t, "1", engine.VolumeDiscountFactorForParty("p6").Infra.String()) 371 // volume 5000 372 require.Equal(t, "1", engine.VolumeDiscountFactorForParty("p7").Infra.String()) 373 374 // running for another epoch 375 marketActivityTracker.EXPECT().NotionalTakerVolumeForAllParties().Return(map[types.PartyID]*num.Uint{ 376 "p8": num.NewUint(2000), 377 "p1": num.NewUint(1500), 378 "p5": num.NewUint(4000), 379 "p6": num.NewUint(4000), 380 }).Times(1) 381 382 expectStatsUpdated(t, broker) 383 currentTime = currentTime.Add(1 * time.Minute) 384 endEpoch(t, engine, currentEpoch, currentTime) 385 386 currentEpoch += 1 387 startEpoch(t, engine, currentEpoch, currentTime) 388 389 hashAfter2Epochs, _, err := engine.GetState(key) 390 require.NoError(t, err) 391 loadedEngine := assertSnapshotMatches(t, key, hashAfter2Epochs) 392 393 // now p8 exists and the volume is 2000 394 require.Equal(t, "0.2", engine.VolumeDiscountFactorForParty("p8").Infra.String()) 395 require.Equal(t, "0.2", loadedEngine.VolumeDiscountFactorForParty("p8").Infra.String()) 396 // volume 2400 397 require.Equal(t, "0.2", engine.VolumeDiscountFactorForParty("p1").Infra.String()) 398 require.Equal(t, "0.2", loadedEngine.VolumeDiscountFactorForParty("p1").Infra.String()) 399 // volume 1000 400 require.Equal(t, "0.1", engine.VolumeDiscountFactorForParty("p2").Infra.String()) 401 require.Equal(t, "0.1", loadedEngine.VolumeDiscountFactorForParty("p2").Infra.String()) 402 // volume 1001 403 require.Equal(t, "0.1", engine.VolumeDiscountFactorForParty("p3").Infra.String()) 404 require.Equal(t, "0.1", loadedEngine.VolumeDiscountFactorForParty("p3").Infra.String()) 405 // volume 2000 406 require.Equal(t, "0.2", engine.VolumeDiscountFactorForParty("p4").Infra.String()) 407 require.Equal(t, "0.2", loadedEngine.VolumeDiscountFactorForParty("p4").Infra.String()) 408 // volume 7000 409 require.Equal(t, "1", engine.VolumeDiscountFactorForParty("p5").Infra.String()) 410 require.Equal(t, "1", loadedEngine.VolumeDiscountFactorForParty("p5").Infra.String()) 411 // volume 8000 412 require.Equal(t, "1", engine.VolumeDiscountFactorForParty("p6").Infra.String()) 413 require.Equal(t, "1", loadedEngine.VolumeDiscountFactorForParty("p6").Infra.String()) 414 // volume 5000 415 require.Equal(t, "1", engine.VolumeDiscountFactorForParty("p7").Infra.String()) 416 require.Equal(t, "1", loadedEngine.VolumeDiscountFactorForParty("p7").Infra.String()) 417 418 marketActivityTracker.EXPECT().NotionalTakerVolumeForAllParties().Return(map[types.PartyID]*num.Uint{}).Times(1) 419 420 expectStatsUpdated(t, broker) 421 currentTime = p1.EndOfProgramTimestamp 422 endEpoch(t, engine, currentEpoch, currentTime) 423 424 expectProgramEnded(t, broker, p1) 425 currentEpoch += 1 426 startEpoch(t, engine, currentEpoch, currentTime) 427 428 hashAfterProgramEnded, _, err := engine.GetState(key) 429 require.NoError(t, err) 430 loadedEngine = assertSnapshotMatches(t, key, hashAfterProgramEnded) 431 432 // no discount for terminated program 433 for _, p := range []string{"p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8"} { 434 require.Equal(t, num.DecimalZero(), engine.VolumeDiscountFactorForParty(types.PartyID(p)).Infra) 435 require.Equal(t, num.DecimalZero(), loadedEngine.VolumeDiscountFactorForParty(types.PartyID(p)).Infra) 436 } 437 }