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 }