code.vegaprotocol.io/vega@v0.79.0/core/liquidity/v2/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 liquidity
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"time"
    22  
    23  	"code.vegaprotocol.io/vega/core/events"
    24  	"code.vegaprotocol.io/vega/core/liquidity/supplied"
    25  	"code.vegaprotocol.io/vega/core/types"
    26  	"code.vegaprotocol.io/vega/core/types/statevar"
    27  	"code.vegaprotocol.io/vega/libs/num"
    28  	"code.vegaprotocol.io/vega/logging"
    29  )
    30  
    31  var (
    32  	ErrLiquidityProvisionDoesNotExist  = errors.New("liquidity provision does not exist")
    33  	ErrLiquidityProvisionAlreadyExists = errors.New("liquidity provision already exists")
    34  	ErrCommitmentAmountIsZero          = errors.New("commitment amount is zero")
    35  )
    36  
    37  //go:generate go run github.com/golang/mock/mockgen -destination mocks/orderbook_mock.go -package mocks code.vegaprotocol.io/vega/core/liquidity/v2 OrderBook
    38  type OrderBook interface {
    39  	GetOrdersPerParty(party string) []*types.Order
    40  	GetBestStaticBidPrice() (*num.Uint, error)
    41  	GetBestStaticAskPrice() (*num.Uint, error)
    42  	GetIndicativePrice() *num.Uint
    43  	GetLastTradedPrice() *num.Uint
    44  }
    45  
    46  //go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/core/liquidity/v2 RiskModel,PriceMonitor,IDGen
    47  
    48  // Broker - event bus (no mocks needed).
    49  type Broker interface {
    50  	Send(e events.Event)
    51  	SendBatch(evts []events.Event)
    52  }
    53  
    54  // TimeService provide the time of the vega node using the tm time.
    55  //
    56  //go:generate go run github.com/golang/mock/mockgen -destination mocks/time_service_mock.go -package mocks code.vegaprotocol.io/vega/core/liquidity/v2 TimeService
    57  type TimeService interface {
    58  	GetTimeNow() time.Time
    59  }
    60  
    61  // RiskModel allows calculation of min/max price range and a probability of trading.
    62  type RiskModel interface {
    63  	ProbabilityOfTrading(currentPrice, orderPrice num.Decimal, minPrice, maxPrice num.Decimal, yFrac num.Decimal, isBid, applyMinMax bool) num.Decimal
    64  	GetProjectionHorizon() num.Decimal
    65  }
    66  
    67  // PriceMonitor provides the range of valid prices, that is prices that
    68  // wouldn't trade the current trading mode.
    69  type PriceMonitor interface {
    70  	GetValidPriceRange() (num.WrappedDecimal, num.WrappedDecimal)
    71  }
    72  
    73  // IDGen is an id generator for orders.
    74  type IDGen interface {
    75  	NextID() string
    76  }
    77  
    78  type StateVarEngine interface {
    79  	RegisterStateVariable(asset, market, name string, converter statevar.Converter, startCalculation func(string, statevar.FinaliseCalculation), trigger []statevar.EventType, result func(context.Context, statevar.StateVariableResult) error) error
    80  }
    81  
    82  type AuctionState interface {
    83  	InAuction() bool
    84  	IsOpeningAuction() bool
    85  }
    86  
    87  type slaPerformance struct {
    88  	s                 time.Duration
    89  	start             time.Time
    90  	previousPenalties *sliceRing[*num.Decimal]
    91  
    92  	lastEpochBondPenalty      string
    93  	lastEpochFeePenalty       string
    94  	lastEpochTimeBookFraction string
    95  	requiredLiquidity         string
    96  	notionalVolumeBuys        string
    97  	notionalVolumeSells       string
    98  }
    99  
   100  type SlaPenalties struct {
   101  	AllPartiesHaveFullFeePenalty bool
   102  	PenaltiesPerParty            map[string]*SlaPenalty
   103  }
   104  
   105  type SlaPenalty struct {
   106  	Fee, Bond num.Decimal
   107  }
   108  
   109  // Engine handles Liquidity provision.
   110  type Engine struct {
   111  	marketID       string
   112  	asset          string
   113  	log            *logging.Logger
   114  	timeService    TimeService
   115  	broker         Broker
   116  	suppliedEngine *supplied.Engine
   117  	orderBook      OrderBook
   118  	auctionState   AuctionState
   119  
   120  	// state
   121  	provisions        *SnapshotableProvisionsPerParty
   122  	pendingProvisions *SnapshotablePendingProvisions
   123  
   124  	// this is the max fee that can be specified
   125  	maxFee num.Decimal
   126  
   127  	// fields used for liquidity score calculation (quality of deployed orders)
   128  	avgScores map[string]num.Decimal
   129  	nAvg      int64 // counter for the liquidity score running average
   130  
   131  	// sla related net params
   132  	stakeToCcyVolume               num.Decimal
   133  	nonPerformanceBondPenaltySlope num.Decimal
   134  	nonPerformanceBondPenaltyMax   num.Decimal
   135  	feeCalculationTimeStep         time.Duration
   136  
   137  	// sla related market parameters
   138  	slaParams *types.LiquiditySLAParams
   139  
   140  	openPlusPriceRange  num.Decimal
   141  	openMinusPriceRange num.Decimal
   142  
   143  	// fields related to SLA commitment
   144  	slaPerformance map[string]*slaPerformance
   145  	slaEpochStart  time.Time
   146  
   147  	lastFeeDistribution time.Time
   148  
   149  	allocatedFeesStats *types.PaidLiquidityFeesStats
   150  
   151  	// FIXME(jerem): to remove in the future,
   152  	// this is neede for the compatibility layer from
   153  	// 72 > 73, as we would need to cancel all remaining LP
   154  	// order which are eventually still seating in the book.
   155  	legacyOrderIDs []string
   156  }
   157  
   158  // NewEngine returns a new Liquidity Engine.
   159  func NewEngine(config Config,
   160  	log *logging.Logger,
   161  	timeService TimeService,
   162  	broker Broker,
   163  	riskModel RiskModel,
   164  	priceMonitor PriceMonitor,
   165  	orderBook OrderBook,
   166  	auctionState AuctionState,
   167  	asset string,
   168  	marketID string,
   169  	stateVarEngine StateVarEngine,
   170  	positionFactor num.Decimal,
   171  	slaParams *types.LiquiditySLAParams,
   172  ) *Engine {
   173  	log = log.Named(namedLogger)
   174  	log.SetLevel(config.Level.Get())
   175  
   176  	one := num.DecimalOne()
   177  
   178  	e := &Engine{
   179  		marketID:    marketID,
   180  		asset:       asset,
   181  		log:         log,
   182  		timeService: timeService,
   183  		broker:      broker,
   184  		// tick size to be used by the supplied engine should actually be in asset decimal
   185  		suppliedEngine: supplied.NewEngine(riskModel, priceMonitor, asset, marketID, stateVarEngine, log, positionFactor),
   186  		orderBook:      orderBook,
   187  		auctionState:   auctionState,
   188  
   189  		// parameters
   190  		maxFee: num.DecimalFromInt64(1),
   191  
   192  		// provisions related state
   193  		provisions:        newSnapshotableProvisionsPerParty(),
   194  		pendingProvisions: newSnapshotablePendingProvisions(),
   195  
   196  		// SLA commitment
   197  		slaPerformance: map[string]*slaPerformance{},
   198  
   199  		openPlusPriceRange:  one.Add(slaParams.PriceRange),
   200  		openMinusPriceRange: one.Sub(slaParams.PriceRange),
   201  		slaParams:           slaParams,
   202  		allocatedFeesStats:  types.NewLiquidityFeeStats(),
   203  	}
   204  	e.ResetAverageLiquidityScores() // initialise
   205  
   206  	return e
   207  }
   208  
   209  func (e *Engine) GetLegacyOrders() (orders []string) {
   210  	orders, e.legacyOrderIDs = e.legacyOrderIDs, []string{}
   211  	return
   212  }
   213  
   214  func (e *Engine) GetLastFeeDistributionTime() time.Time {
   215  	return e.lastFeeDistribution
   216  }
   217  
   218  // SubmitLiquidityProvision handles a new liquidity provision submission.
   219  // Returns whether or not submission has been applied immediately.
   220  func (e *Engine) SubmitLiquidityProvision(
   221  	ctx context.Context,
   222  	lps *types.LiquidityProvisionSubmission,
   223  	party string,
   224  	idgen IDGen,
   225  ) (bool, error) {
   226  	if err := e.ValidateLiquidityProvisionSubmission(lps, false); err != nil {
   227  		e.rejectLiquidityProvisionSubmission(ctx, lps, party, idgen.NextID())
   228  		return false, err
   229  	}
   230  
   231  	if e.IsLiquidityProvider(party) {
   232  		return false, ErrLiquidityProvisionAlreadyExists
   233  	}
   234  
   235  	now := e.timeService.GetTimeNow().UnixNano()
   236  	provision := &types.LiquidityProvision{
   237  		ID:               idgen.NextID(),
   238  		MarketID:         lps.MarketID,
   239  		Party:            party,
   240  		CreatedAt:        now,
   241  		Fee:              lps.Fee,
   242  		Status:           types.LiquidityProvisionStatusPending,
   243  		Reference:        lps.Reference,
   244  		Version:          1,
   245  		CommitmentAmount: lps.CommitmentAmount,
   246  		UpdatedAt:        now,
   247  	}
   248  
   249  	// add immediately during the opening auction
   250  	// otherwise schedule to be added at the beginning of new epoch
   251  	if e.auctionState.IsOpeningAuction() {
   252  		e.provisions.Set(party, provision)
   253  		e.slaPerformance[party] = &slaPerformance{
   254  			previousPenalties: NewSliceRing[*num.Decimal](e.slaParams.PerformanceHysteresisEpochs),
   255  		}
   256  		provision.Status = types.LiquidityProvisionStatusActive
   257  		e.broker.Send(events.NewLiquidityProvisionEvent(ctx, provision))
   258  		return true, nil
   259  	}
   260  
   261  	e.broker.Send(events.NewLiquidityProvisionEvent(ctx, provision))
   262  
   263  	provision.Status = types.LiquidityProvisionStatusPending
   264  	e.pendingProvisions.Set(provision)
   265  	return false, nil
   266  }
   267  
   268  func (e *Engine) ApplyPendingProvisions(ctx context.Context, now time.Time) Provisions {
   269  	updatedProvisionsPerParty := make(Provisions, 0, e.pendingProvisions.Len())
   270  
   271  	for _, provision := range e.pendingProvisions.Slice() {
   272  		party := provision.Party
   273  
   274  		updatedProvisionsPerParty = append(updatedProvisionsPerParty, provision)
   275  		provision.UpdatedAt = now.UnixNano()
   276  
   277  		// if commitment was reduced to 0, all party provision related data can be deleted
   278  		// otherwise we apply the new commitment
   279  		if provision.CommitmentAmount.IsZero() {
   280  			provision.Status = types.LiquidityProvisionStatusCancelled
   281  			e.destroyProvision(party)
   282  		} else {
   283  			provision.Status = types.LiquidityProvisionStatusActive
   284  			e.provisions.Set(party, provision)
   285  			if _, ok := e.slaPerformance[party]; !ok {
   286  				e.slaPerformance[party] = &slaPerformance{
   287  					previousPenalties: NewSliceRing[*num.Decimal](e.slaParams.PerformanceHysteresisEpochs),
   288  				}
   289  			}
   290  		}
   291  
   292  		e.broker.Send(events.NewLiquidityProvisionEvent(ctx, provision))
   293  	}
   294  
   295  	e.pendingProvisions = newSnapshotablePendingProvisions()
   296  	return updatedProvisionsPerParty
   297  }
   298  
   299  func (e *Engine) PendingProvisionByPartyID(party string) *types.LiquidityProvision {
   300  	provision, _ := e.pendingProvisions.Get(party)
   301  	return provision
   302  }
   303  
   304  func (e *Engine) PendingProvision() Provisions {
   305  	return e.pendingProvisions.Slice()
   306  }
   307  
   308  // RejectLiquidityProvision removes a parties commitment of liquidity.
   309  func (e *Engine) RejectLiquidityProvision(ctx context.Context, party string) error {
   310  	return e.stopLiquidityProvision(
   311  		ctx, party, types.LiquidityProvisionStatusRejected)
   312  }
   313  
   314  // CancelLiquidityProvision removes a parties commitment of liquidity
   315  // Returns the liquidityOrders if any.
   316  func (e *Engine) CancelLiquidityProvision(ctx context.Context, party string) error {
   317  	return e.stopLiquidityProvision(
   318  		ctx, party, types.LiquidityProvisionStatusCancelled)
   319  }
   320  
   321  // StopLiquidityProvision removes a parties commitment of liquidity
   322  // Returns the liquidityOrders if any.
   323  func (e *Engine) StopLiquidityProvision(ctx context.Context, party string) error {
   324  	return e.stopLiquidityProvision(
   325  		ctx, party, types.LiquidityProvisionStatusStopped)
   326  }
   327  
   328  func (e *Engine) ValidateLiquidityProvisionSubmission(
   329  	lp *types.LiquidityProvisionSubmission,
   330  	zeroCommitmentIsValid bool,
   331  ) (err error) {
   332  	// we check if the commitment is 0 which would mean this is a cancel
   333  	// a cancel does not need validations
   334  	if lp.CommitmentAmount.IsZero() {
   335  		if zeroCommitmentIsValid {
   336  			return nil
   337  		}
   338  		return ErrCommitmentAmountIsZero
   339  	}
   340  
   341  	// not sure how to check for a missing fee, 0 could be valid
   342  	// then again, that validation should've happened before reaching this point
   343  	if lp.Fee.IsNegative() || lp.Fee.GreaterThan(e.maxFee) {
   344  		return errors.New("invalid liquidity provision fee")
   345  	}
   346  
   347  	return nil
   348  }
   349  
   350  func (e *Engine) stopLiquidityProvision(
   351  	ctx context.Context, party string, status types.LiquidityProvisionStatus,
   352  ) error {
   353  	lp, ok := e.provisions.Get(party)
   354  	if !ok {
   355  		lp, ok = e.pendingProvisions.Get(party)
   356  		if !ok {
   357  			return errors.New("party is not a liquidity provider")
   358  		}
   359  	}
   360  	now := e.timeService.GetTimeNow().UnixNano()
   361  
   362  	lp.Status = status
   363  	lp.UpdatedAt = now
   364  	e.broker.Send(events.NewLiquidityProvisionEvent(ctx, lp))
   365  
   366  	// now delete all party related data stuff
   367  	e.destroyProvision(party)
   368  	return nil
   369  }
   370  
   371  func (e *Engine) destroyProvision(party string) {
   372  	e.provisions.Delete(party)
   373  	delete(e.slaPerformance, party)
   374  	e.pendingProvisions.Delete(party)
   375  }
   376  
   377  func (e *Engine) rejectLiquidityProvisionSubmission(ctx context.Context, lps *types.LiquidityProvisionSubmission, party, id string) {
   378  	lp := &types.LiquidityProvision{
   379  		ID:               id,
   380  		Fee:              lps.Fee,
   381  		MarketID:         lps.MarketID,
   382  		Party:            party,
   383  		Status:           types.LiquidityProvisionStatusRejected,
   384  		CreatedAt:        e.timeService.GetTimeNow().UnixNano(),
   385  		CommitmentAmount: lps.CommitmentAmount.Clone(),
   386  		Reference:        lps.Reference,
   387  	}
   388  
   389  	e.broker.Send(events.NewLiquidityProvisionEvent(ctx, lp))
   390  }
   391  
   392  // IsLiquidityProvider returns true if the party hold any liquidity commitment.
   393  func (e *Engine) IsLiquidityProvider(party string) bool {
   394  	_, ok := e.provisions.Get(party)
   395  	_, pendingOk := e.pendingProvisions.Get(party)
   396  	return ok || pendingOk
   397  }
   398  
   399  // ProvisionsPerParty returns the registered a map of party-id -> LiquidityProvision.
   400  func (e *Engine) ProvisionsPerParty() ProvisionsPerParty {
   401  	return e.provisions.ProvisionsPerParty
   402  }
   403  
   404  // LiquidityProvisionByPartyID returns the LP associated to a Party if any.
   405  // If not, it returns nil.
   406  func (e *Engine) LiquidityProvisionByPartyID(partyID string) *types.LiquidityProvision {
   407  	lp, _ := e.provisions.Get(partyID)
   408  	return lp
   409  }
   410  
   411  // UpdatePartyCommitment allows to change party commitment.
   412  // It should be used for synchronizing commitment with bond account.
   413  func (e *Engine) UpdatePartyCommitment(partyID string, newCommitment *num.Uint) (*types.LiquidityProvision, error) {
   414  	lp, ok := e.provisions.Get(partyID)
   415  	if !ok {
   416  		return nil, ErrLiquidityProvisionDoesNotExist
   417  	}
   418  
   419  	lp.CommitmentAmount = newCommitment.Clone()
   420  	e.provisions.Set(partyID, lp)
   421  	return lp, nil
   422  }
   423  
   424  // CalculateSuppliedStake returns the sum of commitment amounts from all the liquidity providers.
   425  // Includes pending commitment if they are greater then the original one.
   426  func (e *Engine) CalculateSuppliedStake() *num.Uint {
   427  	supplied := num.UintZero()
   428  
   429  	for _, pending := range e.pendingProvisions.PendingProvisions {
   430  		provision, ok := e.provisions.Get(pending.Party)
   431  		if ok && pending.CommitmentAmount.LT(provision.CommitmentAmount) {
   432  			supplied.AddSum(provision.CommitmentAmount)
   433  			continue
   434  		}
   435  		supplied.AddSum(pending.CommitmentAmount)
   436  	}
   437  
   438  	for party, provision := range e.provisions.ProvisionsPerParty {
   439  		_, ok := e.pendingProvisions.Get(party)
   440  		if ok {
   441  			continue
   442  		}
   443  
   444  		supplied.AddSum(provision.CommitmentAmount)
   445  	}
   446  
   447  	return supplied
   448  }
   449  
   450  // CalculateSuppliedStakeWithoutPending returns the sum of commitment amounts
   451  // from all the liquidity providers. Does not include pending commitments.
   452  func (e *Engine) CalculateSuppliedStakeWithoutPending() *num.Uint {
   453  	supplied := num.UintZero()
   454  	for _, provision := range e.provisions.ProvisionsPerParty {
   455  		supplied.AddSum(provision.CommitmentAmount)
   456  	}
   457  	return supplied
   458  }
   459  
   460  func (e *Engine) IsProbabilityOfTradingInitialised() bool {
   461  	return e.suppliedEngine.IsProbabilityOfTradingInitialised()
   462  }
   463  
   464  func (e *Engine) UpdateMarketConfig(model RiskModel, monitor PriceMonitor) {
   465  	e.suppliedEngine.UpdateMarketConfig(model, monitor)
   466  }
   467  
   468  func (e *Engine) UpdateSLAParameters(slaParams *types.LiquiditySLAParams) {
   469  	e.onSLAParamsUpdate(slaParams)
   470  }
   471  
   472  func (e *Engine) SetGetStaticPricesFunc(f func() (num.Decimal, num.Decimal, error)) {
   473  	e.suppliedEngine.SetGetStaticPricesFunc(f)
   474  }
   475  
   476  func (e *Engine) ReadyForFeesAllocation(now time.Time) bool {
   477  	return now.Sub(e.lastFeeDistribution) > e.feeCalculationTimeStep
   478  }
   479  
   480  func (e *Engine) ResetFeeAllocationPeriod(t time.Time) {
   481  	e.ResetAverageLiquidityScores()
   482  	e.lastFeeDistribution = t
   483  }
   484  
   485  func (e *Engine) OnMinProbabilityOfTradingLPOrdersUpdate(v num.Decimal) {
   486  	e.suppliedEngine.OnMinProbabilityOfTradingLPOrdersUpdate(v)
   487  }
   488  
   489  func (e *Engine) OnProbabilityOfTradingTauScalingUpdate(v num.Decimal) {
   490  	e.suppliedEngine.OnProbabilityOfTradingTauScalingUpdate(v)
   491  }
   492  
   493  func (e *Engine) OnMaximumLiquidityFeeFactorLevelUpdate(f num.Decimal) {
   494  	e.maxFee = f
   495  }
   496  
   497  func (e *Engine) OnStakeToCcyVolumeUpdate(stakeToCcyVolume num.Decimal) {
   498  	e.stakeToCcyVolume = stakeToCcyVolume
   499  }
   500  
   501  func (e *Engine) OnNonPerformanceBondPenaltySlopeUpdate(nonPerformanceBondPenaltySlope num.Decimal) {
   502  	e.nonPerformanceBondPenaltySlope = nonPerformanceBondPenaltySlope
   503  }
   504  
   505  func (e *Engine) OnNonPerformanceBondPenaltyMaxUpdate(nonPerformanceBondPenaltyMax num.Decimal) {
   506  	e.nonPerformanceBondPenaltyMax = nonPerformanceBondPenaltyMax
   507  }
   508  
   509  func (e *Engine) OnProvidersFeeCalculationTimeStep(d time.Duration) {
   510  	e.feeCalculationTimeStep = d
   511  }
   512  
   513  func (e *Engine) onSLAParamsUpdate(slaParams *types.LiquiditySLAParams) {
   514  	one := num.DecimalOne()
   515  	e.openPlusPriceRange = one.Add(slaParams.PriceRange)
   516  	e.openMinusPriceRange = one.Sub(slaParams.PriceRange)
   517  	if e.slaParams.PerformanceHysteresisEpochs != slaParams.PerformanceHysteresisEpochs {
   518  		for _, performance := range e.slaPerformance {
   519  			performance.previousPenalties.ModifySize(slaParams.PerformanceHysteresisEpochs)
   520  		}
   521  	}
   522  	e.slaParams = slaParams
   523  }