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  }