code.vegaprotocol.io/vega@v0.79.0/core/matching/indicative_price_and_volume.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 matching 17 18 import ( 19 "slices" 20 "sort" 21 22 "code.vegaprotocol.io/vega/core/types" 23 "code.vegaprotocol.io/vega/libs/num" 24 "code.vegaprotocol.io/vega/logging" 25 26 "golang.org/x/exp/maps" 27 ) 28 29 type IndicativePriceAndVolume struct { 30 log *logging.Logger 31 levels []ipvPriceLevel 32 33 // this is just used to avoid allocations 34 buf []CumulativeVolumeLevel 35 36 // keep track of previouses {min/max}Price 37 // and if orders has been add in the book 38 // with a price in the range 39 lastMinPrice, lastMaxPrice *num.Uint 40 lastMaxTradable uint64 41 lastCumulativeVolumes []CumulativeVolumeLevel 42 needsUpdate bool 43 44 // keep track of expanded off book orders 45 offbook OffbookSource 46 generated map[string]*ipvGeneratedOffbook 47 } 48 49 type ipvPriceLevel struct { 50 price *num.Uint 51 buypl ipvVolume 52 sellpl ipvVolume 53 } 54 55 type ipvVolume struct { 56 volume uint64 57 offbookVolume uint64 // how much of the above total volume is coming from AMMs 58 } 59 60 type ipvGeneratedOffbook struct { 61 buy []*types.Order 62 sell []*types.Order 63 approx bool 64 } 65 66 func (g *ipvGeneratedOffbook) add(order *types.Order) { 67 if order.Side == types.SideSell { 68 g.sell = append(g.sell, order) 69 return 70 } 71 g.buy = append(g.buy, order) 72 } 73 74 func NewIndicativePriceAndVolume(log *logging.Logger, buy, sell *OrderBookSide) *IndicativePriceAndVolume { 75 bestBid, _, err := buy.BestPriceAndVolume() 76 if err != nil { 77 bestBid = num.UintZero() 78 } 79 bestAsk, _, err := sell.BestPriceAndVolume() 80 if err != nil { 81 bestAsk = num.UintZero() 82 } 83 84 if buy.offbook != nil { 85 bid, _, ask, _ := buy.offbook.BestPricesAndVolumes() 86 if bid != nil { 87 if bestBid.IsZero() { 88 bestBid = bid 89 } else { 90 bestBid = num.Max(bestBid, bid) 91 } 92 } 93 if ask != nil { 94 if bestAsk.IsZero() { 95 bestAsk = ask 96 } else { 97 bestAsk = num.Min(bestAsk, ask) 98 } 99 } 100 } 101 102 ipv := IndicativePriceAndVolume{ 103 levels: []ipvPriceLevel{}, 104 log: log, 105 lastMinPrice: num.UintZero(), 106 lastMaxPrice: num.UintZero(), 107 needsUpdate: true, 108 offbook: buy.offbook, 109 generated: map[string]*ipvGeneratedOffbook{}, 110 } 111 112 // if they are crossed set the last min/max values otherwise leave as zero 113 if bestAsk.LTE(bestBid) { 114 ipv.lastMinPrice = bestAsk 115 ipv.lastMaxPrice = bestBid 116 } 117 118 ipv.buildInitialCumulativeLevels(buy, sell) 119 // initialize at the size of all levels at start, we most likely 120 // not gonna need any other allocation if we start an auction 121 // on an existing market 122 ipv.buf = make([]CumulativeVolumeLevel, len(ipv.levels)) 123 return &ipv 124 } 125 126 func (ipv *IndicativePriceAndVolume) buildInitialOffbookShape(offbook OffbookSource, mplm map[num.Uint]ipvPriceLevel) { 127 min, max := ipv.lastMinPrice, ipv.lastMaxPrice 128 if min.IsZero() || max.IsZero() || min.GT(max) { 129 // region is not crossed so we won't expand just yet 130 return 131 } 132 133 // expand all AMM's into orders within the crossed region and add them to the price-level cache 134 r := offbook.OrderbookShape(min, max, nil) 135 136 for _, shape := range r { 137 buys := shape.Buys 138 sells := shape.Sells 139 140 for i := len(buys) - 1; i >= 0; i-- { 141 o := buys[i] 142 mpl, ok := mplm[*o.Price] 143 if !ok { 144 mpl = ipvPriceLevel{price: o.Price, buypl: ipvVolume{0, 0}, sellpl: ipvVolume{0, 0}} 145 } 146 // increment the volume at this level 147 mpl.buypl.volume += o.Size 148 mpl.buypl.offbookVolume += o.Size 149 mplm[*o.Price] = mpl 150 151 if ipv.generated[o.Party] == nil { 152 ipv.generated[o.Party] = &ipvGeneratedOffbook{approx: shape.Approx} 153 } 154 ipv.generated[o.Party].add(o) 155 } 156 157 for _, o := range sells { 158 mpl, ok := mplm[*o.Price] 159 if !ok { 160 mpl = ipvPriceLevel{price: o.Price, buypl: ipvVolume{0, 0}, sellpl: ipvVolume{0, 0}} 161 } 162 163 mpl.sellpl.volume += o.Size 164 mpl.sellpl.offbookVolume += o.Size 165 mplm[*o.Price] = mpl 166 167 if ipv.generated[o.Party] == nil { 168 ipv.generated[o.Party] = &ipvGeneratedOffbook{approx: shape.Approx} 169 } 170 ipv.generated[o.Party].add(o) 171 } 172 } 173 } 174 175 func (ipv *IndicativePriceAndVolume) removeOffbookShape(party string) { 176 orders, ok := ipv.generated[party] 177 if !ok { 178 return 179 } 180 181 // remove all the old volume for the AMM's 182 for _, o := range orders.buy { 183 ipv.RemoveVolumeAtPrice(o.Price, o.Size, o.Side, true) 184 } 185 for _, o := range orders.sell { 186 ipv.RemoveVolumeAtPrice(o.Price, o.Size, o.Side, true) 187 } 188 189 // clear it out the saved generated orders for the offbook shape 190 delete(ipv.generated, party) 191 } 192 193 func (ipv *IndicativePriceAndVolume) addOffbookShape(party *string, minPrice, maxPrice *num.Uint, excludeMin, excludeMax bool) { 194 // recalculate new orders for the shape and add the volume in 195 r := ipv.offbook.OrderbookShape(minPrice, maxPrice, party) 196 197 for _, shape := range r { 198 buys := shape.Buys 199 sells := shape.Sells 200 201 if len(buys) == 0 && len(sells) == 0 { 202 continue 203 } 204 205 if _, ok := ipv.generated[shape.AmmParty]; !ok { 206 ipv.generated[shape.AmmParty] = &ipvGeneratedOffbook{approx: shape.Approx} 207 } 208 209 // add buys backwards so that the best-bid is first 210 for i := len(buys) - 1; i >= 0; i-- { 211 o := buys[i] 212 213 if excludeMin && o.Price.EQ(minPrice) { 214 continue 215 } 216 if excludeMax && o.Price.EQ(maxPrice) { 217 continue 218 } 219 220 ipv.AddVolumeAtPrice(o.Price, o.Size, o.Side, true) 221 ipv.generated[shape.AmmParty].add(o) 222 } 223 224 // add buys fowards so that the best-ask is first 225 for _, o := range sells { 226 if excludeMin && o.Price.EQ(minPrice) { 227 continue 228 } 229 if excludeMax && o.Price.EQ(maxPrice) { 230 continue 231 } 232 233 ipv.AddVolumeAtPrice(o.Price, o.Size, o.Side, true) 234 ipv.generated[shape.AmmParty].add(o) 235 } 236 } 237 } 238 239 func (ipv *IndicativePriceAndVolume) updateOffbookState(minPrice, maxPrice *num.Uint) { 240 parties := maps.Keys(ipv.generated) 241 for _, p := range parties { 242 ipv.removeOffbookShape(p) 243 } 244 245 if minPrice.GT(maxPrice) { 246 // region is not crossed so we won't expand just yet 247 return 248 } 249 250 ipv.addOffbookShape(nil, minPrice, maxPrice, false, false) 251 } 252 253 // this will be used to build the initial set of price levels, when the auction is being started. 254 func (ipv *IndicativePriceAndVolume) buildInitialCumulativeLevels(buy, sell *OrderBookSide) { 255 // we'll keep track of all the pl we encounter 256 mplm := map[num.Uint]ipvPriceLevel{} 257 258 for i := len(buy.levels) - 1; i >= 0; i-- { 259 mplm[*buy.levels[i].price] = ipvPriceLevel{price: buy.levels[i].price.Clone(), buypl: ipvVolume{buy.levels[i].volume, 0}, sellpl: ipvVolume{0, 0}} 260 } 261 262 // now we add all the sells 263 // to our list of pricelevel 264 // making sure we have no duplicates 265 for i := len(sell.levels) - 1; i >= 0; i-- { 266 price := sell.levels[i].price.Clone() 267 if mpl, ok := mplm[*price]; ok { 268 mpl.sellpl = ipvVolume{sell.levels[i].volume, 0} 269 mplm[*price] = mpl 270 } else { 271 mplm[*price] = ipvPriceLevel{price: price, sellpl: ipvVolume{sell.levels[i].volume, 0}, buypl: ipvVolume{0, 0}} 272 } 273 } 274 275 if buy.offbook != nil { 276 ipv.buildInitialOffbookShape(buy.offbook, mplm) 277 } 278 279 // now we insert them all in the slice. 280 // so we can sort them 281 ipv.levels = make([]ipvPriceLevel, 0, len(mplm)) 282 for _, v := range mplm { 283 ipv.levels = append(ipv.levels, v) 284 } 285 286 // sort the slice so we can go through each levels nicely 287 sort.Slice(ipv.levels, func(i, j int) bool { return ipv.levels[i].price.GT(ipv.levels[j].price) }) 288 } 289 290 func (ipv *IndicativePriceAndVolume) incrementLevelVolume(idx int, volume uint64, side types.Side, isOffbook bool) { 291 switch side { 292 case types.SideBuy: 293 ipv.levels[idx].buypl.volume += volume 294 if isOffbook { 295 ipv.levels[idx].buypl.offbookVolume += volume 296 } 297 case types.SideSell: 298 ipv.levels[idx].sellpl.volume += volume 299 if isOffbook { 300 ipv.levels[idx].sellpl.offbookVolume += volume 301 } 302 } 303 } 304 305 func (ipv *IndicativePriceAndVolume) AddVolumeAtPrice(price *num.Uint, volume uint64, side types.Side, isOffbook bool) { 306 if price.GTE(ipv.lastMinPrice) || price.LTE(ipv.lastMaxPrice) { 307 // the new price added is in the range, that will require 308 // to recompute the whole range when we call GetCumulativePriceLevels 309 // again 310 ipv.needsUpdate = true 311 } 312 i := sort.Search(len(ipv.levels), func(i int) bool { 313 return ipv.levels[i].price.LTE(price) 314 }) 315 if i < len(ipv.levels) && ipv.levels[i].price.EQ(price) { 316 // we found the price level, let's add the volume there, and we are done 317 ipv.incrementLevelVolume(i, volume, side, isOffbook) 318 } else { 319 ipv.levels = append(ipv.levels, ipvPriceLevel{}) 320 copy(ipv.levels[i+1:], ipv.levels[i:]) 321 ipv.levels[i] = ipvPriceLevel{price: price.Clone()} 322 ipv.incrementLevelVolume(i, volume, side, isOffbook) 323 } 324 } 325 326 func (ipv *IndicativePriceAndVolume) decrementLevelVolume(idx int, volume uint64, side types.Side, isOffbook bool) { 327 switch side { 328 case types.SideBuy: 329 ipv.levels[idx].buypl.volume -= volume 330 if isOffbook { 331 ipv.levels[idx].buypl.offbookVolume -= volume 332 } 333 case types.SideSell: 334 ipv.levels[idx].sellpl.volume -= volume 335 if isOffbook { 336 ipv.levels[idx].sellpl.offbookVolume -= volume 337 } 338 } 339 } 340 341 func (ipv *IndicativePriceAndVolume) RemoveVolumeAtPrice(price *num.Uint, volume uint64, side types.Side, isOffbook bool) { 342 if price.GTE(ipv.lastMinPrice) || price.LTE(ipv.lastMaxPrice) { 343 // the new price added is in the range, that will require 344 // to recompute the whole range when we call GetCumulativePriceLevels 345 // again 346 ipv.needsUpdate = true 347 } 348 i := sort.Search(len(ipv.levels), func(i int) bool { 349 return ipv.levels[i].price.LTE(price) 350 }) 351 if i < len(ipv.levels) && ipv.levels[i].price.EQ(price) { 352 // we found the price level, let's add the volume there, and we are done 353 ipv.decrementLevelVolume(i, volume, side, isOffbook) 354 } else { 355 ipv.log.Panic("cannot remove volume from a non-existing level", 356 logging.String("side", side.String()), 357 logging.BigUint("price", price), 358 logging.Uint64("volume", volume)) 359 } 360 } 361 362 func (ipv *IndicativePriceAndVolume) getLevelsWithinRange(maxPrice, minPrice *num.Uint) []ipvPriceLevel { 363 // these are ordered descending, se we gonna find first the maxPrice then 364 // the minPrice, and using that we can then subslice like a boss 365 maxPricePos := sort.Search(len(ipv.levels), func(i int) bool { 366 return ipv.levels[i].price.LTE(maxPrice) 367 }) 368 if maxPricePos >= len(ipv.levels) || ipv.levels[maxPricePos].price.NEQ(maxPrice) { 369 // price level not present, that should not be possible? 370 ipv.log.Panic("missing max price in levels", 371 logging.BigUint("max-price", maxPrice)) 372 } 373 minPricePos := sort.Search(len(ipv.levels), func(i int) bool { 374 return ipv.levels[i].price.LTE(minPrice) 375 }) 376 if minPricePos >= len(ipv.levels) || ipv.levels[minPricePos].price.NEQ(minPrice) { 377 // price level not present, that should not be possible? 378 ipv.log.Panic("missing min price in levels", 379 logging.BigUint("min-price", minPrice)) 380 } 381 382 return ipv.levels[maxPricePos : minPricePos+1] 383 } 384 385 func (ipv *IndicativePriceAndVolume) GetCrossedRegion() (*num.Uint, *num.Uint) { 386 min := ipv.lastMinPrice 387 if min != nil { 388 min = min.Clone() 389 } 390 391 max := ipv.lastMaxPrice 392 if max != nil { 393 max = max.Clone() 394 } 395 return min, max 396 } 397 398 func (ipv *IndicativePriceAndVolume) GetCumulativePriceLevels(maxPrice, minPrice *num.Uint) ([]CumulativeVolumeLevel, uint64) { 399 var crossedRegionChanged bool 400 if maxPrice.NEQ(ipv.lastMaxPrice) { 401 maxPrice = maxPrice.Clone() 402 crossedRegionChanged = true 403 } 404 if minPrice.NEQ(ipv.lastMinPrice) { 405 minPrice = minPrice.Clone() 406 crossedRegionChanged = true 407 } 408 409 // if the crossed region hasn't changed and no new orders were added/removed from the crossed region then we do not need 410 // to recalculate 411 if !ipv.needsUpdate && !crossedRegionChanged { 412 return ipv.lastCumulativeVolumes, ipv.lastMaxTradable 413 } 414 415 if crossedRegionChanged && ipv.offbook != nil { 416 ipv.updateOffbookState(minPrice, maxPrice) 417 } 418 419 rangedLevels := ipv.getLevelsWithinRange(maxPrice, minPrice) 420 // now re-allocate the slice only if needed 421 if ipv.buf == nil || cap(ipv.buf) < len(rangedLevels) { 422 ipv.buf = make([]CumulativeVolumeLevel, len(rangedLevels)) 423 } 424 425 var ( 426 cumulativeVolumeSell, cumulativeVolumeBuy, maxTradable uint64 427 cumulativeOffbookSell, cumulativeOffbookBuy uint64 428 // here the caching buf is already allocated, we can just resize it 429 // based on the required length 430 cumulativeVolumes = ipv.buf[:len(rangedLevels)] 431 ln = len(rangedLevels) - 1 432 ) 433 434 half := ln / 2 435 // now we iterate other all the OK price levels 436 for i := ln; i >= 0; i-- { 437 j := ln - i 438 // reset just to make sure 439 cumulativeVolumes[j].bidVolume = 0 440 cumulativeVolumes[i].askVolume = 0 441 442 if j < i { 443 cumulativeVolumes[j].cumulativeAskVolume = 0 444 cumulativeVolumes[i].cumulativeBidVolume = 0 445 } 446 447 // always set the price 448 cumulativeVolumes[i].price = rangedLevels[i].price 449 450 // if we had a price level in the buy side, use it 451 if rangedLevels[j].buypl.volume > 0 { 452 cumulativeVolumeBuy += rangedLevels[j].buypl.volume 453 cumulativeOffbookBuy += rangedLevels[j].buypl.offbookVolume 454 cumulativeVolumes[j].bidVolume = rangedLevels[j].buypl.volume 455 } 456 457 // same same 458 if rangedLevels[i].sellpl.volume > 0 { 459 cumulativeVolumeSell += rangedLevels[i].sellpl.volume 460 cumulativeOffbookSell += rangedLevels[i].sellpl.offbookVolume 461 cumulativeVolumes[i].askVolume = rangedLevels[i].sellpl.volume 462 } 463 464 // this will always erase the previous values 465 cumulativeVolumes[j].cumulativeBidVolume = cumulativeVolumeBuy 466 cumulativeVolumes[j].cumulativeBidOffbook = cumulativeOffbookBuy 467 468 cumulativeVolumes[i].cumulativeAskVolume = cumulativeVolumeSell 469 cumulativeVolumes[i].cumulativeAskOffbook = cumulativeOffbookSell 470 471 // we just do that 472 // price | sell | buy | vol | ibuy | isell 473 // 100 | 0 | 1 | 0 | 0 | 0 474 // 110 | 14 | 2 | 2 | 0 | 2 475 // 120 | 13 | 5 | 5 | 5 | 0 476 // 130 | 10 | 0 | 0 | 0 | 0 477 // or we just do that 478 // price | sell | buy | vol | ibuy | isell 479 // 100 | 0 | 1 | 0 | 0 | 0 480 // 110 | 14 | 2 | 2 | 0 | 2 481 // 120 | 13 | 5 | 5 | 5 | 5 482 // 130 | 11 | 6 | 6 | 6 | 0 483 // 150 | 10 | 0 | 0 | 0 | 0 484 if j >= half { 485 cumulativeVolumes[i].maxTradableAmount = min(cumulativeVolumes[i].cumulativeAskVolume, cumulativeVolumes[i].cumulativeBidVolume) 486 cumulativeVolumes[j].maxTradableAmount = min(cumulativeVolumes[j].cumulativeAskVolume, cumulativeVolumes[j].cumulativeBidVolume) 487 maxTradable = max(maxTradable, max(cumulativeVolumes[i].maxTradableAmount, cumulativeVolumes[j].maxTradableAmount)) 488 } 489 } 490 491 // reset those fields 492 ipv.needsUpdate = false 493 ipv.lastMinPrice = minPrice.Clone() 494 ipv.lastMaxPrice = maxPrice.Clone() 495 ipv.lastMaxTradable = maxTradable 496 ipv.lastCumulativeVolumes = cumulativeVolumes 497 return cumulativeVolumes, maxTradable 498 } 499 500 // ExtractOffbookOrders returns the cached expanded orders of AM M's in the crossed region of the given side. These 501 // are the order that we will send in aggressively to uncrossed the book. 502 func (ipv *IndicativePriceAndVolume) ExtractOffbookOrders(price *num.Uint, side types.Side, target uint64) []*types.Order { 503 if target == 0 { 504 return []*types.Order{} 505 } 506 507 var volume uint64 508 orders := []*types.Order{} 509 // the ipv keeps track of all the expand AMM orders in the crossed region 510 parties := maps.Keys(ipv.generated) 511 slices.Sort(parties) 512 513 for _, p := range parties { 514 cpm := func(p *num.Uint) bool { return p.LT(price) } 515 oo := ipv.generated[p].buy 516 if side == types.SideSell { 517 oo = ipv.generated[p].sell 518 cpm = func(p *num.Uint) bool { return p.GT(price) } 519 } 520 521 var combined *types.Order 522 for _, o := range oo { 523 if cpm(o.Price) { 524 continue 525 } 526 527 // we want to combine all the uncrossing orders into one big one of the combined volume so that 528 // we only uncross with 1 order and not 1000s of expanded ones for a single AMM. We can take the price 529 // to the best of the lot so that it trades -- it'll get overridden by the uncrossing price after uncrossing 530 // anyway. 531 if combined == nil { 532 combined = o.Clone() 533 orders = append(orders, combined) 534 } else { 535 combined.Size += o.Size 536 combined.Remaining += o.Remaining 537 538 if side == types.SideBuy { 539 combined.Price = num.Max(combined.Price, o.Price) 540 } else { 541 combined.Price = num.Min(combined.Price, o.Price) 542 } 543 } 544 volume += o.Size 545 546 // if we're extracted enough we can stop now 547 if volume == target { 548 return orders 549 } 550 } 551 } 552 553 if volume != target { 554 ipv.log.Panic("Failed to extract AMM orders for uncrossing", 555 logging.BigUint("price", price), 556 logging.Uint64("volume", volume), 557 logging.Uint64("extracted-volume", volume), 558 logging.Uint64("target-volume", target), 559 ) 560 } 561 562 return orders 563 }