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  }