Skip to content

Commit

Permalink
Merge pull request #11212 from vegaprotocol/liquidation-vamm
Browse files Browse the repository at this point in the history
Liquidation vamm
  • Loading branch information
EVODelavega authored Apr 29, 2024
2 parents 269216a + 9a15ec7 commit ba37236
Show file tree
Hide file tree
Showing 9 changed files with 279 additions and 9 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
- [11127](https://github.com/vegaprotocol/vega/issues/11127) - Price monitoring engine should record all observations with the same weight
- [10995](https://github.com/vegaprotocol/vega/issues/10995) - Liquidation range defined by its own parameter.
- [11167](https://github.com/vegaprotocol/vega/issues/11167) - Add realised return reward metric.
- [11165] (https://github.com/vegaprotocol/vega/issues/11165) - Include negative returns in relative returns reward metric.
- [11165](https://github.com/vegaprotocol/vega/issues/11165) - Include negative returns in relative returns reward metric.
- [11151](https://github.com/vegaprotocol/vega/issues/11151) - Remove name field from the spot markets.
- [11170](https://github.com/vegaprotocol/vega/issues/11170) - Add transfer interval support.
- [11143](https://github.com/vegaprotocol/vega/issues/11143) - Add support for new asset proposal in batch governance proposal.
Expand All @@ -39,6 +39,7 @@
- [11158](https://github.com/vegaprotocol/vega/issues/11158) - resolve the quote asset for fee estimation in spot market.
- [11143](https://github.com/vegaprotocol/vega/issues/11143) - Add support for new asset proposal in batch governance proposal
- [11182](https://github.com/vegaprotocol/vega/issues/11182) - Remove reduce only restriction on spot markets stop orders.
- [11211](https://github.com/vegaprotocol/vega/issues/11211) - Liquidation engine includes `vAMM` shapes as available volume.

### 🐛 Fixes

Expand Down
13 changes: 13 additions & 0 deletions core/execution/amm/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,19 @@ func (e *Engine) BestPricesAndVolumes() (*num.Uint, uint64, *num.Uint, uint64) {
return bestBid, bestBidVolume, bestAsk, bestAskVolume
}

// GetVolumeAtPrice returns the volumes across all registered AMM's that will uncross with with an order at the given price.
// Calling this function with price 1000 and side == sell will return the buy orders that will uncross.
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
fp := pool.BestPrice(nil)
volume := pool.TradableVolumeInRange(side, fp, price)
vol += volume
}
return vol
}

func (e *Engine) submit(active []*Pool, agg *types.Order, inner, outer *num.Uint) []*types.Order {
if e.log.GetLevel() == logging.DebugLevel {
e.log.Debug("checking for volume between",
Expand Down
9 changes: 9 additions & 0 deletions core/execution/amm/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,15 @@ func testBestPricesAndVolume(t *testing.T) {
assert.Equal(t, "2001", ask.String())
assert.Equal(t, uint64(38526), bvolume)
assert.Equal(t, uint64(36615), avolume)

// test GetVolumeAtPrice returns the same volume given best bid/ask
tst.pos.EXPECT().GetPositionsByParty(gomock.Any()).Times(6 * 2).Return(
[]events.MarketPosition{&marketPosition{size: 0, averageEntry: num.NewUint(0)}},
)
bvAt := tst.engine.GetVolumeAtPrice(bid, types.SideSell)
assert.Equal(t, bvolume, bvAt)
avAt := tst.engine.GetVolumeAtPrice(ask, types.SideBuy)
assert.Equal(t, avolume, avAt)
}

func testClosingReduceOnlyPool(t *testing.T) {
Expand Down
4 changes: 2 additions & 2 deletions core/execution/future/market.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,6 @@ func NewMarket(
if mkt.LiquidationStrategy == nil {
mkt.LiquidationStrategy = liquidation.GetLegacyStrat()
}
le := liquidation.New(log, mkt.LiquidationStrategy, mkt.GetID(), broker, book, auctionState, timeService, positionEngine, pMonitor)

marketType := mkt.MarketType()
market := &Market{
Expand Down Expand Up @@ -337,12 +336,13 @@ func NewMarket(
referralDiscountRewardService: referralDiscountRewardService,
volumeDiscountService: volumeDiscountService,
partyMarginFactor: map[string]num.Decimal{},
liquidation: le,
banking: banking,
markPriceCalculator: common.NewCompositePriceCalculator(ctx, mkt.MarkPriceConfiguration, oracleEngine, timeService),
}

market.amm = amm.New(log, broker, collateralEngine, market, riskEngine, positionEngine, priceFactor, positionFactor, marketActivityTracker)
le := liquidation.New(log, mkt.LiquidationStrategy, mkt.GetID(), broker, book, auctionState, timeService, positionEngine, pMonitor, market.amm)
market.liquidation = le
// now set AMM engine on liquidity market.
market.liquidity.SetAMM(market.amm)

Expand Down
4 changes: 2 additions & 2 deletions core/execution/future/market_snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,6 @@ func NewMarketFromSnapshot(
// @TODO check for migration from v0.75.8, strictly speaking, not doing so should have the same effect, though...
mkt.LiquidationStrategy.DisposalSlippage = mkt.LiquiditySLAParams.PriceRange
}
le := liquidation.New(log, mkt.LiquidationStrategy, mkt.GetID(), broker, book, as, timeService, positionEngine, pMonitor)

partyMargin := make(map[string]num.Decimal, len(em.PartyMarginFactors))
for _, pmf := range em.PartyMarginFactors {
Expand Down Expand Up @@ -248,7 +247,6 @@ func NewMarketFromSnapshot(
expiringStopOrders: expiringStopOrders,
perp: marketType == types.MarketTypePerp,
partyMarginFactor: partyMargin,
liquidation: le,
banking: banking,
markPriceCalculator: markPriceCalculator,
}
Expand All @@ -266,6 +264,8 @@ func NewMarketFromSnapshot(
} else {
market.amm = amm.NewFromProto(log, broker, collateralEngine, market, market.risk, market.position, em.Amm, market.priceFactor, positionFactor, marketActivityTracker)
}
le := liquidation.New(log, mkt.LiquidationStrategy, mkt.GetID(), broker, book, as, timeService, positionEngine, pMonitor, market.amm)
market.liquidation = le
// now we can set the AMM on the market liquidity engine.
market.liquidity.SetAMM(market.amm)

Expand Down
11 changes: 9 additions & 2 deletions core/execution/liquidation/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
"code.vegaprotocol.io/vega/logging"
)

//go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/core/execution/liquidation Book,IDGen,Positions,PriceMonitor
//go:generate go run github.com/golang/mock/mockgen -destination mocks/mocks.go -package mocks code.vegaprotocol.io/vega/core/execution/liquidation Book,IDGen,Positions,PriceMonitor,AMM

type PriceMonitor interface {
GetValidPriceRange() (num.WrappedDecimal, num.WrappedDecimal)
Expand All @@ -42,6 +42,10 @@ type Book interface {
GetVolumeAtPrice(price *num.Uint, side types.Side) uint64
}

type AMM interface {
GetVolumeAtPrice(price *num.Uint, side types.Side) uint64
}

type IDGen interface {
NextID() string
}
Expand All @@ -65,6 +69,7 @@ type Engine struct {
position Positions
stopped bool
pmon PriceMonitor
amm AMM
}

// protocol upgrade - default values for existing markets/proposals.
Expand Down Expand Up @@ -100,7 +105,7 @@ func GetLegacyStrat() *types.LiquidationStrategy {
return legacyStrat.DeepClone()
}

func New(log *logging.Logger, cfg *types.LiquidationStrategy, mktID string, broker common.Broker, book Book, as common.AuctionState, tSvc common.TimeService, pe Positions, pmon PriceMonitor) *Engine {
func New(log *logging.Logger, cfg *types.LiquidationStrategy, mktID string, broker common.Broker, book Book, as common.AuctionState, tSvc common.TimeService, pe Positions, pmon PriceMonitor, amm AMM) *Engine {
// NOTE: This can be removed after protocol upgrade
if cfg == nil {
cfg = legacyStrat.DeepClone()
Expand All @@ -116,6 +121,7 @@ func New(log *logging.Logger, cfg *types.LiquidationStrategy, mktID string, brok
position: pe,
pos: &Pos{},
pmon: pmon,
amm: amm,
}
}

Expand Down Expand Up @@ -166,6 +172,7 @@ func (e *Engine) OnTick(ctx context.Context, now time.Time, midPrice *num.Uint)
size = uint64(num.DecimalFromFloat(float64(size)).Mul(e.cfg.DisposalFraction).Ceil().IntPart())
}
available := e.book.GetVolumeAtPrice(bound, bookSide)
available += e.amm.GetVolumeAtPrice(price, side)
if available == 0 {
return nil, nil
}
Expand Down
73 changes: 72 additions & 1 deletion core/execution/liquidation/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ type tstEngine struct {
tSvc *cmocks.MockTimeService
pos *mocks.MockPositions
pmon *mocks.MockPriceMonitor
amm *mocks.MockAMM
}

type marginStub struct {
Expand All @@ -58,6 +59,7 @@ type SliceLenMatcher[T any] int

func TestOrderbookPriceLimits(t *testing.T) {
t.Run("orderbook has no volume", testOrderbookHasNoVolume)
t.Run("orderbook has no volume, but vAMM's provide volume", testOrderbookEmptyButAMMVolume)
t.Run("orderbook has a volume of one (consumed fraction rounding)", testOrderbookFractionRounding)
t.Run("orderbook has plenty of volume (should not increase order size)", testOrderbookExceedsVolume)
t.Run("orderbook only has volume above price monitoring bounds", testOrderCappedByPriceMonitor)
Expand Down Expand Up @@ -108,18 +110,22 @@ func TestNetworkReducesOverTime(t *testing.T) {
midPrice := num.NewUint(100)

t.Run("call to ontick within the time step does nothing", func(t *testing.T) {
next := now.Add(config.DisposalTimeStep)
now = now.Add(2 * time.Second)
eng.as.EXPECT().InAuction().Times(1).Return(false)
order, err := eng.OnTick(ctx, now, midPrice)
require.Nil(t, order)
require.NoError(t, err)
ns := eng.GetNextCloseoutTS()
require.Equal(t, ns, next.UnixNano())
})

t.Run("after the time step passes, the first batch is disposed of", func(t *testing.T) {
now = now.Add(3 * time.Second)
eng.as.EXPECT().InAuction().Times(1).Return(false)
// return a large volume so the full step is disposed
eng.book.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(1000))
eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0))
order, err := eng.OnTick(ctx, now, midPrice)
require.NoError(t, err)
require.NotNil(t, order)
Expand Down Expand Up @@ -148,6 +154,7 @@ func TestNetworkReducesOverTime(t *testing.T) {
eng.as.EXPECT().InAuction().Times(1).Return(false)
// return a large volume so the full step is disposed
eng.book.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(1000))
eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0))
order, err := eng.OnTick(ctx, now, midPrice)
require.NoError(t, err)
require.NotNil(t, order)
Expand Down Expand Up @@ -179,6 +186,7 @@ func TestNetworkReducesOverTime(t *testing.T) {
eng.as.EXPECT().InAuction().Times(1).Return(false)
// return a large volume so the full step is disposed
eng.book.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(1000))
eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0))
order, err := eng.OnTick(ctx, now, midPrice)
require.NoError(t, err)
require.NotNil(t, order)
Expand All @@ -199,6 +207,7 @@ func TestNetworkReducesOverTime(t *testing.T) {
eng.as.EXPECT().InAuction().Times(1).Return(false)
// return a large volume so the full step is disposed
eng.book.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(1000))
eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0))
order, err = eng.OnTick(ctx, now, midPrice)
require.NoError(t, err)
require.NotNil(t, order)
Expand All @@ -221,6 +230,7 @@ func TestNetworkReducesOverTime(t *testing.T) {
eng.as.EXPECT().InAuction().Times(1).Return(false)
// return a large volume so the full step is disposed
eng.book.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(1000))
eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0))
order, err := eng.OnTick(ctx, now, midPrice)
require.NoError(t, err)
require.NotNil(t, order)
Expand Down Expand Up @@ -263,6 +273,8 @@ func testOrderbookHasNoVolume(t *testing.T) {
// now when we close out, the book returns a volume of 0 is available
eng.as.EXPECT().InAuction().Times(1).Return(false)
eng.book.EXPECT().GetVolumeAtPrice(minP, types.SideBuy).Times(1).Return(uint64(0))
// the side should represent the side of the order the network places.
eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), types.SideSell).Times(1).Return(uint64(0))
order, err := eng.OnTick(ctx, now, midPrice)
require.NoError(t, err)
require.Nil(t, order)
Expand Down Expand Up @@ -313,11 +325,64 @@ func testOrderbookFractionRounding(t *testing.T) {
minP, midPrice := num.UintZero(), num.NewUint(100)
eng.as.EXPECT().InAuction().Times(1).Return(false)
eng.book.EXPECT().GetVolumeAtPrice(minP, types.SideBuy).Times(1).Return(uint64(1))
eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0))
order, err := eng.OnTick(ctx, now, midPrice)
require.NoError(t, err)
require.Equal(t, uint64(1), order.Size)
}

func testOrderbookEmptyButAMMVolume(t *testing.T) {
mID := "market"
ctx := vegacontext.WithTraceID(context.Background(), vgcrypto.RandomHash())
config := types.LiquidationStrategy{
DisposalTimeStep: 0,
DisposalFraction: num.DecimalOne(),
FullDisposalSize: 1000000, // plenty
MaxFractionConsumed: num.DecimalFromFloat(0.5),
DisposalSlippage: num.DecimalFromFloat(10),
}
eng := getTestEngine(t, mID, &config)
require.Zero(t, eng.GetNextCloseoutTS())
defer eng.Finish()

eng.pmon.EXPECT().GetValidPriceRange().AnyTimes().Return(
num.NewWrappedDecimal(num.UintZero(), num.DecimalZero()),
num.NewWrappedDecimal(num.MaxUint(), num.MaxDecimal()),
)

closed := []events.Margin{
createMarginEvent("party", mID, 10),
}
var netVol int64
for _, c := range closed {
netVol += c.Size()
}
now := time.Now()
eng.tSvc.EXPECT().GetTimeNow().Times(2).Return(now)
idCount := len(closed) * 3
eng.idgen.EXPECT().NextID().Times(idCount).Return("nextID")
// 2 orders per closed position
eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](2 * len(closed))).Times(1)
// 1 trade per closed position
eng.broker.EXPECT().SendBatch(SliceLenMatcher[events.Event](1 * len(closed))).Times(1)
eng.pos.EXPECT().RegisterOrder(gomock.Any(), gomock.Any()).Times(len(closed) * 2)
eng.pos.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(len(closed))
pos, parties, trades := eng.ClearDistressedParties(ctx, eng.idgen, closed, num.UintZero(), num.UintZero())
require.Equal(t, len(closed), len(trades))
require.Equal(t, len(closed), len(pos))
require.Equal(t, len(closed), len(parties))
require.Equal(t, closed[0].Party(), parties[0])
minP, midPrice := num.UintZero(), num.NewUint(100)
eng.as.EXPECT().InAuction().Times(1).Return(false)
// no volume on the book
eng.book.EXPECT().GetVolumeAtPrice(minP, types.SideBuy).Times(1).Return(uint64(0))
// vAMM's have 100x the available volume, with a factor of 0.5, that's still 50x
eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), types.SideSell).Times(1).Return(uint64(netVol * 10))
order, err := eng.OnTick(ctx, now, midPrice)
require.NoError(t, err)
require.Equal(t, uint64(netVol), order.Size)
}

func testOrderbookExceedsVolume(t *testing.T) {
mID := "market"
ctx := vegacontext.WithTraceID(context.Background(), vgcrypto.RandomHash())
Expand Down Expand Up @@ -362,6 +427,7 @@ func testOrderbookExceedsVolume(t *testing.T) {
eng.as.EXPECT().InAuction().Times(1).Return(false)
// orderbook has 100x the available volume, with a factor of 0.5, that's still 50x
eng.book.EXPECT().GetVolumeAtPrice(minP, types.SideBuy).Times(1).Return(uint64(netVol * 10))
eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0))
order, err := eng.OnTick(ctx, now, midPrice)
require.NoError(t, err)
require.Equal(t, uint64(netVol), order.Size)
Expand Down Expand Up @@ -417,6 +483,7 @@ func testOrderCappedByPriceMonitor(t *testing.T) {

// we will check for volume at the price monitoring minimum
eng.book.EXPECT().GetVolumeAtPrice(minB, types.SideBuy).Times(1).Return(uint64(netVol * 10))
eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0))
order, err := eng.OnTick(ctx, now, midPrice)
require.NoError(t, err)
require.Equal(t, uint64(netVol), order.Size)
Expand Down Expand Up @@ -483,6 +550,8 @@ func TestLegacySupport(t *testing.T) {
minP, midPrice := num.UintZero(), num.NewUint(100)
eng.as.EXPECT().InAuction().Times(1).Return(false)
eng.book.EXPECT().GetVolumeAtPrice(minP, types.SideBuy).Times(1).Return(uint64(netVol))
// the side should be the side of the order placed by the network, the side used to call the matching engine is the opposite side
eng.amm.EXPECT().GetVolumeAtPrice(gomock.Any(), gomock.Any()).Times(1).Return(uint64(0))
order, err := eng.OnTick(ctx, now, midPrice)
require.NoError(t, err)
require.Equal(t, uint64(netVol), order.Size)
Expand Down Expand Up @@ -609,7 +678,8 @@ func getTestEngine(t *testing.T, marketID string, config *types.LiquidationStrat
tSvc := cmocks.NewMockTimeService(ctrl)
pe := mocks.NewMockPositions(ctrl)
pmon := mocks.NewMockPriceMonitor(ctrl)
engine := liquidation.New(logging.NewDevLogger(), config, marketID, broker, book, as, tSvc, pe, pmon)
amm := mocks.NewMockAMM(ctrl)
engine := liquidation.New(logging.NewDevLogger(), config, marketID, broker, book, as, tSvc, pe, pmon, amm)
return &tstEngine{
Engine: engine,
ctrl: ctrl,
Expand All @@ -620,6 +690,7 @@ func getTestEngine(t *testing.T, marketID string, config *types.LiquidationStrat
tSvc: tSvc,
pos: pe,
pmon: pmon,
amm: amm,
}
}

Expand Down
39 changes: 38 additions & 1 deletion core/execution/liquidation/mocks/mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit ba37236

Please sign in to comment.