Skip to content

Commit

Permalink
cancelOrder bug fix
Browse files Browse the repository at this point in the history
  • Loading branch information
rylorin committed Nov 16, 2024
1 parent bac20cf commit 3fefe16
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 40 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

`@stoqey/ib` is an [Interactive Brokers](http://interactivebrokers.com/) TWS (or IB Gateway) Typescript API client library for [Node.js](http://nodejs.org/). It is a port of Interactive Brokers' Java Client Version 10.32.01 ("latest" relased on Oct 9, 2024).

Refer to the [Trader Workstation API](https://interactivebrokers.github.io/tws-api/) for the official documentation and the C#/Java/VB/C++/Python client.
Refer to [IBKRCampus](https://ibkrcampus.com/campus/ibkr-api-page/twsapi-doc/) for the official documentation and the C#/Java/VB/C++/Python client.

The module makes a socket connection to TWS (or IB Gateway) using the [net](http://nodejs.org/api/net.html) module and all messages are entirely processed in Typescript. It uses [EventEmitter](http://nodejs.org/api/events.html) to pass the result back to user.

Expand Down
10 changes: 3 additions & 7 deletions src/api-next/api-next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2890,11 +2890,7 @@ export class IBApiNext {
* @param orderId Specify which order should be cancelled by its identifier.
* @param orderCancel Specify the time the order should be cancelled. An empty string will cancel the order immediately.
*/
cancelOrder(orderId: number, orderCancelParam?: string | OrderCancel): void {
let orderCancel: OrderCancel;
if (typeof orderCancelParam == "string")
orderCancel = { manualOrderCancelTime: orderCancelParam };
else orderCancel = orderCancelParam;
cancelOrder(orderId: number, orderCancel?: string | OrderCancel): void {
this.api.cancelOrder(orderId, orderCancel);
}

Expand All @@ -2904,8 +2900,8 @@ export class IBApiNext {
*
* @see [[cancelOrder]]
*/
cancelAllOrders(): void {
this.api.reqGlobalCancel();
cancelAllOrders(orderCancel?: OrderCancel): void {
this.api.reqGlobalCancel(orderCancel);
}

/**
Expand Down
28 changes: 25 additions & 3 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,21 @@ export class IBApi extends EventEmitter {
*
* @see [[placeOrder]], [[reqGlobalCancel]]
*/
cancelOrder(orderId: number, orderCancel?: OrderCancel): IBApi {
cancelOrder(orderId: number, orderCancelParam?: string | OrderCancel): IBApi {
let orderCancel: OrderCancel;
if (orderCancelParam == undefined)
orderCancel = {
manualOrderCancelTime: "",
extOperator: "",
manualOrderIndicator: 0,
};
else if (typeof orderCancelParam == "string")
orderCancel = {
manualOrderCancelTime: orderCancelParam,
extOperator: "",
manualOrderIndicator: 0,
};
else orderCancel = orderCancelParam;
this.controller.schedule(() =>
this.controller.encoder.cancelOrder(orderId, orderCancel),
);
Expand Down Expand Up @@ -863,8 +877,16 @@ export class IBApi extends EventEmitter {
*
* @see [[cancelOrder]]
*/
reqGlobalCancel(): IBApi {
this.controller.schedule(() => this.controller.encoder.reqGlobalCancel());
reqGlobalCancel(orderCancel?: OrderCancel): IBApi {
this.controller.schedule(() =>
this.controller.encoder.reqGlobalCancel(
orderCancel || {
manualOrderCancelTime: "",
extOperator: "",
manualOrderIndicator: 0,
},
),
);
return this;
}

Expand Down
4 changes: 2 additions & 2 deletions src/api/order/orderCancel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
*/
export interface OrderCancel {
manualOrderCancelTime: string;
extOperator?: string;
manualOrderIndicator?: number;
extOperator: string;
manualOrderIndicator: number;
}
2 changes: 1 addition & 1 deletion src/core/io/decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ export class Decoder {
* Read a token from queue and return it as boolean value.
*/
readBool(): boolean {
return parseInt(this.readStr(), 10) != 0;
return !!parseInt(this.readStr());
}

/**
Expand Down
10 changes: 5 additions & 5 deletions src/core/io/encoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,7 +532,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] {
/**
* Encode a CANCEL_ORDER message to an array of tokens.
*/
cancelOrder(orderId: number, orderCancel?: OrderCancel): void {
cancelOrder(orderId: number, orderCancel: OrderCancel): void {
if (
this.serverVersion < MIN_SERVER_VER.MANUAL_ORDER_TIME &&
orderCancel?.manualOrderCancelTime.length
Expand Down Expand Up @@ -568,7 +568,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] {
tokens.push(orderId);

if (this.serverVersion >= MIN_SERVER_VER.MANUAL_ORDER_TIME)
tokens.push(orderCancel?.manualOrderCancelTime);
tokens.push(orderCancel.manualOrderCancelTime);

if (
this.serverVersion >= MIN_SERVER_VER.RFQ_FIELDS &&
Expand Down Expand Up @@ -2219,7 +2219,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] {
/**
* Encode a REQ_GLOBAL_CANCEL message.
*/
reqGlobalCancel(orderCancel?: OrderCancel): void {
reqGlobalCancel(orderCancel: OrderCancel): void {
if (this.serverVersion < MIN_SERVER_VER.REQ_GLOBAL_CANCEL) {
return this.emitError(
"It does not support globalCancel requests.",
Expand Down Expand Up @@ -2251,8 +2251,8 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] {
}

if (this.serverVersion >= MIN_SERVER_VER.CME_TAGGING_FIELDS) {
tokens.push(orderCancel?.extOperator);
tokens.push(orderCancel?.manualOrderIndicator);
tokens.push(orderCancel.extOperator);
tokens.push(orderCancel.manualOrderIndicator);
}

this.sendMsg(tokens);
Expand Down
172 changes: 151 additions & 21 deletions src/tests/unit/api/order/cancelOrder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ describe("CancelOrder", () => {
let ib: IBApi;
let clientId = Math.floor(Math.random() * 32766) + 1; // ensure unique client

const contract: Contract = sample_etf;
const order: Order = {
orderType: OrderType.LMT,
action: OrderAction.BUY,
lmtPrice: 3,
totalQuantity: 1,
tif: TimeInForce.DAY,
outsideRth: true,
transmit: true,
goodAfterTime: "20300101-01:01:01",
};

beforeEach(() => {
ib = new IBApi({
host: configuration.ib_host,
Expand All @@ -42,18 +54,8 @@ describe("CancelOrder", () => {
test("cancelOrder", (done) => {
let refId: number;

const contract: Contract = sample_etf;
const order: Order = {
orderType: OrderType.LMT,
action: OrderAction.BUY,
lmtPrice: 3,
totalQuantity: 3,
tif: TimeInForce.DAY,
outsideRth: false,
transmit: true,
};

let cancelling = false;
let isCancelling = false;
let isDone = false;
ib.once(EventName.nextValidId, (orderId: number) => {
refId = orderId;
ib.placeOrder(refId, contract, order);
Expand All @@ -74,14 +76,26 @@ describe("CancelOrder", () => {
_mktCapPrice,
) => {
if (orderId === refId) {
if (
!cancelling &&
[OrderStatus.PreSubmitted, OrderStatus.Submitted].includes(
status as OrderStatus,
)
) {
cancelling = true;
ib.cancelOrder(refId);
// console.log(orderId, status);
if (isDone) {
// ignore any message
} else if (!isCancelling) {
// [OrderStatus.PreSubmitted, OrderStatus.Submitted].includes(
// status as OrderStatus,
// )
isCancelling = true;
ib.cancelOrder(orderId);
} else {
if (
[
OrderStatus.PendingCancel,
OrderStatus.ApiCancelled,
OrderStatus.Cancelled,
].includes(status as OrderStatus)
) {
isDone = true;
done();
}
}
}
},
Expand All @@ -103,11 +117,127 @@ describe("CancelOrder", () => {
} else if (
code == ErrorCode.ORDER_CANCELLED &&
reqId == refId &&
cancelling
isCancelling
) {
// isDone = true;
logger.info(msg);
// done();
} else {
isDone = true;
done(msg);
}
}
},
);

ib.connect().reqOpenOrders();
});

test("cancelOrder immediate", (done) => {
let refId: number;

let isCancelling = false;
let isDone = false;
ib.once(EventName.nextValidId, (orderId: number) => {
refId = orderId;
ib.placeOrder(refId, contract, order);
})
.on(EventName.orderStatus, (orderId) => {
if (orderId === refId) {
if (isDone) {
// ignore any message
} else if (!isCancelling) {
isCancelling = true;
ib.cancelOrder(orderId, "");
}
}
})
.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 &&
isCancelling
) {
isDone = true;
logger.info(msg);
done();
} else {
isDone = true;
done(msg);
}
}
},
);

ib.connect().reqOpenOrders();
});

test("cancelOrder later", (done) => {
// NOTE: this test is not correctly written, but the API doesn't behave as expected neither
let refId: number;

let isCancelling = false;
let isDone = false;
ib.once(EventName.nextValidId, (orderId: number) => {
refId = orderId;
ib.placeOrder(refId, contract, order);
})
.on(EventName.orderStatus, (orderId, status) => {
if (orderId === refId) {
if (isDone) {
// ignore any message
} else if (!isCancelling) {
isCancelling = true;
ib.cancelOrder(orderId, "20260101-23:59:59");
} else {
if (
[
OrderStatus.PendingCancel,
OrderStatus.ApiCancelled,
OrderStatus.Cancelled,
].includes(status as OrderStatus)
) {
isDone = true;
done();
}
}
}
})
.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 &&
isCancelling
) {
logger.info(msg);
} else {
isDone = true;
done(msg);
}
}
Expand Down

0 comments on commit 3fefe16

Please sign in to comment.