Skip to content

Commit

Permalink
Re-connect after TWS restart not working properly #195 issue fix
Browse files Browse the repository at this point in the history
  • Loading branch information
rylorin committed Oct 26, 2023
1 parent 47f74cb commit c369995
Show file tree
Hide file tree
Showing 9 changed files with 174 additions and 28 deletions.
4 changes: 2 additions & 2 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/
/* eslint @typescript-eslint/no-unsafe-declaration-merging:warn */
import { EventEmitter } from "eventemitter3";
import { DurationUnit, WhatToShow } from "..";
import { DurationUnit, MarketDataType, WhatToShow } from "..";

import { ErrorCode } from "../common/errorCode";
import { Controller } from "../core/io/controller";
Expand Down Expand Up @@ -1096,7 +1096,7 @@ export class IBApi extends EventEmitter {
* - 3 (delayed) enables delayed and disables delayed-frozen market data.
* - 4 (delayed-frozen) enables delayed and delayed-frozen market data.
*/
reqMarketDataType(marketDataType: number): IBApi {
reqMarketDataType(marketDataType: MarketDataType): IBApi {
this.controller.schedule(() =>
this.controller.encoder.reqMarketDataType(marketDataType),
);
Expand Down
8 changes: 5 additions & 3 deletions src/api/contract/future.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ export class Future implements Contract {
public lastTradeDateOrContractMonth: string,
public exchange: string,
public multiplier: number,
public currency: string,
) {}
public currency?: string,
) {
this.currency = this.currency ?? "USD";
}

public secType = SecType.FUT;
}

export default Future;
export default Future;
4 changes: 1 addition & 3 deletions src/api/contract/ind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,11 @@ import { Contract } from "./contract";
export class Index implements Contract {
constructor(
public symbol: string,
public expiry: string,
public currency?: string,
public exchange?: string,
public multiplier?: number
) {
this.currency = this.currency ?? "USD";
this.exchange = this.exchange ?? "CBOE";
this.exchange = this.exchange ?? "CME";
}

public secType = SecType.IND;
Expand Down
3 changes: 3 additions & 0 deletions src/common/errorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export enum ErrorCode {
/** Already connected. */
ALREADY_CONNECTED = 501,

/** Requested market data is not subscribed. Delayed market data is not available. */
REQ_MKT_DATA_NOT_AVAIL = 354,

/**
* Couldn't connect to TWS.
*
Expand Down
4 changes: 2 additions & 2 deletions src/core/io/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import configuration from "../../common/configuration";
import { ErrorCode } from "../../common/errorCode";
import { Decoder, DecoderCallbacks } from "./decoder";
import { Encoder, EncoderCallbacks } from "./encoder";
import { Socket } from "./socket";
import { ConnectionStatus, Socket } from "./socket";

/**
* @internal
Expand Down Expand Up @@ -268,7 +268,7 @@ export class Controller implements EncoderCallbacks, DecoderCallbacks {
* @see [[disconnect]]
*/
private executeDisconnect(): void {
if (this.socket.connected) {
if (this.socket.status >= ConnectionStatus.Connecting) {
this.socket.disconnect();
} else {
this.emitInfo(
Expand Down
4 changes: 2 additions & 2 deletions src/core/io/encoder.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { WhatToShow } from "../..";
import { MarketDataType, WhatToShow } from "../..";
import { ScanCode } from "../../api-next/market-scanner/market-scanner";
import { Contract } from "../../api/contract/contract";
import WshEventData from "../../api/contract/wsh";
Expand Down Expand Up @@ -2362,7 +2362,7 @@ function tagValuesToTokens(tagValues: TagValue[]): unknown[] {
/**
* Encode a REQ_MARKET_DATA_TYPE message.
*/
reqMarketDataType(marketDataType: number): void {
reqMarketDataType(marketDataType: MarketDataType): void {
if (this.serverVersion < MIN_SERVER_VER.REQ_MARKET_DATA_TYPE) {
return this.emitError(
"It does not support marketDataType requests.",
Expand Down
32 changes: 26 additions & 6 deletions src/core/io/socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ const EOL = "\0";
*/
// const CONNECT_DELAY = 600;

export const ConnectionStatus = {
Disconnected: 0,
Disconnecting: 1,
Connecting: 2,
Connected: 3,
} as const;
export type ConnectionStatus =
(typeof ConnectionStatus)[keyof typeof ConnectionStatus];

/**
* @internal
*
Expand Down Expand Up @@ -62,8 +71,8 @@ export class Socket {
/** The TCP client socket. */
private client?: net.Socket;

/** `true` if the TCP socket is connected and [[OUT_MSG_ID.START_API]] has been sent, `false` otherwise. */
private _connected = false;
/** `connected` if the TCP socket is connected and [[OUT_MSG_ID.START_API]] has been sent. */
private _status: ConnectionStatus = ConnectionStatus.Disconnected;

/** The IB API Server version, or 0 if not connected yet. */
private _serverVersion = 0;
Expand Down Expand Up @@ -91,7 +100,12 @@ export class Socket {

/** Returns `true` if connected to TWS/IB Gateway, `false` otherwise. */
get connected(): boolean {
return this._connected;
return this._status === ConnectionStatus.Connected;
}

/** Returns connection status */
get status(): ConnectionStatus {
return this._status;
}

/** Returns the IB API Server version. */
Expand Down Expand Up @@ -124,6 +138,10 @@ export class Socket {
* default client id (0) will used.
*/
connect(clientId?: number): void {
// Reject any connect attempt is not disconnected
if (this._status >= ConnectionStatus.Connecting) return;
this._status = ConnectionStatus.Connecting;

// update client id

if (clientId !== undefined) {
Expand Down Expand Up @@ -161,6 +179,8 @@ export class Socket {
* Disconnect from API server.
*/
disconnect(): void {
this._status = ConnectionStatus.Disconnecting;

// pause controller while connection is down.

this.controller.pause();
Expand Down Expand Up @@ -324,7 +344,7 @@ export class Socket {
* Called when first data has arrived on the connection.
*/
private onServerVersion(tokens: string[]): void {
this._connected = true;
this._status = ConnectionStatus.Connected;

this._serverVersion = parseInt(tokens[0], 10);
this._serverConnectionTime = tokens[1];
Expand Down Expand Up @@ -415,8 +435,8 @@ export class Socket {
* Called when TCP socket connection has been closed.
*/
private onEnd(): void {
if (this._connected) {
this._connected = false;
if (this._status) {
this._status = ConnectionStatus.Disconnected;
this.controller.emitEvent(EventName.disconnected);
}

Expand Down
13 changes: 8 additions & 5 deletions src/tests/unit/api/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,16 +69,19 @@ describe("IBApi Tests", () => {

ib.on(EventName.pnl, (reqId: number, pnl: number) => {

Check warning on line 70 in src/tests/unit/api/api.test.ts

View workflow job for this annotation

GitHub Actions / job

'pnl' is defined but never used. Allowed unused args must match /^_/u
expect(reqId).toEqual(refId);
expect(pnl).toBeTruthy();
// expect(pnl).toBeTruthy();
if (!received) {
ib.cancelPnL(reqId);
ib.disconnect();
done();
}
received = true;
}).on(EventName.error, (err, code, reqId) => {
if (reqId == refId) done(`[${reqId}] ${err.message} (#${code})`);
});
})
.on(EventName.disconnected, () => {
done();
})
.on(EventName.error, (err, code, reqId) => {
if (reqId == refId) done(`[${reqId}] ${err.message} (#${code})`);
});

ib.connect().reqPnL(refId, _account);
});
Expand Down
130 changes: 125 additions & 5 deletions src/tests/unit/api/market-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@
*/
import {
Contract,
ErrorCode,
EventName,
Future,
IBApi,
Index,
MarketDataType,
Option,
OptionType,
Stock,
Expand All @@ -13,7 +17,7 @@ import {
import configuration from "../../../common/configuration";

describe("IBApi Market data Tests", () => {
jest.setTimeout(20000);
jest.setTimeout(15 * 1000);

let ib: IBApi;
const clientId = Math.floor(Math.random() * 32766) + 1; // ensure unique client
Expand All @@ -35,7 +39,39 @@ describe("IBApi Market data Tests", () => {
// logger.info("IBApi disconnected");
});

const IsError = (code: ErrorCode) =>
code !== ErrorCode.REQ_MKT_DATA_NOT_AVAIL &&
code !== ErrorCode.DISPLAYING_DELAYED_DATA;

it("Stock market data", (done) => {
const refId = 45;
let received = false;

ib.once(EventName.connected, () => {
const contract: Contract = new Stock("AAPL");
ib.reqMktData(refId, contract, "", true, false);
})
.on(
EventName.tickPrice,
(reqId: number, _field: TickType, _value: number) => {
expect(reqId).toEqual(refId);
if (reqId == refId) received = true;
// console.log(_field, _value);
},
)
.on(EventName.tickSnapshotEnd, (reqId: number) => {
expect(reqId).toEqual(refId);
if (received) done();
else done("Didn't get any result");
})
.on(EventName.error, (err, code, reqId) => {
if (IsError(code)) done(`[${reqId}] ${err.message} (#${code})`);
});

ib.connect().reqMarketDataType(MarketDataType.DELAYED_FROZEN);
});

it("SPY market data", (done) => {
const refId = 46;
let received = false;

Expand All @@ -57,10 +93,10 @@ describe("IBApi Market data Tests", () => {
else done("Didn't get any result");
})
.on(EventName.error, (err, code, reqId) => {
if (reqId == refId) done(`[${reqId}] ${err.message} (#${code})`);
if (IsError(code)) done(`[${reqId}] ${err.message} (#${code})`);
});

ib.connect();
ib.connect().reqMarketDataType(MarketDataType.DELAYED_FROZEN);
});

test("Option market data", (done) => {
Expand Down Expand Up @@ -89,9 +125,93 @@ describe("IBApi Market data Tests", () => {
else done("Didn't get any result");
})
.on(EventName.error, (err, code, reqId) => {
if (reqId == refId) done(`[${reqId}] ${err.message} (#${code})`);
if (IsError(code)) done(`[${reqId}] ${err.message} (#${code})`);
});

ib.connect().reqMarketDataType(MarketDataType.DELAYED_FROZEN);
});

it("Future market data", (done) => {
const refId = 48;
let received = false;

ib.once(EventName.connected, () => {
const contract: Contract = new Future("ES", "ESZ3", "202312", "CME", 50);
ib.reqMktData(refId, contract, "", true, false);
})
.on(
EventName.tickPrice,
(reqId: number, _field: TickType, _value: number) => {
expect(reqId).toEqual(refId);
if (reqId == refId) received = true;
// console.log(_field, _value);
},
)
.on(EventName.tickSnapshotEnd, (reqId: number) => {
expect(reqId).toEqual(refId);
if (received) done();
else done("Didn't get any result");
})
.on(EventName.error, (err, code, reqId) => {
if (IsError(code)) done(`[${reqId}] ${err.message} (#${code})`);
});

ib.connect().reqMarketDataType(MarketDataType.DELAYED_FROZEN);
});

it("DAX market data", (done) => {
const refId = 49;
let received = false;

ib.once(EventName.connected, () => {
const contract: Contract = new Index("DAX", "EUR", "EUREX");
ib.reqMktData(refId, contract, "", true, false);
})
.on(
EventName.tickPrice,
(reqId: number, _field: TickType, _value: number) => {
expect(reqId).toEqual(refId);
if (reqId == refId) received = true;
// console.log(_field, _value);
},
)
.on(EventName.tickSnapshotEnd, (reqId: number) => {
expect(reqId).toEqual(refId);
if (received) done();
else done("Didn't get any result");
})
.on(EventName.error, (err, code: ErrorCode, reqId) => {
if (IsError(code)) done(`[${reqId}] ${err.message} (#${code})`);
});

ib.connect().reqMarketDataType(MarketDataType.DELAYED_FROZEN);
});

it("Index market data", (done) => {
const refId = 50;
let received = false;

ib.once(EventName.connected, () => {
const contract: Contract = new Index("ES");
ib.reqMktData(refId, contract, "", true, false);
})
.on(
EventName.tickPrice,
(reqId: number, _field: TickType, _value: number) => {
expect(reqId).toEqual(refId);
if (reqId == refId) received = true;
// console.log(_field, _value);
},
)
.on(EventName.tickSnapshotEnd, (reqId: number) => {
expect(reqId).toEqual(refId);
if (received) done();
else done("Didn't get any result");
})
.on(EventName.error, (err, code, reqId) => {
if (IsError(code)) done(`[${reqId}] ${err.message} (#${code})`);
});

ib.connect();
ib.connect().reqMarketDataType(MarketDataType.DELAYED_FROZEN);
});
});

0 comments on commit c369995

Please sign in to comment.