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 }