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  }