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 }