code.vegaprotocol.io/vega@v0.79.0/core/matching/pricelevel.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 matching 17 18 import ( 19 "errors" 20 "fmt" 21 "sort" 22 23 "code.vegaprotocol.io/vega/core/types" 24 "code.vegaprotocol.io/vega/libs/num" 25 "code.vegaprotocol.io/vega/logging" 26 ) 27 28 var ( 29 // ErrWashTrade signals an attempt to a wash trade from a party. 30 ErrWashTrade = errors.New("party attempted to submit wash trade") 31 ErrFOKNotFilled = errors.New("FOK order could not be fully filled") 32 ) 33 34 // PriceLevel represents all the Orders placed at a given price. 35 type PriceLevel struct { 36 price *num.Uint 37 orders []*types.Order 38 volume uint64 39 } 40 41 // trackIceberg holds together information about iceberg orders while we are uncrossing 42 // so we can trade against them all again but distributed evenly. 43 type trackIceberg struct { 44 // the iceberg order 45 order *types.Order 46 // the trade that occurred with the icebergs visible peak 47 trade *types.Trade 48 // the index of the iceberg order in the price-level slice 49 idx int 50 } 51 52 func (t *trackIceberg) reservedRemaining() uint64 { 53 return t.order.IcebergOrder.ReservedRemaining 54 } 55 56 // NewPriceLevel instantiate a new PriceLevel. 57 func NewPriceLevel(price *num.Uint) *PriceLevel { 58 return &PriceLevel{ 59 price: price, 60 orders: []*types.Order{}, 61 } 62 } 63 64 func (l *PriceLevel) reduceVolume(reduceBy uint64) { 65 l.volume -= reduceBy 66 } 67 68 func (l *PriceLevel) getOrdersByParty(partyID string) []*types.Order { 69 ret := []*types.Order{} 70 for _, o := range l.orders { 71 if o.Party == partyID { 72 ret = append(ret, o) 73 } 74 } 75 return ret 76 } 77 78 func (l *PriceLevel) addOrder(o *types.Order) { 79 // add orders to slice of orders on this price level 80 l.orders = append(l.orders, o) 81 l.volume += o.TrueRemaining() 82 } 83 84 func (l *PriceLevel) removeOrder(index int) { 85 // decrease total volume 86 l.volume -= l.orders[index].TrueRemaining() 87 // remove the orders at index 88 copy(l.orders[index:], l.orders[index+1:]) 89 l.orders = l.orders[:len(l.orders)-1] 90 } 91 92 // uncrossIcebergs when a large aggressive order consumes the peak of iceberg orders, we trade with the hidden portion of 93 // the icebergs such that when they are refreshed the book does not cross. 94 func (l *PriceLevel) uncrossIcebergs(agg *types.Order, tracked []*trackIceberg, fake bool) ([]*types.Trade, []*types.Order) { 95 var totalReserved uint64 96 for _, t := range tracked { 97 totalReserved += t.reservedRemaining() 98 } 99 100 if totalReserved == 0 { 101 // nothing to do 102 return nil, nil 103 } 104 105 // either the amount left of the aggressive order, or the rest of all the iceberg orders 106 totalCrossed := num.MinV(agg.Remaining, totalReserved) 107 108 // let do it with decimals 109 totalCrossedDec := num.DecimalFromInt64(int64(totalCrossed)) 110 totalReservedDec := num.DecimalFromInt64(int64(totalReserved)) 111 112 // divide up between icebergs 113 var sum uint64 114 extraTraded := []uint64{} 115 for _, t := range tracked { 116 rr := num.DecimalFromInt64(int64(t.reservedRemaining())) 117 extra := uint64(rr.Mul(totalCrossedDec).Div(totalReservedDec).IntPart()) 118 sum += extra 119 extraTraded = append(extraTraded, extra) 120 } 121 122 // if there is some left over due to the rounding when dividing then 123 // it is traded against the iceberg with the highest time priority 124 if rem := totalCrossed - sum; rem > 0 { 125 for i, t := range tracked { 126 max := t.reservedRemaining() - extraTraded[i] 127 dd := num.MinV(max, rem) // can allocate the smallest of the remainder and whats left in the berg 128 129 extraTraded[i] += dd 130 rem -= dd 131 132 if rem == 0 { 133 break 134 } 135 } 136 if rem != 0 { 137 panic("unable to distribute rounding crumbs between iceberg orders") 138 } 139 } 140 141 // increase traded sizes based on consumed hidden iceberg volume 142 newTrades := []*types.Trade{} 143 newImpacted := []*types.Order{} 144 for i, t := range tracked { 145 extra := extraTraded[i] 146 agg.Remaining -= extra 147 148 // if there was not a previous trade with the iceberg's peak, make a fresh one 149 if t.trade == nil { 150 t.trade = newTrade(agg, t.order, 0) 151 newTrades = append(newTrades, t.trade) 152 newImpacted = append(newImpacted, t.order) 153 } 154 t.trade.Size += extra 155 156 if !fake { 157 // only change values in passive orders if uncrossing is for real and not just to see potential trades. 158 t.order.IcebergOrder.ReservedRemaining -= extra 159 l.volume -= extra 160 } 161 } 162 return newTrades, newImpacted 163 } 164 165 // fakeUncross - this updates a copy of the order passed to it, the copied order is returned. 166 func (l *PriceLevel) fakeUncross(o *types.Order, checkWashTrades bool) (agg *types.Order, trades []*types.Trade, err error) { 167 // work on a copy of the order, so we can submit it a second time 168 // after we've done the price monitoring and fees checks 169 agg = o.Clone() 170 if len(l.orders) == 0 { 171 return 172 } 173 174 icebergs := []*trackIceberg{} 175 for i, order := range l.orders { 176 if checkWashTrades { 177 if order.Party == agg.Party { 178 err = ErrWashTrade 179 return 180 } 181 } 182 183 // Get size and make newTrade 184 size := l.getVolumeAllocation(agg, order) 185 if size <= 0 { 186 // this is only fine if it is an iceberg order with only reserve and in that case 187 // we need to trade with it later in uncrossIcebergs 188 if order.IcebergOrder != nil && 189 order.Remaining == 0 && 190 order.IcebergOrder.ReservedRemaining != 0 { 191 icebergs = append(icebergs, &trackIceberg{order, nil, i}) 192 continue 193 } 194 195 panic("Trade.size > order.remaining") 196 } 197 198 // New Trade 199 trade := newTrade(agg, order, size) 200 trade.SellOrder = agg.ID 201 trade.BuyOrder = order.ID 202 if agg.Side == types.SideBuy { 203 trade.SellOrder, trade.BuyOrder = trade.BuyOrder, trade.SellOrder 204 } 205 206 // Update Remaining for aggressive only 207 agg.Remaining -= size 208 209 // Update trades 210 trades = append(trades, trade) 211 212 // if the passive order is an iceberg with a hidden quantity make a note of it and 213 // its trade incase we need to uncross further 214 if order.IcebergOrder != nil && order.IcebergOrder.ReservedRemaining > 0 { 215 icebergs = append(icebergs, &trackIceberg{order, trade, i}) 216 } 217 218 // Exit when done 219 if agg.Remaining == 0 { 220 break 221 } 222 } 223 224 // if the aggressive trade is not filled uncross with iceberg hidden quantity 225 if agg.Remaining != 0 && len(icebergs) > 0 { 226 newTrades, _ := l.uncrossIcebergs(agg, icebergs, true) 227 trades = append(trades, newTrades...) 228 } 229 230 return agg, trades, err 231 } 232 233 func (l *PriceLevel) uncross(agg *types.Order, checkWashTrades bool) (filled bool, trades []*types.Trade, impactedOrders []*types.Order, err error) { 234 // for some reason sometimes it seems the pricelevels are not deleted when getting empty 235 // no big deal, just return early 236 if len(l.orders) <= 0 { 237 return 238 } 239 240 var ( 241 icebergs []*trackIceberg 242 toRemove []int 243 removed int 244 ) 245 246 // l.orders is always sorted by timestamps, that is why when iterating we always start from the beginning 247 for i, order := range l.orders { 248 // prevent wash trade 249 if checkWashTrades { 250 if order.Party == agg.Party { 251 err = ErrWashTrade 252 break 253 } 254 } 255 256 // Get size and make newTrade 257 size := l.getVolumeAllocation(agg, order) 258 259 if size <= 0 { 260 // this is only fine if it is an iceberg order with only reserve and in that case 261 // we need to trade with it later in uncrossIcebergs 262 if order.IcebergOrder != nil && 263 order.Remaining == 0 && 264 order.IcebergOrder.ReservedRemaining != 0 { 265 icebergs = append(icebergs, &trackIceberg{order, nil, i}) 266 continue 267 } 268 panic("Trade.size > order.remaining") 269 } 270 271 // New Trade 272 trade := newTrade(agg, order, size) 273 trade.SellOrder, trade.BuyOrder = agg.ID, order.ID 274 if agg.Side == types.SideBuy { 275 trade.SellOrder, trade.BuyOrder = trade.BuyOrder, trade.SellOrder 276 } 277 278 // Update Remaining for both aggressive and passive 279 agg.Remaining -= size 280 order.Remaining -= size 281 l.volume -= size 282 283 if order.TrueRemaining() == 0 { 284 toRemove = append(toRemove, i) 285 } 286 287 // Update trades 288 trades = append(trades, trade) 289 impactedOrders = append(impactedOrders, order) 290 291 // if the passive order is an iceberg with a hidden quantity make a note of it and 292 // its trade incase we need to uncross further 293 if order.IcebergOrder != nil && order.IcebergOrder.ReservedRemaining > 0 { 294 icebergs = append(icebergs, &trackIceberg{order, trade, i}) 295 } 296 297 // Exit when done 298 if agg.Remaining == 0 { 299 break 300 } 301 } 302 303 // if the aggressive trade is not filled uncross with iceberg hidden reserves 304 if agg.Remaining > 0 && len(icebergs) > 0 { 305 newTrades, newImpacted := l.uncrossIcebergs(agg, icebergs, false) 306 trades = append(trades, newTrades...) 307 impactedOrders = append(impactedOrders, newImpacted...) 308 309 // only remove fully depleted icebergs, icebergs with 0 remaining but some in reserve 310 // stay at the pricelevel until they refresh at the end of execution, or the end of auction uncrossing 311 for _, t := range icebergs { 312 if t.order.TrueRemaining() == 0 { 313 toRemove = append(toRemove, t.idx) 314 } 315 } 316 sort.Ints(toRemove) 317 } 318 319 // FIXME(jeremy): these need to be optimized, we can make a single copy 320 // just by keep the index of the last order which is to remove as they 321 // are all order, then just copy the second part of the slice in the actual s[0] 322 if len(toRemove) > 0 { 323 for _, idx := range toRemove { 324 copy(l.orders[idx-removed:], l.orders[idx-removed+1:]) 325 removed++ 326 } 327 l.orders = l.orders[:len(l.orders)-removed] 328 } 329 330 return agg.Remaining == 0, trades, impactedOrders, err 331 } 332 333 func (l *PriceLevel) getVolumeAllocation(agg, pass *types.Order) uint64 { 334 return min(agg.Remaining, pass.Remaining) 335 } 336 337 // Returns the min of 2 uint64s. 338 func min(x, y uint64) uint64 { 339 if y < x { 340 return y 341 } 342 return x 343 } 344 345 // Returns the max of 2 uint64s. 346 func max(x, y uint64) uint64 { 347 if x > y { 348 return x 349 } 350 return y 351 } 352 353 // Creates a trade of a given size between two orders and updates the order details. 354 func newTrade(agg, pass *types.Order, size uint64) *types.Trade { 355 var buyer, seller *types.Order 356 if agg.Side == types.SideBuy { 357 buyer = agg 358 seller = pass 359 } else { 360 buyer = pass 361 seller = agg 362 } 363 364 if agg.Side == pass.Side { 365 panic(fmt.Sprintf("agg.side == pass.side (agg: %v, pass: %v)", agg, pass)) 366 } 367 368 return &types.Trade{ 369 Type: types.TradeTypeDefault, 370 MarketID: agg.MarketID, 371 Price: pass.Price.Clone(), 372 MarketPrice: pass.OriginalPrice.Clone(), 373 Size: size, 374 Aggressor: agg.Side, 375 Buyer: buyer.Party, 376 Seller: seller.Party, 377 Timestamp: agg.CreatedAt, 378 } 379 } 380 381 func (l PriceLevel) print(log *logging.Logger) { 382 log.Debug(fmt.Sprintf("priceLevel: %d\n", l.price)) 383 for _, o := range l.orders { 384 var side string 385 if o.Side == types.SideBuy { 386 side = "BUY" 387 } else { 388 side = "SELL" 389 } 390 391 log.Debug(fmt.Sprintf(" %s %s @%d size=%d R=%d Type=%d T=%d %s\n", 392 o.Party, side, o.Price, o.Size, o.Remaining, o.TimeInForce, o.CreatedAt, o.ID)) 393 } 394 }