From 839c15f1e090316e707c536297c6fa04e9abfc04 Mon Sep 17 00:00:00 2001 From: wwestgarth Date: Tue, 28 May 2024 13:19:51 +0100 Subject: [PATCH] feat: implement auction uncrossing for AMMs --- commands/amend_amm.go | 33 +- commands/amend_amm_test.go | 37 ++ core/execution/amm/engine.go | 98 ++- core/execution/amm/engine_test.go | 2 +- core/execution/amm/pool.go | 263 ++++++-- core/execution/amm/pool_test.go | 306 ++++++++- core/execution/common/interfaces.go | 3 +- .../common/liquidity_provision_fees.go | 2 +- core/execution/common/mocks_amm/mocks.go | 9 +- core/execution/engine.go | 558 ---------------- core/execution/engine_netparams.go | 602 ++++++++++++++++++ core/execution/future/market.go | 34 +- core/execution/future/market_callbacks.go | 4 + .../features/amm/0090-VAMM-006-014.feature | 14 +- .../features/amm/0090-VAMM-016.feature | 18 +- .../features/amm/0090-VAMM-019.feature | 6 +- .../features/amm/0090-VAMM-020.feature | 6 +- .../features/amm/0090-VAMM-021.feature | 16 +- .../features/amm/0090-VAMM-024-026.feature | 2 +- .../features/amm/0090-VAMM-auction.feature | 370 +++++++++++ core/integration/setup_test.go | 4 + core/matching/cached_orderbook.go | 9 + core/matching/indicative_price_and_volume.go | 173 ++++- core/matching/mocks/mocks.go | 15 + core/matching/orderbook.go | 146 ++++- core/matching/side.go | 5 +- core/netparams/defaults.go | 3 +- core/netparams/keys.go | 2 + core/protocol/all_services.go | 4 + libs/num/compare.go | 8 + 30 files changed, 2013 insertions(+), 739 deletions(-) create mode 100644 core/execution/engine_netparams.go create mode 100644 core/integration/features/amm/0090-VAMM-auction.feature diff --git a/commands/amend_amm.go b/commands/amend_amm.go index a2e225f48fa..17d221a9278 100644 --- a/commands/amend_amm.go +++ b/commands/amend_amm.go @@ -16,6 +16,7 @@ package commands import ( + "errors" "math/big" "code.vegaprotocol.io/vega/libs/num" @@ -69,31 +70,44 @@ func checkAmendAMM(cmd *commandspb.AmendAMM) Errors { if cmd.ConcentratedLiquidityParameters != nil { hasUpdate = true - if amount, _ := big.NewInt(0).SetString(cmd.ConcentratedLiquidityParameters.Base, 10); amount == nil { + var base, lowerBound, upperBound *big.Int + if base, _ = big.NewInt(0).SetString(cmd.ConcentratedLiquidityParameters.Base, 10); base == nil { errs.FinalAddForProperty("amend_amm.concentrated_liquidity_parameters.base", ErrIsNotValidNumber) - } else if amount.Cmp(big.NewInt(0)) <= 0 { + } else if base.Cmp(big.NewInt(0)) <= 0 { errs.AddForProperty("amend_amm.concentrated_liquidity_parameters.base", ErrMustBePositive) } + var haveLower, haveUpper bool if cmd.ConcentratedLiquidityParameters.LowerBound != nil { - hasUpdate = true - if amount, _ := big.NewInt(0).SetString(*cmd.ConcentratedLiquidityParameters.LowerBound, 10); amount == nil { + haveLower = true + if lowerBound, _ = big.NewInt(0).SetString(*cmd.ConcentratedLiquidityParameters.LowerBound, 10); lowerBound == nil { errs.FinalAddForProperty("amend_amm.concentrated_liquidity_parameters.lower_bound", ErrIsNotValidNumber) - } else if amount.Cmp(big.NewInt(0)) <= 0 { + } else if lowerBound.Cmp(big.NewInt(0)) <= 0 { errs.AddForProperty("amend_amm.concentrated_liquidity_parameters.lower_bound", ErrMustBePositive) } } if cmd.ConcentratedLiquidityParameters.UpperBound != nil { - hasUpdate = true - if amount, _ := big.NewInt(0).SetString(*cmd.ConcentratedLiquidityParameters.UpperBound, 10); amount == nil { + haveUpper = true + if upperBound, _ = big.NewInt(0).SetString(*cmd.ConcentratedLiquidityParameters.UpperBound, 10); upperBound == nil { errs.FinalAddForProperty("amend_amm.concentrated_liquidity_parameters.upper_bound", ErrIsNotValidNumber) - } else if amount.Cmp(big.NewInt(0)) <= 0 { + } else if upperBound.Cmp(big.NewInt(0)) <= 0 { errs.AddForProperty("amend_amm.concentrated_liquidity_parameters.upper_bound", ErrMustBePositive) } } + if !haveLower && !haveUpper { + errs.AddForProperty("amend_amm.concentrated_liquidity_parameters.lower_bound", errors.New("lower_bound and upper_bound cannot both be empty")) + } + + if base != nil && lowerBound != nil && base.Cmp(lowerBound) <= 0 { + errs.AddForProperty("amend_amm.concentrated_liquidity_parameters.base", errors.New("should be a bigger value than lower_bound")) + } + + if base != nil && upperBound != nil && base.Cmp(upperBound) >= 0 { + errs.AddForProperty("amend_amm.concentrated_liquidity_parameters.base", errors.New("should be a smaller value than upper_bound")) + } + if cmd.ConcentratedLiquidityParameters.LeverageAtUpperBound != nil { - hasUpdate = true if leverage, err := num.DecimalFromString(*cmd.ConcentratedLiquidityParameters.LeverageAtUpperBound); err != nil { errs.AddForProperty("amend_amm.concentrated_liquidity_parameters.leverage_at_upper_bound", ErrIsNotValidNumber) } else if leverage.LessThan(num.DecimalZero()) { @@ -102,7 +116,6 @@ func checkAmendAMM(cmd *commandspb.AmendAMM) Errors { } if cmd.ConcentratedLiquidityParameters.LeverageAtLowerBound != nil { - hasUpdate = true if leverage, err := num.DecimalFromString(*cmd.ConcentratedLiquidityParameters.LeverageAtLowerBound); err != nil { errs.AddForProperty("amend_amm.concentrated_liquidity_parameters.leverage_at_lower_bound", ErrIsNotValidNumber) } else if leverage.LessThan(num.DecimalZero()) { diff --git a/commands/amend_amm_test.go b/commands/amend_amm_test.go index 0041efb44c9..9c7b9caf2bf 100644 --- a/commands/amend_amm_test.go +++ b/commands/amend_amm_test.go @@ -270,6 +270,43 @@ func TestCheckAmendAMM(t *testing.T) { }, errStr: "* (no updates provided)", }, + { + submission: commandspb.AmendAMM{ + MarketId: "e9982447fb4128f9968f9981612c5ea85d19b62058ec2636efc812dcbbc745ca", + SlippageTolerance: "0.09", + CommitmentAmount: ptr.From("10000"), + ConcentratedLiquidityParameters: &commandspb.AmendAMM_ConcentratedLiquidityParameters{ + Base: "20000", + }, + }, + errStr: "amend_amm.concentrated_liquidity_parameters.lower_bound (lower_bound and upper_bound cannot both be empty)", + }, + { + submission: commandspb.AmendAMM{ + MarketId: "e9982447fb4128f9968f9981612c5ea85d19b62058ec2636efc812dcbbc745ca", + SlippageTolerance: "0.09", + CommitmentAmount: ptr.From("10000"), + ConcentratedLiquidityParameters: &commandspb.AmendAMM_ConcentratedLiquidityParameters{ + LowerBound: ptr.From("10000"), + Base: "20000", + UpperBound: ptr.From("15000"), + }, + }, + errStr: "amend_amm.concentrated_liquidity_parameters.base (should be a smaller value than upper_bound)", + }, + { + submission: commandspb.AmendAMM{ + MarketId: "e9982447fb4128f9968f9981612c5ea85d19b62058ec2636efc812dcbbc745ca", + SlippageTolerance: "0.09", + CommitmentAmount: ptr.From("10000"), + ConcentratedLiquidityParameters: &commandspb.AmendAMM_ConcentratedLiquidityParameters{ + LowerBound: ptr.From("25000"), + Base: "20000", + UpperBound: ptr.From("30000"), + }, + }, + errStr: "amend_amm.concentrated_liquidity_parameters.base (should be a bigger value than lower_bound)", + }, { submission: commandspb.AmendAMM{ MarketId: "e9982447fb4128f9968f9981612c5ea85d19b62058ec2636efc812dcbbc745ca", diff --git a/core/execution/amm/engine.go b/core/execution/amm/engine.go index 29600c0cb7f..ac75b11aee5 100644 --- a/core/execution/amm/engine.go +++ b/core/execution/amm/engine.go @@ -143,6 +143,7 @@ type Engine struct { ammParties map[string]string minCommitmentQuantum *num.Uint + maxCalculationLevels *num.Uint } func New( @@ -247,6 +248,14 @@ func (e *Engine) OnMinCommitmentQuantumUpdate(ctx context.Context, c *num.Uint) e.minCommitmentQuantum = c.Clone() } +func (e *Engine) OnMaxCalculationLevelsUpdate(ctx context.Context, c *num.Uint) { + e.maxCalculationLevels = c.Clone() + + for _, p := range e.poolsCpy { + p.maxCalculationLevels = e.maxCalculationLevels.Clone() + } +} + // 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. func (e *Engine) OnMTM(ctx context.Context) { rm := []string{} @@ -444,23 +453,11 @@ func (e *Engine) submit(active []*Pool, agg *types.Order, inner, outer *num.Uint logging.String("side", types.OtherSide(agg.Side).String()), ) - // construct the orders - o := &types.Order{ - ID: e.idgen.NextID(), - MarketID: p.market, - Party: p.AMMParty, - Size: volume, - Remaining: volume, - Price: price, - Side: types.OtherSide(agg.Side), - TimeInForce: types.OrderTimeInForceFOK, - Type: types.OrderTypeMarket, - CreatedAt: agg.CreatedAt, - Status: types.OrderStatusFilled, - Reference: "vamm-" + p.AMMParty, - GeneratedOffbook: true, - } - o.OriginalPrice, _ = num.UintFromDecimal(o.Price.ToDecimal().Div(e.priceFactor)) + // construct an order + o := p.makeOrder(volume, price, types.OtherSide(agg.Side), e.idgen) + + // fill in extra details + o.CreatedAt = agg.CreatedAt orders = append(orders, o) p.updateEphemeralPosition(o) @@ -507,6 +504,11 @@ func (e *Engine) partition(agg *types.Order, inner, outer *num.Uint) ([]*Pool, [ continue } + // stop early trying to trade with itself, can happens during auction uncrossing + if agg.Party == p.AMMParty { + continue + } + // not active in range if its the pool's curves are wholly outside of [inner, outer] if (inner != nil && p.upper.high.LT(inner)) || (outer != nil && p.lower.low.GT(outer)) { continue @@ -652,6 +654,7 @@ func (e *Engine) Create( slippage, e.priceFactor, e.positionFactor, + e.maxCalculationLevels, ) if err != nil { e.broker.Send( @@ -665,10 +668,10 @@ func (e *Engine) Create( return nil, err } - e.log.Debug("AMM created for market", + e.log.Debug("AMM created", logging.String("owner", submit.Party), - logging.String("marketID", e.marketID), logging.String("poolID", pool.ID), + logging.String("marketID", e.marketID), ) return pool, nil } @@ -677,17 +680,19 @@ func (e *Engine) Create( func (e *Engine) Confirm( ctx context.Context, pool *Pool, -) error { - e.log.Debug("AMM added for market", +) { + e.log.Debug("AMM confirmed", logging.String("owner", pool.owner), logging.String("marketID", e.marketID), logging.String("poolID", pool.ID), ) + pool.status = types.AMMPoolStatusActive + pool.maxCalculationLevels = e.maxCalculationLevels + e.add(pool) e.sendUpdate(ctx, pool) e.parties.AssignDeriveKey(types.PartyID(pool.owner), pool.AMMParty) - return nil } // 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. @@ -717,7 +722,11 @@ func (e *Engine) Amend( if err != nil { return nil, nil, err } - + e.log.Debug("AMM amended", + logging.String("owner", amend.Party), + logging.String("marketID", e.marketID), + logging.String("poolID", pool.ID), + ) return updated, pool, nil } @@ -746,6 +755,11 @@ func (e *Engine) CancelAMM( pool.status = types.AMMPoolStatusCancelled e.remove(ctx, cancel.Party) + e.log.Debug("AMM cancelled", + logging.String("owner", cancel.Party), + logging.String("poolID", pool.ID), + logging.String("marketID", e.marketID), + ) return closeout, nil } @@ -907,6 +921,36 @@ func (e *Engine) GetAMMPoolsBySubAccount() map[string]common.AMMPool { return ret } +// OrderbookShape expands all registered AMM's into orders between the given prices. If `ammParty` is supplied then just the pool +// with that party id is expanded. +func (e *Engine) OrderbookShape(st, nd *num.Uint, ammParty *string) ([]*types.Order, []*types.Order) { + if ammParty == nil { + // no party give, expand all registered + buys, sells := []*types.Order{}, []*types.Order{} + for _, p := range e.poolsCpy { + b, s := p.OrderbookShape(st, nd, e.idgen) + buys = append(buys, b...) + sells = append(sells, s...) + } + return buys, sells + } + + // asked to expand just one AMM, lets find it, first amm-party -> owning party + owner, ok := e.ammParties[*ammParty] + if !ok { + return nil, nil + } + + // now owning party -> pool + p, ok := e.pools[owner] + if !ok { + return nil, nil + } + + // expand it + return p.OrderbookShape(st, nd, e.idgen) +} + func (e *Engine) GetAllSubAccounts() []string { ret := make([]string, 0, len(e.ammParties)) for _, subAccount := range e.ammParties { @@ -915,6 +959,14 @@ func (e *Engine) GetAllSubAccounts() []string { return ret } +// GetAMMParty returns the AMM's key given the owners key. +func (e *Engine) GetAMMParty(party string) (string, error) { + if p, ok := e.pools[party]; ok { + return p.AMMParty, nil + } + return "", ErrNoPoolMatchingParty +} + func (e *Engine) add(p *Pool) { e.pools[p.owner] = p e.poolsCpy = append(e.poolsCpy, p) diff --git a/core/execution/amm/engine_test.go b/core/execution/amm/engine_test.go index a0e52a40028..4a23477c2af 100644 --- a/core/execution/amm/engine_test.go +++ b/core/execution/amm/engine_test.go @@ -568,7 +568,7 @@ func whenAMMIsSubmitted(t *testing.T, tst *tstEngine, submission *types.SubmitAM ctx := context.Background() pool, err := tst.engine.Create(ctx, submission, vgcrypto.RandomHash(), riskFactors, scalingFactors, slippage) require.NoError(t, err) - require.NoError(t, tst.engine.Confirm(ctx, pool)) + tst.engine.Confirm(ctx, pool) } func getParty(t *testing.T, tst *tstEngine) (string, string) { diff --git a/core/execution/amm/pool.go b/core/execution/amm/pool.go index 94cf2c8ced1..41ec912a1de 100644 --- a/core/execution/amm/pool.go +++ b/core/execution/amm/pool.go @@ -18,6 +18,7 @@ package amm import ( "fmt" + "code.vegaprotocol.io/vega/core/idgeneration" "code.vegaprotocol.io/vega/core/types" "code.vegaprotocol.io/vega/libs/num" snapshotpb "code.vegaprotocol.io/vega/protos/vega/snapshot/v1" @@ -29,10 +30,11 @@ type ephemeralPosition struct { } type curve struct { - l num.Decimal // virtual liquidity - high *num.Uint // high price value, upper bound if upper curve, base price is lower curve - low *num.Uint // low price value, base price if upper curve, lower bound if lower curve - empty bool // if true the curve is of zero length and represents no liquidity on this side of the amm + l num.Decimal // virtual liquidity + high *num.Uint // high price value, upper bound if upper curve, base price is lower curve + low *num.Uint // low price value, base price if upper curve, lower bound if lower curve + empty bool // if true the curve is of zero length and represents no liquidity on this side of the amm + isLower bool // whether the curve is for the lower curve or the upper curve // the theoretical position of the curve at its lower boundary // note that this equals Vega's position at the boundary only in the lower curve, since Vega position == curve-position @@ -60,6 +62,19 @@ func (c *curve) volumeBetweenPrices(sqrt sqrtFn, st, nd *num.Uint) uint64 { return volume.Uint64() } +// positionAtPrice returns the position of the AMM if its fair-price were the given price. This +// will be signed for long/short as usual. +func (c *curve) positionAtPrice(sqrt sqrtFn, price *num.Uint) int64 { + pos := impliedPosition(sqrt(price), sqrt(c.high), c.l) + if c.isLower { + return int64(pos.Uint64()) + } + + // if we are in the upper curve the position of 0 in "curve-space" is -cu.pv in Vega position + // so we need to flip the interval + return -c.pv.Sub(pos.ToDecimal()).IntPart() +} + type Pool struct { ID string AMMParty string @@ -91,8 +106,8 @@ type Pool struct { // for the same incoming order, the second round of generated orders are priced as if the first round had traded. eph *ephemeralPosition - // one price tick - oneTick *num.Uint + maxCalculationLevels *num.Uint // maximum number of price levels the AMM will be expanded into + oneTick *num.Uint // one price tick } func NewPool( @@ -108,24 +123,26 @@ func NewPool( linearSlippage num.Decimal, priceFactor num.Decimal, positionFactor num.Decimal, + maxCalculationLevels *num.Uint, ) (*Pool, error) { oneTick, _ := num.UintFromDecimal(num.DecimalOne().Mul(priceFactor)) pool := &Pool{ - ID: id, - AMMParty: ammParty, - Commitment: submit.CommitmentAmount, - ProposedFee: submit.ProposedFee, - Parameters: submit.Parameters, - market: submit.MarketID, - owner: submit.Party, - asset: asset, - sqrt: sqrt, - collateral: collateral, - position: position, - priceFactor: priceFactor, - positionFactor: positionFactor, - oneTick: oneTick, - status: types.AMMPoolStatusActive, + ID: id, + AMMParty: ammParty, + Commitment: submit.CommitmentAmount, + ProposedFee: submit.ProposedFee, + Parameters: submit.Parameters, + market: submit.MarketID, + owner: submit.Party, + asset: asset, + sqrt: sqrt, + collateral: collateral, + position: position, + priceFactor: priceFactor, + positionFactor: positionFactor, + oneTick: oneTick, + status: types.AMMPoolStatusActive, + maxCalculationLevels: maxCalculationLevels, } err := pool.setCurves(rf, sf, linearSlippage) if err != nil { @@ -186,6 +203,7 @@ func NewPoolFromProto( } lowerCu, err := NewCurveFromProto(state.Lower) + lowerCu.isLower = true if err != nil { return nil, err } @@ -301,21 +319,22 @@ func (p *Pool) Update( } updated := &Pool{ - ID: p.ID, - AMMParty: p.AMMParty, - Commitment: commitment, - ProposedFee: proposedFee, - Parameters: parameters, - asset: p.asset, - market: p.market, - owner: p.owner, - collateral: p.collateral, - position: p.position, - priceFactor: p.priceFactor, - positionFactor: p.positionFactor, - status: types.AMMPoolStatusActive, - sqrt: p.sqrt, - oneTick: p.oneTick, + ID: p.ID, + AMMParty: p.AMMParty, + Commitment: commitment, + ProposedFee: proposedFee, + Parameters: parameters, + asset: p.asset, + market: p.market, + owner: p.owner, + collateral: p.collateral, + position: p.position, + priceFactor: p.priceFactor, + positionFactor: p.positionFactor, + status: types.AMMPoolStatusActive, + sqrt: p.sqrt, + oneTick: p.oneTick, + maxCalculationLevels: p.maxCalculationLevels, } if err := updated.setCurves(rf, sf, linearSlippage); err != nil { return nil, err @@ -407,10 +426,11 @@ func generateCurve( // and finally calculate L = pv * Lu return &curve{ - l: pv.Mul(lu), - low: low, - high: high, - pv: pv, + l: pv.Mul(lu), + low: low, + high: high, + pv: pv, + isLower: isLower, } } @@ -488,49 +508,135 @@ func impliedPosition(sqrtPrice, sqrtHigh num.Decimal, l num.Decimal) *num.Uint { // OrderbookShape returns slices of virtual buy and sell orders that the AMM has over a given range // and is essentially a view on the AMM's personal order-book. -func (p *Pool) OrderbookShape(from, to *num.Uint) ([]*types.Order, []*types.Order) { - buys := []*types.Order{} - sells := []*types.Order{} +func (p *Pool) OrderbookShape(from, to *num.Uint, idgen *idgeneration.IDGenerator) ([]*types.Order, []*types.Order) { + buys, sells := []*types.Order{}, []*types.Order{} + + lower := p.lower.low + upper := p.upper.high + fairPrice := p.fairPrice() + + if p.closing() { + // 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 + pos := p.getPosition() + if pos == 0 { + // pool is closed and we're waiting for the next MTM to close, so it has no orders + return nil, nil + } - if from == nil { - from = p.lower.low + if pos > 0 { + // only orders between fair-price -> base + lower = fairPrice.Clone() + upper = p.lower.high.Clone() + } else { + // only orders between base -> fair-price + upper = fairPrice.Clone() + lower = p.lower.high.Clone() + } } - if to == nil { - to = p.upper.high + + if from.GT(upper) || to.LT(lower) { + return nil, nil } - // any volume strictly below the fair price will be a buy, and volume above will be a sell side := types.SideBuy - fairPrice := p.fairPrice() + + // cap the range to the pool's bounds, there will be no orders outside of this + from = num.Max(from, lower) + to = num.Min(to, upper) + + switch { + case from.GT(fairPrice): + // if we are expanding entirely in the sell range to calculate the order at price `from` + // we need to ask the AMM for volume in the range `from - 1 -> from` so we simply + // sub one here to cover than. + side = types.SideSell + from.Sub(from, p.oneTick) + case to.LT(fairPrice): + // if we are expanding entirely in the buy range to calculate the order at price `to` + // we need to ask the AMM for volume in the range `to -> to + 1` so we simply + // add one here to cover than. + to.Add(to, p.oneTick) + case from.EQ(fairPrice): + // if we are starting the expansion at the fair-price all orders will be sells + side = types.SideSell + } + + var approx bool + step := p.oneTick.Clone() + delta, _ := num.UintZero().Delta(from, to) + delta.Div(delta, p.oneTick) + + // if there are too many price levels across `from -> to` we have to approximate the orderbook + // shape using steps larger than tick size + if delta.GT(p.maxCalculationLevels) { + step.Div(delta, p.maxCalculationLevels) + step.Mul(step, p.oneTick) + approx = true + } ordersFromCurve := func(cu *curve, from, to *num.Uint) { + if cu.empty { + return + } + from = num.Max(from, cu.low) to = num.Min(to, cu.high) - price := from - for price.LT(to) { - next := num.UintZero().AddSum(price, p.oneTick) - volume := cu.volumeBetweenPrices(p.sqrt, price, next) - if side == types.SideBuy && next.GT(fairPrice) { - // now switch to sells, we're over the fair-price now + // quick check on whether its possibly that we might step over the AMM's fair-price + // it can only happen if the fair-price is *not* at the curve bounds + canSplit := fairPrice.NEQ(cu.low) && fairPrice.NEQ(cu.high) + + // the price we have currently stepped to and the position of the AMM at that price + current := from + position := cu.positionAtPrice(p.sqrt, current) + + for current.LT(to) && current.LT(cu.high) { + // take the next step + next := num.UintZero().AddSum(current, step) + + if side == types.SideBuy && next.GT(fairPrice) && canSplit { + // we are in "approximation" mode with a step bigger than a tick and have stepped over the AMM's + // fair-price. We need to split this step into two, a buy order from current -> fp, and a sell + // from fp -> next + volume := uint64(num.DeltaV(position, 0)) + price := p.priceForVolumeAtPosition(volume, types.OtherSide(side), 0, fairPrice) + buys = append(buys, p.makeOrder(volume, price, side, idgen)) + + // we've step through fair-price now so orders will becomes sells side = types.SideSell + current = fairPrice + position = 0 } - order := &types.Order{ - Size: volume, - Side: side, - Price: price.Clone(), - } + nextPosition := cu.positionAtPrice(p.sqrt, num.Min(next, cu.high)) + volume := uint64(num.DeltaV(position, nextPosition)) if side == types.SideBuy { + price := current + if approx { + price = p.priceForVolumeAtPosition(volume, types.OtherSide(side), nextPosition, next) + } + order := p.makeOrder(volume, price, side, idgen) buys = append(buys, order) } else { + price := next + if approx { + price = p.priceForVolumeAtPosition(volume, types.OtherSide(side), position, current) + } + order := p.makeOrder(volume, price, side, idgen) sells = append(sells, order) } - price = next + // if we're calculating buys and we hit fair price, switch to sells + if side == types.SideBuy && next.GTE(fairPrice) { + side = types.SideSell + } + + current = next + position = nextPosition } } + ordersFromCurve(p.lower, from, to) ordersFromCurve(p.upper, from, to) return buys, sells @@ -538,7 +644,18 @@ func (p *Pool) OrderbookShape(from, to *num.Uint) ([]*types.Order, []*types.Orde // PriceForVolume returns the price the AMM is willing to trade at to match with the given volume of an incoming order. func (p *Pool) PriceForVolume(volume uint64, side types.Side) *num.Uint { - x, y := p.virtualBalances(p.getPosition(), p.fairPrice(), side) + return p.priceForVolumeAtPosition( + volume, + side, + p.getPosition(), + p.fairPrice(), + ) +} + +// priceForVolumeAtPosition returns the price the AMM is willing to trade at to match with the given volume if its position and fair-price +// are as given. +func (p *Pool) priceForVolumeAtPosition(volume uint64, side types.Side, pos int64, fp *num.Uint) *num.Uint { + x, y := p.virtualBalances(pos, fp, side) // dy = x*y / (x - dx) - y // where y and x are the balances on either side of the pool, and dx is the change in volume @@ -846,3 +963,25 @@ func (p *Pool) canTrade(side types.Side) bool { } return false } + +func (p *Pool) makeOrder(volume uint64, price *num.Uint, side types.Side, idgen *idgeneration.IDGenerator) *types.Order { + order := &types.Order{ + MarketID: p.market, + Party: p.AMMParty, + Size: volume, + Remaining: volume, + Price: price, + Side: side, + TimeInForce: types.OrderTimeInForceGTC, + Type: types.OrderTypeLimit, + Status: types.OrderStatusFilled, + Reference: "vamm-" + p.AMMParty, + GeneratedOffbook: true, + } + order.OriginalPrice, _ = num.UintFromDecimal(order.Price.ToDecimal().Div(p.priceFactor)) + + if idgen != nil { + order.ID = idgen.NextID() + } + return order +} diff --git a/core/execution/amm/pool_test.go b/core/execution/amm/pool_test.go index a9252d9fc83..0714c230530 100644 --- a/core/execution/amm/pool_test.go +++ b/core/execution/amm/pool_test.go @@ -16,6 +16,7 @@ package amm import ( + "strconv" "testing" "code.vegaprotocol.io/vega/core/events" @@ -27,6 +28,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAMMPool(t *testing.T) { @@ -37,6 +39,16 @@ func TestAMMPool(t *testing.T) { t.Run("test near zero volume curve triggers and error", testNearZeroCurveErrors) } +func TestOrderbookShape(t *testing.T) { + t.Run("test orderbook shape when AMM is 0", testOrderbookShapeZeroPosition) + t.Run("test orderbook shape when AMM is long", testOrderbookShapeLong) + t.Run("test orderbook shape when AMM is short", testOrderbookShapeShort) + t.Run("test orderbook shape when calculations are capped", testOrderbookShapeLimited) + t.Run("test orderbook shape step over fair price", testOrderbookShapeStepOverFairPrice) + t.Run("test orderbook shape step fair price at boundary", testOrderbookShapeNoStepOverFairPrice) + t.Run("test orderbook shape AMM reduce only", testOrderbookShapeReduceOnly) +} + func testTradeableVolumeInRange(t *testing.T) { p := newTestPool(t) defer p.ctrl.Finish() @@ -290,6 +302,280 @@ func testNearZeroCurveErrors(t *testing.T) { assert.NoError(t, err) } +func testOrderbookShapeZeroPosition(t *testing.T) { + p := newTestPoolWithRanges(t, num.NewUint(7), num.NewUint(10), num.NewUint(13)) + defer p.ctrl.Finish() + + low := p.submission.Parameters.LowerBound + base := p.submission.Parameters.Base + high := p.submission.Parameters.UpperBound + + // when range [7, 10] expect orders at prices (7, 8, 9) + // there will be no order at price 10 since that is the pools fair-price and it quotes +/-1 eitherside + ensurePosition(t, p.pos, 0, num.UintZero()) + buys, sells := p.pool.OrderbookShape(low, base, nil) + assertOrderPrices(t, buys, types.SideBuy, 7, 9) + assert.Equal(t, 0, len(sells)) + + // when range [7, 9] expect orders at prices (7, 8, 9) + ensurePosition(t, p.pos, 0, num.UintZero()) + buys, sells = p.pool.OrderbookShape(low, num.NewUint(9), nil) + assertOrderPrices(t, buys, types.SideBuy, 7, 9) + assert.Equal(t, 0, len(sells)) + + // when range [10, 13] expect orders at prices (11, 12, 13) + // there will be no order at price 10 since that is the pools fair-price and it quotes +/-1 eitherside + ensurePosition(t, p.pos, 0, num.UintZero()) + buys, sells = p.pool.OrderbookShape(base, high, nil) + assert.Equal(t, 0, len(buys)) + assertOrderPrices(t, sells, types.SideSell, 11, 13) + + // when range [11, 13] expect orders at prices (11, 12, 13) + ensurePosition(t, p.pos, 0, num.UintZero()) + buys, sells = p.pool.OrderbookShape(num.NewUint(11), high, nil) + assert.Equal(t, 0, len(buys)) + assertOrderPrices(t, sells, types.SideSell, 11, 13) + + // whole range from [7, 10] will have buys (7, 8, 9) and sells (11, 12, 13) + ensurePosition(t, p.pos, 0, num.UintZero()) + buys, sells = p.pool.OrderbookShape(low, high, nil) + assertOrderPrices(t, buys, types.SideBuy, 7, 9) + assertOrderPrices(t, sells, types.SideSell, 11, 13) + + // mid both curves spanning buys and sells, range from [8, 12] will have buys (8, 9) and sells (11, 12) + ensurePosition(t, p.pos, 0, num.UintZero()) + buys, sells = p.pool.OrderbookShape(num.NewUint(8), num.NewUint(12), nil) + assertOrderPrices(t, buys, types.SideBuy, 8, 9) + assertOrderPrices(t, sells, types.SideSell, 11, 12) + + // range (8, 8) should return a single buy order at price 8, which is a bit counter intuitive + ensurePosition(t, p.pos, 0, num.UintZero()) + buys, sells = p.pool.OrderbookShape(num.NewUint(8), num.NewUint(8), nil) + assertOrderPrices(t, buys, types.SideBuy, 8, 8) + assert.Equal(t, 0, len(sells)) + + // range (10, 10) should return only the orders at the fair-price, which is 0 orders + ensurePosition(t, p.pos, 0, num.UintZero()) + buys, sells = p.pool.OrderbookShape(num.NewUint(10), num.NewUint(10), nil) + assert.Equal(t, 0, len(buys)) + assert.Equal(t, 0, len(sells)) +} + +func testOrderbookShapeLong(t *testing.T) { + p := newTestPoolWithRanges(t, num.NewUint(7), num.NewUint(10), num.NewUint(13)) + defer p.ctrl.Finish() + + low := p.submission.Parameters.LowerBound + base := p.submission.Parameters.Base + high := p.submission.Parameters.UpperBound + + // AMM is long and will have a fair-price of 8 + position := int64(17980) + ensurePosition(t, p.pos, position, num.UintZero()) + require.Equal(t, "8", p.pool.BestPrice(nil).String()) + + // range [7, 10] with have buy order (7) and sell orders (9, 10) + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells := p.pool.OrderbookShape(low, base, nil) + assertOrderPrices(t, buys, types.SideBuy, 7, 7) + assertOrderPrices(t, sells, types.SideSell, 9, 10) + + // range [10, 13] with have sell orders (10, 11, 12, 13) + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells = p.pool.OrderbookShape(base, high, nil) + assert.Equal(t, 0, len(buys)) + assertOrderPrices(t, sells, types.SideSell, 10, 13) + + // whole range will have buys at (7) and sells at (9, 10, 11, 12, 13) + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells = p.pool.OrderbookShape(low, high, nil) + assertOrderPrices(t, buys, types.SideBuy, 7, 7) + assertOrderPrices(t, sells, types.SideSell, 9, 13) + + // query at fair price returns no orders + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells = p.pool.OrderbookShape(num.NewUint(8), num.NewUint(8), nil) + assert.Equal(t, 0, len(buys)) + assert.Equal(t, 0, len(sells)) +} + +func testOrderbookShapeShort(t *testing.T) { + p := newTestPoolWithRanges(t, num.NewUint(7), num.NewUint(10), num.NewUint(13)) + defer p.ctrl.Finish() + + low := p.submission.Parameters.LowerBound + base := p.submission.Parameters.Base + high := p.submission.Parameters.UpperBound + + // AMM is short and will have a fair-price of 12 + position := int64(-20000) + ensurePosition(t, p.pos, position, num.UintZero()) + require.Equal(t, "12", p.pool.BestPrice(nil).String()) + + // range [7, 10] with have buy order (7,8,9,10) + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells := p.pool.OrderbookShape(low, base, nil) + assertOrderPrices(t, buys, types.SideBuy, 7, 10) + assert.Equal(t, 0, len(sells)) + + // range [10, 13] with have buy orders (10, 11) and sell orders (13) + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells = p.pool.OrderbookShape(base, high, nil) + assertOrderPrices(t, buys, types.SideBuy, 10, 11) + assertOrderPrices(t, sells, types.SideSell, 13, 13) + + // whole range will have buys at (7,8,9,10,11) and sells at (13) + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells = p.pool.OrderbookShape(low, high, nil) + assertOrderPrices(t, buys, types.SideBuy, 7, 11) + assertOrderPrices(t, sells, types.SideSell, 13, 13) + + // query at fair price returns no orders + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells = p.pool.OrderbookShape(num.NewUint(12), num.NewUint(12), nil) + assert.Equal(t, 0, len(buys)) + assert.Equal(t, 0, len(sells)) +} + +func testOrderbookShapeLimited(t *testing.T) { + p := newTestPoolWithRanges(t, num.NewUint(20), num.NewUint(40), num.NewUint(60)) + defer p.ctrl.Finish() + + low := p.submission.Parameters.LowerBound + base := p.submission.Parameters.Base + high := p.submission.Parameters.UpperBound + + // position is zero but we're capping max calculations at ~10 + position := int64(0) + p.pool.maxCalculationLevels = num.NewUint(10) + + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells := p.pool.OrderbookShape(low, base, nil) + assert.Equal(t, 10, len(buys)) + assert.Equal(t, 0, len(sells)) + + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells = p.pool.OrderbookShape(base, high, nil) + assert.Equal(t, 0, len(buys)) + assert.Equal(t, 10, len(sells)) + + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells = p.pool.OrderbookShape(low, high, nil) + assert.Equal(t, 5, len(buys)) + assert.Equal(t, 5, len(sells)) +} + +func testOrderbookShapeStepOverFairPrice(t *testing.T) { + p := newTestPoolWithRanges(t, num.NewUint(20), num.NewUint(40), num.NewUint(60)) + defer p.ctrl.Finish() + + low := p.submission.Parameters.LowerBound + base := p.submission.Parameters.Base + high := p.submission.Parameters.UpperBound + + // make levels of 10 makes the step price 2, and this position gives the pool a fair price of 25 + // when we take the step from 24 -> 26 we want to make sure we split that order into two, so we + // will actually do maxCalculationLevels + 1 calculations but I think thats fine and keeps the calculations + // simple + position := int64(7000) + p.pool.maxCalculationLevels = num.NewUint(10) + ensurePosition(t, p.pos, position, num.UintZero()) + require.Equal(t, "25", p.pool.BestPrice(nil).String()) + + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells := p.pool.OrderbookShape(low, base, nil) + assert.Equal(t, 3, len(buys)) + assert.Equal(t, 8, len(sells)) + + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells = p.pool.OrderbookShape(base, high, nil) + assert.Equal(t, 0, len(buys)) + assert.Equal(t, 11, len(sells)) + + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells = p.pool.OrderbookShape(low, high, nil) + assert.Equal(t, 2, len(buys)) + assert.Equal(t, 9, len(sells)) +} + +func testOrderbookShapeNoStepOverFairPrice(t *testing.T) { + p := newTestPoolWithRanges(t, num.NewUint(20), num.NewUint(40), num.NewUint(60)) + defer p.ctrl.Finish() + + low := p.submission.Parameters.LowerBound + base := p.submission.Parameters.Base + high := p.submission.Parameters.UpperBound + + position := int64(0) + p.pool.maxCalculationLevels = num.NewUint(6) + + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells := p.pool.OrderbookShape(low, base, nil) + assert.Equal(t, 7, len(buys)) + assert.Equal(t, 0, len(sells)) + + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells = p.pool.OrderbookShape(base, high, nil) + assert.Equal(t, 0, len(buys)) + assert.Equal(t, 7, len(sells)) + + ensurePosition(t, p.pos, position, num.UintZero()) + buys, sells = p.pool.OrderbookShape(low, high, nil) + assert.Equal(t, 4, len(buys)) + assert.Equal(t, 4, len(sells)) +} + +func testOrderbookShapeReduceOnly(t *testing.T) { + p := newTestPoolWithRanges(t, num.NewUint(7), num.NewUint(10), num.NewUint(13)) + defer p.ctrl.Finish() + + low := p.submission.Parameters.LowerBound + base := p.submission.Parameters.Base + high := p.submission.Parameters.UpperBound + + // pool is reduce only so will not have any orders above/below fair price depending on position + p.pool.status = types.AMMPoolStatusReduceOnly + + // AMM is position 0 it will have no orders + position := int64(0) + ensurePositionN(t, p.pos, position, num.UintZero(), 2) + buys, sells := p.pool.OrderbookShape(low, base, nil) + assert.Equal(t, 0, len(buys)) + assert.Equal(t, 0, len(sells)) + + // AMM is long and will have a fair-price of 8 and so will only have orders from 8 -> base + position = int64(17980) + ensurePosition(t, p.pos, position, num.UintZero()) + require.Equal(t, "8", p.pool.BestPrice(nil).String()) + + // range [7, 13] will have only sellf orders (9, 10) + ensurePositionN(t, p.pos, position, num.UintZero(), 2) + buys, sells = p.pool.OrderbookShape(low, high, nil) + assert.Equal(t, 0, len(buys)) + assertOrderPrices(t, sells, types.SideSell, 9, 10) + + // AMM is short and will have a fair-price of 12 + position = int64(-20000) + ensurePosition(t, p.pos, position, num.UintZero()) + require.Equal(t, "12", p.pool.BestPrice(nil).String()) + + // range [10, 13] with have buy orders (10, 11) + ensurePositionN(t, p.pos, position, num.UintZero(), 2) + buys, sells = p.pool.OrderbookShape(base, high, nil) + assertOrderPrices(t, buys, types.SideBuy, 10, 11) + assert.Equal(t, 0, len(sells)) +} + +func assertOrderPrices(t *testing.T, orders []*types.Order, side types.Side, st, nd int) { + t.Helper() + require.Equal(t, nd-st+1, len(orders)) + for i, o := range orders { + price := st + i + assert.Equal(t, side, o.Side) + assert.Equal(t, strconv.FormatInt(int64(price), 10), o.Price.String()) + } +} + func newBasicPoolWithSubmit(t *testing.T, submit *types.SubmitAMM) (*Pool, error) { t.Helper() ctrl := gomock.NewController(t) @@ -316,6 +602,7 @@ func newBasicPoolWithSubmit(t *testing.T, submit *types.SubmitAMM) (*Pool, error num.DecimalZero(), num.DecimalOne(), num.DecimalOne(), + num.NewUint(10000), ) } @@ -417,10 +704,11 @@ func TestNotebook(t *testing.T) { } type tstPool struct { - pool *Pool - col *mocks.MockCollateral - pos *mocks.MockPosition - ctrl *gomock.Controller + pool *Pool + col *mocks.MockCollateral + pos *mocks.MockPosition + ctrl *gomock.Controller + submission *types.SubmitAMM } func newTestPool(t *testing.T) *tstPool { @@ -481,14 +769,16 @@ func newTestPoolWithOpts(t *testing.T, positionFactor num.Decimal, low, base, hi num.DecimalZero(), num.DecimalOne(), positionFactor, + num.NewUint(100000), ) assert.NoError(t, err) return &tstPool{ - pool: pool, - col: col, - pos: pos, - ctrl: ctrl, + submission: submit, + pool: pool, + col: col, + pos: pos, + ctrl: ctrl, } } diff --git a/core/execution/common/interfaces.go b/core/execution/common/interfaces.go index fb221baf72d..e9d06d4e866 100644 --- a/core/execution/common/interfaces.go +++ b/core/execution/common/interfaces.go @@ -23,6 +23,7 @@ import ( dscommon "code.vegaprotocol.io/vega/core/datasource/common" "code.vegaprotocol.io/vega/core/datasource/spec" "code.vegaprotocol.io/vega/core/events" + "code.vegaprotocol.io/vega/core/idgeneration" "code.vegaprotocol.io/vega/core/liquidity/v2" "code.vegaprotocol.io/vega/core/monitor/price" "code.vegaprotocol.io/vega/core/risk" @@ -326,7 +327,7 @@ type EquityLikeShares interface { } type AMMPool interface { - OrderbookShape(from, to *num.Uint) ([]*types.Order, []*types.Order) + OrderbookShape(from, to *num.Uint, idgen *idgeneration.IDGenerator) ([]*types.Order, []*types.Order) LiquidityFee() num.Decimal CommitmentAmount() *num.Uint } diff --git a/core/execution/common/liquidity_provision_fees.go b/core/execution/common/liquidity_provision_fees.go index d7ffdc0be38..31a8324ca52 100644 --- a/core/execution/common/liquidity_provision_fees.go +++ b/core/execution/common/liquidity_provision_fees.go @@ -152,7 +152,7 @@ func (m *MarketLiquidity) updateAMMCommitment(count int64) { } bb, ba := num.DecimalFromUint(bestB), num.DecimalFromUint(bestA) for amm, pool := range m.amm.GetAMMPoolsBySubAccount() { - buy, sell := pool.OrderbookShape(minP, maxP) + buy, sell := pool.OrderbookShape(minP, maxP, nil) buyTotal, sellTotal := num.UintZero(), num.UintZero() for _, b := range buy { size := num.UintFromUint64(b.Size) diff --git a/core/execution/common/mocks_amm/mocks.go b/core/execution/common/mocks_amm/mocks.go index a846fbd9965..056c5f94abf 100644 --- a/core/execution/common/mocks_amm/mocks.go +++ b/core/execution/common/mocks_amm/mocks.go @@ -8,6 +8,7 @@ import ( reflect "reflect" common "code.vegaprotocol.io/vega/core/execution/common" + idgeneration "code.vegaprotocol.io/vega/core/idgeneration" types "code.vegaprotocol.io/vega/core/types" num "code.vegaprotocol.io/vega/libs/num" gomock "github.com/golang/mock/gomock" @@ -66,18 +67,18 @@ func (mr *MockAMMPoolMockRecorder) LiquidityFee() *gomock.Call { } // OrderbookShape mocks base method. -func (m *MockAMMPool) OrderbookShape(arg0, arg1 *num.Uint) ([]*types.Order, []*types.Order) { +func (m *MockAMMPool) OrderbookShape(arg0, arg1 *num.Uint, arg2 *idgeneration.IDGenerator) ([]*types.Order, []*types.Order) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "OrderbookShape", arg0, arg1) + ret := m.ctrl.Call(m, "OrderbookShape", arg0, arg1, arg2) ret0, _ := ret[0].([]*types.Order) ret1, _ := ret[1].([]*types.Order) return ret0, ret1 } // OrderbookShape indicates an expected call of OrderbookShape. -func (mr *MockAMMPoolMockRecorder) OrderbookShape(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockAMMPoolMockRecorder) OrderbookShape(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrderbookShape", reflect.TypeOf((*MockAMMPool)(nil).OrderbookShape), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrderbookShape", reflect.TypeOf((*MockAMMPool)(nil).OrderbookShape), arg0, arg1, arg2) } // MockAMM is a mock of AMM interface. diff --git a/core/execution/engine.go b/core/execution/engine.go index 582b31e890a..66f08eba4f8 100644 --- a/core/execution/engine.go +++ b/core/execution/engine.go @@ -116,74 +116,6 @@ type Engine struct { skipRestoreSuccessors map[string]struct{} } -type netParamsValues struct { - feeDistributionTimeStep time.Duration - marketValueWindowLength time.Duration - suppliedStakeToObligationFactor num.Decimal - infrastructureFee num.Decimal - makerFee num.Decimal - scalingFactors *types.ScalingFactors - maxLiquidityFee num.Decimal - bondPenaltyFactor num.Decimal - auctionMinDuration time.Duration - auctionMaxDuration time.Duration - probabilityOfTradingTauScaling num.Decimal - minProbabilityOfTradingLPOrders num.Decimal - minLpStakeQuantumMultiple num.Decimal - marketCreationQuantumMultiple num.Decimal - markPriceUpdateMaximumFrequency time.Duration - internalCompositePriceUpdateFrequency time.Duration - marketPartiesMaximumStopOrdersUpdate *num.Uint - - // Liquidity version 2. - liquidityV2BondPenaltyFactor num.Decimal - liquidityV2EarlyExitPenalty num.Decimal - liquidityV2MaxLiquidityFee num.Decimal - liquidityV2SLANonPerformanceBondPenaltyMax num.Decimal - liquidityV2SLANonPerformanceBondPenaltySlope num.Decimal - liquidityV2StakeToCCYVolume num.Decimal - liquidityV2ProvidersFeeCalculationTimeStep time.Duration - liquidityELSFeeFraction num.Decimal - - // only used for protocol upgrade to v0.74 - chainID uint64 - - ammCommitmentQuantum *num.Uint -} - -func defaultNetParamsValues() netParamsValues { - return netParamsValues{ - feeDistributionTimeStep: -1, - marketValueWindowLength: -1, - suppliedStakeToObligationFactor: num.DecimalFromInt64(-1), - infrastructureFee: num.DecimalFromInt64(-1), - makerFee: num.DecimalFromInt64(-1), - scalingFactors: nil, - maxLiquidityFee: num.DecimalFromInt64(-1), - bondPenaltyFactor: num.DecimalFromInt64(-1), - - auctionMinDuration: -1, - probabilityOfTradingTauScaling: num.DecimalFromInt64(-1), - minProbabilityOfTradingLPOrders: num.DecimalFromInt64(-1), - minLpStakeQuantumMultiple: num.DecimalFromInt64(-1), - marketCreationQuantumMultiple: num.DecimalFromInt64(-1), - markPriceUpdateMaximumFrequency: 5 * time.Second, // default is 5 seconds, should come from net params though - internalCompositePriceUpdateFrequency: 5 * time.Second, - marketPartiesMaximumStopOrdersUpdate: num.UintZero(), - - // Liquidity version 2. - liquidityV2BondPenaltyFactor: num.DecimalFromInt64(-1), - liquidityV2EarlyExitPenalty: num.DecimalFromInt64(-1), - liquidityV2MaxLiquidityFee: num.DecimalFromInt64(-1), - liquidityV2SLANonPerformanceBondPenaltyMax: num.DecimalFromInt64(-1), - liquidityV2SLANonPerformanceBondPenaltySlope: num.DecimalFromInt64(-1), - liquidityV2StakeToCCYVolume: num.DecimalFromInt64(-1), - liquidityV2ProvidersFeeCalculationTimeStep: time.Second * 5, - - ammCommitmentQuantum: num.UintZero(), - } -} - // NewEngine takes stores and engines and returns // a new execution engine to process new orders, etc. func NewEngine( @@ -863,149 +795,6 @@ func (e *Engine) submitSpotMarket(ctx context.Context, marketConfig *types.Marke return nil } -func (e *Engine) propagateSpotInitialNetParams(ctx context.Context, mkt *spot.Market, isRestore bool) error { - if !e.npv.minLpStakeQuantumMultiple.Equal(num.DecimalFromInt64(-1)) { - mkt.OnMarketMinLpStakeQuantumMultipleUpdate(ctx, e.npv.minLpStakeQuantumMultiple) - } - if e.npv.auctionMinDuration != -1 { - mkt.OnMarketAuctionMinimumDurationUpdate(ctx, e.npv.auctionMinDuration) - } - if e.npv.auctionMaxDuration > 0 { - mkt.OnMarketAuctionMaximumDurationUpdate(ctx, e.npv.auctionMaxDuration) - } - if !e.npv.infrastructureFee.Equal(num.DecimalFromInt64(-1)) { - mkt.OnFeeFactorsInfrastructureFeeUpdate(ctx, e.npv.infrastructureFee) - } - - if !e.npv.makerFee.Equal(num.DecimalFromInt64(-1)) { - mkt.OnFeeFactorsMakerFeeUpdate(ctx, e.npv.makerFee) - } - - if e.npv.marketValueWindowLength != -1 { - mkt.OnMarketValueWindowLengthUpdate(e.npv.marketValueWindowLength) - } - - if e.npv.markPriceUpdateMaximumFrequency > 0 { - mkt.OnMarkPriceUpdateMaximumFrequency(ctx, e.npv.markPriceUpdateMaximumFrequency) - } - - if !e.npv.liquidityV2EarlyExitPenalty.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck - mkt.OnMarketLiquidityV2EarlyExitPenaltyUpdate(e.npv.liquidityV2EarlyExitPenalty) - } - - if !e.npv.liquidityV2MaxLiquidityFee.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck - mkt.OnMarketLiquidityV2MaximumLiquidityFeeFactorLevelUpdate(e.npv.liquidityV2MaxLiquidityFee) - } - - if !e.npv.liquidityV2SLANonPerformanceBondPenaltySlope.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck - mkt.OnMarketLiquidityV2SLANonPerformanceBondPenaltySlopeUpdate(e.npv.liquidityV2SLANonPerformanceBondPenaltySlope) - } - - if !e.npv.liquidityV2SLANonPerformanceBondPenaltyMax.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck - mkt.OnMarketLiquidityV2SLANonPerformanceBondPenaltyMaxUpdate(e.npv.liquidityV2SLANonPerformanceBondPenaltyMax) - } - - if !e.npv.liquidityV2StakeToCCYVolume.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck - mkt.OnMarketLiquidityV2StakeToCCYVolume(e.npv.liquidityV2StakeToCCYVolume) - } - - mkt.OnMarketPartiesMaximumStopOrdersUpdate(ctx, e.npv.marketPartiesMaximumStopOrdersUpdate) - - e.propagateSLANetParams(ctx, mkt, isRestore) - - if !e.npv.liquidityELSFeeFraction.IsZero() { - mkt.OnMarketLiquidityEquityLikeShareFeeFractionUpdate(e.npv.liquidityELSFeeFraction) - } - return nil -} - -func (e *Engine) propagateInitialNetParamsToFutureMarket(ctx context.Context, mkt *future.Market, isRestore bool) error { - if !e.npv.probabilityOfTradingTauScaling.Equal(num.DecimalFromInt64(-1)) { - mkt.OnMarketProbabilityOfTradingTauScalingUpdate(ctx, e.npv.probabilityOfTradingTauScaling) - } - if !e.npv.minProbabilityOfTradingLPOrders.Equal(num.DecimalFromInt64(-1)) { - mkt.OnMarketMinProbabilityOfTradingLPOrdersUpdate(ctx, e.npv.minProbabilityOfTradingLPOrders) - } - if !e.npv.minLpStakeQuantumMultiple.Equal(num.DecimalFromInt64(-1)) { - mkt.OnMarketMinLpStakeQuantumMultipleUpdate(ctx, e.npv.minLpStakeQuantumMultiple) - } - if e.npv.auctionMinDuration != -1 { - mkt.OnMarketAuctionMinimumDurationUpdate(ctx, e.npv.auctionMinDuration) - } - if e.npv.auctionMaxDuration > 0 { - mkt.OnMarketAuctionMaximumDurationUpdate(ctx, e.npv.auctionMaxDuration) - } - - if !e.npv.infrastructureFee.Equal(num.DecimalFromInt64(-1)) { - mkt.OnFeeFactorsInfrastructureFeeUpdate(ctx, e.npv.infrastructureFee) - } - - if !e.npv.makerFee.Equal(num.DecimalFromInt64(-1)) { - mkt.OnFeeFactorsMakerFeeUpdate(ctx, e.npv.makerFee) - } - - if e.npv.scalingFactors != nil { - if err := mkt.OnMarginScalingFactorsUpdate(ctx, e.npv.scalingFactors); err != nil { - return err - } - } - - if e.npv.marketValueWindowLength != -1 { - mkt.OnMarketValueWindowLengthUpdate(e.npv.marketValueWindowLength) - } - - if !e.npv.maxLiquidityFee.Equal(num.DecimalFromInt64(-1)) { - mkt.OnMarketLiquidityMaximumLiquidityFeeFactorLevelUpdate(e.npv.maxLiquidityFee) - } - if e.npv.markPriceUpdateMaximumFrequency > 0 { - mkt.OnMarkPriceUpdateMaximumFrequency(ctx, e.npv.markPriceUpdateMaximumFrequency) - } - if e.npv.internalCompositePriceUpdateFrequency > 0 { - mkt.OnInternalCompositePriceUpdateFrequency(ctx, e.npv.internalCompositePriceUpdateFrequency) - } - if !e.npv.liquidityELSFeeFraction.IsZero() { - mkt.OnMarketLiquidityEquityLikeShareFeeFractionUpdate(e.npv.liquidityELSFeeFraction) - } - - mkt.OnMarketPartiesMaximumStopOrdersUpdate(ctx, e.npv.marketPartiesMaximumStopOrdersUpdate) - - mkt.OnAMMMinCommitmentQuantumUpdate(ctx, e.npv.ammCommitmentQuantum) - - e.propagateSLANetParams(ctx, mkt, isRestore) - - return nil -} - -func (e *Engine) propagateSLANetParams(_ context.Context, mkt common.CommonMarket, isRestore bool) { - if !e.npv.liquidityV2BondPenaltyFactor.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck - mkt.OnMarketLiquidityV2BondPenaltyFactorUpdate(e.npv.liquidityV2BondPenaltyFactor) - } - - if !e.npv.liquidityV2EarlyExitPenalty.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck - mkt.OnMarketLiquidityV2EarlyExitPenaltyUpdate(e.npv.liquidityV2EarlyExitPenalty) - } - - if !e.npv.liquidityV2MaxLiquidityFee.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck - mkt.OnMarketLiquidityV2MaximumLiquidityFeeFactorLevelUpdate(e.npv.liquidityV2MaxLiquidityFee) - } - - if !e.npv.liquidityV2SLANonPerformanceBondPenaltySlope.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck - mkt.OnMarketLiquidityV2SLANonPerformanceBondPenaltySlopeUpdate(e.npv.liquidityV2SLANonPerformanceBondPenaltySlope) - } - - if !e.npv.liquidityV2SLANonPerformanceBondPenaltyMax.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck - mkt.OnMarketLiquidityV2SLANonPerformanceBondPenaltyMaxUpdate(e.npv.liquidityV2SLANonPerformanceBondPenaltyMax) - } - - if !e.npv.liquidityV2StakeToCCYVolume.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck - mkt.OnMarketLiquidityV2StakeToCCYVolume(e.npv.liquidityV2StakeToCCYVolume) - } - - if !isRestore && e.npv.liquidityV2ProvidersFeeCalculationTimeStep != 0 { - mkt.OnMarketLiquidityV2ProvidersFeeCalculationTimeStep(e.npv.liquidityV2ProvidersFeeCalculationTimeStep) - } -} - func (e *Engine) removeMarket(mktID string) { e.log.Debug("removing market", logging.String("id", mktID)) delete(e.allMarkets, mktID) @@ -1607,353 +1396,6 @@ func (e *Engine) GetMarketData(mktID string) (types.MarketData, error) { return types.MarketData{}, types.ErrInvalidMarketID } -func (e *Engine) OnMarketAuctionMinimumDurationUpdate(ctx context.Context, d time.Duration) error { - for _, mkt := range e.allMarketsCpy { - mkt.OnMarketAuctionMinimumDurationUpdate(ctx, d) - } - e.npv.auctionMinDuration = d - return nil -} - -func (e *Engine) OnMarketAuctionMaximumDurationUpdate(ctx context.Context, d time.Duration) error { - for _, mkt := range e.allMarketsCpy { - if mkt.IsOpeningAuction() { - mkt.OnMarketAuctionMaximumDurationUpdate(ctx, d) - } - } - e.npv.auctionMaxDuration = d - return nil -} - -func (e *Engine) OnMarkPriceUpdateMaximumFrequency(ctx context.Context, d time.Duration) error { - for _, mkt := range e.allMarketsCpy { - mkt.OnMarkPriceUpdateMaximumFrequency(ctx, d) - } - e.npv.markPriceUpdateMaximumFrequency = d - return nil -} - -func (e *Engine) OnInternalCompositePriceUpdateFrequency(ctx context.Context, d time.Duration) error { - for _, mkt := range e.futureMarkets { - mkt.OnInternalCompositePriceUpdateFrequency(ctx, d) - } - e.npv.internalCompositePriceUpdateFrequency = d - return nil -} - -// OnMarketLiquidityV2BondPenaltyUpdate stores net param on execution engine and applies to markets at the start of new epoch. -func (e *Engine) OnMarketLiquidityV2BondPenaltyUpdate(_ context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update market liquidity bond penalty (liquidity v2)", - logging.Decimal("bond-penalty-factor", d), - ) - } - - // Set immediately during opening auction - for _, mkt := range e.allMarketsCpy { - if mkt.IsOpeningAuction() { - mkt.OnMarketLiquidityV2BondPenaltyFactorUpdate(d) - } - } - - e.npv.liquidityV2BondPenaltyFactor = d - return nil -} - -// OnMarketLiquidityV2EarlyExitPenaltyUpdate stores net param on execution engine and applies to markets -// at the start of new epoch. -func (e *Engine) OnMarketLiquidityV2EarlyExitPenaltyUpdate(_ context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update market liquidity early exit penalty (liquidity v2)", - logging.Decimal("early-exit-penalty", d), - ) - } - - // Set immediately during opening auction - for _, mkt := range e.allMarketsCpy { - if mkt.IsOpeningAuction() { - mkt.OnMarketLiquidityV2EarlyExitPenaltyUpdate(d) - } - } - - e.npv.liquidityV2EarlyExitPenalty = d - return nil -} - -// OnMarketLiquidityV2MaximumLiquidityFeeFactorLevelUpdate stores net param on execution engine and -// applies at the start of new epoch. -func (e *Engine) OnMarketLiquidityV2MaximumLiquidityFeeFactorLevelUpdate(_ context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update liquidity provision max liquidity fee factor (liquidity v2)", - logging.Decimal("max-liquidity-fee", d), - ) - } - - // Set immediately during opening auction - for _, mkt := range e.allMarketsCpy { - if mkt.IsOpeningAuction() { - mkt.OnMarketLiquidityV2MaximumLiquidityFeeFactorLevelUpdate(d) - } - } - - e.npv.liquidityV2MaxLiquidityFee = d - return nil -} - -// OnMarketLiquidityV2SLANonPerformanceBondPenaltySlopeUpdate stores net param on execution engine and applies to markets at the -// start of new epoch. -func (e *Engine) OnMarketLiquidityV2SLANonPerformanceBondPenaltySlopeUpdate(_ context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update market SLA non performance bond penalty slope (liquidity v2)", - logging.Decimal("bond-penalty-slope", d), - ) - } - - // Set immediately during opening auction - for _, mkt := range e.allMarketsCpy { - if mkt.IsOpeningAuction() { - mkt.OnMarketLiquidityV2SLANonPerformanceBondPenaltySlopeUpdate(d) - } - } - - e.npv.liquidityV2SLANonPerformanceBondPenaltySlope = d - return nil -} - -// OnMarketLiquidityV2SLANonPerformanceBondPenaltyMaxUpdate stores net param on execution engine and applies to markets -// at the start of new epoch. -func (e *Engine) OnMarketLiquidityV2SLANonPerformanceBondPenaltyMaxUpdate(_ context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update market SLA non performance bond penalty max (liquidity v2)", - logging.Decimal("bond-penalty-max", d), - ) - } - - for _, m := range e.futureMarketsCpy { - // Set immediately during opening auction - if m.IsOpeningAuction() { - m.OnMarketLiquidityV2SLANonPerformanceBondPenaltyMaxUpdate(d) - } - } - - e.npv.liquidityV2SLANonPerformanceBondPenaltyMax = d - return nil -} - -// OnMarketLiquidityV2StakeToCCYVolumeUpdate stores net param on execution engine and applies to markets -// at the start of new epoch. -func (e *Engine) OnMarketLiquidityV2StakeToCCYVolumeUpdate(_ context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update market stake to CCYVolume (liquidity v2)", - logging.Decimal("stake-to-ccy-volume", d), - ) - } - - for _, m := range e.futureMarketsCpy { - // Set immediately during opening auction - if m.IsOpeningAuction() { - m.OnMarketLiquidityV2StakeToCCYVolume(d) - } - } - - e.npv.liquidityV2StakeToCCYVolume = d - return nil -} - -// OnMarketLiquidityV2ProvidersFeeCalculationTimeStep stores net param on execution engine and applies to markets -// at the start of new epoch. -func (e *Engine) OnMarketLiquidityV2ProvidersFeeCalculationTimeStep(_ context.Context, d time.Duration) error { - if e.log.IsDebug() { - e.log.Debug("update market SLA providers fee calculation time step (liquidity v2)", - logging.Duration("providersFeeCalculationTimeStep", d), - ) - } - - for _, m := range e.allMarketsCpy { - // Set immediately during opening auction - if m.IsOpeningAuction() { - m.OnMarketLiquidityV2ProvidersFeeCalculationTimeStep(d) - } - } - - e.npv.liquidityV2ProvidersFeeCalculationTimeStep = d - return nil -} - -func (e *Engine) OnMarketMarginScalingFactorsUpdate(ctx context.Context, v interface{}) error { - if e.log.IsDebug() { - e.log.Debug("update market scaling factors", - logging.Reflect("scaling-factors", v), - ) - } - - pscalingFactors, ok := v.(*vega.ScalingFactors) - if !ok { - return errors.New("invalid types for Margin ScalingFactors") - } - scalingFactors := types.ScalingFactorsFromProto(pscalingFactors) - for _, mkt := range e.futureMarketsCpy { - if err := mkt.OnMarginScalingFactorsUpdate(ctx, scalingFactors); err != nil { - return err - } - } - e.npv.scalingFactors = scalingFactors - return nil -} - -func (e *Engine) OnMarketFeeFactorsMakerFeeUpdate(ctx context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update maker fee in market fee factors", - logging.Decimal("maker-fee", d), - ) - } - - for _, mkt := range e.allMarketsCpy { - mkt.OnFeeFactorsMakerFeeUpdate(ctx, d) - } - e.npv.makerFee = d - return nil -} - -func (e *Engine) OnMarketFeeFactorsInfrastructureFeeUpdate(ctx context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update infrastructure fee in market fee factors", - logging.Decimal("infrastructure-fee", d), - ) - } - for _, mkt := range e.allMarketsCpy { - mkt.OnFeeFactorsInfrastructureFeeUpdate(ctx, d) - } - e.npv.infrastructureFee = d - return nil -} - -func (e *Engine) OnMarketValueWindowLengthUpdate(_ context.Context, d time.Duration) error { - if e.log.IsDebug() { - e.log.Debug("update market value window length", - logging.Duration("window-length", d), - ) - } - - for _, mkt := range e.allMarketsCpy { - mkt.OnMarketValueWindowLengthUpdate(d) - } - e.npv.marketValueWindowLength = d - return nil -} - -// to be removed and replaced by its v2 counterpart. in use only for future. -func (e *Engine) OnMarketLiquidityMaximumLiquidityFeeFactorLevelUpdate(_ context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update liquidity provision max liquidity fee factor", - logging.Decimal("max-liquidity-fee", d), - ) - } - - for _, mkt := range e.futureMarketsCpy { - mkt.OnMarketLiquidityMaximumLiquidityFeeFactorLevelUpdate(d) - } - e.npv.maxLiquidityFee = d - - return nil -} - -func (e *Engine) OnMarketLiquidityEquityLikeShareFeeFractionUpdate(_ context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update market liquidity equityLikeShareFeeFraction", - logging.Decimal("market.liquidity.equityLikeShareFeeFraction", d), - ) - } - for _, mkt := range e.allMarketsCpy { - mkt.OnMarketLiquidityEquityLikeShareFeeFractionUpdate(d) - } - e.npv.liquidityELSFeeFraction = d - return nil -} - -func (e *Engine) OnMarketProbabilityOfTradingTauScalingUpdate(ctx context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update probability of trading tau scaling", - logging.Decimal("probability-of-trading-tau-scaling", d), - ) - } - for _, mkt := range e.allMarketsCpy { - mkt.OnMarketProbabilityOfTradingTauScalingUpdate(ctx, d) - } - e.npv.probabilityOfTradingTauScaling = d - return nil -} - -func (e *Engine) OnMarketMinProbabilityOfTradingForLPOrdersUpdate(ctx context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update min probability of trading tau scaling", - logging.Decimal("min-probability-of-trading-lp-orders", d), - ) - } - - for _, mkt := range e.allMarketsCpy { - mkt.OnMarketMinProbabilityOfTradingLPOrdersUpdate(ctx, d) - } - e.npv.minProbabilityOfTradingLPOrders = d - return nil -} - -func (e *Engine) OnMinLpStakeQuantumMultipleUpdate(ctx context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update min lp stake quantum multiple", - logging.Decimal("min-lp-stake-quantum-multiple", d), - ) - } - for _, mkt := range e.allMarketsCpy { - mkt.OnMarketMinLpStakeQuantumMultipleUpdate(ctx, d) - } - e.npv.minLpStakeQuantumMultiple = d - return nil -} - -func (e *Engine) OnMarketCreationQuantumMultipleUpdate(ctx context.Context, d num.Decimal) error { - if e.log.IsDebug() { - e.log.Debug("update market creation quantum multiple", - logging.Decimal("market-creation-quantum-multiple", d), - ) - } - e.npv.marketCreationQuantumMultiple = d - return nil -} - -func (e *Engine) OnMarketPartiesMaximumStopOrdersUpdate(ctx context.Context, u *num.Uint) error { - if e.log.IsDebug() { - e.log.Debug("update market parties maxiumum stop orders", - logging.BigUint("value", u), - ) - } - e.npv.marketPartiesMaximumStopOrdersUpdate = u - for _, mkt := range e.allMarketsCpy { - mkt.OnMarketPartiesMaximumStopOrdersUpdate(ctx, u) - } - return nil -} - -func (e *Engine) OnMaxPeggedOrderUpdate(ctx context.Context, max *num.Uint) error { - if e.log.IsDebug() { - e.log.Debug("update max pegged orders", - logging.Uint64("max-pegged-orders", max.Uint64()), - ) - } - e.maxPeggedOrders = max.Uint64() - return nil -} - -func (e *Engine) OnMarketAMMMinCommitmentQuantum(ctx context.Context, c *num.Uint) error { - if e.log.IsDebug() { - e.log.Debug("update amm min commitment quantum", - logging.BigUint("commitment-quantum", c), - ) - } - e.npv.ammCommitmentQuantum = c - return nil -} - func (e *Engine) MarketExists(market string) bool { _, ok := e.allMarkets[market] return ok diff --git a/core/execution/engine_netparams.go b/core/execution/engine_netparams.go new file mode 100644 index 00000000000..7b4e41aa7f6 --- /dev/null +++ b/core/execution/engine_netparams.go @@ -0,0 +1,602 @@ +// Copyright (C) 2023 Gobalsky Labs Limited +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package execution + +import ( + "context" + "errors" + "time" + + "code.vegaprotocol.io/vega/core/execution/common" + "code.vegaprotocol.io/vega/core/execution/future" + "code.vegaprotocol.io/vega/core/execution/spot" + "code.vegaprotocol.io/vega/core/types" + "code.vegaprotocol.io/vega/libs/num" + "code.vegaprotocol.io/vega/logging" + "code.vegaprotocol.io/vega/protos/vega" +) + +type netParamsValues struct { + feeDistributionTimeStep time.Duration + marketValueWindowLength time.Duration + suppliedStakeToObligationFactor num.Decimal + infrastructureFee num.Decimal + makerFee num.Decimal + scalingFactors *types.ScalingFactors + maxLiquidityFee num.Decimal + bondPenaltyFactor num.Decimal + auctionMinDuration time.Duration + auctionMaxDuration time.Duration + probabilityOfTradingTauScaling num.Decimal + minProbabilityOfTradingLPOrders num.Decimal + minLpStakeQuantumMultiple num.Decimal + marketCreationQuantumMultiple num.Decimal + markPriceUpdateMaximumFrequency time.Duration + internalCompositePriceUpdateFrequency time.Duration + marketPartiesMaximumStopOrdersUpdate *num.Uint + + // Liquidity version 2. + liquidityV2BondPenaltyFactor num.Decimal + liquidityV2EarlyExitPenalty num.Decimal + liquidityV2MaxLiquidityFee num.Decimal + liquidityV2SLANonPerformanceBondPenaltyMax num.Decimal + liquidityV2SLANonPerformanceBondPenaltySlope num.Decimal + liquidityV2StakeToCCYVolume num.Decimal + liquidityV2ProvidersFeeCalculationTimeStep time.Duration + liquidityELSFeeFraction num.Decimal + + // AMM + ammCommitmentQuantum *num.Uint + ammCalculationLevels *num.Uint + + // only used for protocol upgrade to v0.74 + chainID uint64 +} + +func defaultNetParamsValues() netParamsValues { + return netParamsValues{ + feeDistributionTimeStep: -1, + marketValueWindowLength: -1, + suppliedStakeToObligationFactor: num.DecimalFromInt64(-1), + infrastructureFee: num.DecimalFromInt64(-1), + makerFee: num.DecimalFromInt64(-1), + scalingFactors: nil, + maxLiquidityFee: num.DecimalFromInt64(-1), + bondPenaltyFactor: num.DecimalFromInt64(-1), + + auctionMinDuration: -1, + probabilityOfTradingTauScaling: num.DecimalFromInt64(-1), + minProbabilityOfTradingLPOrders: num.DecimalFromInt64(-1), + minLpStakeQuantumMultiple: num.DecimalFromInt64(-1), + marketCreationQuantumMultiple: num.DecimalFromInt64(-1), + markPriceUpdateMaximumFrequency: 5 * time.Second, // default is 5 seconds, should come from net params though + internalCompositePriceUpdateFrequency: 5 * time.Second, + marketPartiesMaximumStopOrdersUpdate: num.UintZero(), + + // Liquidity version 2. + liquidityV2BondPenaltyFactor: num.DecimalFromInt64(-1), + liquidityV2EarlyExitPenalty: num.DecimalFromInt64(-1), + liquidityV2MaxLiquidityFee: num.DecimalFromInt64(-1), + liquidityV2SLANonPerformanceBondPenaltyMax: num.DecimalFromInt64(-1), + liquidityV2SLANonPerformanceBondPenaltySlope: num.DecimalFromInt64(-1), + liquidityV2StakeToCCYVolume: num.DecimalFromInt64(-1), + liquidityV2ProvidersFeeCalculationTimeStep: time.Second * 5, + + ammCommitmentQuantum: num.UintZero(), + ammCalculationLevels: num.NewUint(100), + } +} + +func (e *Engine) OnMarketAuctionMinimumDurationUpdate(ctx context.Context, d time.Duration) error { + for _, mkt := range e.allMarketsCpy { + mkt.OnMarketAuctionMinimumDurationUpdate(ctx, d) + } + e.npv.auctionMinDuration = d + return nil +} + +func (e *Engine) OnMarketAuctionMaximumDurationUpdate(ctx context.Context, d time.Duration) error { + for _, mkt := range e.allMarketsCpy { + if mkt.IsOpeningAuction() { + mkt.OnMarketAuctionMaximumDurationUpdate(ctx, d) + } + } + e.npv.auctionMaxDuration = d + return nil +} + +func (e *Engine) OnMarkPriceUpdateMaximumFrequency(ctx context.Context, d time.Duration) error { + for _, mkt := range e.allMarketsCpy { + mkt.OnMarkPriceUpdateMaximumFrequency(ctx, d) + } + e.npv.markPriceUpdateMaximumFrequency = d + return nil +} + +func (e *Engine) OnInternalCompositePriceUpdateFrequency(ctx context.Context, d time.Duration) error { + for _, mkt := range e.futureMarkets { + mkt.OnInternalCompositePriceUpdateFrequency(ctx, d) + } + e.npv.internalCompositePriceUpdateFrequency = d + return nil +} + +// OnMarketLiquidityV2BondPenaltyUpdate stores net param on execution engine and applies to markets at the start of new epoch. +func (e *Engine) OnMarketLiquidityV2BondPenaltyUpdate(_ context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update market liquidity bond penalty (liquidity v2)", + logging.Decimal("bond-penalty-factor", d), + ) + } + + // Set immediately during opening auction + for _, mkt := range e.allMarketsCpy { + if mkt.IsOpeningAuction() { + mkt.OnMarketLiquidityV2BondPenaltyFactorUpdate(d) + } + } + + e.npv.liquidityV2BondPenaltyFactor = d + return nil +} + +// OnMarketLiquidityV2EarlyExitPenaltyUpdate stores net param on execution engine and applies to markets +// at the start of new epoch. +func (e *Engine) OnMarketLiquidityV2EarlyExitPenaltyUpdate(_ context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update market liquidity early exit penalty (liquidity v2)", + logging.Decimal("early-exit-penalty", d), + ) + } + + // Set immediately during opening auction + for _, mkt := range e.allMarketsCpy { + if mkt.IsOpeningAuction() { + mkt.OnMarketLiquidityV2EarlyExitPenaltyUpdate(d) + } + } + + e.npv.liquidityV2EarlyExitPenalty = d + return nil +} + +// OnMarketLiquidityV2MaximumLiquidityFeeFactorLevelUpdate stores net param on execution engine and +// applies at the start of new epoch. +func (e *Engine) OnMarketLiquidityV2MaximumLiquidityFeeFactorLevelUpdate(_ context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update liquidity provision max liquidity fee factor (liquidity v2)", + logging.Decimal("max-liquidity-fee", d), + ) + } + + // Set immediately during opening auction + for _, mkt := range e.allMarketsCpy { + if mkt.IsOpeningAuction() { + mkt.OnMarketLiquidityV2MaximumLiquidityFeeFactorLevelUpdate(d) + } + } + + e.npv.liquidityV2MaxLiquidityFee = d + return nil +} + +// OnMarketLiquidityV2SLANonPerformanceBondPenaltySlopeUpdate stores net param on execution engine and applies to markets at the +// start of new epoch. +func (e *Engine) OnMarketLiquidityV2SLANonPerformanceBondPenaltySlopeUpdate(_ context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update market SLA non performance bond penalty slope (liquidity v2)", + logging.Decimal("bond-penalty-slope", d), + ) + } + + // Set immediately during opening auction + for _, mkt := range e.allMarketsCpy { + if mkt.IsOpeningAuction() { + mkt.OnMarketLiquidityV2SLANonPerformanceBondPenaltySlopeUpdate(d) + } + } + + e.npv.liquidityV2SLANonPerformanceBondPenaltySlope = d + return nil +} + +// OnMarketLiquidityV2SLANonPerformanceBondPenaltyMaxUpdate stores net param on execution engine and applies to markets +// at the start of new epoch. +func (e *Engine) OnMarketLiquidityV2SLANonPerformanceBondPenaltyMaxUpdate(_ context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update market SLA non performance bond penalty max (liquidity v2)", + logging.Decimal("bond-penalty-max", d), + ) + } + + for _, m := range e.futureMarketsCpy { + // Set immediately during opening auction + if m.IsOpeningAuction() { + m.OnMarketLiquidityV2SLANonPerformanceBondPenaltyMaxUpdate(d) + } + } + + e.npv.liquidityV2SLANonPerformanceBondPenaltyMax = d + return nil +} + +// OnMarketLiquidityV2StakeToCCYVolumeUpdate stores net param on execution engine and applies to markets +// at the start of new epoch. +func (e *Engine) OnMarketLiquidityV2StakeToCCYVolumeUpdate(_ context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update market stake to CCYVolume (liquidity v2)", + logging.Decimal("stake-to-ccy-volume", d), + ) + } + + for _, m := range e.futureMarketsCpy { + // Set immediately during opening auction + if m.IsOpeningAuction() { + m.OnMarketLiquidityV2StakeToCCYVolume(d) + } + } + + e.npv.liquidityV2StakeToCCYVolume = d + return nil +} + +// OnMarketLiquidityV2ProvidersFeeCalculationTimeStep stores net param on execution engine and applies to markets +// at the start of new epoch. +func (e *Engine) OnMarketLiquidityV2ProvidersFeeCalculationTimeStep(_ context.Context, d time.Duration) error { + if e.log.IsDebug() { + e.log.Debug("update market SLA providers fee calculation time step (liquidity v2)", + logging.Duration("providersFeeCalculationTimeStep", d), + ) + } + + for _, m := range e.allMarketsCpy { + // Set immediately during opening auction + if m.IsOpeningAuction() { + m.OnMarketLiquidityV2ProvidersFeeCalculationTimeStep(d) + } + } + + e.npv.liquidityV2ProvidersFeeCalculationTimeStep = d + return nil +} + +func (e *Engine) OnMarketMarginScalingFactorsUpdate(ctx context.Context, v interface{}) error { + if e.log.IsDebug() { + e.log.Debug("update market scaling factors", + logging.Reflect("scaling-factors", v), + ) + } + + pscalingFactors, ok := v.(*vega.ScalingFactors) + if !ok { + return errors.New("invalid types for Margin ScalingFactors") + } + scalingFactors := types.ScalingFactorsFromProto(pscalingFactors) + for _, mkt := range e.futureMarketsCpy { + if err := mkt.OnMarginScalingFactorsUpdate(ctx, scalingFactors); err != nil { + return err + } + } + e.npv.scalingFactors = scalingFactors + return nil +} + +func (e *Engine) OnMarketFeeFactorsMakerFeeUpdate(ctx context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update maker fee in market fee factors", + logging.Decimal("maker-fee", d), + ) + } + + for _, mkt := range e.allMarketsCpy { + mkt.OnFeeFactorsMakerFeeUpdate(ctx, d) + } + e.npv.makerFee = d + return nil +} + +func (e *Engine) OnMarketFeeFactorsInfrastructureFeeUpdate(ctx context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update infrastructure fee in market fee factors", + logging.Decimal("infrastructure-fee", d), + ) + } + for _, mkt := range e.allMarketsCpy { + mkt.OnFeeFactorsInfrastructureFeeUpdate(ctx, d) + } + e.npv.infrastructureFee = d + return nil +} + +func (e *Engine) OnMarketValueWindowLengthUpdate(_ context.Context, d time.Duration) error { + if e.log.IsDebug() { + e.log.Debug("update market value window length", + logging.Duration("window-length", d), + ) + } + + for _, mkt := range e.allMarketsCpy { + mkt.OnMarketValueWindowLengthUpdate(d) + } + e.npv.marketValueWindowLength = d + return nil +} + +// to be removed and replaced by its v2 counterpart. in use only for future. +func (e *Engine) OnMarketLiquidityMaximumLiquidityFeeFactorLevelUpdate(_ context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update liquidity provision max liquidity fee factor", + logging.Decimal("max-liquidity-fee", d), + ) + } + + for _, mkt := range e.futureMarketsCpy { + mkt.OnMarketLiquidityMaximumLiquidityFeeFactorLevelUpdate(d) + } + e.npv.maxLiquidityFee = d + + return nil +} + +func (e *Engine) OnMarketLiquidityEquityLikeShareFeeFractionUpdate(_ context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update market liquidity equityLikeShareFeeFraction", + logging.Decimal("market.liquidity.equityLikeShareFeeFraction", d), + ) + } + for _, mkt := range e.allMarketsCpy { + mkt.OnMarketLiquidityEquityLikeShareFeeFractionUpdate(d) + } + e.npv.liquidityELSFeeFraction = d + return nil +} + +func (e *Engine) OnMarketProbabilityOfTradingTauScalingUpdate(ctx context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update probability of trading tau scaling", + logging.Decimal("probability-of-trading-tau-scaling", d), + ) + } + for _, mkt := range e.allMarketsCpy { + mkt.OnMarketProbabilityOfTradingTauScalingUpdate(ctx, d) + } + e.npv.probabilityOfTradingTauScaling = d + return nil +} + +func (e *Engine) OnMarketMinProbabilityOfTradingForLPOrdersUpdate(ctx context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update min probability of trading tau scaling", + logging.Decimal("min-probability-of-trading-lp-orders", d), + ) + } + + for _, mkt := range e.allMarketsCpy { + mkt.OnMarketMinProbabilityOfTradingLPOrdersUpdate(ctx, d) + } + e.npv.minProbabilityOfTradingLPOrders = d + return nil +} + +func (e *Engine) OnMinLpStakeQuantumMultipleUpdate(ctx context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update min lp stake quantum multiple", + logging.Decimal("min-lp-stake-quantum-multiple", d), + ) + } + for _, mkt := range e.allMarketsCpy { + mkt.OnMarketMinLpStakeQuantumMultipleUpdate(ctx, d) + } + e.npv.minLpStakeQuantumMultiple = d + return nil +} + +func (e *Engine) OnMarketCreationQuantumMultipleUpdate(ctx context.Context, d num.Decimal) error { + if e.log.IsDebug() { + e.log.Debug("update market creation quantum multiple", + logging.Decimal("market-creation-quantum-multiple", d), + ) + } + e.npv.marketCreationQuantumMultiple = d + return nil +} + +func (e *Engine) OnMarketPartiesMaximumStopOrdersUpdate(ctx context.Context, u *num.Uint) error { + if e.log.IsDebug() { + e.log.Debug("update market parties maxiumum stop orders", + logging.BigUint("value", u), + ) + } + e.npv.marketPartiesMaximumStopOrdersUpdate = u + for _, mkt := range e.allMarketsCpy { + mkt.OnMarketPartiesMaximumStopOrdersUpdate(ctx, u) + } + return nil +} + +func (e *Engine) OnMaxPeggedOrderUpdate(ctx context.Context, max *num.Uint) error { + if e.log.IsDebug() { + e.log.Debug("update max pegged orders", + logging.Uint64("max-pegged-orders", max.Uint64()), + ) + } + e.maxPeggedOrders = max.Uint64() + return nil +} + +func (e *Engine) OnMarketAMMMinCommitmentQuantum(ctx context.Context, c *num.Uint) error { + if e.log.IsDebug() { + e.log.Debug("update amm min commitment quantum", + logging.BigUint("commitment-quantum", c), + ) + } + e.npv.ammCommitmentQuantum = c + return nil +} + +func (e *Engine) OnMarketAMMMaxCalculationLevels(ctx context.Context, c *num.Uint) error { + if e.log.IsDebug() { + e.log.Debug("update amm max calculation levels", + logging.BigUint("ccalculation-levels", c), + ) + } + e.npv.ammCalculationLevels = c + return nil +} + +func (e *Engine) propagateSpotInitialNetParams(ctx context.Context, mkt *spot.Market, isRestore bool) error { + if !e.npv.minLpStakeQuantumMultiple.Equal(num.DecimalFromInt64(-1)) { + mkt.OnMarketMinLpStakeQuantumMultipleUpdate(ctx, e.npv.minLpStakeQuantumMultiple) + } + if e.npv.auctionMinDuration != -1 { + mkt.OnMarketAuctionMinimumDurationUpdate(ctx, e.npv.auctionMinDuration) + } + if e.npv.auctionMaxDuration > 0 { + mkt.OnMarketAuctionMaximumDurationUpdate(ctx, e.npv.auctionMaxDuration) + } + if !e.npv.infrastructureFee.Equal(num.DecimalFromInt64(-1)) { + mkt.OnFeeFactorsInfrastructureFeeUpdate(ctx, e.npv.infrastructureFee) + } + + if !e.npv.makerFee.Equal(num.DecimalFromInt64(-1)) { + mkt.OnFeeFactorsMakerFeeUpdate(ctx, e.npv.makerFee) + } + + if e.npv.marketValueWindowLength != -1 { + mkt.OnMarketValueWindowLengthUpdate(e.npv.marketValueWindowLength) + } + + if e.npv.markPriceUpdateMaximumFrequency > 0 { + mkt.OnMarkPriceUpdateMaximumFrequency(ctx, e.npv.markPriceUpdateMaximumFrequency) + } + + if !e.npv.liquidityV2EarlyExitPenalty.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck + mkt.OnMarketLiquidityV2EarlyExitPenaltyUpdate(e.npv.liquidityV2EarlyExitPenalty) + } + + if !e.npv.liquidityV2MaxLiquidityFee.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck + mkt.OnMarketLiquidityV2MaximumLiquidityFeeFactorLevelUpdate(e.npv.liquidityV2MaxLiquidityFee) + } + + if !e.npv.liquidityV2SLANonPerformanceBondPenaltySlope.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck + mkt.OnMarketLiquidityV2SLANonPerformanceBondPenaltySlopeUpdate(e.npv.liquidityV2SLANonPerformanceBondPenaltySlope) + } + + if !e.npv.liquidityV2SLANonPerformanceBondPenaltyMax.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck + mkt.OnMarketLiquidityV2SLANonPerformanceBondPenaltyMaxUpdate(e.npv.liquidityV2SLANonPerformanceBondPenaltyMax) + } + + if !e.npv.liquidityV2StakeToCCYVolume.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck + mkt.OnMarketLiquidityV2StakeToCCYVolume(e.npv.liquidityV2StakeToCCYVolume) + } + + mkt.OnMarketPartiesMaximumStopOrdersUpdate(ctx, e.npv.marketPartiesMaximumStopOrdersUpdate) + + e.propagateSLANetParams(ctx, mkt, isRestore) + + if !e.npv.liquidityELSFeeFraction.IsZero() { + mkt.OnMarketLiquidityEquityLikeShareFeeFractionUpdate(e.npv.liquidityELSFeeFraction) + } + return nil +} + +func (e *Engine) propagateInitialNetParamsToFutureMarket(ctx context.Context, mkt *future.Market, isRestore bool) error { + if !e.npv.probabilityOfTradingTauScaling.Equal(num.DecimalFromInt64(-1)) { + mkt.OnMarketProbabilityOfTradingTauScalingUpdate(ctx, e.npv.probabilityOfTradingTauScaling) + } + if !e.npv.minProbabilityOfTradingLPOrders.Equal(num.DecimalFromInt64(-1)) { + mkt.OnMarketMinProbabilityOfTradingLPOrdersUpdate(ctx, e.npv.minProbabilityOfTradingLPOrders) + } + if !e.npv.minLpStakeQuantumMultiple.Equal(num.DecimalFromInt64(-1)) { + mkt.OnMarketMinLpStakeQuantumMultipleUpdate(ctx, e.npv.minLpStakeQuantumMultiple) + } + if e.npv.auctionMinDuration != -1 { + mkt.OnMarketAuctionMinimumDurationUpdate(ctx, e.npv.auctionMinDuration) + } + if e.npv.auctionMaxDuration > 0 { + mkt.OnMarketAuctionMaximumDurationUpdate(ctx, e.npv.auctionMaxDuration) + } + + if !e.npv.infrastructureFee.Equal(num.DecimalFromInt64(-1)) { + mkt.OnFeeFactorsInfrastructureFeeUpdate(ctx, e.npv.infrastructureFee) + } + + if !e.npv.makerFee.Equal(num.DecimalFromInt64(-1)) { + mkt.OnFeeFactorsMakerFeeUpdate(ctx, e.npv.makerFee) + } + + if e.npv.scalingFactors != nil { + if err := mkt.OnMarginScalingFactorsUpdate(ctx, e.npv.scalingFactors); err != nil { + return err + } + } + + if e.npv.marketValueWindowLength != -1 { + mkt.OnMarketValueWindowLengthUpdate(e.npv.marketValueWindowLength) + } + + if !e.npv.maxLiquidityFee.Equal(num.DecimalFromInt64(-1)) { + mkt.OnMarketLiquidityMaximumLiquidityFeeFactorLevelUpdate(e.npv.maxLiquidityFee) + } + if e.npv.markPriceUpdateMaximumFrequency > 0 { + mkt.OnMarkPriceUpdateMaximumFrequency(ctx, e.npv.markPriceUpdateMaximumFrequency) + } + if e.npv.internalCompositePriceUpdateFrequency > 0 { + mkt.OnInternalCompositePriceUpdateFrequency(ctx, e.npv.internalCompositePriceUpdateFrequency) + } + if !e.npv.liquidityELSFeeFraction.IsZero() { + mkt.OnMarketLiquidityEquityLikeShareFeeFractionUpdate(e.npv.liquidityELSFeeFraction) + } + + mkt.OnMarketPartiesMaximumStopOrdersUpdate(ctx, e.npv.marketPartiesMaximumStopOrdersUpdate) + + mkt.OnAMMMinCommitmentQuantumUpdate(ctx, e.npv.ammCommitmentQuantum) + mkt.OnMarketAMMMaxCalculationLevels(ctx, e.npv.ammCalculationLevels) + + e.propagateSLANetParams(ctx, mkt, isRestore) + + return nil +} + +func (e *Engine) propagateSLANetParams(_ context.Context, mkt common.CommonMarket, isRestore bool) { + if !e.npv.liquidityV2BondPenaltyFactor.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck + mkt.OnMarketLiquidityV2BondPenaltyFactorUpdate(e.npv.liquidityV2BondPenaltyFactor) + } + + if !e.npv.liquidityV2EarlyExitPenalty.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck + mkt.OnMarketLiquidityV2EarlyExitPenaltyUpdate(e.npv.liquidityV2EarlyExitPenalty) + } + + if !e.npv.liquidityV2MaxLiquidityFee.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck + mkt.OnMarketLiquidityV2MaximumLiquidityFeeFactorLevelUpdate(e.npv.liquidityV2MaxLiquidityFee) + } + + if !e.npv.liquidityV2SLANonPerformanceBondPenaltySlope.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck + mkt.OnMarketLiquidityV2SLANonPerformanceBondPenaltySlopeUpdate(e.npv.liquidityV2SLANonPerformanceBondPenaltySlope) + } + + if !e.npv.liquidityV2SLANonPerformanceBondPenaltyMax.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck + mkt.OnMarketLiquidityV2SLANonPerformanceBondPenaltyMaxUpdate(e.npv.liquidityV2SLANonPerformanceBondPenaltyMax) + } + + if !e.npv.liquidityV2StakeToCCYVolume.Equal(num.DecimalFromInt64(-1)) { //nolint:staticcheck + mkt.OnMarketLiquidityV2StakeToCCYVolume(e.npv.liquidityV2StakeToCCYVolume) + } + + if !isRestore && e.npv.liquidityV2ProvidersFeeCalculationTimeStep != 0 { + mkt.OnMarketLiquidityV2ProvidersFeeCalculationTimeStep(e.npv.liquidityV2ProvidersFeeCalculationTimeStep) + } +} diff --git a/core/execution/future/market.go b/core/execution/future/market.go index ac97285e5f9..cd21c18bb64 100644 --- a/core/execution/future/market.go +++ b/core/execution/future/market.go @@ -1604,6 +1604,14 @@ func (m *Market) uncrossOnLeaveAuction(ctx context.Context) ([]*types.OrderConfi } } + // if the uncrossed order was generated by an AMM then register its position as if it was submitted + order := uncrossedOrder.Order + if order.GeneratedOffbook { + cpy := order.Clone() + cpy.Remaining = cpy.Size // remaining will be 0 since it has traded, so we copy it back to its full size to register + m.position.RegisterOrder(ctx, cpy) + } + // then do the confirmation m.handleConfirmation(ctx, uncrossedOrder, nil) @@ -5364,7 +5372,9 @@ func (m *Market) SubmitAMM(ctx context.Context, submit *types.SubmitAMM, determi // if a rebase is not necessary we're done, just confirm with the amm-engine if order == nil { - return m.amm.Confirm(ctx, pool) + m.amm.Confirm(ctx, pool) + m.matching.UpdateAMM(pool.AMMParty) + return nil } if conf, _, err := m.submitValidatedOrder(ctx, order); err != nil || len(conf.Trades) == 0 { @@ -5384,7 +5394,11 @@ func (m *Market) SubmitAMM(ctx context.Context, submit *types.SubmitAMM, determi return err } - return m.amm.Confirm(ctx, pool) + // rebase successful so confirm the pool with the engine + m.amm.Confirm(ctx, pool) + // now tell the matching engine something new has appeared incase it needs to update its auction IPV cache + m.matching.UpdateAMM(pool.AMMParty) + return nil } func (m *Market) AmendAMM(ctx context.Context, amend *types.AmendAMM, deterministicID string) error { @@ -5422,7 +5436,9 @@ func (m *Market) AmendAMM(ctx context.Context, amend *types.AmendAMM, determinis } if order == nil { - return m.amm.Confirm(ctx, pool) + m.amm.Confirm(ctx, pool) + m.matching.UpdateAMM(pool.AMMParty) + return nil } conf, _, err := m.submitValidatedOrder(ctx, order) @@ -5437,15 +5453,25 @@ func (m *Market) AmendAMM(ctx context.Context, amend *types.AmendAMM, determinis return err } - return m.amm.Confirm(ctx, pool) + m.amm.Confirm(ctx, pool) + m.matching.UpdateAMM(pool.AMMParty) + return nil } func (m *Market) CancelAMM(ctx context.Context, cancel *types.CancelAMM, deterministicID string) error { + ammParty, err := m.amm.GetAMMParty(cancel.Party) + if err != nil { + return err + } + closeout, err := m.amm.CancelAMM(ctx, cancel) if err != nil { return err } + // tell matching incase it needs to remove the AMM's contribution to the IPV cache + m.matching.UpdateAMM(ammParty) + if closeout == nil { return nil } diff --git a/core/execution/future/market_callbacks.go b/core/execution/future/market_callbacks.go index 5d98ef140f1..66708056109 100644 --- a/core/execution/future/market_callbacks.go +++ b/core/execution/future/market_callbacks.go @@ -24,6 +24,10 @@ import ( "code.vegaprotocol.io/vega/libs/num" ) +func (m *Market) OnMarketAMMMaxCalculationLevels(ctx context.Context, c *num.Uint) { + m.amm.OnMaxCalculationLevelsUpdate(ctx, c) +} + func (m *Market) OnAMMMinCommitmentQuantumUpdate(ctx context.Context, c *num.Uint) { m.amm.OnMinCommitmentQuantumUpdate(ctx, c) } diff --git a/core/integration/features/amm/0090-VAMM-006-014.feature b/core/integration/features/amm/0090-VAMM-006-014.feature index b5d54f0c84b..e3bdc4eb6b6 100644 --- a/core/integration/features/amm/0090-VAMM-006-014.feature +++ b/core/integration/features/amm/0090-VAMM-006-014.feature @@ -133,9 +133,11 @@ Feature: Ensure the vAMM positions follow the market correctly | party4 | -350 | 0 | 0 | | | vamm1-id | 350 | 0 | 0 | true | - @VAMM + @VAMM2 Scenario: 0090-VAMM-008: If other traders trade to move the market mid price to 150 the vAMM will post no further sell orders above this price, and the vAMM's position notional value will be equal to 4x its total account balance. - #When the network moves ahead "1" epochs + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | mid price | static mid price | best offer price | best bid price | + | 100 | TRADING_MODE_CONTINUOUS | 100 | 100 | 101 | 99 | When the parties place the following orders: | party | market id | side | volume | price | resulting trades | type | tif | | party4 | ETH/MAR22 | buy | 500 | 155 | 1 | TYPE_LIMIT | TIF_GTC | @@ -145,7 +147,7 @@ Feature: Ensure the vAMM positions follow the market correctly | party4 | 122 | 291 | vamm1-id | true | And the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 100 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 100 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 155 | # trying to trade again causes no trades because the AMM has no more volume When the parties place the following orders: @@ -155,7 +157,7 @@ Feature: Ensure the vAMM positions follow the market correctly # the AMM's mid price has moved to 150, but it has no volume +150 so that best offer comes from the orderbook of 160 Then the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 100 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 100 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 155 | When the network moves ahead "1" blocks Then the parties should have the following profit and loss: @@ -165,7 +167,7 @@ Feature: Ensure the vAMM positions follow the market correctly # Notional value therefore is 317 * 122 And the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 122 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 122 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 155 | # vAMM receives fees, but loses out in the MTM settlement And the following transfers should happen: @@ -476,7 +478,7 @@ Feature: Ensure the vAMM positions follow the market correctly | party5 | ETH/MAR22 | buy | 65 | 120 | 1 | TYPE_LIMIT | TIF_GTC | Then the market data for the market "ETH/MAR22" should be: | mark price | trading mode | ref price | mid price | static mid price | best offer price | best bid price | - | 95 | TRADING_MODE_CONTINUOUS | 100 | 120 | 120 | 121 | 119 | + | 95 | TRADING_MODE_CONTINUOUS | 100 | 120 | 120 | 121 | 120 | And the following trades should be executed: | buyer | price | size | seller | is amm | | party5 | 114 | 64 | vamm1-id | true | diff --git a/core/integration/features/amm/0090-VAMM-016.feature b/core/integration/features/amm/0090-VAMM-016.feature index ea16f5263e9..ab66ba3eb57 100644 --- a/core/integration/features/amm/0090-VAMM-016.feature +++ b/core/integration/features/amm/0090-VAMM-016.feature @@ -90,7 +90,7 @@ Feature: vAMM has the same ELS as liquidity provision with the same commitment a When the parties submit the following liquidity provision: # Using 9788 instead of exactly 10,000 makes things easier because getting exactly 10,000 from an AMM pool as virtual stake can be tricky due to complex math. | id | party | market id | commitment amount | fee | lp type | - | lp_2 | lp2 | ETH/MAR22 | 9788 | 0.03 | submission | + | lp_2 | lp2 | ETH/MAR22 | 9884 | 0.03 | submission | When the parties submit the following AMM: | party | market id | amount | slippage | base | lower bound | upper bound | lower leverage | upper leverage | proposed fee | @@ -110,8 +110,8 @@ Feature: vAMM has the same ELS as liquidity provision with the same commitment a And the current epoch is "2" And the liquidity provider fee shares for the market "ETH/MAR22" should be: | party | equity like share | virtual stake | average entry valuation | - | lp2 | 0.3309440086556668 | 9788.0000000000000000 | 29576 | - | 137112507e25d3845a56c47db15d8ced0f28daa8498a0fd52648969c4b296aba | 0.3309440086556668 | 9788.0000000000000000 | 19788 | + | lp2 | 0.3320343993550121 | 9884.0000000000000000 | 29768 | + | 137112507e25d3845a56c47db15d8ced0f28daa8498a0fd52648969c4b296aba | 0.3320343993550121 | 9884.0000000000000000 | 19884 | @VAMM Scenario: 0090-VAMM-017: A vAMM's virtual ELS should be equal to the ELS of a regular LP with the same committed volume on the book (i.e. if a vAMM has an average volume on each side of the book across the epoch of 10k USDT, their ELS should be equal to that of a regular LP who has a commitment which requires supplying 10k USDT who joined at the same time as them). @@ -123,7 +123,7 @@ Feature: vAMM has the same ELS as liquidity provision with the same commitment a When the parties submit the following liquidity provision: # Using 10,093 instead of exactly 10,000 makes things easier because getting exactly 10,000 from an AMM pool as virtual stake can be tricky due to complex math. | id | party | market id | commitment amount | fee | lp type | - | lp_2 | lp2 | ETH/MAR22 | 9788 | 0.03 | submission | + | lp_2 | lp2 | ETH/MAR22 | 9884 | 0.03 | submission | And the parties place the following orders: | party | market id | side | volume | price | resulting trades | type | tif | @@ -148,13 +148,13 @@ Feature: vAMM has the same ELS as liquidity provision with the same commitment a And the current epoch is "2" And the liquidity provider fee shares for the market "ETH/MAR22" should be: | party | equity like share | virtual stake | average entry valuation | - | lp2 | 0.3309440086556668 | 9788.0000000000000000 | 29576 | - | 137112507e25d3845a56c47db15d8ced0f28daa8498a0fd52648969c4b296aba | 0.3309440086556668 | 9788.0000000000000000 | 19788 | + | lp2 | 0.3320343993550121 | 9884.0000000000000000 | 29768 | + | 137112507e25d3845a56c47db15d8ced0f28daa8498a0fd52648969c4b296aba | 0.3320343993550121 | 9884.0000000000000000 | 19884 | Then the network moves ahead "2" epochs And the current epoch is "4" And the liquidity provider fee shares for the market "ETH/MAR22" should be: - | party | equity like share | average entry valuation | - | lp2 | 0.3309440086556668 | 29576 | - | 137112507e25d3845a56c47db15d8ced0f28daa8498a0fd52648969c4b296aba | 0.3309440086556668 | 19788 | \ No newline at end of file + | party | equity like share | virtual stake | average entry valuation | + | lp2 | 0.3320343993550121 | 9884.0000000000000000 | 29768 | + | 137112507e25d3845a56c47db15d8ced0f28daa8498a0fd52648969c4b296aba | 0.3320343993550121 | 9884.0000000000000000 | 19884 | \ No newline at end of file diff --git a/core/integration/features/amm/0090-VAMM-019.feature b/core/integration/features/amm/0090-VAMM-019.feature index 57ad7b820ae..6d4d5aa4afd 100644 --- a/core/integration/features/amm/0090-VAMM-019.feature +++ b/core/integration/features/amm/0090-VAMM-019.feature @@ -109,7 +109,7 @@ Feature: Test vAMM cancellation by abandoning. | party4 | 122 | 291 | vamm1-id | true | And the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 100 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 100 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 155 | # trying to trade again causes no trades because the AMM has no more volume When the parties place the following orders: @@ -119,7 +119,7 @@ Feature: Test vAMM cancellation by abandoning. # the AMM's mid price has moved to 150, but it has no volume +150 so that best offer comes from the orderbook of 160 Then the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 100 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 100 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 155 | When the network moves ahead "1" blocks Then the parties should have the following profit and loss: @@ -129,7 +129,7 @@ Feature: Test vAMM cancellation by abandoning. # Notional value therefore is 317 * 122 And the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 122 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 122 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 155 | # vAMM receives fees, but loses out in the MTM settlement And the following transfers should happen: diff --git a/core/integration/features/amm/0090-VAMM-020.feature b/core/integration/features/amm/0090-VAMM-020.feature index 43c3518d6a9..57cc71e1c72 100644 --- a/core/integration/features/amm/0090-VAMM-020.feature +++ b/core/integration/features/amm/0090-VAMM-020.feature @@ -97,7 +97,7 @@ Feature: Test vAMM cancellation by reduce-only from long. | vamm1 | ACCOUNT_TYPE_GENERAL | vamm1-id | ACCOUNT_TYPE_GENERAL | | 100000 | USD | true | TRANSFER_TYPE_AMM_LOW | - @VAMM2 + @VAMM Scenario: 0090-VAMM-020: If a vAMM is cancelled and set in Reduce-Only mode when it is currently long, then It creates no further buy orders even if the current price is above the configured lower price. When one of it's sell orders is executed it still does not produce buy orders, and correctly quotes sell orders from a higher price. When the position reaches 0 the vAMM is closed and all funds are released to the user after the next mark to market. # based on 0090-VAMM-007 When the parties place the following orders: @@ -142,7 +142,7 @@ Feature: Test vAMM cancellation by reduce-only from long. | party4 | ETH/MAR22 | sell | 10 | 91 | 0 | TYPE_LIMIT | TIF_GTC | Then the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 95 | TRADING_MODE_CONTINUOUS | 65 | 65 | 91 | 40 | + | 95 | TRADING_MODE_CONTINUOUS | 64 | 64 | 89 | 40 | # Now start checking if the vAMM still quotes sell orders When the parties place the following orders: @@ -274,7 +274,7 @@ Feature: Test vAMM cancellation by reduce-only from long. | party4 | ETH/MAR22 | sell | 10 | 91 | 0 | TYPE_LIMIT | TIF_GTC | Then the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 95 | TRADING_MODE_CONTINUOUS | 65 | 65 | 91 | 40 | + | 95 | TRADING_MODE_CONTINUOUS | 64 | 64 | 89 | 40 | # Now start checking if the vAMM still quotes sell orders When the parties place the following orders: diff --git a/core/integration/features/amm/0090-VAMM-021.feature b/core/integration/features/amm/0090-VAMM-021.feature index 91a85e60f3e..be590e866a6 100644 --- a/core/integration/features/amm/0090-VAMM-021.feature +++ b/core/integration/features/amm/0090-VAMM-021.feature @@ -109,7 +109,7 @@ Feature: Test vAMM cancellation by reduce-only from short. | party4 | 122 | 291 | vamm1-id | true | And the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 100 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 100 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 155 | # trying to trade again causes no trades because the AMM has no more volume When the parties place the following orders: @@ -119,7 +119,7 @@ Feature: Test vAMM cancellation by reduce-only from short. # the AMM's mid price has moved to 150, but it has no volume +150 so that best offer comes from the orderbook of 160 Then the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 100 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 100 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 155 | When the network moves ahead "1" blocks Then the parties should have the following profit and loss: @@ -129,7 +129,7 @@ Feature: Test vAMM cancellation by reduce-only from short. # Notional value therefore is 291 * 122 And the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 122 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 122 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 155 | # vAMM receives fees, but loses out in the MTM settlement And the following transfers should happen: @@ -162,7 +162,7 @@ Feature: Test vAMM cancellation by reduce-only from short. | party4 | ETH/MAR22 | buy | 10 | 154 | 0 | TYPE_LIMIT | TIF_GTC | And the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 122 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 122 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 154 | # Now bring in another party that will trade with the buy orders we've just placed, and reduce the exposure of the vAMM When the parties place the following orders: @@ -263,7 +263,7 @@ Feature: Test vAMM cancellation by reduce-only from short. | party4 | 122 | 291 | vamm1-id | true | And the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 100 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 100 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 155 | # trying to trade again causes no trades because the AMM has no more volume When the parties place the following orders: @@ -273,7 +273,7 @@ Feature: Test vAMM cancellation by reduce-only from short. # the AMM's mid price has moved to 150, but it has no volume +150 so that best offer comes from the orderbook of 160 Then the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 100 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 100 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 155 | When the network moves ahead "1" blocks Then the parties should have the following profit and loss: @@ -283,7 +283,7 @@ Feature: Test vAMM cancellation by reduce-only from short. # Notional value therefore is 291 * 122 And the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 122 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 122 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 155 | # vAMM receives fees, but loses out in the MTM settlement And the following transfers should happen: @@ -316,7 +316,7 @@ Feature: Test vAMM cancellation by reduce-only from short. | party4 | ETH/MAR22 | buy | 10 | 154 | 0 | TYPE_LIMIT | TIF_GTC | And the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | best offer price | best bid price | - | 122 | TRADING_MODE_CONTINUOUS | 154 | 154 | 160 | 149 | + | 122 | TRADING_MODE_CONTINUOUS | 157 | 157 | 160 | 154 | # Now bring in another party that will trade with the buy orders we've just placed, and reduce the exposure of the vAMM When the parties place the following orders: diff --git a/core/integration/features/amm/0090-VAMM-024-026.feature b/core/integration/features/amm/0090-VAMM-024-026.feature index 32fe69ec36c..7e352a0dc35 100644 --- a/core/integration/features/amm/0090-VAMM-024-026.feature +++ b/core/integration/features/amm/0090-VAMM-024-026.feature @@ -142,7 +142,7 @@ Feature: When market.amm.minCommitmentQuantum is 1000, mid price of the market 1 When the network moves ahead "1" blocks Then the market data for the market "ETH/MAR22" should be: | mark price | trading mode | mid price | static mid price | - | 120 | TRADING_MODE_CONTINUOUS | 120 | 120 | + | 120 | TRADING_MODE_CONTINUOUS | 119 | 119 | And the parties should have the following profit and loss: | party | volume | unrealised pnl | realised pnl | is amm | | party4 | -49 | 0 | 0 | | diff --git a/core/integration/features/amm/0090-VAMM-auction.feature b/core/integration/features/amm/0090-VAMM-auction.feature new file mode 100644 index 00000000000..25fbdc032f4 --- /dev/null +++ b/core/integration/features/amm/0090-VAMM-auction.feature @@ -0,0 +1,370 @@ +Feature: vAMM rebasing when created or amended + + Background: + Given the average block duration is "1" + And the margin calculator named "margin-calculator-1": + | search factor | initial factor | release factor | + | 1.2 | 1.5 | 1.7 | + And the log normal risk model named "log-normal-risk-model": + | risk aversion | tau | mu | r | sigma | + | 0.001 | 0.0011407711613050422 | 0 | 0.9 | 3.0 | + And the liquidity monitoring parameters: + | name | triggering ratio | time window | scaling factor | + | lqm-params | 1.00 | 20s | 1 | + + And the following network parameters are set: + | name | value | + | market.value.windowLength | 60s | + | network.markPriceUpdateMaximumFrequency | 0s | + | limits.markets.maxPeggedOrders | 6 | + | market.auction.minimumDuration | 1 | + | market.fee.factors.infrastructureFee | 0.001 | + | market.fee.factors.makerFee | 0.004 | + | spam.protection.max.stopOrdersPerMarket | 5 | + | market.liquidity.equityLikeShareFeeFraction | 1 | + | market.amm.minCommitmentQuantum | 1 | + | market.liquidity.bondPenaltyParameter | 0.2 | + | market.liquidity.stakeToCcyVolume | 1 | + | market.liquidity.successorLaunchWindowLength | 1h | + | market.liquidity.sla.nonPerformanceBondPenaltySlope | 0 | + | market.liquidity.sla.nonPerformanceBondPenaltyMax | 0.6 | + | validators.epoch.length | 10s | + | market.liquidity.earlyExitPenalty | 0.25 | + | market.liquidity.maximumLiquidityFeeFactorLevel | 0.25 | + #risk factor short:3.5569036 + #risk factor long:0.801225765 + And the following assets are registered: + | id | decimal places | + | USD | 0 | + And the fees configuration named "fees-config-1": + | maker fee | infrastructure fee | + | 0.0004 | 0.001 | + + And the liquidity sla params named "SLA-22": + | price range | commitment min time fraction | performance hysteresis epochs | sla competition factor | + | 0.5 | 0.6 | 1 | 1.0 | + + And the markets: + | id | quote name | asset | liquidity monitoring | risk model | margin calculator | auction duration | fees | price monitoring | data source config | linear slippage factor | quadratic slippage factor | sla params | + | ETH/MAR22 | USD | USD | lqm-params | log-normal-risk-model | margin-calculator-1 | 2 | fees-config-1 | default-none | default-eth-for-future | 1e0 | 0 | SLA-22 | + + # Setting up the accounts and vAMM submission now is part of the background, because we'll be running scenarios 0090-VAMM-006 through 0090-VAMM-014 on this setup + Given the parties deposit on asset's general account the following amount: + | party | asset | amount | + | lp1 | USD | 1000000 | + | lp2 | USD | 1000000 | + | lp3 | USD | 1000000 | + | party1 | USD | 1000000 | + | party2 | USD | 1000000 | + | party3 | USD | 1000000 | + | party4 | USD | 1000000 | + | party5 | USD | 1000000 | + | vamm1 | USD | 1000000 | + | vamm2 | USD | 1000000 | + + @VAMM + Scenario: two crossed AMMs at opening auction end + + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm1 | ETH/MAR22 | 100000 | 0.05 | 102 | 92 | 112 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 102 | 92 | 112 | + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 0 | 0 | + + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm2 | ETH/MAR22 | 100000 | 0.05 | 98 | 88 | 108 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 98 | 88 | 108 | + + + And set the following AMM sub account aliases: + | party | market id | alias | + | vamm1 | ETH/MAR22 | vamm1-id | + | vamm2 | ETH/MAR22 | vamm2-id | + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 100 | 92 | + + When the opening auction period ends for market "ETH/MAR22" + Then the following trades should be executed: + | buyer | price | size | seller | is amm | + | vamm1-id | 100 | 46 | vamm2-id | true | + + Then the network moves ahead "1" blocks + + # two AMMs are now prices at ~100 which is between their base values + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | + | 100 | TRADING_MODE_CONTINUOUS | 100 | 101 | + + + @VAMM + Scenario: AMM crossed with SELL orders + + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm1 | ETH/MAR22 | 100000 | 0.05 | 100 | 90 | 110 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 90 | 110 | + + + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | lp1 | ETH/MAR22 | sell | 100 | 95 | 0 | TYPE_LIMIT | TIF_GTC | lp1-b | + + And set the following AMM sub account aliases: + | party | market id | alias | + | vamm1 | ETH/MAR22 | vamm1-id | + + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 96 | 100 | + + When the opening auction period ends for market "ETH/MAR22" + Then the following trades should be executed: + | buyer | price | size | seller | is amm | + | vamm1-id | 96 | 100 | lp1 | true | + + + Then the network moves ahead "1" blocks + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | + | 96 | TRADING_MODE_CONTINUOUS | 97 | 99 | + + @VAMM + Scenario: AMM crossed with BUY orders + + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm1 | ETH/MAR22 | 100000 | 0.05 | 100 | 90 | 110 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 90 | 110 | + + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | lp1 | ETH/MAR22 | buy | 100 | 105 | 0 | TYPE_LIMIT | TIF_GTC | lp1-b | + + And set the following AMM sub account aliases: + | party | market id | alias | + | vamm1 | ETH/MAR22 | vamm1-id | + + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 104 | 100 | + + When the opening auction period ends for market "ETH/MAR22" + Then the following trades should be executed: + | buyer | price | size | seller | is amm | + | lp1 | 104 | 100 | vamm1-id | true | + + + Then the network moves ahead "1" blocks + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | + | 104 | TRADING_MODE_CONTINUOUS | 102 | 104 | + + + @VAMM + Scenario: AMM's crossed with orders and AMMs + + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm1 | ETH/MAR22 | 100000 | 0.05 | 100 | 90 | 110 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 90 | 110 | + + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm2 | ETH/MAR22 | 100000 | 0.05 | 98 | 88 | 108 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 98 | 88 | 108 | + + And the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | lp1 | ETH/MAR22 | buy | 50 | 105 | 0 | TYPE_LIMIT | TIF_GTC | lp1-b | + | lp1 | ETH/MAR22 | buy | 50 | 102 | 0 | TYPE_LIMIT | TIF_GTC | lp1-b | + | lp2 | ETH/MAR22 | sell | 50 | 95 | 0 | TYPE_LIMIT | TIF_GTC | lp2-b | + | lp2 | ETH/MAR22 | sell | 50 | 98 | 0 | TYPE_LIMIT | TIF_GTC | lp2-b | + + And set the following AMM sub account aliases: + | party | market id | alias | + | vamm1 | ETH/MAR22 | vamm1-id | + | vamm2 | ETH/MAR22 | vamm2-id | + + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 99 | 146 | + + When the opening auction period ends for market "ETH/MAR22" + Then the following trades should be executed: + | buyer | price | size | seller | is amm | + | lp1 | 99 | 46 | vamm2-id | true | + | lp1 | 99 | 4 | lp2 | false | + | lp1 | 99 | 46 | lp2 | false | + | lp1 | 99 | 4 | lp2 | false | + | vamm1-id | 99 | 46 | lp2 | true | + + + Then the network moves ahead "1" blocks + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | + | 99 | TRADING_MODE_CONTINUOUS | 98 | 100 | + + + @VAMM + Scenario: Crossed orders then AMM submitted + + When the parties place the following orders: + | party | market id | side | volume | price | resulting trades | type | tif | reference | + | lp1 | ETH/MAR22 | buy | 50 | 105 | 0 | TYPE_LIMIT | TIF_GTC | lp1-b | + | lp1 | ETH/MAR22 | buy | 50 | 102 | 0 | TYPE_LIMIT | TIF_GTC | lp1-b | + | lp2 | ETH/MAR22 | sell | 50 | 95 | 0 | TYPE_LIMIT | TIF_GTC | lp2-b | + #| lp2 | ETH/MAR22 | sell | 50 | 98 | 0 | TYPE_LIMIT | TIF_GTC | lp2-b | + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 100 | 50 | + + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm1 | ETH/MAR22 | 100000 | 0.05 | 100 | 90 | 110 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 90 | 110 | + + + And set the following AMM sub account aliases: + | party | market id | alias | + | vamm1 | ETH/MAR22 | vamm1-id | + + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 102 | 100 | + + When the opening auction period ends for market "ETH/MAR22" + #Then the following trades should be executed: + # | buyer | price | size | seller | is amm | + # | vamm1-id | 96 | 100 | lp1 | true | + + + Then the network moves ahead "1" blocks + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | + | 102 | TRADING_MODE_CONTINUOUS | 101 | 103 | + + @VAMM + Scenario: AMM cancelled and amending when in auction + + Then the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm1 | ETH/MAR22 | 100000 | 0.05 | 100 | 90 | 110 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 90 | 110 | + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 0 | 0 | + + + When the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm2 | ETH/MAR22 | 100000 | 0.05 | 95 | 85 | 105 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 95 | 85 | 105 | + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 98 | 104 | + + + And set the following AMM sub account aliases: + | party | market id | alias | + | vamm1 | ETH/MAR22 | vamm1-id | + | vamm2 | ETH/MAR22 | vamm2-id | + + + # amend so that its not crossed + When the parties amend the following AMM: + | party | market id | slippage | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 0.1 | 100 | 90 | 110 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 100 | 90 | 110 | + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 0 | 0 | + + + # amend so that its more crossed again at a different point + When the parties amend the following AMM: + | party | market id | slippage | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 0.1 | 98 | 88 | 108 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 98 | 88 | 108 | + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 99 | 46 | + + # then the second AMM is cancels + When the parties cancel the following AMM: + | party | market id | method | + | vamm2 | ETH/MAR22 | METHOD_IMMEDIATE | + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 0 | 0 | + + + # then amend the first AMM and re-create the second + When the parties amend the following AMM: + | party | market id | slippage | base | lower bound | upper bound | + | vamm1 | ETH/MAR22 | 0.1 | 98 | 88 | 108 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm1 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 98 | 88 | 108 | + + When the parties submit the following AMM: + | party | market id | amount | slippage | base | lower bound | upper bound | proposed fee | + | vamm2 | ETH/MAR22 | 100000 | 0.05 | 102 | 92 | 112 | 0.03 | + Then the AMM pool status should be: + | party | market id | amount | status | base | lower bound | upper bound | + | vamm2 | ETH/MAR22 | 100000 | STATUS_ACTIVE | 102 | 92 | 112 | + + + And the market data for the market "ETH/MAR22" should be: + | trading mode | indicative price | indicative volume | + | TRADING_MODE_OPENING_AUCTION | 100 | 92 | + + + # now uncross + When the opening auction period ends for market "ETH/MAR22" + Then the following trades should be executed: + | buyer | price | size | seller | is amm | + | vamm2-id | 100 | 46 | vamm1-id | true | + + + Then the network moves ahead "1" blocks + And the market data for the market "ETH/MAR22" should be: + | mark price | trading mode | best bid price | best offer price | + | 100 | TRADING_MODE_CONTINUOUS | 100 | 101 | \ No newline at end of file diff --git a/core/integration/setup_test.go b/core/integration/setup_test.go index 7172c10f4fc..32979bd7694 100644 --- a/core/integration/setup_test.go +++ b/core/integration/setup_test.go @@ -536,5 +536,9 @@ func (e *executionTestSetup) registerNetParamsCallbacks() error { Param: netparams.MarketAMMMinCommitmentQuantum, Watcher: execsetup.executionEngine.OnMarketAMMMinCommitmentQuantum, }, + netparams.WatchParam{ + Param: netparams.MarketAMMMaxCalculationLevels, + Watcher: execsetup.executionEngine.OnMarketAMMMaxCalculationLevels, + }, ) } diff --git a/core/matching/cached_orderbook.go b/core/matching/cached_orderbook.go index d75676bba50..af997e1b7ff 100644 --- a/core/matching/cached_orderbook.go +++ b/core/matching/cached_orderbook.go @@ -216,3 +216,12 @@ func (b *CachedOrderBook) GetIndicativePrice() *num.Uint { } return price } + +func (b *CachedOrderBook) UpdateAMM(party string) { + if !b.auction { + return + } + + b.cache.Invalidate() + b.OrderBook.UpdateAMM(party) +} diff --git a/core/matching/indicative_price_and_volume.go b/core/matching/indicative_price_and_volume.go index 253dbe04ea7..bcaa38de66e 100644 --- a/core/matching/indicative_price_and_volume.go +++ b/core/matching/indicative_price_and_volume.go @@ -16,11 +16,14 @@ package matching import ( + "slices" "sort" "code.vegaprotocol.io/vega/core/types" "code.vegaprotocol.io/vega/libs/num" "code.vegaprotocol.io/vega/logging" + + "golang.org/x/exp/maps" ) type IndicativePriceAndVolume struct { @@ -37,6 +40,10 @@ type IndicativePriceAndVolume struct { lastMaxTradable uint64 lastCumulativeVolumes []CumulativeVolumeLevel needsUpdate bool + + // keep track of expanded off book orders + offbook OffbookSource + generated map[string]*ipvGeneratedOffbook } type ipvPriceLevel struct { @@ -49,6 +56,19 @@ type ipvVolume struct { volume uint64 } +type ipvGeneratedOffbook struct { + buy []*types.Order + sell []*types.Order +} + +func (g *ipvGeneratedOffbook) add(order *types.Order) { + if order.Side == types.SideSell { + g.sell = append(g.sell, order) + return + } + g.buy = append(g.buy, order) +} + func NewIndicativePriceAndVolume(log *logging.Logger, buy, sell *OrderBookSide) *IndicativePriceAndVolume { bestBid, _, err := buy.BestPriceAndVolume() if err != nil { @@ -59,12 +79,24 @@ func NewIndicativePriceAndVolume(log *logging.Logger, buy, sell *OrderBookSide) bestAsk = num.UintZero() } + if buy.offbook != nil { + bb, _, ba, _ := buy.offbook.BestPricesAndVolumes() + if bb != nil { + bestBid = num.Max(bestBid, bb) + } + if ba != nil { + bestAsk = num.Min(bestAsk, ba) + } + } + ipv := IndicativePriceAndVolume{ levels: []ipvPriceLevel{}, log: log, lastMinPrice: bestBid, lastMaxPrice: bestAsk, needsUpdate: true, + offbook: buy.offbook, + generated: map[string]*ipvGeneratedOffbook{}, } ipv.buildInitialCumulativeLevels(buy, sell) @@ -75,6 +107,101 @@ func NewIndicativePriceAndVolume(log *logging.Logger, buy, sell *OrderBookSide) return &ipv } +func (ipv *IndicativePriceAndVolume) buildInitialOffbookShape(offbook OffbookSource, mplm map[num.Uint]ipvPriceLevel) { + if ipv.lastMinPrice.GT(ipv.lastMaxPrice) { + // region is not crossed so we won't expand just yet + return + } + + // expand all AMM's into orders within the crossed region and add them to the price-level cache + buys, sells := offbook.OrderbookShape(ipv.lastMinPrice, ipv.lastMaxPrice, nil) + + for _, o := range buys { + mpl, ok := mplm[*o.Price] + if !ok { + mpl = ipvPriceLevel{price: o.Price, buypl: ipvVolume{0}, sellpl: ipvVolume{0}} + } + // increment the volume at this level + mpl.buypl.volume += o.Size + mplm[*o.Price] = mpl + + if ipv.generated[o.Party] == nil { + ipv.generated[o.Party] = &ipvGeneratedOffbook{} + } + ipv.generated[o.Party].add(o) + } + + for _, o := range sells { + mpl, ok := mplm[*o.Price] + if !ok { + mpl = ipvPriceLevel{price: o.Price, buypl: ipvVolume{0}, sellpl: ipvVolume{0}} + } + + mpl.sellpl.volume += o.Size + mplm[*o.Price] = mpl + + if ipv.generated[o.Party] == nil { + ipv.generated[o.Party] = &ipvGeneratedOffbook{} + } + ipv.generated[o.Party].add(o) + } +} + +func (ipv *IndicativePriceAndVolume) removeOffbookShape(party string) { + orders, ok := ipv.generated[party] + if !ok { + return + } + + // remove all the old volume for the AMM's + for _, o := range orders.buy { + ipv.RemoveVolumeAtPrice(o.Price, o.Size, o.Side) + } + for _, o := range orders.sell { + ipv.RemoveVolumeAtPrice(o.Price, o.Size, o.Side) + } + + // clear it out the saved generated orders for the offbook shape + delete(ipv.generated, party) +} + +func (ipv *IndicativePriceAndVolume) addOffbookShape(party *string, minPrice, maxPrice *num.Uint) { + // recalculate new orders for the shape and add the volume in + buys, sells := ipv.offbook.OrderbookShape(minPrice, maxPrice, party) + + for _, o := range buys { + ipv.AddVolumeAtPrice(o.Price, o.Size, o.Side) + + if ipv.generated[o.Party] == nil { + ipv.generated[o.Party] = &ipvGeneratedOffbook{} + } + ipv.generated[o.Party].add(o) + } + + for _, o := range sells { + ipv.AddVolumeAtPrice(o.Price, o.Size, o.Side) + + if ipv.generated[o.Party] == nil { + ipv.generated[o.Party] = &ipvGeneratedOffbook{} + } + ipv.generated[o.Party].add(o) + } +} + +func (ipv *IndicativePriceAndVolume) updateOffbookState(minPrice, maxPrice *num.Uint) { + parties := maps.Keys(ipv.generated) + for _, p := range parties { + ipv.removeOffbookShape(p) + } + + if minPrice.GT(maxPrice) { + // region is not crossed so we won't expand just yet + return + } + + ipv.addOffbookShape(nil, minPrice, maxPrice) +} + // this will be used to build the initial set of price levels, when the auction is being started. func (ipv *IndicativePriceAndVolume) buildInitialCumulativeLevels(buy, sell *OrderBookSide) { // we'll keep track of all the pl we encounter @@ -97,6 +224,10 @@ func (ipv *IndicativePriceAndVolume) buildInitialCumulativeLevels(buy, sell *Ord } } + if buy.offbook != nil { + ipv.buildInitialOffbookShape(buy.offbook, mplm) + } + // now we insert them all in the slice. // so we can sort them ipv.levels = make([]ipvPriceLevel, 0, len(mplm)) @@ -192,20 +323,26 @@ func (ipv *IndicativePriceAndVolume) getLevelsWithinRange(maxPrice, minPrice *nu } func (ipv *IndicativePriceAndVolume) GetCumulativePriceLevels(maxPrice, minPrice *num.Uint) ([]CumulativeVolumeLevel, uint64) { - needsUpdate := ipv.needsUpdate + var crossedRegionChanged bool if maxPrice.NEQ(ipv.lastMaxPrice) { maxPrice = maxPrice.Clone() - needsUpdate = true + crossedRegionChanged = true } if minPrice.NEQ(ipv.lastMinPrice) { minPrice = minPrice.Clone() - needsUpdate = true + crossedRegionChanged = true } - if !needsUpdate { + // if the crossed region hasn't changed and no new orders were added/removed from the crossed region then we do not need + // to recalculate + if !ipv.needsUpdate && !crossedRegionChanged { return ipv.lastCumulativeVolumes, ipv.lastMaxTradable } + if crossedRegionChanged && ipv.offbook != nil { + ipv.updateOffbookState(minPrice, maxPrice) + } + rangedLevels := ipv.getLevelsWithinRange(maxPrice, minPrice) // now re-allocate the slice only if needed if ipv.buf == nil || cap(ipv.buf) < len(rangedLevels) { @@ -280,3 +417,31 @@ func (ipv *IndicativePriceAndVolume) GetCumulativePriceLevels(maxPrice, minPrice ipv.lastCumulativeVolumes = cumulativeVolumes return cumulativeVolumes, maxTradable } + +// ExtractOffbookOrders returns the cached expanded orders of AMM's in the crossed region of the given side. These +// are the order that we will send in aggressively to uncrossed the book. +func (ipv *IndicativePriceAndVolume) ExtractOffbookOrders(price *num.Uint, side types.Side) ([]*types.Order, uint64) { + var volume uint64 + orders := []*types.Order{} + // the ipv keeps track of all the expand AMM orders in the crossed region + parties := maps.Keys(ipv.generated) + slices.Sort(parties) + + for _, p := range parties { + cpm := func(p *num.Uint) bool { return p.LT(price) } + oo := ipv.generated[p].buy + if side == types.SideSell { + oo = ipv.generated[p].sell + cpm = func(p *num.Uint) bool { return p.GT(price) } + } + + for _, o := range oo { + if cpm(o.Price) { + continue + } + orders = append(orders, o) + volume += o.Size + } + } + return orders, volume +} diff --git a/core/matching/mocks/mocks.go b/core/matching/mocks/mocks.go index 62af1e0e234..0f854d39684 100644 --- a/core/matching/mocks/mocks.go +++ b/core/matching/mocks/mocks.go @@ -64,6 +64,21 @@ func (mr *MockOffbookSourceMockRecorder) NotifyFinished() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NotifyFinished", reflect.TypeOf((*MockOffbookSource)(nil).NotifyFinished)) } +// OrderbookShape mocks base method. +func (m *MockOffbookSource) OrderbookShape(arg0, arg1 *num.Uint, arg2 *string) ([]*types.Order, []*types.Order) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OrderbookShape", arg0, arg1, arg2) + ret0, _ := ret[0].([]*types.Order) + ret1, _ := ret[1].([]*types.Order) + return ret0, ret1 +} + +// OrderbookShape indicates an expected call of OrderbookShape. +func (mr *MockOffbookSourceMockRecorder) OrderbookShape(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OrderbookShape", reflect.TypeOf((*MockOffbookSource)(nil).OrderbookShape), arg0, arg1, arg2) +} + // SubmitOrder mocks base method. func (m *MockOffbookSource) SubmitOrder(arg0 *types.Order, arg1, arg2 *num.Uint) []*types.Order { m.ctrl.T.Helper() diff --git a/core/matching/orderbook.go b/core/matching/orderbook.go index ede29b8a892..ab95c7abc2a 100644 --- a/core/matching/orderbook.go +++ b/core/matching/orderbook.go @@ -25,6 +25,7 @@ import ( "code.vegaprotocol.io/vega/core/types" "code.vegaprotocol.io/vega/libs/crypto" "code.vegaprotocol.io/vega/libs/num" + "code.vegaprotocol.io/vega/libs/ptr" "code.vegaprotocol.io/vega/logging" "code.vegaprotocol.io/vega/protos/vega" @@ -47,6 +48,7 @@ type OffbookSource interface { BestPricesAndVolumes() (*num.Uint, uint64, *num.Uint, uint64) SubmitOrder(agg *types.Order, inner, outer *num.Uint) []*types.Order NotifyFinished() + OrderbookShape(st, nd *num.Uint, id *string) ([]*types.Order, []*types.Order) } // OrderBook represents the book holding all orders in the system. @@ -234,6 +236,10 @@ func (b *OrderBook) LeaveAuction(at time.Time) ([]*types.OrderConfirmation, []*t b.remove(uo.Order) } + if uo.Order.GeneratedOffbook { + uo.Order.CreatedAt = ts + } + uo.Order.UpdatedAt = ts for idx, po := range uo.PassiveOrdersAffected { po.UpdatedAt = ts @@ -486,8 +492,15 @@ func (b *OrderBook) GetIndicativeTrades() ([]*types.Trade, error) { uncrossingSide = b.sell } + // extract uncrossing orders from all AMMs + var offbookVolume uint64 + uncrossOrders, offbookVolume = b.indicativePriceAndVolume.ExtractOffbookOrders(price, uncrossSide) + + // the remaining volume should now come from the orderbook + volume -= offbookVolume + // Remove all the orders from that side of the book up to the given volume - uncrossOrders = uncrossingSide.ExtractOrders(price, volume, false) + uncrossOrders = append(uncrossOrders, uncrossingSide.ExtractOrders(price, volume, false)...) opSide := b.getOppositeSide(uncrossSide) output := make([]*types.Trade, 0, len(uncrossOrders)) trades, err := opSide.fakeUncrossAuction(uncrossOrders) @@ -535,10 +548,7 @@ func (b *OrderBook) uncrossBook() ([]*types.OrderConfirmation, error) { return nil, nil } - var ( - uncrossOrders []*types.Order - uncrossingSide *OrderBookSide - ) + var uncrossingSide *OrderBookSide if uncrossSide == types.SideBuy { uncrossingSide = b.buy @@ -546,8 +556,15 @@ func (b *OrderBook) uncrossBook() ([]*types.OrderConfirmation, error) { uncrossingSide = b.sell } + // extract uncrossing orders from all AMMs + var offbookVolume uint64 + uncrossOrders, offbookVolume := b.indicativePriceAndVolume.ExtractOffbookOrders(price, uncrossSide) + + // the remaining volume should now come from the orderbook + volume -= offbookVolume + // Remove all the orders from that side of the book up to the given volume - uncrossOrders = uncrossingSide.ExtractOrders(price, volume, true) + uncrossOrders = append(uncrossOrders, uncrossingSide.ExtractOrders(price, volume, true)...) return b.uncrossBookSide(uncrossOrders, b.getOppositeSide(uncrossSide), price.Clone()) } @@ -631,10 +648,17 @@ func (b *OrderBook) BestBidPriceAndVolume() (*num.Uint, uint64, error) { return price, volume, err } + // no orderbook volume or AMM price is better + if err != nil || oPrice.GT(price) { + //nolint: nilerr + return oPrice, oVolume, nil + } + + // AMM price equals orderbook price, combined volumes if err == nil && oPrice.EQ(price) { oVolume += volume + return oPrice, oVolume, nil } - return oPrice, oVolume, nil } return price, volume, err } @@ -642,18 +666,26 @@ func (b *OrderBook) BestBidPriceAndVolume() (*num.Uint, uint64, error) { // BestOfferPriceAndVolume : Return the best bid and volume for the sell side of the book. func (b *OrderBook) BestOfferPriceAndVolume() (*num.Uint, uint64, error) { price, volume, err := b.sell.BestPriceAndVolume() + if b.sell.offbook != nil { - _, _, oPrice, oVolume := b.sell.offbook.BestPricesAndVolumes() + _, _, oPrice, oVolume := b.buy.offbook.BestPricesAndVolumes() // no off source volume, return the orderbook if oVolume == 0 { return price, volume, err } + // no orderbook volume or AMM price is better + if err != nil || oPrice.LT(price) { + //nolint: nilerr + return oPrice, oVolume, nil + } + + // AMM price equals orderbook price, combined volumes if err == nil && oPrice.EQ(price) { oVolume += volume + return oPrice, oVolume, nil } - return oPrice, oVolume, nil } return price, volume, err } @@ -820,6 +852,22 @@ func (b *OrderBook) AmendOrder(originalOrder, amendedOrder *types.Order) error { return nil } +func (b *OrderBook) UpdateAMM(party string) { + if !b.auction { + return + } + + ipv := b.indicativePriceAndVolume + ipv.removeOffbookShape(party) + if ipv.lastMinPrice.GT(ipv.lastMaxPrice) { + // region is not crossed so we won't expand just yet + return + } + + ipv.addOffbookShape(ptr.From(party), ipv.lastMinPrice, ipv.lastMaxPrice) + ipv.needsUpdate = true +} + // GetTrades returns the trades a given order generates if we were to submit it now // this is used to calculate fees, perform price monitoring, etc... func (b *OrderBook) GetTrades(order *types.Order) ([]*types.Trade, error) { @@ -1113,30 +1161,41 @@ func makeResponse(order *types.Order, trades []*types.Trade, impactedOrders []*t } func (b *OrderBook) GetBestBidPrice() (*num.Uint, error) { - // AMM price can never be crossed with the orderbook, so if there is a best price with volume use it + price, _, err := b.buy.BestPriceAndVolume() + if b.buy.offbook != nil { - price, volume, _, _ := b.buy.offbook.BestPricesAndVolumes() - if volume != 0 { - return price, nil + offbook, volume, _, _ := b.buy.offbook.BestPricesAndVolumes() + if volume == 0 { + return price, err + } + + if err != nil || offbook.GT(price) { + //nolint: nilerr + return offbook, nil } } - price, _, err := b.buy.BestPriceAndVolume() return price, err } func (b *OrderBook) GetBestStaticBidPrice() (*num.Uint, error) { - // AMM price can never be crossed with the orderbook, so if there is a best price with volume use it + price, err := b.buy.BestStaticPrice() if b.buy.offbook != nil { - price, volume, _, _ := b.buy.offbook.BestPricesAndVolumes() - if volume != 0 { - return price, nil + offbook, volume, _, _ := b.buy.offbook.BestPricesAndVolumes() + if volume == 0 { + return price, err + } + + if err != nil || offbook.GT(price) { + //nolint: nilerr + return offbook, nil } } - return b.buy.BestStaticPrice() + return price, err } func (b *OrderBook) GetBestStaticBidPriceAndVolume() (*num.Uint, uint64, error) { price, volume, err := b.buy.BestStaticPriceAndVolume() + if b.buy.offbook != nil { oPrice, oVolume, _, _ := b.buy.offbook.BestPricesAndVolumes() @@ -1145,39 +1204,57 @@ func (b *OrderBook) GetBestStaticBidPriceAndVolume() (*num.Uint, uint64, error) return price, volume, err } + // no orderbook volume or AMM price is better + if err != nil || oPrice.GT(price) { + //nolint: nilerr + return oPrice, oVolume, nil + } + + // AMM price equals orderbook price, combined volumes if err == nil && oPrice.EQ(price) { oVolume += volume + return oPrice, oVolume, nil } - return oPrice, oVolume, nil } return price, volume, err } func (b *OrderBook) GetBestAskPrice() (*num.Uint, error) { - // AMM price can never be crossed with the orderbook, so if there is a best price with volume use it + price, _, err := b.sell.BestPriceAndVolume() + if b.sell.offbook != nil { - _, _, price, volume := b.sell.offbook.BestPricesAndVolumes() - if volume != 0 { - return price, nil + _, _, offbook, volume := b.sell.offbook.BestPricesAndVolumes() + if volume == 0 { + return price, err + } + + if err != nil || offbook.LT(price) { + //nolint: nilerr + return offbook, nil } } - price, _, err := b.sell.BestPriceAndVolume() return price, err } func (b *OrderBook) GetBestStaticAskPrice() (*num.Uint, error) { - // AMM price can never be crossed with the orderbook, so if there is a best price with volume use it + price, err := b.sell.BestStaticPrice() if b.sell.offbook != nil { - _, _, price, volume := b.sell.offbook.BestPricesAndVolumes() - if volume != 0 { - return price, nil + _, _, offbook, volume := b.sell.offbook.BestPricesAndVolumes() + if volume == 0 { + return price, err + } + + if err != nil || offbook.LT(price) { + //nolint: nilerr + return offbook, nil } } - return b.sell.BestStaticPrice() + return price, err } func (b *OrderBook) GetBestStaticAskPriceAndVolume() (*num.Uint, uint64, error) { price, volume, err := b.sell.BestStaticPriceAndVolume() + if b.sell.offbook != nil { _, _, oPrice, oVolume := b.sell.offbook.BestPricesAndVolumes() @@ -1186,10 +1263,17 @@ func (b *OrderBook) GetBestStaticAskPriceAndVolume() (*num.Uint, uint64, error) return price, volume, err } + // no orderbook volume or AMM price is better + if err != nil || oPrice.LT(price) { + //nolint: nilerr + return oPrice, oVolume, nil + } + + // AMM price equals orderbook price, combined volumes if err == nil && oPrice.EQ(price) { oVolume += volume + return oPrice, oVolume, nil } - return oPrice, oVolume, nil } return price, volume, err } diff --git a/core/matching/side.go b/core/matching/side.go index 885822412ae..38a5ec079d6 100644 --- a/core/matching/side.go +++ b/core/matching/side.go @@ -277,7 +277,10 @@ func (s *OrderBookSide) ExtractOrders(price *num.Uint, volume uint64, removeOrde // something has gone wrong if totalVolume != volume { s.log.Panic("Failed to extract orders as not enough volume on the book", - logging.BigUint("Price", price), logging.Uint64("volume", volume)) + logging.BigUint("price", price), + logging.Uint64("volume", volume), + logging.Uint64("total-volume", totalVolume), + ) } return extractedOrders diff --git a/core/netparams/defaults.go b/core/netparams/defaults.go index 6bdbc3e7a4e..a6dbd11ba49 100644 --- a/core/netparams/defaults.go +++ b/core/netparams/defaults.go @@ -79,7 +79,8 @@ func defaultNetParams() map[string]value { // ethereum oracles EthereumOraclesEnabled: NewInt(gteI0, lteI1).Mutable(true).MustUpdate("0"), - MarketAMMMinCommitmentQuantum: NewUint(UintGT(num.UintZero())).Mutable(true).MustUpdate("100"), + MarketAMMMinCommitmentQuantum: NewUint(gteU0).Mutable(true).MustUpdate("100"), + MarketAMMMaxCalculationLevels: NewUint(gteU1).Mutable(true).MustUpdate("1000"), // markets MarketMarginScalingFactors: NewJSON(&proto.ScalingFactors{}, checks.MarginScalingFactor(), checks.MarginScalingFactorRange(num.DecimalOne(), num.DecimalFromInt64(100))).Mutable(true).MustUpdate(`{"search_level": 1.1, "initial_margin": 1.2, "collateral_release": 1.4}`), diff --git a/core/netparams/keys.go b/core/netparams/keys.go index 471a5089c28..9824eab4cbb 100644 --- a/core/netparams/keys.go +++ b/core/netparams/keys.go @@ -264,6 +264,7 @@ const ( RewardsActivityStreakMinQuantumTradeVolume = "rewards.activityStreak.minQuantumTradeVolume" MarketAMMMinCommitmentQuantum = "market.amm.minCommitmentQuantum" + MarketAMMMaxCalculationLevels = "market.liquidity.maxAmmCalculationLevels" ) var Deprecated = map[string]struct{}{ @@ -280,6 +281,7 @@ var AllKeys = map[string]struct{}{ SpamProtectionMaxUpdatePartyProfile: {}, SpamProtectionUpdateProfileMinFunds: {}, MarketAMMMinCommitmentQuantum: {}, + MarketAMMMaxCalculationLevels: {}, GovernanceProposalVolumeDiscountProgramMinClose: {}, GovernanceProposalVolumeDiscountProgramMaxClose: {}, GovernanceProposalVolumeDiscountProgramMinEnact: {}, diff --git a/core/protocol/all_services.go b/core/protocol/all_services.go index 926604ef2f0..2b1f2df3794 100644 --- a/core/protocol/all_services.go +++ b/core/protocol/all_services.go @@ -1122,6 +1122,10 @@ func (svcs *allServices) setupNetParameters(powWatchers []netparams.WatchParam) Param: netparams.MarketAMMMinCommitmentQuantum, Watcher: svcs.executionEngine.OnMarketAMMMinCommitmentQuantum, }, + { + Param: netparams.MarketAMMMaxCalculationLevels, + Watcher: svcs.executionEngine.OnMarketAMMMaxCalculationLevels, + }, { Param: netparams.MarketLiquidityEquityLikeShareFeeFraction, Watcher: svcs.executionEngine.OnMarketLiquidityEquityLikeShareFeeFractionUpdate, diff --git a/libs/num/compare.go b/libs/num/compare.go index af9ec849bda..930a33d7dcd 100644 --- a/libs/num/compare.go +++ b/libs/num/compare.go @@ -52,6 +52,14 @@ func AbsV[T Signed](a T) T { return a } +// DeltaV generic delta function signed primitives. +func DeltaV[T Signed](a, b T) T { + if a < b { + return b - a + } + return a - b +} + // MaxAbs - get max value based on absolute values of abolute vals. func MaxAbs[T Signed](vals ...T) T { var r, m T