code.vegaprotocol.io/vega@v0.79.0/core/activitystreak/activitiystreak_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 activitystreak_test
    17  
    18  import (
    19  	"context"
    20  	"testing"
    21  
    22  	"code.vegaprotocol.io/vega/core/activitystreak"
    23  	"code.vegaprotocol.io/vega/core/activitystreak/mocks"
    24  	"code.vegaprotocol.io/vega/core/events"
    25  	"code.vegaprotocol.io/vega/core/types"
    26  	"code.vegaprotocol.io/vega/libs/num"
    27  	"code.vegaprotocol.io/vega/logging"
    28  	vegapb "code.vegaprotocol.io/vega/protos/vega"
    29  
    30  	"github.com/golang/mock/gomock"
    31  	"github.com/stretchr/testify/assert"
    32  )
    33  
    34  type testEngine struct {
    35  	*activitystreak.Engine
    36  
    37  	ctrl         *gomock.Controller
    38  	broker       *mocks.MockBroker
    39  	marketsStats *mocks.MockMarketsStatsAggregator
    40  }
    41  
    42  func getTestEngine(t *testing.T) *testEngine {
    43  	t.Helper()
    44  	ctrl := gomock.NewController(t)
    45  	marketsStats := mocks.NewMockMarketsStatsAggregator(ctrl)
    46  	broker := mocks.NewMockBroker(ctrl)
    47  
    48  	return &testEngine{
    49  		Engine: activitystreak.New(
    50  			logging.NewTestLogger(), marketsStats, broker,
    51  		),
    52  		ctrl:         ctrl,
    53  		broker:       broker,
    54  		marketsStats: marketsStats,
    55  	}
    56  }
    57  
    58  func TestStreak(t *testing.T) {
    59  	engine := getTestEngine(t)
    60  
    61  	engine.OnMinQuantumOpenNationalVolumeUpdate(context.Background(), num.NewUint(100))
    62  	engine.OnMinQuantumTradeVolumeUpdate(context.Background(), num.NewUint(200))
    63  	engine.OnRewardsActivityStreakInactivityLimit(context.Background(), num.NewUint(10))
    64  	assert.NoError(t, engine.OnBenefitTiersUpdate(context.Background(), &vegapb.ActivityStreakBenefitTiers{
    65  		Tiers: []*vegapb.ActivityStreakBenefitTier{
    66  			{
    67  				MinimumActivityStreak: 1,
    68  				RewardMultiplier:      "2",
    69  				VestingMultiplier:     "1.5",
    70  			},
    71  			{
    72  				MinimumActivityStreak: 7,
    73  				RewardMultiplier:      "3",
    74  				VestingMultiplier:     "2.5",
    75  			},
    76  			{
    77  				MinimumActivityStreak: 14,
    78  				RewardMultiplier:      "4",
    79  				VestingMultiplier:     "3.5",
    80  			},
    81  		},
    82  	}))
    83  
    84  	t.Run("no streak for a party == 1x", func(t *testing.T) {
    85  		tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1")
    86  
    87  		assert.Equal(t, num.DecimalOne(), tradeX)
    88  		assert.Equal(t, num.DecimalOne(), volumeX)
    89  	})
    90  
    91  	t.Run("add volume < min == 1x", func(t *testing.T) {
    92  		engine.marketsStats.EXPECT().GetMarketStats().Times(1).Return(
    93  			map[string]*types.MarketStats{
    94  				"market1": {
    95  					PartiesOpenNotionalVolume: map[string]*num.Uint{
    96  						"party1": num.NewUint(20),
    97  					},
    98  					PartiesTotalTradeVolume: map[string]*num.Uint{
    99  						"party1": num.NewUint(50),
   100  					},
   101  				},
   102  				"market2": {
   103  					PartiesOpenNotionalVolume: map[string]*num.Uint{
   104  						"party1": num.NewUint(20),
   105  					},
   106  					PartiesTotalTradeVolume: map[string]*num.Uint{
   107  						"party1": num.NewUint(50),
   108  					},
   109  				},
   110  			},
   111  		)
   112  
   113  		engine.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do(
   114  			func(evts []events.Event) {
   115  				assert.Len(t, evts, 1)
   116  
   117  				pas := evts[0].(*events.PartyActivityStreak)
   118  				assert.False(t, pas.Proto().IsActive)
   119  				assert.Equal(t, int(pas.Proto().ActiveFor), 0)
   120  				assert.Equal(t, int(pas.Proto().InactiveFor), 1)
   121  				assert.Equal(t, int(pas.Proto().Epoch), 1)
   122  				assert.Equal(t, pas.Proto().RewardDistributionActivityMultiplier, "1")
   123  				assert.Equal(t, pas.Proto().RewardVestingActivityMultiplier, "1")
   124  			},
   125  		)
   126  
   127  		engine.OnEpochEvent(context.Background(), types.Epoch{
   128  			Seq:    1,
   129  			Action: vegapb.EpochAction_EPOCH_ACTION_END,
   130  		})
   131  
   132  		tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1")
   133  
   134  		assert.Equal(t, num.DecimalOne(), tradeX)
   135  		assert.Equal(t, num.DecimalOne(), volumeX)
   136  	})
   137  
   138  	t.Run("add volume > min == increase multipliers", func(t *testing.T) {
   139  		engine.marketsStats.EXPECT().GetMarketStats().Times(1).Return(
   140  			map[string]*types.MarketStats{
   141  				"market1": {
   142  					PartiesOpenNotionalVolume: map[string]*num.Uint{
   143  						"party1": num.NewUint(100),
   144  					},
   145  					PartiesTotalTradeVolume: map[string]*num.Uint{
   146  						"party1": num.NewUint(50),
   147  					},
   148  				},
   149  				"market2": {
   150  					PartiesOpenNotionalVolume: map[string]*num.Uint{
   151  						"party1": num.NewUint(20),
   152  					},
   153  					PartiesTotalTradeVolume: map[string]*num.Uint{
   154  						"party1": num.NewUint(50),
   155  					},
   156  				},
   157  			},
   158  		)
   159  
   160  		engine.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do(
   161  			func(evts []events.Event) {
   162  				assert.Len(t, evts, 1)
   163  
   164  				pas := evts[0].(*events.PartyActivityStreak)
   165  				assert.True(t, pas.Proto().IsActive)
   166  				assert.Equal(t, int(pas.Proto().ActiveFor), 1)
   167  				assert.Equal(t, int(pas.Proto().InactiveFor), 0)
   168  				assert.Equal(t, int(pas.Proto().Epoch), 2)
   169  				assert.Equal(t, pas.Proto().RewardDistributionActivityMultiplier, "2")
   170  				assert.Equal(t, pas.Proto().RewardVestingActivityMultiplier, "1.5")
   171  			},
   172  		)
   173  
   174  		engine.OnEpochEvent(context.Background(), types.Epoch{
   175  			Seq:    2,
   176  			Action: vegapb.EpochAction_EPOCH_ACTION_END,
   177  		})
   178  
   179  		tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1")
   180  
   181  		assert.Equal(t, num.MustDecimalFromString("2"), tradeX)
   182  		assert.Equal(t, num.MustDecimalFromString("1.5"), volumeX)
   183  	})
   184  
   185  	t.Run("add volume > min many time == move to next tier", func(t *testing.T) {
   186  		engine.marketsStats.EXPECT().GetMarketStats().Times(6).Return(
   187  			map[string]*types.MarketStats{
   188  				"market1": {
   189  					PartiesOpenNotionalVolume: map[string]*num.Uint{
   190  						"party1": num.NewUint(100),
   191  					},
   192  					PartiesTotalTradeVolume: map[string]*num.Uint{
   193  						"party1": num.NewUint(50),
   194  					},
   195  				},
   196  				"market2": {
   197  					PartiesOpenNotionalVolume: map[string]*num.Uint{
   198  						"party1": num.NewUint(20),
   199  					},
   200  					PartiesTotalTradeVolume: map[string]*num.Uint{
   201  						"party1": num.NewUint(50),
   202  					},
   203  				},
   204  			},
   205  		)
   206  
   207  		// discard first 5
   208  		engine.broker.EXPECT().SendBatch(gomock.Any()).Times(5)
   209  
   210  		engine.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do(
   211  			func(evts []events.Event) {
   212  				assert.Len(t, evts, 1)
   213  
   214  				pas := evts[0].(*events.PartyActivityStreak)
   215  				assert.True(t, pas.Proto().IsActive)
   216  				assert.Equal(t, int(pas.Proto().ActiveFor), 7)
   217  				assert.Equal(t, int(pas.Proto().InactiveFor), 0)
   218  				assert.Equal(t, int(pas.Proto().Epoch), 8)
   219  				assert.Equal(t, pas.Proto().RewardDistributionActivityMultiplier, "3")
   220  				assert.Equal(t, pas.Proto().RewardVestingActivityMultiplier, "2.5")
   221  			},
   222  		)
   223  
   224  		for i := 3; i <= 8; i++ {
   225  			engine.OnEpochEvent(context.Background(), types.Epoch{
   226  				Seq:    uint64(i),
   227  				Action: vegapb.EpochAction_EPOCH_ACTION_END,
   228  			})
   229  		}
   230  
   231  		tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1")
   232  
   233  		assert.Equal(t, num.MustDecimalFromString("3"), tradeX)
   234  		assert.Equal(t, num.MustDecimalFromString("2.5"), volumeX)
   235  	})
   236  
   237  	t.Run("add volume < min less times than current streak == inactive but still have benefits", func(t *testing.T) {
   238  		engine.marketsStats.EXPECT().GetMarketStats().Times(4).Return(
   239  			map[string]*types.MarketStats{
   240  				"market1": {
   241  					PartiesOpenNotionalVolume: map[string]*num.Uint{
   242  						"party1": num.NewUint(20),
   243  					},
   244  					PartiesTotalTradeVolume: map[string]*num.Uint{
   245  						"party1": num.NewUint(50),
   246  					},
   247  				},
   248  				"market2": {
   249  					PartiesOpenNotionalVolume: map[string]*num.Uint{
   250  						"party1": num.NewUint(20),
   251  					},
   252  					PartiesTotalTradeVolume: map[string]*num.Uint{
   253  						"party1": num.NewUint(50),
   254  					},
   255  				},
   256  			},
   257  		)
   258  
   259  		// discard first 5
   260  		engine.broker.EXPECT().SendBatch(gomock.Any()).Times(3)
   261  
   262  		engine.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do(
   263  			func(evts []events.Event) {
   264  				assert.Len(t, evts, 1)
   265  
   266  				pas := evts[0].(*events.PartyActivityStreak)
   267  				assert.False(t, pas.Proto().IsActive)
   268  				assert.Equal(t, int(pas.Proto().ActiveFor), 7)
   269  				assert.Equal(t, int(pas.Proto().InactiveFor), 4)
   270  				assert.Equal(t, int(pas.Proto().Epoch), 12)
   271  				assert.Equal(t, pas.Proto().RewardDistributionActivityMultiplier, "3")
   272  				assert.Equal(t, pas.Proto().RewardVestingActivityMultiplier, "2.5")
   273  			},
   274  		)
   275  
   276  		for i := 9; i <= 12; i++ {
   277  			engine.OnEpochEvent(context.Background(), types.Epoch{
   278  				Seq:    uint64(i),
   279  				Action: vegapb.EpochAction_EPOCH_ACTION_END,
   280  			})
   281  		}
   282  
   283  		tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1")
   284  
   285  		assert.Equal(t, num.MustDecimalFromString("3"), tradeX)
   286  		assert.Equal(t, num.MustDecimalFromString("2.5"), volumeX)
   287  	})
   288  
   289  	t.Run("add volume > min again == becomes active again", func(t *testing.T) {
   290  		engine.marketsStats.EXPECT().GetMarketStats().Times(1).Return(
   291  			map[string]*types.MarketStats{
   292  				"market1": {
   293  					PartiesOpenNotionalVolume: map[string]*num.Uint{
   294  						"party1": num.NewUint(100),
   295  					},
   296  					PartiesTotalTradeVolume: map[string]*num.Uint{
   297  						"party1": num.NewUint(50),
   298  					},
   299  				},
   300  				"market2": {
   301  					PartiesOpenNotionalVolume: map[string]*num.Uint{
   302  						"party1": num.NewUint(20),
   303  					},
   304  					PartiesTotalTradeVolume: map[string]*num.Uint{
   305  						"party1": num.NewUint(50),
   306  					},
   307  				},
   308  			},
   309  		)
   310  
   311  		engine.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do(
   312  			func(evts []events.Event) {
   313  				assert.Len(t, evts, 1)
   314  
   315  				pas := evts[0].(*events.PartyActivityStreak)
   316  				assert.True(t, pas.Proto().IsActive)
   317  				assert.Equal(t, int(pas.Proto().ActiveFor), 8)
   318  				assert.Equal(t, int(pas.Proto().InactiveFor), 0)
   319  				assert.Equal(t, int(pas.Proto().Epoch), 13)
   320  				assert.Equal(t, pas.Proto().RewardDistributionActivityMultiplier, "3")
   321  				assert.Equal(t, pas.Proto().RewardVestingActivityMultiplier, "2.5")
   322  			},
   323  		)
   324  
   325  		engine.OnEpochEvent(context.Background(), types.Epoch{
   326  			Seq:    uint64(13),
   327  			Action: vegapb.EpochAction_EPOCH_ACTION_END,
   328  		})
   329  
   330  		tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1")
   331  
   332  		assert.Equal(t, num.MustDecimalFromString("3"), tradeX)
   333  		assert.Equal(t, num.MustDecimalFromString("2.5"), volumeX)
   334  	})
   335  
   336  	t.Run("add volume < min more times than current streak looses benefits", func(t *testing.T) {
   337  		engine.marketsStats.EXPECT().GetMarketStats().Times(11).Return(
   338  			map[string]*types.MarketStats{
   339  				"market1": {
   340  					PartiesOpenNotionalVolume: map[string]*num.Uint{
   341  						"party1": num.NewUint(20),
   342  					},
   343  					PartiesTotalTradeVolume: map[string]*num.Uint{
   344  						"party1": num.NewUint(50),
   345  					},
   346  				},
   347  				"market2": {
   348  					PartiesOpenNotionalVolume: map[string]*num.Uint{
   349  						"party1": num.NewUint(20),
   350  					},
   351  					PartiesTotalTradeVolume: map[string]*num.Uint{
   352  						"party1": num.NewUint(50),
   353  					},
   354  				},
   355  			},
   356  		)
   357  
   358  		// discard first 5
   359  		engine.broker.EXPECT().SendBatch(gomock.Any()).Times(10)
   360  
   361  		engine.broker.EXPECT().SendBatch(gomock.Any()).Times(1).Do(
   362  			func(evts []events.Event) {
   363  				assert.Len(t, evts, 1)
   364  
   365  				pas := evts[0].(*events.PartyActivityStreak)
   366  				assert.False(t, pas.Proto().IsActive)
   367  				assert.Equal(t, int(pas.Proto().ActiveFor), 0)
   368  				assert.Equal(t, int(pas.Proto().InactiveFor), 11)
   369  				assert.Equal(t, int(pas.Proto().Epoch), 24)
   370  				assert.Equal(t, pas.Proto().RewardDistributionActivityMultiplier, "1")
   371  				assert.Equal(t, pas.Proto().RewardVestingActivityMultiplier, "1")
   372  			},
   373  		)
   374  
   375  		for i := 14; i <= 24; i++ {
   376  			engine.OnEpochEvent(context.Background(), types.Epoch{
   377  				Seq:    uint64(i),
   378  				Action: vegapb.EpochAction_EPOCH_ACTION_END,
   379  			})
   380  		}
   381  
   382  		tradeX, volumeX := engine.GetRewardsDistributionMultiplier("party1"), engine.GetRewardsVestingMultiplier("party1")
   383  
   384  		assert.Equal(t, num.MustDecimalFromString("1"), tradeX)
   385  		assert.Equal(t, num.MustDecimalFromString("1"), volumeX)
   386  	})
   387  }