limit-order-book
Version:
Order book supporting limit and market orders
130 lines (115 loc) • 3.92 kB
JavaScript
const Decimal = require('decimal.js')
const LevelQueue = require('./queue.js')
const { MarketOrder } = require('./orders.js')
const Result = require('./result.js')
const isMarket = (order) => order instanceof MarketOrder
const defaults = { symbol: '', priceStep: '1', sizeStep: '1' }
const LimitOrderBook = function(opts={}) {
opts = {...defaults, ...opts}
this.symbol = opts.symbol
this.askLevels = new LevelQueue('ask')
this.bidLevels = new LevelQueue('bid')
this.config(opts)
this.orders = {}
}
LimitOrderBook.prototype.config = function(opts) {
this.priceStep = opts.priceStep
this.sizeStep = opts.sizeStep
this.askLevels.config(opts)
this.bidLevels.config(opts)
}
LimitOrderBook.prototype._processAsk = function(ask) {
let makers = []
let next = this.bidLevels.takeSizeFromBestLevel(ask)
while (next.length > 0) {
makers = makers.concat(next)
next = this.bidLevels.takeSizeFromBestLevel(ask)
}
if (new Decimal(ask.sizeRemaining).gt(0) && !isMarket(ask)) {
this.askLevels.add(ask)
}
return makers
}
LimitOrderBook.prototype._processBid = function(bid) {
let makers = []
let next = this.askLevels.takeSizeFromBestLevel(bid)
while (next.length > 0) {
makers = makers.concat(next)
next = this.askLevels.takeSizeFromBestLevel(bid)
}
if (new Decimal(bid.sizeRemaining).gt(0) && !isMarket(bid)) {
this.bidLevels.add(bid)
}
return makers
}
const parse = (str) => {
try {
return new Decimal(str)
} catch (err) {
return new Decimal(0)
}
}
LimitOrderBook.prototype._validate = function(order) {
const { priceStep, sizeStep } = this
let { price, size, funds } = order
price = parse(price)
size = parse(size)
funds = parse(funds)
if (!isMarket(order) && price.lte(0)) { throw new Error('limit order needs price > 0') }
if (!isMarket(order) && size.lte(0)) { throw new Error('limit order needs size > 0') }
if (size.lte(0) && funds.lte(0)) { throw new Error('order needs size > 0 or funds > 0') }
let ok = price.mod(priceStep).eq(0)
if (!ok) { throw new Error(`price must be divisible by ${priceStep.toFixed()}`) }
ok = size.mod(sizeStep).eq(0)
if (!ok) { throw new Error(`size must be divisible by ${sizeStep.toFixed()}`) }
}
LimitOrderBook.prototype._removeIfEmpty = function(order) {
if (new Decimal(order.sizeRemaining).gt(0)) { return }
delete this.orders[order.orderId]
}
LimitOrderBook.prototype.add = function(order) {
this._validate(order)
let makers = null
let takeSize = order.sizeRemaining
if (order.side === 'ask') {
makers = this._processAsk(order)
} else {
makers = this._processBid(order)
}
const copies = makers.map((maker) => maker.copy())
makers.forEach((maker) => {
maker.clearFilled()
this._removeIfEmpty(maker)
})
if (isMarket(order)) {
return new Result(this.symbol, order.copy(), copies)
}
if (new Decimal(order.sizeRemaining).gt(0)) { this.orders[order.orderId] = order }
return new Result(this.symbol, order.copy(), copies)
}
LimitOrderBook.prototype.reduce = function(orderId, size) {
const { sizeStep } = this
const ok = new Decimal(size).mod(sizeStep).eq(0)
if (!ok) { throw new Error(`size must be divisible by ${sizeStep.toFixed()}`) }
let order = this.orders[orderId]
if (!order) { return null }
order = order.side === 'ask' ?
this.askLevels.reduce(order.price, orderId, size) :
this.bidLevels.reduce(order.price, orderId, size)
this._removeIfEmpty(order)
return order
}
LimitOrderBook.prototype.remove = function(orderId) {
const order = this.orders[orderId]
if (!order) { return null }
delete this.orders[orderId]
return order.side === 'ask' ?
this.askLevels.remove(order.price, orderId) :
this.bidLevels.remove(order.price, orderId)
}
LimitOrderBook.prototype.clear = function() {
this.orders = {}
this.askLevels.clear()
this.bidLevels.clear()
}
module.exports = LimitOrderBook