UNPKG

ccxws

Version:

Websocket client for 37 cryptocurrency exchanges

175 lines 6.64 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.KucoinOrderBook = void 0; const L2Point_1 = require("./L2Point"); const L3Point_1 = require("./L3Point"); /** * Prototype for maintaining a Level 3 order book for Kucoin according * to the instructions defined here: * https://docs.kucoin.com/#full-matchengine-data-level-3 * * This technique uses a Map to store orders. It has efficient updates * but will be slow for performing tip of book or snapshot operations. * * # Example * ```javascript * const ccxws = require("ccxws"); * const KucoinOrderBook = require("ccxws/src/orderbooks/KucoinOrderBook"); * * let market = { id: "BTC-USDT", base: "BTC", quote: "USDT" }; * let updates = []; * let ob; * * const client = new ccxws.Kucoin(); * client.subscribeLevel3Updates(market); * client.on("l3snapshot", snapshot => { * ob = new KucoinOrderBook(snapshot, updates); * }); * * client.on("l3update", update => { * // enqueue updates until snapshot arrives * if (!ob) { * updates.push(update); * return; * } * * // validate the sequence and exit if we are out of sync * if (ob.sequenceId + 1 !== update.sequenceId) { * console.log(`out of sync, expected ${ob.sequenceId + 1}, got ${update.sequenceId}`); * process.exit(1); * } * * // apply update * ob.update(update); * }); * ``` */ class KucoinOrderBook { /** * Constructs a new order book by starting with a snapshop and replaying * any updates that have been queued. */ constructor(snap, updates) { this.asks = new Map(); this.bids = new Map(); this.sequenceId = snap.sequenceId; // Verify that we have queued updates if (!updates.length || snap.sequenceId >= updates[updates.length - 1].sequenceId) { throw new Error("Must queue updates prior to snapshot"); } // apply asks from snapshot for (const ask of snap.asks) { this.asks.set(ask.orderId, new L3Point_1.L3Point(ask.orderId, Number(ask.price), Number(ask.size), Number(ask.meta.timestampMs))); } // apply bids from snapshot for (const bid of snap.bids) { this.bids.set(bid.orderId, new L3Point_1.L3Point(bid.orderId, Number(bid.price), Number(bid.size), Number(bid.meta.timestampMs))); } // Replay pending updates for (const update of updates) { // Ignore updates that are prior to the snapshot if (update.sequenceId <= this.sequenceId) continue; // Ensure that we are in sync if (update.sequenceId > this.sequenceId + 1) { throw new Error("Missing update"); } this.update(update); } } update(update) { // Always update the sequence this.sequenceId = update.sequenceId; // find the point in the update const updatePoint = update.asks[0] || update.bids[0]; // Skip received orders if (updatePoint.meta.type === "received") return; // Open - insert a new point in the appropriate side (ask, bid). // When receiving a message with price="", size="0", // it means this is a hidden order and we can ignore it. if (updatePoint.meta.type === "open") { const map = update.asks[0] ? this.asks : this.bids; // Ignore private orders if (!Number(updatePoint.price) && !Number(updatePoint.size)) { return; } const obPoint = new L3Point_1.L3Point(updatePoint.orderId, Number(updatePoint.price), Number(updatePoint.size), update.timestampMs); map.set(obPoint.orderId, obPoint); return; } // Done - remove the order, this won't include the side, so we // remove it from both side. if (updatePoint.meta.type === "done") { this.asks.delete(updatePoint.orderId); this.bids.delete(updatePoint.orderId); return; } // Change - modify the amount for the order. Update will be in both // the asks and bids since the update message doesn't include a // side. Change messages are sent when an order changes in size. // This includes resting orders (open) as well as recieved but not // yet open. In the latter case, no point will exist on the book // yet. if (updatePoint.meta.type === "update") { const obPoint = this.asks.get(updatePoint.orderId) || this.bids.get(updatePoint.orderId); // prettier-ignore if (obPoint) obPoint.size = Number(updatePoint.size); return; } // Trade - reduce the size of the maker to the remain size. We ignore // any updates if the remainSize is zero, since the done event may // have already removed the trae if (updatePoint.meta.type === "match") { const obPoint = this.asks.get(updatePoint.orderId) || this.bids.get(updatePoint.orderId); if (obPoint) obPoint.size = Number(updatePoint.size); return; } } /** * Captures a price aggregated snapshot * @param {number} depth */ snapshot(depth = 10) { return { sequenceId: this.sequenceId, asks: snapSide(this.asks, sortAsc, depth), bids: snapSide(this.bids, sortDesc, depth), }; } } exports.KucoinOrderBook = KucoinOrderBook; function snapSide(map, sorter, depth) { const aggMap = aggByPrice(map); return Array.from(aggMap.values()).sort(sorter).slice(0, depth); } function aggByPrice(map) { // Aggregate the values into price points const aggMap = new Map(); for (const point of map.values()) { const price = point.price; const size = point.size; const timestamp = point.timestamp; // If we don't have this price point in the aggregate then we create // a new price point with empty values. if (!aggMap.has(price)) { aggMap.set(price, new L2Point_1.L2Point(price, 0, 0)); } // Obtain the price point from the aggregation const aggPoint = aggMap.get(price); // Update the size aggPoint.size += size; // Update the timestamp if (aggPoint.timestamp < timestamp) aggPoint.timestamp = timestamp; } return aggMap; } function sortAsc(a, b) { return a.price - b.price; } function sortDesc(a, b) { return b.price - a.price; } //# sourceMappingURL=KucoinOrderBook.js.map