decred.org/dcrdex@v1.0.3/client/orderbook/orderbook.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 "sync" 10 "sync/atomic" 11 12 "decred.org/dcrdex/dex" 13 "decred.org/dcrdex/dex/msgjson" 14 "decred.org/dcrdex/dex/order" 15 "decred.org/dcrdex/dex/utils" 16 ) 17 18 // ErrEmptyOrderbook is returned from MidGap when the order book is empty. 19 const ErrEmptyOrderbook = dex.ErrorKind("cannot calculate mid-gap from empty order book") 20 21 // Order represents an ask or bid. 22 type Order struct { 23 OrderID order.OrderID 24 Side uint8 25 Quantity uint64 26 Rate uint64 27 Time uint64 28 // Epoch is only used in the epoch queue, otherwise it is ignored. 29 Epoch uint64 30 } 31 32 func (o *Order) sell() bool { 33 return o.Side == msgjson.SellOrderNum 34 } 35 36 // RemoteOrderBook defines the functions a client tracked order book 37 // must implement. 38 type RemoteOrderBook interface { 39 // Sync instantiates a client tracked order book with the 40 // current order book snapshot. 41 Sync(*msgjson.OrderBook) 42 // Book adds a new order to the order book. 43 Book(*msgjson.BookOrderNote) 44 // Unbook removes an order from the order book. 45 Unbook(*msgjson.UnbookOrderNote) error 46 } 47 48 // CachedOrderNote represents a cached order not entry. 49 type cachedOrderNote struct { 50 Route string 51 OrderNote any 52 } 53 54 // rateSell provides the rate and book side information about an order that is 55 // required for efficiently referencing it in a bookSide. 56 type rateSell struct { 57 rate uint64 58 sell bool 59 } 60 61 // MatchSummary summarizes one or more consecutive matches at a given rate and 62 // buy/sell direction. Consecutive matches of the same rate and direction are 63 // binned by the server. 64 type MatchSummary struct { 65 Rate uint64 `json:"rate"` 66 Qty uint64 `json:"qty"` 67 Stamp uint64 `json:"stamp"` 68 Sell bool `json:"sell"` 69 } 70 71 // OrderBook represents a client tracked order book. 72 type OrderBook struct { 73 // feeRates is at the top to account for atomic field alignment in 74 // 32-bit systems. See also https://golang.org/pkg/sync/atomic/#pkg-note-BUG 75 feeRates struct { 76 base uint64 77 quote uint64 78 } 79 80 log dex.Logger 81 seqMtx sync.Mutex 82 seq uint64 83 marketID string 84 85 noteQueueMtx sync.Mutex 86 noteQueue []*cachedOrderNote 87 88 // Track the orders stored in each bookSide. 89 ordersMtx sync.Mutex 90 orders map[order.OrderID]rateSell 91 92 buys *bookSide 93 sells *bookSide 94 95 syncedMtx sync.Mutex 96 synced bool 97 98 epochMtx sync.Mutex 99 currentEpoch uint64 100 proofedEpoch uint64 101 epochQueues map[uint64]*EpochQueue 102 103 matchSummaryMtx sync.Mutex 104 matchesSummary []*MatchSummary 105 } 106 107 // NewOrderBook creates a new order book. 108 func NewOrderBook(logger dex.Logger) *OrderBook { 109 ob := &OrderBook{ 110 log: logger, 111 noteQueue: make([]*cachedOrderNote, 0, 16), 112 orders: make(map[order.OrderID]rateSell), 113 buys: newBookSide(descending), 114 sells: newBookSide(ascending), 115 epochQueues: make(map[uint64]*EpochQueue), 116 } 117 return ob 118 } 119 120 // BaseFeeRate is the last reported base asset fee rate. 121 func (ob *OrderBook) BaseFeeRate() uint64 { 122 return atomic.LoadUint64(&ob.feeRates.base) 123 } 124 125 // QuoteFeeRate is the last reported quote asset fee rate. 126 func (ob *OrderBook) QuoteFeeRate() uint64 { 127 return atomic.LoadUint64(&ob.feeRates.quote) 128 } 129 130 // setSynced sets the synced state of the order book. 131 func (ob *OrderBook) setSynced(value bool) { 132 ob.syncedMtx.Lock() 133 ob.synced = value 134 ob.syncedMtx.Unlock() 135 } 136 137 // isSynced returns the synced state of the order book. 138 func (ob *OrderBook) isSynced() bool { 139 ob.syncedMtx.Lock() 140 defer ob.syncedMtx.Unlock() 141 return ob.synced 142 } 143 144 // setSeq should be called whenever a sequenced message is received. If seq is 145 // out of sequence, an error is logged. 146 func (ob *OrderBook) setSeq(seq uint64) { 147 ob.seqMtx.Lock() 148 defer ob.seqMtx.Unlock() 149 if seq != ob.seq+1 { 150 ob.log.Errorf("notification received out of sync. %d != %d - 1", ob.seq, seq) 151 } 152 if seq > ob.seq { 153 ob.seq = seq 154 } 155 } 156 157 // cacheOrderNote caches an order note. 158 func (ob *OrderBook) cacheOrderNote(route string, entry any) error { 159 note := new(cachedOrderNote) 160 161 switch route { 162 case msgjson.BookOrderRoute, msgjson.UnbookOrderRoute, msgjson.UpdateRemainingRoute: 163 note.Route = route 164 note.OrderNote = entry 165 166 ob.noteQueueMtx.Lock() 167 ob.noteQueue = append(ob.noteQueue, note) 168 ob.noteQueueMtx.Unlock() 169 170 return nil 171 172 default: 173 return fmt.Errorf("unknown route provided %s", route) 174 } 175 } 176 177 // processCachedNotes processes all cached notes, each processed note is 178 // removed from the cache. 179 func (ob *OrderBook) processCachedNotes() error { 180 ob.noteQueueMtx.Lock() 181 defer ob.noteQueueMtx.Unlock() 182 183 ob.log.Debugf("Processing %d cached order notes", len(ob.noteQueue)) 184 for len(ob.noteQueue) > 0 { 185 var entry *cachedOrderNote 186 entry, ob.noteQueue = ob.noteQueue[0], ob.noteQueue[1:] // so much for preallocating 187 188 switch entry.Route { 189 case msgjson.BookOrderRoute: 190 note, ok := entry.OrderNote.(*msgjson.BookOrderNote) 191 if !ok { 192 panic("failed to cast cached book order note as a BookOrderNote") 193 } 194 err := ob.book(note, true) 195 if err != nil { 196 return err 197 } 198 199 case msgjson.UnbookOrderRoute: 200 note, ok := entry.OrderNote.(*msgjson.UnbookOrderNote) 201 if !ok { 202 panic("failed to cast cached unbook order note as an UnbookOrderNote") 203 } 204 err := ob.unbook(note, true) 205 if err != nil { 206 return err 207 } 208 209 case msgjson.UpdateRemainingRoute: 210 note, ok := entry.OrderNote.(*msgjson.UpdateRemainingNote) 211 if !ok { 212 panic("failed to cast cached update_remaining note as an UnbookOrderNote") 213 } 214 err := ob.updateRemaining(note, true) 215 if err != nil { 216 return err 217 } 218 219 default: 220 return fmt.Errorf("unknown cached note route provided: %s", entry.Route) 221 } 222 } 223 224 return nil 225 } 226 227 // Sync updates a client tracked order book with an order book snapshot. It is 228 // an error if the the OrderBook is already synced. 229 func (ob *OrderBook) Sync(snapshot *msgjson.OrderBook) error { 230 if ob.isSynced() { 231 return fmt.Errorf("order book is already synced") 232 } 233 return ob.Reset(snapshot) 234 } 235 236 // Reset forcibly updates a client tracked order book with an order book 237 // snapshot. This resets the sequence. 238 // TODO: eliminate this and half of the mutexes! 239 func (ob *OrderBook) Reset(snapshot *msgjson.OrderBook) error { 240 // Don't use setSeq here, since this message is the seed and is not expected 241 // to be 1 more than the current seq value. 242 ob.seqMtx.Lock() 243 ob.seq = snapshot.Seq 244 ob.seqMtx.Unlock() 245 246 atomic.StoreUint64(&ob.feeRates.base, snapshot.BaseFeeRate) 247 atomic.StoreUint64(&ob.feeRates.quote, snapshot.QuoteFeeRate) 248 249 ob.marketID = snapshot.MarketID 250 251 func() { // Using a function for mutex management with defer. 252 ob.matchSummaryMtx.Lock() 253 defer ob.matchSummaryMtx.Unlock() 254 255 ob.matchesSummary = make([]*MatchSummary, len(snapshot.RecentMatches)) 256 for i, match := range snapshot.RecentMatches { 257 rate, qty, ts := match[0], match[1], match[2] 258 sell := true 259 if match[1] < 0 { 260 qty *= -1 261 sell = false 262 } 263 264 ob.matchesSummary[i] = &MatchSummary{ 265 Rate: uint64(rate), 266 Qty: uint64(qty), 267 Sell: sell, 268 Stamp: uint64(ts), 269 } 270 } 271 }() 272 273 err := func() error { // Using a function for mutex management with defer. 274 ob.ordersMtx.Lock() 275 defer ob.ordersMtx.Unlock() 276 277 ob.orders = make(map[order.OrderID]rateSell, len(snapshot.Orders)) 278 ob.buys.reset() 279 ob.sells.reset() 280 for _, o := range snapshot.Orders { 281 if len(o.OrderID) != order.OrderIDSize { 282 return fmt.Errorf("expected order id length of %d, got %d", order.OrderIDSize, len(o.OrderID)) 283 } 284 285 var oid order.OrderID 286 copy(oid[:], o.OrderID) 287 order := &Order{ 288 OrderID: oid, 289 Side: o.Side, 290 Quantity: o.Quantity, 291 Rate: o.Rate, 292 Time: o.Time, 293 } 294 295 ob.orders[oid] = rateSell{order.Rate, order.sell()} 296 297 // Append the order to the order book. 298 switch o.Side { 299 case msgjson.BuyOrderNum: 300 ob.buys.Add(order) 301 302 case msgjson.SellOrderNum: 303 ob.sells.Add(order) 304 305 default: 306 ob.log.Errorf("unknown order side provided: %d", o.Side) 307 } 308 } 309 return nil 310 }() 311 if err != nil { 312 return err 313 } 314 315 // Process cached order notes. 316 err = ob.processCachedNotes() 317 if err != nil { 318 return err 319 } 320 321 ob.setSynced(true) 322 323 return nil 324 } 325 326 // book is the workhorse of the exported Book function. It allows booking 327 // cached and uncached order notes. 328 func (ob *OrderBook) book(note *msgjson.BookOrderNote, cached bool) error { 329 if ob.marketID != note.MarketID { 330 return fmt.Errorf("invalid note market id %s", note.MarketID) 331 } 332 333 if !cached { 334 // Cache the note if the order book is not synced. 335 if !ob.isSynced() { 336 return ob.cacheOrderNote(msgjson.BookOrderRoute, note) 337 } 338 } 339 340 ob.setSeq(note.Seq) 341 342 if len(note.OrderID) != order.OrderIDSize { 343 return fmt.Errorf("expected order id length of %d, got %d", 344 order.OrderIDSize, len(note.OrderID)) 345 } 346 347 var oid order.OrderID 348 copy(oid[:], note.OrderID) 349 350 order := &Order{ 351 OrderID: oid, 352 Side: note.Side, 353 Quantity: note.Quantity, 354 Rate: note.Rate, 355 Time: note.Time, 356 } 357 358 ob.ordersMtx.Lock() 359 ob.orders[order.OrderID] = rateSell{order.Rate, order.sell()} 360 ob.ordersMtx.Unlock() 361 362 // Add the order to its associated books side. 363 switch order.Side { 364 case msgjson.BuyOrderNum: 365 ob.buys.Add(order) 366 367 case msgjson.SellOrderNum: 368 ob.sells.Add(order) 369 370 default: 371 return fmt.Errorf("unknown order side provided: %d", order.Side) 372 } 373 374 return nil 375 } 376 377 // Book adds a new order to the order book. 378 func (ob *OrderBook) Book(note *msgjson.BookOrderNote) error { 379 return ob.book(note, false) 380 } 381 382 // updateRemaining is the workhorse of the exported UpdateRemaining function. It 383 // allows updating cached and uncached orders. 384 func (ob *OrderBook) updateRemaining(note *msgjson.UpdateRemainingNote, cached bool) error { 385 if ob.marketID != note.MarketID { 386 return fmt.Errorf("invalid update_remaining note market id %s", note.MarketID) 387 } 388 389 if !cached { 390 // Cache the note if the order book is not synced. 391 if !ob.isSynced() { 392 return ob.cacheOrderNote(msgjson.UpdateRemainingRoute, note) 393 } 394 } 395 396 ob.setSeq(note.Seq) 397 398 if len(note.OrderID) != order.OrderIDSize { 399 return fmt.Errorf("expected order id length of %d, got %d", 400 order.OrderIDSize, len(note.OrderID)) 401 } 402 403 var oid order.OrderID 404 copy(oid[:], note.OrderID) 405 406 ob.ordersMtx.Lock() 407 ordInfo, found := ob.orders[oid] 408 ob.ordersMtx.Unlock() 409 if !found { 410 return fmt.Errorf("update_remaining order %s not found", oid) 411 } 412 413 if ordInfo.sell { 414 ob.sells.UpdateRemaining(oid, ordInfo.rate, note.Remaining) 415 } else { 416 ob.buys.UpdateRemaining(oid, ordInfo.rate, note.Remaining) 417 } 418 return nil 419 } 420 421 // UpdateRemaining updates the remaining quantity of a booked order. 422 func (ob *OrderBook) UpdateRemaining(note *msgjson.UpdateRemainingNote) error { 423 return ob.updateRemaining(note, false) 424 } 425 426 // LogEpochReport is currently a no-op, and will update market history charts in 427 // the future. 428 func (ob *OrderBook) LogEpochReport(note *msgjson.EpochReportNote) error { 429 // TODO: update future candlestick charts. 430 atomic.StoreUint64(&ob.feeRates.base, note.BaseFeeRate) 431 atomic.StoreUint64(&ob.feeRates.quote, note.QuoteFeeRate) 432 return nil 433 } 434 435 // unbook is the workhorse of the exported Unbook function. It allows unbooking 436 // cached and uncached order notes. 437 func (ob *OrderBook) unbook(note *msgjson.UnbookOrderNote, cached bool) error { 438 if ob.marketID != note.MarketID { 439 return fmt.Errorf("invalid note market id %s", note.MarketID) 440 } 441 442 if !cached { 443 // Cache the note if the order book is not synced. 444 if !ob.isSynced() { 445 return ob.cacheOrderNote(msgjson.UnbookOrderRoute, note) 446 } 447 } 448 449 ob.setSeq(note.Seq) 450 451 if len(note.OrderID) != order.OrderIDSize { 452 return fmt.Errorf("expected order id length of %d, got %d", 453 order.OrderIDSize, len(note.OrderID)) 454 } 455 456 var oid order.OrderID 457 copy(oid[:], note.OrderID) 458 459 ob.ordersMtx.Lock() 460 defer ob.ordersMtx.Unlock() // slightly longer than necessary 461 ordInfo, ok := ob.orders[oid] 462 if !ok { 463 return fmt.Errorf("no order found with id %v", oid) 464 } 465 delete(ob.orders, oid) 466 467 // Remove the order from its associated book side and rate bin. 468 if ordInfo.sell { 469 return ob.sells.Remove(oid, ordInfo.rate) 470 } 471 return ob.buys.Remove(oid, ordInfo.rate) 472 } 473 474 // Unbook removes an order from the order book. 475 func (ob *OrderBook) Unbook(note *msgjson.UnbookOrderNote) error { 476 return ob.unbook(note, false) 477 } 478 479 // BestNOrders returns the best n orders from the provided side. 480 func (ob *OrderBook) BestNOrders(n int, sell bool) ([]*Order, bool, error) { 481 if !ob.isSynced() { 482 return nil, false, fmt.Errorf("order book is unsynced") 483 } 484 485 var orders []*Order 486 var filled bool 487 if sell { 488 orders, filled = ob.sells.BestNOrders(n) 489 } else { 490 orders, filled = ob.buys.BestNOrders(n) 491 } 492 493 return orders, filled, nil 494 } 495 496 // OrderIsBooked checks if an order is booked or in the epoch queue. 497 func (ob *OrderBook) OrderIsBooked(oid order.OrderID, sell bool) bool { 498 findOrder := func(orders []*Order) bool { 499 for _, order := range orders { 500 if order.OrderID == oid { 501 return true 502 } 503 } 504 505 return false 506 } 507 508 var orders []*Order 509 if sell { 510 orders = ob.sells.Orders() 511 } else { 512 orders = ob.buys.Orders() 513 } 514 515 if findOrder(orders) { 516 return true 517 } 518 519 ob.epochMtx.Lock() 520 eq := ob.epochQueues[ob.currentEpoch] 521 ob.epochMtx.Unlock() 522 var epochOrders []*Order 523 if eq != nil { 524 epochOrders = eq.Orders() 525 } 526 527 return findOrder(epochOrders) 528 } 529 530 // VWAP calculates the volume weighted average price for the specified number 531 // of lots. 532 func (ob *OrderBook) VWAP(lots, lotSize uint64, sell bool) (avg, extrema uint64, filled bool, err error) { 533 orders, _, err := ob.BestNOrders(int(lots), sell) 534 if err != nil { 535 return 0, 0, false, err 536 } 537 538 remainingLots := lots 539 var weightedSum uint64 540 for _, order := range orders { 541 extrema = order.Rate 542 lotsInOrder := order.Quantity / lotSize 543 if lotsInOrder >= remainingLots { 544 weightedSum += remainingLots * extrema 545 filled = true 546 break 547 } 548 remainingLots -= lotsInOrder 549 weightedSum += lotsInOrder * extrema 550 } 551 552 if !filled { 553 return 0, 0, false, nil 554 } 555 556 return weightedSum / lots, extrema, true, nil 557 } 558 559 // Orders is the full order book, as slices of sorted buys and sells, and 560 // unsorted epoch orders in the current epoch. 561 func (ob *OrderBook) Orders() ([]*Order, []*Order, []*Order) { 562 ob.epochMtx.Lock() 563 eq := ob.epochQueues[ob.currentEpoch] 564 ob.epochMtx.Unlock() 565 var epochOrders []*Order 566 if eq != nil { 567 // NOTE: This epoch is either (1) open or (2) closed but awaiting a 568 // match_proof and with no orders for a subsequent epoch yet. 569 epochOrders = eq.Orders() 570 } 571 return ob.buys.Orders(), ob.sells.Orders(), epochOrders 572 } 573 574 // Enqueue appends the provided order note to the corresponding epoch's queue. 575 func (ob *OrderBook) Enqueue(note *msgjson.EpochOrderNote) error { 576 ob.setSeq(note.Seq) 577 idx := note.Epoch 578 ob.epochMtx.Lock() 579 defer ob.epochMtx.Unlock() 580 eq, have := ob.epochQueues[idx] 581 if !have { 582 eq = NewEpochQueue() 583 ob.epochQueues[idx] = eq // NOTE: trusting server here a bit not to flood us with fake epochs 584 if idx > ob.currentEpoch { 585 ob.currentEpoch = idx 586 } else { 587 ob.log.Errorf("epoch order note received for epoch %d but current epoch is %d", idx, ob.currentEpoch) 588 } 589 } 590 591 return eq.Enqueue(note) 592 } 593 594 // CurrentEpoch returns the current epoch. 595 func (ob *OrderBook) CurrentEpoch() uint64 { 596 ob.epochMtx.Lock() 597 defer ob.epochMtx.Unlock() 598 return ob.currentEpoch 599 } 600 601 // ValidateMatchProof ensures the match proof data provided is correct by 602 // comparing it to a locally generated proof from the same epoch queue. 603 func (ob *OrderBook) ValidateMatchProof(note msgjson.MatchProofNote) error { 604 idx := note.Epoch 605 noteSize := len(note.Preimages) + len(note.Misses) 606 607 // Extract the EpochQueue in a closure for clean epochMtx handling. 608 var firstProof bool 609 extractEpochQueue := func() (*EpochQueue, error) { 610 ob.epochMtx.Lock() 611 defer ob.epochMtx.Unlock() 612 firstProof = ob.proofedEpoch == 0 613 ob.proofedEpoch = idx 614 if eq := ob.epochQueues[idx]; eq != nil { 615 delete(ob.epochQueues, idx) // there will be no more additions to this epoch 616 return eq, nil 617 } 618 // This is expected for an empty match proof or if we started mid-epoch. 619 if noteSize == 0 || firstProof { 620 return nil, nil 621 } 622 return nil, fmt.Errorf("epoch %d match proof note references %d orders, but local epoch queue is empty", 623 idx, noteSize) 624 } 625 eq, err := extractEpochQueue() 626 if eq == nil /* includes err != nil */ { 627 return err 628 } 629 630 if noteSize > 0 { 631 ob.log.Tracef("Validating match proof note for epoch %d (%s) with %d preimages and %d misses.", 632 idx, note.MarketID, len(note.Preimages), len(note.Misses)) 633 } 634 if localSize := eq.Size(); noteSize != localSize { 635 if firstProof && localSize < noteSize { 636 return nil // we only saw part of the epoch 637 } 638 // Since match_proof lags epoch close by up to preimage request timeout, 639 // this can still happen for multiple proofs after (re)connect. 640 return fmt.Errorf("epoch %d match proof note references %d orders, but local epoch queue has %d", 641 idx, noteSize, localSize) 642 } 643 if len(note.Preimages) == 0 { 644 return nil 645 } 646 647 pimgs := make([]order.Preimage, len(note.Preimages)) 648 for i, entry := range note.Preimages { 649 copy(pimgs[i][:], entry) 650 } 651 652 misses := make([]order.OrderID, len(note.Misses)) 653 for i, entry := range note.Misses { 654 copy(misses[i][:], entry) 655 } 656 657 seed, csum, err := eq.GenerateMatchProof(pimgs, misses) 658 if err != nil { 659 return fmt.Errorf("unable to generate match proof for epoch %d: %w", 660 idx, err) 661 } 662 663 if !bytes.Equal(seed, note.Seed) { 664 return fmt.Errorf("match proof seed mismatch for epoch %d: "+ 665 "expected %s, got %s", idx, note.Seed, seed) 666 } 667 668 if !bytes.Equal(csum, note.CSum) { 669 return fmt.Errorf("match proof csum mismatch for epoch %d: "+ 670 "expected %s, got %s", idx, note.CSum, csum) 671 } 672 673 return nil 674 } 675 676 // MidGap returns the mid-gap price for the market. If one market side is empty 677 // the bets rate from the other side will be used. If both sides are empty, an 678 // error will be returned. 679 func (ob *OrderBook) MidGap() (uint64, error) { 680 s, senough := ob.sells.BestNOrders(1) 681 b, benough := ob.buys.BestNOrders(1) 682 if !senough { 683 if !benough { 684 return 0, ErrEmptyOrderbook 685 } 686 return b[0].Rate, nil 687 } 688 if !benough { 689 return s[0].Rate, nil 690 } 691 return (s[0].Rate + b[0].Rate) / 2, nil 692 } 693 694 // BestFill is the best (rate, quantity) fill for an order of the type and 695 // quantity specified. BestFill should be used when the exact quantity of base asset 696 // is known, i.e. limit orders and market sell orders. For market buy orders, 697 // use BestFillMarketBuy. 698 func (ob *OrderBook) BestFill(sell bool, qty uint64) ([]*Fill, bool) { 699 if sell { 700 return ob.buys.BestFill(qty) 701 } 702 return ob.sells.BestFill(qty) 703 } 704 705 // BestFillMarketBuy is the best (rate, quantity) fill for a market buy order. 706 // The qty given will be in units of quote asset. 707 func (ob *OrderBook) BestFillMarketBuy(qty, lotSize uint64) ([]*Fill, bool) { 708 return ob.sells.bestFill(qty, true, lotSize) 709 } 710 711 // AddRecentMatches adds the recent matches. If the recent matches cache length 712 // grows bigger than 100, it will slice out the ones first added. 713 func (ob *OrderBook) AddRecentMatches(matches [][2]int64, ts uint64) []*MatchSummary { 714 if matches == nil { 715 return nil 716 } 717 newMatches := make([]*MatchSummary, len(matches)) 718 for i, m := range matches { 719 rate, qty := m[0], m[1] 720 // negative qty means maker is a sell 721 sell := true 722 if qty < 0 { 723 qty *= -1 724 sell = false 725 } 726 newMatches[i] = &MatchSummary{ 727 Rate: uint64(rate), 728 Qty: uint64(qty), 729 Stamp: ts, 730 Sell: sell, 731 } 732 } 733 734 // Put the newest first. 735 utils.ReverseSlice(newMatches) 736 737 ob.matchSummaryMtx.Lock() 738 defer ob.matchSummaryMtx.Unlock() 739 ob.matchesSummary = append(newMatches, ob.matchesSummary...) // nolint:makezero 740 const maxLength = 100 741 // if ob.matchesSummary length is greater than max length, we slice the array 742 // to maxLength, removing values first added. 743 if len(ob.matchesSummary) > maxLength { 744 ob.matchesSummary = ob.matchesSummary[:maxLength] 745 } 746 return newMatches 747 } 748 749 // RecentMatches returns up to 100 recent matches, newest first. 750 func (ob *OrderBook) RecentMatches() []*MatchSummary { 751 ob.matchSummaryMtx.Lock() 752 defer ob.matchSummaryMtx.Unlock() 753 return ob.matchesSummary 754 }