UNPKG

@stoqey/ib

Version:

Interactive Brokers TWS/IB Gateway API client library for Node.js (TS)

1,130 lines 97.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.IBApiNext = void 0; const rxjs_1 = require("rxjs"); const operators_1 = require("rxjs/operators"); const __1 = require("../"); const errorCode_1 = require("../common/errorCode"); const mutable_account_summary_1 = require("../core/api-next/api/account/mutable-account-summary"); const mutable_market_data_1 = require("../core/api-next/api/market/mutable-market-data"); const mutable_account_positions_update_1 = require("../core/api-next/api/position/mutable-account-positions-update"); const auto_connection_1 = require("../core/api-next/auto-connection"); const console_logger_1 = require("../core/api-next/console-logger"); const logger_1 = require("../core/api-next/logger"); const subscription_registry_1 = require("../core/api-next/subscription-registry"); const _1 = require("./"); /** * @internal * * Log tag used on messages created by IBApiNext. */ const LOG_TAG = "IBApiNext"; /** * @internal * * Log tag used on messages that have been received from TWS / IB Gateway. */ const TWS_LOG_TAG = "TWS"; function filterMap(map, // eslint-disable-line @typescript-eslint/no-explicit-any pred) { const result = new Map(); for (const [k, v] of map) { if (pred(k, v)) { result.set(k, v); } } return result; } /** * Next-gen Typescript implementation of the Interactive Brokers TWS (or IB Gateway) API. * * If you prefer to stay as close as possible to the official TWS API interfaces and functionality, * use [[IBApi]]. * * If you prefer to use an API that provides some more convenience functions, such as auto-reconnect * or RxJS Observables that stay functional during re-connect, use [[IBApiNext]]. * * [[IBApiNext]] does return RxJS Observables on most of the functions. * The first subscriber will send the request to TWS, while the last un-subscriber will cancel it. * Any subscriber in between will get a replay of the latest received value(s). * This is also the case if you call same function with same arguments multiple times ([[IBApiNext]] * will make sure that a similar subscription is not requested multiple times from TWS, but it will * become a new observers to the existing subscription). * In case of an error, a re-subscribe will send the TWS request again (it is fully compatible to RxJS * operators, e.g. retry or retryWhen). * * Note that connection errors are not reported to the returned Observables as returned by get-functions, * but they will simply stop emitting values until TWS connection is re-established. * Use [[IBApiNext.connectState]] for observing the connection state. */ class IBApiNext { /** * Create an [[IBApiNext]] object. * * @param options Creation options. */ constructor(options) { this._nextReqId = 1; /** * The IBApi error [[Subject]]. * * All errors from [[IBApi]] error events will be sent to this subject. */ this.errorSubject = new rxjs_1.Subject(); /** currentTime event handler. */ this.onCurrentTime = (subscriptions, time) => { subscriptions.forEach((sub) => { sub.next({ all: time }); sub.complete(); }); }; /** managedAccounts event handler. */ this.onManagedAccts = (subscriptions, accountsList) => { const accounts = accountsList.split(","); subscriptions.forEach((sub) => { sub.next({ all: accounts }); sub.complete(); }); }; /** accountSummary event handler */ this.onAccountSummary = (subscriptions, reqId, account, tag, value, currency) => { // get the subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // update latest value on cache const cached = subscription.lastAllValue ?? new mutable_account_summary_1.MutableAccountSummaries(); const lastValue = cached .getOrAdd(account, () => new mutable_account_summary_1.MutableAccountSummaryTagValues()) .getOrAdd(tag, () => new mutable_account_summary_1.MutableAccountSummaryValues()); const hasChanged = lastValue.has(currency); const updatedValue = { value: value, ingressTm: Date.now(), }; lastValue.set(currency, updatedValue); // sent change to subscribers const accountSummaryUpdate = new mutable_account_summary_1.MutableAccountSummaries([ [ account, new mutable_account_summary_1.MutableAccountSummaryTagValues([ [tag, new mutable_account_summary_1.MutableAccountSummaryValues([[currency, updatedValue]])], ]), ], ]); if (!subscription.endEventReceived) { subscription.lastAllValue = cached; } else if (hasChanged) { subscription.next({ all: cached, changed: accountSummaryUpdate, }); } else { subscription.next({ all: cached, added: accountSummaryUpdate, }); } }; /** accountSummaryEnd event handler */ this.onAccountSummaryEnd = (subscriptions, reqId) => { // get the subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // get latest value on cache const cached = subscription.lastAllValue ?? new mutable_account_summary_1.MutableAccountSummaries(); // sent data to subscribers subscription.endEventReceived = true; subscription.next({ all: cached }); }; /** * Response to API updateAccountValue control message. * * @param subscriptions listeners * @param account The IBKR account Id. * @param tag the tag of the value. * @param value numetical value associated to the tag. * @param currency the currency of the value. * * @see [[reqAccountUpdates]] * * @todo Filter subscriptions notifications in callbacks using instanceId to finish this implementation */ this.onUpdateAccountValue = (subscriptions, tag, value, currency, account) => { filterMap(subscriptions, (_k, v) => v.instanceId === "getAccountUpdates" || v.instanceId === `getAccountUpdates+${account}`).forEach((subscription) => { // update latest value on cache const all = subscription.lastAllValue ?? {}; const cached = all?.value ?? new mutable_account_summary_1.MutableAccountSummaries(); const lastValue = cached .getOrAdd(account, () => new mutable_account_summary_1.MutableAccountSummaryTagValues()) .getOrAdd(tag, () => new mutable_account_summary_1.MutableAccountSummaryValues()); const hasChanged = lastValue.has(currency); const updatedValue = { value: value, ingressTm: Date.now(), }; lastValue.set(currency, updatedValue); // sent change to subscribers const accountSummaryUpdate = new mutable_account_summary_1.MutableAccountSummaries([ [ account, new mutable_account_summary_1.MutableAccountSummaryTagValues([ [tag, new mutable_account_summary_1.MutableAccountSummaryValues([[currency, updatedValue]])], ]), ], ]); all.value = cached; if (hasChanged) { subscription.next({ all: all, changed: { value: accountSummaryUpdate }, }); } else { subscription.next({ all: all, changed: { value: accountSummaryUpdate }, }); } }); }; /** * Response to API updatePortfolio control message. * * @param subscriptions listeners * @param contract The position's [[Contract]] * @param pos The number of units held. * @param marketPrice the market price of the contract. * @param marketValue the market value of the position. * @param avgCost The average cost of the position. * @param unrealizedPNL The unrealized PNL of the position. * @param realizedPNL The realized PNL of the position. * @param account The IBKR account Id. * * @see [[reqAccountUpdates]] * * @todo Filter subscriptions notifications in callbacks using instanceId to finish this implementation */ this.onUpdatePortfolio = (subscriptions, contract, pos, marketPrice, marketValue, avgCost, unrealizedPNL, realizedPNL, account) => { const updatedPosition = { account, contract, pos, avgCost, marketPrice, marketValue, unrealizedPNL, realizedPNL, }; // notify all subscribers filterMap(subscriptions, (_k, v) => v.instanceId === "getAccountUpdates" || v.instanceId === `getAccountUpdates+${account}`).forEach((subscription) => { // update latest value on cache let hasAdded = false; let hasRemoved = false; const all = subscription.lastAllValue ?? {}; const cached = all?.portfolio ?? new mutable_account_positions_update_1.MutableAccountPositions(); const accountPositions = cached.getOrAdd(account, () => []); const changePositionIndex = accountPositions.findIndex((p) => p.contract.conId == contract.conId); if (changePositionIndex === -1) { // new position - add it accountPositions.push(updatedPosition); hasAdded = true; } else { if (!pos) { // zero size - remove it accountPositions.splice(changePositionIndex); hasRemoved = true; } else { // update accountPositions[changePositionIndex] = updatedPosition; } } all.portfolio = cached; if (hasAdded) { subscription.next({ all: all, added: { portfolio: new mutable_account_positions_update_1.MutableAccountPositions([ [account, [updatedPosition]], ]), }, }); } else if (hasRemoved) { subscription.next({ all: all, removed: { portfolio: new mutable_account_positions_update_1.MutableAccountPositions([ [account, [updatedPosition]], ]), }, }); } else { subscription.next({ all: all, changed: { portfolio: new mutable_account_positions_update_1.MutableAccountPositions([ [account, [updatedPosition]], ]), }, }); } }); }; /** * Response to API updateAccountTime control message. * * @param subscriptions listeners * @param timeStamp the current timestamp * * @see [[reqAccountUpdates]] */ this.onUpdateAccountTime = (subscriptions, timeStamp) => { subscriptions.forEach((sub) => { const changed = { timestamp: timeStamp }; const all = sub.lastAllValue ?? {}; all.timestamp = changed.timestamp; sub.next({ all: all, changed: changed, }); }); }; /** * Response to API accountDownloadEnd control message. * * @param subscriptions listeners * @param accountName the account name * * @see [[reqAccountUpdates]] * * @todo Filter subscriptions notifications in callbacks using instanceId to finish this implementation */ this.onAccountDownloadEnd = (subscriptions, accountName) => { // notify all subscribers filterMap(subscriptions, (_k, v) => v.instanceId === "getAccountUpdates" || v.instanceId === `getAccountUpdates+${accountName}`).forEach((subscription) => { const all = subscription.lastAllValue ?? {}; subscription.endEventReceived = true; subscription.next({ all }); }); }; /** position event handler */ this.onPosition = (subscriptions, account, contract, pos, avgCost) => { const updatedPosition = { account, contract, pos, avgCost }; // notify all subscribers subscriptions.forEach((subscription) => { // update latest value on cache let hasAdded = false; let hasRemoved = false; const cached = subscription.lastAllValue ?? new mutable_account_positions_update_1.MutableAccountPositions(); const accountPositions = cached.getOrAdd(account, () => []); const changePositionIndex = accountPositions.findIndex((p) => p.contract.conId == contract.conId); if (changePositionIndex === -1) { // new position - add it accountPositions.push(updatedPosition); hasAdded = true; } else { if (!pos) { // zero size - remove it accountPositions.splice(changePositionIndex); hasRemoved = true; } else { // update accountPositions[changePositionIndex] = updatedPosition; } } if (!subscription.endEventReceived) { subscription.lastAllValue = cached; } else if (hasAdded) { subscription.next({ all: cached, added: new mutable_account_positions_update_1.MutableAccountPositions([[account, [updatedPosition]]]), }); } else if (hasRemoved) { subscription.next({ all: cached, removed: new mutable_account_positions_update_1.MutableAccountPositions([[account, [updatedPosition]]]), }); } else { subscription.next({ all: cached, changed: new mutable_account_positions_update_1.MutableAccountPositions([[account, [updatedPosition]]]), }); } }); }; /** position end enumeration event handler */ this.onPositionEnd = (subscriptions) => { // notify all subscribers subscriptions.forEach((subscription) => { const lastAllValue = subscription.lastAllValue ?? new mutable_account_positions_update_1.MutableAccountPositions(); subscription.endEventReceived = true; subscription.next({ all: lastAllValue }); }); }; /** contractDetails event handler */ this.onContractDetails = (subscriptions, reqId, details) => { // get the subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // append to list const cached = subscription.lastAllValue ?? []; cached.push(details); // sent change to subscribers subscription.next({ all: cached, }); }; /** contractDetailsEnd event handler */ this.onContractDetailsEnd = (subscriptions, reqId) => { subscriptions.get(reqId)?.complete(); }; /** securityDefinitionOptionParameter event handler */ this.onSecurityDefinitionOptionParameter = (subscriptions, reqId, exchange, underlyingConId, tradingClass, multiplier, expirations, strikes) => { // get the subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // append to list const cached = subscription.lastAllValue ?? []; cached.push({ exchange: exchange, underlyingConId: underlyingConId, tradingClass: tradingClass, multiplier: parseInt(multiplier), expirations: expirations, strikes: strikes, }); // sent change to subscribers subscription.next({ all: cached, }); }; /** securityDefinitionOptionParameterEnd event handler */ this.onSecurityDefinitionOptionParameterEnd = (subscriptions, reqId) => { subscriptions.get(reqId)?.complete(); }; /** pnl event handler. */ this.onPnL = (subscriptions, reqId, dailyPnL, unrealizedPnL, realizedPnL) => { // get subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // sent change to subscribers subscription.next({ all: { dailyPnL, unrealizedPnL, realizedPnL }, }); }; /** pnlSingle event handler. */ this.onPnLSingle = (subscriptions, reqId, pos, dailyPnL, unrealizedPnL, realizedPnL, value) => { // get subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // sent change to subscribers subscription.next({ all: { position: pos, dailyPnL: dailyPnL, unrealizedPnL: unrealizedPnL, realizedPnL: realizedPnL, marketValue: value, }, }); }; /** tickPrice, tickSize and tickGeneric event handler */ this.onTick = (subscriptions, reqId, tickType, value) => { // convert -1 on Bid/Ask to undefined if (value === -1 && (tickType === _1.IBApiTickType.BID || tickType === _1.IBApiTickType.DELAYED_BID || tickType === _1.IBApiTickType.ASK || tickType === _1.IBApiTickType.DELAYED_ASK)) { value = undefined; } // get subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // update latest value on cache const cached = subscription.lastAllValue ?? new mutable_market_data_1.MutableMarketData(); const hasChanged = cached.has(tickType); const updatedValue = { value, ingressTm: Date.now(), }; cached.set(tickType, updatedValue); // deliver to subject if (hasChanged) { subscription.next({ all: cached, changed: new mutable_market_data_1.MutableMarketData([[tickType, updatedValue]]), }); } else { subscription.next({ all: cached, added: new mutable_market_data_1.MutableMarketData([[tickType, updatedValue]]), }); } }; /** tickOptionComputationHandler event handler */ this.onTickOptionComputation = (subscriptions, reqId, field, impliedVolatility, delta, optPrice, pvDividend, gamma, vega, theta, undPrice) => { // get subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // generate [[IBApiNext]] market data ticks const now = Date.now(); const ticks = [ [ _1.IBApiNextTickType.OPTION_UNDERLYING, { value: undPrice, ingressTm: now }, ], [ _1.IBApiNextTickType.OPTION_PV_DIVIDEND, { value: pvDividend, ingressTm: now }, ], ]; switch (field) { case _1.IBApiTickType.BID_OPTION: ticks.push([ _1.IBApiNextTickType.BID_OPTION_IV, { value: impliedVolatility, ingressTm: now }, ], [ _1.IBApiNextTickType.BID_OPTION_DELTA, { value: delta, ingressTm: now }, ], [ _1.IBApiNextTickType.BID_OPTION_PRICE, { value: optPrice, ingressTm: now }, ], [ _1.IBApiNextTickType.BID_OPTION_GAMMA, { value: gamma, ingressTm: now }, ], [_1.IBApiNextTickType.BID_OPTION_VEGA, { value: vega, ingressTm: now }], [ _1.IBApiNextTickType.BID_OPTION_THETA, { value: theta, ingressTm: now }, ]); break; case _1.IBApiTickType.DELAYED_BID_OPTION: ticks.push([ _1.IBApiNextTickType.DELAYED_BID_OPTION_IV, { value: impliedVolatility, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_BID_OPTION_DELTA, { value: delta, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_BID_OPTION_PRICE, { value: optPrice, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_BID_OPTION_GAMMA, { value: gamma, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_BID_OPTION_VEGA, { value: vega, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_BID_OPTION_THETA, { value: theta, ingressTm: now }, ]); break; case _1.IBApiTickType.ASK_OPTION: ticks.push([ _1.IBApiNextTickType.ASK_OPTION_IV, { value: impliedVolatility, ingressTm: now }, ], [ _1.IBApiNextTickType.ASK_OPTION_DELTA, { value: delta, ingressTm: now }, ], [ _1.IBApiNextTickType.ASK_OPTION_PRICE, { value: optPrice, ingressTm: now }, ], [ _1.IBApiNextTickType.ASK_OPTION_GAMMA, { value: gamma, ingressTm: now }, ], [_1.IBApiNextTickType.ASK_OPTION_VEGA, { value: vega, ingressTm: now }], [ _1.IBApiNextTickType.ASK_OPTION_THETA, { value: theta, ingressTm: now }, ]); break; case _1.IBApiTickType.DELAYED_ASK_OPTION: ticks.push([ _1.IBApiNextTickType.DELAYED_ASK_OPTION_IV, { value: impliedVolatility, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_ASK_OPTION_DELTA, { value: delta, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_ASK_OPTION_PRICE, { value: optPrice, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_ASK_OPTION_GAMMA, { value: gamma, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_ASK_OPTION_VEGA, { value: vega, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_ASK_OPTION_THETA, { value: theta, ingressTm: now }, ]); break; case _1.IBApiTickType.LAST_OPTION: ticks.push([ _1.IBApiNextTickType.LAST_OPTION_IV, { value: impliedVolatility, ingressTm: now }, ], [ _1.IBApiNextTickType.LAST_OPTION_DELTA, { value: delta, ingressTm: now }, ], [ _1.IBApiNextTickType.LAST_OPTION_PRICE, { value: optPrice, ingressTm: now }, ], [ _1.IBApiNextTickType.LAST_OPTION_GAMMA, { value: gamma, ingressTm: now }, ], [_1.IBApiNextTickType.LAST_OPTION_VEGA, { value: vega, ingressTm: now }], [ _1.IBApiNextTickType.LAST_OPTION_THETA, { value: theta, ingressTm: now }, ]); break; case _1.IBApiTickType.DELAYED_LAST_OPTION: ticks.push([ _1.IBApiNextTickType.DELAYED_LAST_OPTION_IV, { value: impliedVolatility, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_LAST_OPTION_DELTA, { value: delta, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_LAST_OPTION_PRICE, { value: optPrice, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_LAST_OPTION_GAMMA, { value: gamma, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_LAST_OPTION_VEGA, { value: vega, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_LAST_OPTION_THETA, { value: theta, ingressTm: now }, ]); break; case _1.IBApiTickType.MODEL_OPTION: ticks.push([ _1.IBApiNextTickType.MODEL_OPTION_IV, { value: impliedVolatility, ingressTm: now }, ], [ _1.IBApiNextTickType.MODEL_OPTION_DELTA, { value: delta, ingressTm: now }, ], [ _1.IBApiNextTickType.MODEL_OPTION_PRICE, { value: optPrice, ingressTm: now }, ], [ _1.IBApiNextTickType.MODEL_OPTION_GAMMA, { value: gamma, ingressTm: now }, ], [ _1.IBApiNextTickType.MODEL_OPTION_VEGA, { value: vega, ingressTm: now }, ], [ _1.IBApiNextTickType.MODEL_OPTION_THETA, { value: theta, ingressTm: now }, ]); break; case _1.IBApiTickType.DELAYED_MODEL_OPTION: ticks.push([ _1.IBApiNextTickType.DELAYED_MODEL_OPTION_IV, { value: impliedVolatility, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_MODEL_OPTION_DELTA, { value: delta, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_MODEL_OPTION_PRICE, { value: optPrice, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_MODEL_OPTION_GAMMA, { value: gamma, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_MODEL_OPTION_VEGA, { value: vega, ingressTm: now }, ], [ _1.IBApiNextTickType.DELAYED_MODEL_OPTION_THETA, { value: theta, ingressTm: now }, ]); break; } // update latest value on cache const cached = subscription.lastAllValue ?? new mutable_market_data_1.MutableMarketData(); const added = new mutable_market_data_1.MutableMarketData(); const changed = new mutable_market_data_1.MutableMarketData(); ticks.forEach((tick) => { if (cached.has(tick[0])) { changed.set(tick[0], tick[1]); } else { added.set(tick[0], tick[1]); } cached.set(tick[0], tick[1]); }); // deliver to subject if (cached.size) { subscription.next({ all: cached, added: added.size ? added : undefined, changed: changed.size ? changed : undefined, }); } }; /** tickSnapshotEnd event handler */ this.onTickSnapshotEnd = (subscriptions, reqId) => { subscriptions.get(reqId)?.complete(); }; /** * @deprecated please use getMarketDataSnapshot instead of getMarketDataSingle. */ this.getMarketDataSingle = this.getMarketDataSnapshot; /** headTimestamp event handler. */ this.onHeadTimestamp = (subscriptions, reqId, headTimestamp) => { // get subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // signal timestamp subscription.next({ all: headTimestamp }); subscription.complete(); }; /** historicalData event handler */ this.onHistoricalData = (subscriptions, reqId, time, open, high, low, close, volume, count, WAP) => { // get subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // append bar or signal completion if (time.startsWith("finished")) { subscription.complete(); } else { const all = subscription.lastAllValue ?? []; const current = { time }; if (open !== -1) { current.open = open; } if (high !== -1) { current.high = high; } if (low !== -1) { current.low = low; } if (close !== -1) { current.close = close; } if (volume !== -1) { current.volume = volume; } if (count !== -1) { current.count = count; } if (WAP !== -1) { current.WAP = WAP; } all.push(current); subscription.next({ all, }); } }; /** historicalDataUpdate event handler */ this.onHistoricalDataUpdate = (subscriptions, reqId, time, open, high, low, close, volume, count, WAP) => { // get subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // update bar const current = subscription.lastAllValue ?? {}; current.time = time; current.open = open !== -1 ? open : undefined; current.high = high !== -1 ? high : undefined; current.low = low !== -1 ? low : undefined; current.close = close !== -1 ? close : undefined; current.volume = volume !== -1 ? volume : undefined; current.count = count !== -1 ? count : undefined; current.WAP = WAP !== -1 ? WAP : undefined; subscription.next({ all: current, }); }; /** historicalTicks event handler */ this.onHistoricalTicks = (subscriptions, reqId, ticks, done) => { // get subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // append tick let allTicks = subscription.lastAllValue; allTicks = allTicks ? allTicks.concat(ticks) : ticks; subscription.next({ all: allTicks, }); if (done) { subscription.complete(); } }; /** historicalTicksBidAsk event handler */ this.onHistoricalTicksBidAsk = (subscriptions, reqId, ticks, done) => { // get subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // append tick let allTicks = subscription.lastAllValue; allTicks = allTicks ? allTicks.concat(ticks) : ticks; subscription.next({ all: allTicks, }); if (done) { subscription.complete(); } }; /** historicalTicksLast event handler */ this.onHistoricalTicksLast = (subscriptions, reqId, ticks, done) => { // get subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // append tick let allTicks = subscription.lastAllValue; allTicks = allTicks ? allTicks.concat(ticks) : ticks; subscription.next({ all: allTicks, }); if (done) { subscription.complete(); } }; /** mktDepthExchanges event handler */ this.onMktDepthExchanges = (subscriptions, depthMktDataDescriptions) => { subscriptions.forEach((sub) => { sub.next({ all: depthMktDataDescriptions, }); sub.complete(); }); }; /** updateMktDepth event handler */ this.onUpdateMktDepth = (subscriptions, reqId, position, operation, side, price, size) => { // forward to L2 handler, but w/o market maker and smart depth set to false this.onUpdateMktDepthL2(subscriptions, reqId, position, undefined, operation, side, price, size, false); }; /** marketDepthL2 event handler */ this.onUpdateMktDepthL2 = (subscriptions, reqId, position, marketMaker, operation, side, price, size, isSmartDepth) => { // get subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } // update cached const cached = subscription.lastAllValue ?? { bids: new Map(), asks: new Map(), }; const changed = { bids: new Map(), asks: new Map(), }; let cachedRows = undefined; let changedRows = undefined; if (side == 0) { // ask side cachedRows = cached.asks; // eslint-disable-line @typescript-eslint/consistent-type-assertions changedRows = changed.asks; // eslint-disable-line @typescript-eslint/consistent-type-assertions } else if (side == 1) { // bid side cachedRows = cached.bids; // eslint-disable-line @typescript-eslint/consistent-type-assertions changedRows = changed.bids; // eslint-disable-line @typescript-eslint/consistent-type-assertions } if (cachedRows === undefined || changedRows === undefined) { this.logger.error(LOG_TAG, `onUpdateMktDepthL2: unknown side value ${side} received from TWS`); return; } switch (operation) { case 0: // it's an insert this.insertAtMapIndex(position, position, { marketMaker: marketMaker, price: price, size: size, isSmartDepth: isSmartDepth, }, cachedRows); this.insertAtMapIndex(position, position, { marketMaker: marketMaker, price: price, size: size, isSmartDepth: isSmartDepth, }, changedRows); subscription.next({ all: cached, added: changed, }); break; case 1: // it's an update cachedRows.set(position, { marketMaker: marketMaker, price: price, size: size, isSmartDepth: isSmartDepth, }); changedRows.set(position, { marketMaker: marketMaker, price: price, size: size, isSmartDepth: isSmartDepth, }); subscription.next({ all: cached, changed: changed, }); break; case 2: // it's a delete { const deletedRow = cachedRows.get(position); cachedRows.delete(position); changedRows.set(position, deletedRow); subscription.next({ all: cached, removed: changed, }); } break; default: this.logger.error(LOG_TAG, `onUpdateMktDepthL2: unknown operation value ${operation} received from TWS`); break; } }; this.onScannerParameters = (subscriptions, xml) => { subscriptions.forEach((sub) => { sub.next({ all: xml }); sub.complete(); }); }; /** * Provides the data resulting from the market scanner request. * @param subscriptions * @param reqId the request's identifier * @param rank the ranking within the response of this bar. * @param contract the data's ContractDetails * @param distance according to query * @param benchmark according to query * @param projection according to query * @param legStr describes the combo legs when the scanner is returning EFP * @returns void */ this.onScannerData = (subscriptions, reqId, rank, contract, distance, benchmark, projection, legStr) => { // get subscription const subscription = subscriptions.get(reqId); if (!subscription) { return; } const item = { rank, contract, distance, benchmark, projection, legStr, }; const lastAllValue = subscription.lastAllValue ?? new Map(); const existing = lastAllValue.get(rank) != undefined; lastAllValue.set(rank, item); if (subscription.endEventReceived) { const updated = new Map(); updated.set(rank, item); subscription.next({ all: lastAllValue, changed: existing ? updated : undefined, added: existing ? undefined : updated, }); } else { subscription.lastAllValue = lastAllValue; } }; /** * Indicates the scanner data reception has terminated. * @param subscriptions * @param reqId the request's identifier * @returns */ this.onScannerDataEnd = (subscriptions, reqId) => { const subscription = subscriptions.get(reqId); if (!subscription) { return; } const lastAllValue = subscription.lastAllValue ?? new Map(); const updated = { all: lastAllValue, }; subscription.endEventReceived = true; subscription.next(updated); }; /** histogramData event handler */ this.onHistogramData = (subscriptions, reqId, data) => { // get the subscription const sub = subscriptions.get(reqId); if (!sub) { return; } // deliver data sub.next({ all: data }); sub.complete(); }; /** * Feeds in currently open orders. * * @param subscriptions listeners * @param orderId The order's unique id. * @param contract The order's [[Contract]] * @param order The currently active [[Order]] * @param orderState The order's [[OrderState]] * * @see [[placeOrder]], [[reqAllOpenOrders]], [[reqAutoOpenOrders]] */ this.onOpenOrder = (subscriptions, orderId, contract, order, orderState) => { subscriptions.forEach((sub) => { const allOrders = sub.lastAllValue ?? []; const changeOrderIndex = allOrders.findIndex((p) => p.order.permId == order.permId); if (changeOrderIndex === -1) { // new open order - add it const addedOrder = { orderId, contract, order, orderState, orderStatus: undefined, }; allOrders.push(addedOrder); if (sub.endEventReceived) { sub.next({ all: allOrders, added: [addedOrder], }); } else { sub.lastAllValue = allOrders; } } else { // update const updatedOrder = allOrders[changeOrderIndex]; updatedOrder.order = order; updatedOrder.orderState = orderState; if (updatedOrder.orderStatus !== undefined) { // synchronize orderStatus if exists updatedOrder.orderStatus.clientId = order.clientId; updatedOrder.orderStatus.permId = order.permId; updatedOrder.orderStatus.parentId = order.parentId; updatedOrder.orderStatus.status = orderState.status; } sub.next({ all: allOrders, changed: [updatedOrder], }); } }); }; /** * Ends the subscription once all openOrders are recieved * @param subscriptions listeners */ this.onOpenOrderComplete = (subscriptions) => { subscriptions.forEach((sub) => { const allOrders = sub.lastAllValue ?? []; sub.endEventReceived = true; sub.next({ all: allOrders }); sub.complete(); }); }; /** * Response to API bind order control message. * * @param subscriptions listeners * @param orderId permId (mistake from IB documentation, value is orderId not permId) * @param apiClientId API client id. * @param apiOrderId API order id. * * @see [[reqOpenOrders]] */ this.onOrderBound = ( // TODO finish implementation subscriptions, orderId, apiClientId, apiOrderId) => { /* * This is probably unused now. * Neither reqAllOpenOrders, reqAutoOpenOrders nor reqOpenOrders documentation reference this event. * Even getAutoOpenOrders(true) doesn't call it! */ this.logger.warn(LOG_TAG, `Unexpected onOrderBound(${orderId}, ${apiClientId}, ${apiOrderId}) called.`); }; /** * Response to API status order control message. * * @param orderId the order's client id. * @param status the current status of the order. Possible values: PendingSubmit - indicates that you have transmitted the order, but have not yet received confirmation that it has been accepted by the order destination. PendingCancel - indicates that you have sent a request to cancel the order but have not yet received cancel confirmation from the order destination. At this point, your order is not confirmed canceled. It is not guaranteed that the cancellation will be successful. PreSubmitted - indicates that a simulated order type has been accepted by the IB system and that this order has yet to be elected. The order is held in the IB system until the election criteria are met. At that time the order is transmitted to the order destination as specified . Submitted - indicates that your order has been accepted by the system. ApiCancelled - after an order has been submitted and before it has been acknowledged, an API client client can request its cancelation, producing this state. Cancelled - indicates that the balance of your order has been confirmed canceled by the IB system. This could occur unexpectedly when IB or the destination has rejected your order. Filled - indicates that the order has been completely filled. Market orders executions will not always trigger a Filled status. Inactive - indicates that the order was received by the system but is no longer active because it was rejected or canceled. * @param filled number of filled positions. * @param remaining the remnant positions. * @param avgFillPrice average filling price. * @param permId the order's permId used by the TWS to identify orders. * @param parentId parent's id. Used for bracket and auto trailing stop orders. * @param lastFillPrice price at which the last positions were filled. * @param clientId API client which submitted the order. * @param whyHeld this field is used to identify an order held when TWS is trying to locate shares for a short sell. The value used to indicate this is 'locate'. * @param mktCapPrice If an order has been capped, this indicates the current capped price. Requires TWS 967+ and API v973.04+. Python API specifically requires API v973.06+. * * @see [[reqOpenOrders]] */ this.onOrderStatus = (subscriptions, orderId, status, filled, remaining, avgFillPrice, permId, parentId, lastFillPrice, clientId, whyHeld, mktCapPrice) => { const orderStatus = { status, filled, remaining, avgFillPrice: undefined, permId, parentId, lastFillPrice: undefined, clientId, whyHeld, mktCapPrice, }; if (filled) { orderStatus.avgFillPrice = avgFillPrice; orderStatus.lastFillPrice = lastFillPrice; } subscriptions.forEach((sub) => { const allOrders = sub.lastAllValue ?? []; const changeOrderIndex = allOrders.findInd