import IB , { EventName , Stock } from '@stoqey/ib' ; import isEmpty from 'lodash/isEmpty' ; import findIndex from 'lodash/findIndex' ; import compact from 'lodash/compact' ; import { CreateSale , OrderWithContract , OrderStatus , OrderStatusType , OrderStock , GetOrderType , } from './orders.interfaces' ; import { publishDataToTopic , IbkrEvents , IBKREVENTS } from '../events' ; import IBKRConnection from '../connection/IBKRConnection' ; import { log , verbose } from '../log' ; import { createSymbolAndTickerId } from './Orders.util' ; import { handleEventfulError } from '../events/HandleError' ; const ibkrEvents = IbkrEvents . Instance ; interface SymbolTickerOrder { id : string ; tickerId : number ; orderPermId ? : number ; symbol : string ; stockOrderRequest : OrderStock ; orderStatus ? : OrderStatusType ; } export class Orders { ib : IB = null ; tickerId = 0 ; processing = false ; /**
     * Orders to be taken from nextValidId
     * These are always deleted after order is submitted to IB
     */ stockOrders : OrderStock [ ] = [ ] ; timeoutRetries : { [ x ] : NodeJS . Timeout [ ] ; } = { } ; unique tickersAndOrders : SymbolTickerOrder [ ] = [ ] ; /**
     * Redundant orderIdNext recorded
     */ orderIdNext : number = null ; public openedOrders : OrderWithContract [ ] = [ ] ; public receivedOrders = false ; private static _instance : Orders ; public static get Instance ( ) : Orders { return this . _instance || ( this . _instance = new this ( ) ) ; } private constructor ( ) { const self = this ; ibkrEvents . on ( IBKREVENTS . CONNECTED , async ( ) => { self . init ( ) ; } ) ; ibkrEvents . emit ( IBKREVENTS . PING ) ; } /**
     * init
     */ public init = async ( ) : Promise < void > => { const self = this ; if ( ! self . ib ) { const ib = IBKRConnection . Instance . getIBKR ( ) ; self . ib = ib ; ib . on ( EventName . openOrderEnd , ( ) => { publishDataToTopic ( { topic : IBKREVENTS . OPEN_ORDERS , data : self . openedOrders , } ) ; } ) ; ib . on ( EventName . orderStatus , ( id , status , filled , remaining , avgFillPrice , permId , parentId , lastFillPrice , clientId , whyHeld ) => { const currentOrder = self . openedOrders . find ( ( oo ) => oo . orderId === id ) ; const orderStatus = { status : status as any , filled , remaining , avgFillPrice , permId , parentId , lastFillPrice , clientId , whyHeld : whyHeld as any , } ; publishDataToTopic ( { topic : IBKREVENTS . ORDER_STATUS , data : { order : currentOrder , orderStatus , } , } ) ; verbose ( `Orders > orderStatus ${ currentOrder && currentOrder . symbol }` , JSON . stringify ( { id , status , filled , remaining , symbol : currentOrder && currentOrder . symbol , } ) ) ; } ) ; ib . on ( EventName . openOrder , ( orderId , contract , order , orderState ) => { self . receivedOrders = true ; const openedOrders = self . openedOrders ; const currentOrderindex = findIndex ( openedOrders , { orderId } ) ; openedOrders [ currentOrderindex ] = { orderId , orderState , ... order , ... contract , } ; self . openedOrders = compact ( openedOrders ) ; if ( orderState . status === 'Filled' ) { log ( `Filled -----> DELETE FROM OPEN ORDERS -------> symbol=${ contract && contract . symbol }` ) ; self . openedOrders = compact ( ( self . openedOrders || [ ] ) . filter ( ( i ) => i . orderId !== orderId ) ) ; } if ( [ 'PendingCancel' , 'Cancelled' , 'ApiCancelled' ] . includes ( orderState . status ) ) { log ( `${ orderState . status } -----> DELETE FROM OPEN ORDERS -------> ${ contract && contract . symbol }` ) ; self . openedOrders = compact ( ( self . openedOrders || [ ] ) . filter ( ( i ) => i . orderId !== orderId ) ) ; } const openOrdersArr = self . openedOrders ; log ( `OPEN ORDERS ${ openOrdersArr && openOrdersArr . length }` ) ; const allTickerOrder = self . tickersAndOrders ; const thisOrderTicker = allTickerOrder . find ( ( tickerOrder ) => tickerOrder . tickerId === orderId ) ; if ( ! isEmpty ( thisOrderTicker ) ) { let updatedSymbolTicker = null ; self . tickersAndOrders = self . tickersAndOrders . map ( ( i ) => { if ( i . tickerId === orderId ) { const updatedSymbolTickerX = { ... i , orderPermId : order . permId , symbol : thisOrderTicker . symbol , orderStatus : orderState . status as any , } ; updatedSymbolTicker = updatedSymbolTickerX ; return updatedSymbolTickerX ; } return i ; } ) ; if ( orderState . status === 'Filled' ) { if ( ! isEmpty ( updatedSymbolTicker . stockOrderRequest ) ) { const { stockOrderRequest } = updatedSymbolTicker ; const { exitTrade , exitParams , symbol , capital } = stockOrderRequest ; const dataSaleSymbolOrder = { ... order , ... contract , orderState , orderId , } ; if ( exitTrade ) { const { exitPrice , exitTime , entryTime , entryPrice } = exitParams ; const newSale = { capital , exitPrice , exitTime , entryTime , entryPrice , symbol , profit : entryPrice - exitPrice , } ; log ( `AccountOrderStock.openOrder` , `FILLED, TO CREATE SALE -> ${ contract . symbol } ${ order . action } ${ order . totalQuantity }  ${ orderState . status }` ) ; return publishDataToTopic ( { topic : IBKREVENTS . ORDER_FILLED , data : { sale : newSale , order : dataSaleSymbolOrder } , } ) ; } log ( `AccountOrderStock.openOrder` , `FILLED, but no sale created -> ${ contract . symbol } ${ order . action } ${ order . totalQuantity }  ${ orderState . status }` ) ; publishDataToTopic ( { topic : IBKREVENTS . ORDER_FILLED , data : { sale : null , order : dataSaleSymbolOrder } , } ) ; } } } } ) ; ibkrEvents . on ( IBKREVENTS . PLACE_ORDER , async ( { stockOrder } ) => { await self . placeOrder ( stockOrder ) ; } ) ; } } ; public getOpenOrders = async ( ) : Promise < OrderWithContract [ ] > => { const self = this ; const openedOrders = [ ] ; return new Promise ( ( resolve ) => { let done = false ; const handleOpenOrders = ( ordersData ) => { if ( ! done ) { self . ib . off ( EventName . openOrder , handleOpenOrders ) ; self . ib . off ( EventName . openOrderEnd , openOrderEnd ) ; done = true ; self . openedOrders = ordersData ; return resolve ( ordersData ) ; } } ; const openOrderEnd = ( ) => { handleOpenOrders ( openedOrders ) ; } ; self . ib . once ( EventName . openOrderEnd , openOrderEnd ) ; self . ib . on ( EventName . openOrder , function ( orderId , contract , order , orderState ) { if ( [ 'PendingSubmit' , 'PreSubmitted' , 'Submitted' ] . includes ( orderState . status ) ) { openedOrders . push ( { orderId , orderState , ... order , ... contract , } ) ; } } ) ; self . ib . reqAllOpenOrders ( ) ; return ; } ) ; } ; public isActive = ( ) : boolean => { return this . receivedOrders ; } ; stockOrder options when public placeOrder = async ( stockOrder , options ? ) : Promise < any > => { const self = this ; const ib = self . ib ; const { symbol } = stockOrder ; const shouldBeUniqueOrder = ( options && options . unique ) || false ; const success = ( ) : boolean => { ib . off ( EventName . nextValidId , handleOrderIdNext ) ; self . processing = false ; return true ; } ; const erroredOut = ( error ? ) : boolean => { ib . off ( EventName . nextValidId , handleOrderIdNext ) ; self . processing = false ; return false ; } ; const checkPending = ( ) : boolean => { const orderSize = stockOrder . size ; const orderIsPending = ( ) => { log ( 'placingOrderNow' , `*********************** Order is already being processed for ${ stockOrder . action } symbol=${ symbol } ` ) ; return erroredOut ( ) ; } ; if ( Number . isNaN ( orderSize ) ) { log ( 'placingOrderNow.checkPending' , `*********************** orderSize is NaN size=${ orderSize } action=${ stockOrder . action } symbol=${ symbol }` ) ; return erroredOut ( ) ; } /**
             * Check existing orders from tickersAndOrders
             */ const currentOpenOrders = self . tickersAndOrders ; const currentOpenOrdersSymbolId = currentOpenOrders . filter ( ( cos ) => cos . symbol === symbol ) ; verbose ( 'currentOpenOrdersSymbolId' , JSON . stringify ( currentOpenOrdersSymbolId . map ( ( o ) => o . symbol ) ) ) ; if ( ! isEmpty ( currentOpenOrdersSymbolId ) ) { const existingOrdersStatuses = currentOpenOrdersSymbolId . map ( ( i ) => i . orderStatus ) ; const allOrdersThatArePending = existingOrdersStatuses . filter ( ( status ) => [ 'PreSubmitted' , 'Submitted' , 'PendingSubmit' ] . includes ( status ) ) ; if ( shouldBeUniqueOrder ) { if ( ! isEmpty ( allOrdersThatArePending ) ) { return orderIsPending ( ) ; } } } /**
             * Check existing opened placed orders
             */ const checkExistingOrders = self . openedOrders ; log ( 'placingOrderNow' , `Existing orders in queue -> ${ ( checkExistingOrders || [ ] ) . map ( ( i ) => i . symbol ) }` ) ; if ( ! isEmpty ( checkExistingOrders ) ) { const findMatchingAction = checkExistingOrders . filter ( ( exi ) => exi . action === stockOrder . action && exi . symbol === stockOrder . symbol ) ; if ( ! isEmpty ( findMatchingAction ) ) { if ( shouldBeUniqueOrder ) { log ( 'placingOrderNow' , `Order already exist for ${ stockOrder . action }, ${ findMatchingAction [ 0 ] . symbol } ->  @${ stockOrder . parameters [ 0 ] }` ) ; return erroredOut ( ) ; } } } return true ; } ; const handleOrderIdNext = ( orderIdNext ) => { const tickerToUse = ++ orderIdNext ; const currentOrders = self . stockOrders ; if ( isEmpty ( currentOrders ) ) { log ( 'handleOrderIdNext' , `Stock Orders are empty` ) ; return erroredOut ( ) ; } const stockOrder = self . stockOrders . shift ( ) ; if ( isEmpty ( stockOrder ) ) { log ( 'handleOrderIdNext' , `First Stock Orders Item is empty` ) ; return erroredOut ( ) ; } const { symbol , size } = stockOrder ; const StockOrderType = GetOrderType ( stockOrder . type ) ; const orderCommand = StockOrderType ; const args = stockOrder . parameters ; if ( isEmpty ( args ) ) { log ( 'handleOrderIdNext' , `Arguments cannot be null` ) ; return erroredOut ( ) ; } const tickerIdWithSymbol = createSymbolAndTickerId ( symbol , tickerToUse ) ; const oldTickerSymbol = self . tickersAndOrders . find ( ( t ) => t . id === tickerIdWithSymbol ) ; if ( oldTickerSymbol ) { log ( 'handleOrderIdNext' , `Order already exists` ) ; return erroredOut ( ) ; } const tickerNOrder = { id : tickerIdWithSymbol , tickerId : tickerToUse , symbol , orderStatus : 'PendingSubmit' , stockOrderRequest : stockOrder , } ; ib . placeOrder ( tickerToUse , new Stock ( stockOrder . symbol ) , new orderCommand ( stockOrder . action , ... args ) ) ; self . tickersAndOrders . push ( tickerNOrder ) ; self . tickerId = tickerToUse ; ib . reqAllOpenOrders ( ) ; log ( 'handleOrderIdNext' , `Placing order for ... tickerToUse=${ tickerToUse } orderIdNext=${ orderIdNext } tickerId=${ self . tickerId } symbol=${ symbol } size=${ size }` ) ; return success ( ) ; } ; function placingOrderNow ( ) { if ( isEmpty ( stockOrder . symbol ) ) { erroredOut ( new Error ( 'Please enter order' ) ) ; return ; } self . stockOrders . push ( stockOrder ) ; self . ib . reqIds ( ++ self . orderIdNext ) ; verbose ( 'placingOrderNow' , `Order > placeOrder -> tickerId=${ self . tickerId } symbol=${ stockOrder . symbol }` ) ; } async function run ( ) { const canProceed = await checkPending ( ) ; if ( canProceed === true ) { if ( self . processing ) { return log ( `Broker is already processing an order for ${ self . tickerId }` , symbol ) ; } self . processing = true ; ib . on ( EventName . nextValidId , handleOrderIdNext ) ; return placingOrderNow ( ) ; } } return run ( ) ; } ; param orderId cancelOrder = async ( orderId ) : Promise < boolean > => { const self = this ; const ib = self . ib ; return new Promise ( ( res ) => { const handleResults = ( r ) => { if ( r ) { self . openedOrders = compact ( ( self . openedOrders || [ ] ) . filter ( ( i ) => i . orderId !== orderId ) ) ; } res ( r ) ; handleError ( ) ; } ; const handleError = handleEventfulError ( undefined , [ `OrderId ${ orderId } that needs to be cancelled is not found` ] , ( ) => handleResults ( false ) ) ; ib . cancelOrder ( orderId ) ; setTimeout ( ( ) => handleResults ( true ) , 2000 ) ; } ) ; } ; } export default Orders ;