code.vegaprotocol.io/vega@v0.79.0/core/volumerebate/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 volumerebate 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 parties map[string]struct{} 40 fractionPerParty map[string]num.Decimal 41 makerFeesReceivedInWindowPerParty map[string]*num.Uint 42 latestProgramVersion uint64 43 currentProgram *types.VolumeRebateProgram 44 newProgram *types.VolumeRebateProgram 45 programHasEnded bool 46 47 factorsByParty map[types.PartyID]types.VolumeRebateStats 48 buyBackFee num.Decimal 49 treasureFee num.Decimal 50 maxAdditionalRebate num.Decimal 51 } 52 53 func New(broker Broker, marketActivityTracker MarketActivityTracker) *Engine { 54 return &Engine{ 55 broker: broker, 56 marketActivityTracker: marketActivityTracker, 57 fractionPerParty: map[string]num.Decimal{}, 58 makerFeesReceivedInWindowPerParty: map[string]*num.Uint{}, 59 parties: map[string]struct{}{}, 60 programHasEnded: true, 61 factorsByParty: map[types.PartyID]types.VolumeRebateStats{}, 62 } 63 } 64 65 func (e *Engine) OnEpoch(ctx context.Context, ep types.Epoch) { 66 switch ep.Action { 67 case vegapb.EpochAction_EPOCH_ACTION_START: 68 pp := e.currentProgram 69 e.applyProgramUpdate(ctx, ep.StartTime, ep.Seq) 70 // we have an active program and it changed after the apply update call -> update state and factors. 71 if !e.programHasEnded && pp != e.currentProgram { 72 // update state based on the new program window length 73 e.updateState() 74 e.computeFactorsByParty(ctx, ep.Seq) 75 } 76 case vegapb.EpochAction_EPOCH_ACTION_END: 77 e.updateState() 78 if !e.programHasEnded { 79 e.computeFactorsByParty(ctx, ep.Seq) 80 } 81 } 82 } 83 84 func (e *Engine) OnEpochRestore(_ context.Context, ep types.Epoch) { 85 } 86 87 func (e *Engine) UpdateProgram(newProgram *types.VolumeRebateProgram) { 88 e.latestProgramVersion += 1 89 e.newProgram = newProgram 90 91 sort.Slice(e.newProgram.VolumeRebateBenefitTiers, func(i, j int) bool { 92 return e.newProgram.VolumeRebateBenefitTiers[i].MinimumPartyMakerVolumeFraction.LessThan(e.newProgram.VolumeRebateBenefitTiers[j].MinimumPartyMakerVolumeFraction) 93 }) 94 95 e.newProgram.Version = e.latestProgramVersion 96 } 97 98 func (e *Engine) HasProgramEnded() bool { 99 return e.programHasEnded 100 } 101 102 func (e *Engine) VolumeRebateFactorForParty(party types.PartyID) num.Decimal { 103 if e.programHasEnded { 104 return num.DecimalZero() 105 } 106 107 factors, ok := e.factorsByParty[party] 108 if !ok { 109 return num.DecimalZero() 110 } 111 112 // this is needed here again because the factors are calculated at the end of the epoch and 113 // the fee factors may change during the epoch so to ensure the factor is capped at any time 114 // we apply the min again here 115 return e.effectiveAdditionalRebate(factors.RebateFactor) 116 } 117 118 func (e *Engine) MakerVolumeFractionForParty(party types.PartyID) num.Decimal { 119 if e.programHasEnded { 120 return num.DecimalZero() 121 } 122 123 frac, ok := e.fractionPerParty[party.String()] 124 if !ok { 125 return num.DecimalZero() 126 } 127 128 return frac 129 } 130 131 func (e *Engine) applyProgramUpdate(ctx context.Context, startEpochTime time.Time, epoch uint64) { 132 if e.newProgram != nil { 133 if e.currentProgram != nil { 134 e.endCurrentProgram() 135 e.startNewProgram() 136 e.notifyVolumeRebateProgramUpdated(ctx, startEpochTime, epoch) 137 } else { 138 e.startNewProgram() 139 e.notifyVolumeRebateProgramStarted(ctx, startEpochTime, epoch) 140 } 141 } 142 143 // This handles a edge case where the new program ends before the next 144 // epoch starts. It can happen when the proposal updating the volume discount 145 // program specifies an end date that is within the same epoch as the enactment 146 // time. 147 if e.currentProgram != nil && !e.currentProgram.EndOfProgramTimestamp.IsZero() && !e.currentProgram.EndOfProgramTimestamp.After(startEpochTime) { 148 e.notifyVolumeRebateProgramEnded(ctx, startEpochTime, epoch) 149 e.endCurrentProgram() 150 } 151 } 152 153 func (e *Engine) endCurrentProgram() { 154 e.programHasEnded = true 155 e.currentProgram = nil 156 } 157 158 func (e *Engine) startNewProgram() { 159 e.programHasEnded = false 160 e.currentProgram = e.newProgram 161 e.newProgram = nil 162 } 163 164 func (e *Engine) notifyVolumeRebateProgramStarted(ctx context.Context, epochTime time.Time, epoch uint64) { 165 e.broker.Send(events.NewVolumeRebateProgramStartedEvent(ctx, e.currentProgram, epochTime, epoch)) 166 } 167 168 func (e *Engine) notifyVolumeRebateProgramUpdated(ctx context.Context, epochTime time.Time, epoch uint64) { 169 e.broker.Send(events.NewVolumeRebateProgramUpdatedEvent(ctx, e.currentProgram, epochTime, epoch)) 170 } 171 172 func (e *Engine) notifyVolumeRebateProgramEnded(ctx context.Context, epochTime time.Time, epoch uint64) { 173 e.broker.Send(events.NewVolumeRebateProgramEndedEvent(ctx, e.currentProgram.Version, e.currentProgram.ID, epochTime, epoch)) 174 } 175 176 func (e *Engine) updateState() { 177 if e.currentProgram == nil { 178 return 179 } 180 e.makerFeesReceivedInWindowPerParty, e.fractionPerParty = e.marketActivityTracker.CalculateTotalMakerContributionInQuantum(int(e.currentProgram.WindowLength)) 181 for p := range e.fractionPerParty { 182 if _, ok := e.factorsByParty[types.PartyID(p)]; !ok { 183 e.factorsByParty[types.PartyID(p)] = types.VolumeRebateStats{ 184 RebateFactor: num.DecimalZero(), 185 } 186 } 187 } 188 } 189 190 func (e *Engine) computeFactorsByParty(ctx context.Context, epoch uint64) { 191 parties := maps.Keys(e.factorsByParty) 192 slices.Sort(parties) 193 194 e.factorsByParty = map[types.PartyID]types.VolumeRebateStats{} 195 196 tiersLen := len(e.currentProgram.VolumeRebateBenefitTiers) 197 198 evt := &eventspb.VolumeRebateStatsUpdated{ 199 AtEpoch: epoch, 200 Stats: make([]*eventspb.PartyVolumeRebateStats, 0, len(parties)), 201 } 202 203 for _, party := range parties { 204 makerFraction := e.fractionPerParty[party.String()] 205 receivedFees, ok := e.makerFeesReceivedInWindowPerParty[party.String()] 206 if !ok { 207 receivedFees = num.UintZero() 208 } 209 qualifiedForTier := false 210 for i := tiersLen - 1; i >= 0; i-- { 211 tier := e.currentProgram.VolumeRebateBenefitTiers[i] 212 if makerFraction.GreaterThanOrEqual(tier.MinimumPartyMakerVolumeFraction) { 213 e.factorsByParty[party] = types.VolumeRebateStats{ 214 RebateFactor: e.effectiveAdditionalRebate(tier.AdditionalMakerRebate), 215 } 216 evt.Stats = append(evt.Stats, &eventspb.PartyVolumeRebateStats{ 217 PartyId: party.String(), 218 AdditionalRebate: tier.AdditionalMakerRebate.String(), 219 MakerVolumeFraction: makerFraction.String(), 220 MakerFeesReceived: receivedFees.String(), 221 }) 222 qualifiedForTier = true 223 break 224 } 225 } 226 // if the party hasn't qualified, then still send the stats but with a zero factor 227 if !qualifiedForTier { 228 evt.Stats = append(evt.Stats, &eventspb.PartyVolumeRebateStats{ 229 PartyId: party.String(), 230 AdditionalRebate: "0", 231 MakerVolumeFraction: makerFraction.String(), 232 MakerFeesReceived: receivedFees.String(), 233 }) 234 } 235 } 236 237 e.broker.Send(events.NewVolumeRebateStatsUpdatedEvent(ctx, evt)) 238 } 239 240 func (e *Engine) OnMarketFeeFactorsTreasuryFeeUpdate(ctx context.Context, d num.Decimal) error { 241 e.treasureFee = d 242 e.maxAdditionalRebate = e.treasureFee.Add(e.buyBackFee) 243 return nil 244 } 245 246 func (e *Engine) OnMarketFeeFactorsBuyBackFeeUpdate(ctx context.Context, d num.Decimal) error { 247 e.buyBackFee = d 248 e.maxAdditionalRebate = e.treasureFee.Add(e.buyBackFee) 249 return nil 250 } 251 252 func (e *Engine) effectiveAdditionalRebate(tierRebate num.Decimal) num.Decimal { 253 return num.MinD(e.maxAdditionalRebate, tierRebate) 254 }