code.vegaprotocol.io/vega@v0.79.0/core/volumerebate/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 volumerebate_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/volumerebate" 27 "code.vegaprotocol.io/vega/core/volumerebate/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) *volumerebate.SnapshottedEngine { 38 t.Helper() 39 40 loadCtrl := gomock.NewController(t) 41 loadBroker := mocks.NewMockBroker(loadCtrl) 42 loadMarketActivityTracker := mocks.NewMockMarketActivityTracker(loadCtrl) 43 loadEngine := volumerebate.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 TestVolumeRebateProgramLifecycle(t *testing.T) { 56 key := (&types.PayloadVolumeRebateProgram{}).Key() 57 ctrl := gomock.NewController(t) 58 broker := mocks.NewMockBroker(ctrl) 59 marketActivityTracker := mocks.NewMockMarketActivityTracker(ctrl) 60 engine := volumerebate.NewSnapshottedEngine(broker, marketActivityTracker) 61 marketActivityTracker.EXPECT().CalculateTotalMakerContributionInQuantum(gomock.Any()).Return(map[string]*num.Uint{}, map[string]num.Decimal{}).Times(2) 62 63 // test snapshot with empty engine 64 hashEmpty, _, err := engine.GetState(key) 65 require.NoError(t, err) 66 assertSnapshotMatches(t, key, hashEmpty) 67 68 now := time.Now() 69 70 p1 := &types.VolumeRebateProgram{ 71 ID: "1", 72 Version: 0, 73 EndOfProgramTimestamp: now.Add(time.Hour * 1), 74 WindowLength: 1, 75 VolumeRebateBenefitTiers: []*types.VolumeRebateBenefitTier{ 76 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.1000), AdditionalMakerRebate: num.DecimalFromFloat(0.1)}, 77 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.2000), AdditionalMakerRebate: num.DecimalFromFloat(0.2)}, 78 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.3000), AdditionalMakerRebate: num.DecimalFromFloat(0.5)}, 79 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.4000), AdditionalMakerRebate: num.DecimalFromFloat(1)}, 80 }, 81 } 82 // add the program 83 engine.UpdateProgram(p1) 84 85 // expect an event for the started program 86 broker.EXPECT().Send(startedEvt).DoAndReturn(func(evt events.Event) { 87 e := startedEvt.cast(evt) 88 require.Equal(t, p1.IntoProto(), e.GetVolumeRebateProgramStarted().Program) 89 }).Times(1) 90 broker.EXPECT().Send(statsEvt).Times(1) 91 92 // activate the program 93 engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now}) 94 95 // check snapshot with new program 96 hashWithNew, _, err := engine.GetState(key) 97 require.NoError(t, err) 98 assertSnapshotMatches(t, key, hashWithNew) 99 100 // add a new program 101 p2 := &types.VolumeRebateProgram{ 102 ID: "1", 103 Version: 1, 104 EndOfProgramTimestamp: now.Add(time.Hour * 2), 105 WindowLength: 1, 106 VolumeRebateBenefitTiers: []*types.VolumeRebateBenefitTier{ 107 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.2000), AdditionalMakerRebate: num.DecimalFromFloat(0.2)}, 108 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.3000), AdditionalMakerRebate: num.DecimalFromFloat(0.5)}, 109 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.1000), AdditionalMakerRebate: num.DecimalFromFloat(0.1)}, 110 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.4000), AdditionalMakerRebate: num.DecimalFromFloat(1)}, 111 }, 112 } 113 // add the new program 114 engine.UpdateProgram(p2) 115 116 // check snapshot with new program and current 117 hashWithNewAndCurrent, _, err := engine.GetState(key) 118 require.NoError(t, err) 119 assertSnapshotMatches(t, key, hashWithNewAndCurrent) 120 121 // // expect a program updated event 122 broker.EXPECT().Send(updatedEvt).DoAndReturn(func(evt events.Event) { 123 e := evt.(*events.VolumeRebateProgramUpdated) 124 require.Equal(t, p2.IntoProto(), e.GetVolumeRebateProgramUpdated().Program) 125 }).Times(1) 126 broker.EXPECT().Send(statsEvt).Times(1) 127 engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now.Add(time.Hour * 1)}) 128 129 // // expire the program 130 broker.EXPECT().Send(endedEvt).DoAndReturn(func(evt events.Event) { 131 e := evt.(*events.VolumeRebateProgramEnded) 132 require.Equal(t, p2.Version, e.GetVolumeRebateProgramEnded().Version) 133 }).Times(1) 134 engine.OnEpoch(context.Background(), types.Epoch{Action: vega.EpochAction_EPOCH_ACTION_START, StartTime: now.Add(time.Hour * 2)}) 135 136 // check snapshot with terminated program 137 hashWithPostTermination, _, err := engine.GetState(key) 138 require.NoError(t, err) 139 assertSnapshotMatches(t, key, hashWithPostTermination) 140 } 141 142 func TestRebateFactor(t *testing.T) { 143 key := (&types.PayloadVolumeRebateProgram{}).Key() 144 ctrl := gomock.NewController(t) 145 broker := mocks.NewMockBroker(ctrl) 146 marketActivityTracker := mocks.NewMockMarketActivityTracker(ctrl) 147 engine := volumerebate.NewSnapshottedEngine(broker, marketActivityTracker) 148 engine.OnMarketFeeFactorsBuyBackFeeUpdate(context.Background(), num.NewDecimalFromFloat(0.5)) 149 engine.OnMarketFeeFactorsTreasuryFeeUpdate(context.Background(), num.NewDecimalFromFloat(0.5)) 150 currentTime := time.Now() 151 152 p1 := &types.VolumeRebateProgram{ 153 ID: "1", 154 Version: 0, 155 EndOfProgramTimestamp: currentTime.Add(time.Hour * 1), 156 WindowLength: 1, 157 VolumeRebateBenefitTiers: []*types.VolumeRebateBenefitTier{ 158 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.1000), AdditionalMakerRebate: num.DecimalFromFloat(0.1)}, 159 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.2000), AdditionalMakerRebate: num.DecimalFromFloat(0.2)}, 160 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.3000), AdditionalMakerRebate: num.DecimalFromFloat(0.5)}, 161 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.4000), AdditionalMakerRebate: num.DecimalFromFloat(1)}, 162 }, 163 } 164 // add the program 165 engine.UpdateProgram(p1) 166 167 // activate the program 168 currentEpoch := uint64(1) 169 expectProgramStarted(t, broker, p1) 170 marketActivityTracker.EXPECT().CalculateTotalMakerContributionInQuantum(gomock.Any()).Return(map[string]*num.Uint{}, map[string]num.Decimal{}).Times(1) 171 expectStatsUpdated(t, broker) 172 startEpoch(t, engine, currentEpoch, currentTime) 173 174 // so now we have a program active so at the end of the epoch lets return for some parties some notional 175 marketActivityTracker.EXPECT().CalculateTotalMakerContributionInQuantum(gomock.Any()).Return(map[string]*num.Uint{ 176 "p1": num.NewUint(900), 177 "p2": num.NewUint(1000), 178 "p3": num.NewUint(1001), 179 "p4": num.NewUint(2000), 180 "p5": num.NewUint(3000), 181 "p6": num.NewUint(4000), 182 "p7": num.NewUint(5000), 183 }, 184 map[string]num.Decimal{ 185 "p1": num.DecimalFromFloat(0.09), 186 "p2": num.DecimalFromFloat(0.1000), 187 "p3": num.DecimalFromFloat(0.1001), 188 "p4": num.DecimalFromFloat(0.2000), 189 "p5": num.DecimalFromFloat(0.3000), 190 "p6": num.DecimalFromFloat(0.4000), 191 "p7": num.DecimalFromFloat(0.5000), 192 }).Times(1) 193 194 // end the epoch to get the market activity recorded 195 expectStatsUpdatedWithUnqualifiedParties(t, broker) 196 currentTime = currentTime.Add(1 * time.Minute) 197 endEpoch(t, engine, currentEpoch, currentTime.Add(1*time.Minute)) 198 199 // start a new epoch for the rebate factors to be in place 200 currentEpoch += 1 201 startEpoch(t, engine, currentEpoch, currentTime) 202 203 // check snapshot with terminated program 204 hashWithEpochNotionalsData, _, err := engine.GetState(key) 205 require.NoError(t, err) 206 loadedEngine := assertSnapshotMatches(t, key, hashWithEpochNotionalsData) 207 loadedEngine.OnMarketFeeFactorsBuyBackFeeUpdate(context.Background(), num.NewDecimalFromFloat(0.5)) 208 loadedEngine.OnMarketFeeFactorsTreasuryFeeUpdate(context.Background(), num.NewDecimalFromFloat(0.5)) 209 210 // party does not exist 211 require.Equal(t, num.DecimalZero(), engine.VolumeRebateFactorForParty("p8")) 212 require.Equal(t, num.DecimalZero(), loadedEngine.VolumeRebateFactorForParty("p8")) 213 // party is not eligible 214 require.Equal(t, num.DecimalZero(), engine.VolumeRebateFactorForParty("p1")) 215 require.Equal(t, num.DecimalZero(), loadedEngine.VolumeRebateFactorForParty("p1")) 216 // volume between 1000/2000 217 require.Equal(t, "0.1", engine.VolumeRebateFactorForParty("p2").String()) 218 require.Equal(t, "0.1", loadedEngine.VolumeRebateFactorForParty("p2").String()) 219 require.Equal(t, "0.1", engine.VolumeRebateFactorForParty("p3").String()) 220 require.Equal(t, "0.1", loadedEngine.VolumeRebateFactorForParty("p3").String()) 221 222 // volume 2000<=x<3000 223 require.Equal(t, "0.2", engine.VolumeRebateFactorForParty("p4").String()) 224 require.Equal(t, "0.2", loadedEngine.VolumeRebateFactorForParty("p4").String()) 225 226 // volume 3000<=x<4000 227 require.Equal(t, "0.5", engine.VolumeRebateFactorForParty("p5").String()) 228 require.Equal(t, "0.5", loadedEngine.VolumeRebateFactorForParty("p5").String()) 229 230 // volume >=4000 231 require.Equal(t, "1", engine.VolumeRebateFactorForParty("p6").String()) 232 require.Equal(t, "1", loadedEngine.VolumeRebateFactorForParty("p6").String()) 233 require.Equal(t, "1", engine.VolumeRebateFactorForParty("p7").String()) 234 require.Equal(t, "1", loadedEngine.VolumeRebateFactorForParty("p7").String()) 235 236 marketActivityTracker.EXPECT().CalculateTotalMakerContributionInQuantum(gomock.Any()).Return(map[string]*num.Uint{}, map[string]num.Decimal{}).Times(1) 237 238 expectStatsUpdated(t, broker) 239 currentTime = p1.EndOfProgramTimestamp 240 endEpoch(t, engine, currentEpoch, currentTime) 241 242 // terminate the program 243 currentEpoch += 1 244 expectProgramEnded(t, broker, p1) 245 startEpoch(t, engine, currentEpoch, currentTime) 246 247 hashAfterProgramEnded, _, err := engine.GetState(key) 248 require.NoError(t, err) 249 loadedEngine = assertSnapshotMatches(t, key, hashAfterProgramEnded) 250 251 // no rebate for terminated program 252 for _, p := range []string{"p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8"} { 253 require.Equal(t, num.DecimalZero(), engine.VolumeRebateFactorForParty(types.PartyID(p))) 254 require.Equal(t, num.DecimalZero(), loadedEngine.VolumeRebateFactorForParty(types.PartyID(p))) 255 } 256 } 257 258 func TestRebateFactorWithWindow(t *testing.T) { 259 key := (&types.PayloadVolumeRebateProgram{}).Key() 260 ctrl := gomock.NewController(t) 261 broker := mocks.NewMockBroker(ctrl) 262 marketActivityTracker := mocks.NewMockMarketActivityTracker(ctrl) 263 engine := volumerebate.NewSnapshottedEngine(broker, marketActivityTracker) 264 engine.OnMarketFeeFactorsBuyBackFeeUpdate(context.Background(), num.DecimalFromFloat(0.5)) 265 engine.OnMarketFeeFactorsTreasuryFeeUpdate(context.Background(), num.DecimalFromFloat(0.5)) 266 currentTime := time.Now() 267 268 p1 := &types.VolumeRebateProgram{ 269 ID: "1", 270 Version: 0, 271 EndOfProgramTimestamp: currentTime.Add(time.Hour * 1), 272 WindowLength: 2, 273 VolumeRebateBenefitTiers: []*types.VolumeRebateBenefitTier{ 274 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.1), AdditionalMakerRebate: num.DecimalFromFloat(0.1)}, 275 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.2), AdditionalMakerRebate: num.DecimalFromFloat(0.2)}, 276 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.3), AdditionalMakerRebate: num.DecimalFromFloat(0.5)}, 277 {MinimumPartyMakerVolumeFraction: num.DecimalFromFloat(0.4), AdditionalMakerRebate: num.DecimalFromFloat(1)}, 278 }, 279 } 280 // add the program 281 engine.UpdateProgram(p1) 282 283 // expect an event for the started program 284 expectProgramStarted(t, broker, p1) 285 marketActivityTracker.EXPECT().CalculateTotalMakerContributionInQuantum(gomock.Any()).Return(map[string]*num.Uint{}, map[string]num.Decimal{}).Times(1) 286 expectStatsUpdated(t, broker) 287 // activate the program 288 currentEpoch := uint64(1) 289 startEpoch(t, engine, currentEpoch, currentTime) 290 291 // so now we have a program active so at the end of the epoch lets return for some parties some notional 292 marketActivityTracker.EXPECT().CalculateTotalMakerContributionInQuantum(gomock.Any()).Return( 293 map[string]*num.Uint{ 294 "p1": num.NewUint(900), 295 "p2": num.NewUint(1000), 296 "p3": num.NewUint(1001), 297 "p4": num.NewUint(2000), 298 "p5": num.NewUint(3000), 299 "p6": num.NewUint(4000), 300 "p7": num.NewUint(5000), 301 }, map[string]num.Decimal{ 302 "p1": num.DecimalFromFloat(0.0900), 303 "p2": num.DecimalFromFloat(0.1000), 304 "p3": num.DecimalFromFloat(0.1001), 305 "p4": num.DecimalFromFloat(0.2000), 306 "p5": num.DecimalFromFloat(0.3000), 307 "p6": num.DecimalFromFloat(0.4000), 308 "p7": num.DecimalFromFloat(0.5000), 309 }).Times(1) 310 311 expectStatsUpdatedWithUnqualifiedParties(t, broker) 312 currentTime = currentTime.Add(1 * time.Minute) 313 endEpoch(t, engine, currentEpoch, currentTime) 314 // start a new epoch for the rebate factors to be in place 315 316 // party does not exist 317 require.Equal(t, num.DecimalZero(), engine.VolumeRebateFactorForParty("p8")) 318 // volume 900 319 require.Equal(t, num.DecimalZero(), engine.VolumeRebateFactorForParty("p1")) 320 // volume 1000 321 require.Equal(t, "0.1", engine.VolumeRebateFactorForParty("p2").String()) 322 // volume 1001 323 require.Equal(t, "0.1", engine.VolumeRebateFactorForParty("p3").String()) 324 // volume 2000 325 require.Equal(t, "0.2", engine.VolumeRebateFactorForParty("p4").String()) 326 // volume 3000 327 require.Equal(t, "0.5", engine.VolumeRebateFactorForParty("p5").String()) 328 // volume 4000 329 require.Equal(t, "1", engine.VolumeRebateFactorForParty("p6").String()) 330 // volume 5000 331 require.Equal(t, "1", engine.VolumeRebateFactorForParty("p7").String()) 332 333 engine.OnMarketFeeFactorsBuyBackFeeUpdate(context.Background(), num.DecimalFromFloat(0.1)) 334 engine.OnMarketFeeFactorsTreasuryFeeUpdate(context.Background(), num.DecimalFromFloat(0.2)) 335 336 // running for another epoch 337 marketActivityTracker.EXPECT().CalculateTotalMakerContributionInQuantum(gomock.Any()).Return(map[string]*num.Uint{ 338 "p8": num.NewUint(2000), 339 "p1": num.NewUint(1500), 340 "p5": num.NewUint(4000), 341 "p6": num.NewUint(4000), 342 }, 343 map[string]num.Decimal{ 344 "p8": num.DecimalFromFloat(0.2000), 345 "p1": num.DecimalFromFloat(0.1500), 346 "p5": num.DecimalFromFloat(0.4000), 347 "p6": num.DecimalFromFloat(0.4000), 348 }).Times(1) 349 350 expectStatsUpdated(t, broker) 351 currentTime = currentTime.Add(1 * time.Minute) 352 endEpoch(t, engine, currentEpoch, currentTime) 353 354 currentEpoch += 1 355 startEpoch(t, engine, currentEpoch, currentTime) 356 357 hashAfter2Epochs, _, err := engine.GetState(key) 358 require.NoError(t, err) 359 loadedEngine := assertSnapshotMatches(t, key, hashAfter2Epochs) 360 loadedEngine.OnMarketFeeFactorsBuyBackFeeUpdate(context.Background(), num.NewDecimalFromFloat(0.5)) 361 loadedEngine.OnMarketFeeFactorsTreasuryFeeUpdate(context.Background(), num.NewDecimalFromFloat(0.5)) 362 363 // fraction 0.2 => rebate 0.2 364 require.Equal(t, "0.2", engine.VolumeRebateFactorForParty("p8").String()) 365 require.Equal(t, "0.2", loadedEngine.VolumeRebateFactorForParty("p8").String()) 366 // fraction 0.15 => rebate 0.1 367 require.Equal(t, "0.1", engine.VolumeRebateFactorForParty("p1").String()) 368 require.Equal(t, "0.1", loadedEngine.VolumeRebateFactorForParty("p1").String()) 369 // nothing this time 370 require.Equal(t, "0", engine.VolumeRebateFactorForParty("p2").String()) 371 require.Equal(t, "0", loadedEngine.VolumeRebateFactorForParty("p2").String()) 372 // nothing this time 373 require.Equal(t, "0", engine.VolumeRebateFactorForParty("p3").String()) 374 require.Equal(t, "0", loadedEngine.VolumeRebateFactorForParty("p3").String()) 375 // nothing this time 376 require.Equal(t, "0", engine.VolumeRebateFactorForParty("p4").String()) 377 require.Equal(t, "0", loadedEngine.VolumeRebateFactorForParty("p4").String()) 378 // fraction 0.4 => rebate 1 => capped at 0.3 379 require.Equal(t, "0.3", engine.VolumeRebateFactorForParty("p5").String()) 380 require.Equal(t, "0.3", loadedEngine.VolumeRebateFactorForParty("p5").String()) 381 // fraction 0.4 => rebate 1 => capped at 0.3 382 require.Equal(t, "0.3", engine.VolumeRebateFactorForParty("p6").String()) 383 require.Equal(t, "0.3", loadedEngine.VolumeRebateFactorForParty("p6").String()) 384 // nothing this time 385 require.Equal(t, "0", engine.VolumeRebateFactorForParty("p7").String()) 386 require.Equal(t, "0", loadedEngine.VolumeRebateFactorForParty("p7").String()) 387 388 marketActivityTracker.EXPECT().CalculateTotalMakerContributionInQuantum(gomock.Any()).Return(map[string]*num.Uint{}, map[string]num.Decimal{}).AnyTimes() 389 expectStatsUpdated(t, broker) 390 currentTime = p1.EndOfProgramTimestamp 391 endEpoch(t, engine, currentEpoch, currentTime) 392 393 expectProgramEnded(t, broker, p1) 394 currentEpoch += 1 395 startEpoch(t, engine, currentEpoch, currentTime) 396 397 hashAfterProgramEnded, _, err := engine.GetState(key) 398 require.NoError(t, err) 399 loadedEngine = assertSnapshotMatches(t, key, hashAfterProgramEnded) 400 401 // no rebate for terminated program 402 for _, p := range []string{"p1", "p2", "p3", "p4", "p5", "p6", "p7", "p8"} { 403 require.Equal(t, num.DecimalZero(), engine.VolumeRebateFactorForParty(types.PartyID(p))) 404 require.Equal(t, num.DecimalZero(), loadedEngine.VolumeRebateFactorForParty(types.PartyID(p))) 405 } 406 }