bot18
Version:
A high-frequency cryptocurrency trading bot by Zenbot creator @carlos8f
406 lines (336 loc) • 9.67 kB
JavaScript
'use strict'
const { EventEmitter } = require('events')
const debug = require('debug')('bitfinex:ws:orderbook')
const CRC = require('crc-32')
const { preparePrice } = require('../util/precision')
/**
* High level OB model to automatically integrate WS updates & maintain sort
*/
class OrderBook extends EventEmitter {
/**
* Initializes the order book with an existing snapshot (array form)
*
* @param {Array[]|OrderBook} snapshot
* @param {boolean?} raw - true for raw 'R0' order books
*/
constructor (snap = [], raw = false) {
super()
this.raw = raw
if (snap instanceof OrderBook) {
this.bids = snap.bids.slice()
this.asks = snap.asks.slice()
} else if (snap && Array.isArray(snap)) {
this.updateFromSnapshot(snap)
} else if (snap && Array.isArray(snap.bids) && Array.isArray(snap.asks)) {
this.bids = snap.bids.slice()
this.asks = snap.asks.slice()
} else {
this.bids = []
this.asks = []
}
}
/**
* Generates a crc-32 checksum of our current state. The checksum'ed string
* itself is a concatenated list of the top 25 bids & asks, alternating.
* @see http://blog.bitfinex.com/api/bitfinex-api-order-books-checksums
*
* @return {number} cs
*/
checksum () {
const { raw } = this
const data = []
for (let i = 0; i < 25; i += 1) {
const bid = this.bids[i]
const ask = this.asks[i]
if (bid) {
data.push(
raw ? bid[0] : Number(preparePrice(bid[0])), // order ID or price
bid[2] // amount
)
}
if (ask) {
data.push(
raw ? ask[0] : Number(preparePrice(ask[0])), //
ask[2] //
)
}
}
return CRC.str(data.join(':'))
}
/**
* Like checksum(), but for raw array-format order books
*
* @param {Array[]} arr - assumed sorted, [topBid, bid, ..., topAsk, ask, ...]
* @param {boolean?} raw - true for raw 'R0' order books
* @return {number} cs
*/
static checksumArr (arr, raw = false) {
let topAskI = -1
// find first ask (book is sorted bids first)
for (let i = 0; i < arr.length; i += 1) {
if (arr[i][2] < 0) {
topAskI = i
break
}
}
const data = []
let ask
let bid
// Either bids/asks may be empty, or have differing lengths
for (let i = 0; i < 25; i += 1) {
bid = topAskI === -1 || i < topAskI // still reading bids
? arr[i]
: null // reached asks
ask = topAskI === -1
? null
: arr[topAskI + i]
if (bid) {
data.push(
raw ? bid[0] : Number(preparePrice(bid[0])), // order ID or price
bid[2] // amount
)
}
if (ask) {
data.push(
raw ? ask[0] : Number(preparePrice(ask[0])), //
ask[2] //
)
}
}
return CRC.str(data.join(':'))
}
updateFromSnapshot (snapshot) {
this.bids = []
this.asks = []
for (let i = 0; i < snapshot.length; i++) {
if (snapshot[i][2] < 0) {
this.asks.push(snapshot[i])
} else {
this.bids.push(snapshot[i])
}
}
// snapshots may not be sorted
const priceI = this.raw ? 1 : 0
this.bids.sort((a, b) => b[priceI] - a[priceI])
this.asks.sort((a, b) => a[priceI] - b[priceI])
}
/**
* Integrate an update packet (add, update, or remove a price level). Emits an
* 'update' event on success
*
* @param {Array} entry
* @return {boolean} success - false if entry doesn't match OB
*/
updateWith (entry) {
const { raw } = this
const priceI = raw ? 1 : 0
const count = raw ? -1 : entry[1]
const price = entry[priceI]
const oID = entry[0] // only for raw books
const amount = entry[2]
const side = amount < 0 ? this.asks : this.bids
let insertIndex = -1
// apply insert directly if empty
if (side.length === 0 && (raw || count > 0)) {
side.push(entry)
this.emit('update', entry)
return true
}
for (let i = 0; i < side.length; i++) {
if (insertIndex === -1 && (
(amount > 0 && price > side[i][priceI]) ||
(amount < 0 && price < side[i][priceI])
)) {
insertIndex = i // insert index to maintain sort
}
// Match by price level, or order ID for raw books
if ((!raw && side[i][priceI] === price) || (raw && side[i][0] === oID)) {
if ((!raw && count === 0) || (raw && price === 0)) {
side.splice(i, 1) // remove
} else if (!raw || (raw && price > 0)) {
side[i] = entry // update
}
this.emit('update', entry)
return true
}
}
// remove unkown
if ((raw && price === 0) || (!raw && count === 0)) {
debug(`ignoring unknown price level: ${JSON.stringify(entry)}`)
return false
}
// add
if (insertIndex === -1) {
side.push(entry)
} else {
side.splice(insertIndex, 0, entry)
}
this.emit('update', entry)
return true
}
/**
* @return {number} topBid - may be null
*/
topBid () {
const priceI = this.raw ? 1 : 0
return (this.topBidLevel() || [])[priceI] || null
}
/**
* @return {number} topBidLevel - may be null
*/
topBidLevel () {
return this.bids[0] || null
}
/**
* @return {number} topAsk - may be null
*/
topAsk () {
const priceI = this.raw ? 1 : 0
return (this.topAskLevel() || [])[priceI] || null
}
/**
* @return {number} topAskLevel - may be null
*/
topAskLevel () {
return this.asks[0] || null
}
/**
* @return {number} price
*/
midPrice () {
const priceI = this.raw ? 1 : 0
const topAsk = (this.asks[0] || [])[priceI] || 0
const topBid = (this.bids[0] || [])[priceI] || 0
if (topAsk === 0) return topBid
if (topBid === 0) return topAsk
return (topAsk + topBid) / 2
}
/**
* @return {number} spread - top bid/ask difference
*/
spread () {
const priceI = this.raw ? 1 : 0
const topAsk = (this.asks[0] || [])[priceI] || 0
const topBid = (this.bids[0] || [])[priceI] || 0
if (topAsk === 0 || topBid === 0) {
return 0
}
return topAsk - topBid
}
/**
* @return {number} amount - total buy-side volume
*/
bidAmount () {
let amount = 0
for (let i = 0; i < this.bids.length; i++) {
amount += this.bids[i][2]
}
return amount
}
/**
* @return {number} amount - total sell-side volume
*/
askAmount () {
let amount = 0
for (let i = 0; i < this.asks.length; i++) {
amount += this.asks[i][2]
}
return Math.abs(amount)
}
/**
* @param {number} price
* @return {Object} entry - unserialized, null if not found
*/
getEntry (price) {
const priceI = this.raw ? 1 : 0
const side = this.asks.length > 0
? price >= this.asks[0][priceI] ? this.asks : this.bids
: price <= this.bids[0][priceI] ? this.bids : this.asks
for (let i = 0; i < side.length; i++) {
if (price === side[i][priceI]) {
return OrderBook.unserialize(side[i])
}
}
return null
}
serialize () {
return (this.asks || []).concat(this.bids || [])
}
/**
* Modifies an array-format OB in place with an update entry. Sort is not
* gauranteed!
*
* @param {number[][]} ob
* @param {number[]} entry
* @param {boolean?} raw - true for raw 'R0' order books
* @return {boolean} success - false if entry doesn't match OB
*/
static updateArrayOBWith (ob, entry, raw = false) {
const priceI = raw ? 1 : 0
const price = entry[priceI]
const count = raw ? -1 : entry[1]
let insertIndex = -1
for (let i = 0; i < ob.length; i++) {
if (price > ob[i][priceI] && insertIndex === -1) {
insertIndex = i
}
if ((!raw && ob[i][priceI] === price) || (raw && ob[i][0] === entry[0])) {
if ((!raw && count === 0) || (raw && price === 0)) {
ob.splice(i, 1) // remove existing
} else {
ob[i] = entry // update existing
}
return true
}
}
// remove unkown
if ((!raw && count === 0) || (raw && price === 0)) return false
// add
if (insertIndex === -1) {
ob.push(entry)
} else {
ob.splice(insertIndex, 0, entry)
}
return true
}
static arrayOBMidPrice (ob = [], raw = false) {
if (ob.length === 0) return null
const priceI = raw ? 1 : 0
let bestBuy = -Infinity
let bestAsk = Infinity
let entry
for (let i = 0; i < ob.length; i++) {
entry = ob[i]
if (entry[2] > 0 && entry[priceI] > bestBuy) bestBuy = entry[priceI]
if (entry[2] < 0 && entry[priceI] < bestAsk) bestAsk = entry[priceI]
}
if (bestBuy === -Infinity || bestAsk === Infinity) return null
return (bestAsk + bestBuy) / 2.0
}
/**
* Converts an array order book entry or snapshot to an object, with 'price',
* 'count', and 'amount' keys on entries
*
* @param {number[]|number[][]} arr
* @param {boolean?} raw - true for raw 'R0' order books
* @return {Object} ob - either a map w/ bids & asks, or single entry object
*/
static unserialize (arr, raw = false) {
if (Array.isArray(arr[0])) {
const entries = arr.map(e => OrderBook.unserialize(e))
const bids = entries.filter(e => e.amount > 0)
const asks = entries.filter(e => e.amount < 0)
return { bids, asks }
}
return raw ? {
orderID: arr[0],
price: arr[1],
amount: arr[2]
} : {
price: arr[0],
count: arr[1],
amount: arr[2]
}
}
}
module.exports = OrderBook