@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
text/typescript
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)
}
}