decred.org/dcrdex@v1.0.5/server/matcher/match.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package matcher
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"math/rand"
    10  	"sort"
    11  
    12  	"decred.org/dcrdex/dex/calc"
    13  	"decred.org/dcrdex/dex/order"
    14  	"decred.org/dcrdex/server/matcher/mt19937"
    15  	"github.com/decred/dcrd/crypto/blake256"
    16  )
    17  
    18  // HashFunc is the hash function used to generate the shuffling seed.
    19  var (
    20  	HashFunc    = blake256.Sum256
    21  	BaseToQuote = calc.BaseToQuote
    22  	QuoteToBase = calc.QuoteToBase
    23  )
    24  
    25  const (
    26  	HashSize = blake256.Size
    27  
    28  	peSize = order.PreimageSize
    29  )
    30  
    31  type Matcher struct{}
    32  
    33  // New creates a new Matcher.
    34  func New() *Matcher {
    35  	return &Matcher{}
    36  }
    37  
    38  // orderLotSizeOK checks if the remaining Order quantity is not a multiple of
    39  // lot size, unless the order is a market buy order, which is not subject to
    40  // this constraint.
    41  func orderLotSizeOK(ord order.Order, lotSize uint64) bool {
    42  	var remaining uint64
    43  	switch o := ord.(type) {
    44  	case *order.CancelOrder:
    45  		// NOTE: Cancel orders have 0 remaining by definition.
    46  		return true
    47  	case *order.MarketOrder:
    48  		if !o.Sell {
    49  			return true
    50  		}
    51  		remaining = o.Remaining()
    52  	case *order.LimitOrder:
    53  		remaining = o.Remaining()
    54  	}
    55  	return remaining%lotSize == 0
    56  }
    57  
    58  // assertOrderLotSize will panic if the remaining Order quantity is not a
    59  // multiple of lot size, unless the order is a market buy order.
    60  func assertOrderLotSize(ord order.Order, lotSize uint64) {
    61  	if orderLotSizeOK(ord, lotSize) {
    62  		return
    63  	}
    64  	var remaining uint64
    65  	if ord.Trade() != nil {
    66  		remaining = ord.Trade().Remaining()
    67  	}
    68  	panic(fmt.Sprintf(
    69  		"order %v has remaining quantity %d that is not a multiple of lot size %d",
    70  		ord.ID(), remaining, lotSize))
    71  }
    72  
    73  // CheckMarketBuyBuffer verifies that the given market buy order's quantity
    74  // (specified in quote asset) is sufficient according to the Matcher's
    75  // configured market buy buffer, which is in base asset units, and the best
    76  // standing sell order according to the provided Booker.
    77  func CheckMarketBuyBuffer(book Booker, ord *order.MarketOrder, marketBuyBuffer float64) bool {
    78  	if ord.Sell {
    79  		return true // The market buy buffer does not apply to sell orders.
    80  	}
    81  	minBaseAsset := uint64(marketBuyBuffer * float64(book.LotSize()))
    82  	return ord.Remaining() >= BaseToQuote(book.BestSell().Rate, minBaseAsset)
    83  }
    84  
    85  // OrderRevealed combines an Order interface with a Preimage.
    86  type OrderRevealed struct {
    87  	Order    order.Order // Do not embed so OrderRevealed is not an order.Order.
    88  	Preimage order.Preimage
    89  }
    90  
    91  // OrdersUpdated represents the orders updated by (*Matcher).Match, and may
    92  // include orders from both the epoch queue and the book. Trade orders that are
    93  // not in TradesFailed may appear in multiple other trade order slices, so
    94  // update them in sequence: booked, partial, and finally completed or canceled.
    95  // Orders in TradesFailed will not be in another slice since failed indicates
    96  // unmatched&unbooked or bad lot size. Cancel orders may only be in one of
    97  // CancelsExecuted or CancelsFailed.
    98  type OrdersUpdated struct {
    99  	// CancelsExecuted are cancel orders that were matched to and removed
   100  	// another order from the book.
   101  	CancelsExecuted []*order.CancelOrder
   102  	// CancelsFailed are cancel orders that failed to match and cancel an order.
   103  	CancelsFailed []*order.CancelOrder
   104  
   105  	// TradesFailed are unmatched and unbooked (i.e. unmatched market or limit
   106  	// with immediate time-in-force), or orders with bad lot size. These orders
   107  	// will be in no other slice.
   108  	TradesFailed []order.Order
   109  
   110  	// TradesBooked are limit orders from the epoch queue that were put on the
   111  	// book. Because of downstream epoch processing, these may also be in
   112  	// TradesPartial, and TradesCompleted or TradesCanceled.
   113  	TradesBooked []*order.LimitOrder
   114  	// TradesPartial are limit orders that were partially filled as maker orders
   115  	// (while on the book). Epoch orders that were partially filled prior to
   116  	// being booked (while takers) are not necessarily in TradesPartial unless
   117  	// they are filled again by a taker. This is because all status updates also
   118  	// update the filled amount.
   119  	TradesPartial []*order.LimitOrder
   120  	// TradesCanceled are limit orders that were removed from the the book by a
   121  	// cancel order. These may also be in TradesBooked and TradesPartial, but
   122  	// not TradesCompleted.
   123  	TradesCanceled []*order.LimitOrder
   124  	// TradesCompleted are market or limit orders that filled to completion. For
   125  	// a market order, this means it had a match that partially or completely
   126  	// filled it. For a limit order, this means the time-in-force is immediate
   127  	// with at least one match for any amount, or the time-in-force is standing
   128  	// and it is completely filled.
   129  	TradesCompleted []order.Order
   130  }
   131  
   132  func (ou *OrdersUpdated) String() string {
   133  	return fmt.Sprintf("cExec=%d, cFail=%d, tPartial=%d, tBooked=%d, tCanceled=%d, tComp=%d, tFail=%d",
   134  		len(ou.CancelsExecuted), len(ou.CancelsFailed), len(ou.TradesPartial), len(ou.TradesBooked),
   135  		len(ou.TradesCanceled), len(ou.TradesCompleted), len(ou.TradesFailed))
   136  }
   137  
   138  // Match matches orders given a standing order book and an epoch queue. Matched
   139  // orders from the book are removed from the book. The EpochID of the MatchSet
   140  // is not set. passed = booked + doneOK. queue = passed + failed. unbooked may
   141  // include orders that are not in the queue. Each of partial are in passed.
   142  // nomatched are orders that did not match anything, and discludes booked
   143  // limit orders that only matched as makers to down-queue takers.
   144  //
   145  // TODO: Eliminate order slice return args in favor of just the *OrdersUpdated.
   146  func (m *Matcher) Match(book Booker, queue []*OrderRevealed) (seed []byte, matches []*order.MatchSet,
   147  	passed, failed, doneOK, partial, booked, nomatched []*OrderRevealed,
   148  	unbooked []*order.LimitOrder, updates *OrdersUpdated, stats *MatchCycleStats) {
   149  
   150  	// Apply the deterministic pseudorandom shuffling.
   151  	seed = shuffleQueue(queue)
   152  
   153  	updates = new(OrdersUpdated)
   154  	stats = new(MatchCycleStats)
   155  
   156  	appendTradeSet := func(matchSet *order.MatchSet) {
   157  		matches = append(matches, matchSet)
   158  
   159  		stats.MatchVolume += matchSet.Total
   160  		high, low := matchSet.HighLowRates()
   161  		if high > stats.HighRate {
   162  			stats.HighRate = high
   163  		}
   164  		if low < stats.LowRate || stats.LowRate == 0 {
   165  			stats.LowRate = low
   166  		}
   167  		stats.QuoteVolume += matchSet.QuoteVolume()
   168  	}
   169  
   170  	// Store partially filled limit orders in a map to avoid duplicate
   171  	// entries.
   172  	partialMap := make(map[order.OrderID]*order.LimitOrder)
   173  
   174  	// Track initially unmatched standing limit orders and remove them if they
   175  	// are matched down-queue so that they aren't added to the nomatched slice.
   176  	nomatchStanding := make(map[order.OrderID]*OrderRevealed)
   177  
   178  	tallyMakers := func(makers []*order.LimitOrder) {
   179  		for _, maker := range makers {
   180  			delete(nomatchStanding, maker.ID())
   181  			if maker.Remaining() == 0 {
   182  				unbooked = append(unbooked, maker)
   183  				updates.TradesCompleted = append(updates.TradesCompleted, maker)
   184  				delete(partialMap, maker.ID())
   185  			} else {
   186  				partialMap[maker.ID()] = maker
   187  			}
   188  		}
   189  	}
   190  
   191  	// For each order in the queue, find the best match in the book.
   192  	for _, q := range queue {
   193  		if !orderLotSizeOK(q.Order, book.LotSize()) {
   194  			log.Warnf("Order with bad lot size in the queue: %v!", q.Order.ID())
   195  			failed = append(failed, q)
   196  			updates.TradesFailed = append(updates.TradesFailed, q.Order)
   197  			continue
   198  		}
   199  
   200  		switch o := q.Order.(type) {
   201  		case *order.CancelOrder:
   202  			removed, ok := book.Remove(o.TargetOrderID)
   203  			if !ok {
   204  				// The targeted order might be down queue or non-existent.
   205  				log.Debugf("Order %v not removed by a cancel order %v (target either non-existent or down queue in this epoch)",
   206  					o.ID(), o.TargetOrderID)
   207  				failed = append(failed, q)
   208  				updates.CancelsFailed = append(updates.CancelsFailed, o)
   209  				nomatched = append(nomatched, q)
   210  				continue
   211  			}
   212  
   213  			passed = append(passed, q)
   214  			doneOK = append(doneOK, q)
   215  			updates.CancelsExecuted = append(updates.CancelsExecuted, o)
   216  
   217  			// CancelOrder Match has zero values for Amounts, Rates, and Total.
   218  			matches = append(matches, &order.MatchSet{
   219  				Taker:   q.Order,
   220  				Makers:  []*order.LimitOrder{removed},
   221  				Amounts: []uint64{removed.Remaining()},
   222  				Rates:   []uint64{removed.Rate},
   223  			})
   224  			unbooked = append(unbooked, removed)
   225  			updates.TradesCanceled = append(updates.TradesCanceled, removed)
   226  
   227  		case *order.LimitOrder:
   228  			// limit-limit order matching
   229  			var makers []*order.LimitOrder
   230  			matchSet := matchLimitOrder(book, o)
   231  
   232  			if matchSet != nil {
   233  				appendTradeSet(matchSet)
   234  				makers = matchSet.Makers
   235  			} else {
   236  				if o.Force == order.ImmediateTiF {
   237  					nomatched = append(nomatched, q)
   238  					// There was no match and TiF is Immediate. Fail.
   239  					failed = append(failed, q)
   240  					updates.TradesFailed = append(updates.TradesFailed, o)
   241  					break
   242  				} else {
   243  					nomatchStanding[q.Order.ID()] = q
   244  				}
   245  			}
   246  
   247  			// Either matched or standing unmatched => passed.
   248  			passed = append(passed, q)
   249  
   250  			tallyMakers(makers)
   251  
   252  			var wasBooked bool
   253  			if o.Remaining() > 0 {
   254  				if o.Filled() > 0 {
   255  					partial = append(partial, q)
   256  				}
   257  				if o.Force == order.StandingTiF {
   258  					// Standing TiF orders go on the book.
   259  					book.Insert(o)
   260  					booked = append(booked, q)
   261  					updates.TradesBooked = append(updates.TradesBooked, o)
   262  					wasBooked = true
   263  				}
   264  			}
   265  
   266  			// booked => TradesBooked
   267  			// !booked => TradesCompleted
   268  
   269  			if !wasBooked { // either nothing remaining or immediate force
   270  				doneOK = append(doneOK, q)
   271  				updates.TradesCompleted = append(updates.TradesCompleted, o)
   272  			}
   273  
   274  		case *order.MarketOrder:
   275  			// market-limit order matching
   276  			var matchSet *order.MatchSet
   277  
   278  			if o.Sell {
   279  				matchSet = matchMarketSellOrder(book, o)
   280  			} else {
   281  				// Market buy order Quantity is denominated in the quote asset,
   282  				// and lot size multiples are not applicable.
   283  				matchSet = matchMarketBuyOrder(book, o)
   284  			}
   285  			if matchSet != nil {
   286  				// Only count market order volume that matches.
   287  				appendTradeSet(matchSet)
   288  				passed = append(passed, q)
   289  				doneOK = append(doneOK, q)
   290  				updates.TradesCompleted = append(updates.TradesCompleted, o)
   291  			} else {
   292  				// There was no match and this is a market order. Fail.
   293  				failed = append(failed, q)
   294  				updates.TradesFailed = append(updates.TradesFailed, o)
   295  				nomatched = append(nomatched, q)
   296  				break
   297  			}
   298  
   299  			tallyMakers(matchSet.Makers)
   300  
   301  			// Regardless of remaining amount, market orders never go on the book.
   302  		}
   303  
   304  	}
   305  
   306  	for _, lo := range partialMap {
   307  		updates.TradesPartial = append(updates.TradesPartial, lo)
   308  	}
   309  
   310  	for _, q := range nomatchStanding {
   311  		nomatched = append(nomatched, q)
   312  	}
   313  
   314  	for _, matchSet := range matches {
   315  		if matchSet.Total > 0 { // cancel filter
   316  			stats.StartRate = matchSet.Makers[0].Rate
   317  			break
   318  		}
   319  	}
   320  	if stats.StartRate > 0 { // If we didn't find anything going forward, no need to check going backwards.
   321  		for i := len(matches) - 1; i >= 0; i-- {
   322  			matchSet := matches[i]
   323  			if matchSet.Total > 0 { // cancel filter
   324  				stats.EndRate = matchSet.Makers[len(matchSet.Makers)-1].Rate
   325  				break
   326  			}
   327  		}
   328  	}
   329  
   330  	bookVolumes(book, stats)
   331  
   332  	return
   333  }
   334  
   335  // limit-limit order matching
   336  func matchLimitOrder(book Booker, ord *order.LimitOrder) (matchSet *order.MatchSet) {
   337  	amtRemaining := ord.Remaining() // i.e. ord.Quantity - ord.FillAmt
   338  	if amtRemaining == 0 {
   339  		return
   340  	}
   341  
   342  	lotSize := book.LotSize()
   343  	assertOrderLotSize(ord, lotSize)
   344  
   345  	bestFunc := book.BestSell
   346  	rateMatch := func(b, s uint64) bool { return s <= b }
   347  	if ord.Sell {
   348  		// order is a sell order
   349  		bestFunc = book.BestBuy
   350  		rateMatch = func(s, b uint64) bool { return s <= b }
   351  	}
   352  
   353  	// Find matches until the order has been depleted.
   354  	for amtRemaining > 0 {
   355  		// Get the best book order for this limit order.
   356  		best := bestFunc() // maker
   357  		if best == nil {
   358  			return
   359  		}
   360  		assertOrderLotSize(best, lotSize)
   361  
   362  		// Check rate.
   363  		if !rateMatch(ord.Rate, best.Rate) {
   364  			return
   365  		}
   366  		// now, best.Rate <= ord.Rate
   367  
   368  		// The match amount is the smaller of the order's remaining quantity or
   369  		// the best matching order amount.
   370  		amt := best.Remaining()
   371  		if amtRemaining < amt {
   372  			// Partially fill the standing order, updating its value.
   373  			amt = amtRemaining
   374  		} else {
   375  			// The standing order has been consumed. Remove it from the book.
   376  			if _, ok := book.Remove(best.ID()); !ok {
   377  				log.Errorf("Failed to remove standing order %v.", best)
   378  			}
   379  		}
   380  		best.AddFill(amt)
   381  
   382  		// Reduce the remaining quantity of the taker order.
   383  		amtRemaining -= amt
   384  		ord.AddFill(amt)
   385  
   386  		// Add the matched maker order to the output.
   387  		if matchSet == nil {
   388  			matchSet = &order.MatchSet{
   389  				Taker:   ord,
   390  				Makers:  []*order.LimitOrder{best},
   391  				Amounts: []uint64{amt},
   392  				Rates:   []uint64{best.Rate},
   393  				Total:   amt,
   394  			}
   395  		} else {
   396  			matchSet.Makers = append(matchSet.Makers, best)
   397  			matchSet.Amounts = append(matchSet.Amounts, amt)
   398  			matchSet.Rates = append(matchSet.Rates, best.Rate)
   399  			matchSet.Total += amt
   400  		}
   401  	}
   402  
   403  	return
   404  }
   405  
   406  // market(sell)-limit order matching
   407  func matchMarketSellOrder(book Booker, ord *order.MarketOrder) (matchSet *order.MatchSet) {
   408  	if !ord.Sell {
   409  		panic("matchMarketSellOrder: not a sell order")
   410  	}
   411  
   412  	// A market sell order is a special case of a limit order with time-in-force
   413  	// immediate and no minimum rate (a rate of 0).
   414  	limOrd := &order.LimitOrder{
   415  		P:     ord.P,
   416  		T:     *ord.T.Copy(),
   417  		Force: order.ImmediateTiF,
   418  		Rate:  0,
   419  	}
   420  	matchSet = matchLimitOrder(book, limOrd)
   421  	if matchSet == nil {
   422  		return
   423  	}
   424  	// The Match.Taker must be the *MarketOrder, not the wrapped *LimitOrder.
   425  	matchSet.Taker = ord
   426  	return
   427  }
   428  
   429  // market(buy)-limit order matching
   430  func matchMarketBuyOrder(book Booker, ord *order.MarketOrder) (matchSet *order.MatchSet) {
   431  	if ord.Sell {
   432  		panic("matchMarketBuyOrder: not a buy order")
   433  	}
   434  
   435  	lotSize := book.LotSize()
   436  
   437  	// Amount remaining for market buy is in *quote* asset, not base asset.
   438  	amtRemaining := ord.Remaining() // i.e. ord.Quantity - ord.FillAmt
   439  	if amtRemaining == 0 {
   440  		return
   441  	}
   442  
   443  	// Find matches until the order has been depleted.
   444  	for amtRemaining > 0 {
   445  		// Get the best book order for this limit order.
   446  		best := book.BestSell() // maker
   447  		if best == nil {
   448  			return
   449  		}
   450  
   451  		// Convert the market buy order's quantity into base asset:
   452  		//   quoteAmt = rate * baseAmt
   453  		amtRemainingBase := QuoteToBase(best.Rate, amtRemaining)
   454  		//amtRemainingBase := uint64(float64(amtRemaining) / best.Rate) // trunc
   455  		if amtRemainingBase < lotSize {
   456  			return
   457  		}
   458  
   459  		// To convert the matching limit order's quantity into quote asset:
   460  		// amt := uint64(best.Rate * float64(best.Quantity)) // trunc
   461  
   462  		// The match amount is the smaller of the order's remaining quantity or
   463  		// the best matching order amount.
   464  		amt := best.Remaining()
   465  		if amtRemainingBase < amt {
   466  			// Partially fill the standing order, updating its value.
   467  			amt = amtRemainingBase - amtRemainingBase%lotSize // amt is a multiple of lot size
   468  		} else {
   469  			// The standing order has been consumed. Remove it from the book.
   470  			if _, ok := book.Remove(best.ID()); !ok {
   471  				log.Errorf("Failed to remove standing order %v.", best)
   472  			}
   473  		}
   474  		best.AddFill(amt)
   475  
   476  		// Reduce the remaining quantity of the taker order.
   477  		// amtRemainingBase -= amt // FYI
   478  		amtQuote := BaseToQuote(best.Rate, amt)
   479  		//amtQuote := uint64(float64(amt) * best.Rate)
   480  		amtRemaining -= amtQuote // quote asset remaining
   481  		ord.AddFill(amtQuote)    // quote asset filled
   482  
   483  		// Add the matched maker order to the output.
   484  		if matchSet == nil {
   485  			matchSet = &order.MatchSet{
   486  				Taker:   ord,
   487  				Makers:  []*order.LimitOrder{best},
   488  				Amounts: []uint64{amt},
   489  				Rates:   []uint64{best.Rate},
   490  				Total:   amt,
   491  			}
   492  		} else {
   493  			matchSet.Makers = append(matchSet.Makers, best)
   494  			matchSet.Amounts = append(matchSet.Amounts, amt)
   495  			matchSet.Rates = append(matchSet.Rates, best.Rate)
   496  			matchSet.Total += amt
   497  		}
   498  	}
   499  
   500  	return
   501  }
   502  
   503  // OrdersMatch checks if two orders are valid matches, without regard to quantity.
   504  // - not a cancel order
   505  // - not two market orders
   506  // - orders on different sides (one buy and one sell)
   507  // - two limit orders with overlapping rates, or one market and one limit
   508  func OrdersMatch(a, b order.Order) bool {
   509  	// Get order data needed for comparison.
   510  	aType, aSell, _, aRate := orderData(a)
   511  	bType, bSell, _, bRate := orderData(b)
   512  
   513  	// Orders must be on opposite sides of the market.
   514  	if aSell == bSell {
   515  		return false
   516  	}
   517  
   518  	// Screen order types.
   519  	switch aType {
   520  	case order.MarketOrderType:
   521  		switch bType {
   522  		case order.LimitOrderType:
   523  			return true // market-limit
   524  		case order.MarketOrderType:
   525  			fallthrough // no two market orders
   526  		default:
   527  			return false // cancel or unknown
   528  		}
   529  	case order.LimitOrderType:
   530  		switch bType {
   531  		case order.LimitOrderType:
   532  			// limit-limit: must check rates
   533  		case order.MarketOrderType:
   534  			return true // limit-market
   535  		default:
   536  			return false // cancel or unknown
   537  		}
   538  	default: // cancel or unknown
   539  		return false
   540  	}
   541  
   542  	// For limit-limit orders, check that the rates overlap.
   543  	cmp := func(buyRate, sellRate uint64) bool { return sellRate <= buyRate }
   544  	if bSell {
   545  		// a is buy, b is sell
   546  		return cmp(aRate, bRate)
   547  	}
   548  	// a is sell, b is buy
   549  	return cmp(bRate, aRate)
   550  }
   551  
   552  func orderData(o order.Order) (orderType order.OrderType, sell bool, amount, rate uint64) {
   553  	orderType = o.Type()
   554  
   555  	switch ot := o.(type) {
   556  	case *order.LimitOrder:
   557  		sell = ot.Sell
   558  		amount = ot.Quantity
   559  		rate = ot.Rate
   560  	case *order.MarketOrder:
   561  		sell = ot.Sell
   562  		amount = ot.Quantity
   563  	}
   564  
   565  	return
   566  }
   567  
   568  // sortQueueByCommit lexicographically sorts the Orders by their commitments.
   569  // This is used to compute the commitment checksum, which is sent to the clients
   570  // in the preimage requests prior to queue shuffling. There must not be
   571  // duplicated commitments.
   572  func sortQueueByCommit(queue []order.Order) {
   573  	sort.Slice(queue, func(i, j int) bool {
   574  		ii, ij := queue[i].Commitment(), queue[j].Commitment()
   575  		return bytes.Compare(ii[:], ij[:]) < 0
   576  	})
   577  }
   578  
   579  // CSum computes the commitment checksum for the order queue. For an empty
   580  // queue, the result is a nil slice instead of the initial hash state.
   581  func CSum(queue []order.Order) []byte {
   582  	if len(queue) == 0 {
   583  		return nil
   584  	}
   585  	sortQueueByCommit(queue)
   586  	hasher := blake256.New()
   587  	for _, ord := range queue {
   588  		commit := ord.Commitment()
   589  		hasher.Write(commit[:]) // err is always nil and n is always len(s)
   590  	}
   591  	return hasher.Sum(nil)
   592  }
   593  
   594  // sortQueueByID lexicographically sorts the Orders by their IDs. Note that
   595  // while sorting is done with the order ID, the preimage is still used to
   596  // specify the shuffle order. The result is undefined if the slice contains
   597  // duplicated order IDs.
   598  func sortQueueByID(queue []*OrderRevealed) {
   599  	sort.Slice(queue, func(i, j int) bool {
   600  		ii, ij := queue[i].Order.ID(), queue[j].Order.ID()
   601  		return bytes.Compare(ii[:], ij[:]) < 0
   602  	})
   603  }
   604  
   605  func ShuffleQueue(queue []*OrderRevealed) {
   606  	shuffleQueue(queue)
   607  }
   608  
   609  // shuffleQueue deterministically shuffles the Orders using a Fisher-Yates
   610  // algorithm seeded with the hash of the concatenated order commitment
   611  // preimages. If any orders in the queue are repeated, the order sorting
   612  // behavior is undefined.
   613  func shuffleQueue(queue []*OrderRevealed) (seed []byte) {
   614  	// Nothing to do if there are no orders. For one order, the seed must still
   615  	// be computed.
   616  	if len(queue) == 0 {
   617  		return
   618  	}
   619  
   620  	// The shuffling seed is derived from the concatenation of the order
   621  	// preimages, lexicographically sorted by order ID.
   622  	sortQueueByID(queue)
   623  
   624  	// Hash the concatenation of the preimages.
   625  	qLen := len(queue)
   626  	hasher := blake256.New()
   627  	//peCat := make([]byte, peSize*qLen)
   628  	for _, o := range queue {
   629  		hasher.Write(o.Preimage[:]) // err is always nil and n is always len(s)
   630  		//copy(peCat[peSize*i:peSize*(i+1)], o.Preimage[:])
   631  	}
   632  
   633  	// Fisher-Yates shuffle the slice using MT19937 seeded with the hash.
   634  	seed = hasher.Sum(nil)
   635  	// seed = HashFunc(hashCat)
   636  
   637  	// This seeded random number generator is used to generate one sequence, and
   638  	// the seed is revealed then revealed. It need not be cryptographically
   639  	// secure.
   640  	mtSrc := mt19937.NewSource()
   641  	mtSrc.SeedBytes(seed[:])
   642  	prng := rand.New(mtSrc)
   643  	for i := range queue {
   644  		j := prng.Intn(qLen-i) + i
   645  		queue[i], queue[j] = queue[j], queue[i]
   646  	}
   647  
   648  	return
   649  }
   650  
   651  func midGap(book Booker) uint64 {
   652  	b, s := book.BestBuy(), book.BestSell()
   653  	if b == nil {
   654  		if s == nil {
   655  			return 0
   656  		}
   657  		return s.Rate
   658  	} else if s == nil {
   659  		return b.Rate
   660  	}
   661  	return (b.Rate + s.Rate) / 2
   662  }
   663  
   664  func sideVolume(ords []*order.LimitOrder) (q uint64) {
   665  	for _, ord := range ords {
   666  		q += ord.Remaining()
   667  	}
   668  	return
   669  }
   670  
   671  func bookVolumes(book Booker, stats *MatchCycleStats) {
   672  	midGap := midGap(book)
   673  	cutoff5 := midGap - midGap/20 // 5%
   674  	cutoff25 := midGap - midGap/4 // 25%
   675  	for _, ord := range book.BuyOrders() {
   676  		remaining := ord.Remaining()
   677  		stats.BookBuys += remaining
   678  		if ord.Rate > cutoff25 {
   679  			stats.BookBuys25 += remaining
   680  			if ord.Rate > cutoff5 {
   681  				stats.BookBuys5 += remaining
   682  			}
   683  		}
   684  	}
   685  	cutoff5 = midGap + midGap/20
   686  	cutoff25 = midGap + midGap/4
   687  	for _, ord := range book.SellOrders() {
   688  		remaining := ord.Remaining()
   689  		stats.BookSells += remaining
   690  		if ord.Rate < cutoff25 {
   691  			stats.BookSells25 += remaining
   692  			if ord.Rate < cutoff5 {
   693  				stats.BookSells5 += remaining
   694  			}
   695  		}
   696  	}
   697  }