import { Logger , LoggerService } from '@nestjs/common' ; import BN from 'bn.js' ; import { List } from 'immutable' ; import { Subject } from 'rxjs' ; import { concatMap } from 'rxjs/operators' ; import { DirectBuy } from './DirectBuy' ; import { IMatchableOrder } from './IMatchableOrder' ; import { OrderSide } from './OrderSide' ; import { IPriceStrategy } from './strategy/IPriceStrategy' ; import { Trade } from './Trade' ; import { TradeExecutedEvent } from './TradeExecutedEvent' ; export enum ActionResult { Cancelled , Error } export type ActionResultEvent = { orderId : string ; result : ActionResult ; error ? : string ; } ; export type OrderBook < TProduct , TProductFilter > = { asks : List < IMatchableOrder < TProduct , TProductFilter > > ; bids : List < IMatchableOrder < TProduct , TProductFilter > > ; } ; enum ActionKind { AddOrder , AddDirectBuy , CancelOrder } type OrderBookAction < TProduct , TProductFilter > = { kind : ActionKind ; value : IMatchableOrder < TProduct , TProductFilter > | DirectBuy | string ; } ; const prettyJSON = ( input ) => JSON . stringify ( input , null , 2 ) ; export class MatchingEngine < TProduct , TProductFilter > { private bids : List < IMatchableOrder < TProduct , TProductFilter > > = List < IMatchableOrder < TProduct , TProductFilter > > ( ) ; private asks : List < IMatchableOrder < TProduct , TProductFilter > > = List < IMatchableOrder < TProduct , TProductFilter > > ( ) ; private readonly triggers = new Subject < any > ( ) ; public trades = new Subject < List < TradeExecutedEvent > > ( ) ; public actionResults = new Subject < List < ActionResultEvent > > ( ) ; private pendingActions = List < OrderBookAction < TProduct , TProductFilter > > ( ) ; constructor ( private readonly priceStrategy , private logger = new Logger ( MatchingEngine . name ) ) { this . triggers . pipe ( concatMap ( async ( ) => this . trigger ( ) ) ) . subscribe ( ) ; } public submitOrder ( order ) { this . logger . debug ( `Submitting order: ${ order . id }` ) ; this . pendingActions = this . pendingActions . concat ( { kind : ActionKind . AddOrder , value : order . clone ( ) } ) ; } public submitDirectBuy ( directBuy ) { this . logger . debug ( `Submitting direct buy: ${ prettyJSON ( directBuy ) }` ) ; this . pendingActions = this . pendingActions . concat ( { kind : ActionKind . AddDirectBuy , value : directBuy . clone ( ) } ) ; } public cancelOrder ( orderId ) { this . logger . debug ( `Submitting cancel order: ${ orderId }` ) ; this . pendingActions = this . pendingActions . concat ( { kind : ActionKind . CancelOrder , value : orderId } ) ; } public orderBook ( ) { const now = new Date ( ) ; const validFromFilter = ( order ) => order . validFrom <= now ; return { asks : this . asks . filter ( validFromFilter ) , bids : this . bids . filter ( validFromFilter ) } ; } public orderBookByProduct ( productFilter ) { const { asks , bids } = this . orderBook ( ) ; const filteredAsks = asks . filter ( ( ask ) => ask . filterBy ( productFilter ) ) ; const filteredBids = bids . filter ( ( bid ) => bid . filterBy ( productFilter ) ) ; return { asks : filteredAsks , bids : filteredBids } ; } public tick ( ) { this . triggers . next ( null ) ; } public clear ( ) { this . asks = this . asks . clear ( ) ; this . bids = this . bids . clear ( ) ; this . pendingActions = this . pendingActions . clear ( ) ; } private insertOrder ( order ) { if ( order . side === OrderSide . Ask ) { this . asks = this . insert ( this . asks , order ) ; } else { this . bids = this . insert ( this . bids , order ) ; } } private trigger ( ) { const actions = this . pendingActions ; let trades = List < Trade > ( ) ; let statusChange = List < ActionResultEvent > ( ) ; actions . forEach ( ( action ) => { switch ( action . kind ) { case ActionKind . AddOrder : { try { const order = action . value as IMatchableOrder < TProduct , TProductFilter > ; this . insertOrder ( order ) ; trades = trades . concat ( this . match ( ) ) ; } catch ( error ) { console . log ( error ) ; } break ; } case ActionKind . CancelOrder : { try { const id = action . value as string ; const cancelEvent = this . cancel ( id ) ; statusChange = statusChange . concat ( cancelEvent ) ; } catch ( error ) { console . log ( error ) ; } break ; } case ActionKind . AddDirectBuy : { const directBuy = action . value as DirectBuy ; try { const trade = this . directBuy ( directBuy ) ; trades = trades . concat ( trade ) ; } catch ( error ) { const notExecutedEvent = { orderId : directBuy . id , result : ActionResult . Error , error : error . msg } ; statusChange = statusChange . concat ( notExecutedEvent ) ; } break ; } default : throw new Error ( 'Unexpected action' ) ; } } ) ; this . cleanOrderBook ( ) ; this . pendingActions = this . pendingActions . clear ( ) ; if ( ! trades . isEmpty ( ) ) { this . trades . next ( trades . map ( ( trade ) => new TradeExecutedEvent ( trade ) ) ) ; } if ( ! statusChange . isEmpty ( ) ) { this . actionResults . next ( statusChange ) ; } return true ; } private insert ( orderBook , order ) { const unSorted = orderBook . concat ( order ) ; const direction = order . side === OrderSide . Ask ? 1 : - 1 ; return unSorted . sortBy ( ( o ) => direction * o . price ) ; } private directBuy ( bid ) { const ask = this . asks . find ( ( o ) => o . id === bid . askId ) ; if ( ask . userId === bid . userId ) { throw new Error ( `Unable to direct buy your own bid: bid=${ JSON . stringify ( bid ) } ask=${ JSON . stringify ( ask ) }` ) ; } if ( ask . volume . lt ( bid . volume ) ) { throw new Error ( `Remaining volume is too low: ask.volume=${ ask . volume . toString ( 10 ) } bid.volume=${ bid . volume . toString ( 10 ) }` ) ; } if ( ask . price !== bid . price ) { throw new Error ( 'Unexpected price change' ) ; } const tradedVolume = bid . volume ; const updatedAsk = ask . updateWithTradedVolume ( tradedVolume ) ; const updatedBid = bid . updateWithTradedVolume ( tradedVolume ) ; this . asks = this . updateOrder ( this . asks , updatedAsk ) ; return new Trade ( updatedBid , updatedAsk , tradedVolume , bid . price ) ; } private match ( ) { this . logger . debug ( `Triggering match` ) ; const { bids , asks } = this . orderBook ( ) ; const executed = this . generateTrades ( asks , bids ) ; this . logger . debug ( `Executed trades: ${ prettyJSON ( executed . map ( ( t ) => ( { ask : t . ask . id , bid : t . bid . id , vol : t . volume . toString ( 10 ) , price : t . price } ) ) ) } ` ) ; return executed ; } private cancel ( orderId ) { const asks = this . findAndRemove ( this . asks , orderId ) ; if ( asks . result ) { this . asks = asks . modified ; } else { const bids = this . findAndRemove ( this . bids , orderId ) ; if ( bids . result ) { this . bids = bids . modified ; } else { throw new Error ( 'Unexpected orderId' ) ; } } return { orderId , result : ActionResult . Cancelled } ; } private findAndRemove ( source , orderId ) { const key = source . findKey ( ( o ) => o . id === orderId ) ; return key !== undefined ? { result : true , modified : source . remove ( key ) } : { result : false } ; } private updateOrder ( source , order ) { return source . set ( source . findIndex ( ( o ) => o . id === order . id ) , order ) ; } private cleanOrderBook ( ) { this . asks = this . asks . filterNot ( ( ask ) => ask . isFilled ) ; this . bids = this . bids . filterNot ( ( bid ) => bid . isFilled ) ; } private generateTrades ( asks , bids ) { let executed = List < Trade > ( ) ; bids . forEach ( ( bid ) => { asks . forEach ( ( ask ) => { const isMatching = this . matches ( bid , ask ) ; if ( ! isMatching ) { return true ; } const filled = BN . min ( ask . volume , bid . volume ) ; const price = this . priceStrategy . pickPrice ( ask , bid ) ; ask . updateWithTradedVolume ( filled ) ; bid . updateWithTradedVolume ( filled ) ; executed = executed . concat ( new Trade ( bid , ask , filled , price ) ) ; return true ; } ) ; } ) ; return executed ; } private matches ( bid , ask ) { const hasProductMatched = bid . matches ( ask ) ; const hasAskVolume = ! ask . volume . isNeg ( ) && ! ask . isFilled ; const hasBidVolume = ! bid . volume . isNeg ( ) && ! bid . isFilled ; const hasPriceMatched = ask . price <= bid . price ; const sameOwner = bid . userId === ask . userId ; this . logger . debug ( `[ask: ${ ask . id } <-> bid: ${ bid . id }] hasProductMatched: ${ hasProductMatched } hasAskVolume: ${ hasAskVolume } hasBidVolume: ${ hasBidVolume } hasPriceMatched: ${ hasPriceMatched } notSameOwner: ${ ! sameOwner }` ) ; return hasPriceMatched && hasAskVolume && hasBidVolume && hasProductMatched && ! sameOwner ; } }