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  }