decred.org/dcrdex@v1.0.3/client/webserver/site/src/js/orderbook.ts (about)

     1  import {
     2    MarketOrderBook,
     3    MiniOrder
     4  } from './registry'
     5  
     6  export default class OrderBook {
     7    base: number
     8    baseSymbol: string
     9    quote: number
    10    quoteSymbol: string
    11    buys: MiniOrder[]
    12    sells: MiniOrder[]
    13  
    14    constructor (mktBook: MarketOrderBook, baseSymbol: string, quoteSymbol: string) {
    15      this.base = mktBook.base
    16      this.baseSymbol = baseSymbol
    17      this.quote = mktBook.quote
    18      this.quoteSymbol = quoteSymbol
    19      // Books are sorted mid-gap first.
    20      this.buys = mktBook.book.buys || []
    21      this.sells = mktBook.book.sells || []
    22    }
    23  
    24    /* add adds an order to the order book. */
    25    add (ord: MiniOrder) {
    26      if (ord.qtyAtomic === 0) {
    27        // TODO: Somebody, for the love of god, figure out why the hell this helps
    28        // with the ghost orders problem. As far as I know, this order is a booked
    29        // order that had more than one match in an epoch and completely filled.
    30        // Because the first match didn't exhaust the order, there would be a
    31        // 'update_remaining' notification scheduled for the order. But by the
    32        // time OrderRouter generates the notification long after matching, the
    33        // order has zero qty left to fill. It's all good though, kinda, because
    34        // the notification is quickly followed with an 'unbook_order'
    35        // notification. I have tried my damnedest to catch an update_remaining
    36        // note without an accompanying unbook_order note, and have thus failed.
    37        // Yet, this fix somehow seems to work. It's infuriating, tbh.
    38        window.log('zeroqty', 'zero quantity order encountered', ord)
    39        return
    40      }
    41      const side = ord.sell ? this.sells : this.buys
    42      side.splice(findIdx(side, ord.rate, !ord.sell), 0, ord)
    43    }
    44  
    45    /* remove removes an order from the order book. */
    46    remove (token: string) {
    47      if (this.removeFromSide(this.sells, token)) return
    48      this.removeFromSide(this.buys, token)
    49    }
    50  
    51    /* removeFromSide removes an order from the list of orders. */
    52    removeFromSide (side: MiniOrder[], token: string) {
    53      const [ord, i] = this.findOrder(side, token)
    54      if (ord) {
    55        side.splice(i, 1)
    56        return true
    57      }
    58      return false
    59    }
    60  
    61    /* findOrder finds an order in a specified side */
    62    findOrder (side: MiniOrder[], token: string): [MiniOrder | null, number] {
    63      for (let i = 0; i < side.length; i++) {
    64        if (side[i].token === token) {
    65          return [side[i], i]
    66        }
    67      }
    68      return [null, -1]
    69    }
    70  
    71    /* updates the remaining quantity of an order. */
    72    updateRemaining (token: string, qty: number, qtyAtomic: number) {
    73      if (this.updateRemainingSide(this.sells, token, qty, qtyAtomic)) return
    74      this.updateRemainingSide(this.buys, token, qty, qtyAtomic)
    75    }
    76  
    77    /*
    78     * updateRemainingSide looks for the order in the side and updates the
    79     * quantity, returning true on success, false if order not found.
    80     */
    81    updateRemainingSide (side: MiniOrder[], token: string, qty: number, qtyAtomic: number) {
    82      const ord = this.findOrder(side, token)[0]
    83      if (ord) {
    84        ord.qty = qty
    85        ord.qtyAtomic = qtyAtomic
    86        return true
    87      }
    88      return false
    89    }
    90  
    91    /*
    92     * setEpoch sets the current epoch and clear any orders from previous epochs.
    93     */
    94    setEpoch (epochIdx: number) {
    95      const approve = (ord: MiniOrder) => ord.epoch === undefined || ord.epoch === 0 || ord.epoch === epochIdx
    96      this.sells = this.sells.filter(approve)
    97      this.buys = this.buys.filter(approve)
    98    }
    99  
   100    /* empty will return true if both the buys and sells lists are empty. */
   101    empty () {
   102      return !this.sells.length && !this.buys.length
   103    }
   104  
   105    /* count is the total count of both buy and sell orders. */
   106    count () {
   107      return this.sells.length + this.buys.length
   108    }
   109  
   110    /* bestGapOrder will return the best non-epoch order if one exists, or the
   111     * best epoch order if there are only epoch orders, or null if there are no
   112     * orders.
   113     */
   114    bestGapOrder (side: MiniOrder[]) {
   115      let best = null
   116      for (const ord of side) {
   117        if (!ord.epoch) return ord
   118        if (!best) {
   119          best = ord
   120        }
   121      }
   122      return best
   123    }
   124  
   125    bestGapBuy () {
   126      return this.bestGapOrder(this.buys)
   127    }
   128  
   129    bestGapSell () {
   130      return this.bestGapOrder(this.sells)
   131    }
   132  }
   133  
   134  /*
   135   * findIdx find the index at which to insert the order into the list of orders.
   136   */
   137  function findIdx (side: MiniOrder[], rate: number, less: boolean): number {
   138    for (let i = 0; i < side.length; i++) {
   139      if ((side[i].rate < rate) === less) return i
   140    }
   141    return side.length
   142  }