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 }