code.vegaprotocol.io/vega@v0.79.0/core/liquidity/supplied/statevar.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 supplied 17 18 import ( 19 "context" 20 "sort" 21 22 "code.vegaprotocol.io/vega/core/types/statevar" 23 "code.vegaprotocol.io/vega/libs/num" 24 "code.vegaprotocol.io/vega/logging" 25 ) 26 27 type probabilityOfTradingConverter struct{} 28 29 type probabilityOfTrading struct { 30 bidOffset []uint32 // represents offsets from best bid - not sent over the wire 31 bidProbability []num.Decimal // probability[0] = probability of the best bid, probabtility[len-1] = probability of the worst bid 32 askOffset []uint32 // represents offsets from best ask - not sent over the wire 33 askProbability []num.Decimal // probability[0] = probability of the best ask, probabtility[len-1] = probability of the worst ask 34 } 35 36 var ( 37 defaultInRangeProbabilityOfTrading = num.DecimalFromFloat(0.5) 38 defaultMinimumProbabilityOfTrading = num.DecimalFromFloat(1e-8) 39 tolerance = num.DecimalFromFloat(1e-6) 40 OffsetIncrement = uint32(1000) // the increment of offsets in the offset slice - 1/PriceIncrementFactor as integer 41 OffsetIncrementAsDecimal = num.DecimalFromInt64(int64(OffsetIncrement)) 42 OffsetOneDecimal = OffsetIncrementAsDecimal.Mul(OffsetIncrementAsDecimal) // an offset of 100% 43 PriceIncrementFactor = num.DecimalOne().Div(num.DecimalFromInt64(int64(OffsetIncrement))) // we calculate the probability of trading in increments of 0.001 from of the best bid/ask 44 maxDistanceWhenNoConsensusPct = num.DecimalFromFloat(20) // if there's no consensus yet and the price is within 20% of the best bid/ask it gets the default probability 45 maxDistanceWhenNoConsensusFactor = maxDistanceWhenNoConsensusPct.Div(num.DecimalFromFloat(100)) // if there's no consensus yet and the price is within 0.2 of the best bid/ask it gets the default probability 46 47 maxAskMultiplier = num.DecimalFromFloat(6) // arbitrarily large factor on the best ask - 600% 48 ) 49 50 func (probabilityOfTradingConverter) BundleToInterface(kvb *statevar.KeyValueBundle) statevar.StateVariableResult { 51 return &probabilityOfTrading{ 52 bidProbability: kvb.KVT[0].Val.(*statevar.DecimalVector).Val, 53 askProbability: kvb.KVT[1].Val.(*statevar.DecimalVector).Val, 54 } 55 } 56 57 func (probabilityOfTradingConverter) InterfaceToBundle(res statevar.StateVariableResult) *statevar.KeyValueBundle { 58 value := res.(*probabilityOfTrading) 59 return &statevar.KeyValueBundle{ 60 KVT: []statevar.KeyValueTol{ 61 {Key: "bidProbability", Val: &statevar.DecimalVector{Val: value.bidProbability}, Tolerance: tolerance}, 62 {Key: "askProbability", Val: &statevar.DecimalVector{Val: value.askProbability}, Tolerance: tolerance}, 63 }, 64 } 65 } 66 67 func (e *Engine) IsProbabilityOfTradingInitialised() bool { 68 return e.potInitialised 69 } 70 71 // startCalcPriceRanges kicks off the probability of trading calculation. 72 func (e *Engine) startCalcProbOfTrading(eventID string, endOfCalcCallback statevar.FinaliseCalculation) { 73 tauScaled := e.horizon.Mul(e.probabilityOfTradingTauScaling) 74 75 // get the best bid and ask 76 bestBid, bestAsk, err := e.getBestStaticPrices() 77 if err != nil { 78 e.log.Error("failed to get static price for probability of trading state var", logging.String("error", err.Error())) 79 endOfCalcCallback.CalculationFinished(eventID, nil, err) 80 return 81 } 82 83 // calculate offsets and probabilities for the range 84 bidProbabilities := calculateBidRange(bestBid, PriceIncrementFactor, tauScaled, e.rm.ProbabilityOfTrading) 85 askProbabilities := calculateAskRange(bestAsk, PriceIncrementFactor, tauScaled, e.rm.ProbabilityOfTrading) 86 87 res := &probabilityOfTrading{ 88 bidProbability: bidProbabilities, 89 askProbability: askProbabilities, 90 } 91 endOfCalcCallback.CalculationFinished(eventID, res, nil) 92 } 93 94 // calculateBidRange calculates the probabilities of price between bestBid and the worst bid 95 // in increments of incrementFactor of the best bid and records the offsets and probabilities 96 // such that offset 0 is stored in offsets[0] which corresponds to the best bid 97 // and similarly probabilities[0] corresponds to best bid trading probability 98 // whereas the last entry in offset equals to the maximum distance from bid for which 99 // probability is calculated and is greater than the default minimum acceptable probability of trading. 100 func calculateBidRange(bestBid num.Decimal, priceIncrementFactor, tauScaled num.Decimal, probabilityFunc func(num.Decimal, num.Decimal, num.Decimal, num.Decimal, num.Decimal, bool, bool) num.Decimal) []num.Decimal { 101 probabilities := []num.Decimal{} 102 103 p := bestBid 104 increment := priceIncrementFactor.Mul(bestBid) 105 for { 106 prob := probabilityFunc(bestBid, p, num.DecimalZero(), bestBid, tauScaled, true, true) 107 if prob.LessThanOrEqual(defaultMinimumProbabilityOfTrading) { 108 break 109 } 110 probabilities = append(probabilities, prob) 111 p = p.Sub(increment) 112 } 113 return probabilities 114 } 115 116 // calculateAskRange calculates the probabilities of price between bestAsk and the worst ask (maxAsk) 117 // in increments of incrementFactor of the best ask and records the offsets and probabilities 118 // such that offset 0 is stored in offsets[0] which corresponds to the best ask 119 // and similarly probabilities[0] corresponds to best ask trading probability 120 // whereas the last entry in offset equals to the maximum distance from ask for which 121 // probability is calculated, and the last entry in probabilities corresponds to probability of trading 122 // at the price implied by this offset from best ask. 123 func calculateAskRange(bestAsk num.Decimal, priceIncrementFactor, tauScaled num.Decimal, probabilityFunc func(num.Decimal, num.Decimal, num.Decimal, num.Decimal, num.Decimal, bool, bool) num.Decimal) []num.Decimal { 124 probabilities := []num.Decimal{} 125 126 maxAsk := bestAsk.Mul(maxAskMultiplier) 127 increment := priceIncrementFactor.Mul(bestAsk) 128 129 p := bestAsk 130 for { 131 prob := probabilityFunc(bestAsk, p, bestAsk, maxAsk, tauScaled, false, true) 132 if prob.LessThanOrEqual(defaultMinimumProbabilityOfTrading) { 133 break 134 } 135 probabilities = append(probabilities, prob) 136 p = p.Add(increment) 137 } 138 return probabilities 139 } 140 141 // updatePriceBounds is called back from the state variable consensus engine when consensus is reached for the down/up factors and updates the price bounds. 142 func (e *Engine) updateProbabilities(ctx context.Context, res statevar.StateVariableResult) error { 143 e.pot = res.(*probabilityOfTrading) 144 145 e.pot.bidOffset = make([]uint32, 0, len(e.pot.bidProbability)) 146 for i := range e.pot.bidProbability { 147 e.pot.bidOffset = append(e.pot.bidOffset, uint32(i)*OffsetIncrement) 148 } 149 150 e.pot.askOffset = make([]uint32, 0, len(e.pot.bidProbability)) 151 for i := range e.pot.askProbability { 152 e.pot.askOffset = append(e.pot.askOffset, uint32(i)*OffsetIncrement) 153 } 154 155 if e.log.GetLevel() <= logging.DebugLevel { 156 e.log.Debug("consensus reached for probability of trading", logging.String("market", e.marketID)) 157 } 158 159 e.potInitialised = true 160 return nil 161 } 162 163 // getProbabilityOfTrading returns the probability of trading for the given price 164 // if the price is a bid order that is better than the best bid or an ask order better than the best and ask it returns <defaultInRangeProbabilityOfTrading> 165 // if the price is a bid worse than min or an ask worse than max it returns minProbabilityOfTrading 166 // otherwise if we've not seen a consensus value and the price is within 10% the best (by side) 167 // it returns <defaultProbability> else if there is not yet consensus value it returns <minProbabilityOfTrading>. 168 // If there is consensus value and the price is worse than the worse price, we extrapolate using the last 2 price points 169 // If the price is within the range, the corresponding probability implied by the offset is returned, scaled, with lower bound of <minProbabilityOfTrading>. 170 func getProbabilityOfTrading(bestBid, bestAsk, minPrice, maxPrice num.Decimal, pot *probabilityOfTrading, price num.Decimal, isBid bool, minProbabilityOfTrading num.Decimal, offsetOne num.Decimal) num.Decimal { 171 if (isBid && price.GreaterThanOrEqual(bestBid)) || (!isBid && price.LessThanOrEqual(bestAsk)) { 172 return defaultInRangeProbabilityOfTrading 173 } 174 175 if isBid { 176 if price.LessThan(minPrice) { 177 return minProbabilityOfTrading 178 } 179 return getBidProbabilityOfTrading(bestBid, pot.bidOffset, pot.bidProbability, price, minProbabilityOfTrading, offsetOne) 180 } 181 if price.GreaterThan(maxPrice) { 182 return minProbabilityOfTrading 183 } 184 return getAskProbabilityOfTrading(bestAsk, pot.askOffset, pot.askProbability, price, minProbabilityOfTrading, offsetOne) 185 } 186 187 func getAskProbabilityOfTrading(bestAsk num.Decimal, offsets []uint32, probabilities []num.Decimal, price, minProbabilityOfTrading num.Decimal, offsetOne num.Decimal) num.Decimal { 188 // no consensus yet 189 if len(offsets) == 0 { 190 maxDistance := maxDistanceWhenNoConsensusFactor.Mul(bestAsk) 191 if bestAsk.Sub(price).Abs().LessThanOrEqual(maxDistance) { 192 return defaultInRangeProbabilityOfTrading 193 } 194 return minProbabilityOfTrading 195 } 196 offset := uint32(offsetOne.Mul(price.Sub(bestAsk).Div(bestAsk)).Floor().IntPart()) 197 198 // if outside the range - extrapolate 199 if offset > offsets[len(offsets)-1] { 200 return lexterp(offsets, probabilities, offset, minProbabilityOfTrading) 201 } 202 203 idx := sort.Search(len(offsets), func(i int) bool { 204 return offset < offsets[i] 205 }) 206 // linear interpolation 207 interpolatedProbability := linterp(offsets, probabilities, offset, idx, minProbabilityOfTrading) 208 return interpolatedProbability 209 } 210 211 func getBidProbabilityOfTrading(bestBid num.Decimal, offsets []uint32, probabilities []num.Decimal, price, minProbabilityOfTrading num.Decimal, offsetOne num.Decimal) num.Decimal { 212 // no consensus yet 213 if len(offsets) == 0 { 214 maxDistance := maxDistanceWhenNoConsensusFactor.Mul(bestBid) 215 if bestBid.Sub(price).Abs().LessThanOrEqual(maxDistance) { 216 return defaultInRangeProbabilityOfTrading 217 } 218 return minProbabilityOfTrading 219 } 220 221 offset := uint32(offsetOne.Mul(bestBid.Sub(price).Div(bestBid)).Floor().IntPart()) 222 223 // if outside the range - extrapolate 224 if offset > offsets[len(offsets)-1] { 225 return lexterp(offsets, probabilities, offset, minProbabilityOfTrading) 226 } 227 228 idx := sort.Search(len(offsets), func(i int) bool { 229 return offset < offsets[i] 230 }) 231 // linear interpolation 232 interpolatedProbability := linterp(offsets, probabilities, offset, idx, minProbabilityOfTrading) 233 return interpolatedProbability 234 } 235 236 func lexterp(offsets []uint32, probabilities []num.Decimal, offset uint32, minProbabilityOfTrading num.Decimal) num.Decimal { 237 if len(offsets) == 1 { 238 return minProbabilityOfTrading 239 } 240 last := offsets[len(offsets)-1] 241 last2 := offsets[len(offsets)-2] 242 probLast := probabilities[len(offsets)-1] 243 probLast2 := probabilities[len(offsets)-2] 244 slopeNum := num.DecimalFromInt64(int64(offset - last2)) 245 slopeDenom := num.DecimalFromInt64(int64(last - last2)) 246 slope := slopeNum.Div(slopeDenom) 247 prob := probLast2.Add(probLast.Sub(probLast2).Mul(slope)) 248 scaled := rescaleProbability(prob) 249 prob = num.MinD(num.DecimalFromInt64(1), num.MaxD(minProbabilityOfTrading, scaled)) 250 return prob 251 } 252 253 func linterp(offsets []uint32, probabilities []num.Decimal, priceOffset uint32, i int, minProbabilityOfTrading num.Decimal) num.Decimal { 254 if i >= len(probabilities) { 255 return num.MaxD(minProbabilityOfTrading, rescaleProbability(probabilities[len(probabilities)-1])) 256 } 257 prev := offsets[i-1] 258 size := offsets[i] - (prev) 259 ratio := num.DecimalFromInt64(int64(priceOffset - prev)).Div(num.DecimalFromInt64(int64(size))) 260 cRatio := num.DecimalFromInt64(1).Sub(ratio) 261 prob := ratio.Mul(probabilities[i]).Add(cRatio.Mul(probabilities[i-1])) 262 scaled := rescaleProbability(prob) 263 capped := num.MaxD(minProbabilityOfTrading, scaled) 264 return capped 265 } 266 267 // rescaleProbability rescales probability so that it's at most the value returned between bid and ask. 268 func rescaleProbability(prob num.Decimal) num.Decimal { 269 return prob.Mul(defaultInRangeProbabilityOfTrading) 270 }