From 91988f7563f43d5560760409fa7c0ddfa027092a Mon Sep 17 00:00:00 2001 From: Ronan-Yann Lorin Date: Wed, 20 Sep 2023 19:03:28 +0200 Subject: [PATCH] Upgrade server to v169 --- src/api/api.ts | 2 +- src/api/order/order.ts | 19 ++ src/common/errorCode.ts | 3 + src/core/io/encoder.ts | 36 ++- src/tests/unit/api/market-scanner.test.ts | 2 +- src/tests/unit/api/order/cancelOrder.test.ts | 124 ++++++++++ src/tests/unit/api/order/placeOrder.test.ts | 234 +++++++++---------- 7 files changed, 291 insertions(+), 129 deletions(-) create mode 100644 src/tests/unit/api/order/cancelOrder.test.ts diff --git a/src/api/api.ts b/src/api/api.ts index 8ed436e6..1c5d4257 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -77,7 +77,7 @@ export interface IBApiCreationOptions { } /** Maximum supported version. */ -export const MAX_SUPPORTED_SERVER_VERSION = MIN_SERVER_VER.USER_INFO; +export const MAX_SUPPORTED_SERVER_VERSION = MIN_SERVER_VER.MANUAL_ORDER_TIME; /** Minimum supported version. */ export const MIN_SERVER_VER_SUPPORTED = 38; diff --git a/src/api/order/order.ts b/src/api/order/order.ts index 38cdb65f..39afd8eb 100644 --- a/src/api/order/order.ts +++ b/src/api/order/order.ts @@ -736,7 +736,26 @@ export interface Order { /** Value must be positive, and it is number of seconds that SMART order would be parked for at IBKRATS before being routed to exchange. */ postToAts?: number; + /** Accepts a list with parameters obtained from advancedOrderRejectJson */ advancedErrorOverride?: string; + + /** Used by brokers and advisors when manually entering, modifying or cancelling orders at the direction of a client. Only used when allocating orders to specific groups or accounts. Excluding "All" group. */ + manualOrderTime?: string; + + /** Defines the minimum trade quantity to fill. For IBKRATS orders. */ + minTradeQty?: number; + + /** Defines the minimum size to compete. For IBKRATS orders. */ + minCompeteSize?: number; + + /** Dpecifies the offset Off The Midpoint that will be applied to the order. For IBKRATS orders. */ + competeAgainstBestOffset?: number; + + /** This offset is applied when the spread is an even number of cents wide. This offset must be in whole-penny increments or zero. For IBKRATS orders. */ + midOffsetAtWhole?: number; + + /** This offset is applied when the spread is an odd number of cents wide. This offset must be in half-penny increments. For IBKRATS orders. */ + midOffsetAtHalf?: number; } export default Order; diff --git a/src/common/errorCode.ts b/src/common/errorCode.ts index aaf2c0e4..cbb9c9c6 100644 --- a/src/common/errorCode.ts +++ b/src/common/errorCode.ts @@ -4,6 +4,9 @@ /* eslint-disable @typescript-eslint/no-duplicate-enum-values */ export enum ErrorCode { + /** Order Canceled - reason: */ + ORDER_CANCELLED = 202, + /** Already connected. */ ALREADY_CONNECTED = 501, diff --git a/src/core/io/encoder.ts b/src/core/io/encoder.ts index f5f51004..2afcc276 100644 --- a/src/core/io/encoder.ts +++ b/src/core/io/encoder.ts @@ -518,10 +518,26 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { /** * Encode a CANCEL_ORDER message to an array of tokens. */ - cancelOrder(id: number): void { + cancelOrder(id: number, manualCancelOrderTime?: string): void { const version = 1; - this.sendMsg(OUT_MSG_ID.CANCEL_ORDER, version, id); + if ( + this.serverVersion < MIN_SERVER_VER.MANUAL_ORDER_TIME && + manualCancelOrderTime?.length + ) { + return this.emitError( + "It does not support manual order cancel time attribute", + ErrorCode.UPDATE_TWS, + id, + ); + } + + const tokens: unknown[] = [OUT_MSG_ID.CANCEL_ORDER, version, id]; + + if (this.serverVersion >= MIN_SERVER_VER.MANUAL_ORDER_TIME) + tokens.push(manualCancelOrderTime); + + this.sendMsg(tokens); } /** @@ -1053,7 +1069,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { if ( this.serverVersion < MIN_SERVER_VER.ADVANCED_ORDER_REJECT && - order.advancedErrorOverride != null + order.advancedErrorOverride != undefined ) { return this.emitError( "It does not support advanced error override attribute", @@ -1062,6 +1078,17 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { ); } + if ( + this.serverVersion < MIN_SERVER_VER.MANUAL_ORDER_TIME && + order.manualOrderTime?.length + ) { + return this.emitError( + "It does not support manual order time attribute", + ErrorCode.UPDATE_TWS, + id, + ); + } + const version = this.serverVersion < MIN_SERVER_VER.NOT_HELD ? 27 : 45; // send place order msg @@ -1586,6 +1613,9 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] { if (this.serverVersion >= MIN_SERVER_VER.ADVANCED_ORDER_REJECT) tokens.push(order.advancedErrorOverride); + if (this.serverVersion >= MIN_SERVER_VER.MANUAL_ORDER_TIME) + tokens.push(order.manualOrderTime); + this.sendMsg(tokens); } diff --git a/src/tests/unit/api/market-scanner.test.ts b/src/tests/unit/api/market-scanner.test.ts index 1ce2dd4f..bccf466b 100644 --- a/src/tests/unit/api/market-scanner.test.ts +++ b/src/tests/unit/api/market-scanner.test.ts @@ -31,7 +31,7 @@ describe("IBApi market scanner tests", () => { test("Scanner parameters", (done) => { ib.on(EventName.scannerParameters, (xml: string) => { - const match = ''; + const match = ''; // eslint-disable-line quotes expect(xml.substring(0, match.length)).toEqual(match); ib.disconnect(); }) diff --git a/src/tests/unit/api/order/cancelOrder.test.ts b/src/tests/unit/api/order/cancelOrder.test.ts new file mode 100644 index 00000000..e9c489d5 --- /dev/null +++ b/src/tests/unit/api/order/cancelOrder.test.ts @@ -0,0 +1,124 @@ +/** + * This file implement test code for the placeOrder function . + */ +import { + Contract, + ErrorCode, + EventName, + IBApi, + Order, + OrderAction, + OrderType, + SecType, +} from "../../../.."; +// import OptionType from "../../../../api/data/enum/option-type"; +import configuration from "../../../../common/configuration"; +import logger from "../../../../common/logger"; + +const awaitTimeout = (delay: number): Promise => + new Promise((resolve): NodeJS.Timeout => setTimeout(resolve, delay * 1000)); + +describe("CancelOrder", () => { + jest.setTimeout(20 * 1000); + + let ib: IBApi; + let clientId = Math.floor(Math.random() * 32766) + 1; // ensure unique client + + beforeEach(() => { + ib = new IBApi({ + host: configuration.ib_host, + port: configuration.ib_port, + clientId: clientId++, // increment clientId for each test so they don't interfere on each other + }); + // logger.info("IBApi created"); + }); + + afterEach(() => { + if (ib) { + ib.disconnect(); + ib = undefined; + } + // logger.info("IBApi disconnected"); + }); + + test("cancelOrder", (done) => { + let refId: number; + + const contract: Contract = { + symbol: "AAPL", + exchange: "SMART", + currency: "USD", + secType: SecType.STK, + }; + const order: Order = { + orderType: OrderType.LMT, + action: OrderAction.BUY, + lmtPrice: 1, + orderId: refId, + totalQuantity: 1, + // account: "DU123567", + tif: "DAY", + transmit: true, + }; + + let order_found = false; + ib.once(EventName.nextValidId, (orderId: number) => { + refId = orderId; + ib.placeOrder(refId, contract, order); + }) + .on(EventName.openOrder, (orderId, _contract, _order, _orderState) => { + expect(orderId).toEqual(refId); + if (orderId === refId) { + order_found = true; + ib.cancelOrder(refId); + } + }) + .on( + EventName.orderStatus, + ( + orderId, + _status, + _filled, + _remaining, + _avgFillPrice, + _permId?, + _parentId?, + _lastFillPrice?, + _clientId?, + _whyHeld?, + _mktCapPrice?, + ) => { + expect(orderId).toEqual(refId); + }, + ) + .on(EventName.openOrderEnd, () => {}) + .on( + EventName.error, + ( + error: Error, + code: ErrorCode, + reqId: number, + _advancedOrderReject?: unknown, + ) => { + if (reqId === -1) { + logger.info(error.message); + } else { + const msg = `[${reqId}] ${error.message} (Error #${code})`; + if (error.message.includes("Warning:")) { + logger.warn(msg); + } else if ( + code == ErrorCode.ORDER_CANCELLED && + reqId == refId && + order_found + ) { + done(); + } else { + done(msg); + } + } + }, + ); + + ib.connect().reqOpenOrders(); + }); +}); diff --git a/src/tests/unit/api/order/placeOrder.test.ts b/src/tests/unit/api/order/placeOrder.test.ts index 7a7f3c48..7e451b90 100644 --- a/src/tests/unit/api/order/placeOrder.test.ts +++ b/src/tests/unit/api/order/placeOrder.test.ts @@ -8,6 +8,7 @@ import { EventName, IBApi, OptionType, + Order, OrderAction, OrderType, PriceCondition, @@ -21,7 +22,7 @@ import logger from "../../../../common/logger"; const awaitTimeout = (delay: number): Promise => new Promise((resolve): NodeJS.Timeout => setTimeout(resolve, delay * 1000)); -describe("PlaceOrder", () => { +describe("Place Orders", () => { jest.setTimeout(20 * 1000); let ib: IBApi; @@ -45,141 +46,126 @@ describe("PlaceOrder", () => { }); test("Simple placeOrder", (done) => { - ib.once(EventName.nextValidId, (orderId: number) => { - // buy an Apple call, with a PriceCondition on underlying - - const contract: Contract = { - symbol: "AAPL", - exchange: "SMART", - currency: "USD", - secType: SecType.STK, - }; - - ib.placeOrder(orderId, contract, { - orderType: OrderType.LMT, - action: OrderAction.BUY, - lmtPrice: 1, - orderId, - totalQuantity: 1, - // account: "DU123567", - tif: "DAY", - transmit: true, - }); + let refId: number; + + const contract: Contract = { + symbol: "AAPL", + exchange: "SMART", + currency: "USD", + secType: SecType.STK, + }; + const order: Order = { + orderType: OrderType.LMT, + action: OrderAction.BUY, + lmtPrice: 1, + orderId: refId, + totalQuantity: 1, + // account: "DU123567", + tif: "DAY", + transmit: true, + }; - // verify result - let received = false; - - ib.on(EventName.openOrder, (id, _contract, _order, _orderState) => { - if (id === orderId) { - received = true; + ib.once(EventName.nextValidId, (orderId: number) => { + refId = orderId; + ib.placeOrder(refId, contract, order); + }) + .on(EventName.openOrder, (orderId, _contract, _order, _orderState) => { + expect(orderId).toEqual(refId); + if (orderId === refId) { + done(); } - }).on(EventName.openOrderEnd, () => { - ib.disconnect(); - if (received) done(); - else done(`Order ${orderId} not placed`); - }); - - // Give a few secs delay to get order placed - awaitTimeout(15).then(() => ib.reqOpenOrders()); - }).on( - EventName.error, - ( - error: Error, - code: ErrorCode, - reqId: number, - _advancedOrderReject?: unknown, - ) => { - if (reqId === -1) { - logger.info(error.message); - } else { - const msg = `[${reqId}] ${error.message} (Error #${code})`; - if (error.message.includes("Warning:")) { - logger.warn(msg); + }) + .on( + EventName.error, + ( + error: Error, + code: ErrorCode, + reqId: number, + _advancedOrderReject?: unknown, + ) => { + if (reqId === -1) { + logger.info(error.message); } else { - ib.disconnect(); - done(msg); + const msg = `[${reqId}] ${error.message} (Error #${code})`; + if (error.message.includes("Warning:")) { + logger.warn(msg); + } else { + ib.disconnect(); + done(msg); + } } - } - }, - ); + }, + ); - ib.connect(); + ib.connect().reqOpenOrders(); }); test("placeOrder with PriceCondition", (done) => { - ib.once(EventName.nextValidId, (orderId: number) => { - // buy an Apple call, with a PriceCondition on underlying - - const contract: Contract = { - symbol: "AAPL", - exchange: "SMART", - currency: "USD", - secType: SecType.OPT, - right: OptionType.Call, - strike: 200, - multiplier: 100, - lastTradeDateOrContractMonth: "20251219", - }; - - const priceCondition: PriceCondition = new PriceCondition( - 29, - TriggerMethod.Default, - 3691937, // AMZN Stock on SMART - "SMART", - true, - ConjunctionConnection.OR, - ); - - ib.placeOrder(orderId, contract, { - orderType: OrderType.LMT, - action: OrderAction.BUY, - lmtPrice: 0.01, - orderId, - totalQuantity: 1, - // account: "DU123567", - conditionsIgnoreRth: true, - conditionsCancelOrder: false, - conditions: [priceCondition], - transmit: true, - }); - - // verify result - let received = false; + let refId: number; + + // buy an Apple call, with a PriceCondition on underlying + const contract: Contract = { + symbol: "AAPL", + exchange: "SMART", + currency: "USD", + secType: SecType.OPT, + right: OptionType.Call, + strike: 200, + multiplier: 100, + lastTradeDateOrContractMonth: "20251219", + }; + const priceCondition: PriceCondition = new PriceCondition( + 29, + TriggerMethod.Default, + 265598, + "SMART", + true, + ConjunctionConnection.OR, + ); + const order: Order = { + orderType: OrderType.LMT, + action: OrderAction.BUY, + lmtPrice: 0.01, + totalQuantity: 1, + // account: "DU123567", + conditionsIgnoreRth: true, + conditionsCancelOrder: false, + conditions: [priceCondition], + transmit: true, + }; - ib.on(EventName.openOrder, (id, _contract, _order, _orderState) => { - if (id === orderId) { - received = true; + ib.once(EventName.nextValidId, (orderId: number) => { + refId = orderId; + ib.placeOrder(refId, contract, order); + }) + .on(EventName.openOrder, (orderId, _contract, _order, _orderState) => { + expect(orderId).toEqual(refId); + if (orderId === refId) { + done(); } - }).on(EventName.openOrderEnd, () => { - ib.disconnect(); - if (received) done(); - else done(`Order ${orderId} not placed`); - }); - - // Give a few secs delay to get order placed - awaitTimeout(10).then(() => ib.reqOpenOrders()); - }).on( - EventName.error, - ( - error: Error, - code: ErrorCode, - reqId: number, - _advancedOrderReject?: unknown, - ) => { - if (reqId === -1) { - logger.info(error.message); - } else { - const msg = `[${reqId}] ${error.message} (Error #${code})`; - if (error.message.includes("Warning:")) { - logger.warn(msg); + }) + .on( + EventName.error, + ( + error: Error, + code: ErrorCode, + reqId: number, + _advancedOrderReject?: unknown, + ) => { + if (reqId === -1) { + logger.info(error.message); } else { - ib.disconnect(); - done(msg); + const msg = `[${reqId}] ${error.message} (Error #${code})`; + if (error.message.includes("Warning:")) { + logger.warn(msg); + } else { + ib.disconnect(); + done(msg); + } } - } - }, - ); + }, + ); - ib.connect(); + ib.connect().reqOpenOrders(); }); });