code.vegaprotocol.io/vega@v0.79.0/core/monitor/price/pricemonitoring.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 price 17 18 import ( 19 "context" 20 "errors" 21 "log" 22 "sort" 23 "sync" 24 "time" 25 26 "code.vegaprotocol.io/vega/core/risk" 27 "code.vegaprotocol.io/vega/core/types" 28 "code.vegaprotocol.io/vega/core/types/statevar" 29 "code.vegaprotocol.io/vega/libs/num" 30 "code.vegaprotocol.io/vega/logging" 31 ) 32 33 var ( 34 // ErrNilRangeProvider signals that nil was supplied in place of RangeProvider. 35 ErrNilRangeProvider = errors.New("nil RangeProvider") 36 // ErrTimeSequence signals that time sequence is not in a non-decreasing order. 37 ErrTimeSequence = errors.New("received a time that's before the last received time") 38 // ErrExpiresAtNotSet indicates price monitoring auction is endless somehow. 39 ErrExpiresAtNotSet = errors.New("price monitoring auction with no end time") 40 // ErrNilPriceMonitoringSettings signals that nil was supplied in place of PriceMonitoringSettings. 41 ErrNilPriceMonitoringSettings = errors.New("nil PriceMonitoringSettings") 42 ) 43 44 // can't make this one constant... 45 var ( 46 secondsPerYear = num.DecimalFromFloat(365.25 * 24 * 60 * 60) 47 tolerance, _ = num.DecimalFromString("1e-6") 48 ) 49 50 //go:generate go run github.com/golang/mock/mockgen -destination mocks/auction_state_mock.go -package mocks code.vegaprotocol.io/vega/core/monitor/price AuctionState 51 //nolint:interfacebloat 52 type AuctionState interface { 53 // What is the current trading mode of the market, is it in auction 54 Mode() types.MarketTradingMode 55 InAuction() bool 56 // What type of auction are we dealing with 57 IsOpeningAuction() bool 58 IsPriceAuction() bool 59 IsPriceExtension() bool 60 IsFBA() bool 61 // is it the start/end of the auction 62 CanLeave() bool 63 AuctionStart() bool 64 // start a price-related auction, extend a current auction, or end it 65 StartPriceAuction(t time.Time, d *types.AuctionDuration) 66 ExtendAuctionPrice(delta types.AuctionDuration) 67 SetReadyToLeave() 68 // get parameters for current auction 69 Start() time.Time 70 Duration() types.AuctionDuration // currently not used - might be useful when extending an auction 71 ExpiresAt() *time.Time 72 } 73 74 // bound holds the limits for the valid price movement. 75 type bound struct { 76 Active bool 77 UpFactor num.Decimal 78 DownFactor num.Decimal 79 Trigger *types.PriceMonitoringTrigger 80 } 81 82 type boundFactors struct { 83 up []num.Decimal 84 down []num.Decimal 85 } 86 87 var ( 88 defaultDownFactor = num.MustDecimalFromString("0.9") 89 defaultUpFactor = num.MustDecimalFromString("1.1") 90 ) 91 92 type priceRange struct { 93 MinPrice num.WrappedDecimal 94 MaxPrice num.WrappedDecimal 95 ReferencePrice num.Decimal 96 } 97 98 type pastPrice struct { 99 Time time.Time 100 AveragePrice num.Decimal 101 } 102 103 // RangeProvider provides the minimum and maximum future price corresponding to the current price level, horizon expressed as year fraction (e.g. 0.5 for 6 months) and probability level (e.g. 0.95 for 95%). 104 // 105 //go:generate go run github.com/golang/mock/mockgen -destination mocks/price_range_provider_mock.go -package mocks code.vegaprotocol.io/vega/core/monitor/price RangeProvider 106 type RangeProvider interface { 107 PriceRange(price, yearFraction, probability num.Decimal) (num.Decimal, num.Decimal) 108 } 109 110 //go:generate go run github.com/golang/mock/mockgen -destination mocks/state_var_mock.go -package mocks code.vegaprotocol.io/vega/core/monitor/price StateVarEngine 111 type StateVarEngine interface { 112 RegisterStateVariable(asset, market, name string, converter statevar.Converter, startCalculation func(string, statevar.FinaliseCalculation), trigger []statevar.EventType, result func(context.Context, statevar.StateVariableResult) error) error 113 } 114 115 // Engine allows tracking price changes and verifying them against the theoretical levels implied by the RangeProvider (risk model). 116 type Engine struct { 117 log *logging.Logger 118 riskModel RangeProvider 119 auctionState AuctionState 120 minDuration time.Duration 121 122 initialised bool 123 fpHorizons map[int64]num.Decimal 124 now time.Time 125 update time.Time 126 pricesNow []*num.Uint 127 pricesPast []pastPrice 128 bounds []*bound 129 130 priceRangeCacheTime time.Time 131 priceRangesCache map[int]priceRange 132 133 refPriceCacheTime time.Time 134 refPriceCache map[int64]num.Decimal 135 refPriceLock sync.RWMutex 136 137 boundFactorsInitialised bool 138 139 stateChanged bool 140 stateVarEngine StateVarEngine 141 market string 142 asset string 143 } 144 145 func (e *Engine) UpdateSettings(riskModel risk.Model, settings *types.PriceMonitoringSettings, as AuctionState) { 146 e.riskModel = riskModel 147 e.fpHorizons, e.bounds = computeBoundsAndHorizons(settings, as) 148 e.initialised = false 149 e.boundFactorsInitialised = false 150 e.priceRangesCache = make(map[int]priceRange, len(e.bounds)) // clear the cache 151 // reset reference cache 152 e.refPriceCacheTime = time.Time{} 153 e.refPriceCache = map[int64]num.Decimal{} 154 _ = e.getCurrentPriceRanges(true) // force bound recalc 155 } 156 157 // Initialised returns true if the engine already saw at least one price. 158 func (e *Engine) Initialised() bool { 159 return e.initialised 160 } 161 162 // NewMonitor returns a new instance of PriceMonitoring. 163 func NewMonitor(asset, mktID string, riskModel RangeProvider, auctionState AuctionState, settings *types.PriceMonitoringSettings, stateVarEngine StateVarEngine, log *logging.Logger) (*Engine, error) { 164 if riskModel == nil { 165 return nil, ErrNilRangeProvider 166 } 167 if settings == nil { 168 return nil, ErrNilPriceMonitoringSettings 169 } 170 171 // Other functions depend on this sorting 172 horizons, bounds := computeBoundsAndHorizons(settings, auctionState) 173 174 e := &Engine{ 175 riskModel: riskModel, 176 auctionState: auctionState, 177 fpHorizons: horizons, 178 bounds: bounds, 179 stateChanged: true, 180 stateVarEngine: stateVarEngine, 181 boundFactorsInitialised: false, 182 log: log, 183 market: mktID, 184 asset: asset, 185 } 186 187 stateVarEngine.RegisterStateVariable(asset, mktID, "bound-factors", boundFactorsConverter{}, e.startCalcPriceRanges, []statevar.EventType{statevar.EventTypeTimeTrigger, statevar.EventTypeAuctionEnded, statevar.EventTypeOpeningAuctionFirstUncrossingPrice}, e.updatePriceBounds) 188 return e, nil 189 } 190 191 func (e *Engine) SetMinDuration(d time.Duration) { 192 e.minDuration = d 193 e.stateChanged = true 194 } 195 196 // GetHorizonYearFractions returns horizons of all the triggers specified, expressed as year fraction, sorted in ascending order. 197 func (e *Engine) GetHorizonYearFractions() []num.Decimal { 198 h := make([]num.Decimal, 0, len(e.bounds)) 199 for _, v := range e.fpHorizons { 200 h = append(h, v) 201 } 202 203 sort.Slice(h, func(i, j int) bool { return h[i].LessThan(h[j]) }) 204 return h 205 } 206 207 // GetValidPriceRange returns the range of prices that won't trigger the price monitoring auction. 208 func (e *Engine) GetValidPriceRange() (num.WrappedDecimal, num.WrappedDecimal) { 209 min := num.NewWrappedDecimal(num.UintZero(), num.DecimalZero()) 210 m := num.MaxUint() 211 max := num.NewWrappedDecimal(m, m.ToDecimal()) 212 for _, pr := range e.getCurrentPriceRanges(false) { 213 if pr.MinPrice.Representation().GT(min.Representation()) { 214 min = pr.MinPrice 215 } 216 if !pr.MaxPrice.Representation().IsZero() && pr.MaxPrice.Representation().LT(max.Representation()) { 217 max = pr.MaxPrice 218 } 219 } 220 if min.Original().LessThan(num.DecimalZero()) { 221 min = num.NewWrappedDecimal(num.UintZero(), num.DecimalZero()) 222 } 223 return min, max 224 } 225 226 // GetCurrentBounds returns a list of valid price ranges per price monitoring trigger. Note these are subject to change as the time progresses. 227 func (e *Engine) GetCurrentBounds() []*types.PriceMonitoringBounds { 228 priceRanges := e.getCurrentPriceRanges(false) 229 ret := make([]*types.PriceMonitoringBounds, 0, len(priceRanges)) 230 for ind, pr := range priceRanges { 231 b := e.bounds[ind] 232 if b.Active { 233 ret = append(ret, 234 &types.PriceMonitoringBounds{ 235 MinValidPrice: pr.MinPrice.Representation(), 236 MaxValidPrice: pr.MaxPrice.Representation(), 237 Trigger: b.Trigger, 238 ReferencePrice: pr.ReferencePrice, 239 }) 240 } 241 } 242 243 sort.SliceStable(ret, 244 func(i, j int) bool { 245 if ret[i].Trigger.Horizon == ret[j].Trigger.Horizon { 246 return ret[i].Trigger.Probability.LessThan(ret[j].Trigger.Probability) 247 } 248 return ret[i].Trigger.Horizon < ret[j].Trigger.Horizon 249 }) 250 251 return ret 252 } 253 254 // GetBounds returns a list of valid price ranges per price monitoring trigger. Note these are subject to change as the time progresses. 255 func (e *Engine) GetBounds() []*types.PriceMonitoringBounds { 256 priceRanges := e.getCurrentPriceRanges(false) 257 ret := make([]*types.PriceMonitoringBounds, 0, len(priceRanges)) 258 for ind, pr := range priceRanges { 259 ret = append(ret, 260 &types.PriceMonitoringBounds{ 261 MinValidPrice: pr.MinPrice.Representation(), 262 MaxValidPrice: pr.MaxPrice.Representation(), 263 Trigger: e.bounds[ind].Trigger, 264 ReferencePrice: pr.ReferencePrice, 265 Active: e.bounds[ind].Active, 266 }) 267 } 268 269 sort.SliceStable(ret, 270 func(i, j int) bool { 271 if ret[i].Trigger.Horizon == ret[j].Trigger.Horizon { 272 return ret[i].Trigger.Probability.LessThan(ret[j].Trigger.Probability) 273 } 274 return ret[i].Trigger.Horizon < ret[j].Trigger.Horizon 275 }) 276 277 return ret 278 } 279 280 func (e *Engine) OnTimeUpdate(now time.Time) { 281 e.recordTimeChange(now) 282 } 283 284 // CheckPrice checks how current price, volume and time should impact the auction state and modifies it accordingly: start auction, end auction, extend ongoing auction, 285 // "true" gets returned if non-persistent order should be rejected. 286 func (e *Engine) CheckPrice(ctx context.Context, as AuctionState, price *num.Uint, persistent bool, recordPriceHistory bool) bool { 287 // market is not in auction, or in batch auction 288 if fba := as.IsFBA(); !as.InAuction() || fba { 289 bounds := e.checkBounds(price) 290 // no bounds violations - update price, and we're done (unless we initialised as part of this call, then price has alrady been updated) 291 if len(bounds) == 0 { 292 if recordPriceHistory { 293 e.recordPriceChange(price) 294 } 295 return false 296 } 297 if !persistent { 298 // we're going to stay in continuous trading, make sure we still have bounds 299 e.reactivateBounds() 300 return true 301 } 302 duration := types.AuctionDuration{} 303 for _, b := range bounds { 304 duration.Duration += b.AuctionExtension 305 } 306 // we're dealing with a batch auction that's about to end -> extend it? 307 if fba && as.CanLeave() { 308 // bounds were violated, based on the values in the bounds slice, we can calculate how long the auction should last 309 as.ExtendAuctionPrice(duration) 310 return false 311 } 312 if min := int64(e.minDuration / time.Second); duration.Duration < min { 313 duration.Duration = min 314 } 315 316 as.StartPriceAuction(e.now, &duration) 317 return false 318 } 319 320 bounds := e.checkBounds(price) 321 if len(bounds) == 0 { 322 end := as.ExpiresAt() 323 if !e.now.After(*end) { 324 return false 325 } 326 // auction can be terminated 327 as.SetReadyToLeave() 328 if recordPriceHistory { 329 e.ResetPriceHistory(price) 330 } else { 331 e.ResetPriceHistory(nil) 332 } 333 return false 334 } 335 336 var duration int64 337 for _, b := range bounds { 338 duration += b.AuctionExtension 339 } 340 341 // extend the current auction 342 as.ExtendAuctionPrice(types.AuctionDuration{ 343 Duration: duration, 344 }) 345 346 return false 347 } 348 349 // ResetPriceHistory deletes existing price history and starts it afresh with the supplied value. 350 func (e *Engine) ResetPriceHistory(price *num.Uint) { 351 e.update = e.now 352 if price != nil && !price.IsZero() { 353 e.pricesNow = []*num.Uint{price.Clone()} 354 e.pricesPast = []pastPrice{} 355 } else { 356 // If there's a price history than use the most recent 357 if len(e.pricesPast) > 0 { 358 e.pricesPast = e.pricesPast[len(e.pricesPast)-1:] 359 } else { // Otherwise can't initialise 360 e.initialised = false 361 e.stateChanged = true 362 return 363 } 364 } 365 e.priceRangeCacheTime = time.Time{} 366 e.refPriceCacheTime = time.Time{} 367 // we're not reseetting the down/up factors - they will be updated as triggered by auction end/time 368 e.reactivateBounds() 369 e.stateChanged = true 370 e.initialised = true 371 } 372 373 // reactivateBounds reactivates all bounds. 374 func (e *Engine) reactivateBounds() { 375 for _, b := range e.bounds { 376 if !b.Active { 377 e.stateChanged = true 378 } 379 b.Active = true 380 } 381 e.priceRangeCacheTime = time.Time{} 382 } 383 384 // recordPriceChange informs price monitoring module of a price change within the same instance as specified by the last call to UpdateTime. 385 func (e *Engine) recordPriceChange(price *num.Uint) { 386 if price != nil && !price.IsZero() { 387 e.pricesNow = append(e.pricesNow, price.Clone()) 388 e.stateChanged = true 389 } 390 } 391 392 // recordTimeChange updates the current time and moves prices from current prices to past prices by calculating their corresponding vwp. 393 func (e *Engine) recordTimeChange(now time.Time) { 394 if now.Before(e.now) { 395 log.Panic("invalid state enecountered in price monitoring engine", 396 logging.Error(ErrTimeSequence)) 397 } 398 if now.Equal(e.now) { 399 return 400 } 401 402 if len(e.pricesNow) > 0 { 403 priceSum, numObs := num.UintZero(), num.UintZero() 404 for _, p := range e.pricesNow { 405 numObs.AddSum(num.UintOne()) 406 priceSum.AddSum(p) 407 } 408 e.pricesPast = append(e.pricesPast, 409 pastPrice{ 410 Time: e.now, 411 AveragePrice: priceSum.ToDecimal().Div(numObs.ToDecimal()), 412 }) 413 } 414 e.pricesNow = e.pricesNow[:0] 415 e.now = now 416 e.clearStalePrices() 417 e.stateChanged = true 418 } 419 420 // checkBounds checks if the price is within price range for each of the bound and return trigger for each bound that it's not. 421 func (e *Engine) checkBounds(price *num.Uint) []*types.PriceMonitoringTrigger { 422 ret := []*types.PriceMonitoringTrigger{} // returned price projections, empty if all good 423 if price == nil || price.IsZero() { 424 return ret 425 } 426 priceRanges := e.getCurrentPriceRanges(false) 427 if len(priceRanges) == 0 { 428 return ret 429 } 430 for i, b := range e.bounds { 431 if !b.Active { 432 continue 433 } 434 priceRange := priceRanges[i] 435 if price.LT(priceRange.MinPrice.Representation()) || price.GT(priceRange.MaxPrice.Representation()) { 436 ret = append(ret, b.Trigger) 437 // deactivate the bound that just got violated so it doesn't prevent auction from terminating 438 b.Active = false 439 // only allow breaking one bound at a time 440 return ret 441 } 442 } 443 return ret 444 } 445 446 // getCurrentPriceRanges calculates price ranges from current reference prices and bound down/up factors. 447 func (e *Engine) getCurrentPriceRanges(force bool) map[int]priceRange { 448 if !force && e.priceRangeCacheTime == e.now && len(e.priceRangesCache) > 0 { 449 return e.priceRangesCache 450 } 451 ranges := make(map[int]priceRange, len(e.priceRangesCache)) 452 if e.noHistory() { 453 return ranges 454 } 455 for i, b := range e.bounds { 456 if !b.Active { 457 continue 458 } 459 ref := e.getRefPrice(b.Trigger.Horizon, force) 460 var min, max num.Decimal 461 462 if e.boundFactorsInitialised { 463 min = ref.Mul(b.DownFactor) 464 max = ref.Mul(b.UpFactor) 465 } else { 466 min = ref.Mul(defaultDownFactor) 467 max = ref.Mul(defaultUpFactor) 468 } 469 470 ranges[i] = priceRange{ 471 MinPrice: wrapPriceRange(min, true), 472 MaxPrice: wrapPriceRange(max, false), 473 ReferencePrice: ref, 474 } 475 } 476 e.priceRangesCache = ranges 477 e.priceRangeCacheTime = e.now 478 e.stateChanged = true 479 return e.priceRangesCache 480 } 481 482 // clearStalePrices updates the pricesPast slice to hold only as many prices as implied by the horizon. 483 func (e *Engine) clearStalePrices() { 484 if e.now.Before(e.update) || len(e.bounds) == 0 || len(e.pricesPast) == 0 { 485 return 486 } 487 488 // Remove redundant average prices 489 minRequiredHorizon := e.now 490 if len(e.bounds) > 0 { 491 maxTau := e.bounds[len(e.bounds)-1].Trigger.Horizon 492 minRequiredHorizon = e.now.Add(time.Duration(-maxTau) * time.Second) 493 } 494 495 // Make sure at least one entry is left hence the "len(..) - 1" 496 for i := 0; i < len(e.pricesPast)-1; i++ { 497 if !e.pricesPast[i].Time.Before(minRequiredHorizon) { 498 e.pricesPast = e.pricesPast[i:] 499 return 500 } 501 } 502 e.pricesPast = e.pricesPast[len(e.pricesPast)-1:] 503 } 504 505 // getRefPrice caches and returns the ref price for a given horizon. The cache is invalidated when block changes. 506 func (e *Engine) getRefPrice(horizon int64, force bool) num.Decimal { 507 e.refPriceLock.Lock() 508 defer e.refPriceLock.Unlock() 509 if e.refPriceCache == nil || e.refPriceCacheTime != e.now || force { 510 e.refPriceCache = make(map[int64]num.Decimal, len(e.refPriceCache)) 511 e.stateChanged = true 512 e.refPriceCacheTime = e.now 513 } 514 515 if _, ok := e.refPriceCache[horizon]; !ok { 516 e.refPriceCache[horizon] = e.calculateRefPrice(horizon) 517 e.stateChanged = true 518 } 519 return e.refPriceCache[horizon] 520 } 521 522 func (e *Engine) getRefPriceNoUpdate(horizon int64) num.Decimal { 523 e.refPriceLock.RLock() 524 defer e.refPriceLock.RUnlock() 525 if e.refPriceCacheTime == e.now { 526 if _, ok := e.refPriceCache[horizon]; !ok { 527 return e.calculateRefPrice(horizon) 528 } 529 return e.refPriceCache[horizon] 530 } 531 return e.calculateRefPrice(horizon) 532 } 533 534 // calculateRefPrice returns theh last VolumeWeightedPrice with time preceding currentTime - horizon seconds. If there's only one price it returns the Price. 535 func (e *Engine) calculateRefPrice(horizon int64) num.Decimal { 536 t := e.now.Add(time.Duration(-horizon) * time.Second) 537 if len(e.pricesPast) < 1 { 538 return e.pricesNow[0].ToDecimal() 539 } 540 ref := e.pricesPast[0].AveragePrice 541 for _, p := range e.pricesPast { 542 if p.Time.After(t) { 543 break 544 } 545 ref = p.AveragePrice 546 } 547 return ref 548 } 549 550 func (e *Engine) noHistory() bool { 551 return len(e.pricesPast) == 0 && len(e.pricesNow) == 0 552 } 553 554 func computeBoundsAndHorizons(settings *types.PriceMonitoringSettings, as AuctionState) (map[int64]num.Decimal, []*bound) { 555 // set bounds to inactive if we're in price monitoring auction 556 active := !as.IsPriceAuction() 557 parameters := make([]*types.PriceMonitoringTrigger, 0, len(settings.Parameters.Triggers)) 558 for _, p := range settings.Parameters.Triggers { 559 p := *p 560 parameters = append(parameters, &p) 561 } 562 sort.Slice(parameters, 563 func(i, j int) bool { 564 return parameters[i].Horizon < parameters[j].Horizon && 565 parameters[i].Probability.GreaterThanOrEqual(parameters[j].Probability) 566 }) 567 568 horizons := map[int64]num.Decimal{} 569 bounds := make([]*bound, 0, len(parameters)) 570 for _, p := range parameters { 571 bounds = append(bounds, &bound{ 572 Active: active, 573 Trigger: p, 574 }) 575 if _, ok := horizons[p.Horizon]; !ok { 576 horizons[p.Horizon] = p.HorizonDec.Div(secondsPerYear) 577 } 578 } 579 return horizons, bounds 580 } 581 582 func wrapPriceRange(b num.Decimal, isMin bool) num.WrappedDecimal { 583 var r *num.Uint 584 if isMin { 585 r, _ = num.UintFromDecimal(b.Ceil()) 586 } else { 587 r, _ = num.UintFromDecimal(b.Floor()) 588 } 589 return num.NewWrappedDecimal(r, b) 590 }