code.vegaprotocol.io/vega@v0.79.0/core/volumediscount/engine.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 17 18 import ( 19 "context" 20 "sort" 21 "time" 22 23 "code.vegaprotocol.io/vega/core/events" 24 "code.vegaprotocol.io/vega/core/types" 25 "code.vegaprotocol.io/vega/libs/num" 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 "golang.org/x/exp/slices" 31 ) 32 33 const MaximumWindowLength uint64 = 200 34 35 type Engine struct { 36 broker Broker 37 marketActivityTracker MarketActivityTracker 38 39 epochData []map[types.PartyID]*num.Uint 40 epochDataIndex int 41 parties map[types.PartyID]struct{} 42 avgVolumePerParty map[types.PartyID]num.Decimal 43 latestProgramVersion uint64 44 currentProgram *types.VolumeDiscountProgram 45 newProgram *types.VolumeDiscountProgram 46 programHasEnded bool 47 48 factorsByParty map[types.PartyID]types.VolumeDiscountStats 49 } 50 51 func New(broker Broker, marketActivityTracker MarketActivityTracker) *Engine { 52 return &Engine{ 53 broker: broker, 54 marketActivityTracker: marketActivityTracker, 55 epochData: make([]map[types.PartyID]*num.Uint, MaximumWindowLength), 56 epochDataIndex: 0, 57 avgVolumePerParty: map[types.PartyID]num.Decimal{}, 58 parties: map[types.PartyID]struct{}{}, 59 programHasEnded: true, 60 factorsByParty: map[types.PartyID]types.VolumeDiscountStats{}, 61 } 62 } 63 64 func (e *Engine) OnEpoch(ctx context.Context, ep types.Epoch) { 65 switch ep.Action { 66 case vegapb.EpochAction_EPOCH_ACTION_START: 67 // whatever current program is 68 pp := e.currentProgram 69 e.applyProgramUpdate(ctx, ep.StartTime, ep.Seq) 70 // we have an active program, and it's not the same one after we called applyProgramUpdate -> update factors. 71 if !e.programHasEnded && pp != e.currentProgram { 72 // calculate volume for the window of the new program 73 e.calculatePartiesVolumeForWindow(int(e.currentProgram.WindowLength)) 74 // update the factors 75 e.computeFactorsByParty(ctx, ep.Seq) 76 } 77 case vegapb.EpochAction_EPOCH_ACTION_END: 78 e.updateNotionalVolumeForEpoch() 79 if !e.programHasEnded { 80 e.calculatePartiesVolumeForWindow(int(e.currentProgram.WindowLength)) 81 e.computeFactorsByParty(ctx, ep.Seq) 82 } 83 } 84 } 85 86 func (e *Engine) OnEpochRestore(_ context.Context, ep types.Epoch) { 87 } 88 89 func (e *Engine) UpdateProgram(newProgram *types.VolumeDiscountProgram) { 90 e.latestProgramVersion += 1 91 e.newProgram = newProgram 92 93 sort.Slice(e.newProgram.VolumeBenefitTiers, func(i, j int) bool { 94 return e.newProgram.VolumeBenefitTiers[i].MinimumRunningNotionalTakerVolume.LT(e.newProgram.VolumeBenefitTiers[j].MinimumRunningNotionalTakerVolume) 95 }) 96 97 e.newProgram.Version = e.latestProgramVersion 98 } 99 100 func (e *Engine) HasProgramEnded() bool { 101 return e.programHasEnded 102 } 103 104 func (e *Engine) VolumeDiscountFactorForParty(party types.PartyID) types.Factors { 105 if e.programHasEnded { 106 return types.EmptyFactors 107 } 108 109 factors, ok := e.factorsByParty[party] 110 if !ok { 111 return types.EmptyFactors 112 } 113 114 return factors.DiscountFactors 115 } 116 117 func (e *Engine) TakerNotionalForParty(party types.PartyID) num.Decimal { 118 return e.avgVolumePerParty[party] 119 } 120 121 func (e *Engine) applyProgramUpdate(ctx context.Context, startEpochTime time.Time, epoch uint64) { 122 if e.newProgram != nil { 123 if e.currentProgram != nil { 124 e.endCurrentProgram() 125 e.startNewProgram() 126 e.notifyVolumeDiscountProgramUpdated(ctx, startEpochTime, epoch) 127 } else { 128 e.startNewProgram() 129 e.notifyVolumeDiscountProgramStarted(ctx, startEpochTime, epoch) 130 } 131 } 132 133 // This handles a edge case where the new program ends before the next 134 // epoch starts. It can happen when the proposal updating the volume discount 135 // program specifies an end date that is within the same epoch as the enactment 136 // time. 137 if e.currentProgram != nil && !e.currentProgram.EndOfProgramTimestamp.IsZero() && !e.currentProgram.EndOfProgramTimestamp.After(startEpochTime) { 138 e.notifyVolumeDiscountProgramEnded(ctx, startEpochTime, epoch) 139 e.endCurrentProgram() 140 } 141 } 142 143 func (e *Engine) endCurrentProgram() { 144 e.programHasEnded = true 145 e.currentProgram = nil 146 } 147 148 func (e *Engine) startNewProgram() { 149 e.programHasEnded = false 150 e.currentProgram = e.newProgram 151 e.newProgram = nil 152 } 153 154 func (e *Engine) notifyVolumeDiscountProgramStarted(ctx context.Context, epochTime time.Time, epoch uint64) { 155 e.broker.Send(events.NewVolumeDiscountProgramStartedEvent(ctx, e.currentProgram, epochTime, epoch)) 156 } 157 158 func (e *Engine) notifyVolumeDiscountProgramUpdated(ctx context.Context, epochTime time.Time, epoch uint64) { 159 e.broker.Send(events.NewVolumeDiscountProgramUpdatedEvent(ctx, e.currentProgram, epochTime, epoch)) 160 } 161 162 func (e *Engine) notifyVolumeDiscountProgramEnded(ctx context.Context, epochTime time.Time, epoch uint64) { 163 e.broker.Send(events.NewVolumeDiscountProgramEndedEvent(ctx, e.currentProgram.Version, e.currentProgram.ID, epochTime, epoch)) 164 } 165 166 func (e *Engine) calculatePartiesVolumeForWindow(windowSize int) { 167 for pi := range e.parties { 168 total := num.UintZero() 169 for i := 0; i < windowSize; i++ { 170 valueForEpoch, ok := e.epochData[(e.epochDataIndex+int(MaximumWindowLength)-i-1)%int(MaximumWindowLength)][pi] 171 if !ok { 172 valueForEpoch = num.UintZero() 173 } 174 total.AddSum(valueForEpoch) 175 } 176 e.avgVolumePerParty[pi] = total.ToDecimal() 177 } 178 } 179 180 func (e *Engine) updateNotionalVolumeForEpoch() { 181 e.epochData[e.epochDataIndex] = e.marketActivityTracker.NotionalTakerVolumeForAllParties() 182 for pi := range e.epochData[e.epochDataIndex] { 183 e.parties[pi] = struct{}{} 184 } 185 e.epochDataIndex = (e.epochDataIndex + 1) % int(MaximumWindowLength) 186 } 187 188 func (e *Engine) computeFactorsByParty(ctx context.Context, epoch uint64) { 189 e.factorsByParty = map[types.PartyID]types.VolumeDiscountStats{} 190 191 parties := maps.Keys(e.avgVolumePerParty) 192 slices.Sort(parties) 193 194 tiersLen := len(e.currentProgram.VolumeBenefitTiers) 195 196 evt := &eventspb.VolumeDiscountStatsUpdated{ 197 AtEpoch: epoch, 198 Stats: make([]*eventspb.PartyVolumeDiscountStats, 0, len(e.avgVolumePerParty)), 199 } 200 201 for _, party := range parties { 202 notionalVolume := e.avgVolumePerParty[party] 203 qualifiedForTier := false 204 for i := tiersLen - 1; i >= 0; i-- { 205 tier := e.currentProgram.VolumeBenefitTiers[i] 206 if notionalVolume.GreaterThanOrEqual(tier.MinimumRunningNotionalTakerVolume.ToDecimal()) { 207 e.factorsByParty[party] = types.VolumeDiscountStats{ 208 DiscountFactors: tier.VolumeDiscountFactors, 209 } 210 evt.Stats = append(evt.Stats, &eventspb.PartyVolumeDiscountStats{ 211 PartyId: party.String(), 212 DiscountFactors: tier.VolumeDiscountFactors.IntoDiscountFactorsProto(), 213 RunningVolume: notionalVolume.Round(0).String(), 214 }) 215 qualifiedForTier = true 216 break 217 } 218 } 219 // if the party hasn't qualified, then still send the stats but with a zero factor 220 if !qualifiedForTier { 221 evt.Stats = append(evt.Stats, &eventspb.PartyVolumeDiscountStats{ 222 PartyId: party.String(), 223 DiscountFactors: types.EmptyFactors.IntoDiscountFactorsProto(), 224 RunningVolume: notionalVolume.Round(0).String(), 225 }) 226 } 227 } 228 229 e.broker.Send(events.NewVolumeDiscountStatsUpdatedEvent(ctx, evt)) 230 }