code.vegaprotocol.io/vega@v0.79.0/core/liquidity/target/spot/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 spot 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 // Engine allows tracking price changes and verifying them against the theoretical levels implied by the RangeProvider (risk model). 34 type Engine struct { 35 marketID string 36 37 tWindow time.Duration 38 sFactor num.Decimal 39 40 now time.Time 41 scheduledTruncate time.Time 42 current []uint64 43 previous []timestampedTotalStake 44 max timestampedTotalStake 45 positionFactor num.Decimal 46 } 47 48 type timestampedTotalStake struct { 49 Time time.Time 50 TotalStake uint64 51 } 52 53 // NewEngine returns a new instance of target stake calculation Engine. 54 func NewEngine(parameters types.TargetStakeParameters, marketID string, positionFactor num.Decimal) *Engine { 55 return &Engine{ 56 marketID: marketID, 57 tWindow: time.Duration(parameters.TimeWindow) * time.Second, 58 sFactor: parameters.ScalingFactor, 59 positionFactor: positionFactor, 60 } 61 } 62 63 // UpdateTimeWindow updates the time windows used in target stake calculation. 64 func (e *Engine) UpdateTimeWindow(tWindow time.Duration) { 65 e.tWindow = tWindow 66 } 67 68 // UpdateScalingFactor updates the scaling factor used in target stake calculation 69 // if it's non-negative and returns an error otherwise. 70 func (e *Engine) UpdateScalingFactor(sFactor num.Decimal) error { 71 if sFactor.IsNegative() { 72 return ErrNegativeScalingFactor 73 } 74 e.sFactor = sFactor 75 return nil 76 } 77 78 // RecordTotalStake records open interest history so that target stake can be calculated. 79 func (e *Engine) RecordTotalStake(ts uint64, now time.Time) error { 80 if now.Before(e.now) { 81 return ErrTimeSequence 82 } 83 84 if ts >= e.max.TotalStake { 85 e.max = timestampedTotalStake{Time: now, TotalStake: ts} 86 } 87 88 if now.After(e.now) { 89 // get max from current before updating timestamp 90 e.previous = append(e.previous, e.getMaxFromCurrent()) 91 e.current = make([]uint64, 0, len(e.current)) 92 e.now = now 93 } 94 e.current = append(e.current, ts) 95 if e.now.After(e.scheduledTruncate) { 96 e.truncateHistory(e.minTime(now)) 97 } 98 99 return nil 100 } 101 102 // GetTargetStake returns target stake based current time. 103 func (e *Engine) GetTargetStake(now time.Time) *num.Uint { 104 if minTime := e.minTime(now); minTime.After(e.max.Time) { 105 e.computeMaxTotalStake(minTime) 106 } 107 108 value, _ := num.UintFromDecimal(num.DecimalFromInt64(int64(e.max.TotalStake)).Mul(e.sFactor)) 109 return value 110 } 111 112 func (e *Engine) UpdateParameters(parameters types.TargetStakeParameters) { 113 e.sFactor = parameters.ScalingFactor 114 e.tWindow = time.Duration(parameters.TimeWindow) * time.Second 115 } 116 117 func (e *Engine) getMaxFromCurrent() timestampedTotalStake { 118 if len(e.current) == 0 { 119 return timestampedTotalStake{Time: e.now, TotalStake: 0} 120 } 121 m := e.current[0] 122 for i := 1; i < len(e.current); i++ { 123 if e.current[i] > m { 124 m = e.current[i] 125 } 126 } 127 return timestampedTotalStake{Time: e.now, TotalStake: m} 128 } 129 130 func (e *Engine) computeMaxTotalStake(minTime time.Time) { 131 m := e.getMaxFromCurrent() 132 e.truncateHistory(minTime) 133 var j int 134 for i := 0; i < len(e.previous); i++ { 135 if e.previous[i].TotalStake > m.TotalStake { 136 m = e.previous[i] 137 j = i 138 } 139 } 140 e.max = m 141 142 // remove entries less than max as these won't ever be needed anyway 143 e.previous = e.previous[j:] 144 } 145 146 // minTime returns the lower bound of the sliding time window. 147 func (e *Engine) minTime(now time.Time) time.Time { 148 return now.Add(-e.tWindow) 149 } 150 151 func (e *Engine) truncateHistory(minTime time.Time) { 152 var i int 153 for i = 0; i < len(e.previous); i++ { 154 if !e.previous[i].Time.Before(minTime) { 155 break 156 } 157 } 158 e.previous = e.previous[i:] 159 // Truncate at least every 2 time windows in case not called before to prevent excessive memory usage 160 e.scheduledTruncate = e.now.Add(2 * e.tWindow) 161 }