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 ) : ( ( ) => Computable < BookSnapshot > ) => ( ) => new BookSnapshotComputable ( options ) ; const emptyBookLevel = { price : undefined , amount : undefined } ; const levelsChanged = ( level1 , level2 ) => { 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 } ) { this . _depth = depth ; this . _interval = interval ; this . _grouping = grouping ; this . _groupingDecimalPlaces = this . _grouping ? decimalPlaces ( this . _grouping ) : undefined ; this . _orderBook = new OrderBook ( { removeCrossedLevels , onCrossedLevelRemoved } ) ; 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 ) { if ( this . _hasNewSnapshot ( bookChange . timestamp ) ) { yield this . _getSnapshot ( bookChange ) ; } this . _update ( bookChange ) ; if ( this . _hasNewSnapshot ( bookChange . timestamp ) ) { yield this . _getSnapshot ( bookChange ) ; if ( this . _initialized === false ) { this . _initialized = true ; } } } public _hasNewSnapshot ( timestamp ) { if ( this . _bookChanged === false ) { return false ; } if ( this . _interval === 0 ) { return true ; } if ( this . _initialized === false ) { return true ; } const currentTimestampTimeBucket = this . _getTimeBucket ( timestamp ) ; const snapshotTimestampBucket = this . _getTimeBucket ( this . _lastUpdateTimestamp ) ; if ( currentTimestampTimeBucket > snapshotTimestampBucket ) { this . _lastUpdateTimestamp = new Date ( ( snapshotTimestampBucket + 1 ) * this . _interval ) ; return true ; } return false ; } public _update ( 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 ) => { 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 ) => { 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 , existingGroupedLevels , getGroupedPriceForLevel ) { let currentGroupedPrice = 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 ) { const snapshot = { 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 ) { return Math . floor ( timestamp . valueOf ( ) / this . _interval ) ; } }