code.vegaprotocol.io/vega@v0.79.0/core/liquidity/v2/sla.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 "sort" 20 "time" 21 22 "code.vegaprotocol.io/vega/core/types" 23 "code.vegaprotocol.io/vega/libs/num" 24 ) 25 26 var DefaultSLAParameters = types.LiquiditySLAParams{ 27 PriceRange: num.MustDecimalFromString("0.05"), 28 CommitmentMinTimeFraction: num.MustDecimalFromString("0.95"), 29 PerformanceHysteresisEpochs: 1, 30 SlaCompetitionFactor: num.MustDecimalFromString("0.90"), 31 } 32 33 // ResetSLAEpoch should be called at the beginning of epoch to reset per epoch performance calculations. 34 // Returns a newly added/amended liquidity provisions (pending provisions are automatically applied and the start of a new epoch). 35 func (e *Engine) ResetSLAEpoch( 36 now time.Time, 37 markPrice *num.Uint, 38 midPrice *num.Uint, 39 positionFactor num.Decimal, 40 ) { 41 e.allocatedFeesStats = types.NewLiquidityFeeStats() 42 e.slaEpochStart = now 43 44 if e.auctionState.IsOpeningAuction() { 45 return 46 } 47 48 for party, commitment := range e.slaPerformance { 49 commitment.start = time.Time{} 50 if e.doesLPMeetsCommitment(party, markPrice, midPrice, positionFactor) { 51 commitment.start = now 52 } 53 54 commitment.s = 0 55 } 56 } 57 58 func (e *Engine) EndBlock(markPrice *num.Uint, midPrice *num.Uint, positionFactor num.Decimal) { 59 // Check if the k transaction has been processed 60 if e.auctionState.IsOpeningAuction() { 61 return 62 } 63 64 for party, commitment := range e.slaPerformance { 65 if meetsCommitment := e.doesLPMeetsCommitment(party, markPrice, midPrice, positionFactor); meetsCommitment { 66 // if LP started meeting commitment 67 if commitment.start.IsZero() { 68 commitment.start = e.timeService.GetTimeNow() 69 } 70 continue 71 } 72 // else if LP stopped meeting commitment 73 if !commitment.start.IsZero() { 74 commitment.s += e.timeService.GetTimeNow().Sub(commitment.start) 75 commitment.start = time.Time{} 76 } 77 } 78 } 79 80 func (e *Engine) calculateCurrentTimeBookFraction(now, start time.Time, s time.Duration) num.Decimal { 81 if !start.IsZero() { 82 s += now.Sub(start) 83 } 84 85 observedEpochLength := now.Sub(e.slaEpochStart) 86 lNano := observedEpochLength.Nanoseconds() 87 timeBookFraction := num.DecimalZero() 88 if lNano > 0 { 89 timeBookFraction = num.DecimalFromInt64(s.Nanoseconds()).Div(num.DecimalFromInt64(lNano)) 90 } 91 return num.MinD(num.DecimalOne(), timeBookFraction) 92 } 93 94 // CalculateSLAPenalties should be called at the and of epoch to calculate SLA penalties based on LP performance in the epoch. 95 func (e *Engine) CalculateSLAPenalties(now time.Time) SlaPenalties { 96 penaltiesPerParty := map[string]*SlaPenalty{} 97 98 // Do not apply any penalties during opening auction 99 if e.auctionState.IsOpeningAuction() { 100 return SlaPenalties{ 101 AllPartiesHaveFullFeePenalty: false, 102 PenaltiesPerParty: penaltiesPerParty, 103 } 104 } 105 106 one := num.DecimalOne() 107 partiesWithFullFeePenaltyCount := 0 108 109 for party, commitment := range e.slaPerformance { 110 timeBookFraction := e.calculateCurrentTimeBookFraction(now, commitment.start, commitment.s) 111 112 var feePenalty, bondPenalty num.Decimal 113 114 // if LP meets commitment 115 // else LP does not meet commitment 116 if timeBookFraction.LessThan(e.slaParams.CommitmentMinTimeFraction) { 117 feePenalty = one 118 bondPenalty = e.calculateBondPenalty(timeBookFraction) 119 } else { 120 feePenalty = e.calculateCurrentFeePenalty(timeBookFraction) 121 bondPenalty = num.DecimalZero() 122 } 123 124 penaltiesPerParty[party] = &SlaPenalty{ 125 Bond: bondPenalty, 126 Fee: e.calculateHysteresisFeePenalty(feePenalty, commitment.previousPenalties.Slice()), 127 } 128 129 commitment.previousPenalties.Add(&feePenalty) 130 131 if penaltiesPerParty[party].Fee.Equal(one) { 132 partiesWithFullFeePenaltyCount++ 133 } 134 135 // safe for next epoch stats 136 e.slaPerformance[party].lastEpochBondPenalty = penaltiesPerParty[party].Bond.String() 137 e.slaPerformance[party].lastEpochFeePenalty = penaltiesPerParty[party].Fee.String() 138 e.slaPerformance[party].lastEpochTimeBookFraction = timeBookFraction.String() 139 } 140 141 return SlaPenalties{ 142 AllPartiesHaveFullFeePenalty: partiesWithFullFeePenaltyCount == len(penaltiesPerParty), 143 PenaltiesPerParty: penaltiesPerParty, 144 } 145 } 146 147 func (e *Engine) doesLPMeetsCommitment( 148 party string, 149 markPrice *num.Uint, 150 midPrice *num.Uint, 151 positionFactor num.Decimal, 152 ) bool { 153 lp, ok := e.provisions.Get(party) 154 if !ok { 155 return false 156 } 157 158 var minPrice, maxPrice num.Decimal 159 if e.auctionState.InAuction() { 160 minPriceFactor := num.Min(e.orderBook.GetLastTradedPrice(), e.orderBook.GetIndicativePrice()).ToDecimal() 161 maxPriceFactor := num.Max(e.orderBook.GetLastTradedPrice(), e.orderBook.GetIndicativePrice()).ToDecimal() 162 163 // (1.0-market.liquidity.priceRange) x min(last trade price, indicative uncrossing price) 164 minPrice = e.openMinusPriceRange.Mul(minPriceFactor) 165 // (1.0+market.liquidity.priceRange) x max(last trade price, indicative uncrossing price) 166 maxPrice = e.openPlusPriceRange.Mul(maxPriceFactor) 167 } else { 168 // if there is no mid price then LP is not meeting their committed volume of notional. 169 if midPrice.IsZero() { 170 return false 171 } 172 midD := midPrice.ToDecimal() 173 // (1.0 - market.liquidity.priceRange) x mid 174 minPrice = e.openMinusPriceRange.Mul(midD) 175 // (1.0 + market.liquidity.priceRange) x mid 176 maxPrice = e.openPlusPriceRange.Mul(midD) 177 } 178 179 notionalVolumeBuys := num.DecimalZero() 180 notionalVolumeSells := num.DecimalZero() 181 orders := e.getAllActiveOrders(party) 182 183 for _, o := range orders { 184 price := o.Price.ToDecimal() 185 // this order is in range and does contribute to the volume on notional 186 if price.GreaterThanOrEqual(minPrice) && price.LessThanOrEqual(maxPrice) { 187 orderVolume := num.UintZero().Mul(markPrice, num.NewUint(o.TrueRemaining())).ToDecimal().Div(positionFactor) 188 189 if o.Side == types.SideSell { 190 notionalVolumeSells = notionalVolumeSells.Add(orderVolume) 191 } else { 192 notionalVolumeBuys = notionalVolumeBuys.Add(orderVolume) 193 } 194 } 195 } 196 197 requiredLiquidity := e.stakeToCcyVolume.Mul(lp.CommitmentAmount.ToDecimal()) 198 199 // safe stats 200 e.slaPerformance[party].requiredLiquidity = requiredLiquidity.String() 201 e.slaPerformance[party].notionalVolumeBuys = notionalVolumeBuys.String() 202 e.slaPerformance[party].notionalVolumeSells = notionalVolumeSells.String() 203 204 return notionalVolumeBuys.GreaterThanOrEqual(requiredLiquidity) && 205 notionalVolumeSells.GreaterThanOrEqual(requiredLiquidity) 206 } 207 208 func (e *Engine) calculateCurrentFeePenalty(timeBookFraction num.Decimal) num.Decimal { 209 one := num.DecimalOne() 210 211 if timeBookFraction.LessThan(e.slaParams.CommitmentMinTimeFraction) { 212 return one 213 } 214 215 if timeBookFraction.Equal(e.slaParams.CommitmentMinTimeFraction) && timeBookFraction.Equal(one) { 216 return num.DecimalZero() 217 } 218 219 // p = (1-[timeBookFraction-commitmentMinTimeFraction/1-commitmentMinTimeFraction]) * slaCompetitionFactor 220 return one.Sub( 221 timeBookFraction.Sub(e.slaParams.CommitmentMinTimeFraction).Div(one.Sub(e.slaParams.CommitmentMinTimeFraction)), 222 ).Mul(e.slaParams.SlaCompetitionFactor) 223 } 224 225 func (e *Engine) calculateBondPenalty(timeBookFraction num.Decimal) num.Decimal { 226 // min(nonPerformanceBondPenaltyMax, nonPerformanceBondPenaltySlope * (1-timeBookFraction/commitmentMinTimeFraction)) 227 min := num.MinD( 228 e.nonPerformanceBondPenaltyMax, 229 e.nonPerformanceBondPenaltySlope.Mul(num.DecimalOne().Sub(timeBookFraction.Div(e.slaParams.CommitmentMinTimeFraction))), 230 ) 231 232 // max(0, min) 233 return num.MaxD(num.DecimalZero(), min) 234 } 235 236 func (e *Engine) calculateHysteresisFeePenalty(currentPenalty num.Decimal, previousPenalties []*num.Decimal) num.Decimal { 237 one := num.DecimalOne() 238 previousPenaltiesCount := num.DecimalZero() 239 periodAveragePenalty := num.DecimalZero() 240 241 for _, p := range previousPenalties { 242 if p == nil { 243 continue 244 } 245 246 periodAveragePenalty = periodAveragePenalty.Add(*p) 247 previousPenaltiesCount = previousPenaltiesCount.Add(one) 248 } 249 250 if previousPenaltiesCount.IsZero() { 251 return currentPenalty 252 } 253 254 periodAveragePenalty = periodAveragePenalty.Div(previousPenaltiesCount) 255 256 return num.MaxD(currentPenalty, periodAveragePenalty) 257 } 258 259 func (e *Engine) LiquidityProviderSLAStats(now time.Time) []*types.LiquidityProviderSLA { 260 stats := make([]*types.LiquidityProviderSLA, 0, len(e.slaPerformance)) 261 262 for partyID, commitment := range e.slaPerformance { 263 currentTimeBookFraction := e.calculateCurrentTimeBookFraction(now, commitment.start, commitment.s) 264 265 previousPenalties := commitment.previousPenalties.Slice() 266 hysteresisPeriodFeePenalties := make([]string, 0, len(previousPenalties)) 267 for _, penalty := range previousPenalties { 268 if penalty == nil { 269 continue 270 } 271 hysteresisPeriodFeePenalties = append(hysteresisPeriodFeePenalties, penalty.String()) 272 } 273 274 stats = append(stats, &types.LiquidityProviderSLA{ 275 Party: partyID, 276 CurrentEpochFractionOfTimeOnBook: currentTimeBookFraction.String(), 277 LastEpochFractionOfTimeOnBook: commitment.lastEpochTimeBookFraction, 278 LastEpochFeePenalty: commitment.lastEpochFeePenalty, 279 LastEpochBondPenalty: commitment.lastEpochBondPenalty, 280 HysteresisPeriodFeePenalties: hysteresisPeriodFeePenalties, 281 RequiredLiquidity: commitment.requiredLiquidity, 282 NotionalVolumeBuys: commitment.notionalVolumeBuys, 283 NotionalVolumeSells: commitment.notionalVolumeSells, 284 }) 285 } 286 287 sort.Slice(stats, func(i, j int) bool { 288 if stats[i].Party == stats[j].Party { 289 return stats[i].CurrentEpochFractionOfTimeOnBook > stats[j].CurrentEpochFractionOfTimeOnBook 290 } 291 return stats[i].Party > stats[j].Party 292 }) 293 294 return stats 295 } 296 297 func (e *Engine) RegisterAllocatedFeesPerParty(feesPerParty map[string]*num.Uint) { 298 e.allocatedFeesStats.RegisterTotalFeesAmountPerParty(feesPerParty) 299 } 300 301 func (e *Engine) PaidLiquidityFeesStats() *types.PaidLiquidityFeesStats { 302 return e.allocatedFeesStats 303 }