code.vegaprotocol.io/vega@v0.79.0/core/activitystreak/activitiystreak.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
    17  
    18  import (
    19  	"context"
    20  	"sort"
    21  
    22  	"code.vegaprotocol.io/vega/core/events"
    23  	"code.vegaprotocol.io/vega/core/types"
    24  	"code.vegaprotocol.io/vega/libs/num"
    25  	"code.vegaprotocol.io/vega/logging"
    26  	vegapb "code.vegaprotocol.io/vega/protos/vega"
    27  	eventspb "code.vegaprotocol.io/vega/protos/vega/events/v1"
    28  
    29  	"golang.org/x/exp/maps"
    30  )
    31  
    32  //go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/core/activitystreak Broker,MarketsStatsAggregator
    33  
    34  type PartyActivity struct {
    35  	Active                               uint64
    36  	Inactive                             uint64
    37  	RewardDistributionActivityMultiplier num.Decimal
    38  	RewardVestingActivityMultiplier      num.Decimal
    39  }
    40  
    41  func (p PartyActivity) IsActive() bool {
    42  	return p.Active != 0 && p.Inactive == 0
    43  }
    44  
    45  func (p *PartyActivity) ResetMultipliers() {
    46  	p.RewardDistributionActivityMultiplier = num.DecimalOne()
    47  	p.RewardVestingActivityMultiplier = num.DecimalOne()
    48  }
    49  
    50  func (p *PartyActivity) UpdateMultipliers(benefitTiers []*types.ActivityStreakBenefitTier) {
    51  	if !p.IsActive() {
    52  		// we are not active, nothing changes
    53  		return
    54  	}
    55  
    56  	// these are sorted properly already
    57  	for _, b := range benefitTiers {
    58  		if p.Active < b.MinimumActivityStreak {
    59  			break
    60  		}
    61  
    62  		p.RewardDistributionActivityMultiplier = b.RewardMultiplier
    63  		p.RewardVestingActivityMultiplier = b.VestingMultiplier
    64  	}
    65  }
    66  
    67  type MarketsStatsAggregator interface {
    68  	GetMarketStats() map[string]*types.MarketStats
    69  }
    70  
    71  type Broker interface {
    72  	SendBatch(evt []events.Event)
    73  }
    74  
    75  type Engine struct {
    76  	log *logging.Logger
    77  
    78  	marketStats     MarketsStatsAggregator
    79  	partiesActivity map[string]*PartyActivity
    80  	broker          Broker
    81  
    82  	benefitTiers                 []*types.ActivityStreakBenefitTier
    83  	minQuantumOpenNotionalVolume *num.Uint
    84  	minQuantumTradeVolume        *num.Uint
    85  	inactivityLimit              uint64
    86  }
    87  
    88  func New(
    89  	log *logging.Logger,
    90  	marketStats MarketsStatsAggregator,
    91  	broker Broker,
    92  ) *Engine {
    93  	return &Engine{
    94  		log:                          log,
    95  		marketStats:                  marketStats,
    96  		partiesActivity:              map[string]*PartyActivity{},
    97  		broker:                       broker,
    98  		minQuantumOpenNotionalVolume: num.UintZero(),
    99  		minQuantumTradeVolume:        num.UintZero(),
   100  	}
   101  }
   102  
   103  func (e *Engine) OnRewardsActivityStreakInactivityLimit(
   104  	_ context.Context, v *num.Uint,
   105  ) error {
   106  	e.inactivityLimit = v.Uint64()
   107  	return nil
   108  }
   109  
   110  func (e *Engine) OnMinQuantumOpenNationalVolumeUpdate(
   111  	_ context.Context, v *num.Uint,
   112  ) error {
   113  	e.minQuantumOpenNotionalVolume = v.Clone()
   114  	return nil
   115  }
   116  
   117  func (e *Engine) OnMinQuantumTradeVolumeUpdate(
   118  	_ context.Context, v *num.Uint,
   119  ) error {
   120  	e.minQuantumTradeVolume = v.Clone()
   121  	return nil
   122  }
   123  
   124  func (e *Engine) OnBenefitTiersUpdate(
   125  	_ context.Context, v interface{},
   126  ) error {
   127  	tiers, err := types.ActivityStreakBenefitTiersFromUntypedProto(v)
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	e.benefitTiers = tiers.Clone().Tiers
   133  	sort.Slice(e.benefitTiers, func(i, j int) bool {
   134  		return e.benefitTiers[i].MinimumActivityStreak < e.benefitTiers[j].MinimumActivityStreak
   135  	})
   136  	return nil
   137  }
   138  
   139  func (e *Engine) OnEpochEvent(ctx context.Context, epoch types.Epoch) {
   140  	if epoch.Action == vegapb.EpochAction_EPOCH_ACTION_END {
   141  		e.update(ctx, epoch.Seq)
   142  	}
   143  }
   144  
   145  func (e *Engine) OnEpochRestore(ctx context.Context, epoch types.Epoch) {}
   146  
   147  func (e *Engine) GetRewardsDistributionMultiplier(party string) num.Decimal {
   148  	if _, ok := e.partiesActivity[party]; !ok {
   149  		return num.DecimalOne()
   150  	}
   151  
   152  	return e.partiesActivity[party].RewardDistributionActivityMultiplier
   153  }
   154  
   155  func (e *Engine) GetRewardsVestingMultiplier(party string) num.Decimal {
   156  	if _, ok := e.partiesActivity[party]; !ok {
   157  		return num.DecimalOne()
   158  	}
   159  
   160  	return e.partiesActivity[party].RewardVestingActivityMultiplier
   161  }
   162  
   163  type partyStats struct {
   164  	OpenVolume  *num.Uint
   165  	TradeVolume *num.Uint
   166  }
   167  
   168  func (e *Engine) update(ctx context.Context, epochSeq uint64) {
   169  	stats := e.marketStats.GetMarketStats()
   170  
   171  	// first accumulate the stats
   172  
   173  	// party -> volume across all markets
   174  	parties := map[string]*partyStats{}
   175  	for _, v := range stats {
   176  		for p, vol := range v.PartiesOpenNotionalVolume {
   177  			party := parties[p]
   178  			if party == nil {
   179  				party = &partyStats{
   180  					OpenVolume:  num.UintZero(),
   181  					TradeVolume: num.UintZero(),
   182  				}
   183  				parties[p] = party
   184  			}
   185  			party.OpenVolume.Add(party.OpenVolume, vol.Clone())
   186  		}
   187  
   188  		for p, vol := range v.PartiesTotalTradeVolume {
   189  			party := parties[p]
   190  			if party == nil {
   191  				party = &partyStats{
   192  					OpenVolume:  num.UintZero(),
   193  					TradeVolume: num.UintZero(),
   194  				}
   195  				parties[p] = party
   196  			}
   197  			party.TradeVolume.Add(party.TradeVolume, vol.Clone())
   198  		}
   199  	}
   200  
   201  	partiesKey := maps.Keys(parties)
   202  	sort.Strings(partiesKey)
   203  
   204  	for _, party := range partiesKey {
   205  		v := parties[party]
   206  		e.updateStreak(party, v.OpenVolume, v.TradeVolume)
   207  	}
   208  
   209  	// now iterate over all existing parties,
   210  	// and update the ones for which nothing happen during the epoch
   211  	// and send the events
   212  	partiesKey = maps.Keys(e.partiesActivity)
   213  	sort.Strings(partiesKey)
   214  	evts := []events.Event{}
   215  	for _, party := range partiesKey {
   216  		ps := parties[party]
   217  		if _, ok := parties[party]; !ok {
   218  			e.updateStreak(party, num.UintZero(), num.UintZero())
   219  			ps = &partyStats{
   220  				OpenVolume:  num.UintZero(),
   221  				TradeVolume: num.UintZero(),
   222  			}
   223  		}
   224  
   225  		evt := e.makeEvent(party, epochSeq, ps.OpenVolume, ps.TradeVolume)
   226  		evts = append(evts, events.NewPartyActivityStreakEvent(ctx, evt))
   227  	}
   228  	e.broker.SendBatch(evts)
   229  }
   230  
   231  func (e *Engine) makeEvent(party string, epochSeq uint64, openVolume, tradedVolume *num.Uint) *eventspb.PartyActivityStreak {
   232  	partyActivity := e.partiesActivity[party]
   233  	return &eventspb.PartyActivityStreak{
   234  		Party:                                party,
   235  		ActiveFor:                            partyActivity.Active,
   236  		InactiveFor:                          partyActivity.Inactive,
   237  		IsActive:                             partyActivity.IsActive(),
   238  		RewardDistributionActivityMultiplier: partyActivity.RewardDistributionActivityMultiplier.String(),
   239  		RewardVestingActivityMultiplier:      partyActivity.RewardVestingActivityMultiplier.String(),
   240  		Epoch:                                epochSeq,
   241  		TradedVolume:                         tradedVolume.String(),
   242  		OpenVolume:                           openVolume.String(),
   243  	}
   244  }
   245  
   246  func (e *Engine) updateStreak(party string, openVolume, tradeVolume *num.Uint) {
   247  	partyActivity, ok := e.partiesActivity[party]
   248  	if !ok {
   249  		partyActivity = &PartyActivity{
   250  			RewardDistributionActivityMultiplier: num.DecimalOne(),
   251  			RewardVestingActivityMultiplier:      num.DecimalOne(),
   252  		}
   253  		e.partiesActivity[party] = partyActivity
   254  	}
   255  
   256  	if openVolume.GT(e.minQuantumOpenNotionalVolume) || tradeVolume.GT(e.minQuantumTradeVolume) {
   257  		partyActivity.Active++
   258  		partyActivity.Inactive = 0
   259  	} else {
   260  		partyActivity.Inactive++
   261  
   262  		if partyActivity.Inactive >= e.inactivityLimit {
   263  			partyActivity.Active = 0
   264  			partyActivity.ResetMultipliers()
   265  		}
   266  	}
   267  
   268  	partyActivity.UpdateMultipliers(e.benefitTiers)
   269  }