code.vegaprotocol.io/vega@v0.79.0/core/liquidity/target/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 target 17 18 import ( 19 "errors" 20 "time" 21 22 "code.vegaprotocol.io/vega/core/types" 23 "code.vegaprotocol.io/vega/libs/num" 24 ) 25 26 var ( 27 // ErrTimeSequence signals that time sequence is not in a non-decreasing order. 28 ErrTimeSequence = errors.New("received a time that's before the last received time") 29 // ErrNegativeScalingFactor indicates that a negative scaling factor was supplied to the engine. 30 ErrNegativeScalingFactor = errors.New("scaling factor can't be negative") 31 ) 32 33 var ( 34 exp = num.UintZero().Exp(num.NewUint(10), num.NewUint(5)) 35 exp2 = num.UintZero().Exp(num.NewUint(10), num.NewUint(10)) 36 expDec = num.DecimalFromUint(exp) 37 ) 38 39 // Engine allows tracking price changes and verifying them against the theoretical levels implied by the RangeProvider (risk model). 40 type Engine struct { 41 marketID string 42 43 tWindow time.Duration 44 sFactor *num.Uint 45 oiCalc OpenInterestCalculator 46 47 now time.Time 48 scheduledTruncate time.Time 49 current []uint64 50 previous []timestampedOI 51 max timestampedOI 52 positionFactor num.Decimal 53 } 54 55 type timestampedOI struct { 56 Time time.Time 57 OI uint64 58 } 59 60 //go:generate go run github.com/golang/mock/mockgen -destination mocks/open_interest_calculator_mock.go -package mocks code.vegaprotocol.io/vega/core/liquidity/target OpenInterestCalculator 61 type OpenInterestCalculator interface { 62 GetOpenInterestGivenTrades(trades []*types.Trade) uint64 63 } 64 65 // NewEngine returns a new instance of target stake calculation Engine. 66 func NewEngine(parameters types.TargetStakeParameters, oiCalc OpenInterestCalculator, marketID string, positionFactor num.Decimal) *Engine { 67 factor, _ := num.UintFromDecimal(parameters.ScalingFactor.Mul(expDec)) 68 69 return &Engine{ 70 marketID: marketID, 71 tWindow: time.Duration(parameters.TimeWindow) * time.Second, 72 sFactor: factor, 73 oiCalc: oiCalc, 74 positionFactor: positionFactor, 75 } 76 } 77 78 // UpdateTimeWindow updates the time windows used in target stake calculation. 79 func (e *Engine) UpdateTimeWindow(tWindow time.Duration) { 80 e.tWindow = tWindow 81 } 82 83 // UpdateScalingFactor updates the scaling factor used in target stake calculation 84 // if it's non-negative and returns an error otherwise. 85 func (e *Engine) UpdateScalingFactor(sFactor num.Decimal) error { 86 if sFactor.IsNegative() { 87 return ErrNegativeScalingFactor 88 } 89 factor, _ := num.UintFromDecimal(sFactor.Mul(expDec)) 90 e.sFactor = factor 91 92 return nil 93 } 94 95 // RecordOpenInterest records open interest history so that target stake can be calculated. 96 func (e *Engine) RecordOpenInterest(oi uint64, now time.Time) error { 97 if now.Before(e.now) { 98 return ErrTimeSequence 99 } 100 101 if oi >= e.max.OI { 102 e.max = timestampedOI{Time: now, OI: oi} 103 } 104 105 if now.After(e.now) { 106 // get max from current before updating timestamp 107 e.previous = append(e.previous, e.getMaxFromCurrent()) 108 e.current = make([]uint64, 0, len(e.current)) 109 e.now = now 110 } 111 e.current = append(e.current, oi) 112 113 if e.now.After(e.scheduledTruncate) { 114 e.truncateHistory(e.minTime(now)) 115 } 116 117 return nil 118 } 119 120 // GetTargetStake returns target stake based current time, risk factors 121 // and the open interest time series constructed by calls to RecordOpenInterest. 122 func (e *Engine) GetTargetStake(rf types.RiskFactor, now time.Time, markPrice *num.Uint) (*num.Uint, bool) { 123 var changed bool 124 if minTime := e.minTime(now); minTime.After(e.max.Time) { 125 e.computeMaxOI(minTime) 126 changed = true 127 } 128 129 // float64(markPrice.Uint64()*e.max.OI) * math.Max(rf.Short, rf.Long) * e.sFactor 130 factor := rf.Long 131 if factor.LessThan(rf.Short) { 132 factor = rf.Short 133 } 134 factorUint, _ := num.UintFromDecimal(factor.Mul(expDec)) 135 136 value, _ := num.UintFromDecimal(markPrice.ToDecimal().Mul(num.DecimalFromInt64(int64(e.max.OI))).Div(e.positionFactor)) 137 return num.UintZero().Div(num.UintZero().Mul(value, factorUint.Mul(factorUint, e.sFactor)), exp2), changed 138 } 139 140 // GetTheoreticalTargetStake returns target stake based current time, risk factors 141 // and the supplied trades without modifying the internal state. 142 func (e *Engine) GetTheoreticalTargetStake(rf types.RiskFactor, now time.Time, markPrice *num.Uint, trades []*types.Trade) (*num.Uint, bool) { 143 var changed bool 144 theoreticalOI := e.oiCalc.GetOpenInterestGivenTrades(trades) 145 146 timeWindowStart := e.minTime(now) 147 maxExpired := timeWindowStart.After(e.max.Time) 148 if maxExpired { 149 e.computeMaxOI(timeWindowStart) 150 changed = true 151 } 152 153 maxOI := e.max.OI 154 if theoreticalOI > maxOI || maxExpired { 155 maxOI = theoreticalOI 156 } 157 158 factor := rf.Long 159 if factor.LessThan(rf.Short) { 160 factor = rf.Short 161 } 162 163 factorUint, _ := num.UintFromDecimal(factor.Mul(expDec)) 164 value, _ := num.UintFromDecimal(markPrice.ToDecimal().Mul(num.DecimalFromInt64(int64(maxOI))).Div(e.positionFactor)) 165 return num.UintZero().Div(num.UintZero().Mul(value, factorUint.Mul(factorUint, e.sFactor)), exp2), changed 166 } 167 168 func (e *Engine) UpdateParameters(parameters types.TargetStakeParameters) { 169 factor, _ := num.UintFromDecimal(parameters.ScalingFactor.Mul(expDec)) 170 e.sFactor = factor 171 e.tWindow = time.Duration(parameters.TimeWindow) * time.Second 172 } 173 174 func (e *Engine) getMaxFromCurrent() timestampedOI { 175 if len(e.current) == 0 { 176 return timestampedOI{Time: e.now, OI: 0} 177 } 178 m := e.current[0] 179 for i := 1; i < len(e.current); i++ { 180 if e.current[i] > m { 181 m = e.current[i] 182 } 183 } 184 return timestampedOI{Time: e.now, OI: m} 185 } 186 187 func (e *Engine) computeMaxOI(minTime time.Time) { 188 m := e.getMaxFromCurrent() 189 e.truncateHistory(minTime) 190 var j int 191 for i := 0; i < len(e.previous); i++ { 192 if e.previous[i].OI > m.OI { 193 m = e.previous[i] 194 j = i 195 } 196 } 197 e.max = m 198 199 // remove entries less than max as these won't ever be needed anyway 200 e.previous = e.previous[j:] 201 } 202 203 // minTime returns the lower bound of the sliding time window. 204 func (e *Engine) minTime(now time.Time) time.Time { 205 return now.Add(-e.tWindow) 206 } 207 208 func (e *Engine) truncateHistory(minTime time.Time) { 209 var i int 210 for i = 0; i < len(e.previous); i++ { 211 if !e.previous[i].Time.Before(minTime) { 212 break 213 } 214 } 215 e.previous = e.previous[i:] 216 // Truncate at least every 2 time windows in case not called before to prevent excessive memory usage 217 e.scheduledTruncate = e.now.Add(2 * e.tWindow) 218 }