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 }