tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
255 lines (203 loc) • 7.38 kB
text/typescript
import { decimalPlaces } from '../handy'
import { OrderBook, OnLevelRemovedCB } from '../orderbook'
import { BookChange, BookPriceLevel, BookSnapshot, Optional } from '../types'
import { Computable } from './computable'
type BookSnapshotComputableOptions = {
name?: string
depth: number
grouping?: 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 _initialized = false
private readonly _type = 'book_snapshot'
private readonly _orderBook: OrderBook
private readonly _depth: number
private readonly _interval: number
private readonly _name: string
private readonly _grouping: number | undefined
private readonly _groupingDecimalPlaces: number | undefined
private _lastUpdateTimestamp: Date = new Date(-1)
private _bids: Optional<BookPriceLevel>[] = []
private _asks: Optional<BookPriceLevel>[] = []
constructor({ depth, name, interval, removeCrossedLevels, grouping, onCrossedLevelRemoved }: BookSnapshotComputableOptions) {
this._depth = depth
this._interval = interval
this._grouping = grouping
this._groupingDecimalPlaces = this._grouping ? decimalPlaces(this._grouping) : undefined
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}${this._grouping ? `_grouped${this._grouping}` : ''}_${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
// or it's initial snapshot
if (this._hasNewSnapshot(bookChange.timestamp)) {
yield this._getSnapshot(bookChange)
if (this._initialized === false) {
this._initialized = true
}
}
}
public _hasNewSnapshot(timestamp: Date): boolean {
if (this._bookChanged === false) {
return false
}
// report new snapshot anytime book changed
if (this._interval === 0) {
return true
}
// report new snapshot for book snapshots with interval for initial snapshot
if (this._initialized === false) {
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)
if (this._grouping !== undefined) {
this._updateSideGrouped(this._orderBook.bids(), this._bids, this._getGroupedPriceForBids)
this._updateSideGrouped(this._orderBook.asks(), this._asks, this._getGroupedPriceForAsks)
} else {
this._updatedNotGrouped()
}
this._lastUpdateTimestamp = bookChange.timestamp
}
private _updatedNotGrouped() {
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
}
}
}
private _getGroupedPriceForBids = (price: number) => {
const pow = Math.pow(10, this._groupingDecimalPlaces!)
const pricePow = price * pow
const groupPow = this._grouping! * pow
const remainder = (pricePow % groupPow) / pow
return (pricePow - remainder * pow) / pow
}
private _getGroupedPriceForAsks = (price: number) => {
const pow = Math.pow(10, this._groupingDecimalPlaces!)
const pricePow = price * pow
const groupPow = this._grouping! * pow
const remainder = (pricePow % groupPow) / pow
return (pricePow - remainder * pow + (remainder > 0 ? groupPow : 0)) / pow
}
private _updateSideGrouped(
newLevels: IterableIterator<BookPriceLevel>,
existingGroupedLevels: Optional<BookPriceLevel>[],
getGroupedPriceForLevel: (price: number) => number
) {
let currentGroupedPrice: number | undefined = undefined
let aggAmount = 0
let currentDepth = 0
for (const notGroupedLevel of newLevels) {
const groupedPrice = getGroupedPriceForLevel(notGroupedLevel.price)
if (currentGroupedPrice == undefined) {
currentGroupedPrice = groupedPrice
}
if (currentGroupedPrice != groupedPrice) {
const groupedLevel = {
price: currentGroupedPrice,
amount: aggAmount
}
if (levelsChanged(existingGroupedLevels[currentDepth], groupedLevel)) {
existingGroupedLevels[currentDepth] = groupedLevel
this._bookChanged = true
}
currentDepth++
if (currentDepth === this._depth) {
break
}
currentGroupedPrice = groupedPrice
aggAmount = 0
}
aggAmount += notGroupedLevel.amount
}
if (currentDepth < this._depth && aggAmount > 0) {
const groupedLevel = {
price: currentGroupedPrice,
amount: aggAmount
}
if (levelsChanged(existingGroupedLevels[currentDepth], groupedLevel)) {
existingGroupedLevels[currentDepth] = groupedLevel
this._bookChanged = true
}
}
}
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,
grouping: this._grouping,
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)
}
}