UNPKG

ccxws

Version:

Websocket client for 37 cryptocurrency exchanges

273 lines 9.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.KrakenOrderBook = void 0; /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-return */ const L2Point_1 = require("./L2Point"); const crc_1 = __importDefault(require("crc")); /** * Prototype implementation of an order book for Kraken. This should be * used with a feed that originiates with data provided by Kraken * spec: https://docs.kraken.com/websockets/#message-book. * * ```javascript * const client = new KrakenClient(); * const market = { id: "XXBTZUSD", base: "BTC", quote: "USD" }; * client.subscribeLevel2Updates(market); * * let ob; * client.on("l2snapshot", snap => { * const asks = snap.asks.map(p => new KrakenOrderBookPoint(p.price, p.size, p.timestamp)); * const bids = snap.bids.map(p => new KrakenOrderBookPoint(p.price, p.size, p.timestamp)); * ob = new KrakenOrderBook(asks, bids); * }); * * client.on("l2update", update => { * for (let a of update.asks) { * ob.update(false, a.price, a.size, a.timestamp); * } * for (let b of update.bids) { * ob.update(true, b.price, b.size, b.timestamp); * } * }); * ``` * * @remarks * * This implementation uses sorted arrays to store ask and bid values. * The rationale is that after each batch of updates a snapshot operation * must be performed which will require ordering the values the book. * * This uses binary search to find the mutation index for the array. * This means we have worst case time complexity of: * * updates: O(log n) * insert: O(n) * delete: O(n) * snapshots: O(1) * * Because most order books operate towards the tip of the book, * highest bid and lowest ask, we can get average time complexity * improvements by using sorted arrays such that the bulk of the * operations occur towards the end of the array. This reduces the * number of operations required in shift operation. * * We will perform further research to determine the optimal data * structure before releasing a non-prototype version for other * exchanges. */ class KrakenOrderBook { /** * Creates an order book from the points provided. Can be used with a * snapshot to immeidately construct an orderbook. All input values * are expected to be KrakenOrderBookPoint instances. * * ```javascript * ob = new KrakenOrderBook(snapshot); * ``` */ constructor(snapshot) { this.asks = snapshot.asks .map(p => new L2Point_1.L2Point(Number(p.price), Number(p.size), p.timestamp, { rawPrice: p.price, rawSize: p.size, })) .sort(sortDesc); this.bids = snapshot.bids .map(p => new L2Point_1.L2Point(Number(p.price), Number(p.size), p.timestamp, { rawPrice: p.price, rawSize: p.size, })) .sort(sortAsc); } /** * Updates the orderbook with a new price level entry. This value will * either be and insertion, update, or deletion. The bid parameter * determines which side of the book the update falls on. * * ```javascript * client.on("l2update", update => ob.update(update)); * ``` */ update(update) { for (const { price, size, timestamp } of update.asks) { this._update(false, price, size, timestamp); } for (const { price, size, timestamp } of update.bids) { this._update(true, price, size, timestamp); } } _update(bid, price, size, timestamp) { const priceNum = Number(price); const sizeNum = Number(size); let arr; let index; // The best bids are the highest priced, meaning the tail of array // using the best bids would be sorted ascending if (bid) { arr = this.bids; index = findIndexAsc(arr, priceNum); } // The best asks are the lowest priced, meaning the tail of array // with the best asks would be sorted descending else { arr = this.asks; index = findIndexDesc(arr, priceNum); } // We perform an update when the index of hte current value has // the same price as the update we are now processing. if (arr[index] && arr[index].meta.rawPrice === price) { // Only perform an update if the new value has a newer timestamp // than the existing value if (timestamp <= arr[index].timestamp) { return; } // Remove the value when the size is 0 if (sizeNum === 0) { arr.splice(index, 1); return; } // Otherwise we perform an update by changing the size and timestamp arr[index].size = sizeNum; arr[index].meta.rawSize = size; arr[index].timestamp = timestamp; } // Otherwise we are performing an insert, which we will construct // a new point. Because we are using splice, which should have a // worst case runtime of O(N), we // O() else if (sizeNum > 0) { const point = new L2Point_1.L2Point(Number(price), Number(size), Number(timestamp), { rawPrice: price, rawSize: size, }); arr.splice(index, 0, point); } } /** * Captures a simple snapshot of best asks and bids up to the * requested depth. */ snapshot(depth) { const asks = []; for (let i = this.asks.length - 1; i >= this.asks.length - depth; i--) { const val = this.asks[i]; if (val) asks.push(val); } const bids = []; for (let i = this.bids.length - 1; i >= this.bids.length - depth; i--) { const val = this.bids[i]; if (val) bids.push(val); } return { asks, bids, }; } /** * Returns the checksum of the order book based on the algorithm * specified in https://docs.kraken.com/websockets/#book-checksum */ checksum() { const snap = this.snapshot(10); const data = checksumString(snap.asks, snap.bids); return crc_1.default.crc32(data).toString(10); } } exports.KrakenOrderBook = KrakenOrderBook; /** * Performs a binary search of a sorted array for the insert or update * position of the value and operates on a KrakenOrderBookPoint value */ function findIndexAsc(arr, key, l = 0, r = arr.length) { const mid = Math.floor((l + r) / 2); if (l === r) return mid; if (arr[mid] && arr[mid].price === key) return mid; if (arr[mid] && arr[mid].price > key) return findIndexAsc(arr, key, l, mid); if (arr[mid] && arr[mid].price < key) return findIndexAsc(arr, key, mid + 1, r); } /** * Performs a binary search of a sorted array for the insert or update * position of the value and operates on a KrakenOrderBookPoint value */ function findIndexDesc(arr, key, l = 0, r = arr.length) { const mid = Math.floor((l + r) / 2); if (l === r) return mid; if (arr[mid] && arr[mid].price === key) return mid; if (arr[mid] && arr[mid].price < key) return findIndexDesc(arr, key, l, mid); if (arr[mid] && arr[mid].price > key) return findIndexDesc(arr, key, mid + 1, r); } /** * Converts a raw number value into the format for crc checksum. This * format removes the . from the number, and strips all prefixed 0's * before the first real digit. For examples '0.00050000' => '50000' */ function crcNum(val) { let result = ""; const chars = val.split(""); let start = true; for (const char of chars) { if (start && char === "0") continue; if (char === ".") continue; start = false; result += char; } return result; } /** * Creates the checksum string from the bid and ask values based on the * algorithm. This converts string values into the stripped number * format in `crcNum` and concatenates the price and size for asks then * bids. * @param {L2Point[]} asks * @param {L2Point[]} bids */ function checksumString(asks, bids) { let data = ""; for (const ask of asks) { data += crcNum(ask.meta.rawPrice); data += crcNum(ask.meta.rawSize); } for (const bid of bids) { data += crcNum(bid.meta.rawPrice); data += crcNum(bid.meta.rawSize); } return data; } /** * Sorts points from high to low */ function sortDesc(a, b) { if (a.price > b.price) return -1; if (a.price < b.price) return 1; return 0; } /** * Sorts points from low to high */ function sortAsc(a, b) { if (a.price < b.price) return -1; if (a.price > b.price) return 1; return 0; } //# sourceMappingURL=KrakenOrderBook.js.map