@hackape/tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
132 lines • 5.11 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.OrderBook = void 0;
const bintrees_1 = require("bintrees");
class OrderBook {
constructor({ removeCrossedLevels, onCrossedLevelRemoved } = {}) {
this._bids = new bintrees_1.RBTree((nodeA, nodeB) => nodeB.price - nodeA.price);
this._asks = new bintrees_1.RBTree((nodeA, nodeB) => nodeA.price - nodeB.price);
this._receivedInitialSnapshot = false;
this._removeCrossedLevels = removeCrossedLevels;
this._onCrossedLevelRemoved = onCrossedLevelRemoved;
}
update(bookChange) {
// clear everything up, when snapshot received so we don't have stale levels by accident
if (bookChange.isSnapshot) {
this._bids.clear();
this._asks.clear();
this._receivedInitialSnapshot = true;
}
// process updates as long as we've received initial snapshot, otherwise ignore such messages
if (this._receivedInitialSnapshot) {
applyPriceLevelChanges(this._asks, bookChange.asks);
applyPriceLevelChanges(this._bids, bookChange.bids);
}
if (this._removeCrossedLevels) {
this._removeCrossedLevelsIfNeeded(bookChange);
}
}
bestBid() {
const result = this.bids().next();
if (result.done === false) {
return result.value;
}
return undefined;
}
bestAsk() {
const result = this.asks().next();
if (result.done === false) {
return result.value;
}
return undefined;
}
_removeCrossedLevelsIfNeeded(bookChange) {
let bestBid = this.bestBid();
let bestAsk = this.bestAsk();
let bookIsCrossed = bestBid !== undefined && bestAsk !== undefined && bestBid.price >= bestAsk.price;
// if after update we have crossed order book (best bid >= best ask)
// it most likely means that exchange has not published delete message for the other side of the book
// more info:
// https://www.reddit.com/r/KrakenSupport/comments/d1a4nx/websocket_orderbook_receiving_wrong_bid_price_for/
// https://www.reddit.com/r/BitMEX/comments/8lbj9e/bidask_ledger_weirdness/
// https://twitter.com/coinarb/status/931260529993170944
if (bookIsCrossed) {
// decide from which side of the book we should remove level so book isn't crossed anymore
// if current book update updated "best ask" it means we should remove "best bid" as exchange hasn't provided book change update
// that deletes it, and vice versa for for "best bids"
const shouldRemoveBestBid = bookChange.asks.some((s) => s.price === bestAsk.price);
while (bookIsCrossed) {
if (shouldRemoveBestBid) {
this._removeBestBid();
}
else {
this._removeBestAsk();
}
const newBestBid = this.bestBid();
const newBestAsk = this.bestAsk();
if (this._onCrossedLevelRemoved !== undefined) {
this._onCrossedLevelRemoved(bookChange, bestBid, newBestBid, bestAsk, newBestAsk);
}
bestBid = newBestBid;
bestAsk = newBestAsk;
bookIsCrossed = bestBid !== undefined && bestAsk !== undefined && bestBid.price >= bestAsk.price;
}
}
}
_removeBestAsk() {
const bestAsk = this.bestAsk();
if (bestAsk !== undefined) {
applyPriceLevelChanges(this._asks, [
{
price: bestAsk.price,
amount: 0
}
]);
}
}
_removeBestBid() {
const bestBid = this.bestBid();
if (bestBid !== undefined) {
applyPriceLevelChanges(this._bids, [
{
price: bestBid.price,
amount: 0
}
]);
}
}
*bids() {
const iterator = this._bids.iterator();
let level = iterator.next();
while (level !== null) {
yield level;
level = iterator.next();
}
}
*asks() {
const iterator = this._asks.iterator();
let level = iterator.next();
while (level !== null) {
yield level;
level = iterator.next();
}
}
}
exports.OrderBook = OrderBook;
function applyPriceLevelChanges(tree, priceLevelChanges) {
for (const priceLevel of priceLevelChanges) {
const node = tree.find(priceLevel);
const nodeExists = node !== null;
const levelShouldBeRemoved = priceLevel.amount === 0;
if (nodeExists && levelShouldBeRemoved) {
tree.remove(priceLevel);
}
else if (nodeExists) {
node.amount = priceLevel.amount;
}
else if (levelShouldBeRemoved === false) {
tree.insert({ ...priceLevel });
}
}
}
//# sourceMappingURL=orderbook.js.map
;