UNPKG

@hackape/tardis-dev

Version:

Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js

155 lines (121 loc) 4.32 kB
import { OrderBook, OnLevelRemovedCB } from '../orderbook' import { BookChange, BookPriceLevel, BookSnapshot, Optional } from '../types' import { Computable } from './computable' type BookSnapshotComputableOptions = { name?: string depth: number interval: number removeCrossedLevels?: boolean onCrossedLevelRemoved?: OnLevelRemovedCB } export const computeBookSnapshots = (options: BookSnapshotComputableOptions): (() => Computable<BookSnapshot>) => () => new BookSnapshotComputable(options) const emptyBookLevel = { price: undefined, amount: undefined } const levelsChanged = (level1: Optional<BookPriceLevel>, level2: Optional<BookPriceLevel>) => { if (level1.amount !== level2.amount) { return true } if (level1.price !== level2.price) { return true } return false } class BookSnapshotComputable implements Computable<BookSnapshot> { public readonly sourceDataTypes = ['book_change'] private _bookChanged = false private readonly _type = 'book_snapshot' private readonly _orderBook: OrderBook private readonly _depth: number private readonly _interval: number private readonly _name: string private _lastUpdateTimestamp: Date = new Date(-1) private _bids: Optional<BookPriceLevel>[] = [] private _asks: Optional<BookPriceLevel>[] = [] constructor({ depth, name, interval, removeCrossedLevels, onCrossedLevelRemoved }: BookSnapshotComputableOptions) { this._depth = depth this._interval = interval this._orderBook = new OrderBook({ removeCrossedLevels, onCrossedLevelRemoved }) // initialize all bids/asks levels to empty ones for (let i = 0; i < this._depth; i++) { this._bids[i] = emptyBookLevel this._asks[i] = emptyBookLevel } if (name === undefined) { this._name = `${this._type}_${depth}_${interval}ms` } else { this._name = name } } public *compute(bookChange: BookChange) { if (this._hasNewSnapshot(bookChange.timestamp)) { yield this._getSnapshot(bookChange) } this._update(bookChange) // check again after the update as book snapshot with interval set to 0 (real-time) could have changed if (this._hasNewSnapshot(bookChange.timestamp)) { yield this._getSnapshot(bookChange) } } public _hasNewSnapshot(timestamp: Date): boolean { if (this._bookChanged === false) { return false } // report new snapshot anytime book changed if (this._interval === 0) { return true } const currentTimestampTimeBucket = this._getTimeBucket(timestamp) const snapshotTimestampBucket = this._getTimeBucket(this._lastUpdateTimestamp) if (currentTimestampTimeBucket > snapshotTimestampBucket) { // set timestamp to end of snapshot 'interval' period this._lastUpdateTimestamp = new Date((snapshotTimestampBucket + 1) * this._interval) return true } return false } public _update(bookChange: BookChange) { this._orderBook.update(bookChange) const bidsIterable = this._orderBook.bids() const asksIterable = this._orderBook.asks() for (let i = 0; i < this._depth; i++) { const bidLevelResult = bidsIterable.next() const newBid = bidLevelResult.done ? emptyBookLevel : bidLevelResult.value if (levelsChanged(this._bids[i], newBid)) { this._bids[i] = { ...newBid } this._bookChanged = true } const askLevelResult = asksIterable.next() const newAsk = askLevelResult.done ? emptyBookLevel : askLevelResult.value if (levelsChanged(this._asks[i], newAsk)) { this._asks[i] = { ...newAsk } this._bookChanged = true } } this._lastUpdateTimestamp = bookChange.timestamp } public _getSnapshot(bookChange: BookChange) { const snapshot: BookSnapshot = { type: this._type as any, symbol: bookChange.symbol, exchange: bookChange.exchange, name: this._name, depth: this._depth, interval: this._interval, bids: [...this._bids], asks: [...this._asks], timestamp: this._lastUpdateTimestamp, localTimestamp: bookChange.localTimestamp } this._bookChanged = false return snapshot } private _getTimeBucket(timestamp: Date) { return Math.floor(timestamp.valueOf() / this._interval) } }