import { RBTree } from 'bintrees' ; import { BookChange , BookPriceLevel , Writeable } from './types' ; export type OnLevelRemovedCB = ( bookChange : BookChange , bestBidBeforeRemoval : BookPriceLevel | undefined , bestBidAfterRemoval : BookPriceLevel | undefined , bestAskBeforeRemoval : BookPriceLevel | undefined , bestAskAfterRemoval : BookPriceLevel | undefined ) => void ; export class OrderBook { private readonly _bids = new RBTree < BookPriceLevel > ( ( nodeA , nodeB ) => nodeB . price - nodeA . price ) ; private readonly _asks = new RBTree < BookPriceLevel > ( ( nodeA , nodeB ) => nodeA . price - nodeB . price ) ; private readonly _removeCrossedLevels : boolean | undefined ; private readonly _onCrossedLevelRemoved : OnLevelRemovedCB | undefined ; private _receivedInitialSnapshot = false ; constructor ( { removeCrossedLevels , onCrossedLevelRemoved } = { } ) { this . _removeCrossedLevels = removeCrossedLevels ; this . _onCrossedLevelRemoved = onCrossedLevelRemoved ; } public update ( bookChange ) { if ( bookChange . isSnapshot ) { this . _bids . clear ( ) ; this . _asks . clear ( ) ; this . _receivedInitialSnapshot = true ; } if ( this . _receivedInitialSnapshot ) { applyPriceLevelChanges ( this . _asks , bookChange . asks ) ; applyPriceLevelChanges ( this . _bids , bookChange . bids ) ; } if ( this . _removeCrossedLevels ) { this . _removeCrossedLevelsIfNeeded ( bookChange ) ; } } public bestBid ( ) { const result = this . bids ( ) . next ( ) ; if ( result . done === false ) { return result . value ; } return undefined ; } public bestAsk ( ) { const result = this . asks ( ) . next ( ) ; if ( result . done === false ) { return result . value ; } return undefined ; } private _removeCrossedLevelsIfNeeded ( bookChange ) { let bestBid = this . bestBid ( ) ; let bestAsk = this . bestAsk ( ) ; let bookIsCrossed = bestBid !== undefined && bestAsk !== undefined && bestBid . price >= bestAsk . price ; if ( bookIsCrossed ) { 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 ; } } } private _removeBestAsk ( ) { const bestAsk = this . bestAsk ( ) ; if ( bestAsk !== undefined ) { applyPriceLevelChanges ( this . _asks , [ { price : bestAsk . price , amount : 0 } ] ) ; } } private _removeBestBid ( ) { const bestBid = this . bestBid ( ) ; if ( bestBid !== undefined ) { applyPriceLevelChanges ( this . _bids , [ { price : bestBid . price , amount : 0 } ] ) ; } } public * bids ( ) { const iterator = this . _bids . iterator ( ) ; let level = iterator . next ( ) ; while ( level !== null ) { yield level ; level = iterator . next ( ) ; } } public * asks ( ) { const iterator = this . _asks . iterator ( ) ; let level = iterator . next ( ) ; while ( level !== null ) { yield level ; level = iterator . next ( ) ; } } } function applyPriceLevelChanges ( tree , priceLevelChanges ) { for ( const priceLevel of priceLevelChanges ) { const node = tree . find ( priceLevel ) as Writeable < BookPriceLevel > ; 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 } ) ; } } }