code.vegaprotocol.io/vega@v0.79.0/core/execution/amm/engine.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 "context" 20 "encoding/hex" 21 "errors" 22 "fmt" 23 "sort" 24 "time" 25 26 "code.vegaprotocol.io/vega/core/events" 27 "code.vegaprotocol.io/vega/core/execution/common" 28 "code.vegaprotocol.io/vega/core/idgeneration" 29 "code.vegaprotocol.io/vega/core/positions" 30 "code.vegaprotocol.io/vega/core/types" 31 vgcontext "code.vegaprotocol.io/vega/libs/context" 32 "code.vegaprotocol.io/vega/libs/crypto" 33 "code.vegaprotocol.io/vega/libs/num" 34 "code.vegaprotocol.io/vega/logging" 35 v1 "code.vegaprotocol.io/vega/protos/vega/snapshot/v1" 36 37 "golang.org/x/exp/maps" 38 ) 39 40 var ( 41 ErrNoPoolMatchingParty = errors.New("no pool matching party") 42 ErrPartyAlreadyOwnAPool = func(market string) error { 43 return fmt.Errorf("party already own a pool for market %v", market) 44 } 45 ErrCommitmentTooLow = errors.New("commitment amount too low") 46 ErrRebaseOrderDidNotTrade = errors.New("rebase-order did not trade") 47 ErrRebaseTargetOutsideBounds = errors.New("rebase target outside bounds") 48 ) 49 50 const ( 51 V1 = "AMMv1" 52 ) 53 54 //go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/core/execution/amm Collateral,Position 55 56 type Collateral interface { 57 GetAssetQuantum(asset string) (num.Decimal, error) 58 GetAllParties() []string 59 GetPartyMarginAccount(market, party, asset string) (*types.Account, error) 60 GetPartyGeneralAccount(party, asset string) (*types.Account, error) 61 SubAccountUpdate( 62 ctx context.Context, 63 party, subAccount, asset, market string, 64 transferType types.TransferType, 65 amount *num.Uint, 66 ) (*types.LedgerMovement, error) 67 SubAccountClosed(ctx context.Context, party, subAccount, asset, market string) ([]*types.LedgerMovement, error) 68 SubAccountRelease( 69 ctx context.Context, 70 party, subAccount, asset, market string, mevt events.MarketPosition, 71 ) ([]*types.LedgerMovement, events.Margin, error) 72 CreatePartyAMMsSubAccounts( 73 ctx context.Context, 74 party, subAccount, asset, market string, 75 ) (general *types.Account, margin *types.Account, err error) 76 } 77 78 type Broker interface { 79 Send(events.Event) 80 } 81 82 type Position interface { 83 GetPositionsByParty(ids ...string) []events.MarketPosition 84 } 85 86 type sqrtFn func(*num.Uint) num.Decimal 87 88 // Sqrter calculates sqrt's of Uints and caches the results. We want this cache to be shared across all pools for a market. 89 type Sqrter struct { 90 cache map[string]num.Decimal 91 } 92 93 func NewSqrter() *Sqrter { 94 return &Sqrter{cache: map[string]num.Decimal{}} 95 } 96 97 // sqrt calculates the square root of the uint and caches it. 98 func (s *Sqrter) sqrt(u *num.Uint) num.Decimal { 99 if u.IsZero() { 100 return num.DecimalZero() 101 } 102 103 // caching was disabled here since it caused problems with snapshots (https://github.com/vegaprotocol/vega/issues/11523) 104 // and we changed tact to instead cache constant terms in calculations that *involve* sqrt's instead of the sqrt result 105 // directly. I'm leaving the ghost of this cache here incase we need to introduce it again, maybe as a LRU cache instead. 106 // if r, ok := s.cache[u.String()]; ok { 107 // return r 108 // } 109 110 r := num.UintOne().Sqrt(u) 111 112 // s.cache[u.String()] = r 113 return r 114 } 115 116 type Engine struct { 117 log *logging.Logger 118 119 broker Broker 120 marketActivityTracker *common.MarketActivityTracker 121 122 collateral Collateral 123 position Position 124 parties common.Parties 125 126 marketID string 127 assetID string 128 idgen *idgeneration.IDGenerator 129 130 // gets us from the price in the submission -> price in full asset dp 131 priceFactor num.Decimal 132 positionFactor num.Decimal 133 oneTick *num.Uint 134 135 // map of party -> pool 136 pools map[string]*Pool 137 poolsCpy []*Pool 138 139 // sqrt calculator with cache 140 rooter *Sqrter 141 142 // a mapping of all amm-party-ids to the party owning them. 143 ammParties map[string]string 144 145 minCommitmentQuantum *num.Uint 146 maxCalculationLevels *num.Uint 147 allowedEmptyAMMLevels uint64 148 } 149 150 func New( 151 log *logging.Logger, 152 broker Broker, 153 collateral Collateral, 154 marketID string, 155 assetID string, 156 position Position, 157 priceFactor num.Decimal, 158 positionFactor num.Decimal, 159 marketActivityTracker *common.MarketActivityTracker, 160 parties common.Parties, 161 allowedEmptyAMMLevels uint64, 162 ) *Engine { 163 oneTick, _ := num.UintFromDecimal(priceFactor) 164 return &Engine{ 165 log: log, 166 broker: broker, 167 collateral: collateral, 168 position: position, 169 marketID: marketID, 170 assetID: assetID, 171 marketActivityTracker: marketActivityTracker, 172 pools: map[string]*Pool{}, 173 poolsCpy: []*Pool{}, 174 ammParties: map[string]string{}, 175 minCommitmentQuantum: num.UintZero(), 176 rooter: &Sqrter{cache: map[string]num.Decimal{}}, 177 priceFactor: priceFactor, 178 positionFactor: positionFactor, 179 parties: parties, 180 oneTick: num.Max(num.UintOne(), oneTick), 181 allowedEmptyAMMLevels: allowedEmptyAMMLevels, 182 } 183 } 184 185 func NewFromProto( 186 log *logging.Logger, 187 broker Broker, 188 collateral Collateral, 189 marketID string, 190 assetID string, 191 position Position, 192 state *v1.AmmState, 193 priceFactor num.Decimal, 194 positionFactor num.Decimal, 195 marketActivityTracker *common.MarketActivityTracker, 196 parties common.Parties, 197 allowedEmptyAMMLevels uint64, 198 ) (*Engine, error) { 199 e := New(log, broker, collateral, marketID, assetID, position, priceFactor, positionFactor, marketActivityTracker, parties, allowedEmptyAMMLevels) 200 201 for _, v := range state.AmmPartyIds { 202 e.ammParties[v.Key] = v.Value 203 } 204 205 for _, v := range state.Pools { 206 p, err := NewPoolFromProto(log, e.rooter.sqrt, e.collateral, e.position, v.Pool, v.Party, priceFactor, positionFactor) 207 if err != nil { 208 return e, err 209 } 210 e.add(p) 211 } 212 213 return e, nil 214 } 215 216 func (e *Engine) IntoProto() *v1.AmmState { 217 state := &v1.AmmState{ 218 AmmPartyIds: make([]*v1.StringMapEntry, 0, len(e.ammParties)), 219 Pools: make([]*v1.PoolMapEntry, 0, len(e.pools)), 220 } 221 222 for k, v := range e.ammParties { 223 state.AmmPartyIds = append(state.AmmPartyIds, &v1.StringMapEntry{ 224 Key: k, 225 Value: v, 226 }) 227 } 228 sort.Slice(state.AmmPartyIds, func(i, j int) bool { return state.AmmPartyIds[i].Key < state.AmmPartyIds[j].Key }) 229 230 for _, v := range e.poolsCpy { 231 state.Pools = append(state.Pools, &v1.PoolMapEntry{ 232 Party: v.owner, 233 Pool: v.IntoProto(), 234 }) 235 } 236 return state 237 } 238 239 func (e *Engine) OnMinCommitmentQuantumUpdate(ctx context.Context, c *num.Uint) { 240 e.minCommitmentQuantum = c.Clone() 241 } 242 243 func (e *Engine) OnMaxCalculationLevelsUpdate(ctx context.Context, c *num.Uint) { 244 e.maxCalculationLevels = c.Clone() 245 246 for _, p := range e.poolsCpy { 247 p.maxCalculationLevels = e.maxCalculationLevels.Clone() 248 } 249 } 250 251 func (e *Engine) UpdateAllowedEmptyLevels(allowedEmptyLevels uint64) { 252 e.allowedEmptyAMMLevels = allowedEmptyLevels 253 } 254 255 // OnMTM is called whenever core does an MTM and is a signal that any pool's that are closing and have 0 position can be fully removed. 256 func (e *Engine) OnMTM(ctx context.Context) { 257 rm := []string{} 258 for _, p := range e.poolsCpy { 259 if !p.closing() { 260 continue 261 } 262 if pos := p.getPosition(); pos != 0 { 263 continue 264 } 265 266 // pool is closing and has reached 0 position, we can cancel it now 267 if _, err := e.releaseSubAccounts(ctx, p, false); err != nil { 268 e.log.Error("unable to release subaccount balance", logging.Error(err)) 269 } 270 p.status = types.AMMPoolStatusCancelled 271 rm = append(rm, p.owner) 272 } 273 for _, party := range rm { 274 e.remove(ctx, party) 275 } 276 } 277 278 func (e *Engine) OnTick(ctx context.Context, _ time.Time) { 279 // seed an id-generator to create IDs for any orders generated in this block 280 _, blockHash := vgcontext.TraceIDFromContext(ctx) 281 e.idgen = idgeneration.New(blockHash + crypto.HashStrToHex("amm-engine"+e.marketID)) 282 283 // any pools that for some reason have zero balance in their accounts will get stopped 284 rm := []string{} 285 for _, p := range e.poolsCpy { 286 if p.getBalance().IsZero() { 287 p.status = types.AMMPoolStatusStopped 288 rm = append(rm, p.owner) 289 } 290 } 291 for _, party := range rm { 292 e.remove(ctx, party) 293 } 294 } 295 296 // RemoveDistressed checks if any of the closed out parties are AMM's and if so the AMM is stopped and removed. 297 func (e *Engine) RemoveDistressed(ctx context.Context, closed []events.MarketPosition) { 298 for _, c := range closed { 299 owner, ok := e.ammParties[c.Party()] 300 if !ok { 301 continue 302 } 303 p, ok := e.pools[owner] 304 if !ok { 305 e.log.Panic("could not find pool for owner, not possible", 306 logging.String("owner", c.Party()), 307 logging.String("owner", owner), 308 ) 309 } 310 p.status = types.AMMPoolStatusStopped 311 e.remove(ctx, owner) 312 } 313 } 314 315 // BestPricesAndVolumes returns the best bid/ask and their volumes across all the registered AMM's. 316 func (e *Engine) BestPricesAndVolumes() (*num.Uint, uint64, *num.Uint, uint64) { 317 var bestBid, bestAsk *num.Uint 318 var bestBidVolume, bestAskVolume uint64 319 320 for _, pool := range e.poolsCpy { 321 var volume uint64 322 bid, volume := pool.BestPriceAndVolume(types.SideBuy) 323 if volume != 0 { 324 if bestBid == nil || bid.GT(bestBid) { 325 bestBid = bid 326 bestBidVolume = volume 327 } else if bid.EQ(bestBid) { 328 bestBidVolume += volume 329 } 330 } 331 332 ask, volume := pool.BestPriceAndVolume(types.SideSell) 333 if volume != 0 { 334 if bestAsk == nil || ask.LT(bestAsk) { 335 bestAsk = ask 336 bestAskVolume = volume 337 } else if ask.EQ(bestAsk) { 338 bestAskVolume += volume 339 } 340 } 341 } 342 return bestBid, bestBidVolume, bestAsk, bestAskVolume 343 } 344 345 // GetVolumeAtPrice returns the volumes across all registered AMM's that will uncross with with an order at the given price. 346 // Calling this function with price 1000 and side == sell will return the buy orders that will uncross. 347 func (e *Engine) GetVolumeAtPrice(price *num.Uint, side types.Side) uint64 { 348 vol := uint64(0) 349 for _, pool := range e.poolsCpy { 350 // get the pool's current price 351 best, ok := pool.BestPrice(types.OtherSide(side)) 352 if !ok { 353 continue 354 } 355 356 // make sure price is in tradable range 357 if side == types.SideBuy && best.GT(price) { 358 continue 359 } 360 361 if side == types.SideSell && best.LT(price) { 362 continue 363 } 364 365 volume := pool.TradableVolumeForPrice(side, price) 366 vol += volume 367 } 368 return vol 369 } 370 371 func (e *Engine) submit(active []*Pool, agg *types.Order, inner, outer *num.Uint) []*types.Order { 372 if e.log.GetLevel() == logging.DebugLevel { 373 e.log.Debug("checking for volume between", 374 logging.String("inner", inner.String()), 375 logging.String("outer", outer.String()), 376 ) 377 } 378 379 orders := []*types.Order{} 380 useActive := make([]*Pool, 0, len(active)) 381 for _, p := range active { 382 p.setEphemeralPosition() 383 384 price, ok := p.BestPrice(types.OtherSide(agg.Side)) 385 if !ok { 386 continue 387 } 388 389 if e.log.GetLevel() == logging.DebugLevel { 390 e.log.Debug("best price for pool", 391 logging.String("amm-party", p.AMMParty), 392 logging.String("best-price", price.String()), 393 ) 394 } 395 396 if agg.Side == types.SideBuy { 397 if price.GT(outer) || (agg.Type != types.OrderTypeMarket && price.GT(agg.Price)) { 398 // either fair price is out of bounds, or is selling at higher than incoming buy 399 continue 400 } 401 } 402 403 if agg.Side == types.SideSell { 404 if price.LT(outer) || (agg.Type != types.OrderTypeMarket && price.LT(agg.Price)) { 405 // either fair price is out of bounds, or is buying at lower than incoming sell 406 continue 407 } 408 } 409 useActive = append(useActive, p) 410 } 411 412 // calculate the volume each pool has 413 var total uint64 414 volumes := []uint64{} 415 for _, p := range useActive { 416 volume := p.TradableVolumeForPrice(agg.Side, outer) 417 if e.log.GetLevel() == logging.DebugLevel { 418 e.log.Debug("volume available to trade", 419 logging.Uint64("volume", volume), 420 logging.String("amm-party", p.AMMParty), 421 ) 422 } 423 424 volumes = append(volumes, volume) 425 total += volume 426 } 427 428 // if the pools consume the whole incoming order's volume, share it out pro-rata 429 if agg.Remaining < total { 430 maxVolumes := make([]uint64, 0, len(volumes)) 431 // copy the available volumes for rounding. 432 maxVolumes = append(maxVolumes, volumes...) 433 var retotal uint64 434 for i := range volumes { 435 volumes[i] = agg.Remaining * volumes[i] / total 436 retotal += volumes[i] 437 } 438 439 // any lost crumbs due to integer division is given to the pools that can accommodate it. 440 if d := agg.Remaining - retotal; d != 0 { 441 for i, v := range volumes { 442 if delta := maxVolumes[i] - v; delta != 0 { 443 if delta >= d { 444 volumes[i] += d 445 break 446 } 447 volumes[i] += delta 448 d -= delta 449 } 450 } 451 } 452 } 453 454 // now generate offbook orders 455 for i, p := range useActive { 456 volume := volumes[i] 457 if volume == 0 { 458 continue 459 } 460 461 // calculate the price the pool wil give for the trading volume 462 price := p.PriceForVolume(volume, agg.Side) 463 464 if e.log.IsDebug() { 465 e.log.Debug("generated order at price", 466 logging.String("price", price.String()), 467 logging.Uint64("volume", volume), 468 logging.String("id", p.ID), 469 logging.String("side", types.OtherSide(agg.Side).String()), 470 ) 471 } 472 473 // construct an order 474 o := p.makeOrder(volume, price, types.OtherSide(agg.Side), e.idgen) 475 476 // fill in extra details 477 o.CreatedAt = agg.CreatedAt 478 479 orders = append(orders, o) 480 p.updateEphemeralPosition(o) 481 482 agg.Remaining -= volume 483 } 484 485 return orders 486 } 487 488 // partition takes the given price range and returns which pools have volume in that region, and 489 // divides that range into sub-levels where AMM boundaries end. Note that `outer` can be nil for the case 490 // where the incoming order is a market order (so we have no bound on the price), and we've already consumed 491 // all volume on the orderbook. 492 func (e *Engine) partition(agg *types.Order, inner, outer *num.Uint) ([]*Pool, []*num.Uint) { 493 active := []*Pool{} 494 bounds := map[string]*num.Uint{} 495 496 // cap outer to incoming order price 497 if agg.Type != types.OrderTypeMarket { 498 switch { 499 case outer == nil: 500 outer = agg.Price.Clone() 501 case agg.Side == types.SideSell && agg.Price.GT(outer): 502 outer = agg.Price.Clone() 503 case agg.Side == types.SideBuy && agg.Price.LT(outer): 504 outer = agg.Price.Clone() 505 } 506 } 507 508 if inner == nil { 509 // if inner is given as nil it means the matching engine is trading up to its first price level 510 // and so has no lower bound on the range. So we'll calculate one using best price of all pools 511 // note that if the incoming order is a buy the price range we need to evaluate is from 512 // fair-price -> best-ask -> outer, so we need to step one back. But then if we use fair-price exactly we 513 // risk hitting numerical problems and given this is just to exclude AMM's completely out of range we 514 // can be a bit looser and so step back again so that we evaluate from best-buy -> best-ask -> outer. 515 buy, _, ask, _ := e.BestPricesAndVolumes() 516 two := num.UintZero().AddSum(e.oneTick, e.oneTick) 517 if agg.Side == types.SideBuy && ask != nil { 518 inner = num.UintZero().Sub(ask, two) 519 } 520 if agg.Side == types.SideSell && buy != nil { 521 inner = num.UintZero().Add(buy, two) 522 } 523 } 524 525 // switch so that inner < outer to make it easier to reason with 526 if agg.Side == types.SideSell { 527 inner, outer = outer, inner 528 } 529 530 // if inner and outer are equal then we are wanting to trade with AMMs *only at* this given price 531 // this can happen quite easily during auction uncrossing where two AMMs have bases offset by 2 532 // and the crossed region is simply a point and not an interval. To be able to query the tradable 533 // volume of an AMM at a point, we need to first convert it to an interval by stepping one tick away first. 534 // This is because to get the BUY volume an AMM has at price P, we need to calculate the difference 535 // in its position between prices P -> P + 1. For SELL volume its the other way around and we 536 // need the difference in position from P - 1 -> P. 537 if inner != nil && outer != nil && inner.EQ(outer) { 538 if agg.Side == types.SideSell { 539 outer = num.UintZero().Add(outer, e.oneTick) 540 } else { 541 inner = num.UintZero().Sub(inner, e.oneTick) 542 } 543 } 544 545 if inner != nil { 546 bounds[inner.String()] = inner.Clone() 547 } 548 if outer != nil { 549 bounds[outer.String()] = outer.Clone() 550 } 551 552 for _, p := range e.poolsCpy { 553 // not active in range if it cannot trade 554 if !p.canTrade(agg.Side) { 555 continue 556 } 557 558 // stop early trying to trade with itself, can happens during auction uncrossing 559 if agg.Party == p.AMMParty { 560 continue 561 } 562 563 // not active in range if its the pool's curves are wholly outside of [inner, outer] 564 if (inner != nil && p.upper.high.LT(inner)) || (outer != nil && p.lower.low.GT(outer)) { 565 continue 566 } 567 568 // pool is active in range add it to the slice 569 active = append(active, p) 570 571 // we hit a discontinuity where an AMM's two curves meet if we try to trade over its base-price 572 // so we partition the inner/outer price range at the base price so that we instead trade across it 573 // in two steps. 574 boundary := p.upper.low 575 if inner != nil && outer != nil { 576 if boundary.LT(outer) && boundary.GT(inner) { 577 bounds[boundary.String()] = boundary.Clone() 578 } 579 } else if outer == nil && boundary.GT(inner) { 580 bounds[boundary.String()] = boundary.Clone() 581 } else if inner == nil && boundary.LT(outer) { 582 bounds[boundary.String()] = boundary.Clone() 583 } 584 585 // if a pool's upper or lower boundary exists within (inner, outer) then we consider that a sub-level 586 boundary = p.upper.high 587 if outer == nil || boundary.LT(outer) { 588 bounds[boundary.String()] = boundary.Clone() 589 } 590 591 boundary = p.lower.low 592 if inner == nil || boundary.GT(inner) { 593 bounds[boundary.String()] = boundary.Clone() 594 } 595 } 596 597 // now sort the sub-levels, if the incoming order is a buy we want them ordered ascending so we consider prices in this order: 598 // 2000 -> 2100 -> 2200 599 // 600 // and if its a sell we want them descending so we consider them like: 601 // 2000 -> 1900 -> 1800 602 levels := maps.Values(bounds) 603 sort.Slice(levels, 604 func(i, j int) bool { 605 if agg.Side == types.SideSell { 606 return levels[i].GT(levels[j]) 607 } 608 return levels[i].LT(levels[j]) 609 }, 610 ) 611 return active, levels 612 } 613 614 // SubmitOrder takes an aggressive order and generates matching orders with the registered AMMs such that 615 // volume is only taken in the interval (inner, outer) where inner and outer are price-levels on the orderbook. 616 // For example if agg is a buy order inner < outer, and if its a sell outer < inner. 617 func (e *Engine) SubmitOrder(agg *types.Order, inner, outer *num.Uint) []*types.Order { 618 if len(e.pools) == 0 { 619 return nil 620 } 621 622 if e.log.GetLevel() == logging.DebugLevel { 623 e.log.Debug("looking for match with order", 624 logging.Int("n-pools", len(e.pools)), 625 logging.Order(agg), 626 ) 627 } 628 629 // parition the given range into levels where AMM boundaries end 630 agg = agg.Clone() 631 active, levels := e.partition(agg, inner, outer) 632 633 // submit orders to active pool's between each price level created by any of their high/low boundaries 634 orders := []*types.Order{} 635 for i := 0; i < len(levels)-1; i++ { 636 o := e.submit(active, agg, levels[i], levels[i+1]) 637 orders = append(orders, o...) 638 639 if agg.Remaining == 0 { 640 break 641 } 642 } 643 644 return orders 645 } 646 647 // NotifyFinished is called when the matching engine has finished matching an order and is returning it to 648 // the market for processing. 649 func (e *Engine) NotifyFinished() { 650 for _, p := range e.poolsCpy { 651 p.clearEphemeralPosition() 652 } 653 } 654 655 // Create takes the definition of an AMM and returns it. It is not considered a participating AMM until Confirm as been called with it. 656 func (e *Engine) Create( 657 ctx context.Context, 658 submit *types.SubmitAMM, 659 deterministicID string, 660 riskFactors *types.RiskFactor, 661 scalingFactors *types.ScalingFactors, 662 slippage num.Decimal, 663 ) (*Pool, error) { 664 idgen := idgeneration.New(deterministicID) 665 poolID := idgen.NextID() 666 667 subAccount := DeriveAMMParty(submit.Party, submit.MarketID, V1, 0) 668 _, ok := e.pools[submit.Party] 669 if ok { 670 return nil, ErrPartyAlreadyOwnAPool(e.marketID) 671 } 672 673 if err := e.ensureCommitmentAmount(ctx, submit.Party, subAccount, submit.CommitmentAmount); err != nil { 674 return nil, err 675 } 676 677 _, _, err := e.collateral.CreatePartyAMMsSubAccounts(ctx, submit.Party, subAccount, e.assetID, submit.MarketID) 678 if err != nil { 679 return nil, err 680 } 681 682 pool, err := NewPool( 683 e.log, 684 poolID, 685 subAccount, 686 e.assetID, 687 submit, 688 e.rooter.sqrt, 689 e.collateral, 690 e.position, 691 riskFactors, 692 scalingFactors, 693 slippage, 694 e.priceFactor, 695 e.positionFactor, 696 e.maxCalculationLevels, 697 e.allowedEmptyAMMLevels, 698 submit.SlippageTolerance, 699 submit.MinimumPriceChangeTrigger, 700 ) 701 if err != nil { 702 return nil, err 703 } 704 705 // sanity check, a *new* AMM should not already have a position. If it does it means that the party 706 // previously had an AMM but it was stopped/cancelled while still holding a position which should not happen. 707 // It should have either handed its position over to the liquidation engine, or be in reduce-only mode 708 // and only be removed when its position is 0. 709 if pool.getPosition() != 0 { 710 e.log.Panic("AMM has position before existing") 711 } 712 713 e.log.Debug("AMM created", 714 logging.String("owner", submit.Party), 715 logging.String("poolID", pool.ID), 716 logging.String("marketID", e.marketID), 717 ) 718 return pool, nil 719 } 720 721 // Confirm takes an AMM that was created earlier and now commits it to the engine as a functioning pool. 722 func (e *Engine) Confirm( 723 ctx context.Context, 724 pool *Pool, 725 ) { 726 e.log.Debug("AMM confirmed", 727 logging.String("owner", pool.owner), 728 logging.String("marketID", e.marketID), 729 logging.String("poolID", pool.ID), 730 ) 731 732 pool.maxCalculationLevels = e.maxCalculationLevels 733 734 e.add(pool) 735 e.sendUpdate(ctx, pool) 736 e.parties.AssignDeriveKey(ctx, types.PartyID(pool.owner), pool.AMMParty) 737 } 738 739 // Amend takes the details of an amendment to an AMM and returns a copy of that pool with the updated curves along with the current pool. 740 // The changes are not taken place in the AMM engine until Confirm is called on the updated pool. 741 func (e *Engine) Amend( 742 ctx context.Context, 743 amend *types.AmendAMM, 744 riskFactors *types.RiskFactor, 745 scalingFactors *types.ScalingFactors, 746 slippage num.Decimal, 747 ) (*Pool, *Pool, error) { 748 pool, ok := e.pools[amend.Party] 749 if !ok { 750 return nil, nil, ErrNoPoolMatchingParty 751 } 752 753 if amend.CommitmentAmount != nil { 754 if err := e.ensureCommitmentAmount(ctx, amend.Party, pool.AMMParty, amend.CommitmentAmount); err != nil { 755 return nil, nil, err 756 } 757 } 758 759 updated, err := pool.Update(amend, riskFactors, scalingFactors, slippage, e.allowedEmptyAMMLevels) 760 if err != nil { 761 return nil, nil, err 762 } 763 764 // we need to remove the existing pool from the engine so that when calculating rebasing orders we do not 765 // trade with ourselves. 766 e.remove(ctx, amend.Party) 767 768 e.log.Debug("AMM amended", 769 logging.String("owner", amend.Party), 770 logging.String("marketID", e.marketID), 771 logging.String("poolID", pool.ID), 772 ) 773 return updated, pool, nil 774 } 775 776 // GetDataSourcedAMMs returns any AMM's whose base price is determined by the given data source ID. 777 func (e *Engine) GetDataSourcedAMMs(dataSourceID string) []*Pool { 778 pools := []*Pool{} 779 for _, p := range e.poolsCpy { 780 if p.Parameters.DataSourceID == nil { 781 continue 782 } 783 784 if *p.Parameters.DataSourceID != dataSourceID { 785 continue 786 } 787 788 pools = append(pools, p) 789 } 790 return pools 791 } 792 793 func (e *Engine) CancelAMM( 794 ctx context.Context, 795 cancel *types.CancelAMM, 796 ) (events.Margin, error) { 797 pool, ok := e.pools[cancel.Party] 798 if !ok { 799 return nil, ErrNoPoolMatchingParty 800 } 801 802 if cancel.Method == types.AMMCancellationMethodReduceOnly { 803 // pool will now only accept trades that will reduce its position 804 pool.status = types.AMMPoolStatusReduceOnly 805 e.sendUpdate(ctx, pool) 806 return nil, nil 807 } 808 809 // either pool has no position or owner wants out right now, so release general balance and 810 // get ready for a closeout. 811 closeout, err := e.releaseSubAccounts(ctx, pool, false) 812 if err != nil { 813 return nil, err 814 } 815 816 pool.status = types.AMMPoolStatusCancelled 817 e.remove(ctx, cancel.Party) 818 e.log.Debug("AMM cancelled", 819 logging.String("owner", cancel.Party), 820 logging.String("poolID", pool.ID), 821 logging.String("marketID", e.marketID), 822 ) 823 return closeout, nil 824 } 825 826 func (e *Engine) StopPool( 827 ctx context.Context, 828 key string, 829 ) error { 830 party, ok := e.ammParties[key] 831 if !ok { 832 return ErrNoPoolMatchingParty 833 } 834 e.remove(ctx, party) 835 return nil 836 } 837 838 // MarketClosing stops all AMM's and returns subaccount balances back to the owning party. 839 func (e *Engine) MarketClosing(ctx context.Context) error { 840 for _, p := range e.poolsCpy { 841 if _, err := e.releaseSubAccounts(ctx, p, true); err != nil { 842 return err 843 } 844 p.status = types.AMMPoolStatusStopped 845 e.sendUpdate(ctx, p) 846 e.marketActivityTracker.RemoveAMMParty(e.assetID, e.marketID, p.AMMParty) 847 } 848 849 e.pools = nil 850 e.poolsCpy = nil 851 e.ammParties = nil 852 return nil 853 } 854 855 func (e *Engine) sendUpdate(ctx context.Context, pool *Pool) { 856 e.broker.Send( 857 events.NewAMMPoolEvent( 858 ctx, pool.owner, e.marketID, pool.AMMParty, pool.ID, 859 pool.Commitment, pool.Parameters, 860 pool.status, types.AMMStatusReasonUnspecified, 861 pool.ProposedFee, 862 &events.AMMCurve{ 863 VirtualLiquidity: pool.lower.l, 864 TheoreticalPosition: pool.lower.pv, 865 }, 866 &events.AMMCurve{ 867 VirtualLiquidity: pool.upper.l, 868 TheoreticalPosition: pool.upper.pv, 869 }, 870 pool.MinimumPriceChangeTrigger, 871 ), 872 ) 873 } 874 875 func (e *Engine) ensureCommitmentAmount( 876 _ context.Context, 877 party string, 878 subAccount string, 879 commitmentAmount *num.Uint, 880 ) error { 881 quantum, _ := e.collateral.GetAssetQuantum(e.assetID) 882 quantumCommitment := commitmentAmount.ToDecimal().Div(quantum) 883 884 if quantumCommitment.LessThan(e.minCommitmentQuantum.ToDecimal()) { 885 return ErrCommitmentTooLow 886 } 887 888 total := num.UintZero() 889 890 // check they have enough in their accounts, sub-margin + sub-general + general >= commitment 891 if a, err := e.collateral.GetPartyMarginAccount(e.marketID, subAccount, e.assetID); err == nil { 892 total.Add(total, a.Balance) 893 } 894 895 if a, err := e.collateral.GetPartyGeneralAccount(subAccount, e.assetID); err == nil { 896 total.Add(total, a.Balance) 897 } 898 899 if a, err := e.collateral.GetPartyGeneralAccount(party, e.assetID); err == nil { 900 total.Add(total, a.Balance) 901 } 902 903 if total.LT(commitmentAmount) { 904 return fmt.Errorf("not enough collateral in general account") 905 } 906 907 return nil 908 } 909 910 // releaseSubAccountGeneralBalance returns the full balance of the sub-accounts general account back to the 911 // owner of the pool. 912 func (e *Engine) releaseSubAccounts(ctx context.Context, pool *Pool, mktClose bool) (events.Margin, error) { 913 if mktClose { 914 ledgerMovements, err := e.collateral.SubAccountClosed(ctx, pool.owner, pool.AMMParty, pool.asset, pool.market) 915 if err != nil { 916 return nil, err 917 } 918 e.broker.Send(events.NewLedgerMovements(ctx, ledgerMovements)) 919 return nil, nil 920 } 921 var pos events.MarketPosition 922 if pp := e.position.GetPositionsByParty(pool.AMMParty); len(pp) > 0 { 923 pos = pp[0] 924 } else { 925 // if a pool is cancelled right after creation it won't have a position yet so we just make an empty one to give 926 // to collateral 927 pos = positions.NewMarketPosition(pool.AMMParty) 928 } 929 930 ledgerMovements, closeout, err := e.collateral.SubAccountRelease(ctx, pool.owner, pool.AMMParty, pool.asset, pool.market, pos) 931 if err != nil { 932 return nil, err 933 } 934 935 e.broker.Send(events.NewLedgerMovements( 936 ctx, ledgerMovements)) 937 return closeout, nil 938 } 939 940 func (e *Engine) UpdateSubAccountBalance( 941 ctx context.Context, 942 party, subAccount string, 943 newCommitment *num.Uint, 944 ) (*num.Uint, error) { 945 // first we get the current balance of both the margin, and general subAccount 946 subMargin, err := e.collateral.GetPartyMarginAccount( 947 e.marketID, subAccount, e.assetID) 948 if err != nil { 949 // by that point the account must exist 950 e.log.Panic("no sub margin account", logging.Error(err)) 951 } 952 subGeneral, err := e.collateral.GetPartyGeneralAccount( 953 subAccount, e.assetID) 954 if err != nil { 955 // by that point the account must exist 956 e.log.Panic("no sub general account", logging.Error(err)) 957 } 958 959 var ( 960 currentCommitment = num.Sum(subMargin.Balance, subGeneral.Balance) 961 transferType types.TransferType 962 actualAmount = num.UintZero() 963 ) 964 965 if currentCommitment.LT(newCommitment) { 966 transferType = types.TransferTypeAMMLow 967 actualAmount.Sub(newCommitment, currentCommitment) 968 } else if currentCommitment.GT(newCommitment) { 969 transferType = types.TransferTypeAMMHigh 970 actualAmount.Sub(currentCommitment, newCommitment) 971 } else { 972 // nothing to do 973 return currentCommitment, nil 974 } 975 976 ledgerMovements, err := e.collateral.SubAccountUpdate( 977 ctx, party, subAccount, e.assetID, 978 e.marketID, transferType, actualAmount, 979 ) 980 if err != nil { 981 return nil, err 982 } 983 984 e.broker.Send(events.NewLedgerMovements( 985 ctx, []*types.LedgerMovement{ledgerMovements})) 986 987 return currentCommitment, nil 988 } 989 990 // OrderbookShape expands all registered AMM's into orders between the given prices. If `ammParty` is supplied then just the pool 991 // with that party id is expanded. 992 func (e *Engine) OrderbookShape(st, nd *num.Uint, ammParty *string) []*types.OrderbookShapeResult { 993 if ammParty == nil { 994 // no party give, expand all registered 995 res := make([]*types.OrderbookShapeResult, 0, len(e.poolsCpy)) 996 for _, p := range e.poolsCpy { 997 res = append(res, p.OrderbookShape(st, nd, e.idgen)) 998 } 999 return res 1000 } 1001 1002 // asked to expand just one AMM, lets find it, first amm-party -> owning party 1003 owner, ok := e.ammParties[*ammParty] 1004 if !ok { 1005 return nil 1006 } 1007 1008 // now owning party -> pool 1009 p, ok := e.pools[owner] 1010 if !ok { 1011 return nil 1012 } 1013 1014 // expand it 1015 return []*types.OrderbookShapeResult{p.OrderbookShape(st, nd, e.idgen)} 1016 } 1017 1018 func (e *Engine) GetAMMPoolsBySubAccount() map[string]common.AMMPool { 1019 ret := make(map[string]common.AMMPool, len(e.pools)) 1020 for _, v := range e.pools { 1021 ret[v.AMMParty] = v 1022 } 1023 return ret 1024 } 1025 1026 func (e *Engine) GetAllSubAccounts() []string { 1027 ret := make([]string, 0, len(e.ammParties)) 1028 for _, subAccount := range e.ammParties { 1029 ret = append(ret, subAccount) 1030 } 1031 return ret 1032 } 1033 1034 // GetAMMParty returns the AMM's key given the owners key. 1035 func (e *Engine) GetAMMParty(party string) (string, error) { 1036 if p, ok := e.pools[party]; ok { 1037 return p.AMMParty, nil 1038 } 1039 return "", ErrNoPoolMatchingParty 1040 } 1041 1042 // IsAMMPartyID returns whether the given key is the key of AMM registered with the engine. 1043 func (e *Engine) IsAMMPartyID(key string) bool { 1044 _, yes := e.ammParties[key] 1045 return yes 1046 } 1047 1048 func (e *Engine) add(p *Pool) { 1049 e.pools[p.owner] = p 1050 e.poolsCpy = append(e.poolsCpy, p) 1051 e.ammParties[p.AMMParty] = p.owner 1052 e.marketActivityTracker.AddAMMSubAccount(e.assetID, e.marketID, p.AMMParty) 1053 } 1054 1055 func (e *Engine) remove(ctx context.Context, party string) { 1056 for i := range e.poolsCpy { 1057 if e.poolsCpy[i].owner == party { 1058 e.poolsCpy = append(e.poolsCpy[:i], e.poolsCpy[i+1:]...) 1059 break 1060 } 1061 } 1062 1063 pool := e.pools[party] 1064 delete(e.pools, party) 1065 delete(e.ammParties, pool.AMMParty) 1066 e.sendUpdate(ctx, pool) 1067 e.marketActivityTracker.RemoveAMMParty(e.assetID, e.marketID, pool.AMMParty) 1068 } 1069 1070 func DeriveAMMParty( 1071 party, market, version string, 1072 index uint64, 1073 ) string { 1074 hash := crypto.Hash([]byte(fmt.Sprintf("%v%v%v%v", version, market, party, index))) 1075 return hex.EncodeToString(hash) 1076 }