code.vegaprotocol.io/vega@v0.79.0/core/execution/liquidation/engine.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package liquidation
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"math"
    22  	"time"
    23  
    24  	"code.vegaprotocol.io/vega/core/events"
    25  	"code.vegaprotocol.io/vega/core/execution/common"
    26  	"code.vegaprotocol.io/vega/core/idgeneration"
    27  	"code.vegaprotocol.io/vega/core/positions"
    28  	"code.vegaprotocol.io/vega/core/types"
    29  	vegacontext "code.vegaprotocol.io/vega/libs/context"
    30  	"code.vegaprotocol.io/vega/libs/crypto"
    31  	"code.vegaprotocol.io/vega/libs/num"
    32  	"code.vegaprotocol.io/vega/logging"
    33  )
    34  
    35  //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
    36  
    37  type PriceMonitor interface {
    38  	GetValidPriceRange() (num.WrappedDecimal, num.WrappedDecimal)
    39  }
    40  
    41  type Book interface {
    42  	GetVolumeAtPrice(price *num.Uint, side types.Side) uint64
    43  }
    44  
    45  type AMM interface {
    46  	GetVolumeAtPrice(price *num.Uint, side types.Side) uint64
    47  }
    48  
    49  type IDGen interface {
    50  	NextID() string
    51  }
    52  
    53  type Positions interface {
    54  	RegisterOrder(ctx context.Context, order *types.Order) *positions.MarketPosition
    55  	Update(ctx context.Context, trade *types.Trade, passiveOrder, aggressiveOrder *types.Order) []events.MarketPosition
    56  }
    57  
    58  type Engine struct {
    59  	// settings, orderbook, network pos data
    60  	log      *logging.Logger
    61  	cfg      *types.LiquidationStrategy
    62  	broker   common.Broker
    63  	mID      string
    64  	pos      *Pos
    65  	book     Book
    66  	as       common.AuctionState
    67  	nextStep time.Time
    68  	tSvc     common.TimeService
    69  	position Positions
    70  	stopped  bool
    71  	pmon     PriceMonitor
    72  	amm      AMM
    73  }
    74  
    75  // protocol upgrade - default values for existing markets/proposals.
    76  var (
    77  	defaultStrat = &types.LiquidationStrategy{
    78  		DisposalTimeStep:    10 * time.Second,
    79  		DisposalFraction:    num.DecimalFromFloat(0.1),
    80  		FullDisposalSize:    20,
    81  		MaxFractionConsumed: num.DecimalFromFloat(0.05),
    82  		DisposalSlippage:    num.DecimalFromFloat(0.1),
    83  	}
    84  
    85  	// this comes closest to the existing behaviour (trying to close the network position in full in one go).
    86  	legacyStrat = &types.LiquidationStrategy{
    87  		DisposalTimeStep:    0 * time.Second,
    88  		DisposalFraction:    num.DecimalOne(),
    89  		FullDisposalSize:    math.MaxUint64,
    90  		MaxFractionConsumed: num.DecimalOne(),
    91  		DisposalSlippage:    num.DecimalFromFloat(10.0),
    92  	}
    93  )
    94  
    95  // GetDefaultStrat is exporeted, expected to be used to update existing proposals on protocol upgrade
    96  // once that's happened, this code can be removed.
    97  func GetDefaultStrat() *types.LiquidationStrategy {
    98  	return defaultStrat.DeepClone()
    99  }
   100  
   101  // GetLegacyStrat is exported, same as defaul. This can be used for protocol upgrade
   102  // it most closely resebles the old behaviour (network attempts to close out fully, in one go)
   103  // this can be removed once protocol upgrade has completed.
   104  func GetLegacyStrat() *types.LiquidationStrategy {
   105  	return legacyStrat.DeepClone()
   106  }
   107  
   108  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 {
   109  	// NOTE: This can be removed after protocol upgrade
   110  	if cfg == nil {
   111  		cfg = legacyStrat.DeepClone()
   112  	}
   113  	return &Engine{
   114  		log:      log,
   115  		cfg:      cfg,
   116  		broker:   broker,
   117  		mID:      mktID,
   118  		book:     book,
   119  		as:       as,
   120  		tSvc:     tSvc,
   121  		position: pe,
   122  		pos:      &Pos{},
   123  		pmon:     pmon,
   124  		amm:      amm,
   125  	}
   126  }
   127  
   128  func (e *Engine) Update(cfg *types.LiquidationStrategy) {
   129  	if !e.nextStep.IsZero() {
   130  		since := e.nextStep.Add(-e.cfg.DisposalTimeStep) // work out when the network position was last updated
   131  		e.nextStep = since.Add(cfg.DisposalTimeStep)
   132  	}
   133  	// now update the config
   134  	e.cfg = cfg
   135  }
   136  
   137  func (e *Engine) OnTick(ctx context.Context, now time.Time, midPrice *num.Uint) (*types.Order, error) {
   138  	if e.pos.open == 0 || e.as.InAuction() || e.nextStep.After(now) || midPrice.IsZero() {
   139  		return nil, nil
   140  	}
   141  
   142  	one := num.DecimalOne()
   143  	// get the min/max price from the range based on slippage parameter
   144  	mpDec := num.DecimalFromUint(midPrice)
   145  	minP := num.UintZero()
   146  	if e.cfg.DisposalSlippage.LessThan(one) {
   147  		minD := mpDec.Mul(one.Sub(e.cfg.DisposalSlippage))
   148  		minP, _ = num.UintFromDecimal(minD)
   149  	}
   150  	maxD := mpDec.Mul(one.Add(e.cfg.DisposalSlippage))
   151  	maxP, _ := num.UintFromDecimal(maxD)
   152  
   153  	minB, maxB := e.pmon.GetValidPriceRange()
   154  
   155  	// cap to price monitor bounds
   156  	minP = num.Max(minP, minB.Representation())
   157  	maxP = num.Min(maxP, maxB.Representation())
   158  
   159  	vol := e.pos.open
   160  	bookSide := types.SideBuy
   161  	side := types.SideSell
   162  	bound := minP
   163  	price := minP
   164  	if vol < 0 {
   165  		vol *= -1
   166  		side, bookSide = bookSide, side
   167  		price, bound = maxP, maxP
   168  	}
   169  	size := uint64(vol)
   170  	if size > e.cfg.FullDisposalSize {
   171  		// absolute size of network position * disposal fraction -> rounded
   172  		size = uint64(num.DecimalFromFloat(float64(size)).Mul(e.cfg.DisposalFraction).Ceil().IntPart())
   173  	}
   174  	available := e.book.GetVolumeAtPrice(bound, bookSide)
   175  	available += e.amm.GetVolumeAtPrice(price, side)
   176  	if available == 0 {
   177  		return nil, nil
   178  	}
   179  	// round up, avoid a value like 0.1 to be floored, favour closing out a position of 1 at least
   180  	maxCons := uint64(num.DecimalFromFloat(float64(available)).Mul(e.cfg.MaxFractionConsumed).Ceil().IntPart())
   181  	if maxCons < size {
   182  		size = maxCons
   183  	}
   184  	// get the block hash
   185  	_, blockHash := vegacontext.TraceIDFromContext(ctx)
   186  	idgen := idgeneration.New(blockHash + crypto.HashStrToHex("networkLS"+e.mID))
   187  	// set time for next order, if the position ends up closed out, then that's fine
   188  	// we'll remove this time when the position is updated
   189  	if size == 0 {
   190  		return nil, nil
   191  	}
   192  	e.nextStep = now.Add(e.cfg.DisposalTimeStep)
   193  	// place order using size
   194  	return &types.Order{
   195  		ID:          idgen.NextID(),
   196  		MarketID:    e.mID,
   197  		Party:       types.NetworkParty,
   198  		Side:        side,
   199  		Price:       price,
   200  		Size:        size,
   201  		Remaining:   size,
   202  		TimeInForce: types.OrderTimeInForceIOC,
   203  		Type:        types.OrderTypeLimit,
   204  		CreatedAt:   now.UnixNano(),
   205  		Status:      types.OrderStatusActive,
   206  		Reference:   "LS", // Liquidity sourcing
   207  	}, nil
   208  }
   209  
   210  // ClearDistressedParties transfers the open positions to the network, returns the market position events and party ID's
   211  // for the market to remove the parties from things like positions engine and collateral.
   212  func (e *Engine) ClearDistressedParties(ctx context.Context, idgen IDGen, closed []events.Margin, mp, mmp *num.Uint) ([]events.MarketPosition, []string, []*types.Trade) {
   213  	if len(closed) == 0 {
   214  		return nil, nil, nil
   215  	}
   216  	// netork is most likely going to hold an actual position now, let's set up the time step when we attempt to dispose
   217  	// of (some) of the volume
   218  	if e.pos.open == 0 || e.nextStep.IsZero() {
   219  		e.nextStep = e.tSvc.GetTimeNow().Add(e.cfg.DisposalTimeStep)
   220  	}
   221  	mps := make([]events.MarketPosition, 0, len(closed))
   222  	parties := make([]string, 0, len(closed))
   223  	// order events here
   224  	orders := make([]events.Event, 0, len(closed)*2)
   225  	// trade events here
   226  	trades := make([]events.Event, 0, len(closed))
   227  	netTrades := make([]*types.Trade, 0, len(closed))
   228  	now := e.tSvc.GetTimeNow()
   229  	for _, cp := range closed {
   230  		e.pos.open += cp.Size()
   231  		// get the orders and trades so we can send events to update the datanode
   232  		o1, o2, t := e.getOrdersAndTrade(ctx, cp, idgen, now, mp, mmp)
   233  		orders = append(orders, events.NewOrderEvent(ctx, o1), events.NewOrderEvent(ctx, o2))
   234  		trades = append(trades, events.NewTradeEvent(ctx, *t))
   235  		netTrades = append(netTrades, t)
   236  		// add the confiscated balance to the fee pool that can be taken from the insurance pool to pay fees to
   237  		// the good parties when the network closes itself out.
   238  		mps = append(mps, cp)
   239  		parties = append(parties, cp.Party())
   240  	}
   241  	// send order events
   242  	e.broker.SendBatch(orders)
   243  	// send trade events
   244  	e.broker.SendBatch(trades)
   245  	// the network has no (more) remaining open position -> no need for the e.nextStep to be set
   246  	e.log.Info("network position after close-out", logging.Int64("network-position", e.pos.open))
   247  	if e.pos.open == 0 {
   248  		e.nextStep = time.Time{}
   249  	}
   250  	return mps, parties, netTrades
   251  }
   252  
   253  func (e *Engine) UpdateMarkPrice(mp *num.Uint) {
   254  	e.pos.price = mp
   255  }
   256  
   257  func (e *Engine) GetNetworkPosition() events.MarketPosition {
   258  	return e.pos
   259  }
   260  
   261  func (e *Engine) UpdateNetworkPosition(trades []*types.Trade) {
   262  	sign := int64(1)
   263  	if e.pos.open < 0 {
   264  		sign *= -1
   265  	}
   266  	for _, t := range trades {
   267  		delta := int64(t.Size) * sign
   268  		e.pos.open -= delta
   269  	}
   270  	if e.pos.open == 0 {
   271  		e.nextStep = time.Time{}
   272  	} else if e.nextStep.IsZero() {
   273  		e.nextStep = e.tSvc.GetTimeNow().Add(e.cfg.DisposalTimeStep)
   274  	}
   275  }
   276  
   277  func (e *Engine) getOrdersAndTrade(ctx context.Context, pos events.Margin, idgen IDGen, now time.Time, price, dpPrice *num.Uint) (*types.Order, *types.Order, *types.Trade) {
   278  	tSide, nSide := types.SideSell, types.SideBuy // one of them will have to sell
   279  	s := pos.Size()
   280  	size := uint64(s)
   281  	if s < 0 {
   282  		size = uint64(-s)
   283  		// swap sides
   284  		nSide, tSide = tSide, nSide
   285  	}
   286  	var buyID, sellID, buyParty, sellParty string
   287  	order := types.Order{
   288  		ID:            idgen.NextID(),
   289  		MarketID:      e.mID,
   290  		Status:        types.OrderStatusFilled,
   291  		Party:         types.NetworkParty,
   292  		Price:         price,
   293  		OriginalPrice: dpPrice,
   294  		CreatedAt:     now.UnixNano(),
   295  		Reference:     "close-out distressed",
   296  		TimeInForce:   types.OrderTimeInForceFOK, // this is an all-or-nothing order, so TIME_IN_FORCE == FOK
   297  		Type:          types.OrderTypeNetwork,
   298  		Size:          size,
   299  		Remaining:     size,
   300  		Side:          nSide,
   301  	}
   302  	e.position.RegisterOrder(ctx, &order)
   303  	order.Remaining = 0
   304  	partyOrder := types.Order{
   305  		ID:            idgen.NextID(),
   306  		MarketID:      e.mID,
   307  		Size:          size,
   308  		Remaining:     size,
   309  		Status:        types.OrderStatusFilled,
   310  		Party:         pos.Party(),
   311  		Side:          tSide, // assume sell, price is zero in that case anyway
   312  		Price:         price, // average price
   313  		OriginalPrice: dpPrice,
   314  		CreatedAt:     now.UnixNano(),
   315  		Reference:     fmt.Sprintf("distressed-%s", pos.Party()),
   316  		TimeInForce:   types.OrderTimeInForceFOK, // this is an all-or-nothing order, so TIME_IN_FORCE == FOK
   317  		Type:          types.OrderTypeNetwork,
   318  	}
   319  	e.position.RegisterOrder(ctx, &partyOrder)
   320  	partyOrder.Remaining = 0
   321  	buyParty = order.Party
   322  	sellParty = partyOrder.Party
   323  	sellID = partyOrder.ID
   324  	buyID = order.ID
   325  	if tSide == types.SideBuy {
   326  		sellID, buyID = buyID, sellID
   327  		buyParty, sellParty = sellParty, buyParty
   328  	}
   329  	trade := types.Trade{
   330  		ID:          idgen.NextID(),
   331  		MarketID:    e.mID,
   332  		Price:       price,
   333  		MarketPrice: dpPrice,
   334  		Size:        size,
   335  		Aggressor:   order.Side, // we consider network to be aggressor
   336  		BuyOrder:    buyID,
   337  		SellOrder:   sellID,
   338  		Buyer:       buyParty,
   339  		Seller:      sellParty,
   340  		Timestamp:   now.UnixNano(),
   341  		Type:        types.TradeTypeNetworkCloseOutBad,
   342  		SellerFee:   types.NewFee(),
   343  		BuyerFee:    types.NewFee(),
   344  	}
   345  	// the for the rest of the core, this should not seem like a wash trade though...
   346  	e.position.Update(ctx, &trade, &order, &partyOrder)
   347  	return &order, &partyOrder, &trade
   348  }
   349  
   350  func (e *Engine) GetNextCloseoutTS() int64 {
   351  	if e.nextStep.IsZero() {
   352  		return 0
   353  	}
   354  	return e.nextStep.UnixNano()
   355  }