code.vegaprotocol.io/vega@v0.79.0/core/execution/amm/shape.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 amm
    17  
    18  import (
    19  	"code.vegaprotocol.io/vega/core/idgeneration"
    20  	"code.vegaprotocol.io/vega/core/types"
    21  	"code.vegaprotocol.io/vega/libs/num"
    22  	"code.vegaprotocol.io/vega/logging"
    23  )
    24  
    25  type shapeMaker struct {
    26  	log   *logging.Logger
    27  	idgen *idgeneration.IDGenerator
    28  
    29  	pool      *Pool     // the AMM we are expanding into orders
    30  	pos       int64     // the AMM's current position
    31  	fairPrice *num.Uint // the AMM's fair-price
    32  
    33  	stepLower  *num.Uint // price step we will be taking as we walk over the lower curve
    34  	stepHigher *num.Uint // price step we will be taking as we walk over the upper curve
    35  
    36  	approx  bool      // whether we are taking approximate steps
    37  	oneTick *num.Uint // one price level tick which may be bigger than one given the markets price factor
    38  
    39  	buys  []*types.Order // buy orders are added here as we calculate them
    40  	sells []*types.Order // sell orders are added here as we calculate them
    41  	side  types.Side     // the side the *next* calculated order will be on
    42  
    43  	from *num.Uint // the adjusted start region i.e the input region capped to the AMM's bounds
    44  	to   *num.Uint // the adjusted end region
    45  
    46  	fromLower bool // whether the price at "from" evaluates on the lower or upper curve of the AMM
    47  	toLower   bool // whether the price at "to" evaluates on the lower or upper curve of the AMM
    48  }
    49  
    50  func newShapeMaker(log *logging.Logger, p *Pool, from, to *num.Uint, idgen *idgeneration.IDGenerator) *shapeMaker {
    51  	buys := make([]*types.Order, 0, p.maxCalculationLevels.Uint64())
    52  	sells := make([]*types.Order, 0, p.maxCalculationLevels.Uint64())
    53  
    54  	return &shapeMaker{
    55  		log:       log,
    56  		pool:      p,
    57  		pos:       p.getPosition(),
    58  		fairPrice: p.FairPrice(),
    59  		buys:      buys,
    60  		sells:     sells,
    61  		from:      from.Clone(),
    62  		to:        to.Clone(),
    63  		side:      types.SideBuy,
    64  		oneTick:   p.oneTick,
    65  		idgen:     idgen,
    66  	}
    67  }
    68  
    69  // addOrder creates an order with the given details and adds it to the relevant slice based on its side.
    70  func (e *shapeMaker) addOrder(volume uint64, price *num.Uint, side types.Side) {
    71  	if e.log.IsDebug() {
    72  		e.log.Debug("creating shape order",
    73  			logging.String("price", price.String()),
    74  			logging.String("side", side.String()),
    75  			logging.Uint64("volume", volume),
    76  			logging.String("amm-party", e.pool.AMMParty),
    77  		)
    78  	}
    79  
    80  	e.appendOrder(e.pool.makeOrder(volume, price, side, e.idgen))
    81  }
    82  
    83  // appendOrder takes the concrete order and appends it to the relevant slice based on its side.
    84  func (sm *shapeMaker) appendOrder(o *types.Order) {
    85  	if o.Side == types.SideBuy {
    86  		sm.buys = append(sm.buys, o)
    87  		return
    88  	}
    89  	sm.sells = append(sm.sells, o)
    90  }
    91  
    92  // makeBoundaryOrder creates an accurrate order for the given one-tick interval which will exist at the edges
    93  // of the adjusted expansion region.
    94  func (sm *shapeMaker) makeBoundaryOrder(price *num.Uint, start bool) *types.Order {
    95  	// lets do the starting boundary order
    96  	cu := sm.pool.upper
    97  	if start && sm.fromLower {
    98  		cu = sm.pool.lower
    99  	}
   100  
   101  	if !start && sm.toLower {
   102  		cu = sm.pool.lower
   103  	}
   104  
   105  	var st, nd *num.Uint
   106  	if start {
   107  		// step inwards
   108  		step := num.Max(sm.oneTick, cu.singleVolumeDelta(sm.pool.sqrt, price, types.SideSell))
   109  
   110  		sm.from.Add(price, step)
   111  		st, nd = price, sm.from
   112  	} else {
   113  		step := num.Max(sm.oneTick, cu.singleVolumeDelta(sm.pool.sqrt, price, types.SideBuy))
   114  
   115  		sm.to.Sub(price, step)
   116  		st, nd = sm.to, price
   117  	}
   118  
   119  	// if one of the boundaries it equal to the fair-price then the equivalent position
   120  	// if the AMM's current position and checking removes the risk of precision loss
   121  	stPosition := sm.pos
   122  	if st.NEQ(sm.fairPrice) {
   123  		stPosition = cu.positionAtPrice(sm.pool.sqrt, st)
   124  	}
   125  
   126  	ndPosition := sm.pos
   127  	if nd.NEQ(sm.fairPrice) {
   128  		ndPosition = cu.positionAtPrice(sm.pool.sqrt, nd)
   129  	}
   130  
   131  	// for sparse AMM's there may be some precision loss in going from price -> position
   132  	// we by construction is should be at least one, we so make sure it is.
   133  	volume := num.MaxV(1, num.DeltaV(stPosition, ndPosition))
   134  
   135  	if st.GTE(sm.fairPrice) {
   136  		return sm.pool.makeOrder(uint64(volume), nd, types.SideSell, sm.idgen)
   137  	}
   138  	return sm.pool.makeOrder(uint64(volume), st, types.SideBuy, sm.idgen)
   139  }
   140  
   141  // calculateBoundaryOrders returns two orders which represent the edges of the adjust expansion region.
   142  func (sm *shapeMaker) calculateBoundaryOrders() (*types.Order, *types.Order) {
   143  	// we need to make sure that the orders at the boundary are the region are always accurate and not approximated
   144  	// by that we mean that if the adjusted expansion region is [p1, p2] then we *always* have an order with price p1
   145  	// and always have an order with price p2.
   146  	//
   147  	// The reason for this is that if we are in an auction and have a crossed-region of [p1, p2] and we don't ensure
   148  	// we have orders *at* p1 and p2 then we create an inconsistency between the orderbook asking an AMM for its best bid/ask
   149  	// and the orders it produces it that region.
   150  	//
   151  	// The two situations where we can miss boundary orders are:
   152  	// - the expansion region is too large and we have to limit calculations and approximate orders
   153  	// - the expansion region isn't divisible by `oneTick` and so we have to merge a sub-tick step in with the previous
   154  
   155  	bnd1 := sm.makeBoundaryOrder(sm.from.Clone(), true)
   156  
   157  	if sm.log.IsDebug() {
   158  		sm.log.Debug("created boundary order",
   159  			logging.String("price", bnd1.Price.String()),
   160  			logging.String("side", bnd1.Side.String()),
   161  			logging.Uint64("volume", bnd1.Size),
   162  			logging.String("pool-party", sm.pool.AMMParty),
   163  		)
   164  	}
   165  
   166  	bnd2 := sm.makeBoundaryOrder(sm.to.Clone(), false)
   167  
   168  	if sm.log.IsDebug() {
   169  		sm.log.Debug("created boundary order",
   170  			logging.String("price", bnd2.Price.String()),
   171  			logging.String("side", bnd2.Side.String()),
   172  			logging.Uint64("volume", bnd2.Size),
   173  			logging.String("pool-party", sm.pool.AMMParty),
   174  		)
   175  	}
   176  
   177  	if sm.from.GTE(sm.fairPrice) {
   178  		sm.side = types.SideSell
   179  	}
   180  
   181  	return bnd1, bnd2
   182  }
   183  
   184  func (sm *shapeMaker) calculateVolumeTick() *num.Uint {
   185  	lowerTick := num.UintZero()
   186  	upperTick := num.UintZero()
   187  
   188  	pool := sm.pool
   189  	if !pool.lower.empty {
   190  		lowerTick = pool.lower.singleVolumeDelta(pool.sqrt, pool.lower.high, types.SideBuy)
   191  		sm.stepLower = num.Max(sm.oneTick, lowerTick)
   192  	}
   193  
   194  	if !pool.upper.empty {
   195  		upperTick = pool.upper.singleVolumeDelta(pool.sqrt, pool.upper.high, types.SideBuy)
   196  		sm.stepHigher = num.Max(sm.oneTick, upperTick)
   197  	}
   198  
   199  	volumeTick := num.Max(lowerTick, upperTick)
   200  	if volumeTick.GT(sm.oneTick) {
   201  		return volumeTick
   202  	}
   203  
   204  	return sm.oneTick.Clone()
   205  }
   206  
   207  // calculateStepSize looks at the size of the expansion region and increases the step size if it is too large.
   208  func (sm *shapeMaker) calculateStepSize() {
   209  	// first we check if it is a sparse AMM, since if it is our accurate step size will be bigger than one tick
   210  	// work out the minimum price delta to cover one volume, and if thats bigger than oneTick
   211  	// set oneTick to that. This is for sparse AMM's where there might be less than one volume
   212  	// between price levels
   213  
   214  	volumeTick := sm.calculateVolumeTick()
   215  	delta, _ := num.UintZero().Delta(sm.from, sm.to)
   216  
   217  	delta1 := num.UintOne().Div(delta, sm.oneTick)
   218  	deltav := num.UintOne().Div(delta, volumeTick)
   219  
   220  	// if taking steps of one-tick doesn't breach the max-calculation levels then we can happily expand accurately
   221  	if deltav.LTE(sm.pool.maxCalculationLevels) {
   222  		return
   223  	}
   224  
   225  	// if the expansion region is too wide, we need to approximate with bigger steps
   226  
   227  	step := num.UintZero().Div(delta1, sm.pool.maxCalculationLevels)
   228  	step.AddSum(num.UintOne()) // if delta / maxcals = 1.9 we're going to want steps of 2
   229  	step.Mul(step, sm.oneTick)
   230  
   231  	sm.approx = true
   232  	sm.stepLower = step
   233  	sm.stepHigher = step.Clone()
   234  
   235  	if sm.log.IsDebug() {
   236  		sm.log.Debug("approximating orderbook expansion",
   237  			logging.String("step", step.String()),
   238  			logging.String("pool-party", sm.pool.AMMParty),
   239  		)
   240  	}
   241  }
   242  
   243  // priceForStep returns a tradable order price for the volume between two price levels.
   244  func (sm *shapeMaker) priceForStep(price1, price2 *num.Uint, pos1, pos2 int64, volume uint64) *num.Uint {
   245  	if sm.side == types.SideBuy {
   246  		if !sm.approx {
   247  			return price1
   248  		}
   249  		return sm.pool.priceForVolumeAtPosition(volume, types.OtherSide(sm.side), pos2, price2)
   250  	}
   251  
   252  	if !sm.approx {
   253  		return price2
   254  	}
   255  
   256  	return sm.pool.priceForVolumeAtPosition(volume, types.OtherSide(sm.side), pos1, price1)
   257  }
   258  
   259  // expandCurve walks along the given AMM curve between from -> to creating orders at each step.
   260  func (sm *shapeMaker) expandCurve(cu *curve, from, to *num.Uint) {
   261  	if sm.log.IsDebug() {
   262  		sm.log.Debug("expanding pool curve",
   263  			logging.Bool("lower-curve", cu.isLower),
   264  			logging.String("low", cu.low.String()),
   265  			logging.String("high", cu.high.String()),
   266  			logging.String("from", sm.from.String()),
   267  			logging.String("to", sm.to.String()),
   268  		)
   269  	}
   270  
   271  	if cu.empty {
   272  		return
   273  	}
   274  
   275  	from = num.Max(from, cu.low)
   276  	to = num.Min(to, cu.high)
   277  
   278  	step := sm.stepHigher
   279  	if cu.isLower {
   280  		step = sm.stepLower
   281  	}
   282  
   283  	// the price we have currently stepped to and the position of the AMM at that price
   284  	current := from
   285  	position := cu.positionAtPrice(sm.pool.sqrt, current)
   286  	fairPrice := sm.fairPrice
   287  
   288  	for current.LT(to) && current.LT(cu.high) {
   289  		// take the next step
   290  		next := num.UintZero().AddSum(current, step)
   291  
   292  		if sm.log.IsDebug() {
   293  			sm.log.Debug("step taken",
   294  				logging.String("current", current.String()),
   295  				logging.String("next", next.String()),
   296  			)
   297  		}
   298  
   299  		if num.UintZero().AddSum(next, sm.oneTick).GT(to) {
   300  			// we step from current -> next, but if next is less that one-tick from the end
   301  			// we will merge this into one bigger step so that we don't have a less-than one price level step
   302  			next = to.Clone()
   303  			if sm.log.IsDebug() {
   304  				sm.log.Debug("increasing step size to prevent sub-tick price-level",
   305  					logging.String("current", current.String()),
   306  					logging.String("next-snapped", next.String()),
   307  				)
   308  			}
   309  		}
   310  
   311  		if sm.side == types.SideBuy && next.GT(fairPrice) && current.NEQ(fairPrice) {
   312  			if sm.log.IsDebug() {
   313  				sm.log.Debug("stepping over fair-price, splitting step",
   314  					logging.String("fair-price", fairPrice.String()),
   315  				)
   316  			}
   317  
   318  			if volume := uint64(num.DeltaV(position, sm.pos)); volume != 0 {
   319  				price := sm.priceForStep(current, fairPrice, position, sm.pos, volume)
   320  				sm.addOrder(volume, price, sm.side)
   321  			}
   322  
   323  			// we've step through fair-price now so orders will becomes sells
   324  			sm.side = types.SideSell
   325  			current = fairPrice
   326  			position = sm.pos
   327  		}
   328  
   329  		nextPosition := cu.positionAtPrice(sm.pool.sqrt, num.Min(next, cu.high))
   330  		volume := uint64(num.DeltaV(position, nextPosition))
   331  		if volume != 0 {
   332  			price := sm.priceForStep(current, next, position, nextPosition, volume)
   333  			sm.addOrder(volume, price, sm.side)
   334  		}
   335  
   336  		// if we're calculating buys and we hit fair price, switch to sells
   337  		if sm.side == types.SideBuy && next.GTE(fairPrice) {
   338  			sm.side = types.SideSell
   339  		}
   340  
   341  		current = next
   342  		position = nextPosition
   343  	}
   344  }
   345  
   346  // adjustRegion takes the input to/from and increases or decreases the interval depending on the pool's bounds.
   347  func (sm *shapeMaker) adjustRegion() bool {
   348  	lower := sm.pool.lower.low
   349  	upper := sm.pool.upper.high
   350  
   351  	if sm.pool.closing() {
   352  		// AMM is in reduce only mode so will only have orders between its fair-price and its base so shrink from/to to that region
   353  		if sm.pos == 0 {
   354  			// pool is closed and we're waiting for the next MTM to close, so it has no orders
   355  			return false
   356  		}
   357  
   358  		if sm.pos > 0 {
   359  			// only orders between fair-price -> base
   360  			lower = sm.fairPrice.Clone()
   361  			upper = sm.pool.lower.high.Clone()
   362  
   363  			// if the AMM is super close to closing its position the delta between fair-price -> base
   364  			// could be very small, but the upshot is we know it will only be one order and can calculate
   365  			// directly
   366  			if num.UintZero().Sub(upper, lower).LTE(sm.oneTick) {
   367  				price := num.UintZero().Sub(sm.pool.lower.high, sm.oneTick)
   368  				sm.addOrder(uint64(sm.pos), price, types.SideSell)
   369  				return false
   370  			}
   371  		} else {
   372  			// only orders between base -> fair-price
   373  			upper = sm.fairPrice.Clone()
   374  			lower = sm.pool.lower.high.Clone()
   375  
   376  			if num.UintZero().Sub(upper, lower).LTE(sm.oneTick) {
   377  				price := num.UintZero().Add(sm.pool.lower.high, sm.oneTick)
   378  				sm.addOrder(uint64(-sm.pos), price, types.SideBuy)
   379  				return false
   380  			}
   381  		}
   382  	}
   383  
   384  	if sm.from.GT(upper) || sm.to.LT(lower) {
   385  		// expansion range is completely outside the pools ranges
   386  		return false
   387  	}
   388  
   389  	// cap the range to the pool's bounds, there will be no orders outside of this
   390  	from := num.Max(sm.from, lower)
   391  	to := num.Min(sm.to, upper)
   392  
   393  	// expansion is a point region *at* fair-price, there are no orders
   394  	if from.EQ(to) && from.EQ(sm.fairPrice) {
   395  		return false
   396  	}
   397  
   398  	// work out which curve from/to will lie in
   399  	base := sm.pool.lower.high
   400  
   401  	sm.fromLower = from.LT(base)
   402  
   403  	if from.EQ(base) {
   404  		// if we're equal to base and equal to fair-price then we want the upper curve because
   405  		// we're matching forward so will want volume from base -> base +1
   406  		sm.fromLower = sm.from.GT(sm.fairPrice)
   407  	}
   408  	sm.toLower = to.LT(base)
   409  	if to.EQ(base) {
   410  		// if we're equal to base and equal to fair-price then we want the lower curve because
   411  		// we're matching forward so will want to end at base - 1 -> base
   412  		sm.toLower = sm.to.GTE(sm.fairPrice)
   413  	}
   414  
   415  	switch {
   416  	case sm.from.GT(sm.fairPrice):
   417  		// if we are expanding entirely in the sell range to calculate the order at price `from`
   418  		// we need to ask the AMM for volume in the range `from - 1 -> from` so we simply
   419  		// sub one here to cover than.
   420  		sm.side = types.SideSell
   421  
   422  		cu := sm.pool.upper
   423  		if sm.fromLower {
   424  			cu = sm.pool.lower
   425  		}
   426  
   427  		step := num.Max(sm.oneTick, cu.singleVolumeDelta(sm.pool.sqrt, from, types.SideBuy))
   428  		from.Sub(from, step)
   429  	case to.LT(sm.fairPrice):
   430  		// if we are expanding entirely in the buy range to calculate the order at price `to`
   431  		// we need to ask the AMM for volume in the range `to -> to + 1` so we simply
   432  		// add one here to cover than.
   433  
   434  		cu := sm.pool.upper
   435  		if sm.toLower {
   436  			cu = sm.pool.lower
   437  		}
   438  
   439  		step := num.Max(sm.oneTick, cu.singleVolumeDelta(sm.pool.sqrt, to, types.SideSell))
   440  		to.Add(to, step)
   441  	case from.EQ(sm.fairPrice):
   442  		// if we are starting the expansion at the fair-price all orders will be sells
   443  		sm.side = types.SideSell
   444  	}
   445  
   446  	// we have the new range we will be expanding over, great
   447  	sm.from = from
   448  	sm.to = to
   449  	return true
   450  }
   451  
   452  func (sm *shapeMaker) makeShape() {
   453  	if !sm.adjustRegion() {
   454  		// if there is no overlap between the input region and the AMM's bounds then there are no orders
   455  		return
   456  	}
   457  
   458  	// create accurate orders at the boundary of the adjusted region (even if we are going to make approximate internal steps)
   459  	bnd1, bnd2 := sm.calculateBoundaryOrders()
   460  
   461  	// we can add the start one now because it'll go at the beginning of the slice
   462  	sm.appendOrder(bnd1)
   463  
   464  	// work out the step size and if we'll be in approximate mode
   465  	sm.calculateStepSize()
   466  
   467  	// now walk across the lower curve
   468  	sm.expandCurve(sm.pool.lower, sm.from, sm.to)
   469  
   470  	// and walk across the upper curve
   471  	sm.expandCurve(sm.pool.upper, sm.from, sm.to)
   472  
   473  	// add the final boundary order we calculated earlier
   474  	if bnd1.Price.NEQ(bnd2.Price) {
   475  		sm.appendOrder(bnd2)
   476  	}
   477  
   478  	if sm.log.IsDebug() {
   479  		sm.log.Debug("pool expanded into orders",
   480  			logging.Int("buys", len(sm.buys)),
   481  			logging.Int("sells", len(sm.sells)),
   482  		)
   483  	}
   484  }
   485  
   486  func (p *Pool) OrderbookShape(from, to *num.Uint, idgen *idgeneration.IDGenerator) *types.OrderbookShapeResult {
   487  	if p.IsPending() {
   488  		return &types.OrderbookShapeResult{AmmParty: p.AMMParty}
   489  	}
   490  
   491  	sm := newShapeMaker(
   492  		p.log,
   493  		p,
   494  		from,
   495  		to,
   496  		idgen)
   497  
   498  	sm.makeShape()
   499  
   500  	return &types.OrderbookShapeResult{
   501  		AmmParty: sm.pool.AMMParty,
   502  		Buys:     sm.buys,
   503  		Sells:    sm.sells,
   504  		Approx:   sm.approx,
   505  	}
   506  }