Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: AMMs can specify a spread around their fair-price #11751

Merged
merged 1 commit into from
Nov 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions commands/amend_amm.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,17 @@ func checkAmendAMM(cmd *commandspb.AmendAMM) Errors {

if cmd.MinimumPriceChangeTrigger != nil {
if minPriceChange, err := num.DecimalFromString(*cmd.MinimumPriceChangeTrigger); err != nil {
errs.AddForProperty("submit_amm.mimimum_price_change_trigger", ErrIsNotValid)
errs.AddForProperty("amend_amm.mimimum_price_change_trigger", ErrIsNotValid)
} else if minPriceChange.LessThan(num.DecimalZero()) {
errs.AddForProperty("submit_amm.proposed_fee", ErrMustBePositiveOrZero)
errs.AddForProperty("amend_amm.mimimum_price_change_trigger", ErrMustBePositiveOrZero)
}
}

if cmd.Spread != nil {
if spread, err := num.DecimalFromString(*cmd.Spread); err != nil {
errs.AddForProperty("amend_amm.spread", ErrIsNotValid)
} else if spread.LessThan(num.DecimalZero()) {
errs.AddForProperty("amend_amm.spread", ErrMustBePositiveOrZero)
}
}

Expand Down
8 changes: 8 additions & 0 deletions commands/submit_amm.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ func checkSubmitAMM(cmd *commandspb.SubmitAMM) Errors {
}
}

if cmd.Spread != nil {
if spread, err := num.DecimalFromString(*cmd.Spread); err != nil {
errs.AddForProperty("submit_amm.spread", ErrIsNotValid)
} else if spread.LessThan(num.DecimalZero()) {
errs.AddForProperty("submit_amm.spread", ErrMustBePositiveOrZero)
}
}

if cmd.ConcentratedLiquidityParameters == nil {
errs.FinalAddForProperty("submit_amm.concentrated_liquidity_parameters", ErrIsRequired)
} else {
Expand Down
2 changes: 2 additions & 0 deletions core/events/amm_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func NewAMMPoolEvent(
lowerCurve *AMMCurve,
upperCurve *AMMCurve,
minimumPriceChangeTrigger num.Decimal,
spread num.Decimal,
) *AMMPool {
return &AMMPool{
Base: newBase(ctx, AMMPoolEvent),
Expand All @@ -71,6 +72,7 @@ func NewAMMPoolEvent(
LowerCurve: lowerCurve.ToProtoEvent(),
UpperCurve: upperCurve.ToProtoEvent(),
MinimumPriceChangeTrigger: minimumPriceChangeTrigger.String(),
Spread: spread.String(),
},
}
}
Expand Down
50 changes: 38 additions & 12 deletions core/execution/amm/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ type Engine struct {
minCommitmentQuantum *num.Uint
maxCalculationLevels *num.Uint
allowedEmptyAMMLevels uint64

inAuction bool
}

func New(
Expand Down Expand Up @@ -211,13 +213,16 @@ func NewFromProto(
e.add(p)
}

e.inAuction = state.Auction

return e, nil
}

func (e *Engine) IntoProto() *v1.AmmState {
state := &v1.AmmState{
AmmPartyIds: make([]*v1.StringMapEntry, 0, len(e.ammParties)),
Pools: make([]*v1.PoolMapEntry, 0, len(e.pools)),
Auction: e.inAuction,
}

for k, v := range e.ammParties {
Expand All @@ -237,6 +242,20 @@ func (e *Engine) IntoProto() *v1.AmmState {
return state
}

func (e *Engine) EnterAuction() {
e.inAuction = true
for _, p := range e.poolsCpy {
p.inAuction = true
}
}

func (e *Engine) LeaveAuction() {
e.inAuction = false
for _, p := range e.poolsCpy {
p.inAuction = false
}
}

func (e *Engine) OnMinCommitmentQuantumUpdate(ctx context.Context, c *num.Uint) {
e.minCommitmentQuantum = c.Clone()
}
Expand Down Expand Up @@ -349,7 +368,7 @@ 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, ok := pool.BestPrice(types.OtherSide(side))
best, ok, _ := pool.BestPrice(types.OtherSide(side))
if !ok {
continue
}
Expand Down Expand Up @@ -382,8 +401,8 @@ func (e *Engine) submit(active []*Pool, agg *types.Order, inner, outer *num.Uint
for _, p := range active {
p.setEphemeralPosition()

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

Expand Down Expand Up @@ -572,14 +591,20 @@ func (e *Engine) partition(agg *types.Order, inner, outer *num.Uint) ([]*Pool, [
// we hit a discontinuity where an AMM's two curves meet if we try to trade over its base-price
// so we partition the inner/outer price range at the base price so that we instead trade across it
// in two steps.
var addBase bool
boundary := p.upper.low
if inner != nil && outer != nil {
if boundary.LT(outer) && boundary.GT(inner) {
bounds[boundary.String()] = boundary.Clone()
}
} else if outer == nil && boundary.GT(inner) {
bounds[boundary.String()] = boundary.Clone()
} else if inner == nil && boundary.LT(outer) {
switch {
case inner != nil && outer != nil:
addBase = boundary.LT(outer) && boundary.GT(inner)
case inner == nil && outer == nil:
addBase = true
case outer == nil && boundary.GT(inner):
addBase = true
case inner == nil && boundary.LT(outer):
addBase = true
}

if addBase {
bounds[boundary.String()] = boundary.Clone()
}

Expand Down Expand Up @@ -696,13 +721,13 @@ func (e *Engine) Create(
e.positionFactor,
e.maxCalculationLevels,
e.allowedEmptyAMMLevels,
submit.SlippageTolerance,
submit.MinimumPriceChangeTrigger,
)
if err != nil {
return nil, err
}

pool.inAuction = e.inAuction

// sanity check, a *new* AMM should not already have a position. If it does it means that the party
// previously had an AMM but it was stopped/cancelled while still holding a position which should not happen.
// It should have either handed its position over to the liquidation engine, or be in reduce-only mode
Expand Down Expand Up @@ -874,6 +899,7 @@ func (e *Engine) sendUpdate(ctx context.Context, pool *Pool) {
TheoreticalPosition: pool.upper.pv,
},
pool.MinimumPriceChangeTrigger,
pool.Spread,
),
)
}
Expand Down
24 changes: 12 additions & 12 deletions core/execution/amm/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ func testSubmitOrderAtBestPrice(t *testing.T) {
ensurePosition(t, tst.pos, 0, num.NewUint(0))
orders := tst.engine.SubmitOrder(agg, num.NewUint(2000), num.NewUint(2001))
require.Len(t, orders, 1)
assert.Equal(t, "2000", orders[0].Price.String())
assert.Equal(t, "2001", orders[0].Price.String())
assert.Equal(t, 11927, int(orders[0].Size))

bb, _, ba, _ := tst.engine.BestPricesAndVolumes()
Expand Down Expand Up @@ -360,7 +360,7 @@ func testSubmitOrderProRata(t *testing.T) {
orders := tst.engine.SubmitOrder(agg, num.NewUint(2010), num.NewUint(2020))
require.Len(t, orders, 3)
for _, o := range orders {
assert.Equal(t, "2000", o.Price.String())
assert.Equal(t, "2001", o.Price.String())
assert.Equal(t, uint64(222), o.Size)
}
}
Expand Down Expand Up @@ -402,9 +402,9 @@ func testSubmitOrderAcrossAMMBoundary(t *testing.T) {
require.Len(t, orders, 6)

// first round, three orders moving all pool's to the upper boundary of the shortest
assert.Equal(t, "2049", orders[0].Price.String())
assert.Equal(t, "2049", orders[1].Price.String())
assert.Equal(t, "2049", orders[2].Price.String())
assert.Equal(t, "2048", orders[0].Price.String())
assert.Equal(t, "2048", orders[1].Price.String())
assert.Equal(t, "2048", orders[2].Price.String())

// second round, 2 orders moving all pool's to the upper boundary of the second shortest
assert.Equal(t, "2124", orders[3].Price.String())
Expand Down Expand Up @@ -452,9 +452,9 @@ func testSubmitOrderAcrossAMMBoundarySell(t *testing.T) {
require.Len(t, orders, 6)

// first round, three orders moving all pool's to the upper boundary of the shortest
assert.Equal(t, "1949", orders[0].Price.String())
assert.Equal(t, "1949", orders[1].Price.String())
assert.Equal(t, "1949", orders[2].Price.String())
assert.Equal(t, "1948", orders[0].Price.String())
assert.Equal(t, "1948", orders[1].Price.String())
assert.Equal(t, "1948", orders[2].Price.String())

// second round, 2 orders moving all pool's to the upper boundary of the second shortest
assert.Equal(t, "1874", orders[3].Price.String())
Expand Down Expand Up @@ -503,7 +503,7 @@ func TestBestPricesAndVolumeNearBound(t *testing.T) {
expectSubaccountCreation(t, tst, party, subAccount)
whenAMMIsSubmitted(t, tst, submit)

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

Expand All @@ -514,7 +514,7 @@ 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(10).Return(
tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(8).Return(
[]events.MarketPosition{&marketPosition{size: -222000, averageEntry: num.NewUint(0)}},
)

Expand All @@ -525,7 +525,7 @@ func TestBestPricesAndVolumeNearBound(t *testing.T) {
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(10).Return(
tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(8).Return(
[]events.MarketPosition{&marketPosition{size: 270400, averageEntry: num.NewUint(0)}},
)

Expand Down Expand Up @@ -746,7 +746,7 @@ func testAMMSnapshot(t *testing.T) {
orders := tst.engine.SubmitOrder(agg, num.NewUint(2010), num.NewUint(2020))
require.Len(t, orders, 3)
for _, o := range orders {
assert.Equal(t, "2000", o.Price.String())
assert.Equal(t, "2001", o.Price.String())
assert.Equal(t, uint64(222), o.Size)
}

Expand Down
Loading
Loading