Skip to content

Commit

Permalink
feat: allow AMM's with zero volume price levels
Browse files Browse the repository at this point in the history
  • Loading branch information
wwestgarth committed Sep 19, 2024
1 parent 3a48987 commit fb9450e
Show file tree
Hide file tree
Showing 30 changed files with 1,922 additions and 1,199 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

- [11644](https://github.com/vegaprotocol/vega/issues/11644) - `liveOnly` flag has been added to the `AMM` API to show only active `AMMs`.
- [11519](https://github.com/vegaprotocol/vega/issues/11519) - Add fees to position API types.
- [11642](https://github.com/vegaprotocol/vega/issues/11642) - `AMMs` with empty price levels are now allowed.

### 🐛 Fixes

Expand Down
71 changes: 50 additions & 21 deletions core/execution/amm/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,9 @@ type Engine struct {
// a mapping of all amm-party-ids to the party owning them.
ammParties map[string]string

minCommitmentQuantum *num.Uint
maxCalculationLevels *num.Uint
minCommitmentQuantum *num.Uint
maxCalculationLevels *num.Uint
allowedEmptyAMMLevels uint64
}

func New(
Expand All @@ -157,6 +158,7 @@ func New(
positionFactor num.Decimal,
marketActivityTracker *common.MarketActivityTracker,
parties common.Parties,
allowedEmptyAMMLevels uint64,
) *Engine {
oneTick, _ := num.UintFromDecimal(priceFactor)
return &Engine{
Expand All @@ -176,6 +178,7 @@ func New(
positionFactor: positionFactor,
parties: parties,
oneTick: num.Max(num.UintOne(), oneTick),
allowedEmptyAMMLevels: allowedEmptyAMMLevels,
}
}

Expand All @@ -191,8 +194,9 @@ func NewFromProto(
positionFactor num.Decimal,
marketActivityTracker *common.MarketActivityTracker,
parties common.Parties,
allowedEmptyAMMLevels uint64,
) (*Engine, error) {
e := New(log, broker, collateral, marketID, assetID, position, priceFactor, positionFactor, marketActivityTracker, parties)
e := New(log, broker, collateral, marketID, assetID, position, priceFactor, positionFactor, marketActivityTracker, parties, allowedEmptyAMMLevels)

for _, v := range state.AmmPartyIds {
e.ammParties[v.Key] = v.Value
Expand Down Expand Up @@ -244,6 +248,10 @@ func (e *Engine) OnMaxCalculationLevelsUpdate(ctx context.Context, c *num.Uint)
}
}

func (e *Engine) UpdateAllowedEmptyLevels(allowedEmptyLevels uint64) {
e.allowedEmptyAMMLevels = allowedEmptyLevels
}

// 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{}
Expand Down Expand Up @@ -311,11 +319,20 @@ func (e *Engine) BestPricesAndVolumes() (*num.Uint, uint64, *num.Uint, uint64) {

for _, pool := range e.poolsCpy {
// get the pool's current price
fp := pool.BestPrice(nil)

// get the volume on the buy side by simulating an incoming sell order
bid := num.Max(pool.lower.low, num.UintZero().Sub(fp, pool.oneTick))
volume := pool.TradableVolumeInRange(types.SideSell, fp.Clone(), bid)
fp := pool.FairPrice()

var volume uint64
bid, ok := pool.BestPrice(types.SideBuy)
if ok {
volume = 1

// if the best price is
bidTick := num.Max(pool.lower.low, num.UintZero().Sub(fp, pool.oneTick))
if bid.GTE(bidTick) {
bid = bidTick
volume = pool.TradableVolumeForPrice(types.SideSell, bid)
}
}

if volume != 0 {
if bestBid == nil || bid.GT(bestBid) {
Expand All @@ -326,9 +343,17 @@ func (e *Engine) BestPricesAndVolumes() (*num.Uint, uint64, *num.Uint, uint64) {
}
}

// get the volume on the sell side by simulating an incoming buy order
ask := num.Min(pool.upper.high, num.UintZero().Add(fp, pool.oneTick))
volume = pool.TradableVolumeInRange(types.SideBuy, fp.Clone(), ask)
volume = 0
ask, ok := pool.BestPrice(types.SideSell)
if ok {
volume = 1
askTick := num.Min(pool.upper.high, num.UintZero().Add(fp, pool.oneTick))
if ask.LTE(askTick) {
ask = askTick
volume = pool.TradableVolumeForPrice(types.SideBuy, ask)
}
}

if volume != 0 {
if bestAsk == nil || ask.LT(bestAsk) {
bestAsk = ask
Expand All @@ -347,7 +372,10 @@ func (e *Engine) GetVolumeAtPrice(price *num.Uint, side types.Side) uint64 {
vol := uint64(0)
for _, pool := range e.poolsCpy {
// get the pool's current price
best := pool.BestPrice(&types.Order{Price: price, Side: side})
best, ok := pool.BestPrice(types.OtherSide(side))
if !ok {
continue
}

// make sure price is in tradable range
if side == types.SideBuy && best.GT(price) {
Expand Down Expand Up @@ -377,10 +405,14 @@ func (e *Engine) submit(active []*Pool, agg *types.Order, inner, outer *num.Uint
for _, p := range active {
p.setEphemeralPosition()

price := p.BestPrice(agg)
price, ok := p.BestPrice(types.OtherSide(agg.Side))
if !ok {
continue
}

if e.log.GetLevel() == logging.DebugLevel {
e.log.Debug("best price for pool",
logging.String("id", p.ID),
logging.String("amm-party", p.AMMParty),
logging.String("best-price", price.String()),
)
}
Expand All @@ -401,19 +433,15 @@ func (e *Engine) submit(active []*Pool, agg *types.Order, inner, outer *num.Uint
useActive = append(useActive, p)
}

if agg.Side == types.SideSell {
inner, outer = outer, inner
}

// calculate the volume each pool has
var total uint64
volumes := []uint64{}
for _, p := range useActive {
volume := p.TradableVolumeInRange(agg.Side, inner, outer)
volume := p.TradableVolumeForPrice(agg.Side, outer)
if e.log.GetLevel() == logging.DebugLevel {
e.log.Debug("volume available to trade",
logging.Uint64("volume", volume),
logging.String("id", p.ID),
logging.String("amm-party", p.AMMParty),
)
}

Expand Down Expand Up @@ -676,6 +704,7 @@ func (e *Engine) Create(
e.priceFactor,
e.positionFactor,
e.maxCalculationLevels,
e.allowedEmptyAMMLevels,
)
if err != nil {
return nil, err
Expand Down Expand Up @@ -736,7 +765,7 @@ func (e *Engine) Amend(
}
}

updated, err := pool.Update(amend, riskFactors, scalingFactors, slippage)
updated, err := pool.Update(amend, riskFactors, scalingFactors, slippage, e.allowedEmptyAMMLevels)
if err != nil {
return nil, nil, err
}
Expand Down
47 changes: 34 additions & 13 deletions core/execution/amm/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ var (

func TestSubmitAMM(t *testing.T) {
t.Run("test one pool per party", testOnePoolPerParty)
t.Run("test creation of sparse AMM", testSparseAMMEngine)
}

func TestAMMTrading(t *testing.T) {
Expand Down Expand Up @@ -154,7 +155,7 @@ func testAmendInsufficientCommitment(t *testing.T) {
amend.Parameters.LowerBound.AddSum(num.UintOne())

_, _, err := tst.engine.Amend(ctx, amend, riskFactors, scalingFactors, slippage)
require.ErrorContains(t, err, "insufficient commitment")
require.ErrorContains(t, err, "commitment amount too low")

// check that the original pool still exists
assert.Equal(t, poolID, tst.engine.poolsCpy[0].ID)
Expand Down Expand Up @@ -299,10 +300,10 @@ func testSubmitMarketOrder(t *testing.T) {
}

ensurePosition(t, tst.pos, 0, num.NewUint(0))
orders := tst.engine.SubmitOrder(agg, num.NewUint(1980), num.NewUint(1990))
orders := tst.engine.SubmitOrder(agg, num.NewUint(2000), num.NewUint(1980))
require.Len(t, orders, 1)
assert.Equal(t, "1994", orders[0].Price.String())
assert.Equal(t, 126420, int(orders[0].Size))
assert.Equal(t, "1989", orders[0].Price.String())
assert.Equal(t, 251890, int(orders[0].Size))
}

func testSubmitMarketOrderUnbounded(t *testing.T) {
Expand Down Expand Up @@ -488,7 +489,7 @@ func testBestPricesAndVolume(t *testing.T) {
}

func TestBestPricesAndVolumeNearBound(t *testing.T) {
tst := getTestEngineWithFactors(t, num.DecimalFromInt64(100), num.DecimalFromFloat(10))
tst := getTestEngineWithFactors(t, num.DecimalFromInt64(100), num.DecimalFromFloat(10), 0)

// create three pools
party, subAccount := getParty(t, tst)
Expand All @@ -497,7 +498,7 @@ func TestBestPricesAndVolumeNearBound(t *testing.T) {
expectSubaccountCreation(t, tst, party, subAccount)
whenAMMIsSubmitted(t, tst, submit)

tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(3).Return(
tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(7).Return(
[]events.MarketPosition{&marketPosition{size: 0, averageEntry: num.NewUint(0)}},
)

Expand All @@ -508,25 +509,25 @@ func TestBestPricesAndVolumeNearBound(t *testing.T) {
assert.Equal(t, 1192, int(avolume))

// lets move its position so that the fair price is within one tick of the AMMs upper boundary
tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(3).Return(
tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(7).Return(
[]events.MarketPosition{&marketPosition{size: -222000, averageEntry: num.NewUint(0)}},
)

bid, bvolume, ask, avolume = tst.engine.BestPricesAndVolumes()
assert.Equal(t, "219890", bid.String())
assert.Equal(t, "220000", ask.String()) // make sure we are capped to the boundary and not 220090
assert.Equal(t, 1034, int(bvolume))
assert.Equal(t, 103, int(avolume))
assert.Equal(t, 104, int(avolume))

// lets move its position so that the fair price is within one tick of the AMMs upper boundary
tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(3).Return(
tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(7).Return(
[]events.MarketPosition{&marketPosition{size: 270400, averageEntry: num.NewUint(0)}},
)

bid, bvolume, ask, avolume = tst.engine.BestPricesAndVolumes()
assert.Equal(t, "180000", bid.String()) // make sure we are capped to the boundary and not 179904
assert.Equal(t, "180104", ask.String())
assert.Equal(t, 58, int(bvolume))
assert.Equal(t, 62, int(bvolume))
assert.Equal(t, 1460, int(avolume))
}

Expand Down Expand Up @@ -696,6 +697,26 @@ func testMarketClosure(t *testing.T) {
require.Equal(t, 0, len(tst.engine.ammParties))
}

func testSparseAMMEngine(t *testing.T) {
tst := getTestEngineWithFactors(t, num.DecimalOne(), num.DecimalOne(), 10)

party, subAccount := getParty(t, tst)
submit := getPoolSubmission(t, party, tst.marketID)
submit.CommitmentAmount = num.NewUint(100000)

expectSubaccountCreation(t, tst, party, subAccount)
whenAMMIsSubmitted(t, tst, submit)

tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).AnyTimes().Return(
[]events.MarketPosition{&marketPosition{size: 0, averageEntry: nil}},
)
bb, bv, ba, av := tst.engine.BestPricesAndVolumes()
assert.Equal(t, "1992", bb.String())
assert.Equal(t, 1, int(bv))
assert.Equal(t, "2009", ba.String())
assert.Equal(t, 1, int(av))
}

func expectSubaccountCreation(t *testing.T, tst *tstEngine, party, subAccount string) {
t.Helper()

Expand Down Expand Up @@ -815,7 +836,7 @@ type tstEngine struct {
assetID string
}

func getTestEngineWithFactors(t *testing.T, priceFactor, positionFactor num.Decimal) *tstEngine {
func getTestEngineWithFactors(t *testing.T, priceFactor, positionFactor num.Decimal, allowedEmptyLevels uint64) *tstEngine {
t.Helper()
ctrl := gomock.NewController(t)
col := mocks.NewMockCollateral(ctrl)
Expand All @@ -836,7 +857,7 @@ func getTestEngineWithFactors(t *testing.T, priceFactor, positionFactor num.Deci
parties := cmocks.NewMockParties(ctrl)
parties.EXPECT().AssignDeriveKey(gomock.Any(), gomock.Any(), gomock.Any()).AnyTimes()

eng := New(logging.NewTestLogger(), broker, col, marketID, assetID, pos, priceFactor, positionFactor, mat, parties)
eng := New(logging.NewTestLogger(), broker, col, marketID, assetID, pos, priceFactor, positionFactor, mat, parties, allowedEmptyLevels)

// do an ontick to initialise the idgen
ctx := vgcontext.WithTraceID(context.Background(), vgcrypto.RandomHash())
Expand All @@ -856,7 +877,7 @@ func getTestEngineWithFactors(t *testing.T, priceFactor, positionFactor num.Deci

func getTestEngine(t *testing.T) *tstEngine {
t.Helper()
return getTestEngineWithFactors(t, num.DecimalOne(), num.DecimalOne())
return getTestEngineWithFactors(t, num.DecimalOne(), num.DecimalOne(), 0)
}

func getAccount(balance uint64) *types.Account {
Expand Down
23 changes: 19 additions & 4 deletions core/execution/amm/estimator.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ func EstimateBounds(
linearSlippageFactor, initialMargin,
riskFactorShort, riskFactorLong,
priceFactor, positionFactor num.Decimal,
allowedMaxEmptyLevels uint64,
) EstimatedBounds {
r := EstimatedBounds{}

Expand Down Expand Up @@ -70,8 +71,16 @@ func EstimateBounds(
// now lets check that the lower bound is not too wide that the volume is spread too thin
l := unitLower.Mul(boundPosLower).Abs()

pos := impliedPosition(sqrter.sqrt(num.UintZero().Sub(basePrice, oneTick)), sqrter.sqrt(basePrice), l)
if pos.LessThan(num.DecimalOne()) {
cu := &curve{
l: l,
high: basePrice,
low: lowerPrice,
sqrtHigh: sqrter.sqrt(upperPrice),
isLower: true,
pv: r.PositionSizeAtLower,
}

if err := cu.check(sqrter.sqrt, oneTick, allowedMaxEmptyLevels); err != nil {
r.TooWideLower = true
}
}
Expand Down Expand Up @@ -99,8 +108,14 @@ func EstimateBounds(
// now lets check that the lower bound is not too wide that the volume is spread too thin
l := unitUpper.Mul(boundPosUpper).Abs()

pos := impliedPosition(sqrter.sqrt(num.UintZero().Sub(upperPrice, oneTick)), sqrter.sqrt(upperPrice), l)
if pos.LessThan(num.DecimalOne()) {
cu := &curve{
l: l,
high: upperPrice,
low: basePrice,
sqrtHigh: sqrter.sqrt(upperPrice),
pv: r.PositionSizeAtUpper.Neg(),
}
if err := cu.check(sqrter.sqrt, oneTick, allowedMaxEmptyLevels); err != nil {
r.TooWideUpper = true
}
}
Expand Down
14 changes: 9 additions & 5 deletions core/execution/amm/estimator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func TestEstimate(t *testing.T) {
riskFactorLong,
num.DecimalOne(),
num.DecimalOne(),
0,
)

assert.Equal(t, expectedMetrics.PositionSizeAtUpper.String(), metrics.PositionSizeAtUpper.Round(3).String())
Expand Down Expand Up @@ -161,6 +162,7 @@ func TestEstimate(t *testing.T) {
riskFactorLong,
num.DecimalOne(),
num.DecimalOne(),
0,
)

assert.Equal(t, expectedMetrics.PositionSizeAtUpper.String(), metrics.PositionSizeAtUpper.Round(3).String())
Expand Down Expand Up @@ -205,6 +207,7 @@ func TestEstimatePositionFactor(t *testing.T) {
riskFactorLong,
num.DecimalFromInt64(1000000000000000000),
num.DecimalOne(),
0,
)

assert.Equal(t, expectedMetrics.PositionSizeAtUpper.String(), metrics.PositionSizeAtUpper.Round(3).String())
Expand All @@ -220,17 +223,18 @@ func TestEstimatePositionFactor(t *testing.T) {
upperPrice,
leverageLower,
leverageUpper,
num.UintOne(),
num.MustUintFromString("390500000000000000000", 10),
linearSlippageFactor,
initialMargin,
riskFactorShort,
riskFactorLong,
num.DecimalFromInt64(1000000000000000000),
num.DecimalOne(),
10,
)

assert.Equal(t, "0", metrics.PositionSizeAtUpper.Round(3).String())
assert.Equal(t, "0", metrics.PositionSizeAtLower.Round(3).String())
assert.True(t, metrics.TooWideLower)
assert.True(t, metrics.TooWideUpper)
assert.Equal(t, "-1.559", metrics.PositionSizeAtUpper.Round(3).String())
assert.Equal(t, "2.305", metrics.PositionSizeAtLower.Round(3).String())
assert.False(t, metrics.TooWideLower) // is valid as there are less than 10 empty price levels
assert.True(t, metrics.TooWideUpper) // isn't valid as there are more than 10 empty price levels
}
Loading

0 comments on commit fb9450e

Please sign in to comment.