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 }