decred.org/dcrdex@v1.0.3/client/orderbook/bookside.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 orderbook
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"sort"
    10  	"sync"
    11  
    12  	"decred.org/dcrdex/dex/calc"
    13  	"decred.org/dcrdex/dex/order"
    14  )
    15  
    16  // orderPreference represents ordering preference for a sort.
    17  type orderPreference int
    18  
    19  const (
    20  	ascending orderPreference = iota
    21  	descending
    22  )
    23  
    24  // Fill represents an order fill.
    25  type Fill struct {
    26  	Rate     uint64
    27  	Quantity uint64
    28  }
    29  
    30  // bookSide represents a side of the order book.
    31  type bookSide struct {
    32  	bins      map[uint64][]*Order
    33  	rateIndex *rateIndex
    34  	orderPref orderPreference
    35  	mtx       sync.RWMutex
    36  }
    37  
    38  // newBookSide creates a new book side depth.
    39  func newBookSide(pref orderPreference) *bookSide {
    40  	return &bookSide{
    41  		bins:      make(map[uint64][]*Order),
    42  		rateIndex: newRateIndex(),
    43  		orderPref: pref,
    44  	}
    45  }
    46  
    47  // Reset reinitializes the bookSide without changing the orderPreference.
    48  func (d *bookSide) reset() {
    49  	d.mtx.Lock()
    50  	d.bins = make(map[uint64][]*Order)
    51  	d.rateIndex = newRateIndex()
    52  	d.mtx.Unlock()
    53  }
    54  
    55  // Add puts an order in its associated bin.
    56  func (d *bookSide) Add(order *Order) {
    57  	d.mtx.Lock()
    58  	defer d.mtx.Unlock()
    59  
    60  	bin, exists := d.bins[order.Rate]
    61  	if !exists {
    62  		bin = make([]*Order, 0, 1)
    63  	}
    64  
    65  	i := sort.Search(len(bin), func(i int) bool { return bin[i].Time >= order.Time })
    66  	for ; i < len(bin); i++ {
    67  		if bin[i].Time != order.Time {
    68  			break
    69  		}
    70  		// Differentiate via order ids if timestamps are identical.
    71  		if bytes.Compare(bin[i].OrderID[:], order.OrderID[:]) > 0 {
    72  			break
    73  		}
    74  	}
    75  
    76  	bin = append(bin, nil)
    77  	copy(bin[i+1:], bin[i:])
    78  	bin[i] = order
    79  	d.bins[order.Rate] = bin
    80  
    81  	// Update the sort order if a new order group is created.
    82  	if !exists {
    83  		d.rateIndex.Add(order.Rate)
    84  		return
    85  	}
    86  }
    87  
    88  // Remove deletes an order from the given rate bin.
    89  func (d *bookSide) Remove(oid order.OrderID, rateBin uint64) error {
    90  	d.mtx.Lock()
    91  	defer d.mtx.Unlock()
    92  
    93  	bin, exists := d.bins[rateBin]
    94  	if !exists {
    95  		return fmt.Errorf("no bin found for rate %d", rateBin)
    96  	}
    97  
    98  	for i := range bin {
    99  		if oid == bin[i].OrderID {
   100  			// Remove the entry and preserve the sort order.
   101  			if i < len(bin)-1 {
   102  				copy(bin[i:], bin[i+1:])
   103  			}
   104  			bin[len(bin)-1] = nil
   105  			bin = bin[:len(bin)-1]
   106  
   107  			// Delete the bin if there are no orders left in it.
   108  			if len(bin) == 0 {
   109  				delete(d.bins, rateBin)
   110  				return d.rateIndex.Remove(rateBin)
   111  			}
   112  
   113  			d.bins[rateBin] = bin
   114  			return nil
   115  		}
   116  	}
   117  
   118  	return fmt.Errorf("order %s not found with rate %d", oid, rateBin)
   119  }
   120  
   121  // UpdateRemaining updates the remaining quantity for an order.
   122  func (d *bookSide) UpdateRemaining(oid order.OrderID, rateBin, remaining uint64) {
   123  	d.mtx.Lock()
   124  	defer d.mtx.Unlock()
   125  
   126  	bin := d.bins[rateBin]
   127  	for i, ord := range bin {
   128  		if ord.OrderID == oid {
   129  			// Make a new Order so other copies aren't modified.
   130  			newOrder := *ord // deep copy
   131  			newOrder.Quantity = remaining
   132  			bin[i] = &newOrder
   133  			return
   134  		}
   135  	}
   136  }
   137  
   138  // Orders is all orders for the side, sorted. Returned orders are copies and
   139  // thus safe for concurrent access to their Quantity field.
   140  func (d *bookSide) Orders() []*Order {
   141  	orders, _ := d.BestNOrders(int(^uint(0) >> 1)) // Max int value
   142  	return orders
   143  }
   144  
   145  // BestNOrders returns the best N orders of the book side. Returned orders are
   146  // copies and thus safe for concurrent access to their Quantity field.
   147  func (d *bookSide) BestNOrders(n int) ([]*Order, bool) {
   148  	d.mtx.RLock()
   149  	defer d.mtx.RUnlock()
   150  	count := n
   151  	best := make([]*Order, 0)
   152  
   153  	d.iterateOrders(func(ord *Order) bool {
   154  		if count == 0 {
   155  			return false
   156  		}
   157  		// Quantity is immutable for a given *Order instance (see
   158  		// UpdateRemaining), so there is no need to make a deep copy here.
   159  		best = append(best, ord)
   160  		count--
   161  		return true
   162  	})
   163  
   164  	return best, len(best) == n
   165  }
   166  
   167  // BestFill returns the best fill for the provided quantity.
   168  func (d *bookSide) BestFill(qty uint64) ([]*Fill, bool) {
   169  	return d.bestFill(qty, false, 0)
   170  }
   171  
   172  func (d *bookSide) bestFill(quantity uint64, convert bool, lotSize uint64) ([]*Fill, bool) {
   173  	d.mtx.RLock()
   174  	defer d.mtx.RUnlock()
   175  
   176  	remainingQty := quantity
   177  	best := make([]*Fill, 0)
   178  
   179  	d.iterateOrders(func(ord *Order) bool {
   180  		if remainingQty == 0 {
   181  			return false
   182  		}
   183  
   184  		qty := ord.Quantity
   185  		if convert {
   186  			if calc.QuoteToBase(ord.Rate, remainingQty) < lotSize {
   187  				return false
   188  			}
   189  			qty = calc.BaseToQuote(ord.Rate, ord.Quantity)
   190  		}
   191  
   192  		var entry *Fill
   193  		if remainingQty < qty {
   194  			fillQty := remainingQty
   195  			if convert {
   196  				r := calc.QuoteToBase(ord.Rate, remainingQty)
   197  				fillQty = r - (r % lotSize)
   198  				// remainingQty -= calc.BaseToQuote(ord.Rate, ord.Quantity-fillQty)
   199  			}
   200  
   201  			// remainingQty almost certainly not zero for market buy orders, but
   202  			// set to zero to return filled=true to indicate that the order was
   203  			// exhausted before the book.
   204  			remainingQty = 0
   205  
   206  			entry = &Fill{
   207  				Rate:     ord.Rate,
   208  				Quantity: fillQty,
   209  			}
   210  		} else {
   211  			entry = &Fill{
   212  				Rate:     ord.Rate,
   213  				Quantity: ord.Quantity,
   214  			}
   215  			remainingQty -= qty
   216  		}
   217  
   218  		best = append(best, entry)
   219  		return true
   220  	})
   221  
   222  	// Or maybe should return calc.QuoteToBase(ord.Rate, remainingQty) < lotSize
   223  	// when convert = true?
   224  	return best, remainingQty == 0
   225  }
   226  
   227  func (d *bookSide) idxCalculator() func(i int) int {
   228  	if d.orderPref == ascending {
   229  		return func(i int) int { return i }
   230  	}
   231  	binCount := len(d.rateIndex.Rates)
   232  	return func(i int) int { return binCount - 1 - i }
   233  }
   234  
   235  func (d *bookSide) iterateOrders(check func(*Order) bool) {
   236  	calcIdx := d.idxCalculator() // ascending or descending Rates index
   237  
   238  	for ir := range d.rateIndex.Rates {
   239  		bin := d.bins[d.rateIndex.Rates[calcIdx(ir)]]
   240  		for _, ord := range bin {
   241  			if !check(ord) {
   242  				return
   243  			}
   244  		}
   245  	}
   246  }